Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ba41ae8
add support for canceling workspace builds with expect_state param (l…
kacpersaw Jul 1, 2025
c4ee5b6
Use single transaction for canceling workspace build
kacpersaw Jul 2, 2025
c49c33e
Fix lint problem in ut
kacpersaw Jul 2, 2025
ba1dbf3
add cancel confirmation dialog for workspace builds and add expect_st…
kacpersaw Jul 2, 2025
acffda6
Fix lint
kacpersaw Jul 2, 2025
b672d76
Merge branch 'main' into kacpersaw/cancel-pending-provisioner-jobs
kacpersaw Jul 2, 2025
86a34df
Apply review suggestions
kacpersaw Jul 3, 2025
42170ab
Fix unit test
kacpersaw Jul 3, 2025
5db9d71
Apply review suggestions
kacpersaw Jul 3, 2025
1ede20c
Fix typo
kacpersaw Jul 3, 2025
2597615
Regenerate api types
kacpersaw Jul 3, 2025
6c2d0cf
Fix typo
kacpersaw Jul 3, 2025
c800494
Merge branch 'main' into kacpersaw/cancel-pending-provisioner-jobs
kacpersaw Jul 3, 2025
c5cb203
Apply a new authorization check for GetProvisionerJobByIDForUpdate
kacpersaw Jul 6, 2025
1de84cc
Apply a new authorization check for GetProvisionerJobByIDForUpdate
kacpersaw Jul 6, 2025
17fb6a3
Apply FE review suggestions
kacpersaw Jul 6, 2025
1b7b614
Apply review suggestions
kacpersaw Jul 7, 2025
634f556
Refactor cancelWorkspaceBuild parameter handling
kacpersaw Jul 7, 2025
4deace0
Fix lint
kacpersaw Jul 7, 2025
43430fa
Update coderd/workspacebuilds.go
kacpersaw Jul 7, 2025
6272d93
Extract cancel confirm dialog to a separate component
kacpersaw Jul 7, 2025
4d4a01d
Merge branch 'main' into kacpersaw/cancel-pending-provisioner-jobs
kacpersaw Jul 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
add cancel confirmation dialog for workspace builds and add expect_st…
…atus for pending builds
  • Loading branch information
kacpersaw committed Jul 2, 2025
commit ba1dbf3268675fe8bed83487e8fdbb136ed7ce41
7 changes: 6 additions & 1 deletion site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1277,9 +1277,14 @@ class ApiMethods {

cancelWorkspaceBuild = async (
workspaceBuildId: TypesGen.WorkspaceBuild["id"],
request?: TypesGen.CancelWorkspaceBuildRequest,
): Promise<TypesGen.Response> => {
const params = request?.expect_status
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do:

const params = new URLSearchParams(params)

? new URLSearchParams({ expect_status: request.expect_status }).toString()
: "";

const response = await this.axios.patch(
`/api/v2/workspacebuilds/${workspaceBuildId}/cancel`,
`/api/v2/workspacebuilds/${workspaceBuildId}/cancel${params ? `?${params}` : ""}`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And just use it directly:

`/api/v2/workspacebuilds/${workspaceBuildId}/cancel?${params}`

);

return response.data;
Expand Down
6 changes: 6 additions & 0 deletions site/src/api/queries/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,12 @@ export const startWorkspace = (
export const cancelBuild = (workspace: Workspace, queryClient: QueryClient) => {
return {
mutationFn: () => {
// If workspace status is pending, include expect_status parameter
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is just saying what the code is doing below. I would rather not having this comment at all, since I can see it in the code, or explaining the why we need to set the expect_status to pending.

if (workspace.latest_build.status === "pending") {
return API.cancelWorkspaceBuild(workspace.latest_build.id, {
expect_status: "pending",
});
}
return API.cancelWorkspaceBuild(workspace.latest_build.id);
},
onSuccess: async () => {
Expand Down
10 changes: 9 additions & 1 deletion site/src/api/typesGenerated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion site/src/modules/workspaces/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export const abilitiesByWorkspaceStatus = (
case "pending": {
return {
actions: ["pending"],
canCancel: false,
canCancel: true,
canAcceptJobs: false,
};
}
Expand Down
45 changes: 44 additions & 1 deletion site/src/pages/WorkspacePage/WorkspacePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
MockFailedWorkspace,
MockOrganization,
MockOutdatedWorkspace,
MockPendingWorkspace,
MockStartingWorkspace,
MockStoppedWorkspace,
MockTemplate,
Expand Down Expand Up @@ -223,11 +224,53 @@ describe("WorkspacePage", () => {
}),
);

const user = userEvent.setup({ delay: 0 });
const cancelWorkspaceMock = jest
.spyOn(API, "cancelWorkspaceBuild")
.mockImplementation(() => Promise.resolve({ message: "job canceled" }));
await renderWorkspacePage(MockStartingWorkspace);

// Click on Cancel
const cancelButton = await screen.findByRole("button", { name: "Cancel" });
await user.click(cancelButton);

// Get dialog and confirm
const dialog = await screen.findByTestId("dialog");
const confirmButton = within(dialog).getByRole("button", {
name: "Confirm",
hidden: false,
});
await user.click(confirmButton);

expect(cancelWorkspaceMock).toHaveBeenCalledWith(MockStartingWorkspace.latest_build.id);
});

it("requests cancellation when the user presses Cancel and the workspace is pending", async () => {
server.use(
http.get("/api/v2/users/:userId/workspace/:workspaceName", () => {
return HttpResponse.json(MockPendingWorkspace);
}),
);

const user = userEvent.setup({ delay: 0 });
const cancelWorkspaceMock = jest
.spyOn(API, "cancelWorkspaceBuild")
.mockImplementation(() => Promise.resolve({ message: "job canceled" }));
await renderWorkspacePage(MockPendingWorkspace);

// Click on Cancel
const cancelButton = await screen.findByRole("button", { name: "Cancel" });
await user.click(cancelButton);

// Get dialog and confirm
const dialog = await screen.findByTestId("dialog");
const confirmButton = within(dialog).getByRole("button", {
name: "Confirm",
hidden: false,
});
await user.click(confirmButton);

await testButton(MockStartingWorkspace, "Cancel", cancelWorkspaceMock);
expect(cancelWorkspaceMock).toHaveBeenCalledWith(MockPendingWorkspace.latest_build.id, { expect_status: "pending" });
});

it("requests an update when the user presses Update", async () => {
Expand Down
19 changes: 18 additions & 1 deletion site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
ephemeralParameters: TypesGen.TemplateVersionParameter[];
}>({ open: false, action: "start", ephemeralParameters: [] });

const [isCancelConfirmOpen, setIsCancelConfirmOpen] = useState(false);

const { mutate: mutateRestartWorkspace, isPending: isRestarting } =
useMutation({
mutationFn: API.restartWorkspace,
Expand Down Expand Up @@ -316,7 +318,7 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
}
}}
handleUpdate={workspaceUpdate.update}
handleCancel={cancelBuildMutation.mutate}
handleCancel={() => setIsCancelConfirmOpen(true)}
handleRetry={handleRetry}
handleDebug={handleDebug}
handleDormantActivate={async () => {
Expand Down Expand Up @@ -352,6 +354,21 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
}
/>

{/* Cancel confirmation dialog */}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this comment. The code below is pretty easy to understand.

<ConfirmDialog
open={isCancelConfirmOpen}
title="Cancel workspace build"
description={`Are you sure you want to cancel the build for workspace "${workspace.name}"? This will stop the current build process.`}
confirmText="Confirm"
cancelText="Discard"
onClose={() => setIsCancelConfirmOpen(false)}
onConfirm={() => {
cancelBuildMutation.mutate();
setIsCancelConfirmOpen(false);
}}
type="delete"
/>

<EphemeralParametersDialog
open={ephemeralParametersDialog.open}
onClose={() =>
Expand Down
19 changes: 18 additions & 1 deletion site/src/pages/WorkspacesPage/WorkspacesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@ const WorkspaceActionsCell: FC<WorkspaceActionsCellProps> = ({

// State for stop confirmation dialog
const [isStopConfirmOpen, setIsStopConfirmOpen] = useState(false);
// State for cancel confirmation dialog
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same here, even seeing we do this for the state above.

const [isCancelConfirmOpen, setIsCancelConfirmOpen] = useState(false);

const isRetrying =
startWorkspaceMutation.isPending ||
Expand Down Expand Up @@ -606,7 +608,7 @@ const WorkspaceActionsCell: FC<WorkspaceActionsCellProps> = ({

{abilities.canCancel && (
<PrimaryAction
onClick={cancelBuildMutation.mutate}
onClick={() => setIsCancelConfirmOpen(true)}
isLoading={cancelBuildMutation.isPending}
label="Cancel build"
>
Expand Down Expand Up @@ -643,6 +645,21 @@ const WorkspaceActionsCell: FC<WorkspaceActionsCellProps> = ({
}}
type="delete"
/>

{/* Cancel workspace build confirmation dialog */}
<ConfirmDialog
open={isCancelConfirmOpen}
title="Cancel workspace build"
description={`Are you sure you want to cancel the build for workspace "${workspace.name}"? This will stop the current build process.`}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice if this could differ based on whether it thinks it's going to cancel a pending or running build.

e.g. This will remove the current build from the build queue.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with the Dialog on the other page too, maybe it should be made into a component?

confirmText="Confirm"
cancelText="Discard"
onClose={() => setIsCancelConfirmOpen(false)}
onConfirm={() => {
cancelBuildMutation.mutate();
setIsCancelConfirmOpen(false);
}}
type="delete"
/>
</TableCell>
);
};
Expand Down
Loading