From 00d134ebfd139f3dfadcec2ab24c0aee455bac5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McKayla=20=E3=81=AF=E3=81=AA?= Date: Mon, 27 Jul 2026 19:02:28 -0600 Subject: [PATCH] chore: remove classic parameter UI (#25014) --- .../extending-templates/dynamic-parameters.md | 27 +- .../dynamic-parameters-ga-settings.png | Bin 77804 -> 0 bytes site/e2e/helpers.ts | 4 +- site/src/api/api.test.ts | 109 ++--- site/src/api/api.ts | 194 +------- site/src/api/queries/workspaces.ts | 16 +- .../RichParameterInput.stories.tsx | 418 ---------------- .../RichParameterInput/RichParameterInput.tsx | 462 ------------------ ...icParameterFlowDeprecationWarning.test.tsx | 39 -- ...ClassicParameterFlowDeprecationWarning.tsx | 38 -- .../ErrorDialog/WorkspaceErrorDialog.tsx | 4 +- .../UpdateBuildParametersDialog.tsx | 124 ----- ...pdateBuildParametersDialogExperimental.tsx | 71 --- .../WorkspaceMoreActions.tsx | 49 +- .../WorkspaceOutdatedTooltip.tsx | 2 +- .../workspaces/WorkspaceUpdateDialogs.tsx | 144 +++--- .../TemplateSettingsForm.tsx | 45 -- .../BuildParametersPopover.tsx | 221 ++------- .../WorkspaceActions/Buttons.tsx | 2 - .../WorkspaceActions/DebugButton.tsx | 1 - .../WorkspaceActions/RetryButton.stories.tsx | 30 -- .../WorkspaceActions/RetryButton.tsx | 1 - .../WorkspacePage/WorkspacePage.test.tsx | 223 +-------- .../WorkspacePage/WorkspaceReadyPage.tsx | 7 +- .../WorkspacesPage/WorkspacesPage.stories.tsx | 22 +- .../pages/WorkspacesPage/WorkspacesPage.tsx | 1 - .../pages/WorkspacesPage/WorkspacesTable.tsx | 8 +- site/src/pages/WorkspacesPage/batchActions.ts | 5 +- site/src/testHelpers/entities.ts | 6 - site/src/testHelpers/handlers.ts | 10 + site/src/utils/richParameters.test.ts | 54 -- site/src/utils/richParameters.ts | 199 +------- 32 files changed, 228 insertions(+), 2308 deletions(-) delete mode 100644 docs/images/admin/templates/extend-templates/dyn-params/dynamic-parameters-ga-settings.png delete mode 100644 site/src/components/RichParameterInput/RichParameterInput.stories.tsx delete mode 100644 site/src/components/RichParameterInput/RichParameterInput.tsx delete mode 100644 site/src/modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning.test.tsx delete mode 100644 site/src/modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning.tsx delete mode 100644 site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialog.tsx delete mode 100644 site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialogExperimental.tsx delete mode 100644 site/src/utils/richParameters.test.ts diff --git a/docs/admin/templates/extending-templates/dynamic-parameters.md b/docs/admin/templates/extending-templates/dynamic-parameters.md index be28d17dd5..6d0e47ed99 100644 --- a/docs/admin/templates/extending-templates/dynamic-parameters.md +++ b/docs/admin/templates/extending-templates/dynamic-parameters.md @@ -36,13 +36,12 @@ Dynamic Parameters help you reduce template duplication by setting the condition They reduce the potential complexity of user-facing configuration by allowing administrators to organize a long list of options into interactive, branching paths for workspace customization. They allow you to set resource guardrails by referencing Coder identity in the `coder_workspace_owner` data source. -## How to enable Dynamic Parameters +## How to use Dynamic Parameters -In Coder v2.25.0 and later, Dynamic Parameters are automatically enabled for new templates. For Coder v2.24 and below, you can opt-in to Dynamic Parameters for individual existing templates via template settings. +Dynamic Parameters is the standard workspace creation experience. +In Coder v2.25.0 and later it is enabled automatically, and the classic parameter flow is deprecated. -1. Go to your template's settings and enable the **Enable dynamic parameters for workspace creation** option. - - ![Enable dynamic parameters for workspace creation](../../../images/admin/templates/extend-templates/dyn-params/dynamic-parameters-ga-settings.png) +To use the features described on this page in an existing template: 1. Update your template to use version >=2.4.0 of the Coder provider with the following Terraform block. @@ -57,8 +56,7 @@ In Coder v2.25.0 and later, Dynamic Parameters are automatically enabled for new } ``` -1. This enables Dynamic Parameters in the template. - Add some [conditional parameters](#available-form-input-types). +1. Add some [conditional parameters](#available-form-input-types). Note that these new features must be declared in your Terraform to start leveraging Dynamic Parameters. @@ -67,7 +65,6 @@ In Coder v2.25.0 and later, Dynamic Parameters are automatically enabled for new 1. Users should see the updated workspace creation form. Dynamic Parameters features are backwards compatible, so all existing templates may be upgraded in-place. -If you decide to revert to the legacy flow later, disable Dynamic Parameters in the template's settings. ## Features and Capabilities @@ -806,15 +803,13 @@ This will resolve the necessary template metadata to render the form. ### Reverting to classic parameters -To revert Dynamic Parameters on a template: +The classic parameter flow is deprecated and can no longer be enabled from the UI. +A template can still opt out of Dynamic Parameters by setting the `use_classic_parameter_flow` +field through the [templates API](../../../reference/api/templates.md#update-template-settings-by-id), +but this opt-out will be removed in a future release. -1. Prepare your template by removing any conditional logic or user data references in parameters. -1. As a template administrator or owner, go to your template's settings: - - **Templates** > **Your template** > **Settings** - -1. Uncheck the **Enable dynamic parameters for workspace creation** option. -1. Create a new template version and publish to the active version. +If your template's parameters do not work with Dynamic Parameters, please +[file an issue](https://github.com/coder/coder/issues/new?labels=parameters) with the `parameters` label. ### Template variables not showing up diff --git a/docs/images/admin/templates/extend-templates/dyn-params/dynamic-parameters-ga-settings.png b/docs/images/admin/templates/extend-templates/dyn-params/dynamic-parameters-ga-settings.png deleted file mode 100644 index 14e84ccdef6dc4c0df1e43c891aa873869403f59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 77804 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 { }); }); - it("fails when having missing parameters", async () => { - vi.spyOn(API, "postWorkspaceBuild").mockResolvedValue( - MockWorkspaceBuild, - ); - vi.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate); - vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValue([]); - vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValue([ - MockTemplateVersionParameter1, - { ...MockTemplateVersionParameter2, mutable: false }, - ]); + it("fails when the server returns parameter validation errors", async () => { + const validationErrors = [ + { field: "first_parameter", detail: "First parameter is required" }, + { + field: "second_parameter", + detail: "Second parameter is not valid", + }, + ]; - let error = new Error(); + vi.spyOn(API, "postWorkspaceBuild") + // First postWorkspaceBuild call is for the stop, which should succeed + .mockResolvedValueOnce({ + ...MockWorkspaceBuild, + transition: "stop", + }) + // Second postWorkspaceBuild call is for the start, which should fail + // with validation errors. + .mockRejectedValueOnce({ + isAxiosError: true, + response: { + status: 400, + data: { + message: "Invalid build parameters", + validations: validationErrors, + }, + }, + }); + vi.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate); + + let error: unknown; try { await API.updateWorkspace(MockWorkspace); } catch (e) { - error = e as Error; + error = e; } - expect(error).toBeInstanceOf(MissingBuildParameters); - // Verify if the correct missing parameters are being passed - expect((error as MissingBuildParameters).parameters).toEqual([ - MockTemplateVersionParameter1, - { ...MockTemplateVersionParameter2, mutable: false }, - ]); + expect(error).toBeInstanceOf(ParameterValidationError); + if (!(error instanceof ParameterValidationError)) { + throw new Error("expected a ParameterValidationError"); + } + expect(error.versionId).toBe(MockTemplate.active_version_id); + expect(error.validations).toEqual(validationErrors); }); - it("creates a build with no parameters if it is already filled", async () => { + it("succeeds when the server accepts the build parameters", async () => { vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({ ...MockWorkspaceBuild, transition: "stop", }); vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({ ...MockWorkspaceBuild, - template_version_id: MockTemplateVersion2.id, + template_version_id: MockTemplate.active_version_id, transition: "start", }); vi.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate); - vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValue([ - MockWorkspaceBuildParameter1, - ]); - vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValue([ - { - ...MockTemplateVersionParameter1, - required: true, - mutable: false, - }, - ]); await API.updateWorkspace(MockWorkspace); expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, { transition: "stop", @@ -376,10 +381,6 @@ describe("api.ts", () => { ...MockProvisionerJob, status: "succeeded", }); - vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([]); - vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce( - [], - ); vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({ ...MockWorkspaceBuild, template_version_id: MockTemplateVersion2.id, @@ -398,10 +399,6 @@ describe("api.ts", () => { it("does not stop workspace if already stopped", async () => { vi.spyOn(API, "stopWorkspace"); - vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([]); - vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce( - [], - ); vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({ ...MockWorkspaceBuild, template_version_id: MockTemplateVersion2.id, @@ -425,10 +422,6 @@ describe("api.ts", () => { ...MockProvisionerJob, status: "canceled", }); - vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([]); - vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce( - [], - ); vi.spyOn(API, "postWorkspaceBuild"); await expect( @@ -436,30 +429,6 @@ describe("api.ts", () => { ).rejects.toThrow("Workspace stop was canceled"); expect(API.postWorkspaceBuild).not.toHaveBeenCalled(); }); - - it("throws MissingBuildParameters for missing params", async () => { - vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([]); - vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([ - MockTemplateVersionParameter1, - { ...MockTemplateVersionParameter2, mutable: false }, - ]); - - let error = new Error(); - try { - await API.changeWorkspaceVersion( - MockStoppedWorkspace, - MockTemplateVersion2.id, - ); - } catch (e) { - error = e as Error; - } - - expect(error).toBeInstanceOf(MissingBuildParameters); - expect((error as MissingBuildParameters).parameters).toEqual([ - MockTemplateVersionParameter1, - { ...MockTemplateVersionParameter2, mutable: false }, - ]); - }); }); describe("chat configuration endpoints", () => { diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ccc082368e..284edf485c 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -38,82 +38,6 @@ import type { } from "./typesGenerated"; import * as TypesGen from "./typesGenerated"; -const getMissingParameters = ( - oldBuildParameters: TypesGen.WorkspaceBuildParameter[], - newBuildParameters: TypesGen.WorkspaceBuildParameter[], - templateParameters: TypesGen.TemplateVersionParameter[], -) => { - const missingParameters: TypesGen.TemplateVersionParameter[] = []; - const requiredParameters: TypesGen.TemplateVersionParameter[] = []; - - for (const p of templateParameters) { - // It is mutable and required. Mutable values can be changed after so we - // don't need to ask them if they are not required. - const isMutableAndRequired = p.mutable && p.required; - // Is immutable, so we can check if it is its first time on the build - const isImmutable = !p.mutable; - - if (isMutableAndRequired || isImmutable) { - requiredParameters.push(p); - } - } - - for (const parameter of requiredParameters) { - // Check if there is a new value - let buildParameter = newBuildParameters.find( - (p) => p.name === parameter.name, - ); - - // If not, get the old one - if (!buildParameter) { - buildParameter = oldBuildParameters.find( - (p) => p.name === parameter.name, - ); - } - - // If there is a value from the new or old one, it is not missed - if (buildParameter) { - continue; - } - - missingParameters.push(parameter); - } - - // Check if parameter "options" changed and we can't use old build parameters. - for (const templateParameter of templateParameters) { - if (templateParameter.options.length === 0) { - continue; - } - // For multi-select, extra steps are necessary to JSON parse the value. - if (templateParameter.form_type === "multi-select") { - continue; - } - let buildParameter = newBuildParameters.find( - (p) => p.name === templateParameter.name, - ); - - // If not, get the old one - if (!buildParameter) { - buildParameter = oldBuildParameters.find( - (p) => p.name === templateParameter.name, - ); - } - - if (!buildParameter) { - continue; - } - - const matchingOption = templateParameter.options.find( - (option) => option.value === buildParameter?.value, - ); - if (!matchingOption) { - missingParameters.push(templateParameter); - } - } - - return missingParameters; -}; - /** * Originally from codersdk/client.go. * The below declaration is required to stop Knip from complaining. @@ -479,20 +403,6 @@ export type InsightsTemplateParams = InsightsParams & { interval: "day" | "week"; }; -export class MissingBuildParameters extends Error { - parameters: TypesGen.TemplateVersionParameter[] = []; - versionId: string; - - constructor( - parameters: TypesGen.TemplateVersionParameter[], - versionId: string, - ) { - super("Missing build parameters."); - this.parameters = parameters; - this.versionId = versionId; - } -} - export class ParameterValidationError extends Error { constructor( public readonly versionId: string, @@ -2718,116 +2628,30 @@ class ApiMethods { } }; - /** Steps to change the workspace version - * - Get the latest template to access the latest active version - * - Get the current build parameters - * - Get the template parameters - * - Update the build parameters and check if there are missed parameters for - * the new version - * - If there are missing parameters raise an error - * - Stop the workspace if it is already running - * - Create a build with the version and updated build parameters - */ changeWorkspaceVersion = async ( workspace: TypesGen.Workspace, templateVersionId: string, newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [], - isDynamicParametersEnabled = false, ): Promise => { - const currentBuildParameters = await this.getWorkspaceBuildParameters( - workspace.latest_build.id, - ); - - let templateParameters: TypesGen.TemplateVersionParameter[] = []; - if (isDynamicParametersEnabled) { - templateParameters = await this.getDynamicParameters( - templateVersionId, - workspace.owner_id, - currentBuildParameters, - ); - } else { - templateParameters = - await this.getTemplateVersionRichParameters(templateVersionId); - } - - const missingParameters = getMissingParameters( - currentBuildParameters, - newBuildParameters, - templateParameters, - ); - - if (missingParameters.length > 0) { - throw new MissingBuildParameters(missingParameters, templateVersionId); - } - - await this.stopWorkspaceIfRunning(workspace); - - return this.postWorkspaceBuild(workspace.id, { - transition: "start", - template_version_id: templateVersionId, - rich_parameter_values: newBuildParameters, - }); - }; - - /** Steps to update the workspace - * - Get the latest template to access the latest active version - * - Get the current build parameters - * - Get the template parameters - * - Update the build parameters and check if there are missed parameters for - * the newest version - * - If there are missing parameters raise an error - * - Stop the workspace if it is already running - * - Create a build with the latest version and updated build parameters - */ - updateWorkspace = async ( - workspace: TypesGen.Workspace, - newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [], - isDynamicParametersEnabled = false, - ): Promise => { - const [template, oldBuildParameters] = await Promise.all([ - this.getTemplate(workspace.template_id), - this.getWorkspaceBuildParameters(workspace.latest_build.id), - ]); - - const activeVersionId = template.active_version_id; - - if (!isDynamicParametersEnabled) { - // Dynamic templates rely on the backend to fully validate parameters. - // Legacy templates do not, so do an additional check for any missing params. - const templateParameters = - await this.getTemplateVersionRichParameters(activeVersionId); - - const missingParameters = getMissingParameters( - oldBuildParameters, - newBuildParameters, - templateParameters, - ); - - if (missingParameters.length > 0) { - throw new MissingBuildParameters(missingParameters, activeVersionId); - } - } - await this.stopWorkspaceIfRunning(workspace); try { return await this.postWorkspaceBuild(workspace.id, { transition: "start", - template_version_id: activeVersionId, + template_version_id: templateVersionId, rich_parameter_values: newBuildParameters, }); } catch (error) { // If the build failed because of a parameter validation error, then we // throw a special sentinel error that can be caught by the caller. if ( - isDynamicParametersEnabled && isApiError(error) && error.response.status === 400 && error.response.data.validations && error.response.data.validations.length > 0 ) { throw new ParameterValidationError( - activeVersionId, + templateVersionId, error.response.data.validations, ); } @@ -2835,6 +2659,20 @@ class ApiMethods { } }; + updateWorkspace = async ( + workspace: TypesGen.Workspace, + newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [], + ): Promise => { + const template = await this.getTemplate(workspace.template_id); + const activeVersionId = template.active_version_id; + + return this.changeWorkspaceVersion( + workspace, + activeVersionId, + newBuildParameters, + ); + }; + getWorkspaceResolveAutostart = async ( workspaceId: string, ): Promise => { diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index d5d0fd5476..0b89fd5f1f 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -288,7 +288,6 @@ export const updateDeadline = ( export const changeVersion = ( workspace: Workspace, queryClient: QueryClient, - isDynamicParametersEnabled: boolean, ) => { return { mutationFn: ({ @@ -298,12 +297,7 @@ export const changeVersion = ( versionId: string; buildParameters?: WorkspaceBuildParameter[]; }) => { - return API.changeWorkspaceVersion( - workspace, - versionId, - buildParameters, - isDynamicParametersEnabled, - ); + return API.changeWorkspaceVersion(workspace, versionId, buildParameters); }, onSuccess: async (build: WorkspaceBuild) => { await updateWorkspaceBuild(build, queryClient); @@ -318,16 +312,10 @@ export const updateWorkspace = ( return { mutationFn: ({ buildParameters, - isDynamicParametersEnabled, }: { buildParameters?: WorkspaceBuildParameter[]; - isDynamicParametersEnabled: boolean; }) => { - return API.updateWorkspace( - workspace, - buildParameters, - isDynamicParametersEnabled, - ); + return API.updateWorkspace(workspace, buildParameters); }, onSuccess: async (build: WorkspaceBuild) => { await updateWorkspaceBuild(build, queryClient); diff --git a/site/src/components/RichParameterInput/RichParameterInput.stories.tsx b/site/src/components/RichParameterInput/RichParameterInput.stories.tsx deleted file mode 100644 index b4e27994f7..0000000000 --- a/site/src/components/RichParameterInput/RichParameterInput.stories.tsx +++ /dev/null @@ -1,418 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import type { TemplateVersionParameter } from "#/api/typesGenerated"; -import { chromatic } from "#/testHelpers/chromatic"; -import { RichParameterInput } from "./RichParameterInput"; - -const meta: Meta = { - title: "components/RichParameterInput", - parameters: { chromatic }, - component: RichParameterInput, -}; - -export default meta; -type Story = StoryObj; - -const createTemplateVersionParameter = ( - partial: Partial, -): TemplateVersionParameter => { - return { - name: "first_parameter", - description: "This is first parameter.", - type: "string", - form_type: "input", - mutable: true, - default_value: "default string", - icon: "/icon/folder.svg", - options: [], - validation_error: "", - validation_regex: "", - validation_min: 0, - validation_max: 0, - validation_monotonic: "increasing", - description_plaintext: "", - required: true, - ephemeral: false, - ...partial, - }; -}; - -export const Basic: Story = { - args: { - value: "initial-value", - id: "project_name", - parameter: createTemplateVersionParameter({ - name: "project_name", - description: - "Customize the name of a Google Cloud project that will be created!", - }), - }, -}; - -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", - id: "number_parameter", - parameter: createTemplateVersionParameter({ - name: "number_parameter", - type: "number", - description: "Numeric parameter", - }), - }, -}; - -export const NumberTypeWithIncreasingMonotonicity: Story = { - args: { - value: 4, - id: "number_parameter", - parameter: createTemplateVersionParameter({ - name: "number_parameter", - type: "number", - description: "Numeric parameter", - default_value: "", - validation_min: 0, - validation_max: 10, - validation_monotonic: "increasing", - }), - }, -}; - -export const NumberTypeWithDecreasingMonotonicity: Story = { - args: { - value: 4, - id: "number_parameter", - parameter: createTemplateVersionParameter({ - name: "number_parameter", - type: "number", - description: "Numeric parameter", - default_value: "", - validation_min: 0, - validation_max: 10, - validation_monotonic: "decreasing", - }), - }, -}; - -export const BooleanType: Story = { - args: { - value: "false", - id: "bool_parameter", - parameter: createTemplateVersionParameter({ - name: "bool_parameter", - type: "bool", - description: "Boolean parameter", - }), - }, -}; - -export const Options: Story = { - args: { - value: "first_option", - id: "options_parameter", - parameter: createTemplateVersionParameter({ - name: "options_parameter", - type: "string", - description: "Parameter with options", - options: [ - { - name: "First option", - value: "first_option", - description: "", - icon: "/icon/fedora.svg", - }, - { - name: "Second option", - value: "second_option", - description: "", - icon: "/icon/database.svg", - }, - { - name: "Third option", - value: "third_option", - description: "", - icon: "/icon/aws.svg", - }, - ], - }), - }, -}; - -export const OptionsWithDescriptions: Story = { - args: { - value: "first_option", - id: "options_parameter", - parameter: createTemplateVersionParameter({ - name: "options_parameter", - type: "string", - description: "Parameter with options", - options: [ - { - name: "First option", - value: "first_option", - description: "This is a short description.", - icon: "/icon/fedora.svg", - }, - { - name: "Second option", - value: "second_option", - description: - "This description is a little bit longer, but still not very long.", - icon: "/icon/database.svg", - }, - { - name: "Third option", - value: "third_option", - description: ` -In this description, we will explore the various ways in which this description -is a big long boy. We'll discuss such things as, lots of words wow it's long, and -boy howdy that's a number of sentences that this description contains. By the conclusion -of this essay, I hope to reveal to you, the reader, that this description is just an -absolute chonker. Just way longer than it actually needs to be. Absolutely massive. -Very big. - -> Wow, that description is straight up large. –Some guy, probably -`, - icon: "/icon/aws.svg", - }, - ], - }), - }, -}; - -export const ListStringType: Story = { - args: { - value: JSON.stringify(["first", "second", "third"]), - id: "list_string_parameter", - parameter: createTemplateVersionParameter({ - name: "list_string_parameter", - type: "list(string)", - description: "List string parameter", - }), - }, -}; - -export const IconLabel: Story = { - args: { - value: "initial-value", - id: "project_name", - parameter: createTemplateVersionParameter({ - name: "project_name", - description: - "Customize the name of a Google Cloud project that will be created!", - icon: "/emojis/1f30e.png", - }), - }, -}; - -export const NoDescription: Story = { - args: { - value: "", - id: "region", - parameter: createTemplateVersionParameter({ - name: "Region", - description: "", - description_plaintext: "", - type: "string", - mutable: false, - default_value: "", - icon: "/emojis/1f30e.png", - options: [ - { - name: "Pittsburgh", - description: "", - value: "us-pittsburgh", - icon: "/emojis/1f1fa-1f1f8.png", - }, - { - name: "Helsinki", - description: "", - value: "eu-helsinki", - icon: "/emojis/1f1eb-1f1ee.png", - }, - { - name: "Sydney", - description: "", - value: "ap-sydney", - icon: "/emojis/1f1e6-1f1fa.png", - }, - ], - }), - }, -}; - -export const DescriptionWithLinks: Story = { - args: { - value: "", - id: "coder-repository-directory", - parameter: createTemplateVersionParameter({ - name: "Coder Repository Directory", - description: - "The directory specified will be created and [coder/coder](https://github.com/coder/coder) will be automatically cloned into it 🪄.", - description_plaintext: - "The directory specified will be created and coder/coder (https://github.com/coder/coder) will be automatically cloned into it 🪄.", - type: "string", - mutable: true, - default_value: "~/coder", - icon: "", - options: [], - }), - }, -}; - -export const BasicWithDisplayName: Story = { - args: { - value: "initial-value", - id: "project_name", - parameter: createTemplateVersionParameter({ - name: "project_name", - display_name: "Project Name", - description: - "Customize the name of a Google Cloud project that will be created!", - }), - }, -}; - -// Smaller version of the components. Used in popovers. - -export const SmallBasic: Story = { - args: { - ...Basic.args, - size: "small", - }, -}; - -export const SmallNumberType: Story = { - args: { - ...NumberType.args, - size: "small", - }, -}; - -export const SmallBooleanType: Story = { - args: { - ...BooleanType.args, - size: "small", - }, -}; - -export const SmallOptions: Story = { - args: { - ...Options.args, - size: "small", - }, -}; - -export const SmallOptionsWithDescriptions: Story = { - args: { - ...OptionsWithDescriptions.args, - size: "small", - }, -}; - -export const SmallListStringType: Story = { - args: { - ...ListStringType.args, - size: "small", - }, -}; - -export const SmallIconLabel: Story = { - args: { - ...IconLabel.args, - size: "small", - }, -}; - -export const SmallNoDescription: Story = { - args: { - ...NoDescription.args, - size: "small", - }, -}; - -export const SmallBasicWithDisplayName: Story = { - args: { - ...BasicWithDisplayName.args, - size: "small", - }, -}; - -export const WithPreset: Story = { - args: { - value: "preset-value", - id: "project_name", - parameter: createTemplateVersionParameter({ - name: "project_name", - description: - "Customize the name of a Google Cloud project that will be created!", - }), - isPreset: true, - }, -}; - -export const WithPresetAndImmutable: Story = { - args: { - value: "preset-value", - id: "project_name", - parameter: createTemplateVersionParameter({ - name: "project_name", - description: - "Customize the name of a Google Cloud project that will be created!", - mutable: false, - }), - isPreset: true, - }, -}; - -export const WithPresetAndOptional: Story = { - args: { - value: "preset-value", - id: "project_name", - parameter: createTemplateVersionParameter({ - name: "project_name", - description: - "Customize the name of a Google Cloud project that will be created!", - required: false, - }), - isPreset: true, - }, -}; diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx deleted file mode 100644 index e0d0d4bbe8..0000000000 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ /dev/null @@ -1,462 +0,0 @@ -import type { Interpolation, Theme } from "@emotion/react"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import FormHelperText from "@mui/material/FormHelperText"; -import type { InputBaseComponentProps } from "@mui/material/InputBase"; -import Radio from "@mui/material/Radio"; -import RadioGroup from "@mui/material/RadioGroup"; -import TextField, { type TextFieldProps } from "@mui/material/TextField"; -import { CircleAlertIcon, SettingsIcon } from "lucide-react"; -import { type FC, type ReactNode, useState } from "react"; -import type { TemplateVersionParameter } from "#/api/typesGenerated"; -import { Button } from "#/components/Button/Button"; -import { ExternalImage } from "#/components/ExternalImage/ExternalImage"; -import { MemoizedMarkdown } from "#/components/Markdown/Markdown"; -import { Pill } from "#/components/Pill/Pill"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "#/components/Tooltip/Tooltip"; -import { cn } from "#/utils/cn"; -import type { - AutofillBuildParameter, - AutofillSource, -} from "#/utils/richParameters"; -import { TagInput } from "../TagInput/TagInput"; - -const isBoolean = (parameter: TemplateVersionParameter) => { - return parameter.type === "bool"; -}; - -const styles = { - label: { - marginBottom: 4, - }, - labelCaption: (theme) => ({ - fontSize: 14, - color: theme.palette.text.secondary, - - ".small &": { - fontSize: 13, - lineHeight: "140%", - }, - }), - labelPrimary: (theme) => ({ - fontSize: 16, - color: theme.palette.text.primary, - fontWeight: 500, - display: "flex", - alignItems: "center", - flexWrap: "wrap", - gap: 8, - - "& p": { - margin: 0, - lineHeight: "24px", // Keep the same as ParameterInput - }, - - ".small &": { - fontSize: 14, - }, - }), - optionalLabel: (theme) => ({ - fontSize: 14, - color: theme.palette.text.disabled, - fontWeight: 500, - }), - textField: { - ".small & .MuiInputBase-root": { - height: 36, - fontSize: 14, - borderRadius: 6, - }, - }, - radioGroup: { - ".small & .MuiFormControlLabel-label": { - fontSize: 14, - }, - ".small & .MuiRadio-root": { - padding: "6px 9px", // 8px + 1px border - }, - ".small & .MuiRadio-root svg": { - width: 16, - height: 16, - }, - }, - checkbox: { - display: "flex", - alignItems: "center", - gap: 8, - }, - labelIconWrapper: { - width: 20, - height: 20, - display: "block", - flexShrink: 0, - - ".small &": { - display: "none", - }, - }, - labelIcon: { - width: "100%", - height: "100%", - objectFit: "contain", - }, - optionIcon: { - pointerEvents: "none", - maxHeight: 20, - width: 20, - - ".small &": { - maxHeight: 16, - width: 16, - }, - }, - suggestion: (theme) => ({ - color: theme.roles.notice.fill.solid, - marginLeft: "-4px", - padding: "4px 6px", - lineHeight: "inherit", - fontSize: "inherit", - height: "unset", - minWidth: "unset", - }), -} satisfies Record>; - -interface ParameterLabelProps { - parameter: TemplateVersionParameter; - isPreset?: boolean; -} - -const ParameterLabel: FC = ({ parameter, isPreset }) => { - const hasDescription = parameter.description && parameter.description !== ""; - const displayName = parameter.display_name - ? parameter.display_name - : parameter.name; - - const labelPrimary = ( - - {displayName} - - {!parameter.required && ( - - - (optional) - - - If no value is specified, the system will default to the value set - by the administrator. - - - )} - {!parameter.mutable && ( - - - } - > - Immutable - - - - This value cannot be modified after the workspace has been created. - - - )} - {isPreset && ( - - - }> - Preset - - - - This value was set by a preset - - - )} - - ); - - return ( - - ); -}; - -type Size = "medium" | "small"; - -type RichParameterInputProps = Omit & { - parameter: TemplateVersionParameter; - parameterAutofill?: AutofillBuildParameter; - onChange: (value: string) => void; - size?: Size; - isPreset?: boolean; -}; - -const autofillDescription: Partial> = { - url: " from the URL.", -}; - -export const RichParameterInput: FC = ({ - size = "medium", - parameter, - parameterAutofill, - onChange, - isPreset, - ...fieldProps -}) => { - const autofillSource = parameterAutofill?.source; - const autofillValue = parameterAutofill?.value; - const [hideSuggestion, setHideSuggestion] = useState(false); - - return ( -
- -
- - {!parameter.ephemeral && - autofillSource === "user_history" && - autofillValue && - !hideSuggestion && ( - - {" "} - was recently used for this parameter. - - )} - {autofillSource && autofillDescription[autofillSource] && ( -
- 🪄 Autofilled {autofillDescription[autofillSource]} -
- )} -
-
- ); -}; - -const RichParameterField: FC = ({ - disabled, - onChange, - parameter, - parameterAutofill, - value, - size, - ...props -}) => { - const small = size === "small"; - - if (isBoolean(parameter)) { - return ( - onChange(value)} - > - } - label="True" - /> - } - label="False" - /> - - ); - } - - if (parameter.options.length > 0) { - return ( - onChange(value)} - > - {parameter.options.map((option) => ( - } - label={ -
- {option.icon && ( - - )} - {option.description ? ( -
- {small ? ( - - -
{option.name}
-
- - - {option.description} - - -
- ) : ( - <> - {option.name} - - {option.description} - - - )} -
- ) : ( - option.name - )} -
- } - /> - ))} -
- ); - } - - if (parameter.type === "list(string)") { - let values: string[] = []; - - if (typeof value !== "string") { - throw new Error("Expected value to be a string"); - } - - if (value) { - try { - values = JSON.parse(value) as string[]; - } catch (e) { - console.error("Error parsing list(string) parameter", e); - } - } - - return ( - { - try { - const value = JSON.stringify(values); - onChange(value); - } catch (e) { - console.error("Error on change of list(string) parameter", e); - } - }} - /> - ); - } - - let inputProps: InputBaseComponentProps = {}; - if (parameter.type === "number") { - switch (parameter.validation_monotonic) { - case "increasing": - inputProps = { - max: parameter.validation_max, - min: parameterAutofill?.value, - }; - break; - case "decreasing": - inputProps = { - max: parameterAutofill?.value, - min: parameter.validation_min, - }; - break; - default: - inputProps = { - max: parameter.validation_max, - min: parameter.validation_min, - }; - break; - } - } - - // A text field can technically handle all cases! - // As other cases become more prominent (like filtering for numbers), - // we should break this out into more finely scoped input fields. - return ( - { - onChange(event.target.value); - }} - /> - ); -}; diff --git a/site/src/modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning.test.tsx b/site/src/modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning.test.tsx deleted file mode 100644 index a5bcd6e494..0000000000 --- a/site/src/modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { ClassicParameterFlowDeprecationWarning } from "./ClassicParameterFlowDeprecationWarning"; - -vi.mock("#/modules/navigation", () => ({ - useLinks: () => () => "/mock-link", - linkToTemplate: () => "/mock-template-link", -})); - -describe("ClassicParameterFlowDeprecationWarning", () => { - const defaultProps = { - organizationName: "test-org", - templateName: "test-template", - }; - - it("renders warning when enabled and user has template update permissions", () => { - render( - , - ); - - expect(screen.getByText("deprecated")).toBeInTheDocument(); - expect(screen.getByText("Go to Template Settings")).toBeInTheDocument(); - }); - - it("does not render when enabled is false", () => { - const { container } = render( - , - ); - - expect(container.firstChild).toBeNull(); - }); -}); diff --git a/site/src/modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning.tsx b/site/src/modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning.tsx deleted file mode 100644 index c2c0a97bb5..0000000000 --- a/site/src/modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { FC } from "react"; -import { Alert } from "#/components/Alert/Alert"; -import { Link } from "#/components/Link/Link"; -import { docs } from "#/utils/docs"; - -interface ClassicParameterFlowDeprecationWarningProps { - templateSettingsLink: string; - isEnabled: boolean; -} - -export const ClassicParameterFlowDeprecationWarning: FC< - ClassicParameterFlowDeprecationWarningProps -> = ({ templateSettingsLink, isEnabled }) => { - if (!isEnabled) { - return null; - } - - return ( - -
- This template is using the classic parameter flow, which will be{" "} - deprecated and removed in a future release. Please - migrate to{" "} - - dynamic parameters - {" "} - on template settings for improved functionality. -
- - - Go to Template Settings - -
- ); -}; diff --git a/site/src/modules/workspaces/ErrorDialog/WorkspaceErrorDialog.tsx b/site/src/modules/workspaces/ErrorDialog/WorkspaceErrorDialog.tsx index 7f7ce6696d..2c5ce0b349 100644 --- a/site/src/modules/workspaces/ErrorDialog/WorkspaceErrorDialog.tsx +++ b/site/src/modules/workspaces/ErrorDialog/WorkspaceErrorDialog.tsx @@ -15,7 +15,7 @@ interface WorkspaceErrorDialogProps { open: boolean; error?: unknown; onClose: () => void; - showDetail: boolean; + showDetail?: boolean; workspaceOwner: string; workspaceName: string; templateVersionId: string; @@ -26,7 +26,7 @@ export const WorkspaceErrorDialog: FC = ({ open, error, onClose, - showDetail, + showDetail = false, workspaceOwner, workspaceName, templateVersionId, diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialog.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialog.tsx deleted file mode 100644 index 1d4e16d22a..0000000000 --- a/site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialog.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { css } from "@emotion/css"; -import Dialog from "@mui/material/Dialog"; -import DialogActions from "@mui/material/DialogActions"; -import DialogContent from "@mui/material/DialogContent"; -import DialogContentText from "@mui/material/DialogContentText"; -import DialogTitle from "@mui/material/DialogTitle"; -import { useFormik } from "formik"; -import type { FC } from "react"; -import * as Yup from "yup"; -import type { - TemplateVersionParameter, - WorkspaceBuildParameter, -} from "#/api/typesGenerated"; -import { Button } from "#/components/Button/Button"; -import type { DialogProps } from "#/components/Dialogs/Dialog"; -import { FormFields, VerticalForm } from "#/components/Form/Form"; -import { RichParameterInput } from "#/components/RichParameterInput/RichParameterInput"; -import { getFormHelpers } from "#/utils/formUtils"; -import { - getInitialRichParameterValues, - useValidationSchemaForRichParameters, -} from "#/utils/richParameters"; - -type UpdateBuildParametersDialogProps = DialogProps & { - onClose: () => void; - onUpdate: (buildParameters: WorkspaceBuildParameter[]) => void; - missedParameters: TemplateVersionParameter[]; -}; - -export const UpdateBuildParametersDialog: FC< - UpdateBuildParametersDialogProps -> = ({ missedParameters, onUpdate, ...dialogProps }) => { - const form = useFormik({ - initialValues: { - rich_parameter_values: getInitialRichParameterValues(missedParameters), - }, - validationSchema: Yup.object({ - rich_parameter_values: - useValidationSchemaForRichParameters(missedParameters), - }), - onSubmit: (values) => { - onUpdate(values.rich_parameter_values); - }, - enableReinitialize: true, - }); - const getFieldHelpers = getFormHelpers(form); - - return ( - - - Workspace parameters - - - - This template has new parameters that must be configured to complete - the update - - - {missedParameters && ( - - {missedParameters.map((parameter, index) => { - return ( - { - await form.setFieldValue( - `rich_parameter_values.${index}`, - { - name: parameter.name, - value: value, - }, - ); - }} - /> - ); - })} - - )} - - - - - - - - ); -}; - -const classNames = { - root: css` - padding: 24px 40px; - - & h2 { - font-size: 20px; - font-weight: 400; - } - `, -}; diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialogExperimental.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialogExperimental.tsx deleted file mode 100644 index 72e4afc046..0000000000 --- a/site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialogExperimental.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import type { FC } from "react"; -import { useNavigate } from "react-router"; -import type { FieldError } from "#/api/errors"; -import { Button } from "#/components/Button/Button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "#/components/Dialog/Dialog"; - -type UpdateBuildParametersDialogExperimentalProps = { - open: boolean; - onClose: () => void; - validations: FieldError[]; - workspaceOwnerName: string; - workspaceName: string; - templateVersionId: string | undefined; -}; - -export const UpdateBuildParametersDialogExperimental: FC< - UpdateBuildParametersDialogExperimentalProps -> = ({ - validations, - open, - onClose, - workspaceOwnerName, - workspaceName, - templateVersionId, -}) => { - const navigate = useNavigate(); - - const handleGoToParameters = () => { - onClose(); - navigate( - `/@${workspaceOwnerName}/${workspaceName}/settings/parameters?templateVersionId=${templateVersionId}`, - ); - }; - - return ( - !isOpen && onClose()}> - - - Update workspace parameters - - This template has{" "} - - {validations.length} parameter - {validations.length === 1 ? "" : "s"} - {" "} - that must be configured to complete the update. - - - Would you like to go to the workspace parameters page to review and - update these parameters before continuing? - - - - - - - - - ); -}; diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx index da4ac04ca9..6380bc2742 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx @@ -11,7 +11,7 @@ import { type FC, useEffect, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { Link as RouterLink } from "react-router"; import { toast } from "sonner"; -import { MissingBuildParameters, ParameterValidationError } from "#/api/api"; +import { ParameterValidationError } from "#/api/api"; import { type ApiError, getErrorDetail, @@ -33,10 +33,9 @@ import { DropdownMenuTrigger, } from "#/components/DropdownMenu/DropdownMenu"; import { WorkspaceErrorDialog } from "../ErrorDialog/WorkspaceErrorDialog"; +import { UpdateBuildParametersDialog } from "../WorkspaceUpdateDialogs"; import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog"; import { DownloadLogsDialog } from "./DownloadLogsDialog"; -import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; -import { UpdateBuildParametersDialogExperimental } from "./UpdateBuildParametersDialogExperimental"; import { useWorkspaceDuplication } from "./useWorkspaceDuplication"; import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; @@ -71,11 +70,7 @@ export const WorkspaceMoreActions: FC = ({ // Change version const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); const changeVersionMutation = useMutation( - changeVersion( - workspace, - queryClient, - !workspace.template_use_classic_parameter_flow, - ), + changeVersion(workspace, queryClient), ); const handleError = (error: unknown) => { @@ -201,44 +196,13 @@ export const WorkspaceMoreActions: FC = ({ onClose={() => setIsDownloadDialogOpen(false)} /> - {workspace.template_use_classic_parameter_flow ? ( + {changeVersionMutation.error instanceof ParameterValidationError && ( { changeVersionMutation.reset(); }} - onUpdate={(buildParameters) => { - if (changeVersionMutation.error instanceof MissingBuildParameters) { - changeVersionMutation.mutate({ - versionId: changeVersionMutation.error.versionId, - buildParameters, - }); - } - }} - /> - ) : ( - { - changeVersionMutation.reset(); - }} - workspaceOwnerName={workspace.owner_name} - workspaceName={workspace.name} - templateVersionId={ - changeVersionMutation.error instanceof ParameterValidationError - ? changeVersionMutation.error.versionId - : undefined - } /> )} @@ -271,7 +235,6 @@ export const WorkspaceMoreActions: FC = ({ open={workspaceErrorDialog.open} error={workspaceErrorDialog.error} onClose={() => setWorkspaceErrorDialog({ open: false })} - showDetail={workspace.template_use_classic_parameter_flow} workspaceOwner={workspace.owner_name} workspaceName={workspace.name} templateVersionId={workspace.latest_build.template_version_id} diff --git a/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx b/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx index add8a31610..f9ea8aa7d1 100644 --- a/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx +++ b/site/src/modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx @@ -151,7 +151,7 @@ const WorkspaceOutdatedTooltipContent: FC = ({ - + ); }; diff --git a/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx index 15dd91c1f4..0ff03f6111 100644 --- a/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx +++ b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx @@ -1,17 +1,24 @@ import { type FC, useState } from "react"; import { useMutation, useQueryClient } from "react-query"; -import { MissingBuildParameters, ParameterValidationError } from "#/api/api"; +import { Link } from "react-router"; +import { ParameterValidationError } from "#/api/api"; import { updateWorkspace } from "#/api/queries/workspaces"; import type { TemplateVersion, Workspace, WorkspaceBuild, - WorkspaceBuildParameter, } from "#/api/typesGenerated"; +import { Button } from "#/components/Button/Button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "#/components/Dialog/Dialog"; import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog"; import { MemoizedInlineMarkdown } from "#/components/Markdown/InlineMarkdown"; -import { UpdateBuildParametersDialog } from "#/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialog"; -import { UpdateBuildParametersDialogExperimental } from "#/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialogExperimental"; type UseWorkspaceUpdateOptions = { workspace: Workspace; @@ -23,10 +30,7 @@ type UseWorkspaceUpdateOptions = { type UseWorkspaceUpdateResult = { update: () => void; isUpdating: boolean; - dialogs: { - updateConfirmation: UpdateConfirmationDialogProps; - missingBuildParameters: MissingBuildParametersDialogProps; - }; + dialogProps: WorkspaceUpdateDialogsProps; }; export const useWorkspaceUpdate = ({ @@ -52,11 +56,9 @@ export const useWorkspaceUpdate = ({ setIsConfirmingUpdate(true); }; - const confirmUpdate = (buildParameters: WorkspaceBuildParameter[] = []) => { + const confirmUpdate = () => { updateWorkspaceMutation.mutate({ - buildParameters, - isDynamicParametersEnabled: - !workspace.template_use_classic_parameter_flow, + buildParameters: [], }); setIsConfirmingUpdate(false); }; @@ -64,57 +66,54 @@ export const useWorkspaceUpdate = ({ return { update, isUpdating: updateWorkspaceMutation.isPending, - dialogs: { - updateConfirmation: { + dialogProps: { + confirmUpdateDialogProps: { open: isConfirmingUpdate, onClose: () => setIsConfirmingUpdate(false), onConfirm: () => confirmUpdate(), latestVersion, }, - missingBuildParameters: { - workspace, - error: updateWorkspaceMutation.error, - onClose: () => { - updateWorkspaceMutation.reset(); - }, - onUpdate: (buildParameters: WorkspaceBuildParameter[]) => { - if ( - updateWorkspaceMutation.error instanceof MissingBuildParameters || - updateWorkspaceMutation.error instanceof ParameterValidationError - ) { - confirmUpdate(buildParameters); - } - }, - }, + updateBuildParametersDialogProps: + updateWorkspaceMutation.error instanceof ParameterValidationError + ? { + workspace, + error: updateWorkspaceMutation.error, + onClose: () => { + updateWorkspaceMutation.reset(); + }, + } + : undefined, }, }; }; type WorkspaceUpdateDialogsProps = { - updateConfirmation: UpdateConfirmationDialogProps; - missingBuildParameters: MissingBuildParametersDialogProps; + confirmUpdateDialogProps: ConfirmUpdateDialogProps; + updateBuildParametersDialogProps?: UpdateBuildParametersDialogProps; }; export const WorkspaceUpdateDialogs: FC = ({ - updateConfirmation, - missingBuildParameters, + confirmUpdateDialogProps, + updateBuildParametersDialogProps, }) => { return ( <> - - + + {updateBuildParametersDialogProps && ( + + )} ); }; -type UpdateConfirmationDialogProps = { +type ConfirmUpdateDialogProps = { open: boolean; onClose: () => void; onConfirm: () => void; latestVersion?: TemplateVersion; }; -const UpdateConfirmationDialog: FC = ({ +const ConfirmUpdateDialog: FC = ({ latestVersion, ...dialogProps }) => { @@ -144,42 +143,49 @@ const UpdateConfirmationDialog: FC = ({ ); }; -type MissingBuildParametersDialogProps = { +type UpdateBuildParametersDialogProps = { workspace: Workspace; - error: unknown; + error: ParameterValidationError; onClose: () => void; - onUpdate: (buildParameters: WorkspaceBuildParameter[]) => void; }; -const MissingBuildParametersDialog: FC = ({ - workspace, - error, - ...dialogProps -}) => { - const missedParameters = - error instanceof MissingBuildParameters ? error.parameters : []; - const versionId = - error instanceof ParameterValidationError ? error.versionId : undefined; - const isOpen = - error instanceof MissingBuildParameters || - error instanceof ParameterValidationError; +export const UpdateBuildParametersDialog: FC< + UpdateBuildParametersDialogProps +> = ({ workspace, error, onClose }) => { + const templateVersionId = error.versionId; + const validations = error.validations; - return workspace.template_use_classic_parameter_flow ? ( - - ) : ( - + return ( + onClose()}> + + + Update workspace parameters + + This workspace has{" "} + + {validations.length} parameter + {validations.length === 1 ? "" : "s"} + {" "} + that must be configured to complete the update. + + + Would you like to go to the workspace parameters page to review and + update these parameters before continuing? + + + + + + + + ); }; diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index 0382d72666..626bb65596 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx @@ -21,13 +21,11 @@ import { HorizontalForm, } from "#/components/Form/Form"; import { IconField } from "#/components/IconField/IconField"; -import { Link } from "#/components/Link/Link"; import { Spinner } from "#/components/Spinner/Spinner"; import { StackLabel, StackLabelHelperText, } from "#/components/StackLabel/StackLabel"; -import { docs } from "#/utils/docs"; import { displayNameValidator, getFormHelpers, @@ -49,7 +47,6 @@ export const validationSchema = Yup.object({ allow_user_cancel_workspace_jobs: Yup.boolean(), icon: iconValidator, require_active_version: Yup.boolean(), - use_classic_parameter_flow: Yup.boolean(), disable_module_cache: Yup.boolean(), deprecation_message: Yup.string(), max_port_sharing_level: Yup.string().oneOf(WorkspaceAppSharingLevels), @@ -94,7 +91,6 @@ export const TemplateSettingsForm: FC = ({ deprecation_message: template.deprecation_message, disable_everyone_group_access: false, max_port_share_level: template.max_port_share_level, - use_classic_parameter_flow: template.use_classic_parameter_flow, cors_behavior: template.cors_behavior, disable_module_cache: template.disable_module_cache, }, @@ -225,47 +221,6 @@ export const TemplateSettingsForm: FC = ({ } /> - - form.setFieldValue( - "use_classic_parameter_flow", - !event.currentTarget.checked, - ) - } - disabled={false} - /> - } - label={ - - - Enable dynamic parameters for workspace creation (recommended) - - -
- The dynamic workspace form allows you to design your - template with additional form types and identity-aware - conditional parameters. This is the default option for new - templates. The classic workspace creation flow will be - deprecated in a future release. -
- - Learn more - -
-
- } - /> void; label: string; } @@ -44,19 +29,15 @@ export const BuildParametersPopover: FC = ({ workspace, disabled, label, - onSubmit, }) => { const [isOpen, setIsOpen] = useState(false); const build = workspace.latest_build; const { data: templateVersionParameters } = useQuery( richParameters(build.template_version_id), ); - const { data: buildParameters } = useQuery( - workspaceBuildParameters(build.id), + const ephemeralParameters = templateVersionParameters?.filter( + (p) => p.ephemeral, ); - const ephemeralParameters = templateVersionParameters - ? templateVersionParameters.filter((p) => p.ephemeral) - : undefined; return ( @@ -77,9 +58,6 @@ export const BuildParametersPopover: FC = ({ @@ -88,163 +66,68 @@ export const BuildParametersPopover: FC = ({ interface BuildParametersPopoverContentProps { workspace: Workspace; - ephemeralParameters?: TemplateVersionParameter[]; - buildParameters?: WorkspaceBuildParameter[]; - onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void; - setIsOpen: (newOpen: boolean) => void; + ephemeralParameters: TemplateVersionParameter[] | undefined; } const BuildParametersPopoverContent: FC = ({ workspace, ephemeralParameters, - buildParameters, - onSubmit, - setIsOpen, }) => { - if ( - !workspace.template_use_classic_parameter_flow && - ephemeralParameters && - ephemeralParameters.length > 0 - ) { + if (!ephemeralParameters) { + return ; + } + + if (ephemeralParameters.length === 0) { return ( -
-

- This workspace has ephemeral parameters which may use a temporary - value on workspace start. Configure the following parameters in - workspace settings. -

- -
-
    - {ephemeralParameters.map((param) => ( -
  • -

    - {param.display_name || param.name} -

    - {param.description && ( -

    - {param.description} -

    - )} -
  • - ))} -
-
- - - Go to workspace parameters - +
+ Build Options + + This template has no ephemeral build options. + + + + Read the docs + +
); } return ( - <> - {buildParameters && ephemeralParameters ? ( - ephemeralParameters.length > 0 ? ( -
-
- Build Options - - These parameters only apply for a single workspace start. - -
-
-
{ - onSubmit(buildParameters); - setIsOpen(false); - }} - ephemeralParameters={ephemeralParameters} - buildParameters={buildParameters.map( - (p): AutofillBuildParameter => ({ - ...p, - source: "active_build", - }), - )} - /> -
-
- ) : ( -
- Build Options - - This template has no ephemeral build options. - - - - Read the docs - - -
- ) - ) : ( - - )} - - ); -}; +
+

+ This workspace has ephemeral parameters which may use a temporary value + on workspace start. Configure the following parameters in workspace + settings. +

-interface FormProps { - ephemeralParameters: TemplateVersionParameter[]; - buildParameters: AutofillBuildParameter[]; - onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void; -} - -const Form: FC = ({ - ephemeralParameters, - buildParameters, - onSubmit, -}) => { - const form = useFormik({ - initialValues: { - rich_parameter_values: getInitialRichParameterValues( - ephemeralParameters, - buildParameters, - ), - }, - onSubmit: (values) => { - onSubmit(values.rich_parameter_values); - }, - }); - const getFieldHelpers = getFormHelpers(form); - - return ( - - - {ephemeralParameters.map((parameter, index) => { - return ( - { - await form.setFieldValue(`rich_parameter_values[${index}]`, { - name: parameter.name, - value: value, - }); - }} - /> - ); - })} - -
- +
+
    + {ephemeralParameters.map((param) => ( +
  • +

    + {param.display_name || param.name} +

    + {param.description && ( +

    + {param.description} +

    + )} +
  • + ))} +
- + + + Go to workspace parameters + +
); }; diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx index 5a1bcd5b8b..e2773d6082 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx @@ -114,7 +114,6 @@ export const StartButton: FC = ({ label="Start with build parameters" workspace={workspace} disabled={loading} - onSubmit={handleAction} />
); @@ -155,7 +154,6 @@ export const RestartButton: FC = ({ label="Restart with build parameters" workspace={workspace} disabled={loading} - onSubmit={handleAction} />
); diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DebugButton.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DebugButton.tsx index 04b89f722e..62d23acb1e 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/DebugButton.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/DebugButton.tsx @@ -32,7 +32,6 @@ export const DebugButton: FC = ({ ); diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/RetryButton.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/RetryButton.stories.tsx index 94b463f1c4..913190ede9 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/RetryButton.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/RetryButton.stories.tsx @@ -3,7 +3,6 @@ import { expect, screen, userEvent, waitFor } from "storybook/test"; import { richParametersKey } from "#/api/queries/templates"; import { workspaceBuildParametersKey } from "#/api/queries/workspaceBuilds"; import { - MockNonClassicParameterFlowWorkspace, MockTemplateVersionParameter1, MockTemplateVersionParameter6, MockWorkspace, @@ -83,35 +82,6 @@ export const WithOpenEphemeralBuildParameters: Story = { }, ], }, - play: async ({ step }) => { - await step("open popover", async () => { - await userEvent.click(screen.getByText("Retry with build parameters")); - expect( - await screen.findByText( - "These parameters only apply for a single workspace start.", - ), - ).toBeInTheDocument(); - }); - }, -}; - -export const WithOpenEphemeralBuildParametersNotClassic: Story = { - args: { - enableBuildParameters: true, - workspace: MockNonClassicParameterFlowWorkspace, - }, - parameters: { - queries: [ - { - key: richParametersKey(MockWorkspace.latest_build.template_version_id), - data: [MockTemplateVersionParameter6], - }, - { - key: workspaceBuildParametersKey(MockWorkspace.latest_build.id), - data: [], - }, - ], - }, play: async ({ step }) => { await step("open popover", async () => { await userEvent.click(screen.getByText("Retry with build parameters")); diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/RetryButton.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/RetryButton.tsx index 7308374c08..3b91521a2f 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/RetryButton.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/RetryButton.tsx @@ -32,7 +32,6 @@ export const RetryButton: FC = ({ ); diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index badc7ec08e..f6affdd87c 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -5,7 +5,7 @@ import { HttpResponse, http } from "msw"; import type { FC } from "react"; import type { MockInstance } from "vitest"; import * as apiModule from "#/api/api"; -import type { TemplateVersionParameter, Workspace } from "#/api/typesGenerated"; +import type { Workspace } from "#/api/typesGenerated"; import { DashboardContext, type DashboardProvider, @@ -23,9 +23,6 @@ import { MockStartingWorkspace, MockStoppedWorkspace, MockTemplate, - MockTemplateVersionParameter1, - MockTemplateVersionParameter2, - MockUserOwner, MockWorkspace, MockWorkspaceBuild, MockWorkspaceBuildDelete, @@ -37,7 +34,7 @@ import { import { server } from "#/testHelpers/server"; import WorkspacePage from "./WorkspacePage"; -const { API, MissingBuildParameters } = apiModule; +const { API } = apiModule; type RenderWorkspacePageOptions = Omit; @@ -306,118 +303,6 @@ describe("WorkspacePage", () => { }); }); - // Started flaking after upgrading react-router. Tests the old parameters path - // and isn't worth spending more time to fix since this code will be removed - // in a few releases when dynamic parameters takes over the world. - it("updates the parameters when they are missing during update", async () => { - // Mocks - vi.spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValueOnce( - MockOutdatedWorkspace, - ); - const updateWorkspaceSpy = vi - .spyOn(API, "updateWorkspace") - .mockRejectedValueOnce( - new MissingBuildParameters( - [MockTemplateVersionParameter1, MockTemplateVersionParameter2], - MockOutdatedWorkspace.template_active_version_id, - ), - ); - - // Render - await renderWorkspacePage(MockWorkspace); - - // Actions - const user = userEvent.setup(); - await user.click(screen.getByTestId("workspace-update-button")); - const confirmButton = await screen.findByTestId("confirm-button"); - await user.click(confirmButton); - - // The update was called - await waitFor(() => { - expect(API.updateWorkspace).toBeCalled(); - updateWorkspaceSpy.mockClear(); - }); - - // After trying to update, a new dialog asking for missed parameters should - // be displayed and filled - const dialog = await screen.findByRole("dialog", { - name: /workspace parameters/i, - }); - const firstParameterInput = within(dialog).getByLabelText( - MockTemplateVersionParameter1.name, - { exact: false }, - ); - await user.clear(firstParameterInput); - await user.type(firstParameterInput, "some-value"); - const secondParameterInput = within(dialog).getByLabelText( - MockTemplateVersionParameter2.name, - { exact: false }, - ); - await user.clear(secondParameterInput); - await user.type(secondParameterInput, "2"); - await user.click( - within(dialog).getByRole("button", { name: /update parameters/i }), - ); - - // Check if the update was called using the values from the form - await waitFor(() => { - expect(API.updateWorkspace).toHaveBeenCalledWith( - MockOutdatedWorkspace, - [ - { - name: MockTemplateVersionParameter1.name, - value: "some-value", - }, - { - name: MockTemplateVersionParameter2.name, - value: "2", - }, - ], - false, - ); - }); - }); - - it("restart the workspace with one time parameters when having the confirmation dialog", async () => { - localStorage.removeItem(`${MockUserOwner.id}_ignoredWarnings`); - vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([ - { - ...MockTemplateVersionParameter1, - ephemeral: true, - name: "rebuild", - description: "Rebuild", - required: false, - }, - ]); - vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([ - { name: "rebuild", value: "false" }, - ]); - - const restartWorkspaceSpy = vi.spyOn(API, "restartWorkspace"); - const user = userEvent.setup(); - await renderWorkspacePage(MockWorkspace); - await user.click(screen.getByTestId("build-parameters-button")); - const buildParametersForm = await screen.findByTestId( - "build-parameters-form", - ); - const rebuildField = within(buildParametersForm).getByLabelText("Rebuild", { - exact: false, - }); - await user.clear(rebuildField); - await user.type(rebuildField, "true"); - await user.click(screen.getByTestId("build-parameters-submit")); - await user.click(screen.getByTestId("confirm-button")); - await waitFor(() => { - expect(restartWorkspaceSpy).toBeCalledWith({ - workspace: MockWorkspace, - buildParameters: [{ name: "rebuild", value: "true" }], - }); - }); - }); - - // Tried to get these wired up via describe.each to reduce repetition, but the - // syntax just got too convoluted because of the variance in what arguments - // each function gets called with describe("Retrying failed workspaces", () => { const retryButtonRe = /^Retry$/i; const retryDebugButtonRe = /^Debug$/i; @@ -506,110 +391,6 @@ describe("WorkspacePage", () => { }); }); - it("retry with build parameters", async () => { - const user = userEvent.setup(); - const workspace = { - ...MockFailedWorkspace, - latest_build: { - ...MockFailedWorkspace.latest_build, - transition: "start", - }, - } satisfies Workspace; - const parameter = { - ...MockTemplateVersionParameter1, - display_name: "Parameter 1", - ephemeral: true, - } satisfies TemplateVersionParameter; - - server.use( - http.get("/api/v2/templateversions/:versionId/rich-parameters", () => { - return HttpResponse.json([parameter]); - }), - ); - const retryWorkspaceSpy = vi - .spyOn(API, "retryWorkspace") - .mockResolvedValue(MockWorkspaceBuild); - - await renderWorkspacePage(workspace); - const retryWithBuildParametersButton = await screen.findByRole("button", { - name: "Retry with build parameters", - }); - await user.click(retryWithBuildParametersButton); - await screen.findByText("Build Options"); - const parameterField = await screen.findByLabelText( - parameter.display_name, - { - exact: false, - }, - ); - - await user.clear(parameterField); - await user.type(parameterField, "some-value"); - const submitButton = screen.getByText("Build workspace"); - await user.click(submitButton); - - await waitFor(() => { - expect(retryWorkspaceSpy).toBeCalledWith( - workspace, - workspace.latest_build.template_version_id, - undefined, - [{ name: parameter.name, value: "some-value" }], - ); - }); - }); - - it("debug with build parameters", async () => { - const user = userEvent.setup(); - const workspace = { - ...MockFailedWorkspace, - latest_build: { - ...MockFailedWorkspace.latest_build, - transition: "start", - }, - } satisfies Workspace; - const parameter = { - ...MockTemplateVersionParameter1, - display_name: "Parameter 1", - ephemeral: true, - } satisfies TemplateVersionParameter; - - server.use( - http.get("/api/v2/templateversions/:versionId/rich-parameters", () => { - return HttpResponse.json([parameter]); - }), - ); - const retryWorkspaceSpy = vi - .spyOn(API, "retryWorkspace") - .mockResolvedValue(MockWorkspaceBuild); - - await renderWorkspacePage(workspace); - const retryWithBuildParametersButton = await screen.findByRole("button", { - name: "Debug with build parameters", - }); - await user.click(retryWithBuildParametersButton); - await screen.findByText("Build Options"); - const parameterField = await screen.findByLabelText( - parameter.display_name, - { - exact: false, - }, - ); - - await user.clear(parameterField); - await user.type(parameterField, "some-value"); - const submitButton = screen.getByText("Build workspace"); - await user.click(submitButton); - - await waitFor(() => { - expect(retryWorkspaceSpy).toBeCalledWith( - workspace, - workspace.latest_build.template_version_id, - "debug", - [{ name: parameter.name, value: "some-value" }], - ); - }); - }); - describe("Navigation to other pages", () => { it("Shows a quota link when quota budget is greater than 0. Link navigates user to /workspaces route with the URL params populated with the corresponding organization", async () => { vi.spyOn(API, "getWorkspaceQuota").mockResolvedValueOnce({ diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 5906b1c514..0f1166fbed 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -226,10 +226,6 @@ export const WorkspaceReadyPage: FC = ({ const checkEphemeralParameters = async ( buildParameters?: TypesGen.WorkspaceBuildParameter[], ) => { - if (workspace.template_use_classic_parameter_flow) { - return { hasEphemeral: false, ephemeralParameters: [] }; - } - try { const dynamicParameters = await API.getDynamicParameters( workspace.latest_build.template_version_id, @@ -433,13 +429,12 @@ export const WorkspaceReadyPage: FC = ({ templateVersionId={workspace.latest_build.template_version_id} /> - + setWorkspaceErrorDialog({ open: false })} - showDetail={workspace.template_use_classic_parameter_flow} workspaceOwner={workspace.owner_name} workspaceName={workspace.name} templateVersionId={workspace.latest_build.template_version_id} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx index 210d8bf6d1..9d857e43a1 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx @@ -322,16 +322,8 @@ export const BatchUpdateSkipsUpToDateWorkspaces: Story = { await user.click(within(modal).getByRole("button", { name: /Update/ })); await waitFor(() => expect(API.updateWorkspace).toHaveBeenCalledTimes(2)); - expect(API.updateWorkspace).toHaveBeenCalledWith( - skipUpToDateWorkspaces[2], - [], - false, - ); - expect(API.updateWorkspace).toHaveBeenCalledWith( - skipUpToDateWorkspaces[3], - [], - false, - ); + expect(API.updateWorkspace).toHaveBeenCalledWith(skipUpToDateWorkspaces[2]); + expect(API.updateWorkspace).toHaveBeenCalledWith(skipUpToDateWorkspaces[3]); }, }; @@ -370,18 +362,12 @@ export const BatchUpdateRunningWorkspace: Story = { await waitFor(() => expect(API.updateWorkspace).toHaveBeenCalledTimes(3)); expect(API.updateWorkspace).toHaveBeenCalledWith( updateRunningWorkspaces[0], - [], - false, ); expect(API.updateWorkspace).toHaveBeenCalledWith( updateRunningWorkspaces[1], - [], - false, ); expect(API.updateWorkspace).toHaveBeenCalledWith( updateRunningWorkspaces[2], - [], - false, ); }, }; @@ -416,13 +402,9 @@ export const BatchUpdateIgnoresDormantWorkspaces: Story = { await waitFor(() => expect(API.updateWorkspace).toHaveBeenCalledTimes(2)); expect(API.updateWorkspace).toHaveBeenCalledWith( ignoreDormantWorkspaces[1], - [], - false, ); expect(API.updateWorkspace).toHaveBeenCalledWith( ignoreDormantWorkspaces[2], - [], - false, ); }, }; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index e20e8eaa52..8baaa8f40f 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -248,7 +248,6 @@ const WorkspacesPage: FC = () => { onSubmit={async () => { await batchActions.updateTemplateVersions({ workspaces: checkedWorkspaces, - isDynamicParametersEnabled: false, }); setActiveBatchAction(undefined); }} diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 540227daa4..3526c24d93 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -510,7 +510,7 @@ const WorkspaceActionsCell: FC = ({ > - + )} @@ -523,7 +523,7 @@ const WorkspaceActionsCell: FC = ({ > - + )} @@ -536,7 +536,7 @@ const WorkspaceActionsCell: FC = ({ > - + )} @@ -549,7 +549,7 @@ const WorkspaceActionsCell: FC = ({ > - + )} diff --git a/site/src/pages/WorkspacesPage/batchActions.ts b/site/src/pages/WorkspacesPage/batchActions.ts index b372415a24..2ac7011e1e 100644 --- a/site/src/pages/WorkspacesPage/batchActions.ts +++ b/site/src/pages/WorkspacesPage/batchActions.ts @@ -10,7 +10,6 @@ interface UseBatchActionsOptions { type UpdateAllPayload = Readonly<{ workspaces: readonly Workspace[]; - isDynamicParametersEnabled: boolean; }>; type UseBatchActionsResult = Readonly<{ @@ -78,11 +77,11 @@ export function useBatchActions( const updateAllMutation = useMutation({ mutationFn: (payload: UpdateAllPayload) => { - const { workspaces, isDynamicParametersEnabled } = payload; + const { workspaces } = payload; return Promise.all( workspaces .filter((w) => w.outdated && !w.dormant_at) - .map((w) => API.updateWorkspace(w, [], isDynamicParametersEnabled)), + .map((w) => API.updateWorkspace(w)), ); }, onSuccess, diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 9515870033..001af980d0 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1832,12 +1832,6 @@ export const MockPendingWorkspace: TypesGen.Workspace = { }, }; -export const MockNonClassicParameterFlowWorkspace: TypesGen.Workspace = { - ...MockWorkspace, - id: "test-non-classic-parameter-flow-workspace", - template_use_classic_parameter_flow: false, -}; - // just over one page of workspaces export const MockWorkspacesResponse: TypesGen.WorkspacesResponse = { workspaces: range(1, 27).map((id: number) => ({ diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index c1c6c057f1..4b81b94afc 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -118,6 +118,16 @@ export const handlers = [ return HttpResponse.json([]); }, ), + http.post( + "/api/v2/templateversions/:templateVersionId/dynamic-parameters/evaluate", + () => { + return HttpResponse.json({ + id: 0, + diagnostics: [], + parameters: [], + }); + }, + ), http.get("/api/v2/templateversions/:templateVersionId/external-auth", () => { return HttpResponse.json([]); }), diff --git a/site/src/utils/richParameters.test.ts b/site/src/utils/richParameters.test.ts deleted file mode 100644 index e1cb956f9b..0000000000 --- a/site/src/utils/richParameters.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { TemplateVersionParameter } from "#/api/typesGenerated"; -import { getInitialRichParameterValues } from "./richParameters"; - -test("getInitialRichParameterValues return default value when default build parameter is not valid", () => { - const templateParameters: TemplateVersionParameter[] = [ - { - name: "cpu", - display_name: "CPU", - description: "The number of CPU cores", - description_plaintext: "The number of CPU cores", - type: "string", - form_type: "radio", - mutable: true, - default_value: "2", - icon: "/icon/memory.svg", - options: [ - { - name: "2 Cores", - description: "", - value: "2", - icon: "", - }, - { - name: "4 Cores", - description: "", - value: "4", - icon: "", - }, - { - name: "6 Cores", - description: "", - value: "6", - icon: "", - }, - { - name: "8 Cores", - description: "", - value: "8", - icon: "", - }, - ], - required: false, - ephemeral: false, - }, - ]; - - const cpuParameter = templateParameters[0]; - const [cpuParameterInitialValue] = getInitialRichParameterValues( - templateParameters, - [{ name: cpuParameter.name, value: "100", source: "url" }], - ); - - expect(cpuParameterInitialValue.value).toBe(cpuParameter.default_value); -}); diff --git a/site/src/utils/richParameters.ts b/site/src/utils/richParameters.ts index e9af4bcb5e..0cac57d606 100644 --- a/site/src/utils/richParameters.ts +++ b/site/src/utils/richParameters.ts @@ -1,204 +1,9 @@ -import * as Yup from "yup"; -import type { - TemplateVersionParameter, - WorkspaceBuildParameter, -} from "#/api/typesGenerated"; +import type { WorkspaceBuildParameter } from "#/api/typesGenerated"; -export type AutofillSource = "user_history" | "url" | "active_build"; +type AutofillSource = "user_history" | "url" | "active_build"; // AutofillBuildParameter is a build parameter destined to a form, alongside // its source so that the form can explain where the value comes from. export type AutofillBuildParameter = { source: AutofillSource; } & WorkspaceBuildParameter; - -export const getInitialRichParameterValues = ( - templateParams: TemplateVersionParameter[], - autofillParams?: AutofillBuildParameter[], -): WorkspaceBuildParameter[] => { - return templateParams.map((parameter) => { - // Short-circuit for ephemeral parameters, which are always reset to - // the template-defined default. - if (parameter.ephemeral) { - return { - name: parameter.name, - value: parameter.default_value, - }; - } - - const autofillParam = autofillParams?.find( - ({ name }) => name === parameter.name, - ); - - return { - name: parameter.name, - value: - autofillParam && - isValidValue(parameter, autofillParam) && - autofillParam.source !== "user_history" - ? autofillParam.value - : parameter.default_value, - }; - }); -}; - -const isValidValue = ( - templateParam: TemplateVersionParameter, - buildParam: WorkspaceBuildParameter, -) => { - if (templateParam.options.length > 0) { - const validValues = templateParam.options.map((option) => option.value); - return validValues.includes(buildParam.value); - } - - return true; -}; - -export const useValidationSchemaForRichParameters = ( - templateParameters?: TemplateVersionParameter[], - lastBuildParameters?: WorkspaceBuildParameter[], -): Yup.AnySchema => { - if (!templateParameters) { - return Yup.object(); - } - - return Yup.array() - .of( - Yup.object().shape({ - name: Yup.string().required(), - value: Yup.string() - .test("verify with template", (val, ctx) => { - const name = ctx.parent.name; - const templateParameter = templateParameters.find( - (parameter) => parameter.name === name, - ); - if (templateParameter) { - switch (templateParameter.type) { - case "number": - if ( - templateParameter.validation_min && - !templateParameter.validation_max - ) { - if (Number(val) < templateParameter.validation_min) { - return ctx.createError({ - path: ctx.path, - message: - parameterError(templateParameter, val) ?? - `Value must be greater than ${templateParameter.validation_min}.`, - }); - } - } else if ( - !templateParameter.validation_min && - templateParameter.validation_max - ) { - if (templateParameter.validation_max < Number(val)) { - return ctx.createError({ - path: ctx.path, - message: - parameterError(templateParameter, val) ?? - `Value must be less than ${templateParameter.validation_max}.`, - }); - } - } else if ( - templateParameter.validation_min && - templateParameter.validation_max - ) { - if ( - Number(val) < templateParameter.validation_min || - templateParameter.validation_max < Number(val) - ) { - return ctx.createError({ - path: ctx.path, - message: - parameterError(templateParameter, val) ?? - `Value must be between ${templateParameter.validation_min} and ${templateParameter.validation_max}.`, - }); - } - } - - if ( - templateParameter.validation_monotonic && - lastBuildParameters - ) { - const lastBuildParameter = lastBuildParameters.find( - (last) => last.name === name, - ); - if (lastBuildParameter) { - switch (templateParameter.validation_monotonic) { - case "increasing": - if (Number(lastBuildParameter.value) > Number(val)) { - return ctx.createError({ - path: ctx.path, - message: `Value must only ever increase (last value was ${lastBuildParameter.value})`, - }); - } - break; - case "decreasing": - if (Number(lastBuildParameter.value) < Number(val)) { - return ctx.createError({ - path: ctx.path, - message: `Value must only ever decrease (last value was ${lastBuildParameter.value})`, - }); - } - break; - } - } - } - break; - case "string": - { - if ( - !templateParameter.validation_regex || - templateParameter.validation_regex.length === 0 - ) { - return true; - } - - const regex = new RegExp( - templateParameter.validation_regex, - ); - if (val && !regex.test(val)) { - return ctx.createError({ - path: ctx.path, - message: parameterError(templateParameter, val), - }); - } - } - break; - } - } - return true; - }), - }), - ) - .required(); -}; - -const parameterError = ( - parameter: TemplateVersionParameter, - value?: string, -): string | undefined => { - if (!parameter.validation_error || !value) { - return; - } - - const r = new Map([ - [ - "{min}", - parameter.validation_min !== undefined - ? parameter.validation_min.toString() - : "", - ], - [ - "{max}", - parameter.validation_max !== undefined - ? parameter.validation_max.toString() - : "", - ], - ["{value}", value], - ]); - return parameter.validation_error.replace( - /{min}|{max}|{value}/g, - (match) => r.get(match) || "", - ); -};