fix: handle create workspace errors (#3346)

This commit is contained in:
Abhineet Jain
2022-08-02 13:19:00 -04:00
committed by GitHub
parent 83c63d4a63
commit 8bcf23e60a
5 changed files with 143 additions and 13 deletions

View File

@ -329,7 +329,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
Message: fmt.Sprintf("Workspace %q already exists in the %q template.", createWorkspace.Name, template.Name), Message: fmt.Sprintf("Workspace %q already exists in the %q template.", createWorkspace.Name, template.Name),
Validations: []codersdk.ValidationError{{ Validations: []codersdk.ValidationError{{
Field: "name", Field: "name",
Detail: "this value is already in use and should be unique", Detail: "This value is already in use and should be unique.",
}}, }},
}) })
return return

View File

@ -5,7 +5,7 @@ import { useNavigate, useParams } from "react-router-dom"
import { useOrganizationId } from "../../hooks/useOrganizationId" import { useOrganizationId } from "../../hooks/useOrganizationId"
import { pageTitle } from "../../util/page" import { pageTitle } from "../../util/page"
import { createWorkspaceMachine } from "../../xServices/createWorkspace/createWorkspaceXService" import { createWorkspaceMachine } from "../../xServices/createWorkspace/createWorkspaceXService"
import { CreateWorkspacePageView } from "./CreateWorkspacePageView" import { CreateWorkspaceErrors, CreateWorkspacePageView } from "./CreateWorkspacePageView"
const CreateWorkspacePage: FC = () => { const CreateWorkspacePage: FC = () => {
const organizationId = useOrganizationId() const organizationId = useOrganizationId()
@ -21,6 +21,15 @@ const CreateWorkspacePage: FC = () => {
}, },
}) })
const {
templates,
templateSchema,
selectedTemplate,
getTemplateSchemaError,
getTemplatesError,
createWorkspaceError,
} = createWorkspaceState.context
return ( return (
<> <>
<Helmet> <Helmet>
@ -30,10 +39,16 @@ const CreateWorkspacePage: FC = () => {
loadingTemplates={createWorkspaceState.matches("gettingTemplates")} loadingTemplates={createWorkspaceState.matches("gettingTemplates")}
loadingTemplateSchema={createWorkspaceState.matches("gettingTemplateSchema")} loadingTemplateSchema={createWorkspaceState.matches("gettingTemplateSchema")}
creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")} creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")}
templateName={createWorkspaceState.context.templateName} hasTemplateErrors={createWorkspaceState.matches("error")}
templates={createWorkspaceState.context.templates} templateName={templateName}
selectedTemplate={createWorkspaceState.context.selectedTemplate} templates={templates}
templateSchema={createWorkspaceState.context.templateSchema} selectedTemplate={selectedTemplate}
templateSchema={templateSchema}
createWorkspaceErrors={{
[CreateWorkspaceErrors.GET_TEMPLATES_ERROR]: getTemplatesError,
[CreateWorkspaceErrors.GET_TEMPLATE_SCHEMA_ERROR]: getTemplateSchemaError,
[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]: createWorkspaceError,
}}
onCancel={() => { onCancel={() => {
navigate("/templates") navigate("/templates")
}} }}

View File

@ -1,7 +1,11 @@
import { ComponentMeta, Story } from "@storybook/react" import { ComponentMeta, Story } from "@storybook/react"
import { ParameterSchema } from "../../api/typesGenerated" import { ParameterSchema } from "../../api/typesGenerated"
import { MockTemplate } from "../../testHelpers/entities" import { makeMockApiError, MockTemplate } from "../../testHelpers/entities"
import { CreateWorkspacePageView, CreateWorkspacePageViewProps } from "./CreateWorkspacePageView" import {
CreateWorkspaceErrors,
CreateWorkspacePageView,
CreateWorkspacePageViewProps,
} from "./CreateWorkspacePageView"
const createParameterSchema = (partial: Partial<ParameterSchema>): ParameterSchema => { const createParameterSchema = (partial: Partial<ParameterSchema>): ParameterSchema => {
return { return {
@ -40,6 +44,7 @@ NoParameters.args = {
templates: [MockTemplate], templates: [MockTemplate],
selectedTemplate: MockTemplate, selectedTemplate: MockTemplate,
templateSchema: [], templateSchema: [],
createWorkspaceErrors: {},
} }
export const Parameters = Template.bind({}) export const Parameters = Template.bind({})
@ -60,4 +65,48 @@ Parameters.args = {
validation_contains: ["Small", "Medium", "Big"], validation_contains: ["Small", "Medium", "Big"],
}), }),
], ],
createWorkspaceErrors: {},
}
export const GetTemplatesError = Template.bind({})
GetTemplatesError.args = {
...Parameters.args,
createWorkspaceErrors: {
[CreateWorkspaceErrors.GET_TEMPLATES_ERROR]: makeMockApiError({
message: "Failed to fetch templates.",
detail: "You do not have permission to access this resource.",
}),
},
hasTemplateErrors: true,
}
export const GetTemplateSchemaError = Template.bind({})
GetTemplateSchemaError.args = {
...Parameters.args,
createWorkspaceErrors: {
[CreateWorkspaceErrors.GET_TEMPLATE_SCHEMA_ERROR]: makeMockApiError({
message: 'Failed to fetch template schema for "docker-amd64".',
detail: "You do not have permission to access this resource.",
}),
},
hasTemplateErrors: true,
}
export const CreateWorkspaceError = Template.bind({})
CreateWorkspaceError.args = {
...Parameters.args,
createWorkspaceErrors: {
[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]: makeMockApiError({
message: 'Workspace "test" already exists in the "docker-amd64" template.',
validations: [
{
field: "name",
detail: "This value is already in use and should be unique.",
},
],
}),
},
initialTouched: {
name: true,
},
} }

