diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 43051961fa7e7..13db8b841d969 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1145,6 +1145,15 @@ class ApiMethods { return response.data; }; + getTemplateVersionPresets = async ( + templateVersionId: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/templateversions/${templateVersionId}/presets`, + ); + return response.data; + }; + startWorkspace = ( workspaceId: string, templateVersionId: string, diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index 8f6399cc4b354..2cd2d7693cfda 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -2,6 +2,7 @@ import { API, type GetTemplatesOptions, type GetTemplatesQuery } from "api/api"; import type { CreateTemplateRequest, CreateTemplateVersionRequest, + Preset, ProvisionerJob, ProvisionerJobStatus, Template, @@ -305,6 +306,13 @@ export const previousTemplateVersion = ( }; }; +export const templateVersionPresets = (versionId: string) => { + return { + queryKey: ["templateVersion", versionId, "presets"], + queryFn: () => API.getTemplateVersionPresets(versionId), + }; +}; + const waitBuildToBeFinished = async ( version: TemplateVersion, onRequest?: (data: TemplateVersion) => void, diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx index 56bd0da8a0516..b2481b4729915 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx @@ -5,6 +5,7 @@ import { richParameters, templateByName, templateVersionExternalAuth, + templateVersionPresets, } from "api/queries/templates"; import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces"; import type { @@ -56,6 +57,10 @@ const CreateWorkspacePage: FC = () => { const templateQuery = useQuery( templateByName(organizationName, templateName), ); + const templateVersionPresetsQuery = useQuery({ + ...templateVersionPresets(templateQuery.data?.active_version_id ?? ""), + enabled: templateQuery.data !== undefined, + }); const permissionsQuery = useQuery( templateQuery.data ? checkAuthorization({ @@ -203,6 +208,7 @@ const CreateWorkspacePage: FC = () => { hasAllRequiredExternalAuth={hasAllRequiredExternalAuth} permissions={permissionsQuery.data as CreateWSPermissions} parameters={realizedParameters as TemplateVersionParameter[]} + presets={templateVersionPresetsQuery.data ?? []} creatingWorkspace={createWorkspaceMutation.isLoading} onCancel={() => { navigate(-1); diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx index 46f1f87e8a50f..6f0647c9f28e8 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx @@ -1,5 +1,7 @@ import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; +import { within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { chromatic } from "testHelpers/chromatic"; import { MockTemplate, @@ -116,6 +118,47 @@ export const Parameters: Story = { }, }; +export const PresetsButNoneSelected: Story = { + args: { + presets: [ + { + ID: "preset-1", + Name: "Preset 1", + Parameters: [ + { + Name: MockTemplateVersionParameter1.name, + Value: "preset 1 override", + }, + ], + }, + { + ID: "preset-2", + Name: "Preset 2", + Parameters: [ + { + Name: MockTemplateVersionParameter2.name, + Value: "42", + }, + ], + }, + ], + parameters: [ + MockTemplateVersionParameter1, + MockTemplateVersionParameter2, + MockTemplateVersionParameter3, + ], + }, +}; + +export const PresetSelected: Story = { + args: PresetsButNoneSelected.args, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByLabelText("Preset")); + await userEvent.click(canvas.getByText("Preset 1")); + }, +}; + export const ExternalAuth: Story = { args: { externalAuth: [ diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index cc912e1f6facf..de72a79e456ef 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -6,6 +6,7 @@ import { Alert } from "components/Alert/Alert"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; import { Button } from "components/Button/Button"; +import { SelectFilter } from "components/Filter/SelectFilter"; import { FormFields, FormFooter, @@ -64,6 +65,7 @@ export interface CreateWorkspacePageViewProps { hasAllRequiredExternalAuth: boolean; parameters: TypesGen.TemplateVersionParameter[]; autofillParameters: AutofillBuildParameter[]; + presets: TypesGen.Preset[]; permissions: CreateWSPermissions; creatingWorkspace: boolean; onCancel: () => void; @@ -88,6 +90,7 @@ export const CreateWorkspacePageView: FC = ({ hasAllRequiredExternalAuth, parameters, autofillParameters, + presets = [], permissions, creatingWorkspace, onSubmit, @@ -145,6 +148,62 @@ export const CreateWorkspacePageView: FC = ({ [autofillParameters], ); + const [presetOptions, setPresetOptions] = useState([ + { label: "None", value: "" }, + ]); + useEffect(() => { + setPresetOptions([ + { label: "None", value: "" }, + ...presets.map((preset) => ({ + label: preset.Name, + value: preset.ID, + })), + ]); + }, [presets]); + + const [selectedPresetIndex, setSelectedPresetIndex] = useState(0); + const [presetParameterNames, setPresetParameterNames] = useState( + [], + ); + + useEffect(() => { + const selectedPresetOption = presetOptions[selectedPresetIndex]; + let selectedPreset: TypesGen.Preset | undefined; + for (const preset of presets) { + if (preset.ID === selectedPresetOption.value) { + selectedPreset = preset; + break; + } + } + + if (!selectedPreset || !selectedPreset.Parameters) { + setPresetParameterNames([]); + return; + } + + setPresetParameterNames(selectedPreset.Parameters.map((p) => p.Name)); + + for (const presetParameter of selectedPreset.Parameters) { + const parameterIndex = parameters.findIndex( + (p) => p.name === presetParameter.Name, + ); + if (parameterIndex === -1) continue; + + const parameterField = `rich_parameter_values.${parameterIndex}`; + + form.setFieldValue(parameterField, { + name: presetParameter.Name, + value: presetParameter.Value, + }); + } + }, [ + presetOptions, + selectedPresetIndex, + presets, + parameters, + form.setFieldValue, + ]); + return ( = ({ )} + {presets.length > 0 && ( + + + Select a preset to get started + + + { + setSelectedPresetIndex( + presetOptions.findIndex( + (preset) => preset.value === option?.value, + ), + ); + }} + placeholder="Select a preset" + selectedOption={presetOptions[selectedPresetIndex]} + /> + + + )}
= ({ const isDisabled = disabledParams?.includes( parameter.name.toLowerCase().replace(/ /g, "_"), - ) || creatingWorkspace; + ) || + creatingWorkspace || + presetParameterNames.includes(parameter.name); return (