Skip to content

Commit 8bcf23e

Browse files
authored
fix: handle create workspace errors (#3346)
1 parent 83c63d4 commit 8bcf23e

File tree

5 files changed

+143
-13
lines changed

5 files changed

+143
-13
lines changed

coderd/workspaces.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
329329
Message: fmt.Sprintf("Workspace %q already exists in the %q template.", createWorkspace.Name, template.Name),
330330
Validations: []codersdk.ValidationError{{
331331
Field: "name",
332-
Detail: "this value is already in use and should be unique",
332+
Detail: "This value is already in use and should be unique.",
333333
}},
334334
})
335335
return

site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useNavigate, useParams } from "react-router-dom"
55
import { useOrganizationId } from "../../hooks/useOrganizationId"
66
import { pageTitle } from "../../util/page"
77
import { createWorkspaceMachine } from "../../xServices/createWorkspace/createWorkspaceXService"
8-
import { CreateWorkspacePageView } from "./CreateWorkspacePageView"
8+
import { CreateWorkspaceErrors, CreateWorkspacePageView } from "./CreateWorkspacePageView"
99

1010
const CreateWorkspacePage: FC = () => {
1111
const organizationId = useOrganizationId()
@@ -21,6 +21,15 @@ const CreateWorkspacePage: FC = () => {
2121
},
2222
})
2323

