Show live progress while pruning volumes, and ask first (0.62.0)
The volume prune gets the same treatment as the image prune in 0.61.0:
the candidates are listed and removed one by one in the background, and
the Volumes page shows a progress bar (x / y, the volume being removed,
failures) followed by a toast with the space freed.
The button also used to delete without asking, and a bare docker volume
prune only removes anonymous volumes on Engine 23+ with no way to include
named ones. It now opens a confirmation dialog with an explicit -a option
that warns it also takes the data of every stack that is down.
- Candidates: volumes no container (running or stopped) mounts, local
scope only, and anonymous (com.docker.volume.anonymous) unless -a.
Volumes from pre-23 daemons carry no such label, so they only go with
-a, which errs on the side of keeping data.
- Removed without force, so a volume mounted since the listing is
refused; one refusal does not stop the rest.
- No closing daemon prune: nothing is left over, and on a pre-23 daemon an
unfiltered one would delete named volumes too.
- Freed space is the sum of the deleted volumes' sizes (volumes share no
data), from the size cache, which is invalidated afterwards.
- POST /api/volumes/prune takes all= and background=; without background
it still waits and returns {VolumesDeleted, SpaceReclaimed}.
GET /api/volumes/prune-status is admin-only.
- volume_sizes() ("Compute sizes") now uses the long-timeout client; it
walks every volume and could hit the same 60s limit.
The progress machinery moved into services/prune_job.py (PruneJob,
PrunePlan) and, on the frontend, the usePruneJob hook and PruneProgress
component, shared by both pages. The status now carries a phase
(preparing / removing / finishing) so the parts without a count say what
they are doing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
faff2be944
commit
57231e4a67
18 files changed
+816
-286
No files matched your search
@@ -0,0 +1,53 @@
|
||||
import { Card } from "@/components/ui";
|
||||
import type { PruneStatus } from "@/types";
|
||||
|
||||
const PHASE_DETAIL = {
|
||||
preparing: "Measuring disk usage…",
|
||||
finishing: "Finishing up…",
|
||||
} as const;
|
||||
|
||||
/** Live progress of a running prune, shown above the page's table. */
|
||||
export function PruneProgress({ status, title }: { status: PruneStatus; title: string }) {
|
||||
const measurable = status.total > 0;
|
||||
const pct = measurable ? Math.min((status.done / status.total) * 100, 100) : 0;
|
||||
const detail =
|
||||
status.phase === "removing"
|
||||
? status.current
|
||||
? `Removing ${status.current}`
|
||||
: ""
|
||||
: status.phase
|
||||
? PHASE_DETAIL[status.phase]
|
||||
: "";
|
||||
|
||||
return (
|
||||
<Card className="mb-3 p-3">
|
||||
<div className="flex items-baseline justify-between gap-2 text-sm">
|
||||
<span className="font-medium">{title}</span>
|
||||
{measurable && (
|
||||
<span className="shrink-0 text-xs tabular-nums text-sp-text-2">
|
||||
{status.done} / {status.total}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-sp-surface-2 ring-1 ring-inset ring-sp-border">
|
||||
{measurable && status.phase === "removing" ? (
|
||||
<div
|
||||
className="h-full rounded-full bg-accent transition-[width] duration-300 ease-out dark:bg-accent-dark"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
) : (
|
||||
// Measuring and finishing have nothing to count — slide instead.
|
||||
<div className="h-full w-1/3 rounded-full bg-accent animate-indeterminate dark:bg-accent-dark" />
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 flex items-baseline gap-3 text-[11px] text-sp-text-3">
|
||||
<span className="min-w-0 truncate">{detail}</span>
|
||||
{status.failed > 0 && (
|
||||
<span className="shrink-0 text-amber-600 dark:text-amber-400">
|
||||
{status.failed} failed
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user