mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor(site): improve parameters field (#11802)
This commit is contained in:
@@ -19,7 +19,7 @@ const createTemplateVersionParameter = (
|
||||
name: "first_parameter",
|
||||
description: "This is first parameter.",
|
||||
type: "string",
|
||||
mutable: false,
|
||||
mutable: true,
|
||||
default_value: "default string",
|
||||
icon: "/icon/folder.svg",
|
||||
options: [],
|
||||
@@ -47,6 +47,46 @@ export const Basic: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Optional: Story = {
|
||||
args: {
|
||||
value: "initial-value",
|
||||
id: "project_name",
|
||||
parameter: createTemplateVersionParameter({
|
||||
required: false,
|
||||
name: "project_name",
|
||||
description:
|
||||
"Customize the name of a Google Cloud project that will be created!",
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const Immutable: Story = {
|
||||
args: {
|
||||
value: "initial-value",
|
||||
id: "project_name",
|
||||
parameter: createTemplateVersionParameter({
|
||||
mutable: false,
|
||||
name: "project_name",
|
||||
description:
|
||||
"Customize the name of a Google Cloud project that will be created!",
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const WithError: Story = {
|
||||
args: {
|
||||
id: "number_parameter",
|
||||
parameter: createTemplateVersionParameter({
|
||||
name: "number_parameter",
|
||||
type: "number",
|
||||
description: "Numeric parameter",
|
||||
default_value: "",
|
||||
}),
|
||||
error: true,
|
||||
helperText: "Number must be greater than 5",
|
||||
},
|
||||
};
|
||||
|
||||
export const NumberType: Story = {
|
||||
args: {
|
||||
value: "4",
|
||||
|
||||
@@ -10,6 +10,8 @@ import { MemoizedMarkdown } from "components/Markdown/Markdown";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { MultiTextField } from "./MultiTextField";
|
||||
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import ErrorOutline from "@mui/icons-material/ErrorOutline";
|
||||
|
||||
const isBoolean = (parameter: TemplateVersionParameter) => {
|
||||
return parameter.type === "bool";
|
||||
@@ -31,7 +33,11 @@ const styles = {
|
||||
labelPrimary: (theme) => ({
|
||||
fontSize: 16,
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 600,
|
||||
fontWeight: 500,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: 8,
|
||||
|
||||
"& p": {
|
||||
margin: 0,
|
||||
@@ -42,6 +48,11 @@ const styles = {
|
||||
fontSize: 14,
|
||||
},
|
||||
}),
|
||||
optionalLabel: (theme) => ({
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.disabled,
|
||||
fontWeight: 500,
|
||||
}),
|
||||
textField: {
|
||||
".small & .MuiInputBase-root": {
|
||||
height: 36,
|
||||
@@ -102,6 +113,25 @@ const ParameterLabel: FC<ParameterLabelProps> = ({ parameter }) => {
|
||||
? parameter.display_name
|
||||
: parameter.name;
|
||||
|
||||
const labelPrimary = (
|
||||
<span css={styles.labelPrimary}>
|
||||
{displayName}
|
||||
|
||||
{!parameter.required && (
|
||||
<Tooltip title="If no value is specified, the system will default to the value set by the administrator.">
|
||||
<span css={styles.optionalLabel}>(optional)</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!parameter.mutable && (
|
||||
<Tooltip title="This value cannot be modified after the workspace has been created.">
|
||||
<Pill type="warning" icon={<ErrorOutline />}>
|
||||
Immutable
|
||||
</Pill>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<label htmlFor={parameter.name}>
|
||||
<Stack direction="row" alignItems="center">
|
||||
@@ -117,13 +147,13 @@ const ParameterLabel: FC<ParameterLabelProps> = ({ parameter }) => {
|
||||
|
||||
{hasDescription ? (
|
||||
<Stack spacing={0}>
|
||||
<span css={styles.labelPrimary}>{displayName}</span>
|
||||
{labelPrimary}
|
||||
<MemoizedMarkdown css={styles.labelCaption}>
|
||||
{parameter.description}
|
||||
</MemoizedMarkdown>
|
||||
</Stack>
|
||||
) : (
|
||||
<span css={styles.labelPrimary}>{displayName}</span>
|
||||
labelPrimary
|
||||
)}
|
||||
</Stack>
|
||||
</label>
|
||||
|
||||
@@ -14,9 +14,11 @@ export type TemplateParametersSectionProps = {
|
||||
) => Omit<RichParameterInputProps, "parameter" | "index">;
|
||||
} & Pick<ComponentProps<typeof FormSection>, "classes">;
|
||||
|
||||
export const MutableTemplateParametersSection: FC<
|
||||
TemplateParametersSectionProps
|
||||
> = ({ templateParameters, getInputProps, ...formSectionProps }) => {
|
||||
export const TemplateParametersSection: FC<TemplateParametersSectionProps> = ({
|
||||
templateParameters,
|
||||
getInputProps,
|
||||
...formSectionProps
|
||||
}) => {
|
||||
const hasMutableParameters =
|
||||
templateParameters.filter((p) => p.mutable).length > 0;
|
||||
|
||||
@@ -45,40 +47,3 @@ export const MutableTemplateParametersSection: FC<
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ImmutableTemplateParametersSection: FC<
|
||||
TemplateParametersSectionProps
|
||||
> = ({ templateParameters, getInputProps, ...formSectionProps }) => {
|
||||
const hasImmutableParameters =
|
||||
templateParameters.filter((p) => !p.mutable).length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasImmutableParameters && (
|
||||
<FormSection
|
||||
{...formSectionProps}
|
||||
title="Immutable parameters"
|
||||
description={
|
||||
<>
|
||||
These settings <strong>cannot be changed</strong> after creating
|
||||
the workspace.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FormFields>
|
||||
{templateParameters.map(
|
||||
(parameter, index) =>
|
||||
!parameter.mutable && (
|
||||
<RichParameterInput
|
||||
{...getInputProps(parameter, index)}
|
||||
key={parameter.name}
|
||||
parameter={parameter}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { useTheme, type Interpolation, type Theme } from "@emotion/react";
|
||||
import { type Interpolation, type Theme } from "@emotion/react";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
||||
@@ -21,10 +20,6 @@ import {
|
||||
getInitialRichParameterValues,
|
||||
useValidationSchemaForRichParameters,
|
||||
} from "utils/richParameters";
|
||||
import {
|
||||
ImmutableTemplateParametersSection,
|
||||
MutableTemplateParametersSection,
|
||||
} from "components/TemplateParameters/TemplateParameters";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import {
|
||||
@@ -44,6 +39,7 @@ import {
|
||||
PageHeaderSubtitle,
|
||||
} from "components/PageHeader/PageHeader";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
|
||||
|
||||
export const Language = {
|
||||
duplicationWarning:
|
||||
@@ -90,7 +86,6 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [owner, setOwner] = useState(defaultOwner);
|
||||
const [searchParams] = useSearchParams();
|
||||
const disabledParamsList = searchParams?.get("disable_params")?.split(",");
|
||||
@@ -222,65 +217,41 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
|
||||
{parameters && (
|
||||
<>
|
||||
<MutableTemplateParametersSection
|
||||
templateParameters={parameters}
|
||||
getInputProps={(parameter, index) => {
|
||||
return {
|
||||
...getFieldHelpers(
|
||||
"rich_parameter_values[" + index + "].value",
|
||||
),
|
||||
onChange: async (value) => {
|
||||
await form.setFieldValue(
|
||||
"rich_parameter_values." + index,
|
||||
{
|
||||
{parameters.length > 0 && (
|
||||
<FormSection
|
||||
title="Parameters"
|
||||
description="These are the settings used by your template. Please note that immutable parameters cannot be modified once the workspace is created."
|
||||
>
|
||||
{/*
|
||||
Opted not to use FormFields in order to increase spacing.
|
||||
This decision was made because rich parameter inputs are more visually dense than standard text fields.
|
||||
*/}
|
||||
<div css={{ display: "flex", flexDirection: "column", gap: 36 }}>
|
||||
{parameters.map((parameter, index) => {
|
||||
const parameterField = `rich_parameter_values.${index}`;
|
||||
const parameterInputName = `${parameterField}.value`;
|
||||
const isDisabled =
|
||||
disabledParamsList?.includes(
|
||||
parameter.name.toLowerCase().replace(/ /g, "_"),
|
||||
) || creatingWorkspace;
|
||||
|
||||
return (
|
||||
<RichParameterInput
|
||||
{...getFieldHelpers(parameterInputName)}
|
||||
onChange={async (value) => {
|
||||
await form.setFieldValue(parameterField, {
|
||||
name: parameter.name,
|
||||
value: value,
|
||||
},
|
||||
);
|
||||
},
|
||||
disabled:
|
||||
disabledParamsList?.includes(
|
||||
parameter.name.toLowerCase().replace(/ /g, "_"),
|
||||
) || creatingWorkspace,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
<ImmutableTemplateParametersSection
|
||||
templateParameters={parameters}
|
||||
classes={{
|
||||
root: css`
|
||||
border: 1px solid ${theme.palette.warning.light};
|
||||
border-radius: 8px;
|
||||
background-color: ${theme.palette.background.paper};
|
||||
padding: 80px;
|
||||
margin-left: -80px;
|
||||
margin-right: -80px;
|
||||
`,
|
||||
}}
|
||||
getInputProps={(parameter, index) => {
|
||||
return {
|
||||
...getFieldHelpers(
|
||||
"rich_parameter_values[" + index + "].value",
|
||||
),
|
||||
onChange: async (value) => {
|
||||
await form.setFieldValue(
|
||||
"rich_parameter_values." + index,
|
||||
{
|
||||
name: parameter.name,
|
||||
value: value,
|
||||
},
|
||||
);
|
||||
},
|
||||
disabled:
|
||||
disabledParamsList?.includes(
|
||||
parameter.name.toLowerCase().replace(/ /g, "_"),
|
||||
) || creatingWorkspace,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
value,
|
||||
});
|
||||
}}
|
||||
key={parameter.name}
|
||||
parameter={parameter}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
<FormFooter
|
||||
|
||||
@@ -10,17 +10,13 @@ import { Template, TemplateVersionParameter } from "api/typesGenerated";
|
||||
import { FormSection, VerticalForm } from "components/Form/Form";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
|
||||
import {
|
||||
ImmutableTemplateParametersSection,
|
||||
MutableTemplateParametersSection,
|
||||
TemplateParametersSectionProps,
|
||||
} from "components/TemplateParameters/TemplateParameters";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { getInitialRichParameterValues } from "utils/richParameters";
|
||||
import { paramsUsedToCreateWorkspace } from "utils/workspace";
|
||||
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
|
||||
|
||||
type ButtonValues = Record<string, string>;
|
||||
|
||||
@@ -64,22 +60,6 @@ export const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
|
||||
const buttonUrl = `${createWorkspaceUrl}?${createWorkspaceParams.toString()}`;
|
||||
const buttonMkdCode = `[](${buttonUrl})`;
|
||||
const clipboard = useClipboard(buttonMkdCode);
|
||||
const getInputProps: TemplateParametersSectionProps["getInputProps"] = (
|
||||
parameter,
|
||||
) => {
|
||||
if (!buttonValues) {
|
||||
throw new Error("buttonValues is undefined");
|
||||
}
|
||||
return {
|
||||
value: buttonValues[`param.${parameter.name}`] ?? "",
|
||||
onChange: (value) => {
|
||||
setButtonValues((buttonValues) => ({
|
||||
...buttonValues,
|
||||
[`param.${parameter.name}`]: value,
|
||||
}));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// template parameters is async so we need to initialize the values after it
|
||||
// is loaded
|
||||
@@ -135,16 +115,28 @@ export const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
|
||||
</FormSection>
|
||||
|
||||
{templateParameters.length > 0 && (
|
||||
<>
|
||||
<MutableTemplateParametersSection
|
||||
templateParameters={templateParameters}
|
||||
getInputProps={getInputProps}
|
||||
/>
|
||||
<ImmutableTemplateParametersSection
|
||||
templateParameters={templateParameters}
|
||||
getInputProps={getInputProps}
|
||||
/>
|
||||
</>
|
||||
<div
|
||||
css={{ display: "flex", flexDirection: "column", gap: 36 }}
|
||||
>
|
||||
{templateParameters.map((parameter) => {
|
||||
const parameterValue =
|
||||
buttonValues[`param.${parameter.name}`] ?? "";
|
||||
|
||||
return (
|
||||
<RichParameterInput
|
||||
value={parameterValue}
|
||||
onChange={async (value) => {
|
||||
setButtonValues((buttonValues) => ({
|
||||
...buttonValues,
|
||||
[`param.${parameter.name}`]: value,
|
||||
}));
|
||||
}}
|
||||
key={parameter.name}
|
||||
parameter={parameter}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</VerticalForm>
|
||||
</div>
|
||||
|
||||
+3
-35
@@ -69,9 +69,6 @@ export const WorkspaceParametersForm: FC<WorkspaceParameterFormProps> = ({
|
||||
const hasNonEphemeralParameters = templateVersionRichParameters.some(
|
||||
(parameter) => !parameter.ephemeral,
|
||||
);
|
||||
const hasImmutableParameters = templateVersionRichParameters.some(
|
||||
(parameter) => !parameter.mutable,
|
||||
);
|
||||
|
||||
const disabled =
|
||||
workspace.outdated &&
|
||||
@@ -97,12 +94,12 @@ export const WorkspaceParametersForm: FC<WorkspaceParameterFormProps> = ({
|
||||
{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.mutable && !parameter.ephemeral ? (
|
||||
!parameter.ephemeral ? (
|
||||
<RichParameterInput
|
||||
{...getFieldHelpers(
|
||||
"rich_parameter_values[" + index + "].value",
|
||||
)}
|
||||
disabled={isSubmitting || disabled}
|
||||
disabled={isSubmitting || disabled || !parameter.mutable}
|
||||
key={parameter.name}
|
||||
onChange={async (value) => {
|
||||
await form.setFieldValue(
|
||||
@@ -152,36 +149,7 @@ export const WorkspaceParametersForm: FC<WorkspaceParameterFormProps> = ({
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
)}
|
||||
{/* They are displayed here only for visibility purposes */}
|
||||
{hasImmutableParameters && (
|
||||
<FormSection
|
||||
title="Immutable parameters"
|
||||
description={
|
||||
<>
|
||||
These settings <strong>cannot be changed</strong> after creating
|
||||
the workspace.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FormFields>
|
||||
{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>
|
||||
)}
|
||||
|
||||
<FormFooter
|
||||
onCancel={onCancel}
|
||||
isLoading={isSubmitting}
|
||||
|
||||
Reference in New Issue
Block a user