mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
refactor: show parameter suggestions from user history below field (#12340)
This commit is contained in:
@@ -3,14 +3,16 @@ import Radio from "@mui/material/Radio";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import TextField, { TextFieldProps } from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Button from "@mui/material/Button";
|
||||
import FormHelperText from "@mui/material/FormHelperText";
|
||||
import { type Interpolation, type Theme } from "@emotion/react";
|
||||
import { type FC } from "react";
|
||||
import { type FC, type ReactNode, useState } from "react";
|
||||
import { TemplateVersionParameter } from "api/typesGenerated";
|
||||
import { MemoizedMarkdown } from "components/Markdown/Markdown";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { MultiTextField } from "./MultiTextField";
|
||||
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
||||
import { AutofillSource } from "utils/richParameters";
|
||||
import { AutofillBuildParameter, AutofillSource } from "utils/richParameters";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import ErrorOutline from "@mui/icons-material/ErrorOutline";
|
||||
|
||||
@@ -103,6 +105,15 @@ const styles = {
|
||||
width: 16,
|
||||
},
|
||||
},
|
||||
suggestion: (theme) => ({
|
||||
color: theme.roles.info.fill.solid,
|
||||
marginLeft: "-4px",
|
||||
padding: "4px 6px",
|
||||
lineHeight: "inherit",
|
||||
fontSize: "inherit",
|
||||
height: "unset",
|
||||
minWidth: "unset",
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export interface ParameterLabelProps {
|
||||
@@ -169,17 +180,26 @@ export type RichParameterInputProps = Omit<
|
||||
"size" | "onChange"
|
||||
> & {
|
||||
parameter: TemplateVersionParameter;
|
||||
autofillSource?: AutofillSource;
|
||||
parameterAutofill?: AutofillBuildParameter;
|
||||
onChange: (value: string) => void;
|
||||
size?: Size;
|
||||
};
|
||||
|
||||
const autofillDescription: Partial<Record<AutofillSource, ReactNode>> = {
|
||||
url: " from the URL.",
|
||||
};
|
||||
|
||||
export const RichParameterInput: FC<RichParameterInputProps> = ({
|
||||
parameter,
|
||||
size = "medium",
|
||||
autofillSource,
|
||||
parameter,
|
||||
parameterAutofill,
|
||||
onChange,
|
||||
...fieldProps
|
||||
}) => {
|
||||
const autofillSource = parameterAutofill?.source;
|
||||
const autofillValue = parameterAutofill?.value;
|
||||
const [hideSuggestion, setHideSuggestion] = useState(false);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="column"
|
||||
@@ -189,16 +209,33 @@ export const RichParameterInput: FC<RichParameterInputProps> = ({
|
||||
>
|
||||
<ParameterLabel parameter={parameter} />
|
||||
<div css={{ display: "flex", flexDirection: "column" }}>
|
||||
<RichParameterField {...fieldProps} size={size} parameter={parameter} />
|
||||
{autofillSource && autofillSource !== "active_build" && (
|
||||
<RichParameterField
|
||||
{...fieldProps}
|
||||
onChange={onChange}
|
||||
size={size}
|
||||
parameter={parameter}
|
||||
/>
|
||||
{!parameter.ephemeral &&
|
||||
autofillSource === "user_history" &&
|
||||
autofillValue &&
|
||||
!hideSuggestion && (
|
||||
<FormHelperText>
|
||||
<Button
|
||||
variant="text"
|
||||
css={styles.suggestion}
|
||||
onClick={() => {
|
||||
onChange(autofillValue);
|
||||
setHideSuggestion(true);
|
||||
}}
|
||||
>
|
||||
{autofillValue}
|
||||
</Button>{" "}
|
||||
was recently used for this parameter.
|
||||
</FormHelperText>
|
||||
)}
|
||||
{autofillSource && autofillDescription[autofillSource] && (
|
||||
<div css={{ marginTop: 4, fontSize: 12 }}>
|
||||
🪄 Autofilled:{" "}
|
||||
{
|
||||
{
|
||||
["url"]: "value supplied by URL.",
|
||||
["user_history"]: "recently used value.",
|
||||
}[autofillSource]
|
||||
}
|
||||
🪄 Autofilled {autofillDescription[autofillSource]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { TemplateVersionParameter } from "api/typesGenerated";
|
||||
import { FormFields, FormSection } from "components/Form/Form";
|
||||
import {
|
||||
RichParameterInput,
|
||||
RichParameterInputProps,
|
||||
} from "components/RichParameterInput/RichParameterInput";
|
||||
import { ComponentProps, FC } from "react";
|
||||
import { AutofillSource } from "utils/richParameters";
|
||||
|
||||
export type TemplateParametersSectionProps = {
|
||||
templateParameters: TemplateVersionParameter[];
|
||||
autofillSources?: Record<string, AutofillSource>;
|
||||
getInputProps: (
|
||||
parameter: TemplateVersionParameter,
|
||||
index: number,
|
||||
) => Omit<RichParameterInputProps, "parameter" | "index">;
|
||||
} & Pick<ComponentProps<typeof FormSection>, "classes">;
|
||||
|
||||
export const TemplateParametersSection: FC<TemplateParametersSectionProps> = ({
|
||||
templateParameters,
|
||||
getInputProps,
|
||||
autofillSources,
|
||||
...formSectionProps
|
||||
}) => {
|
||||
const hasMutableParameters =
|
||||
templateParameters.filter((p) => p.mutable).length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasMutableParameters && (
|
||||
<FormSection
|
||||
{...formSectionProps}
|
||||
title="Parameters"
|
||||
description="Settings used by your template"
|
||||
>
|
||||
<FormFields>
|
||||
{templateParameters.map(
|
||||
(parameter, index) =>
|
||||
parameter.mutable && (
|
||||
<RichParameterInput
|
||||
{...getInputProps(parameter, index)}
|
||||
key={parameter.name}
|
||||
parameter={parameter}
|
||||
autofillSource={
|
||||
autofillSources && autofillSources[parameter.name]
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -101,9 +101,14 @@ export const Parameters: Story = {
|
||||
autofillParameters: [
|
||||
{
|
||||
name: "first_parameter",
|
||||
value: "It works!",
|
||||
value: "Cool suggestion",
|
||||
source: "user_history",
|
||||
},
|
||||
{
|
||||
name: "third_parameter",
|
||||
value: "aaaa",
|
||||
source: "url",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -20,8 +20,7 @@ import {
|
||||
} from "components/Form/Form";
|
||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
||||
import {
|
||||
AutofillBuildParameter,
|
||||
AutofillSource,
|
||||
type AutofillBuildParameter,
|
||||
getInitialRichParameterValues,
|
||||
useValidationSchemaForRichParameters,
|
||||
} from "utils/richParameters";
|
||||
@@ -39,11 +38,11 @@ import {
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
|
||||
import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName";
|
||||
import {
|
||||
import type {
|
||||
CreateWorkspaceMode,
|
||||
ExternalAuthPollingState,
|
||||
} from "./CreateWorkspacePage";
|
||||
import { CreateWSPermissions } from "./permissions";
|
||||
import type { CreateWSPermissions } from "./permissions";
|
||||
|
||||
export const Language = {
|
||||
duplicationWarning:
|
||||
@@ -137,15 +136,13 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
error,
|
||||
);
|
||||
|
||||
const autofillSources = useMemo(() => {
|
||||
return autofillParameters.reduce(
|
||||
(acc, param) => {
|
||||
acc[param.name] = param.source;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, AutofillSource>,
|
||||
);
|
||||
}, [autofillParameters]);
|
||||
const autofillByName = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
autofillParameters.map((param) => [param.name, param]),
|
||||
),
|
||||
[autofillParameters],
|
||||
);
|
||||
|
||||
const hasAllRequiredExternalAuth = externalAuth.every(
|
||||
(auth) => auth.optional || auth.authenticated,
|
||||
@@ -301,9 +298,9 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
value,
|
||||
});
|
||||
}}
|
||||
autofillSource={autofillSources[parameter.name]}
|
||||
key={parameter.name}
|
||||
parameter={parameter}
|
||||
parameterAutofill={autofillByName[parameter.name]}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
@@ -330,6 +327,7 @@ const styles = {
|
||||
lineHeight: "inherit",
|
||||
fontSize: "inherit",
|
||||
height: "unset",
|
||||
minWidth: "unset",
|
||||
}),
|
||||
hasDescription: {
|
||||
paddingBottom: 16,
|
||||
|
||||
+10
-13
@@ -1,5 +1,8 @@
|
||||
import { getWorkspaceParameters, postWorkspaceBuild } from "api/api";
|
||||
import Button from "@mui/material/Button";
|
||||
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { getWorkspaceParameters, postWorkspaceBuild } from "api/api";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
import { pageTitle } from "utils/page";
|
||||
import {
|
||||
WorkspacePermissions,
|
||||
@@ -11,7 +14,7 @@ import { templateByName } from "api/queries/templates";
|
||||
import { useMutation, useQuery } from "react-query";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import {
|
||||
WorkspaceParametersFormValues,
|
||||
type WorkspaceParametersFormValues,
|
||||
WorkspaceParametersForm,
|
||||
} from "./WorkspaceParametersForm";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@@ -19,12 +22,8 @@ import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
|
||||
import { type FC } from "react";
|
||||
import { isApiValidationError } from "api/errors";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
import Button from "@mui/material/Button";
|
||||
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
|
||||
import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
|
||||
import { docs } from "utils/docs";
|
||||
import { AutofillBuildParameter } from "utils/richParameters";
|
||||
|
||||
const WorkspaceParametersPage: FC = () => {
|
||||
const workspace = useWorkspaceSettings();
|
||||
@@ -127,12 +126,10 @@ export const WorkspaceParametersPageView: FC<
|
||||
<WorkspaceParametersForm
|
||||
workspace={workspace}
|
||||
canChangeVersions={canChangeVersions}
|
||||
autofillParams={data.buildParameters.map(
|
||||
(p): AutofillBuildParameter => ({
|
||||
...p,
|
||||
source: "active_build",
|
||||
}),
|
||||
)}
|
||||
autofillParams={data.buildParameters.map((p) => ({
|
||||
...p,
|
||||
source: "active_build",
|
||||
}))}
|
||||
templateVersionRichParameters={data.templateVersionRichParameters}
|
||||
error={submitError}
|
||||
isSubmitting={isSubmitting}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TemplateVersionParameter } from "api/typesGenerated";
|
||||
import type { TemplateVersionParameter } from "api/typesGenerated";
|
||||
import { getInitialRichParameterValues } from "./richParameters";
|
||||
|
||||
test("getInitialRichParameterValues return default value when default build parameter is not valid", () => {
|
||||
@@ -46,7 +46,7 @@ test("getInitialRichParameterValues return default value when default build para
|
||||
const cpuParameter = templateParameters[0];
|
||||
const [cpuParameterInitialValue] = getInitialRichParameterValues(
|
||||
templateParameters,
|
||||
[{ name: cpuParameter.name, value: "100", source: "user_history" }],
|
||||
[{ name: cpuParameter.name, value: "100", source: "url" }],
|
||||
);
|
||||
|
||||
expect(cpuParameterInitialValue.value).toBe(cpuParameter.default_value);
|
||||
|
||||
@@ -33,7 +33,9 @@ export const getInitialRichParameterValues = (
|
||||
return {
|
||||
name: parameter.name,
|
||||
value:
|
||||
autofillParam && isValidValue(parameter, autofillParam)
|
||||
autofillParam &&
|
||||
isValidValue(parameter, autofillParam) &&
|
||||
autofillParam.source !== "user_history"
|
||||
? autofillParam.value
|
||||
: parameter.default_value,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user