Skip to content

Commit b7234a6

Browse files
authored
fix: push create workspace UX to templates page (#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

Lines changed: 18 additions & 17 deletions
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

Lines changed: 15 additions & 0 deletions
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

Lines changed: 2 additions & 10 deletions
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

Lines changed: 6 additions & 12 deletions
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

Lines changed: 0 additions & 5 deletions
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

Lines changed: 13 additions & 84 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 4 additions & 1 deletion
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

Lines changed: 10 additions & 8 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy