mirror of
https://github.com/coder/coder.git
synced 2026-09-22 21:22:17 +08:00
feat(site): confirm before batch stopping workspaces (#27631)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
## What
Bulk stopping workspaces from the workspaces table currently fires
immediately with no confirmation, whereas the single-row **Stop** action
and the bulk **Delete** / **Update** actions all show a dialog first.
This adds a confirmation dialog to the bulk **Stop** action so it
matches the rest.
## How
- Added `BatchStopConfirmation`, a small `ConfirmDialog` wrapper
mirroring the wording of the single-workspace stop confirmation but
pluralized for the selected count.
- Wired it into `WorkspacesPage`: `onBatchStopTransition` now opens the
dialog (`setActiveBatchAction("stop")`) instead of calling
`batchActions.stop(...)` directly, and the actual stop runs on confirm.
Added `"stop"` to the `BatchAction` union.
No change to the underlying `batchActions.stop` behavior (still only
stops `running` workspaces).
<details>
<summary>Reviewer notes</summary>
Before: `onBatchStopTransition={() =>
batchActions.stop(checkedWorkspaces)}` — no confirmation.
After: opens `BatchStopConfirmation`; confirm calls
`batchActions.stop(checkedWorkspaces)` then clears the active action,
consistent with how `BatchDeleteConfirmation` and `BatchUpdateModalForm`
are handled.
</details>
---
_Opened as a draft. Disclosure: this PR was generated by Coder Agents on
behalf of @jakehwll._
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import type { FC } from "react";
|
||||
import type { Workspace } from "#/api/typesGenerated";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
|
||||
type BatchStopConfirmationProps = {
|
||||
workspacesToStop: readonly Workspace[];
|
||||
open: boolean;
|
||||
isLoading: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
export const BatchStopConfirmation: FC<BatchStopConfirmationProps> = ({
|
||||
workspacesToStop,
|
||||
open,
|
||||
onClose,
|
||||
onConfirm,
|
||||
isLoading,
|
||||
}) => {
|
||||
const workspaceCount = `${workspacesToStop.length} ${
|
||||
workspacesToStop.length === 1 ? "workspace" : "workspaces"
|
||||
}`;
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
type="delete"
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={`Stop ${workspaceCount}`}
|
||||
confirmLoading={isLoading}
|
||||
confirmText="Stop"
|
||||
onConfirm={onConfirm}
|
||||
description={`Are you sure you want to stop ${workspaceCount}? This will terminate all running processes and disconnect any active sessions.`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -433,6 +433,9 @@ export const StopsOnlySelectedWorkspaces: Story = {
|
||||
await openBulkActions(canvas, user);
|
||||
await user.click(await body.findByRole("menuitem", { name: /stop/i }));
|
||||
|
||||
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");
|
||||
expect(API.stopWorkspace).toHaveBeenCalledWith("2");
|
||||
@@ -532,6 +535,9 @@ export const StopIgnoresAlreadyStoppedWorkspaces: Story = {
|
||||
expect(stopItem).not.toHaveAttribute("data-disabled");
|
||||
await user.click(stopItem);
|
||||
|
||||
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");
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
@@ -163,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 (
|
||||
<>
|
||||
<title>{pageTitle("Workspaces")}</title>
|
||||
@@ -197,7 +205,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 +249,17 @@ const WorkspacesPage: FC = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<BatchStopConfirmation
|
||||
isLoading={batchActions.isProcessing}
|
||||
workspacesToStop={workspacesToStop}
|
||||
open={activeBatchAction === "stop"}
|
||||
onClose={() => setActiveBatchAction(undefined)}
|
||||
onConfirm={async () => {
|
||||
await batchActions.stop(workspacesToStop);
|
||||
setActiveBatchAction(undefined);
|
||||
}}
|
||||
/>
|
||||
|
||||
<BatchUpdateModalForm
|
||||
open={activeBatchAction === "update"}
|
||||
workspacesToUpdate={checkedWorkspaces}
|
||||
|
||||
Reference in New Issue
Block a user