-
Notifications
You must be signed in to change notification settings - Fork 972
Description
Problem
As mentioned by @alexlorenz-coder, deleting hundreds of stopped workspaces currently requires many individual clicks, which is time-consuming and frustrating. Users need to click multiple times per workspace to delete them, and when dealing with 600+ stopped workspaces, this becomes a significant usability issue.
Proposed Solution
Add a "Delete all stopped" button to the workspace bulk actions menu that allows users to quickly delete all stopped workspaces with a single action.
Implementation
I've prepared a complete implementation that adds:
- New menu item: "Delete all stopped" in the bulk actions dropdown
- Smart filtering: Only deletes workspaces with status "stopped"
- Multi-stage confirmation: Similar to existing batch delete, with preview of affected workspaces
- Resource preview: Shows which resources will be deleted before confirmation
Changes Required
- WorkspacesPageView.tsx: Add new
onBatchDeleteStoppedTransition
handler and menu item - WorkspacesPage.tsx: Add logic to filter stopped workspaces and handle the new action
- BatchDeleteStoppedConfirmation.tsx: New component for confirming mass deletion of stopped workspaces
Code Implementation
The implementation is ready and tested. Here are the key changes:
1. New BatchDeleteStoppedConfirmation Component
Create site/src/pages/WorkspacesPage/BatchDeleteStoppedConfirmation.tsx
:
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import { visuallyHidden } from "@mui/utils";
import type { Workspace } from "api/typesGenerated";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { Stack } from "components/Stack/Stack";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { ClockIcon, UserIcon } from "lucide-react";
import { type FC, type ReactNode, useState } from "react";
import { getResourceIconPath } from "utils/workspace";
dayjs.extend(relativeTime);
type BatchDeleteStoppedConfirmationProps = {
stoppedWorkspaces: readonly Workspace[];
open: boolean;
isLoading: boolean;
onClose: () => void;
onConfirm: () => void;
};
export const BatchDeleteStoppedConfirmation: FC<BatchDeleteStoppedConfirmationProps> = ({
stoppedWorkspaces,
open,
onClose,
onConfirm,
isLoading,
}) => {
const [stage, setStage] = useState<
"consequences" | "workspaces" | "resources"
>("consequences");
const onProceed = () => {
switch (stage) {
case "resources":
onConfirm();
break;
case "workspaces":
setStage("resources");
break;
case "consequences":
setStage("workspaces");
break;
}
};
const workspaceCount = `${stoppedWorkspaces.length} stopped ${
stoppedWorkspaces.length === 1 ? "workspace" : "workspaces"
}`;
// ... rest of component implementation
return (
<ConfirmDialog
type="delete"
open={open}
onClose={handleClose}
title={`Delete all stopped workspaces`}
onConfirm={onProceed}
confirmLoading={isLoading}
confirmText={
stage === "consequences"
? "Next"
: stage === "workspaces"
? "Next"
: "Delete all"
}
description={
<>
{stage === "consequences" && (
<ConsequencesStage workspaceCount={workspaceCount} />
)}
{stage === "workspaces" && (
<WorkspacesStage workspaces={stoppedWorkspaces} />
)}
{stage === "resources" && (
<ResourcesStage workspaces={stoppedWorkspaces} />
)}
</>
}
/>
);
};
2. Update WorkspacesPageView.tsx
Add the new menu item and handler:
// Add Trash2Icon to imports
import {
PlayIcon,
SquareIcon,
ArrowUpIcon,
TrashIcon,
ChevronDownIcon,
Trash2Icon,
} from "lucide-react";
// Add new prop to interface
interface WorkspacesPageViewProps {
// ... existing props
onBatchDeleteStoppedTransition: () => void;
}
// In the dropdown menu, after the existing Delete item:
<DropdownMenuItem
className="text-content-destructive focus:text-content-destructive"
onClick={onBatchDeleteTransition}
>
<TrashIcon /> Delete selected…
</DropdownMenuItem>
<DropdownMenuItem
className="text-content-destructive focus:text-content-destructive"
onClick={onBatchDeleteStoppedTransition}
disabled={
!workspaces?.some(
(w) => w.latest_build.status === "stopped",
)
}
>
<Trash2Icon /> Delete all stopped…
</DropdownMenuItem>
3. Update WorkspacesPage.tsx
Add the handler and confirmation dialog:
import { BatchDeleteStoppedConfirmation } from "./BatchDeleteStoppedConfirmation";
// In the component:
const [isBatchDeleteStoppedModalOpen, setIsBatchDeleteStoppedModalOpen] = useState(false);
const stoppedWorkspaces = workspaces?.filter(
(w) => w.latest_build.status === "stopped",
);
// In the return statement:
<WorkspacesPageView
// ... existing props
onBatchDeleteStoppedTransition={() => setIsBatchDeleteStoppedModalOpen(true)}
/>
<BatchDeleteStoppedConfirmation
isLoading={batchActions.isProcessing}
stoppedWorkspaces={stoppedWorkspaces || []}
open={isBatchDeleteStoppedModalOpen}
onClose={() => setIsBatchDeleteStoppedModalOpen(false)}
onConfirm={async () => {
if (stoppedWorkspaces) {
await batchActions.delete(stoppedWorkspaces);
setIsBatchDeleteStoppedModalOpen(false);
setCheckedWorkspaceIds([]);
}
}}
/>
Benefits
- Time-saving: Reduces hundreds of clicks to just a few
- User-friendly: Clear multi-stage confirmation prevents accidental deletions
- Cost-effective: Helps users quickly clean up unused resources
- Consistent UX: Follows existing batch action patterns in Coder
Testing
The implementation has been tested to ensure:
- Only stopped workspaces are selected for deletion
- The confirmation dialog shows accurate counts and workspace details
- The action integrates with existing batch action infrastructure
- The button is disabled when no stopped workspaces exist
Request
Could the Coder team review this implementation and consider adding this feature? I have the complete working code ready and can provide a patch file if needed.
cc: @code-asher @bpmct (tagging some Coder team members for visibility)