mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
fix: handle create workspace errors (#3346)
This commit is contained in:
@ -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),
|
||||
Validations: []codersdk.ValidationError{{
|
||||
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
|
||||
|
@ -5,7 +5,7 @@ import { useNavigate, useParams } from "react-router-dom"
|
||||
import { useOrganizationId } from "../../hooks/useOrganizationId"
|
||||
import { pageTitle } from "../../util/page"
|
||||
import { createWorkspaceMachine } from "../../xServices/createWorkspace/createWorkspaceXService"
|
||||
import { CreateWorkspacePageView } from "./CreateWorkspacePageView"
|
||||
import { CreateWorkspaceErrors, CreateWorkspacePageView } from "./CreateWorkspacePageView"
|
||||
|
||||
const CreateWorkspacePage: FC = () => {
|
||||
const organizationId = useOrganizationId()
|
||||
@ -21,6 +21,15 @@ const CreateWorkspacePage: FC = () => {
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
templates,
|
||||
templateSchema,
|
||||
selectedTemplate,
|
||||
getTemplateSchemaError,
|
||||
getTemplatesError,
|
||||
createWorkspaceError,
|
||||
} = createWorkspaceState.context
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@ -30,10 +39,16 @@ const CreateWorkspacePage: FC = () => {
|
||||
loadingTemplates={createWorkspaceState.matches("gettingTemplates")}
|
||||
loadingTemplateSchema={createWorkspaceState.matches("gettingTemplateSchema")}
|
||||
creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")}
|
||||
templateName={createWorkspaceState.context.templateName}
|
||||
templates={createWorkspaceState.context.templates}
|
||||
selectedTemplate={createWorkspaceState.context.selectedTemplate}
|
||||
templateSchema={createWorkspaceState.context.templateSchema}
|
||||
hasTemplateErrors={createWorkspaceState.matches("error")}
|
||||
templateName={templateName}
|
||||
templates={templates}
|
||||
selectedTemplate={selectedTemplate}
|
||||
templateSchema={templateSchema}
|
||||
createWorkspaceErrors={{
|
||||
[CreateWorkspaceErrors.GET_TEMPLATES_ERROR]: getTemplatesError,
|
||||
[CreateWorkspaceErrors.GET_TEMPLATE_SCHEMA_ERROR]: getTemplateSchemaError,
|
||||
[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]: createWorkspaceError,
|
||||
}}
|
||||
onCancel={() => {
|
||||
navigate("/templates")
|
||||
}}
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import { ParameterSchema } from "../../api/typesGenerated"
|
||||
import { MockTemplate } from "../../testHelpers/entities"
|
||||
import { CreateWorkspacePageView, CreateWorkspacePageViewProps } from "./CreateWorkspacePageView"
|
||||
import { makeMockApiError, MockTemplate } from "../../testHelpers/entities"
|
||||
import {
|
||||
CreateWorkspaceErrors,
|
||||
CreateWorkspacePageView,
|
||||
CreateWorkspacePageViewProps,
|
||||
} from "./CreateWorkspacePageView"
|
||||
|
||||
const createParameterSchema = (partial: Partial<ParameterSchema>): ParameterSchema => {
|
||||
return {
|
||||
@ -40,6 +44,7 @@ NoParameters.args = {
|
||||
templates: [MockTemplate],
|
||||
selectedTemplate: MockTemplate,
|
||||
templateSchema: [],
|
||||
createWorkspaceErrors: {},
|
||||
}
|
||||
|
||||
export const Parameters = Template.bind({})
|
||||
@ -60,4 +65,48 @@ Parameters.args = {
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
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 * as Yup from "yup"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
@ -9,23 +10,33 @@ import { FullPageForm } from "../../components/FullPageForm/FullPageForm"
|
||||
import { Loader } from "../../components/Loader/Loader"
|
||||
import { ParameterInput } from "../../components/ParameterInput/ParameterInput"
|
||||
import { Stack } from "../../components/Stack/Stack"
|
||||
import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils"
|
||||
import { getFormHelpersWithError, nameValidator, onChangeTrimmed } from "../../util/formUtils"
|
||||
|
||||
export const Language = {
|
||||
templateLabel: "Template",
|
||||
nameLabel: "Name",
|
||||
}
|
||||
|
||||
export enum CreateWorkspaceErrors {
|
||||
GET_TEMPLATES_ERROR = "getTemplatesError",
|
||||
GET_TEMPLATE_SCHEMA_ERROR = "getTemplateSchemaError",
|
||||
CREATE_WORKSPACE_ERROR = "createWorkspaceError",
|
||||
}
|
||||
|
||||
export interface CreateWorkspacePageViewProps {
|
||||
loadingTemplates: boolean
|
||||
loadingTemplateSchema: boolean
|
||||
creatingWorkspace: boolean
|
||||
hasTemplateErrors: boolean
|
||||
templateName: string
|
||||
templates?: TypesGen.Template[]
|
||||
selectedTemplate?: TypesGen.Template
|
||||
templateSchema?: TypesGen.ParameterSchema[]
|
||||
createWorkspaceErrors: Partial<Record<CreateWorkspaceErrors, Error | unknown>>
|
||||
onCancel: () => 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({
|
||||
@ -44,6 +55,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props)
|
||||
},
|
||||
enableReinitialize: true,
|
||||
validationSchema,
|
||||
initialTouched: props.initialTouched,
|
||||
onSubmit: (request) => {
|
||||
if (!props.templateSchema) {
|
||||
throw new Error("No template schema loaded")
|
||||
@ -62,18 +74,45 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props)
|
||||
source_value: value,
|
||||
})
|
||||
})
|
||||
return props.onSubmit({
|
||||
props.onSubmit({
|
||||
...request,
|
||||
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 (
|
||||
<FullPageForm title="Create workspace" onCancel={props.onCancel}>
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<Stack>
|
||||
{props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR] && (
|
||||
<ErrorSummary
|
||||
error={props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]}
|
||||
/>
|
||||
)}
|
||||
<TextField
|
||||
disabled
|
||||
fullWidth
|
||||
|
@ -15,6 +15,9 @@ type CreateWorkspaceContext = {
|
||||
templateSchema?: ParameterSchema[]
|
||||
createWorkspaceRequest?: CreateWorkspaceRequest
|
||||
createdWorkspace?: Workspace
|
||||
createWorkspaceError?: Error | unknown
|
||||
getTemplatesError?: Error | unknown
|
||||
getTemplateSchemaError?: Error | unknown
|
||||
}
|
||||
|
||||
type CreateWorkspaceEvent = {
|
||||
@ -44,6 +47,7 @@ export const createWorkspaceMachine = createMachine(
|
||||
tsTypes: {} as import("./createWorkspaceXService.typegen").Typegen0,
|
||||
states: {
|
||||
gettingTemplates: {
|
||||
entry: "clearGetTemplatesError",
|
||||
invoke: {
|
||||
src: "getTemplates",
|
||||
onDone: [
|
||||
@ -57,11 +61,13 @@ export const createWorkspaceMachine = createMachine(
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
actions: ["assignGetTemplatesError"],
|
||||
target: "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
gettingTemplateSchema: {
|
||||
entry: "clearGetTemplateSchemaError",
|
||||
invoke: {
|
||||
src: "getTemplateSchema",
|
||||
onDone: {
|
||||
@ -69,6 +75,7 @@ export const createWorkspaceMachine = createMachine(
|
||||
target: "fillingParams",
|
||||
},
|
||||
onError: {
|
||||
actions: ["assignGetTemplateSchemaError"],
|
||||
target: "error",
|
||||
},
|
||||
},
|
||||
@ -82,6 +89,7 @@ export const createWorkspaceMachine = createMachine(
|
||||
},
|
||||
},
|
||||
creatingWorkspace: {
|
||||
entry: "clearCreateWorkspaceError",
|
||||
invoke: {
|
||||
src: "createWorkspace",
|
||||
onDone: {
|
||||
@ -89,7 +97,8 @@ export const createWorkspaceMachine = createMachine(
|
||||
target: "created",
|
||||
},
|
||||
onError: {
|
||||
target: "error",
|
||||
actions: ["assignCreateWorkspaceError"],
|
||||
target: "fillingParams",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -142,6 +151,24 @@ export const createWorkspaceMachine = createMachine(
|
||||
assignCreateWorkspaceRequest: assign({
|
||||
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,
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
Reference in New Issue
Block a user