refactor: replace badge by status indicator (#17811)

**Why?**
In the workspaces page, it is using the status indicator, and not the
badge anymore, so to keep the UI consistent, I'm replacing the badge by
the indicator in the workspace page too.

**Before:**
<img width="672" alt="Screenshot 2025-05-13 at 19 14 17"
src="https://github.com/user-attachments/assets/0e8ea4bd-68d1-4d27-b81b-f79f15cabb2c"
/>

**After:**
<img width="672" alt="Screenshot 2025-05-13 at 19 14 21"
src="https://github.com/user-attachments/assets/45719262-011e-4fc8-9ebe-fe9e33d9d572"
/>
This commit is contained in:
Bruno Quaresma
2025-05-14 09:11:25 -03:00
committed by GitHub
parent 80e1be0db1
commit fcbdd1a28e
7 changed files with 151 additions and 237 deletions
+11 -13
View File
@@ -152,7 +152,7 @@ export const createWorkspace = async (
const user = currentUser(page);
await expectUrl(page).toHavePathName(`/@${user.username}/${name}`);
await page.waitForSelector("[data-testid='build-status'] >> text=Running", {
await page.waitForSelector("text=Workspace status: Running", {
state: "visible",
});
return name;
@@ -364,7 +364,7 @@ export const stopWorkspace = async (page: Page, workspaceName: string) => {
await page.getByTestId("workspace-stop-button").click();
await page.waitForSelector("*[data-testid='build-status'] >> text=Stopped", {
await page.waitForSelector("text=Workspace status: Stopped", {
state: "visible",
});
};
@@ -389,7 +389,7 @@ export const buildWorkspaceWithParameters = async (
await page.getByTestId("confirm-button").click();
}
await page.waitForSelector("*[data-testid='build-status'] >> text=Running", {
await page.waitForSelector("text=Workspace status: Running", {
state: "visible",
});
};
@@ -412,11 +412,12 @@ export const startAgent = async (
export const downloadCoderVersion = async (
version: string,
): Promise<string> => {
if (version.startsWith("v")) {
version = version.slice(1);
let versionNumber = version;
if (versionNumber.startsWith("v")) {
versionNumber = versionNumber.slice(1);
}
const binaryName = `coder-e2e-${version}`;
const binaryName = `coder-e2e-${versionNumber}`;
const tempDir = "/tmp/coder-e2e-cache";
// The install script adds `./bin` automatically to the path :shrug:
const binaryPath = path.join(tempDir, "bin", binaryName);
@@ -438,7 +439,7 @@ export const downloadCoderVersion = async (
path.join(__dirname, "../../install.sh"),
[
"--version",
version,
versionNumber,
"--method",
"standalone",
"--prefix",
@@ -551,11 +552,8 @@ const emptyPlan = new TextEncoder().encode("{}");
* converts it into an uploadable tar file.
*/
const createTemplateVersionTar = async (
responses?: EchoProvisionerResponses,
responses: EchoProvisionerResponses = {},
): Promise<Buffer> => {
if (!responses) {
responses = {};
}
if (!responses.parse) {
responses.parse = [
{
@@ -1012,7 +1010,7 @@ export const updateWorkspace = async (
await fillParameters(page, richParameters, buildParameters);
await page.getByRole("button", { name: /update parameters/i }).click();
await page.waitForSelector("*[data-testid='build-status'] >> text=Running", {
await page.waitForSelector("text=Workspace status: Running", {
state: "visible",
});
};
@@ -1031,7 +1029,7 @@ export const updateWorkspaceParameters = async (
await fillParameters(page, richParameters, buildParameters);
await page.getByRole("button", { name: /submit and restart/i }).click();
await page.waitForSelector("*[data-testid='build-status'] >> text=Running", {
await page.waitForSelector("text=Workspace status: Running", {
state: "visible",
});
};
@@ -1,93 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
MockBuildInfo,
MockCanceledWorkspace,
MockCancelingWorkspace,
MockDeletedWorkspace,
MockDeletingWorkspace,
MockFailedWorkspace,
MockPendingWorkspace,
MockStartingWorkspace,
MockStoppedWorkspace,
MockStoppingWorkspace,
MockWorkspace,
} from "testHelpers/entities";
import { withDashboardProvider } from "testHelpers/storybook";
import { WorkspaceStatusBadge } from "./WorkspaceStatusBadge";
const meta: Meta<typeof WorkspaceStatusBadge> = {
title: "modules/workspaces/WorkspaceStatusBadge",
component: WorkspaceStatusBadge,
parameters: {
queries: [
{
key: ["buildInfo"],
data: MockBuildInfo,
},
],
},
decorators: [withDashboardProvider],
};
export default meta;
type Story = StoryObj<typeof WorkspaceStatusBadge>;
export const Running: Story = {
args: {
workspace: MockWorkspace,
},
};
export const Starting: Story = {
args: {
workspace: MockStartingWorkspace,
},
};
export const Stopped: Story = {
args: {
workspace: MockStoppedWorkspace,
},
};
export const Stopping: Story = {
args: {
workspace: MockStoppingWorkspace,
},
};
export const Deleting: Story = {
args: {
workspace: MockDeletingWorkspace,
},
};
export const Deleted: Story = {
args: {
workspace: MockDeletedWorkspace,
},
};
export const Canceling: Story = {
args: {
workspace: MockCancelingWorkspace,
},
};
export const Canceled: Story = {
args: {
workspace: MockCanceledWorkspace,
},
};
export const Failed: Story = {
args: {
workspace: MockFailedWorkspace,
},
};
export const Pending: Story = {
args: {
workspace: MockPendingWorkspace,
},
};
@@ -1,90 +0,0 @@
import Tooltip, {
type TooltipProps,
tooltipClasses,
} from "@mui/material/Tooltip";
import type { Workspace } from "api/typesGenerated";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { Pill } from "components/Pill/Pill";
import { useClassName } from "hooks/useClassName";
import { CircleAlertIcon } from "lucide-react";
import type { FC, ReactNode } from "react";
import { getDisplayWorkspaceStatus } from "utils/workspace";
export type WorkspaceStatusBadgeProps = {
workspace: Workspace;
children?: ReactNode;
className?: string;
};
export const WorkspaceStatusBadge: FC<WorkspaceStatusBadgeProps> = ({
workspace,
className,
}) => {
const { text, icon, type } = getDisplayWorkspaceStatus(
workspace.latest_build.status,
workspace.latest_build.job,
);
return (
<ChooseOne>
<Cond condition={workspace.latest_build.status === "failed"}>
<FailureTooltip
title={
<div css={{ display: "flex", alignItems: "center", gap: 10 }}>
<CircleAlertIcon
aria-hidden="true"
className="size-icon-xs"
css={(theme) => ({
color: theme.palette.error.light,
})}
/>
<div>{workspace.latest_build.job.error}</div>
</div>
}
placement="top"
>
<Pill
role="status"
data-testid="build-status"
className={className}
icon={icon}
type={type}
>
{text}
</Pill>
</FailureTooltip>
</Cond>
<Cond>
<Pill
role="status"
data-testid="build-status"
className={className}
icon={icon}
type={type}
>
{text}
</Pill>
</Cond>
</ChooseOne>
);
};
const FailureTooltip: FC<TooltipProps> = ({ children, ...tooltipProps }) => {
const popper = useClassName(
(css, theme) => css`
& .${tooltipClasses.tooltip} {
background-color: ${theme.palette.background.paper};
border: 1px solid ${theme.palette.divider};
font-size: 12px;
padding: 8px 10px;
}
`,
[],
);
return (
<Tooltip {...tooltipProps} classes={{ popper }}>
{children}
</Tooltip>
);
};
@@ -0,0 +1,82 @@
import type { Meta, StoryObj } from "@storybook/react";
import type { Workspace, WorkspaceStatus } from "api/typesGenerated";
import { MockWorkspace } from "testHelpers/entities";
import { WorkspaceStatusIndicator } from "./WorkspaceStatusIndicator";
const meta: Meta<typeof WorkspaceStatusIndicator> = {
title: "modules/workspaces/WorkspaceStatusIndicator",
component: WorkspaceStatusIndicator,
};
export default meta;
type Story = StoryObj<typeof WorkspaceStatusIndicator>;
const createWorkspaceWithStatus = (status: WorkspaceStatus): Workspace => {
return {
...MockWorkspace,
latest_build: {
...MockWorkspace.latest_build,
status,
},
} as Workspace;
};
export const Running: Story = {
args: {
workspace: createWorkspaceWithStatus("running"),
},
};
export const Stopped: Story = {
args: {
workspace: createWorkspaceWithStatus("stopped"),
},
};
export const Starting: Story = {
args: {
workspace: createWorkspaceWithStatus("starting"),
},
};
export const Stopping: Story = {
args: {
workspace: createWorkspaceWithStatus("stopping"),
},
};
export const Failed: Story = {
args: {
workspace: createWorkspaceWithStatus("failed"),
},
};
export const Canceling: Story = {
args: {
workspace: createWorkspaceWithStatus("canceling"),
},
};
export const Canceled: Story = {
args: {
workspace: createWorkspaceWithStatus("canceled"),
},
};
export const Deleting: Story = {
args: {
workspace: createWorkspaceWithStatus("deleting"),
},
};
export const Deleted: Story = {
args: {
workspace: createWorkspaceWithStatus("deleted"),
},
};
export const Pending: Story = {
args: {
workspace: createWorkspaceWithStatus("pending"),
},
};
@@ -0,0 +1,49 @@
import type { Workspace } from "api/typesGenerated";
import {
StatusIndicator,
StatusIndicatorDot,
type StatusIndicatorProps,
} from "components/StatusIndicator/StatusIndicator";
import type { FC } from "react";
import type React from "react";
import {
type DisplayWorkspaceStatusType,
getDisplayWorkspaceStatus,
} from "utils/workspace";
const variantByStatusType: Record<
DisplayWorkspaceStatusType,
StatusIndicatorProps["variant"]
> = {
active: "pending",
inactive: "inactive",
success: "success",
error: "failed",
danger: "warning",
warning: "warning",
};
type WorkspaceStatusIndicatorProps = {
workspace: Workspace;
children?: React.ReactNode;
};
export const WorkspaceStatusIndicator: FC<WorkspaceStatusIndicatorProps> = ({
workspace,
children,
}) => {
const { text, type } = getDisplayWorkspaceStatus(
workspace.latest_build.status,
workspace.latest_build.job,
);
return (
<StatusIndicator variant={variantByStatusType[type]}>
<StatusIndicatorDot />
<span>
<span className="sr-only">Workspace status:</span> {text}
</span>
{children}
</StatusIndicator>
);
};
@@ -20,7 +20,7 @@ import { Popover, PopoverTrigger } from "components/deprecated/Popover/Popover";
import { TrashIcon } from "lucide-react";
import { useDashboard } from "modules/dashboard/useDashboard";
import { linkToTemplate, useLinks } from "modules/navigation";
import { WorkspaceStatusBadge } from "modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge";
import { WorkspaceStatusIndicator } from "modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator";
import type { FC } from "react";
import { useQuery } from "react-query";
import { Link as RouterLink } from "react-router-dom";
@@ -201,18 +201,13 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
</div>
{!isImmutable && (
<div
css={{
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<div className="flex items-center gap-4">
<WorkspaceScheduleControls
workspace={workspace}
template={template}
canUpdateSchedule={permissions.updateWorkspace}
/>
<WorkspaceNotifications
workspace={workspace}
template={template}
@@ -222,7 +217,9 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
onUpdateWorkspace={handleUpdate}
onActivateWorkspace={handleDormantActivate}
/>
<WorkspaceStatusBadge workspace={workspace} />
<WorkspaceStatusIndicator workspace={workspace} />
<WorkspaceActions
workspace={workspace}
permissions={permissions}
@@ -25,11 +25,6 @@ import { VSCodeInsidersIcon } from "components/Icons/VSCodeInsidersIcon";
import { InfoTooltip } from "components/InfoTooltip/InfoTooltip";
import { Spinner } from "components/Spinner/Spinner";
import { Stack } from "components/Stack/Stack";
import {
StatusIndicator,
StatusIndicatorDot,
type StatusIndicatorProps,
} from "components/StatusIndicator/StatusIndicator";
import {
Table,
TableBody,
@@ -48,8 +43,6 @@ import {
TooltipProvider,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { useAuthenticated } from "hooks";
import { useClickableTableRow } from "hooks/useClickableTableRow";
import { EllipsisVertical } from "lucide-react";
@@ -70,6 +63,7 @@ import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/Worksp
import { WorkspaceDormantBadge } from "modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge";
import { WorkspaceMoreActions } from "modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions";
import { WorkspaceOutdatedTooltip } from "modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip";
import { WorkspaceStatusIndicator } from "modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator";
import {
WorkspaceUpdateDialogs,
useWorkspaceUpdate,
@@ -86,15 +80,11 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
import { useNavigate } from "react-router-dom";
import { cn } from "utils/cn";
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[];
@@ -398,30 +388,11 @@ 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}
<WorkspaceStatusIndicator workspace={workspace}>
{workspace.latest_build.status === "running" &&
!workspace.health.healthy && (
<InfoTooltip
@@ -433,7 +404,7 @@ const WorkspaceStatusCell: FC<WorkspaceStatusCellProps> = ({ workspace }) => {
{workspace.dormant_at && (
<WorkspaceDormantBadge workspace={workspace} />
)}
</StatusIndicator>
</WorkspaceStatusIndicator>
<span className="text-xs font-medium text-content-secondary ml-6">
{lastUsedMessage(workspace.last_used_at)}
</span>