From 932335789f2209af41a5d7c1a4144b7b0da6c671 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:56:46 +0000 Subject: [PATCH] fix(site): allow editing parameters when the template forces an update When a template has require_active_version enabled and the workspace is outdated, the parameters form was fully disabled for anyone without template update permission. Members could therefore never edit any parameter, including ones the new version did not change, which left them unable to start their workspace if a new version introduced a validation their current values did not satisfy. Such a user can only ever build against the active version, so evaluate and submit parameters against it instead of disabling the form. --- .../WorkspaceParametersPage.stories.tsx | 8 +-- .../WorkspaceParametersPage.test.tsx | 61 +++++++++++++++++++ .../WorkspaceParametersPage.tsx | 21 +++++-- .../WorkspaceParametersPageView.tsx | 20 +++--- 4 files changed, 89 insertions(+), 21 deletions(-) diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.stories.tsx index 91d00017296..d3129003558 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.stories.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.stories.tsx @@ -162,7 +162,7 @@ export const StartWorkspace: Story = { }, }; -export const RequireActiveVersionBlocked: Story = { +export const RequireActiveVersionForcedUpdate: Story = { parameters: { reactRouter: workspaceRouterParameters( MockOutdatedStoppedWorkspaceRequireActiveVersion, @@ -183,11 +183,11 @@ export const RequireActiveVersionBlocked: Story = { const submitButton = canvas.getByRole("button", { name: "Update and start", }); - expect(submitButton).toBeDisabled(); + expect(submitButton).not.toBeDisabled(); }, }; -export const RequireActiveVersionBlockedRunning: Story = { +export const RequireActiveVersionForcedUpdateRunning: Story = { parameters: { reactRouter: workspaceRouterParameters( MockOutdatedRunningWorkspaceRequireActiveVersion, @@ -208,7 +208,7 @@ export const RequireActiveVersionBlockedRunning: Story = { const submitButton = canvas.getByRole("button", { name: "Update and restart", }); - expect(submitButton).toBeDisabled(); + expect(submitButton).not.toBeDisabled(); }, }; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx index 83ee2589ff9..fdbf41ca2a4 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx @@ -358,4 +358,65 @@ describe("WorkspaceParametersPage", () => { }); await waitFor(() => expect(submitButton).toBeDisabled()); }); + + it("stays editable when the template forces an update", async () => { + const workspace: TypesGen.Workspace = { + ...MockWorkspace, + outdated: true, + template_require_active_version: true, + template_active_version_id: "active-template-version-id", + }; + const workspaceSpy = vi.spyOn(API, "getWorkspaceByOwnerAndName"); + workspaceSpy.mockReset(); + workspaceSpy.mockResolvedValue(workspace); + + // A user with no template update permission, such as a member. + vi.spyOn(API, "checkAuthorization").mockResolvedValue({ + readWorkspace: true, + shareWorkspace: true, + updateWorkspace: true, + updateWorkspaceVersion: false, + deleteFailedWorkspace: false, + }); + + vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([]); + + const [, mockPublisher] = mockDynamicParameterWebSocket(); + + renderWorkspaceParametersPage(); + + await connectWithInitialParameters(mockPublisher, [ + MockPreviewParameter1, + MockPreviewParameter4, + ]); + + // Parameters are evaluated against the active version, since that is the + // only version this user is able to build. + expect(API.templateVersionDynamicParameters).toHaveBeenCalledWith( + workspace.template_active_version_id, + workspace.owner_id, + expect.anything(), + ); + + await waitForLoaderToBeRemoved(); + + const form = screen.getByTestId("form"); + + // Mutable parameters are editable even though the workspace is outdated. + const mutableField = within(form).getByTestId( + `parameter-field-${MockPreviewParameter1.name}`, + ); + expect(within(mutableField).getByRole("textbox")).toBeEnabled(); + + // Immutable parameters are still locked. + const immutableField = within(form).getByTestId( + `parameter-field-${MockPreviewParameter4.name}`, + ); + expect(within(immutableField).getByRole("textbox")).toBeDisabled(); + + const submitButton = within(form).getByRole("button", { + name: /update and restart/i, + }); + await waitFor(() => expect(submitButton).toBeEnabled()); + }); }); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx index 4b27633f69e..00f62e436b3 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx @@ -31,7 +31,22 @@ const WorkspaceParametersPage: FC = () => { const { permissions, workspace } = useWorkspaceSettings(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); - const templateVersionId = searchParams.get("templateVersionId") ?? undefined; + const requestedTemplateVersionId = + searchParams.get("templateVersionId") ?? undefined; + + const canChangeVersions = Boolean(permissions?.updateWorkspaceVersion); + + // When the template requires the active version and the user is not allowed + // to pin versions, the only build they can ever submit is one against the + // active version, so edit the parameters against it. Otherwise a new version + // that adds a validation the current values do not satisfy would leave the + // user unable to fix those values, and therefore unable to start their + // workspace at all. + const forcedActiveVersion = + workspace.template_require_active_version && !canChangeVersions; + const templateVersionId = forcedActiveVersion + ? workspace.template_active_version_id + : requestedTemplateVersionId; const [confirmingRestart, setConfirmingRestart] = useState<{ open: boolean; @@ -209,8 +224,6 @@ const WorkspaceParametersPage: FC = () => { }, }); - const canChangeVersions = Boolean(permissions?.updateWorkspaceVersion); - const handleSubmit = (values: { rich_parameter_values: WorkspaceBuildParameter[]; }) => { @@ -310,7 +323,7 @@ const WorkspaceParametersPage: FC = () => { templateVersionId={templateVersionId} workspace={workspace} autofillParameters={autofillParameters} - canChangeVersions={canChangeVersions} + updatesToActiveVersion={forcedActiveVersion && workspace.outdated} parameters={sortedParams} diagnostics={latestResponse?.diagnostics ?? []} isSubmitting={ diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageView.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageView.tsx index 3a1fd79bb78..cb0497db542 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageView.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageView.tsx @@ -26,7 +26,7 @@ type WorkspaceParametersPageViewProps = { autofillParameters: AutofillBuildParameter[]; parameters: PreviewParameter[]; diagnostics: PreviewParameter["diagnostics"]; - canChangeVersions: boolean; + updatesToActiveVersion: boolean; isSubmitting: boolean; submitLabel: string; onCancel: () => void; @@ -44,7 +44,7 @@ export const WorkspaceParametersPageView: FC< autofillParameters, parameters, diagnostics, - canChangeVersions, + updatesToActiveVersion, isSubmitting, submitLabel, onSubmit, @@ -69,11 +69,6 @@ export const WorkspaceParametersPageView: FC< validateOnBlur: true, }); - const disabled = - workspace.outdated && - workspace.template_require_active_version && - !canChangeVersions; - // Debounce websocket sends to avoid stale responses overwriting // the form while the user is still typing. const { debounced: sendDynamicParamsRequest } = useDebouncedFunction( @@ -142,10 +137,11 @@ export const WorkspaceParametersPageView: FC< return ( <> - {disabled && ( - - The template for this workspace requires automatic updates. Update the - workspace to edit parameters. + {updatesToActiveVersion && ( + + The template for this workspace requires automatic updates. These + parameters are for the latest template version, and submitting them + will update the workspace to it. )} @@ -253,7 +249,6 @@ export const WorkspaceParametersPageView: FC< const parameterField = `rich_parameter_values.${parameterFieldIndex}`; const isDisabled = - disabled || parameter.styling?.disabled || !parameter.mutable || isSubmitting; @@ -282,7 +277,6 @@ export const WorkspaceParametersPageView: FC< type="submit" disabled={ isSubmitting || - disabled || hasUnsyncedParameters || diagnostics.some( (diagnostic) => diagnostic.severity === "error",