View File

@ -1,6 +1,7 @@
import { makeStyles } from "@material-ui/core/styles" import { makeStyles } from "@material-ui/core/styles"
import TextField from "@material-ui/core/TextField" import TextField from "@material-ui/core/TextField"
import { FormikContextType, useFormik } from "formik" import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
import { FormikContextType, FormikTouched, useFormik } from "formik"
import { FC, useState } from "react" import { FC, useState } from "react"
import * as Yup from "yup" import * as Yup from "yup"
import * as TypesGen from "../../api/typesGenerated" import * as TypesGen from "../../api/typesGenerated"
@ -9,23 +10,33 @@ import { FullPageForm } from "../../components/FullPageForm/FullPageForm"
import { Loader } from "../../components/Loader/Loader" import { Loader } from "../../components/Loader/Loader"
import { ParameterInput } from "../../components/ParameterInput/ParameterInput" import { ParameterInput } from "../../components/ParameterInput/ParameterInput"
import { Stack } from "../../components/Stack/Stack" import { Stack } from "../../components/Stack/Stack"
import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils" import { getFormHelpersWithError, nameValidator, onChangeTrimmed } from "../../util/formUtils"
export const Language = { export const Language = {
templateLabel: "Template", templateLabel: "Template",
nameLabel: "Name", nameLabel: "Name",
} }
export enum CreateWorkspaceErrors {
GET_TEMPLATES_ERROR = "getTemplatesError",
GET_TEMPLATE_SCHEMA_ERROR = "getTemplateSchemaError",
CREATE_WORKSPACE_ERROR = "createWorkspaceError",
}
export interface CreateWorkspacePageViewProps { export interface CreateWorkspacePageViewProps {
loadingTemplates: boolean loadingTemplates: boolean
loadingTemplateSchema: boolean loadingTemplateSchema: boolean
creatingWorkspace: boolean creatingWorkspace: boolean
hasTemplateErrors: boolean
templateName: string templateName: string
templates?: TypesGen.Template[] templates?: TypesGen.Template[]
selectedTemplate?: TypesGen.Template selectedTemplate?: TypesGen.Template
templateSchema?: TypesGen.ParameterSchema[] templateSchema?: TypesGen.ParameterSchema[]
createWorkspaceErrors: Partial<Record<CreateWorkspaceErrors, Error | unknown>>
onCancel: () => void onCancel: () => void
onSubmit: (req: TypesGen.CreateWorkspaceRequest) => void onSubmit: (req: TypesGen.CreateWorkspaceRequest) => void
// initialTouched is only used for testing the error state of the form.
initialTouched?: FormikTouched<TypesGen.CreateWorkspaceRequest>
} }
export const validationSchema = Yup.object({ export const validationSchema = Yup.object({
@ -44,6 +55,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props)
}, },
enableReinitialize: true, enableReinitialize: true,
validationSchema, validationSchema,
initialTouched: props.initialTouched,
onSubmit: (request) => { onSubmit: (request) => {
if (!props.templateSchema) { if (!props.templateSchema) {
throw new Error("No template schema loaded") throw new Error("No template schema loaded")
@ -62,18 +74,45 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props)
source_value: value, source_value: value,
}) })
}) })
return props.onSubmit({ props.onSubmit({
...request, ...request,
parameter_values: createRequests, parameter_values: createRequests,
}) })
form.setSubmitting(false)
}, },
}) })
const getFieldHelpers = getFormHelpers<TypesGen.CreateWorkspaceRequest>(form)
const getFieldHelpers = getFormHelpersWithError<TypesGen.CreateWorkspaceRequest>(
form,
props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR],
)
if (props.hasTemplateErrors) {
return (
<Stack>
{props.createWorkspaceErrors[CreateWorkspaceErrors.GET_TEMPLATES_ERROR] && (
<ErrorSummary
error={props.createWorkspaceErrors[CreateWorkspaceErrors.GET_TEMPLATES_ERROR]}
/>
)}
{props.createWorkspaceErrors[CreateWorkspaceErrors.GET_TEMPLATE_SCHEMA_ERROR] && (
<ErrorSummary
error={props.createWorkspaceErrors[CreateWorkspaceErrors.GET_TEMPLATE_SCHEMA_ERROR]}
/>
)}
</Stack>
)
}
return ( return (
<FullPageForm title="Create workspace" onCancel={props.onCancel}> <FullPageForm title="Create workspace" onCancel={props.onCancel}>
<form onSubmit={form.handleSubmit}> <form onSubmit={form.handleSubmit}>
<Stack> <Stack>
{props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR] && (
<ErrorSummary
error={props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]}
/>
)}
<TextField <TextField
disabled disabled
fullWidth fullWidth

