Phase 9: network management + stack delete in UI (0.9.0)

- Networks: network_service (list w/ subnet/containers/in-use/owning-stack,
  create bridge/macvlan/ipvlan/overlay + optional subnet/gateway/internal,
  delete with default-network guard, prune) + routers/networks.py; real
  Networks page replaces the placeholder.
- Fix: local stacks can now be deleted from the UI — Delete button on stack
  detail (with optional keep-files-on-disk) and a trash action on stack cards,
  via a shared ConfirmDialog. (Backend DELETE existed; no UI surfaced it.)

Verified: py_compile, frontend tsc build, live network list smoke test
(defaults flagged, compose nets + in-use detected); main 104 routes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
menzelj
2026-06-07 22:37:30 +00:00
co-authored by Claude Opus 4.8
parent 5cd55382ed
commit a4e26f880a
12 changed files with 641 additions and 10 deletions
+63 -2
View File
@@ -1,6 +1,6 @@
import { useState } from "react";
import { Link, useParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { Link, useNavigate, useParams } from "react-router-dom";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
Play,
Square,
@@ -9,11 +9,15 @@ import {
ArrowUpCircle,
Pencil,
Power,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { Badge, Button, Card, Spinner, StatusDot } from "@/components/ui";
import { ConfirmDialog } from "@/components/ui/ConfirmDialog";
import { LogViewer } from "@/components/stacks/LogViewer";
import { BackupButton } from "@/components/stacks/BackupRestore";
import { stacksApi } from "@/api/stacks";
import { apiErrorMessage } from "@/api/client";
import { useAuthStore } from "@/store/auth";
import { useStackActions } from "@/hooks/useStackActions";
@@ -74,6 +78,7 @@ export function StackDetail() {
<Pencil className="h-4 w-4" /> Edit
</Button>
</Link>
<DeleteStackButton stackId={id} />
</div>
)}
</div>
@@ -161,3 +166,59 @@ function ComposeView({ yaml }: { yaml: string }) {
</Card>
);
}
function DeleteStackButton({ stackId }: { stackId: string }) {
const navigate = useNavigate();
const qc = useQueryClient();
const [open, setOpen] = useState(false);
const [deleteFiles, setDeleteFiles] = useState(true);
const [busy, setBusy] = useState(false);
const remove = async () => {
setBusy(true);
const t = toast.loading(`Deleting ${stackId}`);
try {
await stacksApi.remove(stackId, deleteFiles);
toast.success(`Deleted ${stackId}`, { id: t });
qc.invalidateQueries({ queryKey: ["stacks"] });
navigate("/stacks");
} catch (e) {
toast.error(apiErrorMessage(e), { id: t });
setBusy(false);
}
};
return (
<>
<Button variant="danger" onClick={() => setOpen(true)}>
<Trash2 className="h-4 w-4" /> Delete
</Button>
{open && (
<ConfirmDialog
title={`Delete stack “${stackId}”?`}
message="The stack is stopped and removed. This cannot be undone."
confirmLabel="Delete stack"
danger
busy={busy}
onConfirm={remove}
onCancel={() => setOpen(false)}
>
<label className="flex items-start gap-2 text-sm">
<input
type="checkbox"
checked={deleteFiles}
onChange={(e) => setDeleteFiles(e.target.checked)}
className="mt-0.5 h-4 w-4"
/>
<span>
Also delete the compose files from disk
<span className="block text-xs text-slate-500">
Uncheck to keep <code>{stackId}/</code> on disk (it can be re-discovered later).
</span>
</span>
</label>
</ConfirmDialog>
)}
</>
);
}