24+
const {
25+
templates,
26+
templateSchema,
27+
selectedTemplate,
28+
getTemplateSchemaError,
29+
getTemplatesError,
30+
createWorkspaceError,
31+
} = createWorkspaceState.context
32+
2433
return (
2534
<>
2635
<Helmet>
@@ -30,10 +39,16 @@ const CreateWorkspacePage: FC = () => {
3039
loadingTemplates={createWorkspaceState.matches("gettingTemplates")}
3140
loadingTemplateSchema={createWorkspaceState.matches("gettingTemplateSchema")}
3241
creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")}
33-
templateName={createWorkspaceState.context.templateName}
34-
templates={createWorkspaceState.context.templates}
35-
selectedTemplate={createWorkspaceState.context.selectedTemplate}
36-
templateSchema={createWorkspaceState.context.templateSchema}
42+
hasTemplateErrors={createWorkspaceState.matches("error")}
43+
templateName={templateName}
44+
templates={templates}
45+
selectedTemplate={selectedTemplate}
46+
templateSchema={templateSchema}
47+
createWorkspaceErrors={{
48+
[CreateWorkspaceErrors.GET_TEMPLATES_ERROR]: getTemplatesError,
49+
[CreateWorkspaceErrors.GET_TEMPLATE_SCHEMA_ERROR]: getTemplateSchemaError,
50+
[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]: createWorkspaceError,
51+
}}
3752
onCancel={() => {
3853
navigate("/templates")
3954
}}

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

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { ComponentMeta, Story } from "@storybook/react"
22
import { ParameterSchema } from "../../api/typesGenerated"
3-
import { MockTemplate } from "../../testHelpers/entities"
4-
import { CreateWorkspacePageView, CreateWorkspacePageViewProps } from "./CreateWorkspacePageView"
3+
import { makeMockApiError, MockTemplate } from "../../testHelpers/entities"
4+
import {
5+
CreateWorkspaceErrors,
6+
CreateWorkspacePageView,
7+
CreateWorkspacePageViewProps,
8+
} from "./CreateWorkspacePageView"
59

610
const createParameterSchema = (partial: Partial<ParameterSchema>): ParameterSchema => {
711
return {
@@ -40,6 +44,7 @@ NoParameters.args = {
4044
templates: [MockTemplate],
4145
selectedTemplate: MockTemplate,
4246
templateSchema: [],
47+
createWorkspaceErrors: {},
4348
}
4449

4550
export const Parameters = Template.bind({})
@@ -60,4 +65,48 @@ Parameters.args = {
6065
validation_contains: ["Small", "Medium", "Big"],
6166
}),
6267
],
68+
createWorkspaceErrors: {},
69+
}
70+
71+
export const GetTemplatesError = Template.bind({})
72+
GetTemplatesError.args = {
73+
...Parameters.args,
74+
createWorkspaceErrors: {
75+
[CreateWorkspaceErrors.GET_TEMPLATES_ERROR]: makeMockApiError({
76+
message: "Failed to fetch templates.",
77+
detail: "You do not have permission to access this resource.",
78+
}),
79+
},
80+
hasTemplateErrors: true,
81+
}
82+
83+
export const GetTemplateSchemaError = Template.bind({})
84+
GetTemplateSchemaError.args = {
85+
...Parameters.args,
86+
createWorkspaceErrors: {
87+
[CreateWorkspaceErrors.GET_TEMPLATE_SCHEMA_ERROR]: makeMockApiError({
88+
message: 'Failed to fetch template schema for "docker-amd64".',
89+
detail: "You do not have permission to access this resource.",
90+
}),
91+
},
92+
hasTemplateErrors: true,
93+
}
94+
95+
export const CreateWorkspaceError = Template.bind({})
96+
CreateWorkspaceError.args = {
97+
...Parameters.args,
98+
createWorkspaceErrors: {
99+
[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]: makeMockApiError({
100+
message: 'Workspace "test" already exists in the "docker-amd64" template.',
101+
validations: [
102+
{
103+
field: "name",
104+
detail: "This value is already in use and should be unique.",
105+
},
106+
],
107+
}),
108+
},
109+
initialTouched: {
110+
name: true,
111+
},
63112
}

site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { makeStyles } from "@material-ui/core/styles"
22
import TextField from "@material-ui/core/TextField"
3-
import { FormikContextType, useFormik } from "formik"
3+
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
4+
import { FormikContextType, FormikTouched, useFormik } from "formik"
45
import { FC, useState } from "react"
56
import * as Yup from "yup"
67
import * as TypesGen from "../../api/typesGenerated"
@@ -9,23 +10,33 @@ import { FullPageForm } from "../../components/FullPageForm/FullPageForm"
910
import { Loader } from "../../components/Loader/Loader"
1011
import { ParameterInput } from "../../components/ParameterInput/ParameterInput"
1112
import { Stack } from "../../components/Stack/Stack"
12-
import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils"
13+
import { getFormHelpersWithError, nameValidator, onChangeTrimmed } from "../../util/formUtils"
1314

1415
export const Language = {
1516
templateLabel: "Template",
1617
nameLabel: "Name",
1718
}
1819

20+
export enum CreateWorkspaceErrors {
21+
GET_TEMPLATES_ERROR = "getTemplatesError",
22+
GET_TEMPLATE_SCHEMA_ERROR = "getTemplateSchemaError",
23+
CREATE_WORKSPACE_ERROR = "createWorkspaceError",
24+
}
25+
1926
export interface CreateWorkspacePageViewProps {
2027
loadingTemplates: boolean
2128
loadingTemplateSchema: boolean
2229
creatingWorkspace: boolean
30+
hasTemplateErrors: boolean
2331
templateName: string
2432
templates?: TypesGen.Template[]
2533
selectedTemplate?: TypesGen.Template
2634
templateSchema?: TypesGen.ParameterSchema[]
35+
createWorkspaceErrors: Partial<Record<CreateWorkspaceErrors, Error | unknown>>
2736
onCancel: () => void
2837
onSubmit: (req: TypesGen.CreateWorkspaceRequest) => void
38+
// initialTouched is only used for testing the error state of the form.
39+
initialTouched?: FormikTouched<TypesGen.CreateWorkspaceRequest>
2940
}
3041

3142
export const validationSchema = Yup.object({
@@ -44,6 +55,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props)
4455
},
4556
enableReinitialize: true,
4657
validationSchema,
58+
initialTouched: props.initialTouched,
4759
onSubmit: (request) => {
4860
if (!props.templateSchema) {
4961
throw new Error("No template schema loaded")
@@ -62,18 +74,45 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props)
6274
source_value: value,
6375
})
6476
})
65-
return props.onSubmit({
77+
props.onSubmit({
6678
...request,
6779
parameter_values: createRequests,
6880
})
81+
form.setSubmitting(false)
6982
},
7083
})
71-
const getFieldHelpers = getFormHelpers<TypesGen.CreateWorkspaceRequest>(form)
84+
85+
const getFieldHelpers = getFormHelpersWithError<TypesGen.CreateWorkspaceRequest>(
86+
form,
87+
props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR],
88+
)
89+
90+
if (props.hasTemplateErrors) {
91+
return (
92+
<Stack>
93+
{props.createWorkspaceErrors[CreateWorkspaceErrors.GET_TEMPLATES_ERROR] && (
94+
<ErrorSummary
95+
error={props.createWorkspaceErrors[CreateWorkspaceErrors.GET_TEMPLATES_ERROR]}
96+
/>
97+
)}
98+
{props.createWorkspaceErrors[CreateWorkspaceErrors.GET_TEMPLATE_SCHEMA_ERROR] && (
99+
<ErrorSummary
100+
error={props.createWorkspaceErrors[CreateWorkspaceErrors.GET_TEMPLATE_SCHEMA_ERROR]}
101+
/>
102+
)}
103+
</Stack>
104+
)
105+
}
72106

