diff --git a/.vscode/settings.json b/.vscode/settings.json index 4aa07eee8282f..e962a55458f04 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "coderd", "coderdtest", "codersdk", + "cronstrue", "devel", "drpc", "drpcconn", diff --git a/site/src/components/Stack/Stack.tsx b/site/src/components/Stack/Stack.tsx index ed1015d9815de..de9bacede3f73 100644 --- a/site/src/components/Stack/Stack.tsx +++ b/site/src/components/Stack/Stack.tsx @@ -1,19 +1,27 @@ import { makeStyles } from "@material-ui/core/styles" import React from "react" -export interface StackProps { - spacing?: number +type Direction = "column" | "row" + +interface StyleProps { + spacing: number + direction: Direction } const useStyles = makeStyles((theme) => ({ stack: { display: "flex", - flexDirection: "column", - gap: ({ spacing }: { spacing: number }) => theme.spacing(spacing), + flexDirection: ({ direction }: StyleProps) => direction, + gap: ({ spacing }: StyleProps) => theme.spacing(spacing), }, })) -export const Stack: React.FC = ({ children, spacing = 2 }) => { - const styles = useStyles({ spacing }) +export interface StackProps { + spacing?: number + direction?: Direction +} + +export const Stack: React.FC = ({ children, spacing = 2, direction = "column" }) => { + const styles = useStyles({ spacing, direction }) return
{children}
} diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index c8b4b87f2eea5..4d8b1bd36d28c 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -2,11 +2,13 @@ import { makeStyles } from "@material-ui/core/styles" import Typography from "@material-ui/core/Typography" import React from "react" import * as TypesGen from "../../api/typesGenerated" +import { MONOSPACE_FONT_FAMILY } from "../../theme/constants" import { WorkspaceStatus } from "../../util/workspace" import { BuildsTable } from "../BuildsTable/BuildsTable" -import { WorkspaceSchedule } from "../WorkspaceSchedule/WorkspaceSchedule" +import { Stack } from "../Stack/Stack" +import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions" import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection" -import { WorkspaceStatusBar } from "../WorkspaceStatusBar/WorkspaceStatusBar" +import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats" export interface WorkspaceProps { handleStart: () => void @@ -34,76 +36,62 @@ export const Workspace: React.FC = ({ return (
-
- +
+
+ + {workspace.name} + -
-
- - - - - - - - - - - - -
+ + {workspace.owner_name} + +
-
- - - -
+
+
-
- ) -} -/** - * Temporary placeholder component until we have the sections implemented - * Can be removed once the Workspace page has all the necessary sections - */ -const Placeholder: React.FC = () => { - return ( -
- Not yet implemented + + + + + +
) } -export const useStyles = makeStyles(() => { +export const useStyles = makeStyles((theme) => { return { root: { display: "flex", flexDirection: "column", }, - horizontal: { + header: { + paddingTop: theme.spacing(5), + paddingBottom: theme.spacing(5), + fontFamily: MONOSPACE_FONT_FAMILY, display: "flex", - flexDirection: "row", + alignItems: "center", }, - vertical: { - display: "flex", - flexDirection: "column", + headerActions: { + marginLeft: "auto", }, - sidebarContainer: { - display: "flex", - flexDirection: "column", - flex: "0 0 350px", + title: { + fontWeight: 600, + fontFamily: "inherit", }, - timelineContainer: { - flex: 1, + subtitle: { + fontFamily: "inherit", + marginTop: theme.spacing(0.5), }, timelineContents: { margin: 0, diff --git a/site/src/components/WorkspaceActionButton/WorkspaceActionButton.stories.tsx b/site/src/components/WorkspaceActionButton/WorkspaceActionButton.stories.tsx new file mode 100644 index 0000000000000..0a3b9b315a83e --- /dev/null +++ b/site/src/components/WorkspaceActionButton/WorkspaceActionButton.stories.tsx @@ -0,0 +1,27 @@ +import PlayArrowRoundedIcon from "@material-ui/icons/PlayArrowRounded" +import { ComponentMeta, Story } from "@storybook/react" +import React from "react" +import { WorkspaceActionButton, WorkspaceActionButtonProps } from "./WorkspaceActionButton" + +export default { + title: "components/WorkspaceActionButton", + component: WorkspaceActionButton, +} as ComponentMeta + +const Template: Story = (args) => + +export const Example = Template.bind({}) +Example.args = { + icon: , + label: "Start workspace", + loadingLabel: "Starting workspace", + isLoading: false, +} + +export const Loading = Template.bind({}) +Loading.args = { + icon: , + label: "Start workspace", + loadingLabel: "Starting workspace", + isLoading: true, +} diff --git a/site/src/components/WorkspaceActionButton/WorkspaceActionButton.tsx b/site/src/components/WorkspaceActionButton/WorkspaceActionButton.tsx new file mode 100644 index 0000000000000..1188dec6a771c --- /dev/null +++ b/site/src/components/WorkspaceActionButton/WorkspaceActionButton.tsx @@ -0,0 +1,42 @@ +import Button from "@material-ui/core/Button" +import CircularProgress from "@material-ui/core/CircularProgress" +import { makeStyles } from "@material-ui/core/styles" +import React from "react" + +export interface WorkspaceActionButtonProps { + label: string + loadingLabel: string + isLoading: boolean + icon: JSX.Element + onClick: () => void + className?: string +} + +export const WorkspaceActionButton: React.FC = ({ + label, + loadingLabel, + isLoading, + icon, + onClick, + className, +}) => { + const styles = useStyles() + + return ( + + ) +} + +const useStyles = makeStyles((theme) => ({ + spinner: { + color: theme.palette.text.disabled, + marginRight: theme.spacing(1), + }, +})) diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx new file mode 100644 index 0000000000000..6bec9312bb91d --- /dev/null +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -0,0 +1,95 @@ +import Button from "@material-ui/core/Button" +import Link from "@material-ui/core/Link" +import { makeStyles } from "@material-ui/core/styles" +import CloudDownloadIcon from "@material-ui/icons/CloudDownload" +import PlayArrowRoundedIcon from "@material-ui/icons/PlayArrowRounded" +import ReplayIcon from "@material-ui/icons/Replay" +import StopIcon from "@material-ui/icons/Stop" +import React from "react" +import { Link as RouterLink } from "react-router-dom" +import { Workspace } from "../../api/typesGenerated" +import { WorkspaceStatus } from "../../util/workspace" +import { Stack } from "../Stack/Stack" +import { WorkspaceActionButton } from "../WorkspaceActionButton/WorkspaceActionButton" + +export const Language = { + stop: "Stop workspace", + stopping: "Stopping workspace", + start: "Start workspace", + starting: "Starting workspace", + retry: "Retry", + update: "Update workspace", +} + +/** + * Jobs submitted while another job is in progress will be discarded, + * so check whether workspace job status has reached completion (whether successful or not). + */ +const canAcceptJobs = (workspaceStatus: WorkspaceStatus) => + ["started", "stopped", "deleted", "error", "canceled"].includes(workspaceStatus) + +export interface WorkspaceActionsProps { + workspace: Workspace + workspaceStatus: WorkspaceStatus + handleStart: () => void + handleStop: () => void + handleRetry: () => void + handleUpdate: () => void +} + +export const WorkspaceActions: React.FC = ({ + workspace, + workspaceStatus, + handleStart, + handleStop, + handleRetry, + handleUpdate, +}) => { + const styles = useStyles() + + return ( + + + + + {(workspaceStatus === "started" || workspaceStatus === "stopping") && ( + } + onClick={handleStop} + label={Language.stop} + loadingLabel={Language.stopping} + isLoading={workspaceStatus === "stopping"} + /> + )} + {(workspaceStatus === "stopped" || workspaceStatus === "starting") && ( + } + onClick={handleStart} + label={Language.start} + loadingLabel={Language.starting} + isLoading={workspaceStatus === "starting"} + /> + )} + {workspaceStatus === "error" && ( + + )} + {workspace.outdated && canAcceptJobs(workspaceStatus) && ( + + )} + + ) +} + +const useStyles = makeStyles((theme) => ({ + actionButton: { + // Set fixed width for the action buttons so they will not change the size + // during the transitions + width: theme.spacing(30), + }, +})) diff --git a/site/src/components/WorkspaceBuildStats/WorkspaceBuildStats.tsx b/site/src/components/WorkspaceBuildStats/WorkspaceBuildStats.tsx index 6d510955d0789..0d936035ec1f3 100644 --- a/site/src/components/WorkspaceBuildStats/WorkspaceBuildStats.tsx +++ b/site/src/components/WorkspaceBuildStats/WorkspaceBuildStats.tsx @@ -3,7 +3,7 @@ import { makeStyles, useTheme } from "@material-ui/core/styles" import React from "react" import { Link as RouterLink } from "react-router-dom" import { WorkspaceBuild } from "../../api/typesGenerated" -import { MONOSPACE_FONT_FAMILY } from "../../theme/constants" +import { CardRadius, MONOSPACE_FONT_FAMILY } from "../../theme/constants" import { combineClasses } from "../../util/combineClasses" import { displayWorkspaceBuildDuration, getDisplayStatus } from "../../util/workspace" @@ -57,17 +57,21 @@ export const WorkspaceBuildStats: React.FC = ({ build const useStyles = makeStyles((theme) => ({ stats: { - paddingTop: theme.spacing(3), - paddingBottom: theme.spacing(3), + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + backgroundColor: theme.palette.background.paper, + borderRadius: CardRadius, display: "flex", alignItems: "center", color: theme.palette.text.secondary, fontFamily: MONOSPACE_FONT_FAMILY, + border: `1px solid ${theme.palette.divider}`, }, statItem: { minWidth: theme.spacing(20), - paddingRight: theme.spacing(3), + padding: theme.spacing(2), + paddingTop: theme.spacing(1.75), }, statsLabel: { @@ -80,14 +84,14 @@ const useStyles = makeStyles((theme) => ({ statsValue: { fontSize: 16, marginTop: theme.spacing(0.25), - display: "block", + display: "inline-block", }, statsDivider: { width: 1, height: theme.spacing(5), backgroundColor: theme.palette.divider, - marginRight: theme.spacing(3), + marginRight: theme.spacing(2), }, capitalize: { diff --git a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx deleted file mode 100644 index 71d4bf6cd5cf4..0000000000000 --- a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import Box from "@material-ui/core/Box" -import Typography from "@material-ui/core/Typography" -import cronstrue from "cronstrue" -import dayjs from "dayjs" -import duration from "dayjs/plugin/duration" -import relativeTime from "dayjs/plugin/relativeTime" -import React from "react" -import * as TypesGen from "../../api/typesGenerated" -import { extractTimezone, stripTimezone } from "../../util/schedule" -import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection" - -dayjs.extend(duration) -dayjs.extend(relativeTime) - -const Language = { - autoStartLabel: (schedule: string): string => { - const prefix = "Start" - - if (schedule) { - return `${prefix} (${extractTimezone(schedule)})` - } else { - return prefix - } - }, - autoStartDisplay: (schedule: string): string => { - if (schedule) { - return cronstrue.toString(stripTimezone(schedule), { throwExceptionOnParseError: false }) - } - return "Manual" - }, - autoStopLabel: "Shutdown", - autoStopDisplay: (workspace: TypesGen.Workspace): string => { - const latest = workspace.latest_build - - if (!workspace.ttl || workspace.ttl < 1) { - return "Manual" - } - - if (latest.transition === "start") { - const now = dayjs() - const updatedAt = dayjs(latest.updated_at) - const deadline = updatedAt.add(workspace.ttl / 1_000_000, "ms") - if (now.isAfter(deadline)) { - return "workspace is shutting down now" - } - return now.to(deadline) - } - - const duration = dayjs.duration(workspace.ttl / 1_000_000, "milliseconds") - return `${duration.humanize()} after start` - }, -} - -export interface WorkspaceScheduleProps { - workspace: TypesGen.Workspace -} - -/** - * WorkspaceSchedule displays a workspace schedule in a human-readable format - * - * @remarks Visual Component - */ -export const WorkspaceSchedule: React.FC = ({ workspace }) => { - return ( - - - {Language.autoStartLabel(workspace.autostart_schedule)} - {Language.autoStartDisplay(workspace.autostart_schedule)} - - - - {Language.autoStopLabel} - {Language.autoStopDisplay(workspace)} - - - ) -} diff --git a/site/src/components/WorkspaceSection/WorkspaceSection.tsx b/site/src/components/WorkspaceSection/WorkspaceSection.tsx index fd02ab2c66a99..9360b6980a38b 100644 --- a/site/src/components/WorkspaceSection/WorkspaceSection.tsx +++ b/site/src/components/WorkspaceSection/WorkspaceSection.tsx @@ -39,7 +39,6 @@ const useStyles = makeStyles((theme) => ({ root: { border: `1px solid ${theme.palette.divider}`, borderRadius: CardRadius, - margin: theme.spacing(1), }, headerContainer: { borderBottom: `1px solid ${theme.palette.divider}`, diff --git a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.stories.tsx b/site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx similarity index 86% rename from site/src/components/WorkspaceSchedule/WorkspaceSchedule.stories.tsx rename to site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx index 8c64ba563b9bc..03dcdf6047231 100644 --- a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.stories.tsx +++ b/site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx @@ -2,14 +2,14 @@ import { Story } from "@storybook/react" import dayjs from "dayjs" import React from "react" import * as Mocks from "../../testHelpers/renderHelpers" -import { WorkspaceSchedule, WorkspaceScheduleProps } from "./WorkspaceSchedule" +import { WorkspaceStats, WorkspaceStatsProps } from "../WorkspaceStats/WorkspaceStats" export default { - title: "components/WorkspaceSchedule", - component: WorkspaceSchedule, + title: "components/WorkspaceStats", + component: WorkspaceStats, } -const Template: Story = (args) => +const Template: Story = (args) => export const NoTTL = Template.bind({}) NoTTL.args = { diff --git a/site/src/components/WorkspaceStats/WorkspaceStats.tsx b/site/src/components/WorkspaceStats/WorkspaceStats.tsx new file mode 100644 index 0000000000000..e22e800a74149 --- /dev/null +++ b/site/src/components/WorkspaceStats/WorkspaceStats.tsx @@ -0,0 +1,163 @@ +import Link from "@material-ui/core/Link" +import { makeStyles, useTheme } from "@material-ui/core/styles" +import cronstrue from "cronstrue" +import dayjs from "dayjs" +import duration from "dayjs/plugin/duration" +import relativeTime from "dayjs/plugin/relativeTime" +import React from "react" +import { Link as RouterLink } from "react-router-dom" +import { Workspace } from "../../api/typesGenerated" +import { CardRadius, MONOSPACE_FONT_FAMILY } from "../../theme/constants" +import { combineClasses } from "../../util/combineClasses" +import { extractTimezone, stripTimezone } from "../../util/schedule" +import { getDisplayStatus } from "../../util/workspace" + +dayjs.extend(duration) +dayjs.extend(relativeTime) + +const autoStartLabel = (schedule: string): string => { + const prefix = "Start" + + if (schedule) { + return `${prefix} (${extractTimezone(schedule)})` + } else { + return prefix + } +} + +const autoStartDisplay = (schedule: string): string => { + if (schedule) { + return cronstrue.toString(stripTimezone(schedule), { throwExceptionOnParseError: false }) + } + return "Manual" +} + +const autoStopDisplay = (workspace: Workspace): string => { + const latest = workspace.latest_build + + if (!workspace.ttl || workspace.ttl < 1) { + return "Manual" + } + + if (latest.transition === "start") { + const now = dayjs() + const updatedAt = dayjs(latest.updated_at) + const deadline = updatedAt.add(workspace.ttl / 1_000_000, "ms") + if (now.isAfter(deadline)) { + return "workspace is shutting down now" + } + return now.to(deadline) + } + + const duration = dayjs.duration(workspace.ttl / 1_000_000, "milliseconds") + return `${duration.humanize()} after start` +} + +export interface WorkspaceStatsProps { + workspace: Workspace +} + +export const WorkspaceStats: React.FC = ({ workspace }) => { + const styles = useStyles() + const theme = useTheme() + const status = getDisplayStatus(theme, workspace.latest_build) + + return ( +
+
+ Workspace + + {workspace.template_name} + +
+
+
+ Status + + + {status.status} + + +
+
+
+ Version + + {workspace.outdated ? ( + outdated + ) : ( + up to date + )} + +
+
+
+ Last Built + {dayjs().to(dayjs(workspace.latest_build.created_at))} +
+
+
+ {autoStartLabel(workspace.autostart_schedule)} + {autoStartDisplay(workspace.autostart_schedule)} +
+
+
+ Shutdown + {autoStopDisplay(workspace)} +
+
+ ) +} + +const useStyles = makeStyles((theme) => ({ + stats: { + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + backgroundColor: theme.palette.background.paper, + borderRadius: CardRadius, + display: "flex", + alignItems: "center", + color: theme.palette.text.secondary, + fontFamily: MONOSPACE_FONT_FAMILY, + border: `1px solid ${theme.palette.divider}`, + }, + + statItem: { + minWidth: theme.spacing(20), + padding: theme.spacing(2), + paddingTop: theme.spacing(1.75), + }, + + statsLabel: { + fontSize: 12, + textTransform: "uppercase", + display: "block", + fontWeight: 600, + }, + + statsValue: { + fontSize: 16, + marginTop: theme.spacing(0.25), + display: "inline-block", + }, + + statsDivider: { + width: 1, + height: theme.spacing(5), + backgroundColor: theme.palette.divider, + marginRight: theme.spacing(2), + }, + + capitalize: { + textTransform: "capitalize", + }, + + link: { + color: theme.palette.text.primary, + fontWeight: 600, + }, +})) diff --git a/site/src/components/WorkspaceStatusBar/WorkspaceStatusBar.tsx b/site/src/components/WorkspaceStatusBar/WorkspaceStatusBar.tsx deleted file mode 100644 index 4ab0182feb527..0000000000000 --- a/site/src/components/WorkspaceStatusBar/WorkspaceStatusBar.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import Box from "@material-ui/core/Box" -import Button from "@material-ui/core/Button" -import { makeStyles } from "@material-ui/core/styles" -import Typography from "@material-ui/core/Typography" -import React from "react" -import { Link } from "react-router-dom" -import * as TypesGen from "../../api/typesGenerated" -import { TitleIconSize } from "../../theme/constants" -import { combineClasses } from "../../util/combineClasses" -import { WorkspaceStatus } from "../../util/workspace" -import { Stack } from "../Stack/Stack" -import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection" - -export const Language = { - stop: "Stop", - start: "Start", - retry: "Retry", - update: "Update", - settings: "Settings", - started: "Running", - stopped: "Stopped", - starting: "Building", - stopping: "Stopping", - canceled: "Canceled", - queued: "Queued", - error: "Build Failed", - loading: "Loading Status", - deleting: "Deleting", - deleted: "Deleted", - // "Canceling" would be misleading because it refers to a build, not the workspace. - // So just stall. When it is canceled it will appear as the error workspaceStatus. - canceling: "Loading Status", -} - -export interface WorkspaceStatusBarProps { - organization?: TypesGen.Organization - workspace: TypesGen.Workspace - template?: TypesGen.Template - handleStart: () => void - handleStop: () => void - handleRetry: () => void - handleUpdate: () => void - workspaceStatus: WorkspaceStatus -} - -/** - * Jobs submitted while another job is in progress will be discarded, - * so check whether workspace job status has reached completion (whether successful or not). - */ -const canAcceptJobs = (workspaceStatus: WorkspaceStatus) => - ["started", "stopped", "deleted", "error", "canceled"].includes(workspaceStatus) - -/** - * Component for the header at the top of the workspace page - */ -export const WorkspaceStatusBar: React.FC = ({ - workspace, - handleStart, - handleStop, - handleRetry, - handleUpdate, - workspaceStatus, -}) => { - const styles = useStyles() - - const settingsLink = "edit" - - return ( - - -
-
- - {Language.settings} - -
-
- -
-
- {workspace.name} - - {Language[workspaceStatus]} - -
- -
- {workspaceStatus === "started" && ( - - )} - {workspaceStatus === "stopped" && ( - - )} - {workspaceStatus === "error" && ( - - )} - - {workspace.outdated && canAcceptJobs(workspaceStatus) && ( - - )} -
-
-
-
- ) -} - -const useStyles = makeStyles((theme) => { - return { - link: { - textDecoration: "none", - color: theme.palette.text.primary, - }, - icon: { - width: TitleIconSize, - height: TitleIconSize, - }, - horizontal: { - display: "flex", - justifyContent: "space-between", - alignItems: "center", - gap: theme.spacing(2), - }, - reverse: { - flexDirection: "row-reverse", - }, - statusChip: { - border: `solid 1px ${theme.palette.text.hint}`, - borderRadius: theme.shape.borderRadius, - padding: theme.spacing(1), - }, - vertical: { - display: "flex", - flexDirection: "column", - }, - } -}) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index ad23e0cd4ed9c..57d8d72c9a502 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -3,7 +3,7 @@ import { rest } from "msw" import React from "react" import * as api from "../../api/api" import { Workspace } from "../../api/typesGenerated" -import { Language } from "../../components/WorkspaceStatusBar/WorkspaceStatusBar" +import { Language } from "../../components/WorkspaceActions/WorkspaceActions" import { MockBuilds, MockCancelingWorkspace, @@ -20,6 +20,7 @@ import { renderWithAuth, } from "../../testHelpers/renderHelpers" import { server } from "../../testHelpers/server" +import { DisplayStatusLanguage } from "../../util/workspace" import { WorkspacePage } from "./WorkspacePage" // It renders the workspace page and waits for it be loaded @@ -133,28 +134,28 @@ describe("Workspace Page", () => { await testButton(Language.update, getTemplateMock) }) it("shows the Stopping status when the workspace is stopping", async () => { - await testStatus(MockStoppingWorkspace, Language.stopping) + await testStatus(MockStoppingWorkspace, DisplayStatusLanguage.stopping) }) it("shows the Stopped status when the workspace is stopped", async () => { - await testStatus(MockStoppedWorkspace, Language.stopped) + await testStatus(MockStoppedWorkspace, DisplayStatusLanguage.stopped) }) it("shows the Building status when the workspace is starting", async () => { - await testStatus(MockStartingWorkspace, Language.starting) + await testStatus(MockStartingWorkspace, DisplayStatusLanguage.starting) }) it("shows the Running status when the workspace is started", async () => { - await testStatus(MockWorkspace, Language.started) + await testStatus(MockWorkspace, DisplayStatusLanguage.started) }) - it("shows the Error status when the workspace is failed or canceled", async () => { - await testStatus(MockFailedWorkspace, Language.error) + it("shows the Failed status when the workspace is failed or canceled", async () => { + await testStatus(MockFailedWorkspace, DisplayStatusLanguage.failed) }) - it("shows the Loading status when the workspace is canceling", async () => { - await testStatus(MockCancelingWorkspace, Language.canceling) + it("shows the Canceling status when the workspace is canceling", async () => { + await testStatus(MockCancelingWorkspace, DisplayStatusLanguage.canceling) }) it("shows the Deleting status when the workspace is deleting", async () => { - await testStatus(MockDeletingWorkspace, Language.deleting) + await testStatus(MockDeletingWorkspace, DisplayStatusLanguage.deleting) }) it("shows the Deleted status when the workspace is deleted", async () => { - await testStatus(MockDeletedWorkspace, Language.deleted) + await testStatus(MockDeletedWorkspace, DisplayStatusLanguage.deleted) }) it("shows the timeline build", async () => { await renderWorkspacePage() diff --git a/site/src/theme/constants.ts b/site/src/theme/constants.ts index 3c99f6d5c3428..3dde9819a7de3 100644 --- a/site/src/theme/constants.ts +++ b/site/src/theme/constants.ts @@ -10,5 +10,5 @@ export const navHeight = 42 export const maxWidth = 1380 export const sidePadding = "50px" export const TitleIconSize = 48 -export const CardRadius = 8 +export const CardRadius = 2 export const CardPadding = 20 diff --git a/site/src/util/workspace.ts b/site/src/util/workspace.ts index fba0542a53d79..6c72c72677aea 100644 --- a/site/src/util/workspace.ts +++ b/site/src/util/workspace.ts @@ -50,6 +50,20 @@ export const getWorkspaceStatus = (workspaceBuild?: WorkspaceBuild): WorkspaceSt } } +export const DisplayStatusLanguage = { + loading: "Loading...", + started: "Running", + starting: "Starting", + stopping: "Stopping", + stopped: "Stopped", + deleting: "Deleting", + deleted: "Deleted", + canceling: "Canceling", + canceled: "Canceled", + failed: "Failed", + queued: "Queued", +} + export const getDisplayStatus = ( theme: Theme, build: WorkspaceBuild, @@ -62,57 +76,57 @@ export const getDisplayStatus = ( case undefined: return { color: theme.palette.text.secondary, - status: "Loading...", + status: DisplayStatusLanguage.loading, } case "started": return { color: theme.palette.success.main, - status: "⦿ Running", + status: `⦿ ${DisplayStatusLanguage.started}`, } case "starting": return { color: theme.palette.success.main, - status: "⦿ Starting", + status: `⦿ ${DisplayStatusLanguage.starting}`, } case "stopping": return { color: theme.palette.text.secondary, - status: "◍ Stopping", + status: `◍ ${DisplayStatusLanguage.stopping}`, } case "stopped": return { color: theme.palette.text.secondary, - status: "◍ Stopped", + status: `◍ ${DisplayStatusLanguage.stopped}`, } case "deleting": return { color: theme.palette.text.secondary, - status: "⦸ Deleting", + status: `⦸ ${DisplayStatusLanguage.deleting}`, } case "deleted": return { color: theme.palette.text.secondary, - status: "⦸ Deleted", + status: `⦸ ${DisplayStatusLanguage.deleted}`, } case "canceling": return { color: theme.palette.warning.light, - status: "◍ Canceling", + status: `◍ ${DisplayStatusLanguage.canceling}`, } case "canceled": return { color: theme.palette.text.secondary, - status: "◍ Canceled", + status: `◍ ${DisplayStatusLanguage.canceled}`, } case "error": return { color: theme.palette.error.main, - status: "ⓧ Failed", + status: `ⓧ ${DisplayStatusLanguage.failed}`, } case "queued": return { color: theme.palette.text.secondary, - status: "◍ Queued", + status: `◍ ${DisplayStatusLanguage.queued}`, } } throw new Error("unknown status " + status)