refactor: redesign workspace status on workspaces table (#17425)

Closes https://github.com/coder/coder/issues/17310

**Before:**
<img width="1624" alt="Screenshot 2025-04-16 at 11 49 52"
src="https://github.com/user-attachments/assets/4fb6c8e5-329f-476f-99bb-192c0f9562a2"
/>

**After:**
<img width="1624" alt="Screenshot 2025-04-16 at 11 49 19"
src="https://github.com/user-attachments/assets/c7025fee-fefd-4064-9101-d7a1b364dd80"
/>

**Notice!**
- I've create a new size variation for the badge, `xs`. Since we reduced
the line-height for the `text-xs` to be 16px instead of 18px, having a
smaller badge, reducing the vertical size and horizontal paddings, just
worked better.
- I have to update Figma to reflect these changes. I tried, but I was
not able to get it working and updated correctly. I'm going to take a
pause during this week to learn that.
- Updated the destructive, and warning badges to use borders as defined
in the designs
[here](https://www.figma.com/design/WfqIgsTFXN2BscBSSyXWF8/Coder-kit?node-id=489-3472&t=gfnYeLOIFUqHx6qv-0).
This commit is contained in:
Bruno Quaresma
2025-04-17 10:57:02 -03:00
committed by GitHub
parent aa02c9ffb8
commit c8edadae10
6 changed files with 119 additions and 41 deletions
+4 -1
View File
@@ -17,9 +17,12 @@ export const badgeVariants = cva(
default:
"border-transparent bg-surface-secondary text-content-secondary shadow",
warning:
"border-transparent bg-surface-orange text-content-warning shadow",
"border border-solid border-border-warning bg-surface-orange text-content-warning shadow",
destructive:
"border border-solid border-border-destructive bg-surface-red text-content-highlight-red shadow",
},
size: {
xs: "text-2xs font-regular h-5 [&_svg]:hidden rounded px-1.5",
sm: "text-2xs font-regular h-5.5 [&_svg]:size-icon-xs",
md: "text-xs font-medium [&_svg]:size-icon-sm",
},
+5 -1
View File
@@ -28,11 +28,13 @@
--surface-grey: 240 5% 96%;
--surface-orange: 34 100% 92%;
--surface-sky: 201 94% 86%;
--surface-red: 0 93% 94%;
--border-default: 240 6% 90%;
--border-success: 142 76% 36%;
--border-warning: 30.66, 97.16%, 72.35%;
--border-destructive: 0 84% 60%;
--border-hover: 240, 5%, 34%;
--border-warning: 27 96% 61%;
--border-hover: 240 5% 34%;
--overlay-default: 240 5% 84% / 80%;
--radius: 0.5rem;
--highlight-purple: 262 83% 58%;
@@ -66,10 +68,12 @@
--surface-grey: 240 6% 10%;
--surface-orange: 13 81% 15%;
--surface-sky: 204 80% 16%;
--surface-red: 0 75% 15%;
--border-default: 240 4% 16%;
--border-success: 142 76% 36%;
--border-warning: 30.66, 97.16%, 72.35%;
--border-destructive: 0 91% 71%;
--border-warning: 31 97% 72%;
--border-hover: 240, 5%, 34%;
--overlay-default: 240 10% 4% / 80%;
--highlight-purple: 252 95% 85%;
@@ -1,8 +1,6 @@
import AutoDeleteIcon from "@mui/icons-material/AutoDelete";
import RecyclingIcon from "@mui/icons-material/Recycling";
import Tooltip from "@mui/material/Tooltip";
import type { Workspace } from "api/typesGenerated";
import { Pill } from "components/Pill/Pill";
import { Badge } from "components/Badge/Badge";
import { formatDistanceToNow } from "date-fns";
import type { FC } from "react";
@@ -35,9 +33,9 @@ export const WorkspaceDormantBadge: FC<WorkspaceDormantBadgeProps> = ({
</>
}
>
<Pill role="status" icon={<AutoDeleteIcon />} type="error">
<Badge role="status" variant="destructive" size="xs">
Deletion Pending
</Pill>
</Badge>
</Tooltip>
) : (
<Tooltip
@@ -50,9 +48,9 @@ export const WorkspaceDormantBadge: FC<WorkspaceDormantBadgeProps> = ({
</>
}
>
<Pill role="status" icon={<RecyclingIcon />} type="warning">
<Badge role="status" variant="warning" size="xs">
Dormant
</Pill>
</Badge>
</Tooltip>
);
};
@@ -13,6 +13,11 @@ import { AvatarData } from "components/Avatar/AvatarData";
import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton";
import { InfoTooltip } from "components/InfoTooltip/InfoTooltip";
import { Stack } from "components/Stack/Stack";
import {
StatusIndicator,
StatusIndicatorDot,
type StatusIndicatorProps,
} from "components/StatusIndicator/StatusIndicator";
import {
Table,
TableBody,
@@ -25,19 +30,26 @@ import {
TableLoaderSkeleton,
TableRowSkeleton,
} from "components/TableLoader/TableLoader";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { useClickableTableRow } from "hooks/useClickableTableRow";
import { useDashboard } from "modules/dashboard/useDashboard";
import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus";
import { WorkspaceDormantBadge } from "modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge";
import { WorkspaceOutdatedTooltip } from "modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip";
import { WorkspaceStatusBadge } from "modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge";
import { LastUsed } from "pages/WorkspacesPage/LastUsed";
import { type FC, type ReactNode, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { cn } from "utils/cn";
import { getDisplayWorkspaceTemplateName } from "utils/workspace";
import {
type DisplayWorkspaceStatusType,
getDisplayWorkspaceStatus,
getDisplayWorkspaceTemplateName,
lastUsedMessage,
} from "utils/workspace";
import { WorkspacesEmpty } from "./WorkspacesEmpty";
dayjs.extend(relativeTime);
export interface WorkspacesTableProps {
workspaces?: readonly Workspace[];
checkedWorkspaces: readonly Workspace[];
@@ -125,8 +137,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
</TableHead>
{hasAppStatus && <TableHead className="w-2/6">Activity</TableHead>}
<TableHead className="w-2/6">Template</TableHead>
<TableHead className="w-1/6">Last used</TableHead>
<TableHead className="w-1/6">Status</TableHead>
<TableHead className="w-2/6">Status</TableHead>
<TableHead className="w-0" />
</TableRow>
</TableHeader>
@@ -248,26 +259,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
/>
</TableCell>
<TableCell>
<LastUsed lastUsedAt={workspace.last_used_at} />
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<WorkspaceStatusBadge workspace={workspace} />
{workspace.latest_build.status === "running" &&
!workspace.health.healthy && (
<InfoTooltip
type="warning"
title="Workspace is unhealthy"
message="Your workspace is running but some agents are unhealthy."
/>
)}
{workspace.dormant_at && (
<WorkspaceDormantBadge workspace={workspace} />
)}
</div>
</TableCell>
<WorkspaceStatusCell workspace={workspace} />
<TableCell>
<div className="flex pl-4">
@@ -345,14 +337,11 @@ const TableLoader: FC<TableLoaderProps> = ({ canCheckWorkspaces }) => {
<TableCell className="w-2/6">
<AvatarDataSkeleton />
</TableCell>
<TableCell className="w-1/6">
<Skeleton variant="text" width="75%" />
</TableCell>
<TableCell className="w-1/6">
<Skeleton variant="text" width="75%" />
<TableCell className="w-2/6">
<Skeleton variant="text" width="50%" />
</TableCell>
<TableCell className="w-0">
<Skeleton variant="text" width="75%" />
<Skeleton variant="text" width="25%" />
</TableCell>
</TableRowSkeleton>
</TableLoaderSkeleton>
@@ -362,3 +351,51 @@ const TableLoader: FC<TableLoaderProps> = ({ canCheckWorkspaces }) => {
const cantBeChecked = (workspace: Workspace) => {
return ["deleting", "pending"].includes(workspace.latest_build.status);
};
type WorkspaceStatusCellProps = {
workspace: Workspace;
};
const variantByStatusType: Record<
DisplayWorkspaceStatusType,
StatusIndicatorProps["variant"]
> = {
active: "pending",
inactive: "inactive",
success: "success",
error: "failed",
danger: "warning",
warning: "warning",
};
const WorkspaceStatusCell: FC<WorkspaceStatusCellProps> = ({ workspace }) => {
const { text, type } = getDisplayWorkspaceStatus(
workspace.latest_build.status,
workspace.latest_build.job,
);
return (
<TableCell>
<div className="flex flex-col">
<StatusIndicator variant={variantByStatusType[type]}>
<StatusIndicatorDot />
{text}
{workspace.latest_build.status === "running" &&
!workspace.health.healthy && (
<InfoTooltip
type="warning"
title="Workspace is unhealthy"
message="Your workspace is running but some agents are unhealthy."
/>
)}
{workspace.dormant_at && (
<WorkspaceDormantBadge workspace={workspace} />
)}
</StatusIndicator>
<span className="text-xs font-medium text-content-secondary ml-6">
{lastUsedMessage(workspace.last_used_at)}
</span>
</div>
</TableCell>
);
};
+36 -1
View File
@@ -168,14 +168,29 @@ export const getDisplayWorkspaceTemplateName = (
: workspace.template_name;
};
export type DisplayWorkspaceStatusType =
| "success"
| "active"
| "inactive"
| "error"
| "warning"
| "danger";
type DisplayWorkspaceStatus = {
text: string;
type: DisplayWorkspaceStatusType;
icon: React.ReactNode;
};
export const getDisplayWorkspaceStatus = (
workspaceStatus: TypesGen.WorkspaceStatus,
provisionerJob?: TypesGen.ProvisionerJob,
) => {
): DisplayWorkspaceStatus => {
switch (workspaceStatus) {
case undefined:
return {
text: "Loading",
type: "active",
icon: <PillSpinner />,
} as const;
case "running":
@@ -307,3 +322,23 @@ const FALLBACK_ICON = "/icon/widgets.svg";
export const getResourceIconPath = (resourceType: string): string => {
return BUILT_IN_ICON_PATHS[resourceType] ?? FALLBACK_ICON;
};
export const lastUsedMessage = (lastUsedAt: string | Date): string => {
const t = dayjs(lastUsedAt);
const now = dayjs();
let message = t.fromNow();
if (t.isAfter(now.subtract(1, "hour"))) {
message = "Now";
} else if (t.isAfter(now.subtract(3, "day"))) {
message = t.fromNow();
} else if (t.isAfter(now.subtract(1, "month"))) {
message = t.fromNow();
} else if (t.isAfter(now.subtract(100, "year"))) {
message = t.fromNow();
} else {
message = "Never";
}
return message;
};
+1
View File
@@ -49,6 +49,7 @@ module.exports = {
grey: "hsl(var(--surface-grey))",
orange: "hsl(var(--surface-orange))",
sky: "hsl(var(--surface-sky))",
red: "hsl(var(--surface-red))",
},
border: {
DEFAULT: "hsl(var(--border-default))",