Skip to content

feat: Add mass delete action for stopped workspaces #19423

@blink-so

Description

@blink-so

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:

  1. New menu item: "Delete all stopped" in the bulk actions dropdown
  2. Smart filtering: Only deletes workspaces with status "stopped"
  3. Multi-stage confirmation: Similar to existing batch delete, with preview of affected workspaces
  4. Resource preview: Shows which resources will be deleted before confirmation

Changes Required

  1. WorkspacesPageView.tsx: Add new onBatchDeleteStoppedTransition handler and menu item
  2. WorkspacesPage.tsx: Add logic to filter stopped workspaces and handle the new action
  3. 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&hellip;
</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&hellip;
</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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      pFad - Phonifier reborn

      Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

      Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy