feat: Redesign the workspace page (#1620)

This commit is contained in:
Bruno Quaresma
2022-05-20 17:05:00 +00:00
committed by GitHub
parent 0622603220
commit ce7bf0b847
15 changed files with 437 additions and 317 deletions
+1
View File
@@ -7,6 +7,7 @@
"coderd",
"coderdtest",
"codersdk",
"cronstrue",
"devel",
"drpc",
"drpcconn",
+14 -6
View File
@@ -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<StackProps> = ({ children, spacing = 2 }) => {
const styles = useStyles({ spacing })
export interface StackProps {
spacing?: number
direction?: Direction
}
export const Stack: React.FC<StackProps> = ({ children, spacing = 2, direction = "column" }) => {
const styles = useStyles({ spacing, direction })
return <div className={styles.stack}>{children}</div>
}
+43 -55
View File
@@ -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<WorkspaceProps> = ({
return (
<div className={styles.root}>
<div className={styles.vertical}>
<WorkspaceStatusBar
workspace={workspace}
handleStart={handleStart}
handleStop={handleStop}
handleRetry={handleRetry}
handleUpdate={handleUpdate}
workspaceStatus={workspaceStatus}
/>
<div className={styles.header}>
<div>
<Typography variant="h4" className={styles.title}>
{workspace.name}
</Typography>
<div className={styles.horizontal}>
<div className={styles.sidebarContainer}>
<WorkspaceSection title="Applications">
<Placeholder />
</WorkspaceSection>
<WorkspaceSchedule workspace={workspace} />
<Typography color="textSecondary" className={styles.subtitle}>
{workspace.owner_name}
</Typography>
</div>
<WorkspaceSection title="Dev URLs">
<Placeholder />
</WorkspaceSection>
<WorkspaceSection title="Resources">
<Placeholder />
</WorkspaceSection>
</div>
<div className={styles.timelineContainer}>
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>
<BuildsTable builds={builds} className={styles.timelineTable} />
</WorkspaceSection>
</div>
<div className={styles.headerActions}>
<WorkspaceActions
workspace={workspace}
handleStart={handleStart}
handleStop={handleStop}
handleRetry={handleRetry}
handleUpdate={handleUpdate}
workspaceStatus={workspaceStatus}
/>
</div>
</div>
<Stack spacing={3}>
<WorkspaceStats workspace={workspace} />
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>
<BuildsTable builds={builds} className={styles.timelineTable} />
</WorkspaceSection>
</Stack>
</div>
)
}
/**
* 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 (
<div style={{ textAlign: "center", opacity: "0.5" }}>
<Typography variant="caption">Not yet implemented</Typography>
</div>
)
}
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,
@@ -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<typeof WorkspaceActionButton>
const Template: Story<WorkspaceActionButtonProps> = (args) => <WorkspaceActionButton {...args} />
export const Example = Template.bind({})
Example.args = {
icon: <PlayArrowRoundedIcon />,
label: "Start workspace",
loadingLabel: "Starting workspace",
isLoading: false,
}
export const Loading = Template.bind({})
Loading.args = {
icon: <PlayArrowRoundedIcon />,
label: "Start workspace",
loadingLabel: "Starting workspace",
isLoading: true,
}
@@ -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<WorkspaceActionButtonProps> = ({
label,
loadingLabel,
isLoading,
icon,
onClick,
className,
}) => {
const styles = useStyles()
return (
<Button
className={className}
startIcon={isLoading ? <CircularProgress size={12} className={styles.spinner} /> : icon}
onClick={onClick}
disabled={isLoading}
>
{isLoading ? loadingLabel : label}
</Button>
)
}
const useStyles = makeStyles((theme) => ({
spinner: {
color: theme.palette.text.disabled,
marginRight: theme.spacing(1),
},
}))
@@ -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<WorkspaceActionsProps> = ({
workspace,
workspaceStatus,
handleStart,
handleStop,
handleRetry,
handleUpdate,
}) => {
const styles = useStyles()
return (
<Stack direction="row" spacing={1}>
<Link underline="none" component={RouterLink} to="edit">
<Button variant="outlined">Settings</Button>
</Link>
{(workspaceStatus === "started" || workspaceStatus === "stopping") && (
<WorkspaceActionButton
className={styles.actionButton}
icon={<StopIcon />}
onClick={handleStop}
label={Language.stop}
loadingLabel={Language.stopping}
isLoading={workspaceStatus === "stopping"}
/>
)}
{(workspaceStatus === "stopped" || workspaceStatus === "starting") && (
<WorkspaceActionButton
className={styles.actionButton}
icon={<PlayArrowRoundedIcon />}
onClick={handleStart}
label={Language.start}
loadingLabel={Language.starting}
isLoading={workspaceStatus === "starting"}
/>
)}
{workspaceStatus === "error" && (
<Button className={styles.actionButton} startIcon={<ReplayIcon />} onClick={handleRetry}>
{Language.retry}
</Button>
)}
{workspace.outdated && canAcceptJobs(workspaceStatus) && (
<Button className={styles.actionButton} startIcon={<CloudDownloadIcon />} onClick={handleUpdate}>
{Language.update}
</Button>
)}
</Stack>
)
}
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),
},
}))
@@ -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<WorkspaceBuildStatsProps> = ({ 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: {
@@ -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<WorkspaceScheduleProps> = ({ workspace }) => {
return (
<WorkspaceSection title="Workspace schedule">
<Box mt={2}>
<Typography variant="h6">{Language.autoStartLabel(workspace.autostart_schedule)}</Typography>
<Typography>{Language.autoStartDisplay(workspace.autostart_schedule)}</Typography>
</Box>
<Box mt={2}>
<Typography variant="h6">{Language.autoStopLabel}</Typography>
<Typography data-chromatic="ignore">{Language.autoStopDisplay(workspace)}</Typography>
</Box>
</WorkspaceSection>
)
}
@@ -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}`,
@@ -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<WorkspaceScheduleProps> = (args) => <WorkspaceSchedule {...args} />
const Template: Story<WorkspaceStatsProps> = (args) => <WorkspaceStats {...args} />
export const NoTTL = Template.bind({})
NoTTL.args = {
@@ -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<WorkspaceStatsProps> = ({ workspace }) => {
const styles = useStyles()
const theme = useTheme()
const status = getDisplayStatus(theme, workspace.latest_build)
return (
<div className={styles.stats}>
<div className={styles.statItem}>
<span className={styles.statsLabel}>Workspace</span>
<Link
component={RouterLink}
to={`/templates/${workspace.template_name}`}
className={combineClasses([styles.statsValue, styles.link])}
>
{workspace.template_name}
</Link>
</div>
<div className={styles.statsDivider} />
<div className={styles.statItem}>
<span className={styles.statsLabel}>Status</span>
<span className={styles.statsValue}>
<span style={{ color: status.color }} role="status">
{status.status}
</span>
</span>
</div>
<div className={styles.statsDivider} />
<div className={styles.statItem}>
<span className={styles.statsLabel}>Version</span>
<span className={styles.statsValue}>
{workspace.outdated ? (
<span style={{ color: theme.palette.error.main }}>outdated</span>
) : (
<span style={{ color: theme.palette.text.secondary }}>up to date</span>
)}
</span>
</div>
<div className={styles.statsDivider} />
<div className={styles.statItem}>
<span className={styles.statsLabel}>Last Built</span>
<span className={styles.statsValue}>{dayjs().to(dayjs(workspace.latest_build.created_at))}</span>
</div>
<div className={styles.statsDivider} />
<div className={styles.statItem}>
<span className={styles.statsLabel}>{autoStartLabel(workspace.autostart_schedule)}</span>
<span className={styles.statsValue}>{autoStartDisplay(workspace.autostart_schedule)}</span>
</div>
<div className={styles.statsDivider} />
<div className={styles.statItem}>
<span className={styles.statsLabel}>Shutdown</span>
<span className={styles.statsValue}>{autoStopDisplay(workspace)}</span>
</div>
</div>
)
}
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,
},
}))
@@ -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<WorkspaceStatusBarProps> = ({
workspace,
handleStart,
handleStop,
handleRetry,
handleUpdate,
workspaceStatus,
}) => {
const styles = useStyles()
const settingsLink = "edit"
return (
<WorkspaceSection>
<Stack spacing={1}>
<div className={combineClasses([styles.horizontal, styles.reverse])}>
<div className={styles.horizontal}>
<Link className={styles.link} to={settingsLink}>
{Language.settings}
</Link>
</div>
</div>
<div className={styles.horizontal}>
<div className={styles.horizontal}>
<Typography variant="h4">{workspace.name}</Typography>
<Box className={styles.statusChip} role="status">
{Language[workspaceStatus]}
</Box>
</div>
<div className={styles.horizontal}>
{workspaceStatus === "started" && (
<Button onClick={handleStop} color="primary">
{Language.stop}
</Button>
)}
{workspaceStatus === "stopped" && (
<Button onClick={handleStart} color="primary">
{Language.start}
</Button>
)}
{workspaceStatus === "error" && (
<Button onClick={handleRetry} color="primary">
{Language.retry}
</Button>
)}
{workspace.outdated && canAcceptJobs(workspaceStatus) && (
<Button onClick={handleUpdate} color="primary">
{Language.update}
</Button>
)}
</div>
</div>
</Stack>
</WorkspaceSection>
)
}
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",
},
}
})
@@ -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()
+1 -1
View File
@@ -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
+25 -11
View File
@@ -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)