73107
return (
74108
<FullPageForm title="Create workspace" onCancel={props.onCancel}>
75109
<form onSubmit={form.handleSubmit}>
76110
<Stack>
111+
{props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR] && (
112+
<ErrorSummary
113+
error={props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]}
114+
/>
115+
)}
77116
<TextField
78117
disabled
79118
fullWidth

site/src/xServices/createWorkspace/createWorkspaceXService.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ type CreateWorkspaceContext = {
1515
templateSchema?: ParameterSchema[]
1616
createWorkspaceRequest?: CreateWorkspaceRequest
1717
createdWorkspace?: Workspace
18+
createWorkspaceError?: Error | unknown
19+
getTemplatesError?: Error | unknown
20+
getTemplateSchemaError?: Error | unknown
1821
}
1922

2023
type CreateWorkspaceEvent = {
@@ -44,6 +47,7 @@ export const createWorkspaceMachine = createMachine(
4447
tsTypes: {} as import("./createWorkspaceXService.typegen").Typegen0,
4548
states: {
4649
gettingTemplates: {
50+
entry: "clearGetTemplatesError",
4751
invoke: {
4852
src: "getTemplates",
4953
onDone: [
@@ -57,18 +61,21 @@ export const createWorkspaceMachine = createMachine(
5761
},
5862
],
5963
onError: {
64+
actions: ["assignGetTemplatesError"],
6065
target: "error",
6166
},
6267
},
6368
},
6469
gettingTemplateSchema: {
70+
entry: "clearGetTemplateSchemaError",
6571
invoke: {
6672
src: "getTemplateSchema",
6773
onDone: {
6874
actions: ["assignTemplateSchema"],
6975
target: "fillingParams",
7076
},
7177
onError: {
78+
actions: ["assignGetTemplateSchemaError"],
7279
target: "error",
7380
},
7481
},
@@ -82,14 +89,16 @@ export const createWorkspaceMachine = createMachine(
8289
},
8390
},
8491
creatingWorkspace: {
92+
entry: "clearCreateWorkspaceError",
8593
invoke: {
8694
src: "createWorkspace",
8795
onDone: {
8896
actions: ["onCreateWorkspace"],
8997
target: "created",
9098
},
9199
onError: {
92-
target: "error",
100+
actions: ["assignCreateWorkspaceError"],
101+
target: "fillingParams",
93102
},
94103
},
95104
},
@@ -142,6 +151,24 @@ export const createWorkspaceMachine = createMachine(
142151
assignCreateWorkspaceRequest: assign({
143152
createWorkspaceRequest: (_, event) => event.request,
144153
}),
154+
assignCreateWorkspaceError: assign({
155+
createWorkspaceError: (_, event) => event.data,
156+
}),
157+
clearCreateWorkspaceError: assign({
158+
createWorkspaceError: (_) => undefined,
159+
}),
160+
assignGetTemplatesError: assign({
161+
getTemplatesError: (_, event) => event.data,
162+
}),
163+
clearGetTemplatesError: assign({
164+
getTemplatesError: (_) => undefined,
165+
}),
166+
assignGetTemplateSchemaError: assign({
167+
getTemplateSchemaError: (_, event) => event.data,
168+
}),
169+
clearGetTemplateSchemaError: assign({
170+
getTemplateSchemaError: (_) => undefined,
171+
}),
145172
},
146173
},
147174
)

0 commit comments

Comments
 (0)