From b1f379eca44b10229a8954075a929b36f4b5b398 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 7 Mar 2023 18:14:39 +0000 Subject: [PATCH 1/2] refactor(site): Highlight immutable parameters and do a few tweaks --- .../HorizontalForm/HorizontalForm.tsx | 11 +- .../RichParameterInput.stories.tsx | 73 ++- .../RichParameterInput/RichParameterInput.tsx | 97 ++-- .../CreateWorkspacePageView.stories.tsx | 29 ++ .../CreateWorkspacePageView.tsx | 480 +++++++----------- 5 files changed, 342 insertions(+), 348 deletions(-) diff --git a/site/src/components/HorizontalForm/HorizontalForm.tsx b/site/src/components/HorizontalForm/HorizontalForm.tsx index 42dd9e00fa369..50ea8657b288b 100644 --- a/site/src/components/HorizontalForm/HorizontalForm.tsx +++ b/site/src/components/HorizontalForm/HorizontalForm.tsx @@ -5,6 +5,7 @@ import { } from "components/FormFooter/FormFooter" import { Stack } from "components/Stack/Stack" import { FC, HTMLProps, PropsWithChildren } from "react" +import { combineClasses } from "util/combineClasses" export const HorizontalForm: FC< PropsWithChildren & HTMLProps @@ -21,12 +22,16 @@ export const HorizontalForm: FC< } export const FormSection: FC< - PropsWithChildren & { title: string; description: string | JSX.Element } -> = ({ children, title, description }) => { + PropsWithChildren & { + title: string + description: string | JSX.Element + className?: string + } +> = ({ children, title, description, className }) => { const styles = useStyles() return ( -
+

{title}

{description}
diff --git a/site/src/components/RichParameterInput/RichParameterInput.stories.tsx b/site/src/components/RichParameterInput/RichParameterInput.stories.tsx index 9338a4233c313..fb6530cf8017e 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.stories.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.stories.tsx @@ -29,8 +29,8 @@ const createTemplateVersionParameter = ( validation_regex: "", validation_min: 0, validation_max: 0, - validation_monotonic: "", - + validation_monotonic: "increasing", + description_plaintext: "", ...partial, } } @@ -38,6 +38,7 @@ const createTemplateVersionParameter = ( export const Basic = Template.bind({}) Basic.args = { initialValue: "initial-value", + id: "project_name", parameter: createTemplateVersionParameter({ name: "project_name", description: @@ -48,6 +49,7 @@ Basic.args = { export const NumberType = Template.bind({}) NumberType.args = { initialValue: "4", + id: "number_parameter", parameter: createTemplateVersionParameter({ name: "number_parameter", type: "number", @@ -58,6 +60,7 @@ NumberType.args = { export const BooleanType = Template.bind({}) BooleanType.args = { initialValue: "false", + id: "bool_parameter", parameter: createTemplateVersionParameter({ name: "bool_parameter", type: "bool", @@ -68,6 +71,7 @@ BooleanType.args = { export const OptionsType = Template.bind({}) OptionsType.args = { initialValue: "first_option", + id: "options_parameter", parameter: createTemplateVersionParameter({ name: "options_parameter", type: "string", @@ -94,3 +98,68 @@ OptionsType.args = { ], }), } + +export const IconLabel = Template.bind({}) +IconLabel.args = { + initialValue: "initial-value", + id: "project_name", + parameter: createTemplateVersionParameter({ + name: "project_name", + description: + "Customize the name of a Google Cloud project that will be created!", + icon: "/emojis/1f30e.png", + }), +} + +export const NoDescription = Template.bind({}) +NoDescription.args = { + initialValue: "", + id: "region", + parameter: createTemplateVersionParameter({ + name: "Region", + description: "", + description_plaintext: "", + type: "string", + mutable: false, + default_value: "", + icon: "/emojis/1f30e.png", + options: [ + { + name: "Pittsburgh", + description: "", + value: "us-pittsburgh", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "Helsinki", + description: "", + value: "eu-helsinki", + icon: "/emojis/1f1eb-1f1ee.png", + }, + { + name: "Sydney", + description: "", + value: "ap-sydney", + icon: "/emojis/1f1e6-1f1fa.png", + }, + ], + }), +} + +export const DescriptionWithLinks = Template.bind({}) +DescriptionWithLinks.args = { + initialValue: "", + id: "coder-repository-directory", + parameter: createTemplateVersionParameter({ + name: "Coder Repository Directory", + description: + "The directory specified will be created and [coder/coder](https://github.com/coder/coder) will be automatically cloned into it 🪄.", + description_plaintext: + "The directory specified will be created and coder/coder (https://github.com/coder/coder) will be automatically cloned into it 🪄.", + type: "string", + mutable: true, + default_value: "~/coder", + icon: "", + options: [], + }), +} diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index 960faebde35ae..161b5db08bfd8 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -14,45 +14,39 @@ const isBoolean = (parameter: TemplateVersionParameter) => { } export interface ParameterLabelProps { - index: number + id: string parameter: TemplateVersionParameter } -const ParameterLabel: FC = ({ index, parameter }) => { +const ParameterLabel: FC = ({ id, parameter }) => { const styles = useStyles() + const hasDescription = parameter.description && parameter.description !== "" return ( - - + + + {hasDescription ? ( + + {parameter.name} + + {parameter.description} + + + ) : ( + {parameter.name} + )} + + ) } @@ -62,6 +56,7 @@ export interface RichParameterInputProps { parameter: TemplateVersionParameter onChange: (value: string) => void initialValue?: string + id: string } export const RichParameterInput: FC = ({ @@ -70,16 +65,16 @@ export const RichParameterInput: FC = ({ onChange, parameter, initialValue, - ...props + ...fieldProps }) => { const styles = useStyles() return ( - +
= ({ value={option.value} control={} label={ - + {option.icon && ( = ({ ) } -const iconSize = 20 -const optionIconSize = 24 +const optionIconSize = 20 const useStyles = makeStyles((theme) => ({ - labelName: { + label: { + marginBottom: theme.spacing(0.5), + }, + labelCaption: { fontSize: 14, color: theme.palette.text.secondary, - display: "block", - marginBottom: theme.spacing(1.0), }, - labelNameWithIcon: { - marginBottom: theme.spacing(0.5), - }, - labelDescription: { + labelPrimary: { fontSize: 16, color: theme.palette.text.primary, - display: "block", fontWeight: 600, + + "& p": { + margin: 0, + lineHeight: "20px", // Keep the same as ParameterInput + }, }, labelImmutable: { marginTop: theme.spacing(0.5), @@ -212,18 +208,23 @@ const useStyles = makeStyles((theme) => ({ alignItems: "center", gap: theme.spacing(1), }, - iconWrapper: { - float: "left", + labelIconWrapper: { + width: theme.spacing(2.5), + height: theme.spacing(2.5), + display: "block", }, - icon: { - maxHeight: iconSize, - width: iconSize, - marginRight: theme.spacing(1.0), + labelIcon: { + width: "100%", + height: "100%", + objectFit: "contain", + }, + radioOption: { + display: "flex", + alignItems: "center", + gap: theme.spacing(1.5), }, optionIcon: { maxHeight: optionIconSize, width: optionIconSize, - marginRight: theme.spacing(1.0), - float: "left", }, })) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx index 3a600db95e6b4..82e705b733ab8 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx @@ -133,6 +133,35 @@ RichParameters.args = { MockTemplateVersionParameter1, MockTemplateVersionParameter2, MockTemplateVersionParameter3, + { + name: "Region", + description: "", + description_plaintext: "", + type: "string", + mutable: false, + default_value: "", + icon: "/emojis/1f30e.png", + options: [ + { + name: "Pittsburgh", + description: "", + value: "us-pittsburgh", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "Helsinki", + description: "", + value: "eu-helsinki", + icon: "/emojis/1f1eb-1f1ee.png", + }, + { + name: "Sydney", + description: "", + value: "ap-sydney", + icon: "/emojis/1f1e6-1f1fa.png", + }, + ], + }, ], createWorkspaceErrors: {}, } diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index e4876bff3e28b..23ae9c1bb8c64 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -1,6 +1,5 @@ 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" @@ -11,11 +10,17 @@ import { useTranslation } from "react-i18next" import { getFormHelpers, nameValidator, onChangeTrimmed } from "util/formUtils" import * as Yup from "yup" import { AlertBanner } from "components/AlertBanner/AlertBanner" -import { makeStyles } from "@material-ui/core/styles" import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm" import { SelectedTemplate } from "./SelectedTemplate" import { Loader } from "components/Loader/Loader" import { GitAuth } from "components/GitAuth/GitAuth" +import { + FormFields, + FormSection, + FormFooter, + HorizontalForm, +} from "components/HorizontalForm/HorizontalForm" +import { makeStyles } from "@material-ui/core/styles" export enum CreateWorkspaceErrors { GET_TEMPLATES_ERROR = "getTemplatesError", @@ -49,8 +54,6 @@ export interface CreateWorkspacePageViewProps { export const CreateWorkspacePageView: FC< React.PropsWithChildren > = (props) => { - const styles = useStyles() - const formFooterStyles = useFormFooterStyles() const [parameterValues, setParameterValues] = useState< Record >(props.defaultParameterValues ?? {}) @@ -67,8 +70,8 @@ export const CreateWorkspacePageView: FC< // to disappear. setGitAuthErrors({}) }, [props.templateGitAuth]) - const { t } = useTranslation("createWorkspacePage") + const styles = useStyles() const form: FormikContextType = useFormik({ @@ -203,313 +206,200 @@ export const CreateWorkspacePageView: FC< return ( -
- - {/* General info */} -
-
-

General info

-

- The template and name of your new workspace. -

-
- - - {props.selectedTemplate && ( - - )} + + {/* General info */} + + + {props.selectedTemplate && ( + + )} - + + + + {/* Workspace owner */} + {props.canCreateForUser && ( + + + - -
- - {/* Workspace owner */} - {props.canCreateForUser && ( -
-
-

Workspace owner

-

- The user that is going to own this workspace. If you are - admin, you can create workspace for others. -

-
+ + + )} - - 0 && ( + + + {props.templateGitAuth.map((auth, index) => ( + - -
- )} - - {/* Template git auth */} - {props.templateGitAuth && props.templateGitAuth.length > 0 && ( -
-
-

- Git Authentication -

-

- This template requires authentication to automatically perform - Git operations on create. -

-
+ ))} + + + )} - - {props.templateGitAuth.map((auth, index) => ( - 0 && ( + + + {props.templateSchema + // We only want to show schema that have redisplay_value equals true + .filter((schema) => schema.redisplay_value) + .map((schema) => ( + { + setParameterValues({ + ...parameterValues, + [schema.name]: value, + }) + }} + schema={schema} /> ))} - -
- )} - - {/* Template params */} - {props.templateSchema && props.templateSchema.length > 0 && ( -
-
-

Template params

-

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

-
+ + + )} - - {props.templateSchema - // We only want to show schema that have redisplay_value equals true - .filter((schema) => schema.redisplay_value) - .map((schema) => ( - { - setParameterValues({ - ...parameterValues, - [schema.name]: value, - }) - }} - schema={schema} - /> - ))} - -
+ {/* Mutable rich parameters */} + {props.templateParameters && + props.templateParameters.filter((p) => p.mutable).length > 0 && ( + + + {props.templateParameters.map( + (parameter, index) => + parameter.mutable && ( + { + form.setFieldValue("rich_parameter_values." + index, { + name: parameter.name, + value: value, + }) + }} + parameter={parameter} + initialValue={workspaceBuildParameterValue( + initialRichParameterValues, + parameter, + )} + /> + ), + )} + + )} - {/* Immutable rich parameters */} - {props.templateParameters && - props.templateParameters.filter((p) => !p.mutable).length > 0 && ( -
-
-

- Immutable parameters -

-

- Those values are provided by your template‘s Terraform - configuration. Values cannot be changed after creating the - workspace. -

-
- - - {props.templateParameters.map( - (parameter, index) => - !parameter.mutable && ( - { - form.setFieldValue( - "rich_parameter_values." + index, - { - name: parameter.name, - value: value, - }, - ) - }} - parameter={parameter} - initialValue={workspaceBuildParameterValue( - initialRichParameterValues, - parameter, - )} - /> - ), - )} - -
- )} - - {/* Mutable rich parameters */} - {props.templateParameters && - props.templateParameters.filter((p) => p.mutable).length > 0 && ( -
-
-

- Mutable parameters -

-

- Those values are provided by your template‘s Terraform - configuration. Values can be changed after creating the - workspace. -

-
+ {/* Immutable rich parameters */} + {props.templateParameters && + props.templateParameters.filter((p) => !p.mutable).length > 0 && ( + + Those values are also parameters provided from your Terraform + configuration but they{" "} + + cannot be changed after creating the workspace. + + + } + > + + {props.templateParameters.map( + (parameter, index) => + !parameter.mutable && ( + { + form.setFieldValue("rich_parameter_values." + index, { + name: parameter.name, + value: value, + }) + }} + parameter={parameter} + initialValue={workspaceBuildParameterValue( + initialRichParameterValues, + parameter, + )} + /> + ), + )} + + + )} - - {props.templateParameters.map( - (parameter, index) => - parameter.mutable && ( - { - form.setFieldValue( - "rich_parameter_values." + index, - { - name: parameter.name, - value: value, - }, - ) - }} - parameter={parameter} - initialValue={workspaceBuildParameterValue( - initialRichParameterValues, - parameter, - )} - /> - ), - )} - -
- )} - -
-
+ +
) } const useStyles = makeStyles((theme) => ({ - formSections: { - [theme.breakpoints.down("sm")]: { - gap: theme.spacing(8), - }, + warningText: { + color: theme.palette.warning.light, }, - - formSection: { - display: "flex", - alignItems: "flex-start", - gap: theme.spacing(15), - - [theme.breakpoints.down("sm")]: { - flexDirection: "column", - gap: theme.spacing(2), - }, - }, - - formSectionInfo: { - width: 312, - flexShrink: 0, - position: "sticky", - top: theme.spacing(3), - - [theme.breakpoints.down("sm")]: { - width: "100%", - position: "initial", - }, - }, - - formSectionInfoTitle: { - fontSize: 20, - color: theme.palette.text.primary, - fontWeight: 400, - margin: 0, - marginBottom: theme.spacing(1), - }, - - formSectionInfoDescription: { - fontSize: 14, - color: theme.palette.text.secondary, - lineHeight: "160%", - margin: 0, - }, - - 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), - }, + warningSection: { + border: `1px solid ${theme.palette.warning.light}`, + borderRadius: 8, + backgroundColor: theme.palette.background.paper, + padding: theme.spacing(10), + marginLeft: -theme.spacing(10), + marginRight: -theme.spacing(10), }, })) From 134ba0ea48675f25adb51058e3b08548609792d2 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 8 Mar 2023 13:22:17 +0000 Subject: [PATCH 2/2] Fix tests --- .../RichParameterInput/RichParameterInput.tsx | 5 ++--- .../CreateWorkspacePage.test.tsx | 2 ++ .../CreateWorkspacePage/CreateWorkspacePageView.tsx | 13 ++++--------- .../WorkspaceBuildParametersPage.test.tsx | 5 +++++ 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index 161b5db08bfd8..c0cdf0721a27c 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -2,7 +2,7 @@ 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 TextField, { TextFieldProps } from "@material-ui/core/TextField" import { Stack } from "components/Stack/Stack" import { FC, useState } from "react" import { TemplateVersionParameter } from "../../api/typesGenerated" @@ -50,9 +50,8 @@ const ParameterLabel: FC = ({ id, parameter }) => { ) } -export interface RichParameterInputProps { +export type RichParameterInputProps = TextFieldProps & { index: number - disabled?: boolean parameter: TemplateVersionParameter onChange: (value: string) => void initialValue?: string diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index c2f8e24fdba75..fa0a248369e13 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -179,6 +179,7 @@ describe("CreateWorkspacePage", () => { const secondParameterField = await screen.findByLabelText( MockTemplateVersionParameter2.name, + { exact: false }, ) expect(secondParameterField).toBeDefined() @@ -212,6 +213,7 @@ describe("CreateWorkspacePage", () => { const thirdParameterField = await screen.findByLabelText( MockTemplateVersionParameter3.name, + { exact: false }, ) expect(thirdParameterField).toBeDefined() fireEvent.change(thirdParameterField, { diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 23ae9c1bb8c64..9858842554839 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -233,8 +233,7 @@ export const CreateWorkspacePageView: FC< {props.canCreateForUser && ( 0 && ( {props.templateGitAuth.map((auth, index) => ( @@ -271,8 +269,7 @@ export const CreateWorkspacePageView: FC< {props.templateSchema && props.templateSchema.length > 0 && ( {props.templateSchema @@ -301,9 +298,7 @@ export const CreateWorkspacePageView: FC< props.templateParameters.filter((p) => p.mutable).length > 0 && ( {props.templateParameters.map( diff --git a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx index 8da25a7f2399a..57d1de52439c3 100644 --- a/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx +++ b/site/src/pages/WorkspaceBuildParametersPage/WorkspaceBuildParametersPage.test.tsx @@ -80,11 +80,13 @@ describe("WorkspaceBuildParametersPage", () => { const firstParameter = await screen.findByLabelText( MockTemplateVersionParameter1.name, + { exact: false }, ) expect(firstParameter).toBeDefined() const secondParameter = await screen.findByLabelText( MockTemplateVersionParameter2.name, + { exact: false }, ) expect(secondParameter).toBeDefined() }) @@ -113,6 +115,7 @@ describe("WorkspaceBuildParametersPage", () => { const secondParameterField = await screen.findByLabelText( MockTemplateVersionParameter2.name, + { exact: false }, ) expect(secondParameterField).toBeDefined() @@ -151,6 +154,7 @@ describe("WorkspaceBuildParametersPage", () => { const secondParameterField = await screen.findByLabelText( MockTemplateVersionParameter2.name, + { exact: false }, ) expect(secondParameterField).toBeDefined() @@ -189,6 +193,7 @@ describe("WorkspaceBuildParametersPage", () => { const secondParameterField = await screen.findByLabelText( MockTemplateVersionParameter5.name, + { exact: false }, ) expect(secondParameterField).toBeDefined()