Skip to content

Commit ce7bf0b

Browse files
feat: Redesign the workspace page (#1620)
1 parent 0622603 commit ce7bf0b

File tree

15 files changed

+436
-316
lines changed

15 files changed

+436
-316
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"coderd",
88
"coderdtest",
99
"codersdk",
10+
"cronstrue",
1011
"devel",
1112
"drpc",
1213
"drpcconn",

site/src/components/Stack/Stack.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
import { makeStyles } from "@material-ui/core/styles"
22
import React from "react"
33

4-
export interface StackProps {
5-
spacing?: number
4+
type Direction = "column" | "row"
5+
6+
interface StyleProps {
7+
spacing: number
8+
direction: Direction
69
}
710

811
const useStyles = makeStyles((theme) => ({
912
stack: {
1013
display: "flex",
11-
flexDirection: "column",
12-
gap: ({ spacing }: { spacing: number }) => theme.spacing(spacing),
14+
flexDirection: ({ direction }: StyleProps) => direction,
15+
gap: ({ spacing }: StyleProps) => theme.spacing(spacing),
1316
},
1417
}))
1518

16-
export const Stack: React.FC<StackProps> = ({ children, spacing = 2 }) => {
17-
const styles = useStyles({ spacing })
19+
export interface StackProps {
20+
spacing?: number
21+
direction?: Direction
22+
}
23+
24+
export const Stack: React.FC<StackProps> = ({ children, spacing = 2, direction = "column" }) => {
25+
const styles = useStyles({ spacing, direction })
1826
return <div className={styles.stack}>{children}</div>
1927
}

site/src/components/Workspace/Workspace.tsx

Lines changed: 42 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import { makeStyles } from "@material-ui/core/styles"
22
import Typography from "@material-ui/core/Typography"
33
import React from "react"
44
import * as TypesGen from "../../api/typesGenerated"
5+
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
56
import { WorkspaceStatus } from "../../util/workspace"
67
import { BuildsTable } from "../BuildsTable/BuildsTable"
7-
import { WorkspaceSchedule } from "../WorkspaceSchedule/WorkspaceSchedule"
8+
import { Stack } from "../Stack/Stack"
9+
import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions"
810
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
9-
import { WorkspaceStatusBar } from "../WorkspaceStatusBar/WorkspaceStatusBar"
11+
import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats"
1012

1113
export interface WorkspaceProps {
1214
handleStart: () => void
@@ -34,76 +36,62 @@ export const Workspace: React.FC<WorkspaceProps> = ({
3436

3537
return (
3638
<div className={styles.root}>
37-
<div className={styles.vertical}>
38-
<WorkspaceStatusBar
39-
workspace={workspace}
40-
handleStart={handleStart}
41-
handleStop={handleStop}
42-
handleRetry={handleRetry}
43-
handleUpdate={handleUpdate}
44-
workspaceStatus={workspaceStatus}
45-
/>
39+
<div className={styles.header}>
40+
<div>
41+
<Typography variant="h4" className={styles.title}>
42+
{workspace.name}
43+
</Typography>
4644

47-
<div className={styles.horizontal}>
48-
<div className={styles.sidebarContainer}>
49-
<WorkspaceSection title="Applications">
50-
<Placeholder />
51-
</WorkspaceSection>
52-
<WorkspaceSchedule workspace={workspace} />
53-
54-
<WorkspaceSection title="Dev URLs">
55-
<Placeholder />
56-
</WorkspaceSection>
57-
58-
<WorkspaceSection title="Resources">
59-
<Placeholder />
60-
</WorkspaceSection>
61-
</div>
45+
<Typography color="textSecondary" className={styles.subtitle}>
46+
{workspace.owner_name}
47+
</Typography>
48+
</div>
6249

63-
<div className={styles.timelineContainer}>
64-
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>
65-
<BuildsTable builds={builds} className={styles.timelineTable} />
66-
</WorkspaceSection>
67-
</div>
50+
<div className={styles.headerActions}>
51+
<WorkspaceActions
52+
workspace={workspace}
53+
handleStart={handleStart}
54+
handleStop={handleStop}
55+
handleRetry={handleRetry}
56+
handleUpdate={handleUpdate}
57+
workspaceStatus={workspaceStatus}
58+
/>
6859
</div>
6960
</div>
70-
</div>
71-
)
72-
}
7361

74-
/**
75-
* Temporary placeholder component until we have the sections implemented
76-
* Can be removed once the Workspace page has all the necessary sections
77-
*/
78-
const Placeholder: React.FC = () => {
79-
return (
80-
<div style={{ textAlign: "center", opacity: "0.5" }}>
81-
<Typography variant="caption">Not yet implemented</Typography>
62+
<Stack spacing={3}>
63+
<WorkspaceStats workspace={workspace} />
64+
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>
65+
<BuildsTable builds={builds} className={styles.timelineTable} />
66+
</WorkspaceSection>
67+
</Stack>
8268
</div>
8369
)
8470
}
8571

86-
export const useStyles = makeStyles(() => {
72+
export const useStyles = makeStyles((theme) => {
8773
return {
8874
root: {
8975
display: "flex",
9076
flexDirection: "column",
9177
},
92-
horizontal: {
78+
header: {
79+
paddingTop: theme.spacing(5),
80+
paddingBottom: theme.spacing(5),
81+
fontFamily: MONOSPACE_FONT_FAMILY,
9382
display: "flex",
94-
flexDirection: "row",
83+
alignItems: "center",
9584
},
96-
vertical: {
97-
display: "flex",
98-
flexDirection: "column",
85+
headerActions: {
86+
marginLeft: "auto",
9987
},
100-
sidebarContainer: {
101-
display: "flex",
102-
flexDirection: "column",
103-
flex: "0 0 350px",
88+
title: {
89+
fontWeight: 600,
90+
fontFamily: "inherit",
10491
},
105-
timelineContainer: {
106-
flex: 1,
92+
subtitle: {
93+
fontFamily: "inherit",
94+
marginTop: theme.spacing(0.5),
10795
},
10896
timelineContents: {
10997
margin: 0,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import PlayArrowRoundedIcon from "@material-ui/icons/PlayArrowRounded"
2+
import { ComponentMeta, Story } from "@storybook/react"
3+
import React from "react"
4+
import { WorkspaceActionButton, WorkspaceActionButtonProps } from "./WorkspaceActionButton"
5+
6+
export default {
7+
title: "components/WorkspaceActionButton",
8+
component: WorkspaceActionButton,
9+
} as ComponentMeta<typeof WorkspaceActionButton>
10+
11+
const Template: Story<WorkspaceActionButtonProps> = (args) => <WorkspaceActionButton {...args} />
12+
13+
export const Example = Template.bind({})
14+
Example.args = {
15+
icon: <PlayArrowRoundedIcon />,
16+
label: "Start workspace",
17+
loadingLabel: "Starting workspace",
18+
isLoading: false,
19+
}
20+
21+
export const Loading = Template.bind({})
22+
Loading.args = {
23+
icon: <PlayArrowRoundedIcon />,
24+
label: "Start workspace",
25+
loadingLabel: "Starting workspace",
26+
isLoading: true,
27+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import Button from "@material-ui/core/Button"
2+
import CircularProgress from "@material-ui/core/CircularProgress"
3+
import { makeStyles } from "@material-ui/core/styles"
4+
import React from "react"
5+
6+
export interface WorkspaceActionButtonProps {
7+
label: string
8+
loadingLabel: string
9+
isLoading: boolean
10+
icon: JSX.Element
11+
onClick: () => void
12+
className?: string
13+
}
14+
15+
export const WorkspaceActionButton: React.FC<WorkspaceActionButtonProps> = ({
16+
label,
17+
loadingLabel,
18+
isLoading,
19+
icon,
20+
onClick,
21+
className,
22+
}) => {
23+
const styles = useStyles()
24+
25+
return (
26+
<Button
27+
className={className}
28+
startIcon={isLoading ? <CircularProgress size={12} className={styles.spinner} /> : icon}
29+
onClick={onClick}
30+
disabled={isLoading}
31+
>
32+
{isLoading ? loadingLabel : label}
33+
</Button>
34+
)
35+
}
36+
37+
const useStyles = makeStyles((theme) => ({
38+
spinner: {
39+
color: theme.palette.text.disabled,
40+
marginRight: theme.spacing(1),
41+
},
42+
}))
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import Button from "@material-ui/core/Button"
2+
import Link from "@material-ui/core/Link"
3+
import { makeStyles } from "@material-ui/core/styles"
4+
import CloudDownloadIcon from "@material-ui/icons/CloudDownload"
5+
import PlayArrowRoundedIcon from "@material-ui/icons/PlayArrowRounded"
6+
import ReplayIcon from "@material-ui/icons/Replay"
7+
import StopIcon from "@material-ui/icons/Stop"
8+
import React from "react"
9+
import { Link as RouterLink } from "react-router-dom"
10+
import { Workspace } from "../../api/typesGenerated"
11+
import { WorkspaceStatus } from "../../util/workspace"
12+
import { Stack } from "../Stack/Stack"
13+
import { WorkspaceActionButton } from "../WorkspaceActionButton/WorkspaceActionButton"
14+
15+
export const Language = {
16+
stop: "Stop workspace",
17+
stopping: "Stopping workspace",
18+
start: "Start workspace",
19+
starting: "Starting workspace",
20+
retry: "Retry",
21+
update: "Update workspace",
22+
}
23+
24+
/**
25+
* Jobs submitted while another job is in progress will be discarded,
26+
* so check whether workspace job status has reached completion (whether successful or not).
27+
*/
28+
const canAcceptJobs = (workspaceStatus: WorkspaceStatus) =>
29+
["started", "stopped", "deleted", "error", "canceled"].includes(workspaceStatus)
30+
31+
export interface WorkspaceActionsProps {
32+
workspace: Workspace
33+
workspaceStatus: WorkspaceStatus
34+
handleStart: () => void
35+
handleStop: () => void
36+
handleRetry: () => void
37+
handleUpdate: () => void
38+
}
39+
40+
export const WorkspaceActions: React.FC<WorkspaceActionsProps> = ({
41+
workspace,
42+
workspaceStatus,
43+
handleStart,
44+
handleStop,
45+
handleRetry,
46+
handleUpdate,
47+
}) => {
48+
const styles = useStyles()
49+
50+
return (
51+
<Stack direction="row" spacing={1}>
52+
<Link underline="none" component={RouterLink} to="edit">
53+
<Button variant="outlined">Settings</Button>
54+
</Link>
55+
{(workspaceStatus === "started" || workspaceStatus === "stopping") && (
56+
<WorkspaceActionButton
57+
className={styles.actionButton}
58+
icon={<StopIcon />}
59+
onClick={handleStop}
60+
label={Language.stop}
61+
loadingLabel={Language.stopping}
62+
isLoading={workspaceStatus === "stopping"}
63+
/>
64+
)}
65+
{(workspaceStatus === "stopped" || workspaceStatus === "starting") && (
66+
<WorkspaceActionButton
67+
className={styles.actionButton}
68+
icon={<PlayArrowRoundedIcon />}
69+
onClick={handleStart}
70+
label={Language.start}
71+
loadingLabel={Language.starting}
72+
isLoading={workspaceStatus === "starting"}
73+
/>
74+
)}
75+
{workspaceStatus === "error" && (
76+
<Button className={styles.actionButton} startIcon={<ReplayIcon />} onClick={handleRetry}>
77+
{Language.retry}
78+
</Button>
79+
)}
80+
{workspace.outdated && canAcceptJobs(workspaceStatus) && (
81+
<Button className={styles.actionButton} startIcon={<CloudDownloadIcon />} onClick={handleUpdate}>
82+
{Language.update}
83+
</Button>
84+
)}
85+
</Stack>
86+
)
87+
}
88+
89+
const useStyles = makeStyles((theme) => ({
90+
actionButton: {
91+
// Set fixed width for the action buttons so they will not change the size
92+
// during the transitions
93+
width: theme.spacing(30),
94+
},
95+
}))

site/src/components/WorkspaceBuildStats/WorkspaceBuildStats.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { makeStyles, useTheme } from "@material-ui/core/styles"
33
import React from "react"
44
import { Link as RouterLink } from "react-router-dom"
55
import { WorkspaceBuild } from "../../api/typesGenerated"
6-
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
6+
import { CardRadius, MONOSPACE_FONT_FAMILY } from "../../theme/constants"
77
import { combineClasses } from "../../util/combineClasses"
88
import { displayWorkspaceBuildDuration, getDisplayStatus } from "../../util/workspace"
99

@@ -57,17 +57,21 @@ export const WorkspaceBuildStats: React.FC<WorkspaceBuildStatsProps> = ({ build
5757

5858
const useStyles = makeStyles((theme) => ({
5959
stats: {
60-
paddingTop: theme.spacing(3),
61-
paddingBottom: theme.spacing(3),
60+
paddingLeft: theme.spacing(2),
61+
paddingRight: theme.spacing(2),
62+
backgroundColor: theme.palette.background.paper,
63+
borderRadius: CardRadius,
6264
display: "flex",
6365
alignItems: "center",
6466
color: theme.palette.text.secondary,
6567
fontFamily: MONOSPACE_FONT_FAMILY,
68+
border: `1px solid ${theme.palette.divider}`,
6669
},
6770

6871
statItem: {
6972
minWidth: theme.spacing(20),
70-
paddingRight: theme.spacing(3),
73+
padding: theme.spacing(2),
74+
paddingTop: theme.spacing(1.75),
7175
},
7276

7377
statsLabel: {
@@ -80,14 +84,14 @@ const useStyles = makeStyles((theme) => ({
8084
statsValue: {
8185
fontSize: 16,
8286
marginTop: theme.spacing(0.25),
83-
display: "block",
87+
display: "inline-block",
8488
},
8589

8690
statsDivider: {
8791
width: 1,
8892
height: theme.spacing(5),
8993
backgroundColor: theme.palette.divider,
90-
marginRight: theme.spacing(3),
94+
marginRight: theme.spacing(2),
9195
},
9296

9397
capitalize: {

0 commit comments

Comments
 (0)