mirror of
https://github.com/coder/coder.git
synced 2025-07-21 01:28:49 +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),
|
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
|
||||||
|
@ -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")
|
||||||
}}
|
}}
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user