diff --git a/site/src/components/SearchBarWithFilter/SearchBarWithFilter.tsx b/site/src/components/SearchBarWithFilter/SearchBarWithFilter.tsx index 315971700cb60..668ec0e9bda8e 100644 --- a/site/src/components/SearchBarWithFilter/SearchBarWithFilter.tsx +++ b/site/src/components/SearchBarWithFilter/SearchBarWithFilter.tsx @@ -168,7 +168,7 @@ const useStyles = makeStyles((theme) => ({ borderRadius: `${theme.shape.borderRadius}px 0px 0px ${theme.shape.borderRadius}px`, }, errorRoot: { - color: theme.palette.error.dark, + color: theme.palette.error.main, }, inputStyles: { height: "100%", diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 3d920f1551fa0..07319da6a94d9 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -1,4 +1,5 @@ import { makeStyles } from "@material-ui/core/styles" +import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge" import React, { FC } from "react" import { useNavigate } from "react-router-dom" import * as TypesGen from "../../api/typesGenerated" @@ -77,6 +78,7 @@ export const Workspace: FC = ({ } > + {workspace.name} {workspace.owner_name} @@ -118,6 +120,9 @@ const spacerWidth = 300 export const useStyles = makeStyles((theme) => { return { + statusBadge: { + marginBottom: theme.spacing(3), + }, firstColumnSpacer: { flex: 2, }, diff --git a/site/src/components/WorkspaceStats/WorkspaceStats.tsx b/site/src/components/WorkspaceStats/WorkspaceStats.tsx index 094fe50c3c1fb..0db43a78f513d 100644 --- a/site/src/components/WorkspaceStats/WorkspaceStats.tsx +++ b/site/src/components/WorkspaceStats/WorkspaceStats.tsx @@ -5,7 +5,7 @@ import { FC } from "react" import { Link as RouterLink } from "react-router-dom" import { combineClasses } from "util/combineClasses" import { createDayString } from "util/createDayString" -import { getDisplayStatus, getDisplayWorkspaceBuildInitiatedBy } from "util/workspace" +import { getDisplayWorkspaceBuildInitiatedBy } from "util/workspace" import { Workspace } from "../../api/typesGenerated" import { MONOSPACE_FONT_FAMILY } from "../../theme/constants" @@ -28,7 +28,6 @@ export interface WorkspaceStatsProps { export const WorkspaceStats: FC = ({ workspace, handleUpdate }) => { const styles = useStyles() const theme = useTheme() - const status = getDisplayStatus(theme, workspace.latest_build) const initiatedBy = getDisplayWorkspaceBuildInitiatedBy(workspace.latest_build) return ( @@ -69,15 +68,6 @@ export const WorkspaceStats: FC = ({ workspace, handleUpdat {Language.byLabel} {initiatedBy} -
-
- {Language.statusLabel} - - - {status.status} - - -
) } diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx new file mode 100644 index 0000000000000..e91f1ca35b79a --- /dev/null +++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx @@ -0,0 +1,71 @@ +import { Story } from "@storybook/react" +import { + MockCanceledWorkspace, + MockCancelingWorkspace, + MockDeletedWorkspace, + MockDeletingWorkspace, + MockFailedWorkspace, + MockQueuedWorkspace, + MockStartingWorkspace, + MockStoppedWorkspace, + MockStoppingWorkspace, + MockWorkspace, +} from "testHelpers/renderHelpers" +import { WorkspaceStatusBadge, WorkspaceStatusBadgeProps } from "./WorkspaceStatusBadge" + +export default { + title: "components/WorkspaceStatusBadge", + component: WorkspaceStatusBadge, +} + +const Template: Story = (args) => + +export const Running = Template.bind({}) +Running.args = { + build: MockWorkspace.latest_build, +} + +export const Starting = Template.bind({}) +Starting.args = { + build: MockStartingWorkspace.latest_build, +} + +export const Stopped = Template.bind({}) +Stopped.args = { + build: MockStoppedWorkspace.latest_build, +} + +export const Stopping = Template.bind({}) +Stopping.args = { + build: MockStoppingWorkspace.latest_build, +} + +export const Deleting = Template.bind({}) +Deleting.args = { + build: MockDeletingWorkspace.latest_build, +} + +export const Deleted = Template.bind({}) +Deleted.args = { + build: MockDeletedWorkspace.latest_build, +} + +export const Canceling = Template.bind({}) +Canceling.args = { + build: MockCancelingWorkspace.latest_build, +} + +export const Canceled = Template.bind({}) +Canceled.args = { + build: MockCanceledWorkspace.latest_build, +} + +export const Failed = Template.bind({}) +Failed.args = { + build: MockFailedWorkspace.latest_build, +} + +export const Queued = Template.bind({}) +Queued.args = { + build: MockQueuedWorkspace.latest_build, +} diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx new file mode 100644 index 0000000000000..3e8f5046792d3 --- /dev/null +++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx @@ -0,0 +1,174 @@ +import CircularProgress from "@material-ui/core/CircularProgress" +import { makeStyles, Theme, useTheme } from "@material-ui/core/styles" +import ErrorIcon from "@material-ui/icons/ErrorOutline" +import PlayIcon from "@material-ui/icons/PlayArrowOutlined" +import StopIcon from "@material-ui/icons/StopOutlined" +import { WorkspaceBuild } from "api/typesGenerated" +import React from "react" +import { MONOSPACE_FONT_FAMILY } from "theme/constants" +import { combineClasses } from "util/combineClasses" +import { getWorkspaceStatus } from "util/workspace" + +const StatusLanguage = { + loading: "Loading", + started: "Running", + starting: "Starting", + stopping: "Stopping", + stopped: "Stopped", + deleting: "Deleting", + deleted: "Deleted", + canceling: "Canceling action", + canceled: "Canceled action", + failed: "Failed", + queued: "Queued", +} + +const LoadingIcon: React.FC = () => { + return +} + +export const getStatus = ( + theme: Theme, + build: WorkspaceBuild, +): { + borderColor: string + backgroundColor: string + text: string + icon: React.ReactNode +} => { + const status = getWorkspaceStatus(build) + switch (status) { + case undefined: + return { + borderColor: theme.palette.text.secondary, + backgroundColor: theme.palette.text.secondary, + text: StatusLanguage.loading, + icon: , + } + case "started": + return { + borderColor: theme.palette.success.main, + backgroundColor: theme.palette.success.dark, + text: StatusLanguage.started, + icon: , + } + case "starting": + return { + borderColor: theme.palette.success.main, + backgroundColor: theme.palette.success.dark, + text: StatusLanguage.starting, + icon: , + } + case "stopping": + return { + borderColor: theme.palette.warning.main, + backgroundColor: theme.palette.warning.dark, + text: StatusLanguage.stopping, + icon: , + } + case "stopped": + return { + borderColor: theme.palette.warning.main, + backgroundColor: theme.palette.warning.dark, + text: StatusLanguage.stopped, + icon: , + } + case "deleting": + return { + borderColor: theme.palette.warning.main, + backgroundColor: theme.palette.warning.dark, + text: StatusLanguage.deleting, + icon: , + } + case "deleted": + return { + borderColor: theme.palette.error.main, + backgroundColor: theme.palette.error.dark, + text: StatusLanguage.deleted, + icon: , + } + case "canceling": + return { + borderColor: theme.palette.warning.main, + backgroundColor: theme.palette.warning.dark, + text: StatusLanguage.canceling, + icon: , + } + case "canceled": + return { + borderColor: theme.palette.warning.main, + backgroundColor: theme.palette.warning.dark, + text: StatusLanguage.canceled, + icon: , + } + case "error": + return { + borderColor: theme.palette.error.main, + backgroundColor: theme.palette.error.dark, + text: StatusLanguage.failed, + icon: , + } + case "queued": + return { + borderColor: theme.palette.info.main, + backgroundColor: theme.palette.info.dark, + text: StatusLanguage.queued, + icon: , + } + } + throw new Error("unknown text " + status) +} + +export type WorkspaceStatusBadgeProps = { + build: WorkspaceBuild + className?: string +} + +export const WorkspaceStatusBadge: React.FC = ({ build, className }) => { + const styles = useStyles() + const theme = useTheme() + const { text, icon, ...colorStyles } = getStatus(theme, build) + return ( +
+
{icon}
+ {text} +
+ ) +} + +const useStyles = makeStyles((theme) => ({ + wrapper: { + fontFamily: MONOSPACE_FONT_FAMILY, + display: "inline-flex", + alignItems: "center", + borderWidth: 1, + borderStyle: "solid", + borderRadius: 99999, + fontSize: 14, + fontWeight: 500, + color: "#FFF", + height: theme.spacing(3), + paddingLeft: theme.spacing(0.75), + paddingRight: theme.spacing(1.5), + whiteSpace: "nowrap", + }, + + iconWrapper: { + marginRight: theme.spacing(0.5), + width: theme.spacing(2), + height: theme.spacing(2), + lineHeight: 0, + display: "flex", + alignItems: "center", + justifyContent: "center", + + "& > svg": { + width: theme.spacing(2), + height: theme.spacing(2), + }, + }, +})) diff --git a/site/src/components/WorkspacesTable/WorkspacesRow.tsx b/site/src/components/WorkspacesTable/WorkspacesRow.tsx index 24a2c534b7ebe..4467b9a965f9a 100644 --- a/site/src/components/WorkspacesTable/WorkspacesRow.tsx +++ b/site/src/components/WorkspacesTable/WorkspacesRow.tsx @@ -3,10 +3,11 @@ import TableRow from "@material-ui/core/TableRow" import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight" import useTheme from "@material-ui/styles/useTheme" import { useActor } from "@xstate/react" +import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge" import { FC } from "react" import { useNavigate } from "react-router-dom" import { createDayString } from "util/createDayString" -import { getDisplayStatus, getDisplayWorkspaceBuildInitiatedBy } from "../../util/workspace" +import { getDisplayWorkspaceBuildInitiatedBy } from "util/workspace" import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService" import { AvatarData } from "../AvatarData/AvatarData" import { @@ -28,7 +29,6 @@ export const WorkspacesRow: FC<{ workspaceRef: WorkspaceItemMachineRef }> = ({ w const theme: Theme = useTheme() const [workspaceState, send] = useActor(workspaceRef) const { data: workspace } = workspaceState.context - const status = getDisplayStatus(theme, workspace.latest_build) const initiatedBy = getDisplayWorkspaceBuildInitiatedBy(workspace.latest_build) const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}` @@ -74,7 +74,7 @@ export const WorkspacesRow: FC<{ workspaceRef: WorkspaceItemMachineRef }> = ({ w - {status.status} +
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index acf05c0c09f6f..d847cdca8fadd 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -98,7 +98,10 @@ export const MockRunningProvisionerJob: TypesGen.ProvisionerJob = { ...MockProvisionerJob, status: "running", } - +export const MockPendingProvisionerJob: TypesGen.ProvisionerJob = { + ...MockProvisionerJob, + status: "pending", +} export const MockTemplateVersion: TypesGen.TemplateVersion = { id: "test-template-version", created_at: "2022-05-17T17:39:01.382927298Z", @@ -236,6 +239,15 @@ export const MockDeletedWorkspace: TypesGen.Workspace = { export const MockOutdatedWorkspace: TypesGen.Workspace = { ...MockFailedWorkspace, outdated: true } +export const MockQueuedWorkspace: TypesGen.Workspace = { + ...MockWorkspace, + latest_build: { + ...MockWorkspaceBuild, + job: MockPendingProvisionerJob, + transition: "start", + }, +} + export const MockWorkspaceApp: TypesGen.WorkspaceApp = { id: "test-app", name: "test-app", diff --git a/site/src/theme/palettes.ts b/site/src/theme/palettes.ts index 399ee47662502..da9526b22bf02 100644 --- a/site/src/theme/palettes.ts +++ b/site/src/theme/palettes.ts @@ -24,11 +24,18 @@ export const darkPalette: PaletteOptions = { divider: "hsl(221, 32%, 26%)", warning: { main: "hsl(20, 79%, 53%)", + dark: "#57250C", }, success: { main: "hsl(142, 58%, 41%)", + dark: "#205027", }, info: { main: "hsl(219, 67%, 54%)", + dark: "#0C2551", + }, + error: { + main: "#D8666C", + dark: "#511112", }, } diff --git a/site/src/util/workspace.ts b/site/src/util/workspace.ts index b5cdad8978265..7949b2548275a 100644 --- a/site/src/util/workspace.ts +++ b/site/src/util/workspace.ts @@ -67,75 +67,6 @@ export const DisplayStatusLanguage = { queued: "Queued", } -// Localize workspace status and provide corresponding color from theme -export const getDisplayStatus = ( - theme: Theme, - build: TypesGen.WorkspaceBuild, -): { - color: string - status: string -} => { - const status = getWorkspaceStatus(build) - switch (status) { - case undefined: - return { - color: theme.palette.text.secondary, - status: DisplayStatusLanguage.loading, - } - case "started": - return { - color: theme.palette.success.main, - status: `⦿ ${DisplayStatusLanguage.started}`, - } - case "starting": - return { - color: theme.palette.primary.main, - status: `⦿ ${DisplayStatusLanguage.starting}`, - } - case "stopping": - return { - color: theme.palette.primary.main, - status: `◍ ${DisplayStatusLanguage.stopping}`, - } - case "stopped": - return { - color: theme.palette.text.secondary, - status: `◍ ${DisplayStatusLanguage.stopped}`, - } - case "deleting": - return { - color: theme.palette.text.secondary, - status: `⦸ ${DisplayStatusLanguage.deleting}`, - } - case "deleted": - return { - color: theme.palette.text.secondary, - status: `⦸ ${DisplayStatusLanguage.deleted}`, - } - case "canceling": - return { - color: theme.palette.warning.light, - status: `◍ ${DisplayStatusLanguage.canceling}`, - } - case "canceled": - return { - color: theme.palette.text.secondary, - status: `◍ ${DisplayStatusLanguage.canceled}`, - } - case "error": - return { - color: theme.palette.error.main, - status: `ⓧ ${DisplayStatusLanguage.failed}`, - } - case "queued": - return { - color: theme.palette.text.secondary, - status: `◍ ${DisplayStatusLanguage.queued}`, - } - } - throw new Error("unknown status " + status) -} - export const DisplayWorkspaceBuildStatusLanguage = { succeeded: "Succeeded", pending: "Pending",