From ed77ef74f9361150f57cd1d6acf09166d9501ae9 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 24 Jan 2023 15:56:06 +0100 Subject: [PATCH 01/46] XService: GetTemplateParameters --- site/src/api/api.ts | 9 ++++ .../createWorkspaceXService.ts | 44 +++++++++++++++++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index a02683bece396..baa6b2604545a 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -290,6 +290,15 @@ export const getTemplateVersionParameters = async ( return response.data } +export const getTemplateVersionRichParameters = async ( + versionId: string, +): Promise => { + const response = await axios.get( + `/api/v2/templateversions/${versionId}/rich-parameters`, + ) + return response.data +} + export const createTemplate = async ( organizationId: string, data: TypesGen.CreateTemplateRequest, diff --git a/site/src/xServices/createWorkspace/createWorkspaceXService.ts b/site/src/xServices/createWorkspace/createWorkspaceXService.ts index 80b9ce6b6f42f..41808a8aac732 100644 --- a/site/src/xServices/createWorkspace/createWorkspaceXService.ts +++ b/site/src/xServices/createWorkspace/createWorkspaceXService.ts @@ -2,12 +2,14 @@ import { checkAuthorization, createWorkspace, getTemplates, + getTemplateVersionRichParameters, getTemplateVersionSchema, } from "api/api" import { CreateWorkspaceRequest, ParameterSchema, Template, + TemplateVersionParameter, User, Workspace, } from "api/typesGenerated" @@ -19,11 +21,13 @@ type CreateWorkspaceContext = { templateName: string templates?: Template[] selectedTemplate?: Template + templateParameters?: TemplateVersionParameter[] templateSchema?: ParameterSchema[] createWorkspaceRequest?: CreateWorkspaceRequest createdWorkspace?: Workspace createWorkspaceError?: Error | unknown getTemplatesError?: Error | unknown + getTemplateParametersError?: Error | unknown getTemplateSchemaError?: Error | unknown permissions?: Record checkPermissionsError?: Error | unknown @@ -52,6 +56,9 @@ export const createWorkspaceMachine = createMachine( getTemplates: { data: Template[] } + getTemplateParameters: { + data: TemplateVersionParameter[] + } getTemplateSchema: { data: ParameterSchema[] } @@ -88,7 +95,7 @@ export const createWorkspaceMachine = createMachine( src: "getTemplateSchema", onDone: { actions: ["assignTemplateSchema"], - target: "checkingPermissions", + target: "gettingTemplateParameters", }, onError: { actions: ["assignGetTemplateSchemaError"], @@ -96,6 +103,20 @@ export const createWorkspaceMachine = createMachine( }, }, }, + gettingTemplateParameters: { + entry: "clearGetTemplateParametersError", + invoke: { + src: "getTemplateParameters", + onDone: { + actions: ["assignTemplateParameters"], + target: "checkingPermissions", + }, + onError: { + actions: ["assignGetTemplateParametersError"], + target: "error", + }, + }, + }, checkingPermissions: { entry: "clearCheckPermissionsError", invoke: { @@ -145,6 +166,15 @@ export const createWorkspaceMachine = createMachine( { services: { getTemplates: (context) => getTemplates(context.organizationId), + getTemplateParameters: (context) => { + const { selectedTemplate } = context + + if (!selectedTemplate) { + throw new Error("No selected template") + } + + return getTemplateVersionRichParameters(selectedTemplate.active_version_id) + }, getTemplateSchema: (context) => { const { selectedTemplate } = context @@ -206,11 +236,13 @@ export const createWorkspaceMachine = createMachine( return templates.length > 0 ? templates[0] : undefined }, }), + assignTemplateParameters: assign({ + templateParameters: (_, event) => event.data, + }), assignTemplateSchema: assign({ // Only show parameters that are allowed to be overridden. // CLI code: https://github.com/coder/coder/blob/main/cli/create.go#L152-L155 - templateSchema: (_, event) => - event.data.filter((param) => param.allow_override_source), + templateSchema: (_, event) => event.data }), assignPermissions: assign({ permissions: (_, event) => event.data as Record, @@ -239,6 +271,12 @@ export const createWorkspaceMachine = createMachine( clearGetTemplatesError: assign({ getTemplatesError: (_) => undefined, }), + assignGetTemplateParametersError: assign({ + getTemplateParametersError: (_, event) => event.data, + }), + clearGetTemplateParametersError: assign({ + getTemplateParametersError: (_) => undefined, + }), assignGetTemplateSchemaError: assign({ getTemplateSchemaError: (_, event) => event.data, }), From 5d8330c9ff893f4c3b5a023d3e4081aa8df10cac Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 24 Jan 2023 19:00:52 +0100 Subject: [PATCH 02/46] Rich parameter input shows up --- .../RichParameterInput/RichParameterInput.tsx | 131 ++++++++++++++++++ .../CreateWorkspacePage.tsx | 2 + .../CreateWorkspacePageView.tsx | 37 +++++ 3 files changed, 170 insertions(+) create mode 100644 site/src/components/RichParameterInput/RichParameterInput.tsx diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx new file mode 100644 index 0000000000000..98e975b16a1f4 --- /dev/null +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -0,0 +1,131 @@ +import FormControlLabel from "@material-ui/core/FormControlLabel" +import Radio from "@material-ui/core/Radio" +import RadioGroup from "@material-ui/core/RadioGroup" +import { makeStyles } from "@material-ui/core/styles" +import TextField from "@material-ui/core/TextField" +import { Stack } from "components/Stack/Stack" +import { FC } from "react" +import { TemplateVersionParameter } from "../../api/typesGenerated" + +const isBoolean = (parameter: TemplateVersionParameter) => { + return parameter.type === "bool" +} + +const ParameterLabel: React.FC<{ parameter: TemplateVersionParameter }> = ({ parameter }) => { + const styles = useStyles() + + if (parameter.name && parameter.description) { + return ( + + ) + } + + return ( + + ) +} + +export interface RichParameterInputProps { + disabled?: boolean + parameter: TemplateVersionParameter + onChange: (value: string) => void + defaultValue?: string +} + +export const RichParameterInput: FC = ({ + disabled, + onChange, + parameter, +}) => { + const styles = useStyles() + + return ( + + +
+ +
+
+ ) +} + +const RichParameterField: React.FC = ({ + disabled, + onChange, + parameter +}) => { + if (isBoolean(parameter)) { + return ( + { + onChange(event.target.value) + }} + > + } + label="True" + /> + } + label="False" + /> + + ) + } + + // A text field can technically handle all cases! + // As other cases become more prominent (like filtering for numbers), + // we should break this out into more finely scoped input fields. + return ( + { + onChange(event.target.value) + }} + /> + ) +} + +const useStyles = makeStyles((theme) => ({ + labelName: { + fontSize: 14, + color: theme.palette.text.secondary, + display: "block", + marginBottom: theme.spacing(0.5), + }, + labelDescription: { + fontSize: 16, + color: theme.palette.text.primary, + display: "block", + fontWeight: 600, + }, + input: { + display: "flex", + flexDirection: "column", + }, + checkbox: { + display: "flex", + alignItems: "center", + gap: theme.spacing(1), + }, +})) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx index c0d01ebf7e5fe..30934d629735b 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx @@ -30,6 +30,7 @@ const CreateWorkspacePage: FC = () => { }) const { templates, + templateParameters, templateSchema, selectedTemplate, getTemplateSchemaError, @@ -57,6 +58,7 @@ const CreateWorkspacePage: FC = () => { templateName={templateName} templates={templates} selectedTemplate={selectedTemplate} + templateParameters={templateParameters} templateSchema={templateSchema} createWorkspaceErrors={{ [CreateWorkspaceErrors.GET_TEMPLATES_ERROR]: getTemplatesError, diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 7cb9cb880f0f2..02317c77dce57 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -2,6 +2,7 @@ import TextField from "@material-ui/core/TextField" import * as TypesGen from "api/typesGenerated" import { FormFooter } from "components/FormFooter/FormFooter" import { ParameterInput } from "components/ParameterInput/ParameterInput" +import { RichParameterInput } from "components/RichParameterInput/RichParameterInput" import { Stack } from "components/Stack/Stack" import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete" import { FormikContextType, FormikTouched, useFormik } from "formik" @@ -30,6 +31,8 @@ export interface CreateWorkspacePageViewProps { templateName: string templates?: TypesGen.Template[] selectedTemplate?: TypesGen.Template + templateParameters?: TypesGen.TemplateVersionParameter[] + templateSchema?: TypesGen.ParameterSchema[] createWorkspaceErrors: Partial> canCreateForUser?: boolean @@ -249,6 +252,34 @@ export const CreateWorkspacePageView: FC< )} + {/* Rich parameters */} + {props.templateParameters && props.templateParameters.length > 0 && ( +
+
+

Rich template params

+

+ Those values are provided by your template‘s Terraform + configuration. +

+
+ + + {props.templateParameters.map((parameter) => ( + + ))} + +
+ )} ({ }, }, })) + +const validate = (value :string) => { + if (value === "1") { + value = value + "0" + } +}; From 25140ebb729a4da9f1171ea48871d2f5f8c2f1cd Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 25 Jan 2023 11:26:52 +0100 Subject: [PATCH 03/46] Render option icons --- .../RichParameterInput/RichParameterInput.tsx | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index 98e975b16a1f4..d400efb011206 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -1,4 +1,5 @@ import FormControlLabel from "@material-ui/core/FormControlLabel" +import Icon from "@material-ui/icons/Brightness1" import Radio from "@material-ui/core/Radio" import RadioGroup from "@material-ui/core/RadioGroup" import { makeStyles } from "@material-ui/core/styles" @@ -17,7 +18,21 @@ const ParameterLabel: React.FC<{ parameter: TemplateVersionParameter }> = ({ par if (parameter.name && parameter.description) { return ( ) @@ -63,6 +78,8 @@ const RichParameterField: React.FC = ({ onChange, parameter }) => { + const styles = useStyles(); + if (isBoolean(parameter)) { return ( = ({ ) } + if (parameter.options.length > 0) { + return ( + { + onChange(event.target.value) + }} + > + { + parameter.options.map((option) => ( + } + label={( + + Parameter icon + {option.name} + + )} + /> + )) + } + + ) + } + // A text field can technically handle all cases! // As other cases become more prominent (like filtering for numbers), // we should break this out into more finely scoped input fields. @@ -106,11 +158,17 @@ const RichParameterField: React.FC = ({ ) } +const iconSize = 20 +const optionIconSize = 24 + const useStyles = makeStyles((theme) => ({ labelName: { fontSize: 14, color: theme.palette.text.secondary, display: "block", + marginBottom: theme.spacing(1.0), + }, + labelNameWithIcon: { marginBottom: theme.spacing(0.5), }, labelDescription: { @@ -128,4 +186,18 @@ const useStyles = makeStyles((theme) => ({ alignItems: "center", gap: theme.spacing(1), }, + iconWrapper: { + float: "left", + }, + icon: { + height: iconSize, + width: iconSize, + marginRight: theme.spacing(1.0), + }, + optionIcon: { + height: optionIconSize, + width: optionIconSize, + marginRight: theme.spacing(1.0), + float: "left", + }, })) From 622eb4b57080860ee39f3bb85d4c6790f55b989e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 25 Jan 2023 11:45:01 +0100 Subject: [PATCH 04/46] Icons --- .../RichParameterInput/RichParameterInput.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index d400efb011206..af73e50df952f 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -122,14 +122,16 @@ const RichParameterField: React.FC = ({ control={} label={( - Parameter icon + )} {option.name} )} @@ -190,12 +192,12 @@ const useStyles = makeStyles((theme) => ({ float: "left", }, icon: { - height: iconSize, + maxHeight: iconSize, width: iconSize, marginRight: theme.spacing(1.0), }, optionIcon: { - height: optionIconSize, + maxHeight: optionIconSize, width: optionIconSize, marginRight: theme.spacing(1.0), float: "left", From 6a9fce9d47c8392d840b484ded17a72b517867f6 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 25 Jan 2023 17:52:33 +0100 Subject: [PATCH 05/46] WIP --- .../RichParameterInput/RichParameterInput.tsx | 14 ++--- .../CreateWorkspacePageView.tsx | 58 ++++++++++++++----- site/src/util/formUtils.ts | 2 +- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index af73e50df952f..bc68b741093fd 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -1,5 +1,4 @@ import FormControlLabel from "@material-ui/core/FormControlLabel" -import Icon from "@material-ui/icons/Brightness1" import Radio from "@material-ui/core/Radio" import RadioGroup from "@material-ui/core/RadioGroup" import { makeStyles } from "@material-ui/core/styles" @@ -49,13 +48,13 @@ export interface RichParameterInputProps { disabled?: boolean parameter: TemplateVersionParameter onChange: (value: string) => void - defaultValue?: string } export const RichParameterInput: FC = ({ disabled, onChange, parameter, + ...props }) => { const styles = useStyles() @@ -64,6 +63,7 @@ export const RichParameterInput: FC = ({
= ({ const RichParameterField: React.FC = ({ disabled, onChange, - parameter + parameter, + ...props }) => { const styles = useStyles(); if (isBoolean(parameter)) { return ( { onChange(event.target.value) @@ -108,8 +108,7 @@ const RichParameterField: React.FC = ({ if (parameter.options.length > 0) { return ( { onChange(event.target.value) }} @@ -117,6 +116,7 @@ const RichParameterField: React.FC = ({ { parameter.options.map((option) => ( } @@ -147,7 +147,7 @@ const RichParameterField: React.FC = ({ // we should break this out into more finely scoped input fields. return ( } -const { t } = i18n - -export const validationSchema = Yup.object({ - name: nameValidator(t("nameLabel", { ns: "createWorkspacePage" })), -}) - export const CreateWorkspacePageView: FC< React.PropsWithChildren > = (props) => { @@ -61,11 +54,24 @@ export const CreateWorkspacePageView: FC< Record >(props.defaultParameterValues ?? {}) + const validationSchema = Yup.object({ + name: nameValidator(t("nameLabel", { ns: "createWorkspacePage" })), + rich_parameter_values: Yup.array() + .of( + Yup.object().shape({ + "name": Yup.string().required(), + "value": Yup.string().required().test('len', 'Must be exactly 5 characters', val => val !== undefined && val.length === 5), + }) + ) + .required() + }) + const form: FormikContextType = useFormik({ initialValues: { name: "", template_id: props.selectedTemplate ? props.selectedTemplate.id : "", + rich_parameter_values: defaultRichParameters(props.templateParameters), }, enableReinitialize: true, validationSchema, @@ -268,12 +274,17 @@ export const CreateWorkspacePageView: FC< spacing={4} // Spacing here is diff because the fields here don't have the MUI floating label spacing className={styles.formSectionFields} > - {props.templateParameters.map((parameter) => ( + {props.templateParameters.map((parameter, index) => ( { + form.setFieldValue("rich_parameter_values." + index, { + name: parameter.name, + value: value, + }); + }} parameter={parameter} /> ))} @@ -364,8 +375,27 @@ const useFormFooterStyles = makeStyles((theme) => ({ }, })) -const validate = (value :string) => { - if (value === "1") { - value = value + "0" +const defaultRichParameters = (templateParameters?: TypesGen.TemplateVersionParameter[]): TypesGen.WorkspaceBuildParameter[] => { + const defaults: TypesGen.WorkspaceBuildParameter[] = []; + if (!templateParameters) { + return defaults } -}; + + templateParameters.forEach((parameter) => { + if (parameter.options.length > 0) { + const buildParameter: TypesGen.WorkspaceBuildParameter = { + name: parameter.name, + value: parameter.options[0].value, + } + defaults.push(buildParameter) + return + } + + const buildParameter: TypesGen.WorkspaceBuildParameter = { + name: parameter.name, + value: parameter.default_value || '', + } + defaults.push(buildParameter) + }) + return defaults; +} diff --git a/site/src/util/formUtils.ts b/site/src/util/formUtils.ts index c0dc1aa53bac9..029a40727f087 100644 --- a/site/src/util/formUtils.ts +++ b/site/src/util/formUtils.ts @@ -36,7 +36,7 @@ interface FormHelpers { export const getFormHelpers = (form: FormikContextType, error?: Error | unknown) => ( - name: keyof T, + name: string, HelperText: ReactNode = "", backendErrorName?: string, ): FormHelpers => { From d5f1afcd77a8a7382ad586fa2dc15f43e3da2246 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 25 Jan 2023 18:56:59 +0100 Subject: [PATCH 06/46] For testing purposes: template --- mtojek/docker-rich/build/Dockerfile | 18 +++ mtojek/docker-rich/main.tf | 208 ++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 mtojek/docker-rich/build/Dockerfile create mode 100644 mtojek/docker-rich/main.tf diff --git a/mtojek/docker-rich/build/Dockerfile b/mtojek/docker-rich/build/Dockerfile new file mode 100644 index 0000000000000..29d3bf7cf0236 --- /dev/null +++ b/mtojek/docker-rich/build/Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu + +RUN apt-get update \ + && apt-get install -y \ + curl \ + git \ + golang \ + sudo \ + vim \ + wget \ + && rm -rf /var/lib/apt/lists/* + +ARG USER=coder +RUN useradd --groups sudo --no-create-home ${USER} \ + && echo "${USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USER} \ + && chmod 0440 /etc/sudoers.d/${USER} +USER ${USER} +WORKDIR /home/${USER} diff --git a/mtojek/docker-rich/main.tf b/mtojek/docker-rich/main.tf new file mode 100644 index 0000000000000..452c955246d41 --- /dev/null +++ b/mtojek/docker-rich/main.tf @@ -0,0 +1,208 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "0.6.6" + } + docker = { + source = "kreuzwerker/docker" + version = "~> 2.22" + } + } +} + +locals { + username = data.coder_workspace.me.owner +} + +data "coder_provisioner" "me" { +} + +provider "docker" { +} + +data "coder_workspace" "me" { +} + +data "coder_parameter" "project_id" { + name = "Project ID" + icon = "/icon/azure.png" + description = "This is the Project ID. 1" + default = "12345" + validation { + regex = "^[a-z0-9]+$" + error = "Unfortunately this is invalid value" + } +} + +data "coder_parameter" "sample_mutable" { + name = "Sample mutable" + icon = "/icon/aws.png" + description = "This is a sample, mutable parameter." + default = "helloworld" + mutable = true +} + +data "coder_parameter" "sample_options" { + name = "Sample options" + icon = "/icon/database.svg" + description = "These are options." + mutable = true + option { + name = "US Central" + description = "Select for central!" + value = "us-central1-a" + icon = "/icon/goland.svg" + } + option { + name = "US East" + description = "Select for east!" + value = "us-east1-a" + icon = "/icon/folder.svg" + } + option { + name = "US West" + description = "Select for west!" + value = "us-west2-a" + } +} + +data "coder_parameter" "bool_mutable" { + name = "Bool mutable" + icon = "/icon/rider.svg" + type = "bool" + description = "This is a sample, mutable parameter." + default = "false" + mutable = true +} + +data "coder_parameter" "number_mutable" { + name = "Number mutable" + icon = "/icon/rubymine.svg" + type = "number" + description = "This is a number, mutable parameter." + default = "3" + mutable = true + validation { + min = 1 + max = 8 + } +} + +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + startup_script = < Date: Wed, 25 Jan 2023 20:29:20 +0100 Subject: [PATCH 07/46] Fix: useState --- site/src/components/ParameterInput/ParameterInput.tsx | 1 - .../components/RichParameterInput/RichParameterInput.tsx | 6 ++++-- .../pages/CreateWorkspacePage/CreateWorkspacePageView.tsx | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/site/src/components/ParameterInput/ParameterInput.tsx b/site/src/components/ParameterInput/ParameterInput.tsx index 05f263896a98b..74a2cfaae680d 100644 --- a/site/src/components/ParameterInput/ParameterInput.tsx +++ b/site/src/components/ParameterInput/ParameterInput.tsx @@ -124,7 +124,6 @@ const ParameterField: React.FC = ({ size="small" disabled={disabled} placeholder={schema.default_source_value} - defaultValue={defaultValue ?? schema.default_source_value} onChange={(event) => { onChange(event.target.value) }} diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index bc68b741093fd..922a8452d8ebb 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -4,7 +4,7 @@ import RadioGroup from "@material-ui/core/RadioGroup" import { makeStyles } from "@material-ui/core/styles" import TextField from "@material-ui/core/TextField" import { Stack } from "components/Stack/Stack" -import { FC } from "react" +import { FC, useState } from "react" import { TemplateVersionParameter } from "../../api/typesGenerated" const isBoolean = (parameter: TemplateVersionParameter) => { @@ -79,6 +79,7 @@ const RichParameterField: React.FC = ({ parameter, ...props }) => { + const [parameterValue, setParameterValue] = useState(parameter.default_value) const styles = useStyles(); if (isBoolean(parameter)) { @@ -152,8 +153,9 @@ const RichParameterField: React.FC = ({ size="small" disabled={disabled} placeholder={parameter.default_value} - defaultValue={parameter.default_value} + value={parameterValue} onChange={(event) => { + setParameterValue(event.target.value) onChange(event.target.value) }} /> diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 8498f585d4188..e9103e584af93 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -60,7 +60,7 @@ export const CreateWorkspacePageView: FC< .of( Yup.object().shape({ "name": Yup.string().required(), - "value": Yup.string().required().test('len', 'Must be exactly 5 characters', val => val !== undefined && val.length === 5), + "value": Yup.string().required().test('len', 'Must be > 2 characters', val => val !== undefined && val.length > 2), }) ) .required() From 82d7710baff3d83f02e5c03f0a821a304e214890 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 26 Jan 2023 13:04:53 +0100 Subject: [PATCH 08/46] WIP: dynamic validation --- .../CreateWorkspacePageView.tsx | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index e9103e584af93..314f417f0d718 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -47,24 +47,13 @@ export interface CreateWorkspacePageViewProps { export const CreateWorkspacePageView: FC< React.PropsWithChildren > = (props) => { - const { t } = useTranslation("createWorkspacePage") const styles = useStyles() const formFooterStyles = useFormFooterStyles() const [parameterValues, setParameterValues] = useState< Record >(props.defaultParameterValues ?? {}) - const validationSchema = Yup.object({ - name: nameValidator(t("nameLabel", { ns: "createWorkspacePage" })), - rich_parameter_values: Yup.array() - .of( - Yup.object().shape({ - "name": Yup.string().required(), - "value": Yup.string().required().test('len', 'Must be > 2 characters', val => val !== undefined && val.length > 2), - }) - ) - .required() - }) + const { t } = useTranslation("createWorkspacePage") const form: FormikContextType = useFormik({ @@ -73,8 +62,8 @@ export const CreateWorkspacePageView: FC< template_id: props.selectedTemplate ? props.selectedTemplate.id : "", rich_parameter_values: defaultRichParameters(props.templateParameters), }, + validationSchema: ValidationSchemaForRichParameters(props.templateParameters), enableReinitialize: true, - validationSchema, initialTouched: props.initialTouched, onSubmit: (request) => { if (!props.templateSchema) { @@ -399,3 +388,31 @@ const defaultRichParameters = (templateParameters?: TypesGen.TemplateVersionPara }) return defaults; } + +const ValidationSchemaForRichParameters = (templateParameters?: TypesGen.TemplateVersionParameter[]): Yup.AnySchema => { + const { t } = useTranslation("createWorkspacePage") + + if (!templateParameters) { + return Yup.object() + } + + return Yup.object({ + name: nameValidator(t("nameLabel", { ns: "createWorkspacePage" })), + rich_parameter_values: Yup.array() + .of( + Yup.object().shape({ + "name": Yup.string().required(), + "value": Yup.string().required().test('verify with template', (val, ctx) => { + const name = ctx.parent.name; + if (name === "Project ID") { + return ctx.createError({ + path: ctx.path, + message: "Testing something" }) + } + return true + }) + }) + ) + .required() + }) +} From 79dbb5d32fe4f8e3e3ac7cf641a0638fa84a14bb Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 26 Jan 2023 15:41:32 +0100 Subject: [PATCH 09/46] Yup validation --- .../CreateWorkspacePageView.tsx | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 314f417f0d718..c8a2f52668fbb 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -402,12 +402,37 @@ const ValidationSchemaForRichParameters = (templateParameters?: TypesGen.Templat .of( Yup.object().shape({ "name": Yup.string().required(), - "value": Yup.string().required().test('verify with template', (val, ctx) => { + "value": Yup.string().required("Field is required.").test("verify with template", (val, ctx) => { const name = ctx.parent.name; - if (name === "Project ID") { - return ctx.createError({ - path: ctx.path, - message: "Testing something" }) + const templateParameter = templateParameters.find((parameter) => parameter.name === name); + if (templateParameter) { + switch (templateParameter.type) { + case 'number': + if (templateParameter.validation_min === 0 && templateParameter.validation_max === 0) { + return true + } + + if (Number(val) < templateParameter.validation_min || templateParameter.validation_max < Number(val)) { + return ctx.createError({ + path: ctx.path, + message: `Value must be between ${templateParameter.validation_min} and ${templateParameter.validation_max}.` }) + } + break + case 'string': + { + if (templateParameter.validation_regex.length === 0) { + return true + } + + const regex = new RegExp(templateParameter.validation_regex); + if (val && !regex.test(val)) { + return ctx.createError({ + path: ctx.path, + message: `${templateParameter.validation_error} (value does not match the pattern ${templateParameter.validation_regex}).` }) + } + } + break + } } return true }) From 17dece61b19d848c53378c4495b0f97e272c4e0a Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 26 Jan 2023 16:27:59 +0100 Subject: [PATCH 10/46] Translations --- site/src/i18n/en/createWorkspacePage.json | 5 ++++- .../pages/CreateWorkspacePage/CreateWorkspacePageView.tsx | 7 ++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/site/src/i18n/en/createWorkspacePage.json b/site/src/i18n/en/createWorkspacePage.json index 26f302d2f1f08..4c127c23a41b8 100644 --- a/site/src/i18n/en/createWorkspacePage.json +++ b/site/src/i18n/en/createWorkspacePage.json @@ -2,5 +2,8 @@ "templateLabel": "Template", "nameLabel": "Workspace Name", "ownerLabel": "Owner", - "createWorkspace": "Create workspace" + "createWorkspace": "Create workspace", + "validationRequiredParameter": "Parameter is required.", + "validationNumberNotInRange": "Value must be between {{min}} and {{max}}.", + "validationPatternNotMatched": "{{error}} (value does not match the pattern {{pattern}})." } diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index c8a2f52668fbb..9e98836e2d7bc 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -402,7 +402,7 @@ const ValidationSchemaForRichParameters = (templateParameters?: TypesGen.Templat .of( Yup.object().shape({ "name": Yup.string().required(), - "value": Yup.string().required("Field is required.").test("verify with template", (val, ctx) => { + "value": Yup.string().required(t("validationRequiredParameter")).test("verify with template", (val, ctx) => { const name = ctx.parent.name; const templateParameter = templateParameters.find((parameter) => parameter.name === name); if (templateParameter) { @@ -415,7 +415,7 @@ const ValidationSchemaForRichParameters = (templateParameters?: TypesGen.Templat if (Number(val) < templateParameter.validation_min || templateParameter.validation_max < Number(val)) { return ctx.createError({ path: ctx.path, - message: `Value must be between ${templateParameter.validation_min} and ${templateParameter.validation_max}.` }) + message: t("validationNumberNotInRange", { min: templateParameter.validation_min, max: templateParameter.validation_max }) }) } break case 'string': @@ -428,7 +428,8 @@ const ValidationSchemaForRichParameters = (templateParameters?: TypesGen.Templat if (val && !regex.test(val)) { return ctx.createError({ path: ctx.path, - message: `${templateParameter.validation_error} (value does not match the pattern ${templateParameter.validation_regex}).` }) + message: t("validationPatternNotMatched", { error: templateParameter.validation_error, pattern: templateParameter.validation_regex }) + }) } } break From a8863019d029b077142e92e1a267d0d85dd35a44 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 26 Jan 2023 16:29:35 +0100 Subject: [PATCH 11/46] Remove temporary template --- mtojek/docker-rich/build/Dockerfile | 18 --- mtojek/docker-rich/main.tf | 208 ---------------------------- 2 files changed, 226 deletions(-) delete mode 100644 mtojek/docker-rich/build/Dockerfile delete mode 100644 mtojek/docker-rich/main.tf diff --git a/mtojek/docker-rich/build/Dockerfile b/mtojek/docker-rich/build/Dockerfile deleted file mode 100644 index 29d3bf7cf0236..0000000000000 --- a/mtojek/docker-rich/build/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM ubuntu - -RUN apt-get update \ - && apt-get install -y \ - curl \ - git \ - golang \ - sudo \ - vim \ - wget \ - && rm -rf /var/lib/apt/lists/* - -ARG USER=coder -RUN useradd --groups sudo --no-create-home ${USER} \ - && echo "${USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USER} \ - && chmod 0440 /etc/sudoers.d/${USER} -USER ${USER} -WORKDIR /home/${USER} diff --git a/mtojek/docker-rich/main.tf b/mtojek/docker-rich/main.tf deleted file mode 100644 index 452c955246d41..0000000000000 --- a/mtojek/docker-rich/main.tf +++ /dev/null @@ -1,208 +0,0 @@ -terraform { - required_providers { - coder = { - source = "coder/coder" - version = "0.6.6" - } - docker = { - source = "kreuzwerker/docker" - version = "~> 2.22" - } - } -} - -locals { - username = data.coder_workspace.me.owner -} - -data "coder_provisioner" "me" { -} - -provider "docker" { -} - -data "coder_workspace" "me" { -} - -data "coder_parameter" "project_id" { - name = "Project ID" - icon = "/icon/azure.png" - description = "This is the Project ID. 1" - default = "12345" - validation { - regex = "^[a-z0-9]+$" - error = "Unfortunately this is invalid value" - } -} - -data "coder_parameter" "sample_mutable" { - name = "Sample mutable" - icon = "/icon/aws.png" - description = "This is a sample, mutable parameter." - default = "helloworld" - mutable = true -} - -data "coder_parameter" "sample_options" { - name = "Sample options" - icon = "/icon/database.svg" - description = "These are options." - mutable = true - option { - name = "US Central" - description = "Select for central!" - value = "us-central1-a" - icon = "/icon/goland.svg" - } - option { - name = "US East" - description = "Select for east!" - value = "us-east1-a" - icon = "/icon/folder.svg" - } - option { - name = "US West" - description = "Select for west!" - value = "us-west2-a" - } -} - -data "coder_parameter" "bool_mutable" { - name = "Bool mutable" - icon = "/icon/rider.svg" - type = "bool" - description = "This is a sample, mutable parameter." - default = "false" - mutable = true -} - -data "coder_parameter" "number_mutable" { - name = "Number mutable" - icon = "/icon/rubymine.svg" - type = "number" - description = "This is a number, mutable parameter." - default = "3" - mutable = true - validation { - min = 1 - max = 8 - } -} - -resource "coder_agent" "main" { - arch = data.coder_provisioner.me.arch - os = "linux" - startup_script = < Date: Thu, 26 Jan 2023 16:37:54 +0100 Subject: [PATCH 12/46] make fmt --- .../RichParameterInput/RichParameterInput.tsx | 58 +++++----- .../CreateWorkspacePageView.tsx | 107 +++++++++++------- .../createWorkspaceXService.ts | 6 +- 3 files changed, 101 insertions(+), 70 deletions(-) diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index 922a8452d8ebb..2abf549fd0b8e 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -11,7 +11,9 @@ const isBoolean = (parameter: TemplateVersionParameter) => { return parameter.type === "bool" } -const ParameterLabel: React.FC<{ parameter: TemplateVersionParameter }> = ({ parameter }) => { +const ParameterLabel: React.FC<{ parameter: TemplateVersionParameter }> = ({ + parameter, +}) => { const styles = useStyles() if (parameter.name && parameter.description) { @@ -28,8 +30,8 @@ const ParameterLabel: React.FC<{ parameter: TemplateVersionParameter }> = ({ par pointerEvents: "none", }} /> - - )} + + )} {parameter.name} {parameter.description} @@ -80,7 +82,7 @@ const RichParameterField: React.FC = ({ ...props }) => { const [parameterValue, setParameterValue] = useState(parameter.default_value) - const styles = useStyles(); + const styles = useStyles() if (isBoolean(parameter)) { return ( @@ -114,31 +116,29 @@ const RichParameterField: React.FC = ({ onChange(event.target.value) }} > - { - parameter.options.map((option) => ( - } - label={( - - {option.icon && ( - Parameter icon - )} - {option.name} - - )} - /> - )) - } + {parameter.options.map((option) => ( + } + label={ + + {option.icon && ( + Parameter icon + )} + {option.name} + + } + /> + ))} ) } diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 9e98836e2d7bc..306fb6057ecf3 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -62,7 +62,9 @@ export const CreateWorkspacePageView: FC< template_id: props.selectedTemplate ? props.selectedTemplate.id : "", rich_parameter_values: defaultRichParameters(props.templateParameters), }, - validationSchema: ValidationSchemaForRichParameters(props.templateParameters), + validationSchema: ValidationSchemaForRichParameters( + props.templateParameters, + ), enableReinitialize: true, initialTouched: props.initialTouched, onSubmit: (request) => { @@ -251,7 +253,9 @@ export const CreateWorkspacePageView: FC< {props.templateParameters && props.templateParameters.length > 0 && (
-

Rich template params

+

+ Rich template params +

Those values are provided by your template‘s Terraform configuration. @@ -265,14 +269,16 @@ export const CreateWorkspacePageView: FC< > {props.templateParameters.map((parameter, index) => ( { form.setFieldValue("rich_parameter_values." + index, { name: parameter.name, value: value, - }); + }) }} parameter={parameter} /> @@ -364,8 +370,10 @@ const useFormFooterStyles = makeStyles((theme) => ({ }, })) -const defaultRichParameters = (templateParameters?: TypesGen.TemplateVersionParameter[]): TypesGen.WorkspaceBuildParameter[] => { - const defaults: TypesGen.WorkspaceBuildParameter[] = []; +const defaultRichParameters = ( + templateParameters?: TypesGen.TemplateVersionParameter[], +): TypesGen.WorkspaceBuildParameter[] => { + const defaults: TypesGen.WorkspaceBuildParameter[] = [] if (!templateParameters) { return defaults } @@ -382,14 +390,16 @@ const defaultRichParameters = (templateParameters?: TypesGen.TemplateVersionPara const buildParameter: TypesGen.WorkspaceBuildParameter = { name: parameter.name, - value: parameter.default_value || '', + value: parameter.default_value || "", } defaults.push(buildParameter) }) - return defaults; + return defaults } -const ValidationSchemaForRichParameters = (templateParameters?: TypesGen.TemplateVersionParameter[]): Yup.AnySchema => { +const ValidationSchemaForRichParameters = ( + templateParameters?: TypesGen.TemplateVersionParameter[], +): Yup.AnySchema => { const { t } = useTranslation("createWorkspacePage") if (!templateParameters) { @@ -401,44 +411,63 @@ const ValidationSchemaForRichParameters = (templateParameters?: TypesGen.Templat rich_parameter_values: Yup.array() .of( Yup.object().shape({ - "name": Yup.string().required(), - "value": Yup.string().required(t("validationRequiredParameter")).test("verify with template", (val, ctx) => { - const name = ctx.parent.name; - const templateParameter = templateParameters.find((parameter) => parameter.name === name); - if (templateParameter) { - switch (templateParameter.type) { - case 'number': - if (templateParameter.validation_min === 0 && templateParameter.validation_max === 0) { - return true - } - - if (Number(val) < templateParameter.validation_min || templateParameter.validation_max < Number(val)) { - return ctx.createError({ - path: ctx.path, - message: t("validationNumberNotInRange", { min: templateParameter.validation_min, max: templateParameter.validation_max }) }) - } - break - case 'string': - { - if (templateParameter.validation_regex.length === 0) { + name: Yup.string().required(), + value: Yup.string() + .required(t("validationRequiredParameter")) + .test("verify with template", (val, ctx) => { + const name = ctx.parent.name + const templateParameter = templateParameters.find( + (parameter) => parameter.name === name, + ) + if (templateParameter) { + switch (templateParameter.type) { + case "number": + if ( + templateParameter.validation_min === 0 && + templateParameter.validation_max === 0 + ) { return true } - const regex = new RegExp(templateParameter.validation_regex); - if (val && !regex.test(val)) { + if ( + Number(val) < templateParameter.validation_min || + templateParameter.validation_max < Number(val) + ) { return ctx.createError({ path: ctx.path, - message: t("validationPatternNotMatched", { error: templateParameter.validation_error, pattern: templateParameter.validation_regex }) + message: t("validationNumberNotInRange", { + min: templateParameter.validation_min, + max: templateParameter.validation_max, + }), }) } - } - break + break + case "string": + { + if (templateParameter.validation_regex.length === 0) { + return true + } + + const regex = new RegExp( + templateParameter.validation_regex, + ) + if (val && !regex.test(val)) { + return ctx.createError({ + path: ctx.path, + message: t("validationPatternNotMatched", { + error: templateParameter.validation_error, + pattern: templateParameter.validation_regex, + }), + }) + } + } + break + } } - } - return true - }) - }) + return true + }), + }), ) - .required() + .required(), }) } diff --git a/site/src/xServices/createWorkspace/createWorkspaceXService.ts b/site/src/xServices/createWorkspace/createWorkspaceXService.ts index 41808a8aac732..433c341b3c41c 100644 --- a/site/src/xServices/createWorkspace/createWorkspaceXService.ts +++ b/site/src/xServices/createWorkspace/createWorkspaceXService.ts @@ -173,7 +173,9 @@ export const createWorkspaceMachine = createMachine( throw new Error("No selected template") } - return getTemplateVersionRichParameters(selectedTemplate.active_version_id) + return getTemplateVersionRichParameters( + selectedTemplate.active_version_id, + ) }, getTemplateSchema: (context) => { const { selectedTemplate } = context @@ -242,7 +244,7 @@ export const createWorkspaceMachine = createMachine( assignTemplateSchema: assign({ // Only show parameters that are allowed to be overridden. // CLI code: https://github.com/coder/coder/blob/main/cli/create.go#L152-L155 - templateSchema: (_, event) => event.data + templateSchema: (_, event) => event.data, }), assignPermissions: assign({ permissions: (_, event) => event.data as Record, From e5aa5a9d27ebe5066338ee030a223d4082295812 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 26 Jan 2023 19:13:29 +0100 Subject: [PATCH 13/46] WIP --- .../CreateWorkspacePage.test.tsx | 16 +++++++++++---- site/src/testHelpers/entities.ts | 20 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index 48735f077bdb6..0b3f96d2e992e 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -9,6 +9,7 @@ import { MockWorkspace, MockWorkspaceQuota, MockWorkspaceRequest, + MockTemplateVersionParameter1, } from "testHelpers/entities" import { renderWithAuth } from "testHelpers/renderHelpers" import CreateWorkspacePage from "./CreateWorkspacePage" @@ -26,10 +27,17 @@ const renderCreateWorkspacePage = () => { } describe("CreateWorkspacePage", () => { - it("renders", async () => { - renderCreateWorkspacePage() - const element = await screen.findByText("Create workspace") + /*it("renders", async () => { + jest + .spyOn(API, "getTemplateVersionRichParameters") + .mockResolvedValueOnce([MockTemplateVersionParameter1]) + + await waitFor(() => renderCreateWorkspacePage()) + + const element = screen.findByText("Create workspace") expect(element).toBeDefined() + const firstParameter = screen.findByText(MockTemplateVersionParameter1.description) + expect(firstParameter).toBeDefined() }) it("succeeds with default owner", async () => { @@ -62,7 +70,7 @@ describe("CreateWorkspacePage", () => { }, ), ) - }) + })*/ it("uses default param values passed from the URL", async () => { const param = "dotfile_uri" diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 027357a4653f9..af6a06c87c177 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -624,11 +624,31 @@ export const MockWorkspacesResponse: TypesGen.WorkspacesResponse = { count: 26, } +export const MockTemplateVersionParameter1: TypesGen.TemplateVersionParameter = { + name: "first_parameter", + type: "string", + description: "This is first parameter", + default_value: "abc", + mutable: true, + icon: "/icons/icon.svg", + options: [], + validation_error: "", + validation_regex: "", + validation_min: 0, + validation_max: 0, +} + // requests the MockWorkspace export const MockWorkspaceRequest: TypesGen.CreateWorkspaceRequest = { name: "test", parameter_values: [], template_id: "test-template", + rich_parameter_values: [ + { + name: MockTemplateVersionParameter1.name, + value: MockTemplateVersionParameter1.default_value, + } + ], } export const MockUserAgent: Types.UserAgent = { From a00955565ec45b7ea15bf207d957d597a6090876 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 27 Jan 2023 11:31:48 +0100 Subject: [PATCH 14/46] Fix: tests --- .../components/ParameterInput/ParameterInput.tsx | 1 + .../CreateWorkspacePage.test.tsx | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/site/src/components/ParameterInput/ParameterInput.tsx b/site/src/components/ParameterInput/ParameterInput.tsx index 74a2cfaae680d..05f263896a98b 100644 --- a/site/src/components/ParameterInput/ParameterInput.tsx +++ b/site/src/components/ParameterInput/ParameterInput.tsx @@ -124,6 +124,7 @@ const ParameterField: React.FC = ({ size="small" disabled={disabled} placeholder={schema.default_source_value} + defaultValue={defaultValue ?? schema.default_source_value} onChange={(event) => { onChange(event.target.value) }} diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index 0b3f96d2e992e..32567e7f9d6c9 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -27,7 +27,7 @@ const renderCreateWorkspacePage = () => { } describe("CreateWorkspacePage", () => { - /*it("renders", async () => { + it("renders", async () => { jest .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValueOnce([MockTemplateVersionParameter1]) @@ -70,7 +70,7 @@ describe("CreateWorkspacePage", () => { }, ), ) - })*/ + }) it("uses default param values passed from the URL", async () => { const param = "dotfile_uri" @@ -81,13 +81,18 @@ describe("CreateWorkspacePage", () => { default_source_value: "", }), ]) - renderWithAuth(, { + jest + .spyOn(API, "getTemplateVersionRichParameters") + .mockResolvedValueOnce([MockTemplateVersionParameter1]) + + await waitFor(() => renderWithAuth(, { route: "/templates/" + MockTemplate.name + `/workspace?param.${param}=${paramValue}`, path: "/templates/:template/workspace", - }) + })) + await screen.findByDisplayValue(paramValue) }) }) From 131149319f9f5f36fbd552246a69f5f596b7a190 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 27 Jan 2023 11:40:47 +0100 Subject: [PATCH 15/46] Fix: fmt --- .../CreateWorkspacePage.test.tsx | 20 ++++++++----- site/src/testHelpers/entities.ts | 29 ++++++++++--------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index 32567e7f9d6c9..5c2e06352e726 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -36,7 +36,9 @@ describe("CreateWorkspacePage", () => { const element = screen.findByText("Create workspace") expect(element).toBeDefined() - const firstParameter = screen.findByText(MockTemplateVersionParameter1.description) + const firstParameter = screen.findByText( + MockTemplateVersionParameter1.description, + ) expect(firstParameter).toBeDefined() }) @@ -85,13 +87,15 @@ describe("CreateWorkspacePage", () => { .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValueOnce([MockTemplateVersionParameter1]) - await waitFor(() => renderWithAuth(, { - route: - "/templates/" + - MockTemplate.name + - `/workspace?param.${param}=${paramValue}`, - path: "/templates/:template/workspace", - })) + await waitFor(() => + renderWithAuth(, { + route: + "/templates/" + + MockTemplate.name + + `/workspace?param.${param}=${paramValue}`, + path: "/templates/:template/workspace", + }), + ) await screen.findByDisplayValue(paramValue) }) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index af6a06c87c177..9abfa840e86d5 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -624,19 +624,20 @@ export const MockWorkspacesResponse: TypesGen.WorkspacesResponse = { count: 26, } -export const MockTemplateVersionParameter1: TypesGen.TemplateVersionParameter = { - name: "first_parameter", - type: "string", - description: "This is first parameter", - default_value: "abc", - mutable: true, - icon: "/icons/icon.svg", - options: [], - validation_error: "", - validation_regex: "", - validation_min: 0, - validation_max: 0, -} +export const MockTemplateVersionParameter1: TypesGen.TemplateVersionParameter = + { + name: "first_parameter", + type: "string", + description: "This is first parameter", + default_value: "abc", + mutable: true, + icon: "/icons/icon.svg", + options: [], + validation_error: "", + validation_regex: "", + validation_min: 0, + validation_max: 0, + } // requests the MockWorkspace export const MockWorkspaceRequest: TypesGen.CreateWorkspaceRequest = { @@ -647,7 +648,7 @@ export const MockWorkspaceRequest: TypesGen.CreateWorkspaceRequest = { { name: MockTemplateVersionParameter1.name, value: MockTemplateVersionParameter1.default_value, - } + }, ], } From feeb10c40937f4d4f28d6bf6818dea5c865067c0 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 27 Jan 2023 12:53:52 +0100 Subject: [PATCH 16/46] URL param --- .../RichParameterInput/RichParameterInput.tsx | 10 +++-- .../CreateWorkspacePageView.tsx | 37 +++++++++++++++++-- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index 2abf549fd0b8e..a562a94d4851b 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -50,12 +50,14 @@ export interface RichParameterInputProps { disabled?: boolean parameter: TemplateVersionParameter onChange: (value: string) => void + defaultValue?: string } export const RichParameterInput: FC = ({ disabled, onChange, parameter, + defaultValue, ...props }) => { const styles = useStyles() @@ -69,6 +71,7 @@ export const RichParameterInput: FC = ({ disabled={disabled} onChange={onChange} parameter={parameter} + defaultValue={defaultValue} />

@@ -79,15 +82,16 @@ const RichParameterField: React.FC = ({ disabled, onChange, parameter, + defaultValue, ...props }) => { - const [parameterValue, setParameterValue] = useState(parameter.default_value) + const [parameterValue, setParameterValue] = useState(defaultValue) const styles = useStyles() if (isBoolean(parameter)) { return ( { onChange(event.target.value) }} @@ -111,7 +115,7 @@ const RichParameterField: React.FC = ({ if (parameter.options.length > 0) { return ( { onChange(event.target.value) }} diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 306fb6057ecf3..390cea0eb674c 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -52,6 +52,10 @@ export const CreateWorkspacePageView: FC< const [parameterValues, setParameterValues] = useState< Record >(props.defaultParameterValues ?? {}) + const defaultRichParameterValues = extractRichParametersValues( + props.templateParameters, + props.defaultParameterValues, + ) const { t } = useTranslation("createWorkspacePage") @@ -60,7 +64,7 @@ export const CreateWorkspacePageView: FC< initialValues: { name: "", template_id: props.selectedTemplate ? props.selectedTemplate.id : "", - rich_parameter_values: defaultRichParameters(props.templateParameters), + rich_parameter_values: defaultRichParameterValues, }, validationSchema: ValidationSchemaForRichParameters( props.templateParameters, @@ -281,6 +285,10 @@ export const CreateWorkspacePageView: FC< }) }} parameter={parameter} + defaultValue={initialRichParameterValue( + defaultRichParameterValues, + parameter, + )} /> ))} @@ -370,8 +378,9 @@ const useFormFooterStyles = makeStyles((theme) => ({ }, })) -const defaultRichParameters = ( +const extractRichParametersValues = ( templateParameters?: TypesGen.TemplateVersionParameter[], + defaultValuesFromQuery?: Record, ): TypesGen.WorkspaceBuildParameter[] => { const defaults: TypesGen.WorkspaceBuildParameter[] = [] if (!templateParameters) { @@ -380,23 +389,43 @@ const defaultRichParameters = ( templateParameters.forEach((parameter) => { if (parameter.options.length > 0) { + let parameterValue = parameter.options[0].value + if (defaultValuesFromQuery && defaultValuesFromQuery[parameter.name]) { + parameterValue = defaultValuesFromQuery[parameter.name] + } + const buildParameter: TypesGen.WorkspaceBuildParameter = { name: parameter.name, - value: parameter.options[0].value, + value: parameterValue, } defaults.push(buildParameter) return } + let parameterValue = parameter.default_value + if (defaultValuesFromQuery && defaultValuesFromQuery[parameter.name]) { + parameterValue = defaultValuesFromQuery[parameter.name] + } + const buildParameter: TypesGen.WorkspaceBuildParameter = { name: parameter.name, - value: parameter.default_value || "", + value: parameterValue || "", } defaults.push(buildParameter) }) return defaults } +const initialRichParameterValue = ( + workspaceBuildParameters: TypesGen.WorkspaceBuildParameter[], + parameter: TypesGen.TemplateVersionParameter, +): string => { + const buildParameter = workspaceBuildParameters.find((buildParameter) => { + return buildParameter.name === parameter.name + }) + return (buildParameter && buildParameter.value) || "" +} + const ValidationSchemaForRichParameters = ( templateParameters?: TypesGen.TemplateVersionParameter[], ): Yup.AnySchema => { From 64d3016b1178da4a5a011277ab303b36827218f8 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 27 Jan 2023 13:09:04 +0100 Subject: [PATCH 17/46] Refactor --- .../RichParameterInput/RichParameterInput.tsx | 10 +++++----- .../CreateWorkspacePage/CreateWorkspacePageView.tsx | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index a562a94d4851b..17c0e40e5c207 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -50,14 +50,14 @@ export interface RichParameterInputProps { disabled?: boolean parameter: TemplateVersionParameter onChange: (value: string) => void - defaultValue?: string + initialValue?: string } export const RichParameterInput: FC = ({ disabled, onChange, parameter, - defaultValue, + initialValue, ...props }) => { const styles = useStyles() @@ -71,7 +71,7 @@ export const RichParameterInput: FC = ({ disabled={disabled} onChange={onChange} parameter={parameter} - defaultValue={defaultValue} + initialValue={initialValue} />
@@ -82,10 +82,10 @@ const RichParameterField: React.FC = ({ disabled, onChange, parameter, - defaultValue, + initialValue, ...props }) => { - const [parameterValue, setParameterValue] = useState(defaultValue) + const [parameterValue, setParameterValue] = useState(initialValue) const styles = useStyles() if (isBoolean(parameter)) { diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 390cea0eb674c..2719218cb8296 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -52,7 +52,7 @@ export const CreateWorkspacePageView: FC< const [parameterValues, setParameterValues] = useState< Record >(props.defaultParameterValues ?? {}) - const defaultRichParameterValues = extractRichParametersValues( + const initialRichParameterValues = selectInitialRichParametersValues( props.templateParameters, props.defaultParameterValues, ) @@ -64,7 +64,7 @@ export const CreateWorkspacePageView: FC< initialValues: { name: "", template_id: props.selectedTemplate ? props.selectedTemplate.id : "", - rich_parameter_values: defaultRichParameterValues, + rich_parameter_values: initialRichParameterValues, }, validationSchema: ValidationSchemaForRichParameters( props.templateParameters, @@ -285,8 +285,8 @@ export const CreateWorkspacePageView: FC< }) }} parameter={parameter} - defaultValue={initialRichParameterValue( - defaultRichParameterValues, + initialValue={workspaceBuildParameterValue( + initialRichParameterValues, parameter, )} /> @@ -378,7 +378,7 @@ const useFormFooterStyles = makeStyles((theme) => ({ }, })) -const extractRichParametersValues = ( +const selectInitialRichParametersValues = ( templateParameters?: TypesGen.TemplateVersionParameter[], defaultValuesFromQuery?: Record, ): TypesGen.WorkspaceBuildParameter[] => { @@ -416,7 +416,7 @@ const extractRichParametersValues = ( return defaults } -const initialRichParameterValue = ( +const workspaceBuildParameterValue = ( workspaceBuildParameters: TypesGen.WorkspaceBuildParameter[], parameter: TypesGen.TemplateVersionParameter, ): string => { From ac61aae577599fa04bfa70170ec366b0ffcceb81 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 27 Jan 2023 13:13:22 +0100 Subject: [PATCH 18/46] Test: rich param value --- .../CreateWorkspacePage.test.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index 5c2e06352e726..5a85044f2385e 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -99,4 +99,30 @@ describe("CreateWorkspacePage", () => { await screen.findByDisplayValue(paramValue) }) + + it("uses default rich param values passed from the URL", async () => { + const param = "first_parameter" + const paramValue = "It works!" + jest.spyOn(API, "getTemplateVersionSchema").mockResolvedValueOnce([ + mockParameterSchema({ + name: param, + default_source_value: "", + }), + ]) + jest + .spyOn(API, "getTemplateVersionRichParameters") + .mockResolvedValueOnce([MockTemplateVersionParameter1]) + + await waitFor(() => + renderWithAuth(, { + route: + "/templates/" + + MockTemplate.name + + `/workspace?param.${param}=${paramValue}`, + path: "/templates/:template/workspace", + }), + ) + + await screen.findByDisplayValue(paramValue) + }) }) From a1f22e95cbeb711979f374e554aefc2f373e354e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 27 Jan 2023 14:07:49 +0100 Subject: [PATCH 19/46] Storybook --- .../RichParameterInput.stories.tsx | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 site/src/components/RichParameterInput/RichParameterInput.stories.tsx diff --git a/site/src/components/RichParameterInput/RichParameterInput.stories.tsx b/site/src/components/RichParameterInput/RichParameterInput.stories.tsx new file mode 100644 index 0000000000000..f4e1cf20a7c16 --- /dev/null +++ b/site/src/components/RichParameterInput/RichParameterInput.stories.tsx @@ -0,0 +1,92 @@ +import { Story } from "@storybook/react" +import { TemplateVersionParameter } from "api/typesGenerated" +import { + RichParameterInput, + RichParameterInputProps, +} from "./RichParameterInput" + +export default { + title: "components/ParameterInput", + component: RichParameterInput, +} + +const Template: Story = ( + args: RichParameterInputProps, +) => + +const createTemplateVersionParameter = ( + partial: Partial, +): TemplateVersionParameter => { + return { + name: "first_parameter", + description: "This is first parameter.", + type: "string", + mutable: false, + default_value: "default string", + icon: "/icon/folder.svg", + options: [], + validation_error: "", + validation_regex: "", + validation_min: 0, + validation_max: 0, + + ...partial, + } +} + +export const Basic = Template.bind({}) +Basic.args = { + initialValue: "initial-value", + parameter: createTemplateVersionParameter({ + name: "project_name", + description: + "Customize the name of a Google Cloud project that will be created!", + }), +} + +export const NumberType = Template.bind({}) +NumberType.args = { + parameter: createTemplateVersionParameter({ + name: "number_parameter", + type: "number", + description: "Numeric parameter", + }), +} + +export const BooleanType = Template.bind({}) +BooleanType.args = { + parameter: createTemplateVersionParameter({ + name: "bool_parameter", + type: "bool", + description: "Boolean parameter", + }), +} + +export const OptionsType = Template.bind({}) +BooleanType.args = { + parameter: createTemplateVersionParameter({ + name: "options_parameter", + type: "string", + description: "Parameter with options", + options: [ + { + name: "First option", + value: "first_option", + description: "This is option 1", + icon: "", + }, + { + name: "Second option", + value: "second_option", + description: "This is option 3", + icon: "/icon/database.svg", + }, + { + name: "Third option", + value: "third_option", + description: "This is option 3", + icon: "/icon/aws.png", + }, + ], + }), +} From dfec8ca9cfb63cb10a4feea39c0c03cb08427220 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 27 Jan 2023 14:49:18 +0100 Subject: [PATCH 20/46] Fix --- .../RichParameterInput/RichParameterInput.stories.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/components/RichParameterInput/RichParameterInput.stories.tsx b/site/src/components/RichParameterInput/RichParameterInput.stories.tsx index f4e1cf20a7c16..ec5e525195a7a 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.stories.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.stories.tsx @@ -6,7 +6,7 @@ import { } from "./RichParameterInput" export default { - title: "components/ParameterInput", + title: "components/RichParameterInput", component: RichParameterInput, } @@ -63,7 +63,7 @@ BooleanType.args = { } export const OptionsType = Template.bind({}) -BooleanType.args = { +OptionsType.args = { parameter: createTemplateVersionParameter({ name: "options_parameter", type: "string", @@ -78,7 +78,7 @@ BooleanType.args = { { name: "Second option", value: "second_option", - description: "This is option 3", + description: "This is option 2", icon: "/icon/database.svg", }, { From 2e321df0e12de39fee0db96d9cadd0dfad3ae587 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 27 Jan 2023 16:29:05 +0100 Subject: [PATCH 21/46] Refactor for testing purposes --- .../RichParameterInput.stories.tsx | 3 + .../RichParameterInput/RichParameterInput.tsx | 62 ++++++++++--------- .../CreateWorkspacePage.test.tsx | 31 +++++++++- .../CreateWorkspacePageView.tsx | 1 + site/src/testHelpers/entities.ts | 15 +++++ 5 files changed, 81 insertions(+), 31 deletions(-) diff --git a/site/src/components/RichParameterInput/RichParameterInput.stories.tsx b/site/src/components/RichParameterInput/RichParameterInput.stories.tsx index ec5e525195a7a..65cbd160d7c69 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.stories.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.stories.tsx @@ -46,6 +46,7 @@ Basic.args = { export const NumberType = Template.bind({}) NumberType.args = { + initialValue: "4", parameter: createTemplateVersionParameter({ name: "number_parameter", type: "number", @@ -55,6 +56,7 @@ NumberType.args = { export const BooleanType = Template.bind({}) BooleanType.args = { + initialValue: "false", parameter: createTemplateVersionParameter({ name: "bool_parameter", type: "bool", @@ -64,6 +66,7 @@ BooleanType.args = { export const OptionsType = Template.bind({}) OptionsType.args = { + initialValue: "first_option", parameter: createTemplateVersionParameter({ name: "options_parameter", type: "string", diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index 17c0e40e5c207..6370be0f507c6 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -11,42 +11,42 @@ const isBoolean = (parameter: TemplateVersionParameter) => { return parameter.type === "bool" } -const ParameterLabel: React.FC<{ parameter: TemplateVersionParameter }> = ({ - parameter, -}) => { - const styles = useStyles() +export interface ParameterLabelProps { + index: number + parameter: TemplateVersionParameter +} - if (parameter.name && parameter.description) { - return ( - - ) - } +const ParameterLabel: FC = ({ index, parameter }) => { + const styles = useStyles() return ( - + + + {parameter.icon && ( + + Parameter icon + + )} + + + + + {parameter.description} + ) } export interface RichParameterInputProps { + index: number disabled?: boolean parameter: TemplateVersionParameter onChange: (value: string) => void @@ -54,6 +54,7 @@ export interface RichParameterInputProps { } export const RichParameterInput: FC = ({ + index, disabled, onChange, parameter, @@ -64,10 +65,11 @@ export const RichParameterInput: FC = ({ return ( - +
{ } describe("CreateWorkspacePage", () => { - it("renders", async () => { + it("renders with rich parameter", async () => { jest .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValueOnce([MockTemplateVersionParameter1]) @@ -125,4 +126,32 @@ describe("CreateWorkspacePage", () => { await screen.findByDisplayValue(paramValue) }) + + it("renders with rich parameter", async () => { + jest + .spyOn(API, "getTemplateVersionRichParameters") + .mockResolvedValueOnce([ + MockTemplateVersionParameter1, + MockTemplateVersionParameter2, + ]) + + await waitFor(() => renderCreateWorkspacePage()) + + const element = screen.findByText("Create workspace") + expect(element).toBeDefined() + const secondParameter = screen.findByText( + MockTemplateVersionParameter2.description, + ) + expect(secondParameter).toBeDefined() + + const secondParameterField = await screen.findByLabelText( + MockTemplateVersionParameter2.name, + ) + fireEvent.change(secondParameterField, { + target: { value: "4" }, + }) + + const validationError = screen.findByText("Value must be between") + expect(validationError).toBeDefined() + }) }) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 2719218cb8296..da2116e270698 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -277,6 +277,7 @@ export const CreateWorkspacePageView: FC< "rich_parameter_values[" + index + "].value", )} disabled={form.isSubmitting} + index={index} key={parameter.name} onChange={(value) => { form.setFieldValue("rich_parameter_values." + index, { diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 9abfa840e86d5..fc14a7582efd6 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -639,6 +639,21 @@ export const MockTemplateVersionParameter1: TypesGen.TemplateVersionParameter = validation_max: 0, } +export const MockTemplateVersionParameter2: TypesGen.TemplateVersionParameter = + { + name: "second_parameter", + type: "number", + description: "This is second parameter", + default_value: "2", + mutable: true, + icon: "/icons/folder.svg", + options: [], + validation_error: "", + validation_regex: "", + validation_min: 1, + validation_max: 3, + } + // requests the MockWorkspace export const MockWorkspaceRequest: TypesGen.CreateWorkspaceRequest = { name: "test", From f41de128cc0aa9c09f6a829dd712f452a94850ea Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 27 Jan 2023 16:29:44 +0100 Subject: [PATCH 22/46] Typo --- site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index 3cba5552259b0..9c69227d98ff1 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -127,7 +127,7 @@ describe("CreateWorkspacePage", () => { await screen.findByDisplayValue(paramValue) }) - it("renders with rich parameter", async () => { + it("rich parameter: number validation fails", async () => { jest .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValueOnce([ From 40fa739a6c29d0882a5bca73b36ded25ab5616ac Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 27 Jan 2023 16:40:30 +0100 Subject: [PATCH 23/46] test: string validation --- .../CreateWorkspacePage.test.tsx | 31 +++++++++++++++++++ site/src/testHelpers/entities.ts | 15 +++++++++ 2 files changed, 46 insertions(+) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index 9c69227d98ff1..80f29caef85b2 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -11,6 +11,7 @@ import { MockWorkspaceRequest, MockTemplateVersionParameter1, MockTemplateVersionParameter2, + MockTemplateVersionParameter3, } from "testHelpers/entities" import { renderWithAuth } from "testHelpers/renderHelpers" import CreateWorkspacePage from "./CreateWorkspacePage" @@ -154,4 +155,34 @@ describe("CreateWorkspacePage", () => { const validationError = screen.findByText("Value must be between") expect(validationError).toBeDefined() }) + + it("rich parameter: string validation fails", async () => { + jest + .spyOn(API, "getTemplateVersionRichParameters") + .mockResolvedValueOnce([ + MockTemplateVersionParameter1, + MockTemplateVersionParameter3, + ]) + + await waitFor(() => renderCreateWorkspacePage()) + + const element = screen.findByText("Create workspace") + expect(element).toBeDefined() + const thirdParameter = screen.findByText( + MockTemplateVersionParameter3.description, + ) + expect(thirdParameter).toBeDefined() + + const thirdParameterField = await screen.findByLabelText( + MockTemplateVersionParameter3.name, + ) + fireEvent.change(thirdParameterField, { + target: { value: "1234" }, + }) + + const validationError = screen.findByText( + MockTemplateVersionParameter3.validation_error, + ) + expect(validationError).toBeDefined() + }) }) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index fc14a7582efd6..6e8f8d36769ca 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -654,6 +654,21 @@ export const MockTemplateVersionParameter2: TypesGen.TemplateVersionParameter = validation_max: 3, } +export const MockTemplateVersionParameter3: TypesGen.TemplateVersionParameter = + { + name: "third_parameter", + type: "string", + description: "This is third parameter", + default_value: "aaa", + mutable: true, + icon: "/icons/folder.svg", + options: [], + validation_error: "No way!", + validation_regex: "^[a-z]{3}$", + validation_min: 0, + validation_max: 0, + } + // requests the MockWorkspace export const MockWorkspaceRequest: TypesGen.CreateWorkspaceRequest = { name: "test", From d9f03b6b165567a69e1fb1df3e41703190945744 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 30 Jan 2023 14:36:50 +0100 Subject: [PATCH 24/46] Button: build parameters --- .../components/DropdownButton/ActionCtas.tsx | 18 ++++++++++++++++++ site/src/components/Workspace/Workspace.tsx | 3 +++ .../WorkspaceActions/WorkspaceActions.tsx | 6 ++++++ .../components/WorkspaceActions/constants.ts | 5 +++++ site/src/i18n/en/workspacePage.json | 3 ++- 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/site/src/components/DropdownButton/ActionCtas.tsx b/site/src/components/DropdownButton/ActionCtas.tsx index 1ccbb82ed2b72..8be23778a6a85 100644 --- a/site/src/components/DropdownButton/ActionCtas.tsx +++ b/site/src/components/DropdownButton/ActionCtas.tsx @@ -4,6 +4,7 @@ import { makeStyles } from "@material-ui/core/styles" import BlockIcon from "@material-ui/icons/Block" import CloudQueueIcon from "@material-ui/icons/CloudQueue" import UpdateOutlined from "@material-ui/icons/UpdateOutlined" +import SettingsOutlined from "@material-ui/icons/SettingsOutlined" import CropSquareIcon from "@material-ui/icons/CropSquare" import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline" import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline" @@ -51,6 +52,23 @@ export const ChangeVersionButton: FC< ) } +export const BuildParametersButton: FC< + React.PropsWithChildren +> = ({ handleAction }) => { + const styles = useStyles() + const { t } = useTranslation("workspacePage") + + return ( + + ) +} + export const StartButton: FC> = ({ handleAction, }) => { diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 63d0d671efcf4..11253cd1576e5 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -45,6 +45,7 @@ export interface WorkspaceProps { handleUpdate: () => void handleCancel: () => void handleChangeVersion: () => void + handleBuildParameters: () => void isUpdating: boolean workspace: TypesGen.Workspace resources?: TypesGen.WorkspaceResource[] @@ -70,6 +71,7 @@ export const Workspace: FC> = ({ handleUpdate, handleCancel, handleChangeVersion, + handleBuildParameters, workspace, isUpdating, resources, @@ -145,6 +147,7 @@ export const Workspace: FC> = ({ handleUpdate={handleUpdate} handleCancel={handleCancel} handleChangeVersion={handleChangeVersion} + handleBuildParameters={handleBuildParameters} isUpdating={isUpdating} /> diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index 0f5064fe82229..c86d477f0da3c 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -7,6 +7,7 @@ import { ChangeVersionButton, DeleteButton, DisabledButton, + BuildParametersButton, StartButton, StopButton, UpdateButton, @@ -22,6 +23,7 @@ export interface WorkspaceActionsProps { handleUpdate: () => void handleCancel: () => void handleChangeVersion: () => void + handleBuildParameters: () => void isUpdating: boolean children?: ReactNode } @@ -35,6 +37,7 @@ export const WorkspaceActions: FC = ({ handleUpdate, handleCancel, handleChangeVersion, + handleBuildParameters, isUpdating, }) => { const { t } = useTranslation("workspacePage") @@ -51,6 +54,9 @@ export const WorkspaceActions: FC = ({ [ButtonTypesEnum.changeVersion]: ( ), + [ButtonTypesEnum.buildParameters]: ( + + ), [ButtonTypesEnum.start]: , [ButtonTypesEnum.starting]: ( diff --git a/site/src/components/WorkspaceActions/constants.ts b/site/src/components/WorkspaceActions/constants.ts index 5d5c59434d48b..abb2af5987bbc 100644 --- a/site/src/components/WorkspaceActions/constants.ts +++ b/site/src/components/WorkspaceActions/constants.ts @@ -12,6 +12,7 @@ export enum ButtonTypesEnum { update = "update", updating = "updating", changeVersion = "changeVersion", + buildParameters = "buildParameters", // disabled buttons canceling = "canceling", deleted = "deleted", @@ -37,6 +38,7 @@ export const statusToAbilities: Record = { running: { actions: [ ButtonTypesEnum.stop, + ButtonTypesEnum.buildParameters, ButtonTypesEnum.changeVersion, ButtonTypesEnum.delete, ], @@ -51,6 +53,7 @@ export const statusToAbilities: Record = { stopped: { actions: [ ButtonTypesEnum.start, + ButtonTypesEnum.buildParameters, ButtonTypesEnum.changeVersion, ButtonTypesEnum.delete, ], @@ -61,6 +64,7 @@ export const statusToAbilities: Record = { actions: [ ButtonTypesEnum.start, ButtonTypesEnum.stop, + ButtonTypesEnum.buildParameters, ButtonTypesEnum.changeVersion, ButtonTypesEnum.delete, ], @@ -71,6 +75,7 @@ export const statusToAbilities: Record = { failed: { actions: [ ButtonTypesEnum.start, + ButtonTypesEnum.buildParameters, ButtonTypesEnum.changeVersion, ButtonTypesEnum.delete, ], diff --git a/site/src/i18n/en/workspacePage.json b/site/src/i18n/en/workspacePage.json index 17f7e2aba3941..bb69d7524b429 100644 --- a/site/src/i18n/en/workspacePage.json +++ b/site/src/i18n/en/workspacePage.json @@ -28,7 +28,8 @@ "starting": "Starting...", "stopping": "Stopping...", "deleting": "Deleting...", - "changeVersion": "Change version" + "changeVersion": "Change version", + "buildParameters": "Build parameters" }, "disabledButton": { "canceling": "Canceling", From 17bee41c7f57fd1b13d73dea0b4f5ab9d74d8534 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 30 Jan 2023 15:32:50 +0100 Subject: [PATCH 25/46] Full screen page --- site/src/AppRouter.tsx | 2 ++ site/src/i18n/en/index.ts | 2 ++ .../i18n/en/workspaceBuildParametersPage.json | 3 +++ .../WorkspaceBuildParametersPage.tsx | 20 +++++++++++++++++++ .../WorkspaceBuildParametersPageView.tsx | 18 +++++++++++++++++ 5 files changed, 45 insertions(+) create mode 100644 site/src/i18n/en/workspaceBuildParametersPage.json create mode 100644 site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx create mode 100644 site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 715a4b080bb96..1f8b39293ab40 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -7,6 +7,7 @@ import GroupsPage from "pages/GroupsPage/GroupsPage" import LoginPage from "pages/LoginPage/LoginPage" import { SetupPage } from "pages/SetupPage/SetupPage" import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateSettingsPage" +import { WorkspaceBuildParametersPage } from "pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage" import TemplatesPage from "pages/TemplatesPage/TemplatesPage" import UsersPage from "pages/UsersPage/UsersPage" import WorkspacesPage from "pages/WorkspacesPage/WorkspacesPage" @@ -213,6 +214,7 @@ export const AppRouter: FC = () => { path="change-version" element={} /> + } /> diff --git a/site/src/i18n/en/index.ts b/site/src/i18n/en/index.ts index d44bb2e202e0d..8450a9050d271 100644 --- a/site/src/i18n/en/index.ts +++ b/site/src/i18n/en/index.ts @@ -11,6 +11,7 @@ import usersPage from "./usersPage.json" import templateSettingsPage from "./templateSettingsPage.json" import templateVersionPage from "./templateVersionPage.json" import loginPage from "./loginPage.json" +import workspaceBuildParametersPage from "./workspaceBuildParametersPage.json" import workspaceChangeVersionPage from "./workspaceChangeVersionPage.json" import workspaceSchedulePage from "./workspaceSchedulePage.json" import appearanceSettings from "./appearanceSettings.json" @@ -33,6 +34,7 @@ export const en = { templateSettingsPage, templateVersionPage, loginPage, + workspaceBuildParametersPage, workspaceChangeVersionPage, workspaceSchedulePage, appearanceSettings, diff --git a/site/src/i18n/en/workspaceBuildParametersPage.json b/site/src/i18n/en/workspaceBuildParametersPage.json new file mode 100644 index 0000000000000..544779209e49c --- /dev/null +++ b/site/src/i18n/en/workspaceBuildParametersPage.json @@ -0,0 +1,3 @@ +{ + "title": "Workspace build parameters" +} diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx new file mode 100644 index 0000000000000..45722022a59bc --- /dev/null +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx @@ -0,0 +1,20 @@ +import { FC } from "react" +import { Helmet } from "react-helmet-async" +import { useTranslation } from "react-i18next" +import { pageTitle } from "util/page" +import { WorkspaceBuildParametersPageView } from "./WorkspaceBuildParametersPageView" + +export const WorkspaceBuildParametersPage: FC = () => { + const { t } = useTranslation("workspaceBuildParametersPage") + + return ( + <> + + Codestin Search App + + + + ) +} diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx new file mode 100644 index 0000000000000..b89b5e7003dcc --- /dev/null +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx @@ -0,0 +1,18 @@ +import { FC } from "react" +import { FullPageForm } from "components/FullPageForm/FullPageForm" +import { useTranslation } from "react-i18next" + +export interface WorkspaceBuildParametersPageViewProps { + isLoading: boolean +} + +export const WorkspaceBuildParametersPageView: FC = ({ + isLoading, +}) => { + const { t } = useTranslation("workspaceBuildParametersPage") + + return ( + + + ) +} From b9da6e01495988b608a24c4ce94267fb78c532e1 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 30 Jan 2023 15:37:05 +0100 Subject: [PATCH 26/46] Fix: navigate --- site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index ecec679938913..bd02e8fb03c41 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -111,6 +111,7 @@ export const WorkspaceReadyPage = ({ handleUpdate={() => workspaceSend({ type: "UPDATE" })} handleCancel={() => workspaceSend({ type: "CANCEL" })} handleChangeVersion={() => navigate("change-version")} + handleBuildParameters={() => navigate("build-parameters")} resources={workspace.latest_build.resources} builds={builds} canUpdateWorkspace={canUpdateWorkspace} From f372027ddc263fc485b855f79cbcf10dfa00fbee Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 31 Jan 2023 11:53:52 +0100 Subject: [PATCH 27/46] XState done --- site/src/AppRouter.tsx | 5 +- site/src/api/api.ts | 20 ++ .../WorkspaceBuildParametersPage.tsx | 32 ++- .../WorkspaceBuildParametersPageView.tsx | 11 +- .../workspaceBuildParametersXService.ts | 223 ++++++++++++++++++ 5 files changed, 280 insertions(+), 11 deletions(-) create mode 100644 site/src/xServices/workspace/workspaceBuildParametersXService.ts diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 1f8b39293ab40..08b1973b999b6 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -214,7 +214,10 @@ export const AppRouter: FC = () => { path="change-version" element={} /> - } /> + } + /> diff --git a/site/src/api/api.ts b/site/src/api/api.ts index f514e27d46a72..5d965096b4471 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -799,3 +799,23 @@ export const updateWorkspaceVersion = async ( const template = await getTemplate(workspace.template_id) return startWorkspace(workspace.id, template.active_version_id) } + +export const getWorkspaceBuildParameters = async ( + workspaceBuildId: TypesGen.WorkspaceBuild["id"], +): Promise => { + const response = await axios.get( + `/api/v2/workspacebuilds/${workspaceBuildId}/parameters`, + ) + return response.data +} + +export const updateWorkspaceBuild = async ( + workspaceId: string, + data: TypesGen.WorkspaceBuildsRequest, +): Promise => { + const response = await axios.post( + `/api/v2/workspaces/${workspaceId}/builds`, + data, + ) + return response.data +} diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx index 45722022a59bc..cc7504841c9ed 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx @@ -2,19 +2,45 @@ import { FC } from "react" import { Helmet } from "react-helmet-async" import { useTranslation } from "react-i18next" import { pageTitle } from "util/page" +import { useMachine } from "@xstate/react" +import { useNavigate, useParams } from "react-router-dom" +import { workspaceBuildParametersMachine } from "xServices/workspace/workspaceBuildParametersXService" import { WorkspaceBuildParametersPageView } from "./WorkspaceBuildParametersPageView" export const WorkspaceBuildParametersPage: FC = () => { const { t } = useTranslation("workspaceBuildParametersPage") + const navigate = useNavigate() + const { owner: workspaceOwner, workspace: workspaceName } = useParams() as { + owner: string + workspace: string + } + const [state, send] = useMachine(workspaceBuildParametersMachine, { + context: { + workspaceOwner, + workspaceName, + }, + actions: { + onUpdateWorkspace: (_, event) => { + navigate( + `/@${event.data.workspace_owner_name}/${event.data.workspace_name}`, + ) + }, + }, + }) + const { + selectedTemplate, + templateParameters, + buildParameters, + updateWorkspaceError, + } = state.context + return ( <> Codestin Search App - + ) } diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx index b89b5e7003dcc..a9c7d4696f39c 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx @@ -6,13 +6,10 @@ export interface WorkspaceBuildParametersPageViewProps { isLoading: boolean } -export const WorkspaceBuildParametersPageView: FC = ({ - isLoading, -}) => { +export const WorkspaceBuildParametersPageView: FC< + WorkspaceBuildParametersPageViewProps +> = ({ isLoading }) => { const { t } = useTranslation("workspaceBuildParametersPage") - return ( - - - ) + return } diff --git a/site/src/xServices/workspace/workspaceBuildParametersXService.ts b/site/src/xServices/workspace/workspaceBuildParametersXService.ts new file mode 100644 index 0000000000000..5fa1d470b6cbf --- /dev/null +++ b/site/src/xServices/workspace/workspaceBuildParametersXService.ts @@ -0,0 +1,223 @@ +import { + getTemplateVersionRichParameters, + getWorkspaceByOwnerAndName, + getWorkspaceBuildParameters, + updateWorkspaceBuild, +} from "api/api" +import { + Template, + TemplateVersionParameter, + Workspace, + WorkspaceBuild, + WorkspaceBuildParameter, + WorkspaceBuildsRequest, +} from "api/typesGenerated" +import { assign, createMachine } from "xstate" + +type WorkspaceBuildParametersContext = { + workspaceOwner: string + workspaceName: string + + selectedWorkspace?: Workspace + selectedTemplate?: Template + templateParameters?: TemplateVersionParameter[] + workspaceBuildParameters?: WorkspaceBuildParameter[] + + updateWorkspaceBuildRequest?: WorkspaceBuildsRequest + + getWorkspaceError?: Error | unknown + getTemplateParametersError?: Error | unknown + getWorkspaceBuildParametersError?: Error | unknown + updateWorkspaceError?: Error | unknown +} + +type UpdateWorkspaceEvent = { + type: "UPDATE_WORKSPACE" + request: WorkspaceBuildsRequest +} + +export const workspaceBuildParametersMachine = createMachine( + { + id: "workspaceBuildParametersState", + predictableActionArguments: true, + tsTypes: + {} as import("./workspaceBuildParametersXService.typegen").Typegen0, + schema: { + context: {} as WorkspaceBuildParametersContext, + events: {} as UpdateWorkspaceEvent, + services: {} as { + getWorkspace: { + data: Workspace + } + getTemplateParameters: { + data: TemplateVersionParameter[] + } + getWorkspaceBuildParameters: { + data: WorkspaceBuildParameter[] + } + updateWorkspace: { + data: WorkspaceBuild + } + }, + }, + initial: "gettingWorkspace", + states: { + gettingWorkspace: { + entry: "clearGetWorkspaceError", + invoke: { + src: "getWorkspace", + onDone: [ + { + actions: ["assignWorkspace"], + target: "gettingTemplateParameters", + }, + ], + onError: { + actions: ["assignGetWorkspaceError"], + target: "error", + }, + }, + }, + gettingTemplateParameters: { + entry: "clearGetTemplateParametersError", + invoke: { + src: "getTemplateParameters", + onDone: [ + { + actions: ["assignTemplateParameters"], + target: "gettingWorkspaceBuildParameters", + }, + ], + onError: { + actions: ["assignGetTemplateParametersError"], + target: "error", + }, + }, + }, + gettingWorkspaceBuildParameters: { + entry: "clearGetWorkspaceBuildParametersError", + invoke: { + src: "getWorkspaceBuildParameters", + onDone: { + actions: ["assignWorkspaceBuildParameters"], + target: "fillingParams", + }, + onError: { + actions: ["assignGetWorkspaceBuildParametersError"], + target: "error", + }, + }, + }, + fillingParams: { + on: { + UPDATE_WORKSPACE: { + actions: ["assignUpdateWorkspaceBuildRequest"], + target: "updatingWorkspace", + }, + }, + }, + updatingWorkspace: { + entry: "clearUpdateWorkspaceError", + invoke: { + src: "updateWorkspace", + onDone: { + actions: ["onUpdateWorkspace"], + target: "updated", + }, + onError: { + actions: ["assignUpdateWorkspaceError"], + target: "fillingParams", + }, + }, + }, + updated: { + entry: "onUpdateWorkspace", + type: "final", + }, + error: {}, + }, + }, + { + services: { + getWorkspace: (context) => { + const { workspaceOwner, workspaceName } = context + return getWorkspaceByOwnerAndName(workspaceOwner, workspaceName) + }, + getTemplateParameters: (context) => { + const { selectedWorkspace } = context + + if (!selectedWorkspace) { + throw new Error("No workspace selected") + } + + return getTemplateVersionRichParameters( + selectedWorkspace.latest_build.template_version_id, + ) + }, + getWorkspaceBuildParameters: (context) => { + const { selectedWorkspace } = context + + if (!selectedWorkspace) { + throw new Error("No workspace selected") + } + + return getWorkspaceBuildParameters(selectedWorkspace.latest_build.id) + }, + updateWorkspace: (context) => { + const { selectedWorkspace, updateWorkspaceBuildRequest } = context + + if (!selectedWorkspace) { + throw new Error("No workspace selected") + } + + if (!updateWorkspaceBuildRequest) { + throw new Error("No workspace build request") + } + + return updateWorkspaceBuild( + selectedWorkspace.id, + updateWorkspaceBuildRequest, + ) + }, + }, + actions: { + assignWorkspace: assign({ + selectedWorkspace: (_, event) => event.data, + }), + assignTemplateParameters: assign({ + templateParameters: (_, event) => event.data, + }), + assignWorkspaceBuildParameters: assign({ + workspaceBuildParameters: (_, event) => event.data, + }), + + assignUpdateWorkspaceBuildRequest: assign({ + updateWorkspaceBuildRequest: (_, event) => event.request, + }), + assignGetWorkspaceError: assign({ + getWorkspaceError: (_, event) => event.data, + }), + clearGetWorkspaceError: assign({ + getWorkspaceError: (_) => undefined, + }), + assignGetTemplateParametersError: assign({ + getTemplateParametersError: (_, event) => event.data, + }), + clearGetTemplateParametersError: assign({ + getTemplateParametersError: (_) => undefined, + }), + clearGetWorkspaceBuildParametersError: assign({ + getWorkspaceBuildParametersError: (_) => undefined, + }), + assignGetWorkspaceBuildParametersError: assign({ + getWorkspaceBuildParametersError: (_, event) => event.data, + }), + clearUpdateWorkspaceError: assign({ + updateWorkspaceError: (_) => undefined, + }), + assignUpdateWorkspaceError: assign({ + updateWorkspaceError: (_, event) => event.data, + }), + }, + }, +) From b7c091641e9374d6d1841b5523d7409e443bf953 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 31 Jan 2023 12:08:35 +0100 Subject: [PATCH 28/46] refactor: postWorkspaceBuild --- site/src/api/api.ts | 52 ++++++++----------- .../workspaceBuildParametersXService.ts | 1 - 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 5d965096b4471..161b05939285f 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -399,26 +399,29 @@ export const getWorkspaceByOwnerAndName = async ( return response.data } -const postWorkspaceBuild = - (transition: WorkspaceBuildTransition) => - async ( - workspaceId: string, - template_version_id?: string, - ): Promise => { - const payload = { - transition, - template_version_id, - } - const response = await axios.post( - `/api/v2/workspaces/${workspaceId}/builds`, - payload, - ) - return response.data - } +export const postWorkspaceBuild = async ( + workspaceId: string, + data: TypesGen.CreateWorkspaceBuildRequest, +): Promise => { + const response = await axios.post( + `/api/v2/workspaces/${workspaceId}/builds`, + data, + ) + return response.data +} -export const startWorkspace = postWorkspaceBuild("start") -export const stopWorkspace = postWorkspaceBuild("stop") -export const deleteWorkspace = postWorkspaceBuild("delete") +export const startWorkspace = ( + workspaceId: string, + templateVersionID: string, +) => + postWorkspaceBuild(workspaceId, { + transition: "start", + template_version_id: templateVersionID, + }) +export const stopWorkspace = (workspaceId: string) => + postWorkspaceBuild(workspaceId, { transition: "stop" }) +export const deleteWorkspace = (workspaceId: string) => + postWorkspaceBuild(workspaceId, { transition: "delete" }) export const cancelWorkspaceBuild = async ( workspaceBuildId: TypesGen.WorkspaceBuild["id"], @@ -808,14 +811,3 @@ export const getWorkspaceBuildParameters = async ( ) return response.data } - -export const updateWorkspaceBuild = async ( - workspaceId: string, - data: TypesGen.WorkspaceBuildsRequest, -): Promise => { - const response = await axios.post( - `/api/v2/workspaces/${workspaceId}/builds`, - data, - ) - return response.data -} diff --git a/site/src/xServices/workspace/workspaceBuildParametersXService.ts b/site/src/xServices/workspace/workspaceBuildParametersXService.ts index 5fa1d470b6cbf..5599559aac763 100644 --- a/site/src/xServices/workspace/workspaceBuildParametersXService.ts +++ b/site/src/xServices/workspace/workspaceBuildParametersXService.ts @@ -2,7 +2,6 @@ import { getTemplateVersionRichParameters, getWorkspaceByOwnerAndName, getWorkspaceBuildParameters, - updateWorkspaceBuild, } from "api/api" import { Template, From d8f7cc3472ca1116ec81a13a90b12d34116dcde3 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 31 Jan 2023 13:44:21 +0100 Subject: [PATCH 29/46] RichParameterInput rendered --- .../CreateWorkspacePageView.tsx | 10 +- .../WorkspaceBuildParametersPage.tsx | 28 +- .../WorkspaceBuildParametersPageView.tsx | 249 +++++++++++++++++- .../workspaceBuildParametersXService.ts | 21 +- 4 files changed, 286 insertions(+), 22 deletions(-) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index da2116e270698..300356a879648 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -67,6 +67,7 @@ export const CreateWorkspacePageView: FC< rich_parameter_values: initialRichParameterValues, }, validationSchema: ValidationSchemaForRichParameters( + "createWorkspacePage", props.templateParameters, ), enableReinitialize: true, @@ -417,7 +418,7 @@ const selectInitialRichParametersValues = ( return defaults } -const workspaceBuildParameterValue = ( +export const workspaceBuildParameterValue = ( workspaceBuildParameters: TypesGen.WorkspaceBuildParameter[], parameter: TypesGen.TemplateVersionParameter, ): string => { @@ -427,17 +428,18 @@ const workspaceBuildParameterValue = ( return (buildParameter && buildParameter.value) || "" } -const ValidationSchemaForRichParameters = ( +export const ValidationSchemaForRichParameters = ( + ns: string, templateParameters?: TypesGen.TemplateVersionParameter[], ): Yup.AnySchema => { - const { t } = useTranslation("createWorkspacePage") + const { t } = useTranslation(ns) if (!templateParameters) { return Yup.object() } return Yup.object({ - name: nameValidator(t("nameLabel", { ns: "createWorkspacePage" })), + name: nameValidator(t("nameLabel", { ns })), rich_parameter_values: Yup.array() .of( Yup.object().shape({ diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx index cc7504841c9ed..079b85f02a470 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx @@ -5,7 +5,11 @@ import { pageTitle } from "util/page" import { useMachine } from "@xstate/react" import { useNavigate, useParams } from "react-router-dom" import { workspaceBuildParametersMachine } from "xServices/workspace/workspaceBuildParametersXService" -import { WorkspaceBuildParametersPageView } from "./WorkspaceBuildParametersPageView" +import { + UpdateWorkspaceErrors, + WorkspaceBuildParametersPageView, +} from "./WorkspaceBuildParametersPageView" +import { getWorkspaceBuildParameters } from "api/api" export const WorkspaceBuildParametersPage: FC = () => { const { t } = useTranslation("workspaceBuildParametersPage") @@ -29,9 +33,12 @@ export const WorkspaceBuildParametersPage: FC = () => { }, }) const { - selectedTemplate, + selectedWorkspace, templateParameters, - buildParameters, + workspaceBuildParameters, + getWorkspaceError, + getTemplateParametersError, + getWorkspaceBuildParametersError, updateWorkspaceError, } = state.context @@ -40,7 +47,20 @@ export const WorkspaceBuildParametersPage: FC = () => { Codestin Search App - + ) } diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx index a9c7d4696f39c..320f6a12ffef4 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx @@ -1,15 +1,256 @@ import { FC } from "react" import { FullPageForm } from "components/FullPageForm/FullPageForm" import { useTranslation } from "react-i18next" +import * as TypesGen from "api/typesGenerated" +import { AlertBanner } from "components/AlertBanner/AlertBanner" +import { Stack } from "components/Stack/Stack" +import { makeStyles } from "@material-ui/core/styles" +import { getFormHelpers } from "util/formUtils" +import { FormikContextType, FormikTouched, useFormik } from "formik" +import { RichParameterInput } from "components/RichParameterInput/RichParameterInput" +import { + ValidationSchemaForRichParameters, + workspaceBuildParameterValue, +} from "pages/CreateWorkspacePage/CreateWorkspacePageView" + +export enum UpdateWorkspaceErrors { + GET_WORKSPACE_ERROR = "getWorkspaceError", + GET_TEMPLATE_PARAMETERS_ERROR = "getTemplateParametersError", + GET_WORKSPACE_BUILD_PARAMETERS_ERROR = "getWorkspaceBuildParametersError", + UPDATE_WORKSPACE_ERROR = "updateWorkspaceError", +} export interface WorkspaceBuildParametersPageViewProps { - isLoading: boolean + workspace?: TypesGen.Workspace + templateParameters?: TypesGen.TemplateVersionParameter[] + workspaceBuildParameters?: TypesGen.WorkspaceBuildParameter[] + + initialTouched?: FormikTouched + + hasErrors: boolean + updateWorkspaceErrors: Partial> } export const WorkspaceBuildParametersPageView: FC< - WorkspaceBuildParametersPageViewProps -> = ({ isLoading }) => { + React.PropsWithChildren +> = (props) => { const { t } = useTranslation("workspaceBuildParametersPage") + const styles = useStyles() + + const initialRichParameterValues = selectInitialRichParametersValues( + props.templateParameters, + props.workspaceBuildParameters, + ) + + const form: FormikContextType = + useFormik({ + initialValues: { + template_version_id: props.workspace + ? props.workspace.latest_build.template_version_id + : "", + transition: "start", + rich_parameter_values: initialRichParameterValues, + }, + validationSchema: ValidationSchemaForRichParameters( + "workspaceBuildParametersPage", + props.templateParameters, + ), + enableReinitialize: true, + initialTouched: props.initialTouched, + onSubmit: () => { + form.setSubmitting(false) + }, + }) + + const getFieldHelpers = getFormHelpers( + form, + props.updateWorkspaceErrors[UpdateWorkspaceErrors.UPDATE_WORKSPACE_ERROR], + ) + + if (props.hasErrors) { + return ( + + {Boolean( + props.updateWorkspaceErrors[ + UpdateWorkspaceErrors.GET_WORKSPACE_ERROR + ], + ) && ( + + )} + {Boolean( + props.updateWorkspaceErrors[ + UpdateWorkspaceErrors.GET_TEMPLATE_PARAMETERS_ERROR + ], + ) && ( + + )} + {Boolean( + props.updateWorkspaceErrors[ + UpdateWorkspaceErrors.GET_WORKSPACE_BUILD_PARAMETERS_ERROR + ], + ) && ( + + )} + + ) + } + + if ( + props.updateWorkspaceErrors[UpdateWorkspaceErrors.UPDATE_WORKSPACE_ERROR] + ) { + return ( + + ) + } + + return ( + + {props.templateParameters && ( +
+ + {props.templateParameters.map((parameter, index) => ( + { + form.setFieldValue("rich_parameter_values." + index, { + name: parameter.name, + value: value, + }) + }} + parameter={parameter} + initialValue={workspaceBuildParameterValue( + initialRichParameterValues, + parameter, + )} + /> + ))} + +
+ )} +
+ ) +} + +const selectInitialRichParametersValues = ( + templateParameters?: TypesGen.TemplateVersionParameter[], + workspaceBuildParameters?: TypesGen.WorkspaceBuildParameter[], +): TypesGen.WorkspaceBuildParameter[] => { + const defaults: TypesGen.WorkspaceBuildParameter[] = [] + if (!templateParameters) { + return defaults + } + + templateParameters.forEach((parameter) => { + if (parameter.options.length > 0) { + let parameterValue = parameter.options[0].value + if (workspaceBuildParameters) { + const foundBuildParameter = workspaceBuildParameters.find( + (buildParameter) => { + return buildParameter.name === parameter.name + }, + ) + if (foundBuildParameter) { + parameterValue = foundBuildParameter.value + } + } + + const buildParameter: TypesGen.WorkspaceBuildParameter = { + name: parameter.name, + value: parameterValue, + } + defaults.push(buildParameter) + return + } + + let parameterValue = parameter.default_value + if (workspaceBuildParameters) { + const foundBuildParameter = workspaceBuildParameters.find( + (buildParameter) => { + return buildParameter.name === parameter.name + }, + ) + if (foundBuildParameter) { + parameterValue = foundBuildParameter.value + } + } - return + const buildParameter: TypesGen.WorkspaceBuildParameter = { + name: parameter.name, + value: parameterValue || "", + } + defaults.push(buildParameter) + }) + return defaults } + +const useStyles = makeStyles(() => ({ + formSection: { + marginTop: 28, + }, + + formSectionFields: { + width: "100%", + }, +})) + +const useFormFooterStyles = makeStyles((theme) => ({ + button: { + minWidth: theme.spacing(23), + + [theme.breakpoints.down("sm")]: { + width: "100%", + }, + }, + footer: { + display: "flex", + alignItems: "center", + justifyContent: "flex-start", + flexDirection: "row-reverse", + gap: theme.spacing(2), + + [theme.breakpoints.down("sm")]: { + flexDirection: "column", + gap: theme.spacing(1), + }, + }, +})) diff --git a/site/src/xServices/workspace/workspaceBuildParametersXService.ts b/site/src/xServices/workspace/workspaceBuildParametersXService.ts index 5599559aac763..b4b2e7fad42f1 100644 --- a/site/src/xServices/workspace/workspaceBuildParametersXService.ts +++ b/site/src/xServices/workspace/workspaceBuildParametersXService.ts @@ -2,14 +2,15 @@ import { getTemplateVersionRichParameters, getWorkspaceByOwnerAndName, getWorkspaceBuildParameters, + postWorkspaceBuild, } from "api/api" import { + CreateWorkspaceBuildRequest, Template, TemplateVersionParameter, Workspace, WorkspaceBuild, WorkspaceBuildParameter, - WorkspaceBuildsRequest, } from "api/typesGenerated" import { assign, createMachine } from "xstate" @@ -22,7 +23,7 @@ type WorkspaceBuildParametersContext = { templateParameters?: TemplateVersionParameter[] workspaceBuildParameters?: WorkspaceBuildParameter[] - updateWorkspaceBuildRequest?: WorkspaceBuildsRequest + createWorkspaceBuildRequest?: CreateWorkspaceBuildRequest getWorkspaceError?: Error | unknown getTemplateParametersError?: Error | unknown @@ -32,7 +33,7 @@ type WorkspaceBuildParametersContext = { type UpdateWorkspaceEvent = { type: "UPDATE_WORKSPACE" - request: WorkspaceBuildsRequest + request: CreateWorkspaceBuildRequest } export const workspaceBuildParametersMachine = createMachine( @@ -110,7 +111,7 @@ export const workspaceBuildParametersMachine = createMachine( fillingParams: { on: { UPDATE_WORKSPACE: { - actions: ["assignUpdateWorkspaceBuildRequest"], + actions: ["assignCreateWorkspaceBuildRequest"], target: "updatingWorkspace", }, }, @@ -163,19 +164,19 @@ export const workspaceBuildParametersMachine = createMachine( return getWorkspaceBuildParameters(selectedWorkspace.latest_build.id) }, updateWorkspace: (context) => { - const { selectedWorkspace, updateWorkspaceBuildRequest } = context + const { selectedWorkspace, createWorkspaceBuildRequest } = context if (!selectedWorkspace) { throw new Error("No workspace selected") } - if (!updateWorkspaceBuildRequest) { + if (!createWorkspaceBuildRequest) { throw new Error("No workspace build request") } - return updateWorkspaceBuild( + return postWorkspaceBuild( selectedWorkspace.id, - updateWorkspaceBuildRequest, + createWorkspaceBuildRequest, ) }, }, @@ -190,8 +191,8 @@ export const workspaceBuildParametersMachine = createMachine( workspaceBuildParameters: (_, event) => event.data, }), - assignUpdateWorkspaceBuildRequest: assign({ - updateWorkspaceBuildRequest: (_, event) => event.request, + assignCreateWorkspaceBuildRequest: assign({ + createWorkspaceBuildRequest: (_, event) => event.request, }), assignGetWorkspaceError: assign({ getWorkspaceError: (_, event) => event.data, From 0dac060cb4146ee01712dcc6ddc12e68c874af2b Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 31 Jan 2023 13:58:15 +0100 Subject: [PATCH 30/46] Fix: bad initial value --- .../WorkspaceBuildParametersPageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx index 320f6a12ffef4..9991261c55119 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx @@ -136,7 +136,7 @@ export const WorkspaceBuildParametersPageView: FC< title={t("title")} detail="Those values are provided by your templateā€˜s Terraform configuration." > - {props.templateParameters && ( + {props.templateParameters && props.workspaceBuildParameters && (
Date: Tue, 31 Jan 2023 14:52:11 +0100 Subject: [PATCH 31/46] Validation works --- site/src/api/api.ts | 1 - .../WorkspaceActions.test.tsx | 2 + .../CreateWorkspacePageView.tsx | 116 +++++++++--------- .../WorkspaceBuildParametersPage.tsx | 13 +- .../WorkspaceBuildParametersPageView.tsx | 80 +++++++----- 5 files changed, 120 insertions(+), 92 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 161b05939285f..dd651a54aeb1b 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1,7 +1,6 @@ import axios, { AxiosRequestHeaders } from "axios" import dayjs from "dayjs" import * as Types from "./types" -import { WorkspaceBuildTransition } from "./types" import * as TypesGen from "./typesGenerated" export const hardCodedCSRFCookie = (): string => { diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx index 27d59496fbf06..b4ffedb4680a6 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx @@ -19,6 +19,7 @@ const renderComponent = async (props: Partial = {}) => { handleUpdate={jest.fn()} handleCancel={jest.fn()} handleChangeVersion={jest.fn()} + handleBuildParameters={jest.fn()} isUpdating={false} />, ) @@ -37,6 +38,7 @@ const renderAndClick = async (props: Partial = {}) => { handleUpdate={jest.fn()} handleCancel={jest.fn()} handleChangeVersion={jest.fn()} + handleBuildParameters={jest.fn()} isUpdating={false} />, ) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 300356a879648..b2518fb3e743f 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -66,10 +66,13 @@ export const CreateWorkspacePageView: FC< template_id: props.selectedTemplate ? props.selectedTemplate.id : "", rich_parameter_values: initialRichParameterValues, }, - validationSchema: ValidationSchemaForRichParameters( - "createWorkspacePage", - props.templateParameters, - ), + validationSchema: Yup.object({ + name: nameValidator(t("nameLabel", { ns: "createWorkspacePage" })), + rich_parameter_values: ValidationSchemaForRichParameters( + "createWorkspacePage", + props.templateParameters, + ), + }), enableReinitialize: true, initialTouched: props.initialTouched, onSubmit: (request) => { @@ -438,68 +441,63 @@ export const ValidationSchemaForRichParameters = ( return Yup.object() } - return Yup.object({ - name: nameValidator(t("nameLabel", { ns })), - rich_parameter_values: Yup.array() - .of( - Yup.object().shape({ - name: Yup.string().required(), - value: Yup.string() - .required(t("validationRequiredParameter")) - .test("verify with template", (val, ctx) => { - const name = ctx.parent.name - const templateParameter = templateParameters.find( - (parameter) => parameter.name === name, - ) - if (templateParameter) { - switch (templateParameter.type) { - case "number": - if ( - templateParameter.validation_min === 0 && - templateParameter.validation_max === 0 - ) { + return Yup.array() + .of( + Yup.object().shape({ + name: Yup.string().required(), + value: Yup.string() + .required(t("validationRequiredParameter")) + .test("verify with template", (val, ctx) => { + const name = ctx.parent.name + const templateParameter = templateParameters.find( + (parameter) => parameter.name === name, + ) + if (templateParameter) { + switch (templateParameter.type) { + case "number": + if ( + templateParameter.validation_min === 0 && + templateParameter.validation_max === 0 + ) { + return true + } + + if ( + Number(val) < templateParameter.validation_min || + templateParameter.validation_max < Number(val) + ) { + return ctx.createError({ + path: ctx.path, + message: t("validationNumberNotInRange", { + min: templateParameter.validation_min, + max: templateParameter.validation_max, + }), + }) + } + break + case "string": + { + if (templateParameter.validation_regex.length === 0) { return true } - if ( - Number(val) < templateParameter.validation_min || - templateParameter.validation_max < Number(val) - ) { + const regex = new RegExp(templateParameter.validation_regex) + if (val && !regex.test(val)) { return ctx.createError({ path: ctx.path, - message: t("validationNumberNotInRange", { - min: templateParameter.validation_min, - max: templateParameter.validation_max, + message: t("validationPatternNotMatched", { + error: templateParameter.validation_error, + pattern: templateParameter.validation_regex, }), }) } - break - case "string": - { - if (templateParameter.validation_regex.length === 0) { - return true - } - - const regex = new RegExp( - templateParameter.validation_regex, - ) - if (val && !regex.test(val)) { - return ctx.createError({ - path: ctx.path, - message: t("validationPatternNotMatched", { - error: templateParameter.validation_error, - pattern: templateParameter.validation_regex, - }), - }) - } - } - break - } + } + break } - return true - }), - }), - ) - .required(), - }) + } + return true + }), + }), + ) + .required() } diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx index 079b85f02a470..f866b12e511d8 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx @@ -9,7 +9,6 @@ import { UpdateWorkspaceErrors, WorkspaceBuildParametersPageView, } from "./WorkspaceBuildParametersPageView" -import { getWorkspaceBuildParameters } from "api/api" export const WorkspaceBuildParametersPage: FC = () => { const { t } = useTranslation("workspaceBuildParametersPage") @@ -51,6 +50,7 @@ export const WorkspaceBuildParametersPage: FC = () => { workspace={selectedWorkspace} templateParameters={templateParameters} workspaceBuildParameters={workspaceBuildParameters} + updatingWorkspace={state.matches("updatingWorkspace")} hasErrors={state.matches("error")} updateWorkspaceErrors={{ [UpdateWorkspaceErrors.GET_WORKSPACE_ERROR]: getWorkspaceError, @@ -60,6 +60,17 @@ export const WorkspaceBuildParametersPage: FC = () => { getWorkspaceBuildParametersError, [UpdateWorkspaceErrors.UPDATE_WORKSPACE_ERROR]: updateWorkspaceError, }} + onCancel={() => { + // Go back + navigate(-1) + }} + onSubmit={(request) => { + console.log("onSubmit 1") + send({ + type: "UPDATE_WORKSPACE", + request, + }) + }} /> ) diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx index 9991261c55119..f390ba75ed33f 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx @@ -12,6 +12,8 @@ import { ValidationSchemaForRichParameters, workspaceBuildParameterValue, } from "pages/CreateWorkspacePage/CreateWorkspacePageView" +import { FormFooter } from "components/FormFooter/FormFooter" +import * as Yup from "yup" export enum UpdateWorkspaceErrors { GET_WORKSPACE_ERROR = "getWorkspaceError", @@ -26,6 +28,9 @@ export interface WorkspaceBuildParametersPageViewProps { workspaceBuildParameters?: TypesGen.WorkspaceBuildParameter[] initialTouched?: FormikTouched + updatingWorkspace: boolean + onCancel: () => void + onSubmit: (req: TypesGen.CreateWorkspaceBuildRequest) => void hasErrors: boolean updateWorkspaceErrors: Partial> @@ -36,6 +41,7 @@ export const WorkspaceBuildParametersPageView: FC< > = (props) => { const { t } = useTranslation("workspaceBuildParametersPage") const styles = useStyles() + const formFooterStyles = useFormFooterStyles() const initialRichParameterValues = selectInitialRichParametersValues( props.templateParameters, @@ -51,13 +57,17 @@ export const WorkspaceBuildParametersPageView: FC< transition: "start", rich_parameter_values: initialRichParameterValues, }, - validationSchema: ValidationSchemaForRichParameters( - "workspaceBuildParametersPage", - props.templateParameters, - ), + validationSchema: Yup.object({ + rich_parameter_values: ValidationSchemaForRichParameters( + "workspaceBuildParametersPage", + props.templateParameters, + ), + }), enableReinitialize: true, initialTouched: props.initialTouched, - onSubmit: () => { + onSubmit: (request) => { + console.info("onSubmit 2") + props.onSubmit(request) form.setSubmitting(false) }, }) @@ -138,33 +148,41 @@ export const WorkspaceBuildParametersPageView: FC< > {props.templateParameters && props.workspaceBuildParameters && (
- - {props.templateParameters.map((parameter, index) => ( - { - form.setFieldValue("rich_parameter_values." + index, { - name: parameter.name, - value: value, - }) - }} - parameter={parameter} - initialValue={workspaceBuildParameterValue( - initialRichParameterValues, - parameter, - )} +
+ + {props.templateParameters.map((parameter, index) => ( + { + form.setFieldValue("rich_parameter_values." + index, { + name: parameter.name, + value: value, + }) + }} + parameter={parameter} + initialValue={workspaceBuildParameterValue( + initialRichParameterValues, + parameter, + )} + /> + ))} + - ))} - + +
)} From 608d713abfd35252deab1cf3e411a181a1c72dcf Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 31 Jan 2023 15:13:24 +0100 Subject: [PATCH 32/46] Maybe --- .../WorkspaceBuildParametersPageView.tsx | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx index f390ba75ed33f..1e30c698b31c8 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx @@ -14,6 +14,7 @@ import { } from "pages/CreateWorkspacePage/CreateWorkspacePageView" import { FormFooter } from "components/FormFooter/FormFooter" import * as Yup from "yup" +import { Maybe } from "components/Conditionals/Maybe" export enum UpdateWorkspaceErrors { GET_WORKSPACE_ERROR = "getWorkspaceError", @@ -77,8 +78,8 @@ export const WorkspaceBuildParametersPageView: FC< props.updateWorkspaceErrors[UpdateWorkspaceErrors.UPDATE_WORKSPACE_ERROR], ) - if (props.hasErrors) { - return ( + { + props.hasErrors && ( {Boolean( props.updateWorkspaceErrors[ @@ -126,26 +127,25 @@ export const WorkspaceBuildParametersPageView: FC< ) } - if ( - props.updateWorkspaceErrors[UpdateWorkspaceErrors.UPDATE_WORKSPACE_ERROR] - ) { - return ( - + - ) - } + ], + )} + > + + - return ( - {props.templateParameters && props.workspaceBuildParameters && (
@@ -243,7 +243,7 @@ const selectInitialRichParametersValues = ( const useStyles = makeStyles(() => ({ formSection: { - marginTop: 28, + marginTop: 20, }, formSectionFields: { From 2edca1e05a2a12db934d3f07b97f1074deda7d37 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 31 Jan 2023 15:17:13 +0100 Subject: [PATCH 33/46] Fix --- site/src/i18n/en/workspaceBuildParametersPage.json | 7 ++++++- .../WorkspaceBuildParametersPage.tsx | 1 - .../WorkspaceBuildParametersPageView.tsx | 1 - 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/site/src/i18n/en/workspaceBuildParametersPage.json b/site/src/i18n/en/workspaceBuildParametersPage.json index 544779209e49c..2826ac0f2b21c 100644 --- a/site/src/i18n/en/workspaceBuildParametersPage.json +++ b/site/src/i18n/en/workspaceBuildParametersPage.json @@ -1,3 +1,8 @@ { - "title": "Workspace build parameters" + "title": "Workspace build parameters", + "detail": "Those values were provided by the workspace owner.", + "validationRequiredParameter": "Parameter is required.", + "validationNumberNotInRange": "Value must be between {{min}} and {{max}}.", + "validationPatternNotMatched": "{{error}} (value does not match the pattern {{pattern}}).", + "updateWorkspace": "Update workspace" } diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx index f866b12e511d8..44d60e1217e88 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.tsx @@ -65,7 +65,6 @@ export const WorkspaceBuildParametersPage: FC = () => { navigate(-1) }} onSubmit={(request) => { - console.log("onSubmit 1") send({ type: "UPDATE_WORKSPACE", request, diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx index 1e30c698b31c8..cd37612eeb288 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx @@ -67,7 +67,6 @@ export const WorkspaceBuildParametersPageView: FC< enableReinitialize: true, initialTouched: props.initialTouched, onSubmit: (request) => { - console.info("onSubmit 2") props.onSubmit(request) form.setSubmitting(false) }, From c77119941e161f0769bef5d500ee98b21505d2ad Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 31 Jan 2023 16:12:05 +0100 Subject: [PATCH 34/46] Go back button --- .../i18n/en/workspaceBuildParametersPage.json | 1 + .../WorkspaceBuildParametersPageView.tsx | 98 ++++++++++++------- 2 files changed, 62 insertions(+), 37 deletions(-) diff --git a/site/src/i18n/en/workspaceBuildParametersPage.json b/site/src/i18n/en/workspaceBuildParametersPage.json index 2826ac0f2b21c..473f55eae533d 100644 --- a/site/src/i18n/en/workspaceBuildParametersPage.json +++ b/site/src/i18n/en/workspaceBuildParametersPage.json @@ -1,6 +1,7 @@ { "title": "Workspace build parameters", "detail": "Those values were provided by the workspace owner.", + "noParametersDefined": "This template does not use any rich parameters.", "validationRequiredParameter": "Parameter is required.", "validationNumberNotInRange": "Value must be between {{min}} and {{max}}.", "validationPatternNotMatched": "{{error}} (value does not match the pattern {{pattern}}).", diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx index cd37612eeb288..49719798f18c1 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx @@ -15,6 +15,8 @@ import { import { FormFooter } from "components/FormFooter/FormFooter" import * as Yup from "yup" import { Maybe } from "components/Conditionals/Maybe" +import { CancelButton } from "components/DropdownButton/ActionCtas" +import { Button } from "@material-ui/core" export enum UpdateWorkspaceErrors { GET_WORKSPACE_ERROR = "getWorkspaceError", @@ -145,45 +147,62 @@ export const WorkspaceBuildParametersPageView: FC< /> - {props.templateParameters && props.workspaceBuildParameters && ( +
- - - {props.templateParameters.map((parameter, index) => ( - { - form.setFieldValue("rich_parameter_values." + index, { - name: parameter.name, - value: value, - }) - }} - parameter={parameter} - initialValue={workspaceBuildParameterValue( - initialRichParameterValues, - parameter, - )} - /> - ))} - - - + +
+ +
- )} +
+ + {props.templateParameters && + props.templateParameters.length > 0 && + props.workspaceBuildParameters && ( +
+
+ + {props.templateParameters.map((parameter, index) => ( + { + form.setFieldValue("rich_parameter_values." + index, { + name: parameter.name, + value: value, + }) + }} + parameter={parameter} + initialValue={workspaceBuildParameterValue( + initialRichParameterValues, + parameter, + )} + /> + ))} + + +
+
+ )} ) } @@ -241,6 +260,11 @@ const selectInitialRichParametersValues = ( } const useStyles = makeStyles(() => ({ + goBackSection: { + display: "flex", + width: "100%", + marginTop: 32, + }, formSection: { marginTop: 20, }, From 35cf6037b2bf803629497806b66bf039060022cd Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 31 Jan 2023 18:41:59 +0100 Subject: [PATCH 35/46] GoBack button --- .../components/GoBackButton/GoBackButton.tsx | 19 +++++++++++++++++++ .../WorkspaceBuildParametersPageView.tsx | 7 ++----- 2 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 site/src/components/GoBackButton/GoBackButton.tsx diff --git a/site/src/components/GoBackButton/GoBackButton.tsx b/site/src/components/GoBackButton/GoBackButton.tsx new file mode 100644 index 0000000000000..36818eb6458ae --- /dev/null +++ b/site/src/components/GoBackButton/GoBackButton.tsx @@ -0,0 +1,19 @@ +import Button from "@material-ui/core/Button" + +interface GoBackButtonProps { + onClick: () => void +} + +export const Language = { + ariaLabel: "Go back", +} + +export const GoBackButton: React.FC< + React.PropsWithChildren +> = ({ onClick }) => { + return ( + + ) +} diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx index 49719798f18c1..eab7bf4ac3697 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx @@ -15,8 +15,7 @@ import { import { FormFooter } from "components/FormFooter/FormFooter" import * as Yup from "yup" import { Maybe } from "components/Conditionals/Maybe" -import { CancelButton } from "components/DropdownButton/ActionCtas" -import { Button } from "@material-ui/core" +import { GoBackButton } from "components/GoBackButton/GoBackButton" export enum UpdateWorkspaceErrors { GET_WORKSPACE_ERROR = "getWorkspaceError", @@ -155,9 +154,7 @@ export const WorkspaceBuildParametersPageView: FC<
- +
From 640ae427dbb8380bfd659b541a3599f21e8d45f2 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 31 Jan 2023 19:28:49 +0100 Subject: [PATCH 36/46] Form --- .../RichParameterInput/RichParameterInput.tsx | 11 ++++++++ .../WorkspaceBuildParametersPageView.tsx | 27 +++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index 6370be0f507c6..b60c584220918 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -6,6 +6,7 @@ import TextField from "@material-ui/core/TextField" import { Stack } from "components/Stack/Stack" import { FC, useState } from "react" import { TemplateVersionParameter } from "../../api/typesGenerated" +import { colors } from "theme/colors" const isBoolean = (parameter: TemplateVersionParameter) => { return parameter.type === "bool" @@ -41,6 +42,11 @@ const ParameterLabel: FC = ({ index, parameter }) => { {parameter.description} + {!parameter.mutable && ( +
+ This parameter cannot be changed after creating workspace. +
+ )} ) } @@ -187,6 +193,11 @@ const useStyles = makeStyles((theme) => ({ display: "block", fontWeight: 600, }, + labelImmutable: { + marginTop: theme.spacing(0.5), + marginBottom: theme.spacing(0.5), + color: colors.yellow[7], + }, input: { display: "flex", flexDirection: "column", diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx index eab7bf4ac3697..355c04d148d10 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx @@ -68,7 +68,9 @@ export const WorkspaceBuildParametersPageView: FC< enableReinitialize: true, initialTouched: props.initialTouched, onSubmit: (request) => { - props.onSubmit(request) + props.onSubmit( + stripImmutableParameters(props.templateParameters, request), + ) form.setSubmitting(false) }, }) @@ -174,7 +176,7 @@ export const WorkspaceBuildParametersPageView: FC< {...getFieldHelpers( "rich_parameter_values[" + index + "].value", )} - disabled={form.isSubmitting} + disabled={!parameter.mutable || form.isSubmitting} index={index} key={parameter.name} onChange={(value) => { @@ -256,6 +258,27 @@ const selectInitialRichParametersValues = ( return defaults } +const stripImmutableParameters = ( + templateParameters: TypesGen.TemplateVersionParameter[], + request: TypesGen.CreateWorkspaceBuildRequest, +): TypesGen.CreateWorkspaceBuildRequest => { + if (!request.rich_parameter_values) { + return request + } + + const mutableBuildParameters = request.rich_parameter_values.filter( + (buildParameter) => + templateParameters.find( + (templateParameter) => templateParameter.name === buildParameter.name, + )?.mutable, + ) + + return { + ...request, + rich_parameter_values: mutableBuildParameters, + } +} + const useStyles = makeStyles(() => ({ goBackSection: { display: "flex", From f31a7a0ca62c554509c786b7ba819e0a47ce6d65 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 31 Jan 2023 19:39:42 +0100 Subject: [PATCH 37/46] Fix --- .../WorkspaceBuildParametersPageView.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx index 355c04d148d10..b8b603bc09cd2 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.tsx @@ -69,7 +69,7 @@ export const WorkspaceBuildParametersPageView: FC< initialTouched: props.initialTouched, onSubmit: (request) => { props.onSubmit( - stripImmutableParameters(props.templateParameters, request), + stripImmutableParameters(request, props.templateParameters), ) form.setSubmitting(false) }, @@ -259,10 +259,10 @@ const selectInitialRichParametersValues = ( } const stripImmutableParameters = ( - templateParameters: TypesGen.TemplateVersionParameter[], request: TypesGen.CreateWorkspaceBuildRequest, + templateParameters?: TypesGen.TemplateVersionParameter[], ): TypesGen.CreateWorkspaceBuildRequest => { - if (!request.rich_parameter_values) { + if (!templateParameters || !request.rich_parameter_values) { return request } From 5228e9cdc0303a249d48d681ff22405996126da6 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Feb 2023 10:12:36 +0100 Subject: [PATCH 38/46] Storybook --- .../CreateWorkspacePageView.stories.tsx | 15 ++++++ ...rkspaceBuildParametersPageView.stories.tsx | 48 +++++++++++++++++++ site/src/testHelpers/entities.ts | 21 ++++++-- 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.stories.tsx diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx index 9fc79b659dbac..e202ab9d60d8b 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx @@ -3,6 +3,9 @@ import { makeMockApiError, mockParameterSchema, MockTemplate, + MockTemplateVersionParameter1, + MockTemplateVersionParameter2, + MockTemplateVersionParameter3, } from "../../testHelpers/entities" import { CreateWorkspaceErrors, @@ -108,3 +111,15 @@ CreateWorkspaceError.args = { name: true, }, } + +export const RichParameters = Template.bind({}) +RichParameters.args = { + templates: [MockTemplate], + selectedTemplate: MockTemplate, + templateParameters: [ + MockTemplateVersionParameter1, + MockTemplateVersionParameter2, + MockTemplateVersionParameter3, + ], + createWorkspaceErrors: {}, +} diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.stories.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.stories.tsx new file mode 100644 index 0000000000000..aec6fc73a1f08 --- /dev/null +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPageView.stories.tsx @@ -0,0 +1,48 @@ +import { ComponentMeta, Story } from "@storybook/react" +import { + MockTemplateVersionParameter1, + MockTemplateVersionParameter2, + MockTemplateVersionParameter3, + MockTemplateVersionParameter4, + MockWorkspace, +} from "testHelpers/entities" +import { + WorkspaceBuildParametersPageView, + WorkspaceBuildParametersPageViewProps, +} from "./WorkspaceBuildParametersPageView" + +export default { + title: "pages/WorkspaceBuildParametersPageView", + component: WorkspaceBuildParametersPageView, +} as ComponentMeta + +const Template: Story = (args) => ( + +) + +export const NoRichParametersDefined = Template.bind({}) +NoRichParametersDefined.args = { + workspace: MockWorkspace, + templateParameters: [], + workspaceBuildParameters: [], + updateWorkspaceErrors: {}, + initialTouched: { + name: true, + }, +} + +export const RichParametersDefined = Template.bind({}) +RichParametersDefined.args = { + workspace: MockWorkspace, + templateParameters: [ + MockTemplateVersionParameter1, + MockTemplateVersionParameter2, + MockTemplateVersionParameter3, + MockTemplateVersionParameter4, + ], + workspaceBuildParameters: [], + updateWorkspaceErrors: {}, + initialTouched: { + name: true, + }, +} diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index b52357a1454c7..653ea2f199ebd 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -638,7 +638,7 @@ export const MockTemplateVersionParameter1: TypesGen.TemplateVersionParameter = description: "This is first parameter", default_value: "abc", mutable: true, - icon: "/icons/icon.svg", + icon: "/icon/folder.svg", options: [], validation_error: "", validation_regex: "", @@ -653,7 +653,7 @@ export const MockTemplateVersionParameter2: TypesGen.TemplateVersionParameter = description: "This is second parameter", default_value: "2", mutable: true, - icon: "/icons/folder.svg", + icon: "/icon/folder.svg", options: [], validation_error: "", validation_regex: "", @@ -668,7 +668,7 @@ export const MockTemplateVersionParameter3: TypesGen.TemplateVersionParameter = description: "This is third parameter", default_value: "aaa", mutable: true, - icon: "/icons/folder.svg", + icon: "/icon/database.svg", options: [], validation_error: "No way!", validation_regex: "^[a-z]{3}$", @@ -676,6 +676,21 @@ export const MockTemplateVersionParameter3: TypesGen.TemplateVersionParameter = validation_max: 0, } +export const MockTemplateVersionParameter4: TypesGen.TemplateVersionParameter = + { + name: "fourth_parameter", + type: "string", + description: "This is fourth parameter", + default_value: "def", + mutable: false, + icon: "/icon/database.svg", + options: [], + validation_error: "", + validation_regex: "", + validation_min: 0, + validation_max: 0, + } + // requests the MockWorkspace export const MockWorkspaceRequest: TypesGen.CreateWorkspaceRequest = { name: "test", From 3b842f2fb6b216f9140157a3932403f43e0c387b Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Feb 2023 12:13:00 +0100 Subject: [PATCH 39/46] Fix: CreateWorkspacePage --- .../CreateWorkspacePage.test.tsx | 63 ++++++++++++------- .../WorkspaceBuildParametersPage.test.tsx | 62 ++++++++++++++++++ site/src/testHelpers/entities.ts | 10 +++ 3 files changed, 112 insertions(+), 23 deletions(-) create mode 100644 site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index 80f29caef85b2..7a9205067fa2c 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -29,16 +29,25 @@ const renderCreateWorkspacePage = () => { } describe("CreateWorkspacePage", () => { - it("renders with rich parameter", async () => { + it("renders", async () => { jest .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValueOnce([MockTemplateVersionParameter1]) + renderCreateWorkspacePage() - await waitFor(() => renderCreateWorkspacePage()) + const element = await screen.findByText("Create workspace") + expect(element).toBeDefined() + }) + + it("renders with rich parameter", async () => { + jest + .spyOn(API, "getTemplateVersionRichParameters") + .mockResolvedValueOnce([MockTemplateVersionParameter1]) + renderCreateWorkspacePage() - const element = screen.findByText("Create workspace") + const element = await screen.findByText("Create workspace") expect(element).toBeDefined() - const firstParameter = screen.findByText( + const firstParameter = await screen.findByText( MockTemplateVersionParameter1.description, ) expect(firstParameter).toBeDefined() @@ -52,6 +61,9 @@ describe("CreateWorkspacePage", () => { .spyOn(API, "getWorkspaceQuota") .mockResolvedValueOnce(MockWorkspaceQuota) jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace) + jest + .spyOn(API, "getTemplateVersionRichParameters") + .mockResolvedValueOnce([MockTemplateVersionParameter1]) renderCreateWorkspacePage() @@ -89,17 +101,14 @@ describe("CreateWorkspacePage", () => { .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValueOnce([MockTemplateVersionParameter1]) - await waitFor(() => - renderWithAuth(, { - route: - "/templates/" + - MockTemplate.name + - `/workspace?param.${param}=${paramValue}`, - path: "/templates/:template/workspace", - }), - ) - - await screen.findByDisplayValue(paramValue) + renderWithAuth(, { + route: + "/templates/" + + MockTemplate.name + + `/workspace?param.${param}=${paramValue}`, + path: "/templates/:template/workspace", + }), + await screen.findByDisplayValue(paramValue) }) it("uses default rich param values passed from the URL", async () => { @@ -138,9 +147,9 @@ describe("CreateWorkspacePage", () => { await waitFor(() => renderCreateWorkspacePage()) - const element = screen.findByText("Create workspace") + const element = await screen.findByText("Create workspace") expect(element).toBeDefined() - const secondParameter = screen.findByText( + const secondParameter = await screen.findByText( MockTemplateVersionParameter2.description, ) expect(secondParameter).toBeDefined() @@ -148,11 +157,16 @@ describe("CreateWorkspacePage", () => { const secondParameterField = await screen.findByLabelText( MockTemplateVersionParameter2.name, ) + expect(secondParameterField).toBeDefined() + fireEvent.change(secondParameterField, { target: { value: "4" }, }) + fireEvent.submit(secondParameter) - const validationError = screen.findByText("Value must be between") + const validationError = await screen.findByText( + "Value must be between 1 and 3.", + ) expect(validationError).toBeDefined() }) @@ -166,9 +180,9 @@ describe("CreateWorkspacePage", () => { await waitFor(() => renderCreateWorkspacePage()) - const element = screen.findByText("Create workspace") + const element = await screen.findByText("Create workspace") expect(element).toBeDefined() - const thirdParameter = screen.findByText( + const thirdParameter = await screen.findByText( MockTemplateVersionParameter3.description, ) expect(thirdParameter).toBeDefined() @@ -176,13 +190,16 @@ describe("CreateWorkspacePage", () => { const thirdParameterField = await screen.findByLabelText( MockTemplateVersionParameter3.name, ) + expect(thirdParameterField).toBeDefined() fireEvent.change(thirdParameterField, { target: { value: "1234" }, }) + fireEvent.submit(thirdParameterField) - const validationError = screen.findByText( - MockTemplateVersionParameter3.validation_error, + const validationError = await screen.findByText( + MockTemplateVersionParameter3.validation_error + + " (value does not match the pattern ^[a-z]{3}$).", ) - expect(validationError).toBeDefined() + expect(validationError).toBeInTheDocument() }) }) diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx new file mode 100644 index 0000000000000..6fe30db84ef5b --- /dev/null +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx @@ -0,0 +1,62 @@ +import { screen, waitFor } from "@testing-library/react" +import { + MockTemplateVersionParameter1, + MockWorkspace, + MockWorkspaceBuildParameter1, + MockWorkspaceBuildParameter2, + renderWithAuth, +} from "testHelpers/renderHelpers" +import * as API from "api/api" +import { WorkspaceBuildParametersPage } from "./WorkspaceBuildParametersPage" + +const renderWorkspaceBuildParametersPage = () => { + return renderWithAuth(, { + route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}/build-parameters`, + path: `/@:ownerName/:workspaceName/build-parameters`, + }) +} + +describe("WorkspaceBuildParametersPage", () => { + it("renders without rich parameters", async () => { + jest.spyOn(API, "getWorkspace").mockResolvedValueOnce(MockWorkspace) + jest + .spyOn(API, "getTemplateVersionRichParameters") + .mockResolvedValueOnce([]) + + await waitFor(() => renderWorkspaceBuildParametersPage()) + + const element = screen.findByDisplayValue("Workspace build parameters") + expect(element).toBeDefined() + + const goBackButton = screen.findByDisplayValue("Go back") + expect(goBackButton).toBeDefined() + }) + + it("renders with rich parameter", async () => { + jest.spyOn(API, "getWorkspace").mockResolvedValueOnce(MockWorkspace) + jest + .spyOn(API, "getTemplateVersionRichParameters") + .mockResolvedValueOnce([MockTemplateVersionParameter1]) + jest + .spyOn(API, "getWorkspaceBuildParameters") + .mockResolvedValueOnce([ + MockWorkspaceBuildParameter1, + MockWorkspaceBuildParameter2, + ]) + + await waitFor(() => renderWorkspaceBuildParametersPage()) + + const element = screen.findByText("Workspace build parameters") + expect(element).toBeDefined() + + const firstParameter = screen.findByLabelText( + MockTemplateVersionParameter1.name, + ) + expect(firstParameter).toBeDefined() + + const firstParameterValue = screen.findByText( + MockWorkspaceBuildParameter1.value, + ) + expect(firstParameterValue).toBeDefined() + }) +}) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 653ea2f199ebd..5d7ed7e925469 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1251,6 +1251,16 @@ export const MockAppearance: TypesGen.AppearanceConfig = { }, } +export const MockWorkspaceBuildParameter1: TypesGen.WorkspaceBuildParameter = { + name: MockTemplateVersionParameter1.name, + value: "mock-abc", +} + +export const MockWorkspaceBuildParameter2: TypesGen.WorkspaceBuildParameter = { + name: MockTemplateVersionParameter2.name, + value: "3", +} + export const mockParameterSchema = ( partial: Partial, ): TypesGen.ParameterSchema => { From c62d5a434a6fe0324f3e9068c6d97bf55731321e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Feb 2023 12:33:08 +0100 Subject: [PATCH 40/46] fmt --- .../WorkspaceBuildParametersPage.test.tsx | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx index 6fe30db84ef5b..2bf630f30b3fb 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx @@ -1,6 +1,7 @@ -import { screen, waitFor } from "@testing-library/react" +import { screen } from "@testing-library/react" import { MockTemplateVersionParameter1, + MockTemplateVersionParameter2, MockWorkspace, MockWorkspaceBuildParameter1, MockWorkspaceBuildParameter2, @@ -23,12 +24,12 @@ describe("WorkspaceBuildParametersPage", () => { .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValueOnce([]) - await waitFor(() => renderWorkspaceBuildParametersPage()) + renderWorkspaceBuildParametersPage() - const element = screen.findByDisplayValue("Workspace build parameters") + const element = await screen.findByText("Workspace build parameters") expect(element).toBeDefined() - const goBackButton = screen.findByDisplayValue("Go back") + const goBackButton = await screen.findByText("Go back") expect(goBackButton).toBeDefined() }) @@ -36,7 +37,10 @@ describe("WorkspaceBuildParametersPage", () => { jest.spyOn(API, "getWorkspace").mockResolvedValueOnce(MockWorkspace) jest .spyOn(API, "getTemplateVersionRichParameters") - .mockResolvedValueOnce([MockTemplateVersionParameter1]) + .mockResolvedValueOnce([ + MockTemplateVersionParameter1, + MockTemplateVersionParameter2, + ]) jest .spyOn(API, "getWorkspaceBuildParameters") .mockResolvedValueOnce([ @@ -44,19 +48,19 @@ describe("WorkspaceBuildParametersPage", () => { MockWorkspaceBuildParameter2, ]) - await waitFor(() => renderWorkspaceBuildParametersPage()) + renderWorkspaceBuildParametersPage() - const element = screen.findByText("Workspace build parameters") + const element = await screen.findByText("Workspace build parameters") expect(element).toBeDefined() - const firstParameter = screen.findByLabelText( + const firstParameter = await screen.findByLabelText( MockTemplateVersionParameter1.name, ) expect(firstParameter).toBeDefined() - const firstParameterValue = screen.findByText( - MockWorkspaceBuildParameter1.value, + const secondParameter = await screen.findByLabelText( + MockTemplateVersionParameter2.name, ) - expect(firstParameterValue).toBeDefined() + expect(secondParameter).toBeDefined() }) }) From 6cab87f3b2f8a488e80613f9c96990a7e41fbc39 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Feb 2023 13:14:28 +0100 Subject: [PATCH 41/46] Test --- .../WorkspaceBuildParametersPage.test.tsx | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx index 2bf630f30b3fb..dc48f8c6b3a80 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx @@ -1,4 +1,4 @@ -import { screen } from "@testing-library/react" +import { fireEvent, screen } from "@testing-library/react" import { MockTemplateVersionParameter1, MockTemplateVersionParameter2, @@ -63,4 +63,42 @@ describe("WorkspaceBuildParametersPage", () => { ) expect(secondParameter).toBeDefined() }) + + it("rich parameter: number validation fails", async () => { + jest + .spyOn(API, "getTemplateVersionRichParameters") + .mockResolvedValueOnce([ + MockTemplateVersionParameter1, + MockTemplateVersionParameter2, + ]) + jest + .spyOn(API, "getWorkspaceBuildParameters") + .mockResolvedValueOnce([ + MockWorkspaceBuildParameter1, + MockWorkspaceBuildParameter2, + ]) + renderWorkspaceBuildParametersPage() + + const element = await screen.findByText("Workspace build parameters") + expect(element).toBeDefined() + const secondParameter = await screen.findByText( + MockTemplateVersionParameter2.description, + ) + expect(secondParameter).toBeDefined() + + const secondParameterField = await screen.findByLabelText( + MockTemplateVersionParameter2.name, + ) + expect(secondParameterField).toBeDefined() + + fireEvent.change(secondParameterField, { + target: { value: "4" }, + }) + fireEvent.submit(secondParameter) + + const validationError = await screen.findByText( + "Value must be between 1 and 3.", + ) + expect(validationError).toBeDefined() + }) }) From 097be54eb5234a6907975f5520f476e1b11a405f Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Feb 2023 13:33:42 +0100 Subject: [PATCH 42/46] ns --- .../CreateWorkspacePage.test.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index 7a9205067fa2c..9250619969a0d 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -20,6 +20,8 @@ const { t } = i18next const nameLabelText = t("nameLabel", { ns: "createWorkspacePage" }) const createWorkspaceText = t("createWorkspace", { ns: "createWorkspacePage" }) +const validationNumberNotInRangeText = t("validationNumberNotInRange", { ns: "createWorkspacePage", min: "1", max: "3" }) +const validationPatternNotMatched = t("validationPatternNotMatched", { ns: "createWorkspacePage", error: MockTemplateVersionParameter3.validation_error, pattern: "^[a-z]{3}$" }) const renderCreateWorkspacePage = () => { return renderWithAuth(, { @@ -35,7 +37,7 @@ describe("CreateWorkspacePage", () => { .mockResolvedValueOnce([MockTemplateVersionParameter1]) renderCreateWorkspacePage() - const element = await screen.findByText("Create workspace") + const element = await screen.findByText(createWorkspaceText) expect(element).toBeDefined() }) @@ -45,7 +47,7 @@ describe("CreateWorkspacePage", () => { .mockResolvedValueOnce([MockTemplateVersionParameter1]) renderCreateWorkspacePage() - const element = await screen.findByText("Create workspace") + const element = await screen.findByText(createWorkspaceText) expect(element).toBeDefined() const firstParameter = await screen.findByText( MockTemplateVersionParameter1.description, @@ -165,7 +167,7 @@ describe("CreateWorkspacePage", () => { fireEvent.submit(secondParameter) const validationError = await screen.findByText( - "Value must be between 1 and 3.", + validationNumberNotInRangeText, ) expect(validationError).toBeDefined() }) @@ -180,7 +182,7 @@ describe("CreateWorkspacePage", () => { await waitFor(() => renderCreateWorkspacePage()) - const element = await screen.findByText("Create workspace") + const element = await screen.findByText(createWorkspaceText) expect(element).toBeDefined() const thirdParameter = await screen.findByText( MockTemplateVersionParameter3.description, @@ -196,10 +198,7 @@ describe("CreateWorkspacePage", () => { }) fireEvent.submit(thirdParameterField) - const validationError = await screen.findByText( - MockTemplateVersionParameter3.validation_error + - " (value does not match the pattern ^[a-z]{3}$).", - ) + const validationError = await screen.findByText(validationPatternNotMatched) expect(validationError).toBeInTheDocument() }) }) From 1210435182d365954f7abf68f4ef7c578ab7b39c Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Feb 2023 13:34:13 +0100 Subject: [PATCH 43/46] fmt --- .../CreateWorkspacePage/CreateWorkspacePage.test.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index 9250619969a0d..39bf44ea87ffa 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -20,8 +20,16 @@ const { t } = i18next const nameLabelText = t("nameLabel", { ns: "createWorkspacePage" }) const createWorkspaceText = t("createWorkspace", { ns: "createWorkspacePage" }) -const validationNumberNotInRangeText = t("validationNumberNotInRange", { ns: "createWorkspacePage", min: "1", max: "3" }) -const validationPatternNotMatched = t("validationPatternNotMatched", { ns: "createWorkspacePage", error: MockTemplateVersionParameter3.validation_error, pattern: "^[a-z]{3}$" }) +const validationNumberNotInRangeText = t("validationNumberNotInRange", { + ns: "createWorkspacePage", + min: "1", + max: "3", +}) +const validationPatternNotMatched = t("validationPatternNotMatched", { + ns: "createWorkspacePage", + error: MockTemplateVersionParameter3.validation_error, + pattern: "^[a-z]{3}$", +}) const renderCreateWorkspacePage = () => { return renderWithAuth(, { From 26a5e5d084696d7a166565abe7b55820b68e91d7 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Feb 2023 13:40:47 +0100 Subject: [PATCH 44/46] All tests --- .../WorkspaceBuildParametersPage.test.tsx | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx index dc48f8c6b3a80..3727ebf29c7b4 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx @@ -8,8 +8,18 @@ import { renderWithAuth, } from "testHelpers/renderHelpers" import * as API from "api/api" +import i18next from "i18next" import { WorkspaceBuildParametersPage } from "./WorkspaceBuildParametersPage" +const { t } = i18next + +const pageTitleText = t("title", { ns: "workspaceBuildParametersPage" }) +const validationNumberNotInRangeText = t("validationNumberNotInRange", { + ns: "workspaceBuildParametersPage", + min: "1", + max: "3", +}) + const renderWorkspaceBuildParametersPage = () => { return renderWithAuth(, { route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}/build-parameters`, @@ -23,10 +33,15 @@ describe("WorkspaceBuildParametersPage", () => { jest .spyOn(API, "getTemplateVersionRichParameters") .mockResolvedValueOnce([]) - + jest + .spyOn(API, "getWorkspaceBuildParameters") + .mockResolvedValueOnce([ + MockWorkspaceBuildParameter1, + MockWorkspaceBuildParameter2, + ]) renderWorkspaceBuildParametersPage() - const element = await screen.findByText("Workspace build parameters") + const element = await screen.findByText(pageTitleText) expect(element).toBeDefined() const goBackButton = await screen.findByText("Go back") @@ -50,7 +65,7 @@ describe("WorkspaceBuildParametersPage", () => { renderWorkspaceBuildParametersPage() - const element = await screen.findByText("Workspace build parameters") + const element = await screen.findByText(pageTitleText) expect(element).toBeDefined() const firstParameter = await screen.findByLabelText( @@ -79,7 +94,7 @@ describe("WorkspaceBuildParametersPage", () => { ]) renderWorkspaceBuildParametersPage() - const element = await screen.findByText("Workspace build parameters") + const element = await screen.findByText(pageTitleText) expect(element).toBeDefined() const secondParameter = await screen.findByText( MockTemplateVersionParameter2.description, @@ -97,7 +112,7 @@ describe("WorkspaceBuildParametersPage", () => { fireEvent.submit(secondParameter) const validationError = await screen.findByText( - "Value must be between 1 and 3.", + validationNumberNotInRangeText, ) expect(validationError).toBeDefined() }) From 8e2576ef6c2f283efa58660517b10a23a8a2e1eb Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Feb 2023 15:18:38 +0100 Subject: [PATCH 45/46] feat: WorkspaceActions depend on template parameters --- site/src/components/Workspace/Workspace.tsx | 5 ++ .../WorkspaceActions.test.tsx | 29 ++++++++++ .../WorkspaceActions/WorkspaceActions.tsx | 10 +++- .../components/WorkspaceActions/constants.ts | 19 ++++++- .../src/pages/WorkspacePage/WorkspacePage.tsx | 7 +++ .../WorkspacePage/WorkspaceReadyPage.tsx | 2 + .../xServices/workspace/workspaceXService.ts | 57 ++++++++++++++++++- 7 files changed, 122 insertions(+), 7 deletions(-) diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 11253cd1576e5..6abf08bb8d7b0 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -57,6 +57,7 @@ export interface WorkspaceProps { buildInfo?: TypesGen.BuildInfoResponse applicationsHost?: string template?: TypesGen.Template + templateParameters?: TypesGen.TemplateVersionParameter[] quota_budget?: number } @@ -83,6 +84,7 @@ export const Workspace: FC> = ({ buildInfo, applicationsHost, template, + templateParameters, quota_budget, }) => { const { t } = useTranslation("workspacePage") @@ -140,6 +142,9 @@ export const Workspace: FC> = ({ /> 0 + } isOutdated={workspace.outdated} handleStart={handleStart} handleStop={handleStop} diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx index b4ffedb4680a6..a83aa41cb1144 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx @@ -12,6 +12,7 @@ const renderComponent = async (props: Partial = {}) => { workspaceStatus={ props.workspaceStatus ?? Mocks.MockWorkspace.latest_build.status } + hasTemplateParameters={props.hasTemplateParameters ?? false} isOutdated={props.isOutdated ?? false} handleStart={jest.fn()} handleStop={jest.fn()} @@ -31,6 +32,7 @@ const renderAndClick = async (props: Partial = {}) => { workspaceStatus={ props.workspaceStatus ?? Mocks.MockWorkspace.latest_build.status } + hasTemplateParameters={props.hasTemplateParameters ?? false} isOutdated={props.isOutdated ?? false} handleStart={jest.fn()} handleStop={jest.fn()} @@ -76,6 +78,33 @@ describe("WorkspaceActions", () => { ) }) }) + describe("when the workspace is started", () => { + it("primary is stop; secondary is delete", async () => { + await renderAndClick({ + workspaceStatus: Mocks.MockWorkspace.latest_build.status, + }) + expect(screen.getByTestId("primary-cta")).toHaveTextContent( + t("actionButton.stop", { ns: "workspacePage" }), + ) + expect(screen.getByTestId("secondary-ctas")).toHaveTextContent( + t("actionButton.delete", { ns: "workspacePage" }), + ) + }) + }) + describe("when the workspace with rich parameters is started", () => { + it("primary is stop; secondary is build parameters", async () => { + await renderAndClick({ + workspaceStatus: Mocks.MockWorkspace.latest_build.status, + hasTemplateParameters: true, + }) + expect(screen.getByTestId("primary-cta")).toHaveTextContent( + t("actionButton.stop", { ns: "workspacePage" }), + ) + expect(screen.getByTestId("secondary-ctas")).toHaveTextContent( + t("actionButton.buildParameters", { ns: "workspacePage" }), + ) + }) + }) describe("when the workspace is stopping", () => { it("primary is stopping; cancel is available; no secondary", async () => { await renderComponent({ diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index c86d477f0da3c..fad29ebc7a209 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -12,10 +12,11 @@ import { StopButton, UpdateButton, } from "../DropdownButton/ActionCtas" -import { ButtonMapping, ButtonTypesEnum, statusToAbilities } from "./constants" +import { ButtonMapping, ButtonTypesEnum, buttonAbilities } from "./constants" export interface WorkspaceActionsProps { workspaceStatus: WorkspaceStatus + hasTemplateParameters: boolean isOutdated: boolean handleStart: () => void handleStop: () => void @@ -30,6 +31,7 @@ export interface WorkspaceActionsProps { export const WorkspaceActions: FC = ({ workspaceStatus, + hasTemplateParameters, isOutdated, handleStart, handleStop, @@ -41,8 +43,10 @@ export const WorkspaceActions: FC = ({ isUpdating, }) => { const { t } = useTranslation("workspacePage") - const { canCancel, canAcceptJobs, actions } = - statusToAbilities[workspaceStatus] + const { canCancel, canAcceptJobs, actions } = buttonAbilities( + workspaceStatus, + hasTemplateParameters, + ) const canBeUpdated = isOutdated && canAcceptJobs // A mapping of button type to the corresponding React component diff --git a/site/src/components/WorkspaceActions/constants.ts b/site/src/components/WorkspaceActions/constants.ts index abb2af5987bbc..d019721f6a852 100644 --- a/site/src/components/WorkspaceActions/constants.ts +++ b/site/src/components/WorkspaceActions/constants.ts @@ -29,7 +29,24 @@ interface WorkspaceAbilities { canAcceptJobs: boolean } -export const statusToAbilities: Record = { +export const buttonAbilities = ( + status: WorkspaceStatus, + hasTemplateParameters: boolean, +): WorkspaceAbilities => { + if (hasTemplateParameters) { + return statusToAbilities[status] + } + + const all = statusToAbilities[status] + return { + ...all, + actions: all.actions.filter( + (action) => action !== ButtonTypesEnum.buildParameters, + ), + } +} + +const statusToAbilities: Record = { starting: { actions: [ButtonTypesEnum.starting], canCancel: true, diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index a1882c4422c5e..1fae46c62c795 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -20,6 +20,7 @@ export const WorkspacePage: FC = () => { workspace, getWorkspaceError, getTemplateWarning, + getTemplateParametersWarning, checkPermissionsError, } = workspaceState.context const [quotaState, quotaSend] = useMachine(quotaMachine) @@ -50,6 +51,12 @@ export const WorkspacePage: FC = () => { {Boolean(getTemplateWarning) && ( )} + {Boolean(getTemplateParametersWarning) && ( + + )} {Boolean(checkPermissionsError) && ( )} diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index bd02e8fb03c41..d346b48caa3b9 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -45,6 +45,7 @@ export const WorkspaceReadyPage = ({ const { workspace, template, + templateParameters, refreshWorkspaceWarning, builds, getBuildsError, @@ -126,6 +127,7 @@ export const WorkspaceReadyPage = ({ buildInfo={buildInfo} applicationsHost={applicationsHost} template={template} + templateParameters={templateParameters} quota_budget={quotaState.context.quota?.budget} /> event.data, }), + assignTemplateParameters: assign({ + templateParameters: (_, event) => event.data, + }), assignPermissions: assign({ // Setting event.data as Permissions to be more stricted. So we know // what permissions we asked for. @@ -566,9 +599,18 @@ export const workspaceMachine = createMachine( displayGetTemplateWarning: () => { displayError(Language.getTemplateWarning) }, - clearGettingTemplateWarning: assign({ + clearGetTemplateWarning: assign({ getTemplateWarning: (_) => undefined, }), + assignGetTemplateParametersWarning: assign({ + getTemplateParametersWarning: (_, event) => event.data, + }), + displayGetTemplateParametersWarning: () => { + displayError(Language.getTemplateParametersWarning) + }, + clearGetTemplateParametersWarning: assign({ + getTemplateParametersWarning: (_) => undefined, + }), // Timeline assignBuilds: assign({ builds: (_, event) => event.data, @@ -629,6 +671,15 @@ export const workspaceMachine = createMachine( throw Error("Cannot get template without workspace") } }, + getTemplateParameters: async (context) => { + if (context.workspace) { + return await API.getTemplateVersionRichParameters( + context.workspace.latest_build.template_version_id, + ) + } else { + throw Error("Cannot get template parameters without workspace") + } + }, startWorkspaceWithLatestTemplate: (context) => async (send) => { if (context.workspace && context.template) { const startWorkspacePromise = await API.startWorkspace( From 1873645dbc2b7ff366d278ee475d00d21cb7b754 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 1 Feb 2023 15:39:44 +0100 Subject: [PATCH 46/46] Fix --- site/src/components/Workspace/Workspace.tsx | 3 +-- site/src/pages/WorkspacePage/WorkspacePage.test.tsx | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 6abf08bb8d7b0..0b06fb693648d 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -126,7 +126,6 @@ export const Workspace: FC> = ({ if (template !== undefined) { transitionStats = ActiveTransition(template, workspace) } - return ( > = ({ 0 + templateParameters ? templateParameters.length > 0 : false } isOutdated={workspace.outdated} handleStart={handleStart} diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 2110e27ada732..c793863b16d9d 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -30,6 +30,7 @@ const { t } = i18next // It renders the workspace page and waits for it be loaded const renderWorkspacePage = async () => { jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate) + jest.spyOn(api, "getTemplateVersionRichParameters").mockResolvedValueOnce([]) renderWithAuth(, { route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`, path: "/@:username/:workspace",