View File

@ -15,6 +15,9 @@ type CreateWorkspaceContext = {
templateSchema?: ParameterSchema[] templateSchema?: ParameterSchema[]
createWorkspaceRequest?: CreateWorkspaceRequest createWorkspaceRequest?: CreateWorkspaceRequest
createdWorkspace?: Workspace createdWorkspace?: Workspace
createWorkspaceError?: Error | unknown
getTemplatesError?: Error | unknown
getTemplateSchemaError?: Error | unknown
} }
type CreateWorkspaceEvent = { type CreateWorkspaceEvent = {
@ -44,6 +47,7 @@ export const createWorkspaceMachine = createMachine(
tsTypes: {} as import("./createWorkspaceXService.typegen").Typegen0, tsTypes: {} as import("./createWorkspaceXService.typegen").Typegen0,
states: { states: {
gettingTemplates: { gettingTemplates: {
entry: "clearGetTemplatesError",
invoke: { invoke: {
src: "getTemplates", src: "getTemplates",
onDone: [ onDone: [
@ -57,11 +61,13 @@ export const createWorkspaceMachine = createMachine(
}, },
], ],
onError: { onError: {
actions: ["assignGetTemplatesError"],
target: "error", target: "error",
}, },
}, },
}, },
gettingTemplateSchema: { gettingTemplateSchema: {
entry: "clearGetTemplateSchemaError",
invoke: { invoke: {
src: "getTemplateSchema", src: "getTemplateSchema",
onDone: { onDone: {
@ -69,6 +75,7 @@ export const createWorkspaceMachine = createMachine(
target: "fillingParams", target: "fillingParams",
}, },
onError: { onError: {
actions: ["assignGetTemplateSchemaError"],
target: "error", target: "error",
}, },
}, },
@ -82,6 +89,7 @@ export const createWorkspaceMachine = createMachine(
}, },
}, },
creatingWorkspace: { creatingWorkspace: {
entry: "clearCreateWorkspaceError",
invoke: { invoke: {
src: "createWorkspace", src: "createWorkspace",
onDone: { onDone: {
@ -89,7 +97,8 @@ export const createWorkspaceMachine = createMachine(
target: "created", target: "created",
}, },
onError: { onError: {
target: "error", actions: ["assignCreateWorkspaceError"],
target: "fillingParams",
}, },
}, },
}, },
@ -142,6 +151,24 @@ export const createWorkspaceMachine = createMachine(
assignCreateWorkspaceRequest: assign({ assignCreateWorkspaceRequest: assign({
createWorkspaceRequest: (_, event) => event.request, createWorkspaceRequest: (_, event) => event.request,
}), }),
assignCreateWorkspaceError: assign({
createWorkspaceError: (_, event) => event.data,
}),
clearCreateWorkspaceError: assign({
createWorkspaceError: (_) => undefined,
}),
assignGetTemplatesError: assign({
getTemplatesError: (_, event) => event.data,
}),
clearGetTemplatesError: assign({
getTemplatesError: (_) => undefined,
}),
assignGetTemplateSchemaError: assign({
getTemplateSchemaError: (_, event) => event.data,
}),
clearGetTemplateSchemaError: assign({
getTemplateSchemaError: (_) => undefined,
}),
}, },
}, },
) )