All checks were successful
Admin-only per-row Remove + multi-select bulk removal on the machines view, plus per-row purge Remove on the sessions view, wired to the Task-5 admin API (DELETE /api/machines|sessions/:id?purge=true, POST /api/machines/bulk-remove). Confirm modals (danger-styled, focus-trapped), TanStack refetch so purged rows leave the console, structured ApiError surfacing, honest partial-bulk summary, and admin-gating via useAuth().isAdmin as defense-in-depth over the server 403. Replaces the legacy all-user delete trigger. typecheck/lint/build clean. Implements specs/v2-stable-identity/plan.md Task 5 (dashboard portion). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
113 lines
3.6 KiB
TypeScript
113 lines
3.6 KiB
TypeScript
import { ApiError } from "../../api/client";
|
|
import type { BulkRemoveItem } from "../../api/types";
|
|
import { ConfirmDialog } from "../../components/ui/ConfirmDialog";
|
|
import { useToast } from "../../components/ui/toast-context";
|
|
import { useBulkRemoveMachines } from "./hooks";
|
|
|
|
interface BulkRemoveMachinesDialogProps {
|
|
/** Selected agent_ids to remove, or empty when the dialog is closed. */
|
|
agentIds: string[];
|
|
/** Whether the dialog is open. Kept explicit so an empty list can stay open. */
|
|
open: boolean;
|
|
onClose: () => void;
|
|
/** Called after a successful batch so the page can clear its selection. */
|
|
onRemoved: () => void;
|
|
}
|
|
|
|
/** Count outcomes by status for a compact "12 removed, 1 not found" summary. */
|
|
function summarize(results: BulkRemoveItem[]): string {
|
|
const counts = new Map<string, number>();
|
|
for (const r of results) counts.set(r.status, (counts.get(r.status) ?? 0) + 1);
|
|
const order = ["removed", "not_found", "invalid", "error"];
|
|
const labels: Record<string, string> = {
|
|
removed: "removed",
|
|
not_found: "not found",
|
|
invalid: "invalid",
|
|
error: "errored",
|
|
};
|
|
const parts: string[] = [];
|
|
for (const status of order) {
|
|
const n = counts.get(status);
|
|
if (n) parts.push(`${n} ${labels[status] ?? status}`);
|
|
}
|
|
// Surface any unexpected status the server may add in the future.
|
|
for (const [status, n] of counts) {
|
|
if (!order.includes(status)) parts.push(`${n} ${status}`);
|
|
}
|
|
return parts.join(", ");
|
|
}
|
|
|
|
/**
|
|
* Confirm + bulk-remove the selected machines (Task 5). On confirm the selected
|
|
* agent_ids are purged in one request; the per-id summary the server returns is
|
|
* surfaced as a toast (e.g. "12 removed, 1 not found") so a partial outcome is
|
|
* visible rather than silently swallowed.
|
|
*/
|
|
export function BulkRemoveMachinesDialog({
|
|
agentIds,
|
|
open,
|
|
onClose,
|
|
onRemoved,
|
|
}: BulkRemoveMachinesDialogProps) {
|
|
const toast = useToast();
|
|
const bulkRemove = useBulkRemoveMachines();
|
|
const count = agentIds.length;
|
|
|
|
function onConfirm() {
|
|
if (count === 0) {
|
|
onClose();
|
|
return;
|
|
}
|
|
bulkRemove.mutate(agentIds, {
|
|
onSuccess: (res) => {
|
|
const summary = summarize(res.results);
|
|
if (res.removed === res.requested) {
|
|
toast.success(
|
|
`Removed ${res.removed} ${res.removed === 1 ? "machine" : "machines"}`,
|
|
summary || undefined,
|
|
);
|
|
} else {
|
|
// Partial: some ids were not found / invalid. Report as info, not an
|
|
// error — the requested removals that could happen, did.
|
|
toast.info(
|
|
`Removed ${res.removed} of ${res.requested}`,
|
|
summary || undefined,
|
|
);
|
|
}
|
|
onRemoved();
|
|
onClose();
|
|
},
|
|
onError: (err) => {
|
|
toast.error(
|
|
"Could not remove machines",
|
|
err instanceof ApiError
|
|
? `${err.message}${err.code ? ` (${err.code})` : ""}`
|
|
: "The server did not respond. No machines were removed.",
|
|
);
|
|
},
|
|
});
|
|
}
|
|
|
|
return (
|
|
<ConfirmDialog
|
|
open={open}
|
|
title={`Remove ${count} ${count === 1 ? "machine" : "machines"}?`}
|
|
danger
|
|
busy={bulkRemove.isPending}
|
|
confirmLabel={`Remove ${count}`}
|
|
cancelLabel="Keep machines"
|
|
onConfirm={onConfirm}
|
|
onCancel={onClose}
|
|
body={
|
|
<p style={{ marginTop: 0 }}>
|
|
Remove the {count} selected{" "}
|
|
{count === 1 ? "machine" : "machines"} from the GuruConnect console.
|
|
Their live sessions are dropped and the rows disappear from the list.
|
|
Any that are genuinely still in service re-appear when their agents
|
|
next check in.
|
|
</p>
|
|
}
|
|
/>
|
|
);
|
|
}
|