mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
fix: allow fixing stale parameters when the active version is required (#30660)
This commit is contained in:
@@ -2,6 +2,7 @@ import { screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import MockServerSocket from "jest-websocket-mock";
|
||||
import { HttpResponse, http } from "msw";
|
||||
import { useSearchParams } from "react-router";
|
||||
import type { MockInstance } from "vitest";
|
||||
import * as apiModule from "#/api/api";
|
||||
import type { Workspace } from "#/api/typesGenerated";
|
||||
@@ -22,9 +23,11 @@ import {
|
||||
MockStartingWorkspace,
|
||||
MockStoppedWorkspace,
|
||||
MockTemplate,
|
||||
MockTemplateVersion2,
|
||||
MockWorkspace,
|
||||
MockWorkspaceBuild,
|
||||
MockWorkspaceBuildDelete,
|
||||
mockApiError,
|
||||
} from "#/testHelpers/entities";
|
||||
import {
|
||||
type RenderWithAuthOptions,
|
||||
@@ -386,6 +389,61 @@ describe("WorkspacePage", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("sends users who must use the active version to its parameters when a retry fails", async () => {
|
||||
const workspace: Workspace = {
|
||||
...MockFailedWorkspace,
|
||||
outdated: true,
|
||||
template_require_active_version: true,
|
||||
template_active_version_id: MockTemplateVersion2.id,
|
||||
};
|
||||
server.use(
|
||||
http.post("/api/v2/authcheck", () => {
|
||||
const permissions: WorkspacePermissions = {
|
||||
readWorkspace: true,
|
||||
shareWorkspace: true,
|
||||
updateWorkspace: true,
|
||||
updateWorkspaceVersion: false,
|
||||
deleteFailedWorkspace: false,
|
||||
};
|
||||
return HttpResponse.json(permissions);
|
||||
}),
|
||||
);
|
||||
vi.spyOn(API, "retryWorkspace").mockRejectedValueOnce({
|
||||
...mockApiError({ message: "cannot use non-active version" }),
|
||||
code: "ERR_BAD_REQUEST",
|
||||
});
|
||||
|
||||
const ParametersPage: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
return (
|
||||
<div>Parameters for {searchParams.get("templateVersionId")}</div>
|
||||
);
|
||||
};
|
||||
await renderWorkspacePage(workspace, {
|
||||
extraRoutes: [
|
||||
{
|
||||
path: "/:username/:workspace/settings/parameters",
|
||||
element: <ParametersPage />,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
const workspaceActions = screen.getByTestId("workspace-actions");
|
||||
await user.click(
|
||||
within(workspaceActions).getByRole("button", { name: retryButtonRe }),
|
||||
);
|
||||
await user.click(
|
||||
await screen.findByRole("button", {
|
||||
name: "Review workspace settings",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByText(`Parameters for ${MockTemplateVersion2.id}`),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Navigation to other pages", () => {
|
||||
|
||||
@@ -92,6 +92,15 @@ export const WorkspaceReadyPage: React.FC<WorkspaceReadyPageProps> = ({
|
||||
ephemeralParameters: TypesGen.TemplateVersionParameter[];
|
||||
}>({ open: false, action: "start", ephemeralParameters: [] });
|
||||
|
||||
// Users who must stay on the active version can only fix parameters
|
||||
// against the active version, so send them there from the error dialog.
|
||||
const parametersVersionId =
|
||||
workspace.outdated &&
|
||||
workspace.template_require_active_version &&
|
||||
!permissions.updateWorkspaceVersion
|
||||
? workspace.template_active_version_id
|
||||
: workspace.latest_build.template_version_id;
|
||||
|
||||
const [isCancelConfirmOpen, setIsCancelConfirmOpen] = useState(false);
|
||||
|
||||
const { mutate: mutateRestartWorkspace, isPending: isRestarting } =
|
||||
@@ -437,7 +446,7 @@ export const WorkspaceReadyPage: React.FC<WorkspaceReadyPageProps> = ({
|
||||
onClose={() => setWorkspaceErrorDialog({ open: false })}
|
||||
workspaceOwner={workspace.owner_name}
|
||||
workspaceName={workspace.name}
|
||||
templateVersionId={workspace.latest_build.template_version_id}
|
||||
templateVersionId={parametersVersionId}
|
||||
isDeleting={false}
|
||||
/>
|
||||
</>
|
||||
|
||||
+46
@@ -1,9 +1,11 @@
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { act } from "react";
|
||||
import { API } from "#/api/api";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { createDeferred } from "#/testHelpers/deferred";
|
||||
import {
|
||||
MockOutdatedStoppedWorkspaceRequireActiveVersion,
|
||||
MockPreviewParameter1,
|
||||
MockPreviewParameter2,
|
||||
MockPreviewParameter4,
|
||||
@@ -269,6 +271,50 @@ describe("WorkspaceParametersPage", () => {
|
||||
await waitFor(() => expect(submitButton).toBeDisabled());
|
||||
});
|
||||
|
||||
it("allows updating to a required active version without permission to change versions", async () => {
|
||||
const workspace = MockOutdatedStoppedWorkspaceRequireActiveVersion;
|
||||
vi.mocked(API.getWorkspaceByOwnerAndName)
|
||||
.mockReset()
|
||||
.mockResolvedValue(workspace);
|
||||
vi.spyOn(API, "checkAuthorization").mockResolvedValue({
|
||||
readWorkspace: true,
|
||||
shareWorkspace: true,
|
||||
updateWorkspace: true,
|
||||
updateWorkspaceVersion: false,
|
||||
deleteFailedWorkspace: false,
|
||||
});
|
||||
vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([]);
|
||||
|
||||
const [, mockPublisher] = mockDynamicParameterWebSocket();
|
||||
|
||||
renderWorkspaceParametersPage(
|
||||
`/@${workspace.owner_name}/${workspace.name}/settings?templateVersionId=${workspace.template_active_version_id}`,
|
||||
);
|
||||
|
||||
await connectWithInitialParameters(mockPublisher, [
|
||||
MockPreviewParameter1,
|
||||
MockPreviewParameter4,
|
||||
MockPreviewParameter7,
|
||||
]);
|
||||
await waitForLoaderToBeRemoved();
|
||||
|
||||
const form = screen.getByTestId("form");
|
||||
const submitButton = within(form).getByRole("button", {
|
||||
name: /update and start/i,
|
||||
});
|
||||
await waitFor(() => expect(submitButton).toBeEnabled());
|
||||
|
||||
await userEvent.click(submitButton);
|
||||
await waitFor(() =>
|
||||
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(
|
||||
workspace.id,
|
||||
expect.objectContaining({
|
||||
template_version_id: workspace.template_active_version_id,
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not clobber edited parameters", async () => {
|
||||
vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([]);
|
||||
|
||||
|
||||
+5
-1
@@ -68,10 +68,14 @@ export const WorkspaceParametersPageView: React.FC<
|
||||
validateOnBlur: true,
|
||||
});
|
||||
|
||||
// Users who must stay on the active version can still edit parameters
|
||||
// when the form targets the active version, since submitting it is the
|
||||
// update the template requires.
|
||||
const disabled =
|
||||
workspace.outdated &&
|
||||
workspace.template_require_active_version &&
|
||||
!canChangeVersions;
|
||||
!canChangeVersions &&
|
||||
templateVersionId !== workspace.template_active_version_id;
|
||||
|
||||
// Debounce websocket sends to avoid stale responses overwriting
|
||||
// the form while the user is still typing.
|
||||
|
||||
Reference in New Issue
Block a user