Skip to content

Commit ed2207f

Browse files
BrunoQuaresmakylecarbs
authored andcommitted
feat: Improve empty states for workspaces and templates (#1950)
1 parent b41750d commit ed2207f

File tree

11 files changed

+196
-89
lines changed

11 files changed

+196
-89
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import { makeStyles } from "@material-ui/core/styles"
22
import { FC } from "react"
33
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
4+
import { combineClasses } from "../../util/combineClasses"
45
import { CopyButton } from "../CopyButton/CopyButton"
56

67
export interface CodeExampleProps {
78
code: string
9+
className?: string
10+
buttonClassName?: string
811
}
912

1013
/**
1114
* Component to show single-line code examples, with a copy button
1215
*/
13-
export const CodeExample: FC<CodeExampleProps> = ({ code }) => {
16+
export const CodeExample: FC<CodeExampleProps> = ({ code, className, buttonClassName }) => {
1417
const styles = useStyles()
1518

1619
return (
17-
<div className={styles.root}>
18-
<code>{code}</code>
19-
<CopyButton text={code} />
20+
<div className={combineClasses([styles.root, className])}>
21+
<code className={styles.code}>{code}</code>
22+
<CopyButton text={code} buttonClassName={combineClasses([styles.button, buttonClassName])} />
2023
</div>
2124
)
2225
}
@@ -30,8 +33,17 @@ const useStyles = makeStyles((theme) => ({
3033
background: theme.palette.background.default,
3134
color: theme.palette.primary.contrastText,
3235
fontFamily: MONOSPACE_FONT_FAMILY,
33-
fontSize: 13,
34-
padding: theme.spacing(2),
36+
fontSize: 14,
37+
borderRadius: theme.shape.borderRadius,
38+
padding: theme.spacing(0.5),
39+
},
40+
code: {
41+
padding: `${theme.spacing(0.5)}px ${theme.spacing(0.75)}px ${theme.spacing(0.5)}px ${theme.spacing(2)}px`,
42+
},
43+
button: {
44+
border: 0,
45+
minWidth: 42,
46+
minHeight: 42,
3547
borderRadius: theme.shape.borderRadius,
3648
},
3749
}))

site/src/components/EmptyState/EmptyState.tsx

+17-6
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import Box from "@material-ui/core/Box"
22
import { makeStyles } from "@material-ui/core/styles"
33
import Typography from "@material-ui/core/Typography"
44
import { FC, ReactNode } from "react"
5+
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
6+
import { combineClasses } from "../../util/combineClasses"
57

68
export interface EmptyStateProps {
79
/** Text Message to display, placed inside Typography component */
810
message: string
911
/** Longer optional description to display below the message */
10-
description?: string
12+
description?: string | React.ReactNode
13+
descriptionClassName?: string
1114
cta?: ReactNode
15+
className?: string
1216
}
1317

1418
/**
@@ -20,17 +24,21 @@ export interface EmptyStateProps {
2024
* that you can directly pass props through to to customize the shape and layout of it.
2125
*/
2226
export const EmptyState: FC<EmptyStateProps> = (props) => {
23-
const { message, description, cta, ...boxProps } = props
27+
const { message, description, cta, descriptionClassName, className, ...boxProps } = props
2428
const styles = useStyles()
2529

2630
return (
27-
<Box className={styles.root} {...boxProps}>
31+
<Box className={combineClasses([styles.root, className])} {...boxProps}>
2832
<div className={styles.header}>
2933
<Typography variant="h5" className={styles.title}>
3034
{message}
3135
</Typography>
3236
{description && (
33-
<Typography variant="body2" color="textSecondary" className={styles.description}>
37+
<Typography
38+
variant="body2"
39+
color="textSecondary"
40+
className={combineClasses([styles.description, descriptionClassName])}
41+
>
3442
{description}
3543
</Typography>
3644
)}
@@ -48,17 +56,20 @@ const useStyles = makeStyles(
4856
justifyContent: "center",
4957
alignItems: "center",
5058
textAlign: "center",
51-
minHeight: 120,
59+
minHeight: 300,
5260
padding: theme.spacing(3),
61+
fontFamily: MONOSPACE_FONT_FAMILY,
5362
},
5463
header: {
5564
marginBottom: theme.spacing(3),
5665
},
5766
title: {
58-
fontWeight: 400,
67+
fontWeight: 600,
68+
fontFamily: "inherit",
5969
},
6070
description: {
6171
marginTop: theme.spacing(1),
72+
fontFamily: "inherit",
6273
},
6374
}),
6475
{ name: "EmptyState" },

site/src/components/Footer/Footer.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ export const Footer: React.FC = ({ children }) => {
2727
</div>
2828
{buildInfoState.context.buildInfo && (
2929
<div className={styles.buildInfo}>
30-
<Link variant="caption" target="_blank" href={buildInfoState.context.buildInfo.external_url}>
30+
<Link
31+
className={styles.link}
32+
variant="caption"
33+
target="_blank"
34+
href={buildInfoState.context.buildInfo.external_url}
35+
>
3136
{Language.buildInfoText(buildInfoState.context.buildInfo)}
3237
</Link>
3338
</div>
@@ -38,6 +43,7 @@ export const Footer: React.FC = ({ children }) => {
3843

3944
const useFooterStyles = makeStyles((theme) => ({
4045
root: {
46+
opacity: 0.6,
4147
textAlign: "center",
4248
flex: "0",
4349
paddingTop: theme.spacing(2),
@@ -50,4 +56,8 @@ const useFooterStyles = makeStyles((theme) => ({
5056
buildInfo: {
5157
margin: theme.spacing(0.25),
5258
},
59+
link: {
60+
color: theme.palette.text.secondary,
61+
fontWeight: 600,
62+
},
5363
}))

site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ export default {
3333

3434
const Template: Story<CreateWorkspacePageViewProps> = (args) => <CreateWorkspacePageView {...args} />
3535

36+
export const NoTemplates = Template.bind({})
37+
NoTemplates.args = {
38+
templates: [],
39+
}
40+
3641
export const NoParameters = Template.bind({})
3742
NoParameters.args = {
3843
templates: [MockTemplate],

site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx

+43-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { FC, useState } from "react"
88
import { Link as RouterLink } from "react-router-dom"
99
import * as Yup from "yup"
1010
import * as TypesGen from "../../api/typesGenerated"
11+
import { CodeExample } from "../../components/CodeExample/CodeExample"
12+
import { EmptyState } from "../../components/EmptyState/EmptyState"
1113
import { FormFooter } from "../../components/FormFooter/FormFooter"
1214
import { FullPageForm } from "../../components/FullPageForm/FullPageForm"
1315
import { Loader } from "../../components/Loader/Loader"
@@ -18,6 +20,17 @@ import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formU
1820
export const Language = {
1921
templateLabel: "Template",
2022
nameLabel: "Name",
23+
emptyMessage: "Let's create your first template",
24+
emptyDescription: (
25+
<>
26+
To create a workspace you need to have a template. You can{" "}
27+
<Link target="_blank" href="https://github.com/coder/coder/blob/main/docs/templates.md">
28+
create one from scratch
29+
</Link>{" "}
30+
or use a built-in template by typing the following Coder CLI command:
31+
</>
32+
),
33+
templateLink: "Read more about this template",
2134
}
2235

2336
export interface CreateWorkspacePageViewProps {
@@ -98,7 +111,18 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props)
98111
{props.loadingTemplates && <Loader />}
99112

100113
<Stack>
101-
{props.templates && (
114+
{props.templates && props.templates.length === 0 && (
115+
<EmptyState
116+
className={styles.emptyState}
117+
message={Language.emptyMessage}
118+
description={Language.emptyDescription}
119+
descriptionClassName={styles.emptyStateDescription}
120+
cta={
121+
<CodeExample className={styles.code} buttonClassName={styles.codeButton} code="coder template init" />
122+
}
123+
/>
124+
)}
125+
{props.templates && props.templates.length > 0 && (
102126
<TextField
103127
{...getFieldHelpers("template_id")}
104128
disabled={form.isSubmitting}
@@ -116,7 +140,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props)
116140
to={`/templates/${selectedTemplate.name}`}
117141
target="_blank"
118142
>
119-
Read more about this template <OpenInNewIcon />
143+
{Language.templateLink} <OpenInNewIcon />
120144
</Link>
121145
)
122146
}
@@ -179,4 +203,21 @@ const useStyles = makeStyles((theme) => ({
179203
marginLeft: theme.spacing(0.5),
180204
},
181205
},
206+
emptyState: {
207+
padding: 0,
208+
fontFamily: "inherit",
209+
textAlign: "left",
210+
minHeight: "auto",
211+
alignItems: "flex-start",
212+
},
213+
emptyStateDescription: {
214+
lineHeight: "160%",
215+
},
216+
code: {
217+
background: theme.palette.background.paper,
218+
width: "100%",
219+
},
220+
codeButton: {
221+
background: theme.palette.background.paper,
222+
},
182223
}))

site/src/pages/TemplatesPage/TemplatesPage.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe("TemplatesPage", () => {
3131
render(<TemplatesPage />)
3232

3333
// Then
34-
await screen.findByText(Language.emptyViewCreate)
34+
await screen.findByText(Language.emptyMessage)
3535
})
3636

3737
it("renders a filled templates page", async () => {

site/src/pages/TemplatesPage/TemplatesPageView.tsx

+22-30
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import TableRow from "@material-ui/core/TableRow"
88
import dayjs from "dayjs"
99
import relativeTime from "dayjs/plugin/relativeTime"
1010
import { FC } from "react"
11-
import { Link as RouterLink } from "react-router-dom"
1211
import * as TypesGen from "../../api/typesGenerated"
1312
import { AvatarData } from "../../components/AvatarData/AvatarData"
13+
import { CodeExample } from "../../components/CodeExample/CodeExample"
14+
import { EmptyState } from "../../components/EmptyState/EmptyState"
1415
import { Margins } from "../../components/Margins/Margins"
1516
import { Stack } from "../../components/Stack/Stack"
1617
import { TableLoader } from "../../components/TableLoader/TableLoader"
@@ -24,9 +25,17 @@ export const Language = {
2425
nameLabel: "Name",
2526
usedByLabel: "Used by",
2627
lastUpdatedLabel: "Last updated",
27-
emptyViewCreateCTA: "Create a template",
28-
emptyViewCreate: "to standardize development workspaces for your team.",
29-
emptyViewNoPerms: "No templates have been created! Contact your Coder administrator.",
28+
emptyViewNoPerms: "Contact your Coder administrator to create a template. You can share the code below.",
29+
emptyMessage: "Create your first template",
30+
emptyDescription: (
31+
<>
32+
To create a workspace you need to have a template. You can{" "}
33+
<Link target="_blank" href="https://github.com/coder/coder/blob/main/docs/templates.md">
34+
create one from scratch
35+
</Link>{" "}
36+
or use a built-in template using the following Coder CLI command:
37+
</>
38+
),
3039
}
3140

3241
export interface TemplatesPageViewProps {
@@ -53,18 +62,12 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = (props) => {
5362
{!props.loading && !props.templates?.length && (
5463
<TableRow>
5564
<TableCell colSpan={999}>
56-
<div className={styles.welcome}>
57-
{props.canCreateTemplate ? (
58-
<span>
59-
<Link component={RouterLink} to="/templates/new">
60-
{Language.emptyViewCreateCTA}
61-
</Link>
62-
&nbsp;{Language.emptyViewCreate}
63-
</span>
64-
) : (
65-
<span>{Language.emptyViewNoPerms}</span>
66-
)}
67-
</div>
65+
<EmptyState
66+
message={Language.emptyMessage}
67+
description={props.canCreateTemplate ? Language.emptyDescription : Language.emptyViewNoPerms}
68+
descriptionClassName={styles.emptyDescription}
69+
cta={<CodeExample code="coder template init" />}
70+
/>
6871
</TableCell>
6972
</TableRow>
7073
)}
@@ -92,20 +95,9 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = (props) => {
9295

9396
const useStyles = makeStyles((theme) => ({
9497
root: {
95-
marginTop: theme.spacing(3),
98+
marginTop: theme.spacing(10),
9699
},
97-
welcome: {
98-
paddingTop: theme.spacing(12),
99-
paddingBottom: theme.spacing(12),
100-
display: "flex",
101-
flexDirection: "column",
102-
alignItems: "center",
103-
justifyContent: "center",
104-
"& span": {
105-
maxWidth: 600,
106-
textAlign: "center",
107-
fontSize: theme.spacing(2),
108-
lineHeight: `${theme.spacing(3)}px`,
109-
},
100+
emptyDescription: {
101+
maxWidth: theme.spacing(62),
110102
},
111103
}))

site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe("WorkspacesPage", () => {
2323
render(<WorkspacesPage />)
2424

2525
// Then
26-
await screen.findByText(Language.emptyView)
26+
await screen.findByText(Language.emptyMessage)
2727
})
2828

2929
it("renders a filled workspaces page", async () => {

site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ComponentMeta, Story } from "@storybook/react"
2-
import { ProvisionerJobStatus, Workspace } from "../../api/typesGenerated"
2+
import { ProvisionerJobStatus, Workspace, WorkspaceTransition } from "../../api/typesGenerated"
33
import { MockWorkspace } from "../../testHelpers/entities"
44
import { WorkspacesPageView, WorkspacesPageViewProps } from "./WorkspacesPageView"
55

@@ -10,7 +10,10 @@ export default {
1010

1111
const Template: Story<WorkspacesPageViewProps> = (args) => <WorkspacesPageView {...args} />
1212

13-
const createWorkspaceWithStatus = (status: ProvisionerJobStatus, transition = "start"): Workspace => {
13+
const createWorkspaceWithStatus = (
14+
status: ProvisionerJobStatus,
15+
transition: WorkspaceTransition = "start",
16+
): Workspace => {
1417
return {
1518
...MockWorkspace,
1619
latest_build: {
@@ -46,4 +49,6 @@ AllStates.args = {
4649
}
4750

4851
export const Empty = Template.bind({})
49-
Empty.args = {}
52+
Empty.args = {
53+
workspaces: [],
54+
}

0 commit comments

Comments
 (0)