Skip to content

Commit ba1dbf3

Browse files
committed
add cancel confirmation dialog for workspace builds and add expect_status for pending builds
1 parent c49c33e commit ba1dbf3

File tree

7 files changed

+102
-6
lines changed

7 files changed

+102
-6
lines changed

site/src/api/api.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1277,9 +1277,14 @@ class ApiMethods {
12771277

12781278
cancelWorkspaceBuild = async (
12791279
workspaceBuildId: TypesGen.WorkspaceBuild["id"],
1280+
request?: TypesGen.CancelWorkspaceBuildRequest,
12801281
): Promise<TypesGen.Response> => {
1282+
const params = request?.expect_status
1283+
? new URLSearchParams({ expect_status: request.expect_status }).toString()
1284+
: "";
1285+
12811286
const response = await this.axios.patch(
1282-
`/api/v2/workspacebuilds/${workspaceBuildId}/cancel`,
1287+
`/api/v2/workspacebuilds/${workspaceBuildId}/cancel${params ? `?${params}` : ""}`,
12831288
);
12841289

12851290
return response.data;

site/src/api/queries/workspaces.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,12 @@ export const startWorkspace = (
266266
export const cancelBuild = (workspace: Workspace, queryClient: QueryClient) => {
267267
return {
268268
mutationFn: () => {
269+
// If workspace status is pending, include expect_status parameter
270+
if (workspace.latest_build.status === "pending") {
271+
return API.cancelWorkspaceBuild(workspace.latest_build.id, {
272+
expect_status: "pending",
273+
});
274+
}
269275
return API.cancelWorkspaceBuild(workspace.latest_build.id);
270276
},
271277
onSuccess: async () => {

site/src/api/typesGenerated.ts

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/modules/workspaces/actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export const abilitiesByWorkspaceStatus = (
145145
case "pending": {
146146
return {
147147
actions: ["pending"],
148-
canCancel: false,
148+
canCancel: true,
149149
canAcceptJobs: false,
150150
};
151151
}

site/src/pages/WorkspacePage/WorkspacePage.test.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
MockFailedWorkspace,
1919
MockOrganization,
2020
MockOutdatedWorkspace,
21+
MockPendingWorkspace,
2122
MockStartingWorkspace,
2223
MockStoppedWorkspace,
2324
MockTemplate,
@@ -223,11 +224,53 @@ describe("WorkspacePage", () => {
223224
}),
224225
);
225226

227+
const user = userEvent.setup({ delay: 0 });
226228
const cancelWorkspaceMock = jest
227229
.spyOn(API, "cancelWorkspaceBuild")
228230
.mockImplementation(() => Promise.resolve({ message: "job canceled" }));
231+
await renderWorkspacePage(MockStartingWorkspace);
232+
233+
// Click on Cancel
234+
const cancelButton = await screen.findByRole("button", { name: "Cancel" });
235+
await user.click(cancelButton);
236+
237+
// Get dialog and confirm
238+
const dialog = await screen.findByTestId("dialog");
239+
const confirmButton = within(dialog).getByRole("button", {
240+
name: "Confirm",
241+
hidden: false,
242+
});
243+
await user.click(confirmButton);
244+
245+
expect(cancelWorkspaceMock).toHaveBeenCalledWith(MockStartingWorkspace.latest_build.id);
246+
});
247+
248+
it("requests cancellation when the user presses Cancel and the workspace is pending", async () => {
249+
server.use(
250+
http.get("/api/v2/users/:userId/workspace/:workspaceName", () => {
251+
return HttpResponse.json(MockPendingWorkspace);
252+
}),
253+
);
254+
255+
const user = userEvent.setup({ delay: 0 });
256+
const cancelWorkspaceMock = jest
257+
.spyOn(API, "cancelWorkspaceBuild")
258+
.mockImplementation(() => Promise.resolve({ message: "job canceled" }));
259+
await renderWorkspacePage(MockPendingWorkspace);
260+
261+
// Click on Cancel
262+
const cancelButton = await screen.findByRole("button", { name: "Cancel" });
263+
await user.click(cancelButton);
264+
265+
// Get dialog and confirm
266+
const dialog = await screen.findByTestId("dialog");
267+
const confirmButton = within(dialog).getByRole("button", {
268+
name: "Confirm",
269+
hidden: false,
270+
});
271+
await user.click(confirmButton);
229272

230-
await testButton(MockStartingWorkspace, "Cancel", cancelWorkspaceMock);
273+
expect(cancelWorkspaceMock).toHaveBeenCalledWith(MockPendingWorkspace.latest_build.id, { expect_status: "pending" });
231274
});
232275

233276
it("requests an update when the user presses Update", async () => {

site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
8080
ephemeralParameters: TypesGen.TemplateVersionParameter[];
8181
}>({ open: false, action: "start", ephemeralParameters: [] });
8282

83+
const [isCancelConfirmOpen, setIsCancelConfirmOpen] = useState(false);
84+
8385
const { mutate: mutateRestartWorkspace, isPending: isRestarting } =
8486
useMutation({
8587
mutationFn: API.restartWorkspace,
@@ -316,7 +318,7 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
316318
}
317319
}}
318320
handleUpdate={workspaceUpdate.update}
319-
handleCancel={cancelBuildMutation.mutate}
321+
handleCancel={() => setIsCancelConfirmOpen(true)}
320322
handleRetry={handleRetry}
321323
handleDebug={handleDebug}
322324
handleDormantActivate={async () => {
@@ -352,6 +354,21 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
352354
}
353355
/>
354356

357+
{/* Cancel confirmation dialog */}
358+
<ConfirmDialog
359+
open={isCancelConfirmOpen}
360+
title="Cancel workspace build"
361+
description={`Are you sure you want to cancel the build for workspace "${workspace.name}"? This will stop the current build process.`}
362+
confirmText="Confirm"
363+
cancelText="Discard"
364+
onClose={() => setIsCancelConfirmOpen(false)}
365+
onConfirm={() => {
366+
cancelBuildMutation.mutate();
367+
setIsCancelConfirmOpen(false);
368+
}}
369+
type="delete"
370+
/>
371+
355372
<EphemeralParametersDialog
356373
open={ephemeralParametersDialog.open}
357374
onClose={() =>

site/src/pages/WorkspacesPage/WorkspacesTable.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,8 @@ const WorkspaceActionsCell: FC<WorkspaceActionsCellProps> = ({
497497

498498
// State for stop confirmation dialog
499499
const [isStopConfirmOpen, setIsStopConfirmOpen] = useState(false);
500+
// State for cancel confirmation dialog
501+
const [isCancelConfirmOpen, setIsCancelConfirmOpen] = useState(false);
500502

501503
const isRetrying =
502504
startWorkspaceMutation.isPending ||
@@ -606,7 +608,7 @@ const WorkspaceActionsCell: FC<WorkspaceActionsCellProps> = ({
606608

607609
{abilities.canCancel && (
608610
<PrimaryAction
609-
onClick={cancelBuildMutation.mutate}
611+
onClick={() => setIsCancelConfirmOpen(true)}
610612
isLoading={cancelBuildMutation.isPending}
611613
label="Cancel build"
612614
>
@@ -643,6 +645,21 @@ const WorkspaceActionsCell: FC<WorkspaceActionsCellProps> = ({
643645
}}
644646
type="delete"
645647
/>
648+
649+
{/* Cancel workspace build confirmation dialog */}
650+
<ConfirmDialog
651+
open={isCancelConfirmOpen}
652+
title="Cancel workspace build"
653+
description={`Are you sure you want to cancel the build for workspace "${workspace.name}"? This will stop the current build process.`}
654+
confirmText="Confirm"
655+
cancelText="Discard"
656+
onClose={() => setIsCancelConfirmOpen(false)}
657+
onConfirm={() => {
658+
cancelBuildMutation.mutate();
659+
setIsCancelConfirmOpen(false);
660+
}}
661+
type="delete"
662+
/>
646663
</TableCell>
647664
);
648665
};

0 commit comments

Comments
 (0)