From fcbdd1a28e0097142ba005a352d57ae39390ba93 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 14 May 2025 09:11:25 -0300 Subject: [PATCH] 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:** Screenshot 2025-05-13 at 19 14 17 **After:** Screenshot 2025-05-13 at 19 14 21 --- site/e2e/helpers.ts | 24 +++-- .../WorkspaceStatusBadge.stories.tsx | 93 ------------------- .../WorkspaceStatusBadge.tsx | 90 ------------------ .../WorkspaceStatusIndicator.stories.tsx | 82 ++++++++++++++++ .../WorkspaceStatusIndicator.tsx | 49 ++++++++++ .../pages/WorkspacePage/WorkspaceTopbar.tsx | 15 ++- .../pages/WorkspacesPage/WorkspacesTable.tsx | 35 +------ 7 files changed, 151 insertions(+), 237 deletions(-) delete mode 100644 site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx delete mode 100644 site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx create mode 100644 site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.stories.tsx create mode 100644 site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.tsx diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index ffadc3fa34..d56dc51f66 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -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 => { - 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 => { - 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", }); }; diff --git a/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx b/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx deleted file mode 100644 index 352153cda6..0000000000 --- a/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx +++ /dev/null @@ -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 = { - title: "modules/workspaces/WorkspaceStatusBadge", - component: WorkspaceStatusBadge, - parameters: { - queries: [ - { - key: ["buildInfo"], - data: MockBuildInfo, - }, - ], - }, - decorators: [withDashboardProvider], -}; - -export default meta; -type Story = StoryObj; - -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, - }, -}; diff --git a/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx deleted file mode 100644 index 1bde6f9181..0000000000 --- a/site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx +++ /dev/null @@ -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 = ({ - workspace, - className, -}) => { - const { text, icon, type } = getDisplayWorkspaceStatus( - workspace.latest_build.status, - workspace.latest_build.job, - ); - - return ( - - - - - - - - {text} - - - - ); -}; - -const FailureTooltip: FC = ({ 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 ( - - {children} - - ); -}; diff --git a/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.stories.tsx b/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.stories.tsx new file mode 100644 index 0000000000..75205db8ff --- /dev/null +++ b/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.stories.tsx @@ -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 = { + title: "modules/workspaces/WorkspaceStatusIndicator", + component: WorkspaceStatusIndicator, +}; + +export default meta; +type Story = StoryObj; + +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"), + }, +}; diff --git a/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.tsx b/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.tsx new file mode 100644 index 0000000000..bd928844cd --- /dev/null +++ b/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.tsx @@ -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 = ({ + workspace, + children, +}) => { + const { text, type } = getDisplayWorkspaceStatus( + workspace.latest_build.status, + workspace.latest_build.job, + ); + + return ( + + + + Workspace status: {text} + + {children} + + ); +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 32908156c5..8f75e61589 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -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 = ({ {!isImmutable && ( -
+
+ = ({ onUpdateWorkspace={handleUpdate} onActivateWorkspace={handleDormantActivate} /> - + + + = { - active: "pending", - inactive: "inactive", - success: "success", - error: "failed", - danger: "warning", - warning: "warning", -}; - const WorkspaceStatusCell: FC = ({ workspace }) => { - const { text, type } = getDisplayWorkspaceStatus( - workspace.latest_build.status, - workspace.latest_build.job, - ); - return (
- - - {text} + {workspace.latest_build.status === "running" && !workspace.health.healthy && ( = ({ workspace }) => { {workspace.dormant_at && ( )} - + {lastUsedMessage(workspace.last_used_at)}