refactor(site): refactor rich parameter input and form initial data (#9440)

This commit is contained in:
Bruno Quaresma
2023-08-30 18:42:36 -03:00
committed by GitHub
parent db93f17dab
commit d29696296f
12 changed files with 132 additions and 230 deletions
@@ -5,12 +5,14 @@ import { FC } from "react"
export type MultiTextFieldProps = {
label: string
id?: string
values: string[]
onChange: (values: string[]) => void
}
export const MultiTextField: FC<MultiTextFieldProps> = ({
label,
id,
values,
onChange,
}) => {
@@ -30,6 +32,7 @@ export const MultiTextField: FC<MultiTextFieldProps> = ({
/>
))}
<input
id={id}
aria-label={label}
className={styles.input}
onKeyDown={(event) => {
@@ -35,7 +35,7 @@ const createTemplateVersionParameter = (
export const Basic: Story = {
args: {
initialValue: "initial-value",
value: "initial-value",
id: "project_name",
parameter: createTemplateVersionParameter({
name: "project_name",
@@ -47,7 +47,7 @@ export const Basic: Story = {
export const NumberType: Story = {
args: {
initialValue: "4",
value: "4",
id: "number_parameter",
parameter: createTemplateVersionParameter({
name: "number_parameter",
@@ -59,7 +59,7 @@ export const NumberType: Story = {
export const BooleanType: Story = {
args: {
initialValue: "false",
value: "false",
id: "bool_parameter",
parameter: createTemplateVersionParameter({
name: "bool_parameter",
@@ -71,7 +71,7 @@ export const BooleanType: Story = {
export const OptionsType: Story = {
args: {
initialValue: "first_option",
value: "first_option",
id: "options_parameter",
parameter: createTemplateVersionParameter({
name: "options_parameter",
@@ -103,7 +103,7 @@ export const OptionsType: Story = {
export const ListStringType: Story = {
args: {
initialValue: JSON.stringify(["first", "second", "third"]),
value: JSON.stringify(["first", "second", "third"]),
id: "list_string_parameter",
parameter: createTemplateVersionParameter({
name: "list_string_parameter",
@@ -115,7 +115,7 @@ export const ListStringType: Story = {
export const IconLabel: Story = {
args: {
initialValue: "initial-value",
value: "initial-value",
id: "project_name",
parameter: createTemplateVersionParameter({
name: "project_name",
@@ -128,7 +128,7 @@ export const IconLabel: Story = {
export const NoDescription: Story = {
args: {
initialValue: "",
value: "",
id: "region",
parameter: createTemplateVersionParameter({
name: "Region",
@@ -164,7 +164,7 @@ export const NoDescription: Story = {
export const DescriptionWithLinks: Story = {
args: {
initialValue: "",
value: "",
id: "coder-repository-directory",
parameter: createTemplateVersionParameter({
name: "Coder Repository Directory",
@@ -183,7 +183,7 @@ export const DescriptionWithLinks: Story = {
export const BasicWithDisplayName: Story = {
args: {
initialValue: "initial-value",
value: "initial-value",
id: "project_name",
parameter: createTemplateVersionParameter({
name: "project_name",
@@ -4,7 +4,7 @@ import RadioGroup from "@mui/material/RadioGroup"
import { makeStyles } from "@mui/styles"
import TextField, { TextFieldProps } from "@mui/material/TextField"
import { Stack } from "components/Stack/Stack"
import { FC, useState } from "react"
import { FC } from "react"
import { TemplateVersionParameter } from "../../api/typesGenerated"
import { colors } from "theme/colors"
import { MemoizedMarkdown } from "components/Markdown/Markdown"
@@ -17,11 +17,10 @@ const isBoolean = (parameter: TemplateVersionParameter) => {
}
export interface ParameterLabelProps {
id: string
parameter: TemplateVersionParameter
}
const ParameterLabel: FC<ParameterLabelProps> = ({ id, parameter }) => {
const ParameterLabel: FC<ParameterLabelProps> = ({ parameter }) => {
const styles = useStyles()
const hasDescription = parameter.description && parameter.description !== ""
const displayName = parameter.display_name
@@ -29,7 +28,7 @@ const ParameterLabel: FC<ParameterLabelProps> = ({ id, parameter }) => {
: parameter.name
return (
<label htmlFor={id}>
<label htmlFor={parameter.name}>
<Stack direction="row" alignItems="center">
{parameter.icon && (
<span className={styles.labelIconWrapper}>
@@ -60,22 +59,15 @@ type Size = "medium" | "small"
export type RichParameterInputProps = Omit<
TextFieldProps,
"onChange" | "size"
"size" | "onChange"
> & {
index: number
parameter: TemplateVersionParameter
onChange: (value: string) => void
initialValue?: string
id: string
size?: Size
}
export const RichParameterInput: FC<RichParameterInputProps> = ({
index,
disabled,
onChange,
parameter,
initialValue,
size = "medium",
...fieldProps
}) => {
@@ -86,16 +78,9 @@ export const RichParameterInput: FC<RichParameterInputProps> = ({
className={size}
data-testid={`parameter-field-${parameter.name}`}
>
<ParameterLabel id={fieldProps.id} parameter={parameter} />
<ParameterLabel parameter={parameter} />
<Box sx={{ display: "flex", flexDirection: "column" }}>
<RichParameterField
{...fieldProps}
index={index}
disabled={disabled}
onChange={onChange}
parameter={parameter}
initialValue={initialValue}
/>
<RichParameterField {...fieldProps} size={size} parameter={parameter} />
</Box>
</Stack>
)
@@ -105,22 +90,20 @@ const RichParameterField: React.FC<RichParameterInputProps> = ({
disabled,
onChange,
parameter,
initialValue,
value,
size,
...props
}) => {
const [parameterValue, setParameterValue] = useState(initialValue)
const styles = useStyles()
if (isBoolean(parameter)) {
return (
<RadioGroup
id={parameter.name}
data-testid="parameter-field-bool"
className={styles.radioGroup}
defaultValue={parameterValue}
onChange={(event) => {
onChange(event.target.value)
}}
value={value}
onChange={(_, value) => onChange(value)}
>
<FormControlLabel
disabled={disabled}
@@ -141,12 +124,11 @@ const RichParameterField: React.FC<RichParameterInputProps> = ({
if (parameter.options.length > 0) {
return (
<RadioGroup
id={parameter.name}
data-testid="parameter-field-options"
className={styles.radioGroup}
defaultValue={parameterValue}
onChange={(event) => {
onChange(event.target.value)
}}
value={value}
onChange={(_, value) => onChange(value)}
>
{parameter.options.map((option) => (
<FormControlLabel
@@ -178,9 +160,13 @@ const RichParameterField: React.FC<RichParameterInputProps> = ({
if (parameter.type === "list(string)") {
let values: string[] = []
if (parameterValue) {
if (typeof value !== "string") {
throw new Error("Expected value to be a string")
}
if (value) {
try {
values = JSON.parse(parameterValue) as string[]
values = JSON.parse(value) as string[]
} catch (e) {
console.error("Error parsing list(string) parameter", e)
}
@@ -188,13 +174,13 @@ const RichParameterField: React.FC<RichParameterInputProps> = ({
return (
<MultiTextField
id={parameter.name}
data-testid="parameter-field-list-of-string"
label={props.label as string}
values={values}
onChange={(values) => {
try {
const value = JSON.stringify(values)
setParameterValue(value)
onChange(value)
} catch (e) {
console.error("Error on change of list(string) parameter", e)
@@ -210,15 +196,15 @@ const RichParameterField: React.FC<RichParameterInputProps> = ({
return (
<TextField
{...props}
id={parameter.name}
data-testid="parameter-field-text"
className={styles.textField}
type={parameter.type}
disabled={disabled}
required={parameter.required}
placeholder={parameter.default_value}
value={parameterValue}
value={value}
onChange={(event) => {
setParameterValue(event.target.value)
onChange(event.target.value)
}}
/>
@@ -34,7 +34,6 @@ export const MutableTemplateParametersSection: FC<
parameter.mutable && (
<RichParameterInput
{...getInputProps(parameter, index)}
index={index}
key={parameter.name}
parameter={parameter}
/>
@@ -73,7 +72,6 @@ export const ImmutableTemplateParametersSection: FC<
!parameter.mutable && (
<RichParameterInput
{...getInputProps(parameter, index)}
index={index}
key={parameter.name}
parameter={parameter}
/>
@@ -22,7 +22,7 @@ import { useFormik } from "formik"
import { useRef, useState } from "react"
import { docs } from "utils/docs"
import { getFormHelpers } from "utils/formUtils"
import { getInitialParameterValues } from "utils/richParameters"
import { getInitialRichParameterValues } from "utils/richParameters"
export const BuildParametersPopover = ({
workspace,
@@ -148,7 +148,7 @@ const Form = ({
}) => {
const form = useFormik({
initialValues: {
rich_parameter_values: getInitialParameterValues(
rich_parameter_values: getInitialRichParameterValues(
ephemeralParameters,
buildParameters,
),
@@ -168,8 +168,6 @@ const Form = ({
{...getFieldHelpers("rich_parameter_values[" + index + "].value")}
key={parameter.name}
parameter={parameter}
initialValue={form.values.rich_parameter_values[index]?.value}
index={index}
size="small"
onChange={async (value) => {
await form.setFieldValue(`rich_parameter_values[${index}]`, {
@@ -16,9 +16,8 @@ import {
} from "components/Form/Form"
import { makeStyles } from "@mui/styles"
import {
selectInitialRichParametersValues,
getInitialRichParameterValues,
useValidationSchemaForRichParameters,
workspaceBuildParameterValue,
} from "utils/richParameters"
import {
ImmutableTemplateParametersSection,
@@ -55,10 +54,6 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
onSubmit,
onCancel,
}) => {
const initialRichParameterValues = selectInitialRichParametersValues(
parameters,
defaultBuildParameters,
)
const { t } = useTranslation("createWorkspacePage")
const styles = useStyles()
const [owner, setOwner] = useState(defaultOwner)
@@ -68,7 +63,10 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
initialValues: {
name: defaultName,
template_id: template.id,
rich_parameter_values: initialRichParameterValues,
rich_parameter_values: getInitialRichParameterValues(
parameters,
defaultBuildParameters,
),
},
validationSchema: Yup.object({
name: nameValidator(t("nameLabel", { ns: "createWorkspacePage" })),
@@ -173,10 +171,6 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
value: value,
})
},
initialValue: workspaceBuildParameterValue(
initialRichParameterValues,
parameter,
),
disabled: form.isSubmitting,
}
}}
@@ -195,10 +189,6 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
value: value,
})
},
initialValue: workspaceBuildParameterValue(
initialRichParameterValues,
parameter,
),
disabled: form.isSubmitting,
}
}}
@@ -17,13 +17,10 @@ import {
TemplateParametersSectionProps,
} from "components/TemplateParameters/TemplateParameters"
import { useClipboard } from "hooks/useClipboard"
import { FC, useState } from "react"
import { FC, useEffect, useState } from "react"
import { Helmet } from "react-helmet-async"
import { pageTitle } from "utils/page"
import {
selectInitialRichParametersValues,
workspaceBuildParameterValue,
} from "utils/richParameters"
import { getInitialRichParameterValues } from "utils/richParameters"
import { paramsUsedToCreateWorkspace } from "utils/workspace"
type ButtonValues = Record<string, string>
@@ -54,31 +51,23 @@ export const TemplateEmbedPageView: FC<{
template: Template
templateParameters?: TemplateVersionParameter[]
}> = ({ template, templateParameters }) => {
const [buttonValues, setButtonValues] = useState<ButtonValues>({
mode: "manual",
})
const initialRichParametersValues = templateParameters
? selectInitialRichParametersValues(templateParameters)
: undefined
const [buttonValues, setButtonValues] = useState<ButtonValues | undefined>(
undefined,
)
const deploymentUrl = `${window.location.protocol}//${window.location.host}`
const createWorkspaceUrl = `${deploymentUrl}/templates/${template.name}/workspace`
const createWorkspaceParams = new URLSearchParams(buttonValues)
const buttonUrl = `${createWorkspaceUrl}?${createWorkspaceParams.toString()}`
const buttonMkdCode = `[![Open in Coder](${deploymentUrl}/open-in-coder.svg)](${buttonUrl})`
const clipboard = useClipboard(buttonMkdCode)
const getInputProps: TemplateParametersSectionProps["getInputProps"] = (
parameter,
) => {
if (!initialRichParametersValues) {
throw new Error("initialRichParametersValues is undefined")
if (!buttonValues) {
throw new Error("buttonValues is undefined")
}
return {
id: parameter.name,
initialValue: workspaceBuildParameterValue(
initialRichParametersValues,
parameter,
),
value: buttonValues[`param.${parameter.name}`] ?? "",
onChange: (value) => {
setButtonValues((buttonValues) => ({
...buttonValues,
@@ -88,12 +77,28 @@ export const TemplateEmbedPageView: FC<{
}
}
// template parameters is async so we need to initialize the values after it
// is loaded
useEffect(() => {
if (templateParameters && !buttonValues) {
const buttonValues: ButtonValues = {
mode: "manual",
}
for (const parameter of getInitialRichParameterValues(
templateParameters,
)) {
buttonValues[`param.${parameter.name}`] = parameter.value
}
setButtonValues(buttonValues)
}
}, [buttonValues, templateParameters])
return (
<>
<Helmet>
<title>{pageTitle(`${template.name} · Embed`)}</title>
</Helmet>
{!templateParameters ? (
{!buttonValues || !templateParameters ? (
<Loader />
) : (
<Box display="flex" alignItems="flex-start" gap={6}>
@@ -14,9 +14,8 @@ import {
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"
import { useFormik } from "formik"
import {
selectInitialRichParametersValues,
getInitialRichParameterValues,
useValidationSchemaForRichParameters,
workspaceBuildParameterValue,
} from "utils/richParameters"
import * as Yup from "yup"
import DialogActions from "@mui/material/DialogActions"
@@ -26,18 +25,16 @@ import { useTranslation } from "react-i18next"
export type UpdateBuildParametersDialogProps = DialogProps & {
onClose: () => void
onUpdate: (buildParameters: WorkspaceBuildParameter[]) => void
missedParameters?: TemplateVersionParameter[]
missedParameters: TemplateVersionParameter[]
}
export const UpdateBuildParametersDialog: FC<
UpdateBuildParametersDialogProps
> = ({ missedParameters, onUpdate, ...dialogProps }) => {
const styles = useStyles()
const initialRichParameterValues =
selectInitialRichParametersValues(missedParameters)
const form = useFormik({
initialValues: {
rich_parameter_values: initialRichParameterValues,
rich_parameter_values: getInitialRichParameterValues(missedParameters),
},
validationSchema: Yup.object({
rich_parameter_values: useValidationSchemaForRichParameters(
@@ -84,13 +81,8 @@ export const UpdateBuildParametersDialog: FC<
{...getFieldHelpers(
"rich_parameter_values[" + index + "].value",
)}
initialValue={workspaceBuildParameterValue(
initialRichParameterValues,
parameter,
)}
key={parameter.name}
parameter={parameter}
index={index}
onChange={async (value) => {
await form.setFieldValue(
"rich_parameter_values." + index,
@@ -212,7 +212,7 @@ export const WorkspaceReadyPage = ({
}}
/>
<UpdateBuildParametersDialog
missedParameters={missedParameters}
missedParameters={missedParameters ?? []}
open={workspaceState.matches(
"ready.build.askingForMissedBuildParameters",
)}
@@ -9,9 +9,8 @@ import { useFormik } from "formik"
import { FC } from "react"
import { useTranslation } from "react-i18next"
import {
getInitialParameterValues,
getInitialRichParameterValues,
useValidationSchemaForRichParameters,
workspaceBuildParameterValue,
} from "utils/richParameters"
import * as Yup from "yup"
import { getFormHelpers } from "utils/formUtils"
@@ -40,17 +39,12 @@ export const WorkspaceParametersForm: FC<{
isSubmitting,
}) => {
const { t } = useTranslation("workspaceSettingsPage")
const mutableParameters = templateVersionRichParameters.filter(
(param) => param.mutable === true,
)
const immutableParameters = templateVersionRichParameters.filter(
(param) => param.mutable === false,
)
const form = useFormik<WorkspaceParametersFormValues>({
onSubmit,
initialValues: {
rich_parameter_values: getInitialParameterValues(
mutableParameters,
rich_parameter_values: getInitialRichParameterValues(
templateVersionRichParameters,
buildParameters,
),
},
@@ -65,12 +59,15 @@ export const WorkspaceParametersForm: FC<{
form,
error,
)
const hasEphemeralParameters = mutableParameters.some(
const hasEphemeralParameters = templateVersionRichParameters.some(
(parameter) => parameter.ephemeral,
)
const hasNonEphemeralParameters = mutableParameters.some(
const hasNonEphemeralParameters = templateVersionRichParameters.some(
(parameter) => !parameter.ephemeral,
)
const hasImmutableParameters = templateVersionRichParameters.some(
(parameter) => !parameter.mutable,
)
return (
<HorizontalForm onSubmit={form.handleSubmit} data-testid="form">
@@ -80,16 +77,15 @@ export const WorkspaceParametersForm: FC<{
description={t("parametersDescription").toString()}
>
<FormFields>
{mutableParameters.map((parameter, index) =>
{templateVersionRichParameters.map((parameter, index) =>
// Since we are adding the values to the form based on the index
// we can't filter them to not loose the right index position
parameter.ephemeral ? null : (
parameter.mutable && !parameter.ephemeral ? (
<RichParameterInput
{...getFieldHelpers(
"rich_parameter_values[" + index + "].value",
)}
disabled={isSubmitting}
index={index}
key={parameter.name}
onChange={async (value) => {
await form.setFieldValue("rich_parameter_values." + index, {
@@ -98,12 +94,8 @@ export const WorkspaceParametersForm: FC<{
})
}}
parameter={parameter}
initialValue={workspaceBuildParameterValue(
buildParameters,
parameter,
)}
/>
),
) : null,
)}
</FormFields>
</FormSection>
@@ -114,16 +106,15 @@ export const WorkspaceParametersForm: FC<{
description="These parameters only apply for a single workspace start."
>
<FormFields>
{mutableParameters.map((parameter, index) =>
{templateVersionRichParameters.map((parameter, index) =>
// Since we are adding the values to the form based on the index
// we can't filter them to not loose the right index position
parameter.ephemeral ? (
parameter.mutable && parameter.ephemeral ? (
<RichParameterInput
{...getFieldHelpers(
"rich_parameter_values[" + index + "].value",
)}
disabled={isSubmitting}
index={index}
key={parameter.name}
onChange={async (value) => {
await form.setFieldValue("rich_parameter_values." + index, {
@@ -132,7 +123,6 @@ export const WorkspaceParametersForm: FC<{
})
}}
parameter={parameter}
initialValue={form.values.rich_parameter_values[index]?.value}
/>
) : null,
)}
@@ -140,7 +130,7 @@ export const WorkspaceParametersForm: FC<{
</FormSection>
)}
{/* They are displayed here only for visibility purposes */}
{immutableParameters.length > 0 && (
{hasImmutableParameters && (
<FormSection
title="Immutable parameters"
description={
@@ -152,26 +142,21 @@ export const WorkspaceParametersForm: FC<{
}
>
<FormFields>
{immutableParameters.map((parameter, index) => (
<RichParameterInput
disabled
{...getFieldHelpers(
"rich_parameter_values[" + index + "].value",
)}
index={index}
key={parameter.name}
onChange={async () => {
throw new Error(
"Cannot change immutable parameter after creation",
)
}}
parameter={parameter}
initialValue={workspaceBuildParameterValue(
buildParameters,
parameter,
)}
/>
))}
{templateVersionRichParameters.map((parameter, index) =>
!parameter.mutable ? (
<RichParameterInput
disabled
{...getFieldHelpers(
"rich_parameter_values[" + index + "].value",
)}
key={parameter.name}
parameter={parameter}
onChange={() => {
throw new Error("Immutable parameters cannot be changed")
}}
/>
) : null,
)}
</FormFields>
</FormSection>
)}
+3 -3
View File
@@ -1,7 +1,7 @@
import { TemplateVersionParameter } from "api/typesGenerated"
import { selectInitialRichParametersValues } from "./richParameters"
import { getInitialRichParameterValues } from "./richParameters"
test("selectInitialRichParametersValues return default value when default build parameter is not valid", () => {
test("getInitialRichParameterValues return default value when default build parameter is not valid", () => {
const templateParameters: TemplateVersionParameter[] = [
{
name: "cpu",
@@ -44,7 +44,7 @@ test("selectInitialRichParametersValues return default value when default build
]
const cpuParameter = templateParameters[0]
const [cpuParameterInitialValue] = selectInitialRichParametersValues(
const [cpuParameterInitialValue] = getInitialRichParameterValues(
templateParameters,
[{ name: cpuParameter.name, value: "100" }],
)
+29 -84
View File
@@ -5,65 +5,38 @@ import {
import { useTranslation } from "react-i18next"
import * as Yup from "yup"
export const selectInitialRichParametersValues = (
templateParameters?: TemplateVersionParameter[],
defaultBuildParameters?: WorkspaceBuildParameter[],
export const getInitialRichParameterValues = (
templateParameters: TemplateVersionParameter[],
buildParameters?: WorkspaceBuildParameter[],
): WorkspaceBuildParameter[] => {
const defaults: WorkspaceBuildParameter[] = []
if (!templateParameters) {
return defaults
return templateParameters.map((parameter) => {
const existentBuildParameter = buildParameters?.find(
(p) => p.name === parameter.name,
)
const shouldReturnTheDefaultValue =
!existentBuildParameter ||
!isValidValue(parameter, existentBuildParameter) ||
parameter.ephemeral
if (shouldReturnTheDefaultValue) {
return {
name: parameter.name,
value: parameter.default_value,
}
}
return existentBuildParameter
})
}
const isValidValue = (
templateParam: TemplateVersionParameter,
buildParam: WorkspaceBuildParameter,
) => {
if (templateParam.options.length > 0) {
const validValues = templateParam.options.map((option) => option.value)
return validValues.includes(buildParam.value)
}
templateParameters.forEach((parameter) => {
let parameterValue = parameter.default_value
if (parameter.options.length > 0) {
parameterValue = parameterValue ?? parameter.options[0].value
const validValues = parameter.options.map((option) => option.value)
if (defaultBuildParameters) {
const defaultBuildParameter = defaultBuildParameters.find(
(p) => p.name === parameter.name,
)
// We don't want invalid values from default parameters to be set
if (
defaultBuildParameter &&
validValues.includes(defaultBuildParameter.value)
) {
parameterValue = defaultBuildParameter?.value
}
}
const buildParameter: WorkspaceBuildParameter = {
name: parameter.name,
value: parameterValue,
}
defaults.push(buildParameter)
return
}
if (parameter.ephemeral) {
parameterValue = parameter.default_value
}
if (defaultBuildParameters) {
const buildParameter = defaultBuildParameters.find(
(p) => p.name === parameter.name,
)
if (buildParameter) {
parameterValue = buildParameter?.value
}
}
const buildParameter: WorkspaceBuildParameter = {
name: parameter.name,
value: parameterValue || "",
}
defaults.push(buildParameter)
})
return defaults
return true
}
export const useValidationSchemaForRichParameters = (
@@ -193,31 +166,3 @@ export const useValidationSchemaForRichParameters = (
)
.required()
}
export const workspaceBuildParameterValue = (
workspaceBuildParameters: WorkspaceBuildParameter[],
parameter: TemplateVersionParameter,
): string => {
const buildParameter = workspaceBuildParameters.find((buildParameter) => {
return buildParameter.name === parameter.name
})
return (buildParameter && buildParameter.value) || ""
}
export const getInitialParameterValues = (
templateParameters: TemplateVersionParameter[],
buildParameters: WorkspaceBuildParameter[],
) => {
return templateParameters.map((parameter) => {
const buildParameter = buildParameters.find(
(p) => p.name === parameter.name,
)
if (!buildParameter || parameter.ephemeral) {
return {
name: parameter.name,
value: parameter.default_value,
}
}
return buildParameter
})
}