Skip to content

Commit b7234a6

Browse files
authored
fix: push create workspace UX to templates page (coder#2142)
1 parent 119db78 commit b7234a6

File tree

10 files changed

+79
-210
lines changed

10 files changed

+79
-210
lines changed

site/src/AppRouter.tsx

+18-17
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,6 @@ export const AppRouter: FC = () => (
5656
</AuthAndFrame>
5757
}
5858
/>
59-
60-
<Route
61-
path="new"
62-
element={
63-
<RequireAuth>
64-
<CreateWorkspacePage />
65-
</RequireAuth>
66-
}
67-
/>
6859
</Route>
6960

7061
<Route path="templates">
@@ -77,14 +68,24 @@ export const AppRouter: FC = () => (
7768
}
7869
/>
7970

80-
<Route
81-
path=":template"
82-
element={
83-
<AuthAndFrame>
84-
<TemplatePage />
85-
</AuthAndFrame>
86-
}
87-
/>
71+
<Route path=":template">
72+
<Route
73+
index
74+
element={
75+
<AuthAndFrame>
76+
<TemplatePage />
77+
</AuthAndFrame>
78+
}
79+
/>
80+
<Route
81+
path="workspace"
82+
element={
83+
<RequireAuth>
84+
<CreateWorkspacePage />
85+
</RequireAuth>
86+
}
87+
/>
88+
</Route>
8889
</Route>
8990

9091
<Route path="users">

site/src/components/PageHeader/PageHeader.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ export const PageHeaderSubtitle: React.FC = ({ children }) => {
3232
return <h2 className={styles.subtitle}>{children}</h2>
3333
}
3434

35+
export const PageHeaderText: React.FC = ({ children }) => {
36+
const styles = useStyles()
37+
38+
return <h3 className={styles.text}>{children}</h3>
39+
}
40+
3541
const useStyles = makeStyles((theme) => ({
3642
root: {
3743
display: "flex",
@@ -58,6 +64,15 @@ const useStyles = makeStyles((theme) => ({
5864
marginTop: theme.spacing(1),
5965
},
6066

67+
text: {
68+
fontSize: theme.spacing(2),
69+
color: theme.palette.text.secondary,
70+
fontWeight: 400,
71+
display: "block",
72+
margin: 0,
73+
marginTop: theme.spacing(1),
74+
},
75+
6176
actions: {
6277
marginLeft: "auto",
6378
},

site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx

+2-10
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ import * as API from "../../api/api"
44
import { Language as FooterLanguage } from "../../components/FormFooter/FormFooter"
55
import { MockTemplate, MockWorkspace } from "../../testHelpers/entities"
66
import { renderWithAuth } from "../../testHelpers/renderHelpers"
7-
import { Language as FormLanguage } from "../../util/formUtils"
87
import CreateWorkspacePage from "./CreateWorkspacePage"
98
import { Language } from "./CreateWorkspacePageView"
109

1110
const renderCreateWorkspacePage = () => {
1211
return renderWithAuth(<CreateWorkspacePage />, {
13-
route: "/workspaces/new?template=" + MockTemplate.name,
14-
path: "/workspaces/new",
12+
route: "/templates/" + MockTemplate.name + "/workspace",
13+
path: "/templates/:template/workspace",
1514
})
1615
}
1716

@@ -29,13 +28,6 @@ describe("CreateWorkspacePage", () => {
2928
expect(element).toBeDefined()
3029
})
3130

32-
it("shows validation error message", async () => {
33-
renderCreateWorkspacePage()
34-
await fillForm({ name: "$$$" })
35-
const errorMessage = await screen.findByText(FormLanguage.nameInvalidChars(Language.nameLabel))
36-
expect(errorMessage).toBeDefined()
37-
})
38-
3931
it("succeeds", async () => {
4032
renderCreateWorkspacePage()
4133
// You have to spy the method before it is used.

site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx

+6-12
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
import { useMachine } from "@xstate/react"
22
import { FC } from "react"
33
import { Helmet } from "react-helmet"
4-
import { useNavigate, useSearchParams } from "react-router-dom"
5-
import { Template } from "../../api/typesGenerated"
4+
import { useNavigate, useParams } from "react-router-dom"
65
import { useOrganizationId } from "../../hooks/useOrganizationId"
76
import { pageTitle } from "../../util/page"
87
import { createWorkspaceMachine } from "../../xServices/createWorkspace/createWorkspaceXService"
98
import { CreateWorkspacePageView } from "./CreateWorkspacePageView"
109

1110
const CreateWorkspacePage: FC = () => {
1211
const organizationId = useOrganizationId()
13-
const [searchParams] = useSearchParams()
14-
const preSelectedTemplateName = searchParams.get("template")
12+
const { template } = useParams()
13+
const templateName = template ? template : ""
1514
const navigate = useNavigate()
1615
const [createWorkspaceState, send] = useMachine(createWorkspaceMachine, {
17-
context: { organizationId, preSelectedTemplateName },
16+
context: { organizationId, templateName },
1817
actions: {
1918
onCreateWorkspace: (_, event) => {
2019
navigate(`/@${event.data.owner_name}/${event.data.name}`)
@@ -31,24 +30,19 @@ const CreateWorkspacePage: FC = () => {
3130
loadingTemplates={createWorkspaceState.matches("gettingTemplates")}
3231
loadingTemplateSchema={createWorkspaceState.matches("gettingTemplateSchema")}
3332
creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")}
33+
templateName={createWorkspaceState.context.templateName}
3434
templates={createWorkspaceState.context.templates}
3535
selectedTemplate={createWorkspaceState.context.selectedTemplate}
3636
templateSchema={createWorkspaceState.context.templateSchema}
3737
onCancel={() => {
38-
navigate(preSelectedTemplateName ? "/templates" : "/workspaces")
38+
navigate("/templates")
3939
}}
4040
onSubmit={(request) => {
4141
send({
4242
type: "CREATE_WORKSPACE",
4343
request,
4444
})
4545
}}
46-
onSelectTemplate={(template: Template) => {
47-
send({
48-
type: "SELECT_TEMPLATE",
49-
template,
50-
})
51-
}}
5246
/>
5347
</>
5448
)

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

-5
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,6 @@ 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-
4136
export const NoParameters = Template.bind({})
4237
NoParameters.args = {
4338
templates: [MockTemplate],

site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx

+13-84
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
import Link from "@material-ui/core/Link"
2-
import MenuItem from "@material-ui/core/MenuItem"
31
import { makeStyles } from "@material-ui/core/styles"
4-
import TextField, { TextFieldProps } from "@material-ui/core/TextField"
5-
import OpenInNewIcon from "@material-ui/icons/OpenInNew"
2+
import TextField from "@material-ui/core/TextField"
63
import { FormikContextType, useFormik } from "formik"
74
import { FC, useState } from "react"
8-
import { Link as RouterLink } from "react-router-dom"
95
import * as Yup from "yup"
106
import * as TypesGen from "../../api/typesGenerated"
11-
import { CodeExample } from "../../components/CodeExample/CodeExample"
12-
import { EmptyState } from "../../components/EmptyState/EmptyState"
137
import { FormFooter } from "../../components/FormFooter/FormFooter"
148
import { FullPageForm } from "../../components/FullPageForm/FullPageForm"
159
import { Loader } from "../../components/Loader/Loader"
@@ -20,29 +14,18 @@ import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formU
2014
export const Language = {
2115
templateLabel: "Template",
2216
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",
3417
}
3518

3619
export interface CreateWorkspacePageViewProps {
3720
loadingTemplates: boolean
3821
loadingTemplateSchema: boolean
3922
creatingWorkspace: boolean
23+
templateName: string
4024
templates?: TypesGen.Template[]
4125
selectedTemplate?: TypesGen.Template
4226
templateSchema?: TypesGen.ParameterSchema[]
4327
onCancel: () => void
4428
onSubmit: (req: TypesGen.CreateWorkspaceRequest) => void
45-
onSelectTemplate: (template: TypesGen.Template) => void
4629
}
4730

4831
export const validationSchema = Yup.object({
@@ -51,7 +34,8 @@ export const validationSchema = Yup.object({
5134

5235
export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props) => {
5336
const [parameterValues, setParameterValues] = useState<Record<string, string>>({})
54-
const styles = useStyles()
37+
useStyles()
38+
5539
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> = useFormik<TypesGen.CreateWorkspaceRequest>({
5640
initialValues: {
5741
name: "",
@@ -84,75 +68,20 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props)
8468
},
8569
})
8670
const getFieldHelpers = getFormHelpers<TypesGen.CreateWorkspaceRequest>(form)
87-
const selectedTemplate =
88-
props.templates &&
89-
form.values.template_id &&
90-
props.templates.find((template) => template.id === form.values.template_id)
91-
92-
const handleTemplateChange: TextFieldProps["onChange"] = (event) => {
93-
if (!props.templates) {
94-
throw new Error("Templates are not loaded")
95-
}
96-
97-
const templateId = event.target.value
98-
const selectedTemplate = props.templates.find((template) => template.id === templateId)
99-
100-
if (!selectedTemplate) {
101-
throw new Error(`Template ${templateId} not found`)
102-
}
103-
104-
form.setFieldValue("template_id", selectedTemplate.id)
105-
props.onSelectTemplate(selectedTemplate)
106-
}
10771

10872
return (
10973
<FullPageForm title="Create workspace" onCancel={props.onCancel}>
11074
<form onSubmit={form.handleSubmit}>
111-
{props.loadingTemplates && <Loader />}
112-
11375
<Stack>
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 && (
126-
<TextField
127-
{...getFieldHelpers("template_id")}
128-
disabled={form.isSubmitting}
129-
onChange={handleTemplateChange}
130-
autoFocus
131-
fullWidth
132-
label={Language.templateLabel}
133-
variant="outlined"
134-
select
135-
helperText={
136-
selectedTemplate && (
137-
<Link
138-
className={styles.readMoreLink}
139-
component={RouterLink}
140-
to={`/templates/${selectedTemplate.name}`}
141-
target="_blank"
142-
>
143-
{Language.templateLink} <OpenInNewIcon />
144-
</Link>
145-
)
146-
}
147-
>
148-
{props.templates.map((template) => (
149-
<MenuItem key={template.id} value={template.id}>
150-
{template.name}
151-
</MenuItem>
152-
))}
153-
</TextField>
154-
)}
155-
76+
<TextField
77+
disabled
78+
fullWidth
79+
label={Language.templateLabel}
80+
value={props.selectedTemplate?.name || props.templateName}
81+
variant="outlined"
82+
/>
83+
84+
{props.loadingTemplateSchema && <Loader />}
15685
{props.selectedTemplate && props.templateSchema && (
15786
<>
15887
<TextField

site/src/pages/TemplatePage/TemplatePageView.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const TemplatePageView: FC<TemplatePageViewProps> = ({ template, activeTe
3939
<Margins>
4040
<PageHeader
4141
actions={
42-
<Link underline="none" component={RouterLink} to={`/workspaces/new?template=${template.name}`}>
42+
<Link underline="none" component={RouterLink} to={`/templates/${template.name}/workspace`}>
4343
<Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>
4444
</Link>
4545
}

site/src/pages/TemplatesPage/TemplatesPageView.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
HelpTooltipTitle,
2323
} from "../../components/HelpTooltip/HelpTooltip"
2424
import { Margins } from "../../components/Margins/Margins"
25-
import { PageHeader, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
25+
import { PageHeader, PageHeaderText, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
2626
import { Stack } from "../../components/Stack/Stack"
2727
import { TableLoader } from "../../components/TableLoader/TableLoader"
2828

@@ -84,6 +84,9 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = (props) => {
8484
<TemplateHelpTooltip />
8585
</Stack>
8686
</PageHeaderTitle>
87+
{props.templates && props.templates.length > 0 && (
88+
<PageHeaderText>Choose a template to create a new workspace.</PageHeaderText>
89+
)}
8790
</PageHeader>
8891

8992
<Table>

site/src/pages/WorkspacesPage/WorkspacesPageView.tsx

+10-8
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
HelpTooltipTitle,
3333
} from "../../components/HelpTooltip/HelpTooltip"
3434
import { Margins } from "../../components/Margins/Margins"
35-
import { PageHeader, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
35+
import { PageHeader, PageHeaderText, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
3636
import { Stack } from "../../components/Stack/Stack"
3737
import { TableLoader } from "../../components/TableLoader/TableLoader"
3838
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
@@ -41,7 +41,7 @@ import { getDisplayStatus, workspaceFilterQuery } from "../../util/workspace"
4141
dayjs.extend(relativeTime)
4242

4343
export const Language = {
44-
createWorkspaceButton: "Create workspace",
44+
createFromTemplateButton: "Create from template",
4545
emptyCreateWorkspaceMessage: "Create your first workspace",
4646
emptyCreateWorkspaceDescription: "Start editing your source code and building your software",
4747
emptyResultsMessage: "No results matched your search",
@@ -132,11 +132,13 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, works
132132
<Margins>
133133
<PageHeader
134134
actions={
135-
<Link underline="none" component={RouterLink} to="/workspaces/new">
136-
<Button startIcon={<AddCircleOutline />} style={{ height: "44px" }}>
137-
{Language.createWorkspaceButton}
138-
</Button>
139-
</Link>
135+
<PageHeaderText>
136+
Create a new workspace from a{" "}
137+
<Link component={RouterLink} to="/templates">
138+
Template
139+
</Link>
140+
.
141+
</PageHeaderText>
140142
}
141143
>
142144
<PageHeaderTitle>
@@ -213,7 +215,7 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, works
213215
description={Language.emptyCreateWorkspaceDescription}
214216
cta={
215217
<Link underline="none" component={RouterLink} to="/workspaces/new">
216-
<Button startIcon={<AddCircleOutline />}>{Language.createWorkspaceButton}</Button>
218+
<Button startIcon={<AddCircleOutline />}>{Language.createFromTemplateButton}</Button>
217219
</Link>
218220
}
219221
/>

0 commit comments

Comments
 (0)