diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 8101772232e1e..fd696d5f0edb1 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -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 diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx index d534c0bbc9f26..2359ccb42a4e1 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx @@ -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 ( <> @@ -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") }} diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx index 926e0d61f286d..0554389fd3924 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx @@ -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 => { 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, + }, } diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 1b4e27d3b08e5..564891ccae025 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -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> onCancel: () => void onSubmit: (req: TypesGen.CreateWorkspaceRequest) => void + // initialTouched is only used for testing the error state of the form. + initialTouched?: FormikTouched } export const validationSchema = Yup.object({ @@ -44,6 +55,7 @@ export const CreateWorkspacePageView: FC = (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 = (props) source_value: value, }) }) - return props.onSubmit({ + props.onSubmit({ ...request, parameter_values: createRequests, }) + form.setSubmitting(false) }, }) - const getFieldHelpers = getFormHelpers(form) + + const getFieldHelpers = getFormHelpersWithError( + form, + props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR], + ) + + if (props.hasTemplateErrors) { + return ( + + {props.createWorkspaceErrors[CreateWorkspaceErrors.GET_TEMPLATES_ERROR] && ( + + )} + {props.createWorkspaceErrors[CreateWorkspaceErrors.GET_TEMPLATE_SCHEMA_ERROR] && ( + + )} + + ) + } return (
+ {props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR] && ( + + )} 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, + }), }, }, ) 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