mirror of https://github.com/coder/coder.git
refactor: Make workspace status more visible (#3130)
This commit is contained in:
parent
1b19a09a37
commit
ca93614c3f
|
@ -168,7 +168,7 @@ const useStyles = makeStyles<Theme, StyleProps>((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%",
|
||||
|
|
|
@ -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<WorkspaceProps> = ({
|
|||
</Stack>
|
||||
}
|
||||
>
|
||||
<WorkspaceStatusBadge build={workspace.latest_build} className={styles.statusBadge} />
|
||||
<PageHeaderTitle>{workspace.name}</PageHeaderTitle>
|
||||
<PageHeaderSubtitle>{workspace.owner_name}</PageHeaderSubtitle>
|
||||
</PageHeader>
|
||||
|
@ -118,6 +120,9 @@ const spacerWidth = 300
|
|||
|
||||
export const useStyles = makeStyles((theme) => {
|
||||
return {
|
||||
statusBadge: {
|
||||
marginBottom: theme.spacing(3),
|
||||
},
|
||||
firstColumnSpacer: {
|
||||
flex: 2,
|
||||
},
|
||||
|
|
|
@ -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<WorkspaceStatsProps> = ({ 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<WorkspaceStatsProps> = ({ workspace, handleUpdat
|
|||
<span className={styles.statsLabel}>{Language.byLabel}</span>
|
||||
<span className={styles.statsValue}>{initiatedBy}</span>
|
||||
</div>
|
||||
<div className={styles.statsDivider} />
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statsLabel}>{Language.statusLabel}</span>
|
||||
<span className={styles.statsValue}>
|
||||
<span style={{ color: status.color }} role="status">
|
||||
{status.status}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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<WorkspaceStatusBadgeProps> = (args) => <WorkspaceStatusBadge {...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,
|
||||
}
|
|
@ -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 <CircularProgress size={10} style={{ color: "#FFF" }} />
|
||||
}
|
||||
|
||||
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: <LoadingIcon />,
|
||||
}
|
||||
case "started":
|
||||
return {
|
||||
borderColor: theme.palette.success.main,
|
||||
backgroundColor: theme.palette.success.dark,
|
||||
text: StatusLanguage.started,
|
||||
icon: <PlayIcon />,
|
||||
}
|
||||
case "starting":
|
||||
return {
|
||||
borderColor: theme.palette.success.main,
|
||||
backgroundColor: theme.palette.success.dark,
|
||||
text: StatusLanguage.starting,
|
||||
icon: <LoadingIcon />,
|
||||
}
|
||||
case "stopping":
|
||||
return {
|
||||
borderColor: theme.palette.warning.main,
|
||||
backgroundColor: theme.palette.warning.dark,
|
||||
text: StatusLanguage.stopping,
|
||||
icon: <LoadingIcon />,
|
||||
}
|
||||
case "stopped":
|
||||
return {
|
||||
borderColor: theme.palette.warning.main,
|
||||
backgroundColor: theme.palette.warning.dark,
|
||||
text: StatusLanguage.stopped,
|
||||
icon: <StopIcon />,
|
||||
}
|
||||
case "deleting":
|
||||
return {
|
||||
borderColor: theme.palette.warning.main,
|
||||
backgroundColor: theme.palette.warning.dark,
|
||||
text: StatusLanguage.deleting,
|
||||
icon: <LoadingIcon />,
|
||||
}
|
||||
case "deleted":
|
||||
return {
|
||||
borderColor: theme.palette.error.main,
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
text: StatusLanguage.deleted,
|
||||
icon: <ErrorIcon />,
|
||||
}
|
||||
case "canceling":
|
||||
return {
|
||||
borderColor: theme.palette.warning.main,
|
||||
backgroundColor: theme.palette.warning.dark,
|
||||
text: StatusLanguage.canceling,
|
||||
icon: <LoadingIcon />,
|
||||
}
|
||||
case "canceled":
|
||||
return {
|
||||
borderColor: theme.palette.warning.main,
|
||||
backgroundColor: theme.palette.warning.dark,
|
||||
text: StatusLanguage.canceled,
|
||||
icon: <ErrorIcon />,
|
||||
}
|
||||
case "error":
|
||||
return {
|
||||
borderColor: theme.palette.error.main,
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
text: StatusLanguage.failed,
|
||||
icon: <ErrorIcon />,
|
||||
}
|
||||
case "queued":
|
||||
return {
|
||||
borderColor: theme.palette.info.main,
|
||||
backgroundColor: theme.palette.info.dark,
|
||||
text: StatusLanguage.queued,
|
||||
icon: <LoadingIcon />,
|
||||
}
|
||||
}
|
||||
throw new Error("unknown text " + status)
|
||||
}
|
||||
|
||||
export type WorkspaceStatusBadgeProps = {
|
||||
build: WorkspaceBuild
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const WorkspaceStatusBadge: React.FC<WorkspaceStatusBadgeProps> = ({ build, className }) => {
|
||||
const styles = useStyles()
|
||||
const theme = useTheme()
|
||||
const { text, icon, ...colorStyles } = getStatus(theme, build)
|
||||
return (
|
||||
<div
|
||||
className={combineClasses([styles.wrapper, className])}
|
||||
style={{ ...colorStyles }}
|
||||
role="status"
|
||||
>
|
||||
<div className={styles.iconWrapper}>{icon}</div>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
},
|
||||
}))
|
|
@ -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
|
|||
</TableCellLink>
|
||||
|
||||
<TableCellLink to={workspacePageLink}>
|
||||
<span style={{ color: status.color }}>{status.status}</span>
|
||||
<WorkspaceStatusBadge build={workspace.latest_build} />
|
||||
</TableCellLink>
|
||||
<TableCellLink to={workspacePageLink}>
|
||||
<div className={styles.arrowCell}>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue