diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.stories.tsx index 91d00017296f7..d31290035581a 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 83ee2589ff98c..fdbf41ca2a419 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 4b27633f69e8c..00f62e436b3d6 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 3a1fd79bb7879..cb0497db542ce 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",