From 1d587bcdfd870adacece5e42cbdf0d42b96996f3 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Wed, 29 Jul 2026 12:35:32 +1000 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=A4=96=20feat(WorkspacesPage):=20add?= =?UTF-8?q?=20batch=20stop=20confirmation=20dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WorkspacesPage/BatchStopConfirmation.tsx | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 site/src/pages/WorkspacesPage/BatchStopConfirmation.tsx diff --git a/site/src/pages/WorkspacesPage/BatchStopConfirmation.tsx b/site/src/pages/WorkspacesPage/BatchStopConfirmation.tsx new file mode 100644 index 0000000000000..cacbc07f681e5 --- /dev/null +++ b/site/src/pages/WorkspacesPage/BatchStopConfirmation.tsx @@ -0,0 +1,36 @@ +import type { FC } from "react"; +import type { Workspace } from "#/api/typesGenerated"; +import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog"; + +type BatchStopConfirmationProps = { + checkedWorkspaces: readonly Workspace[]; + open: boolean; + isLoading: boolean; + onClose: () => void; + onConfirm: () => void; +}; + +export const BatchStopConfirmation: FC = ({ + checkedWorkspaces, + open, + onClose, + onConfirm, + isLoading, +}) => { + const workspaceCount = `${checkedWorkspaces.length} ${ + checkedWorkspaces.length === 1 ? "workspace" : "workspaces" + }`; + + return ( + + ); +}; From 7a92a9bffa8b0c5d7fa18cada5b3d0f46c811ecc Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Wed, 29 Jul 2026 12:36:40 +1000 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=A4=96=20feat(WorkspacesPage):=20conf?= =?UTF-8?q?irm=20before=20batch=20stopping=20workspaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/pages/WorkspacesPage/WorkspacesPage.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 2c08c716e85ba..f2464ba7736f9 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -23,6 +23,7 @@ import { useOrganizationsFilterMenu } from "#/modules/tableFiltering/options"; import { ACTIVE_BUILD_STATUSES } from "#/modules/workspaces/status"; import { pageTitle } from "#/utils/page"; import { BatchDeleteConfirmation } from "./BatchDeleteConfirmation"; +import { BatchStopConfirmation } from "./BatchStopConfirmation"; import { BatchUpdateModalForm } from "./BatchUpdateModalForm"; import { useBatchActions } from "./batchActions"; import { useStatusFilterMenu, useTemplateFilterMenu } from "./filter/menus"; @@ -52,7 +53,7 @@ function useSafeSearchParams() { >; } -type BatchAction = "delete" | "update"; +type BatchAction = "delete" | "stop" | "update"; const WorkspacesPage: FC = () => { const queryClient = useQueryClient(); @@ -197,7 +198,7 @@ const WorkspacesPage: FC = () => { isRunningBatchAction={batchActions.isProcessing} onBatchDeleteTransition={() => setActiveBatchAction("delete")} onBatchStartTransition={() => batchActions.start(checkedWorkspaces)} - onBatchStopTransition={() => batchActions.stop(checkedWorkspaces)} + onBatchStopTransition={() => setActiveBatchAction("stop")} onBatchUpdateTransition={() => { // Just because batch-updating can be really dangerous // action for running workspaces, we're going to invalidate @@ -241,6 +242,17 @@ const WorkspacesPage: FC = () => { }} /> + setActiveBatchAction(undefined)} + onConfirm={async () => { + await batchActions.stop(checkedWorkspaces); + setActiveBatchAction(undefined); + }} + /> + Date: Wed, 29 Jul 2026 12:49:27 +1000 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=A4=96=20test(WorkspacesPage):=20conf?= =?UTF-8?q?irm=20batch=20stop=20dialog=20in=20stories?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx index 0fec96ff6da47..12208e55bf2ea 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx @@ -433,6 +433,9 @@ export const StopsOnlySelectedWorkspaces: Story = { await openBulkActions(canvas, user); await user.click(await body.findByRole("menuitem", { name: /stop/i })); + const confirmButton = await body.findByTestId("confirm-button"); + await user.click(confirmButton); + await waitFor(() => expect(API.stopWorkspace).toHaveBeenCalledTimes(2)); expect(API.stopWorkspace).toHaveBeenCalledWith("1"); expect(API.stopWorkspace).toHaveBeenCalledWith("2"); @@ -532,6 +535,9 @@ export const StopIgnoresAlreadyStoppedWorkspaces: Story = { expect(stopItem).not.toHaveAttribute("data-disabled"); await user.click(stopItem); + const confirmButton = await body.findByTestId("confirm-button"); + await user.click(confirmButton); + await waitFor(() => expect(API.stopWorkspace).toHaveBeenCalledTimes(1)); expect(API.stopWorkspace).toHaveBeenCalledWith("2"); }, From 17610627a0e1f42877dde73e7ac02828679a2f54 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Wed, 29 Jul 2026 14:03:22 +1000 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=A4=96=20refactor(BatchStopConfirmati?= =?UTF-8?q?on):=20count=20only=20stoppable=20workspaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/pages/WorkspacesPage/BatchStopConfirmation.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/pages/WorkspacesPage/BatchStopConfirmation.tsx b/site/src/pages/WorkspacesPage/BatchStopConfirmation.tsx index cacbc07f681e5..6cee45d14550d 100644 --- a/site/src/pages/WorkspacesPage/BatchStopConfirmation.tsx +++ b/site/src/pages/WorkspacesPage/BatchStopConfirmation.tsx @@ -3,7 +3,7 @@ import type { Workspace } from "#/api/typesGenerated"; import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog"; type BatchStopConfirmationProps = { - checkedWorkspaces: readonly Workspace[]; + workspacesToStop: readonly Workspace[]; open: boolean; isLoading: boolean; onClose: () => void; @@ -11,14 +11,14 @@ type BatchStopConfirmationProps = { }; export const BatchStopConfirmation: FC = ({ - checkedWorkspaces, + workspacesToStop, open, onClose, onConfirm, isLoading, }) => { - const workspaceCount = `${checkedWorkspaces.length} ${ - checkedWorkspaces.length === 1 ? "workspace" : "workspaces" + const workspaceCount = `${workspacesToStop.length} ${ + workspacesToStop.length === 1 ? "workspace" : "workspaces" }`; return ( From 908350303b54a79148bc9e9ea94ac43de922ab81 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Wed, 29 Jul 2026 14:04:26 +1000 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=A4=96=20refactor(WorkspacesPage):=20?= =?UTF-8?q?stop=20only=20running=20workspaces=20in=20bulk=20stop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/pages/WorkspacesPage/WorkspacesPage.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index f2464ba7736f9..1712f2ea7f2bd 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -164,6 +164,13 @@ const WorkspacesPage: FC = () => { const checkedWorkspaces = data?.workspaces.filter((w) => checkedWorkspaceIds.has(w.id)) ?? []; + // Bulk stop only affects running workspaces, so the confirmation dialog and + // the mutation should both operate on that subset to avoid over-reporting + // how many workspaces will actually be stopped. + const workspacesToStop = checkedWorkspaces.filter( + (w) => w.latest_build.status === "running", + ); + return ( <> Codestin Search App @@ -244,11 +251,11 @@ const WorkspacesPage: FC = () => { setActiveBatchAction(undefined)} onConfirm={async () => { - await batchActions.stop(checkedWorkspaces); + await batchActions.stop(workspacesToStop); setActiveBatchAction(undefined); }} /> From 36f1c582b866306639435ebed186132db7b8e04b Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Wed, 29 Jul 2026 14:06:23 +1000 Subject: [PATCH 6/9] =?UTF-8?q?=F0=9F=A4=96=20test(WorkspacesPage):=20quer?= =?UTF-8?q?y=20batch=20stop=20dialog=20button=20by=20role?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx index 12208e55bf2ea..cf5ff0ab72bff 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx @@ -433,8 +433,8 @@ export const StopsOnlySelectedWorkspaces: Story = { await openBulkActions(canvas, user); await user.click(await body.findByRole("menuitem", { name: /stop/i })); - const confirmButton = await body.findByTestId("confirm-button"); - await user.click(confirmButton); + const dialog = await body.findByRole("dialog"); + await user.click(within(dialog).getByRole("button", { name: "Stop" })); await waitFor(() => expect(API.stopWorkspace).toHaveBeenCalledTimes(2)); expect(API.stopWorkspace).toHaveBeenCalledWith("1"); @@ -535,8 +535,8 @@ export const StopIgnoresAlreadyStoppedWorkspaces: Story = { expect(stopItem).not.toHaveAttribute("data-disabled"); await user.click(stopItem); - const confirmButton = await body.findByTestId("confirm-button"); - await user.click(confirmButton); + const dialog = await body.findByRole("dialog"); + await user.click(within(dialog).getByRole("button", { name: "Stop" })); await waitFor(() => expect(API.stopWorkspace).toHaveBeenCalledTimes(1)); expect(API.stopWorkspace).toHaveBeenCalledWith("2"); From e6a0affbebf2e404344a96dfaef461e9623b0694 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Wed, 29 Jul 2026 14:06:58 +1000 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=A4=96=20docs(workspace-management):?= =?UTF-8?q?=20note=20bulk=20stop=20confirmation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/user-guides/workspace-management.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/user-guides/workspace-management.md b/docs/user-guides/workspace-management.md index dd30127452956..0823ab65c0740 100644 --- a/docs/user-guides/workspace-management.md +++ b/docs/user-guides/workspace-management.md @@ -110,8 +110,10 @@ operation. The start and stop operations can be applied even when the selected workspaces are not all in the same state. Bulk start will only apply to selected workspaces that are currently stopped, and bulk stop will only apply to selected workspaces -that are currently running. For update and delete, the user will be prompted for -confirmation before any action is taken. +that are currently running. For update, delete, and stop, the user is prompted +for confirmation before any action is taken. The stop confirmation only counts +the running workspaces in the selection, since those are the only ones that will +be stopped. ![Bulk workspace actions](../images/user-guides/workspace-bulk-actions.png) From 97344f31ab971b67dd9976370a8a41a344e3942f Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Wed, 29 Jul 2026 04:16:39 +0000 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=A4=96=20docs(workspace-management):?= =?UTF-8?q?=20drop=20bulk=20stop=20implementation=20detail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/user-guides/workspace-management.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/user-guides/workspace-management.md b/docs/user-guides/workspace-management.md index 0823ab65c0740..57a5f4ee502ba 100644 --- a/docs/user-guides/workspace-management.md +++ b/docs/user-guides/workspace-management.md @@ -111,9 +111,7 @@ The start and stop operations can be applied even when the selected workspaces are not all in the same state. Bulk start will only apply to selected workspaces that are currently stopped, and bulk stop will only apply to selected workspaces that are currently running. For update, delete, and stop, the user is prompted -for confirmation before any action is taken. The stop confirmation only counts -the running workspaces in the selection, since those are the only ones that will -be stopped. +for confirmation before any action is taken. ![Bulk workspace actions](../images/user-guides/workspace-bulk-actions.png) From 2378960f18ae773ae340b772729ea11271ad6ca5 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Wed, 29 Jul 2026 04:17:41 +0000 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=A4=96=20docs(workspace-management):?= =?UTF-8?q?=20revert=20bulk=20operations=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/user-guides/workspace-management.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user-guides/workspace-management.md b/docs/user-guides/workspace-management.md index 57a5f4ee502ba..dd30127452956 100644 --- a/docs/user-guides/workspace-management.md +++ b/docs/user-guides/workspace-management.md @@ -110,8 +110,8 @@ operation. The start and stop operations can be applied even when the selected workspaces are not all in the same state. Bulk start will only apply to selected workspaces that are currently stopped, and bulk stop will only apply to selected workspaces -that are currently running. For update, delete, and stop, the user is prompted -for confirmation before any action is taken. +that are currently running. For update and delete, the user will be prompted for +confirmation before any action is taken. ![Bulk workspace actions](../images/user-guides/workspace-bulk-actions.png)