From a17cf03980541e7a11974f9b74b95287511ec548 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Tue, 18 Feb 2025 15:26:55 +0200 Subject: [PATCH] feat(site): add support for presets to the create workspace page (#16567) This pull request adds support for presets to the create workspace page. This behaviour can be seen in the storybook. This will not be visible in dogfood until we merge support for presets in the provisioners. There is more frontend work to be done before this is ready for a general release, but this should be sufficient for dogfood testing --- site/src/api/api.ts | 9 ++ site/src/api/queries/templates.ts | 8 ++ .../CreateWorkspacePage.tsx | 6 ++ .../CreateWorkspacePageView.stories.tsx | 43 ++++++++++ .../CreateWorkspacePageView.tsx | 85 ++++++++++++++++++- 5 files changed, 150 insertions(+), 1 deletion(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 43051961fa..13db8b841d 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 8f6399cc4b..2cd2d7693c 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 56bd0da8a0..b2481b4729 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 46f1f87e8a..6f0647c9f2 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 cc912e1f6f..de72a79e45 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 (