feat(web): wire REAP ORPHANS button in topology list
Exposes POST /topologies/reap-orphans via an arm-to-confirm button in the topology list header. Shows a transient status line with removal counts or the error. Admin-only on the backend; non-admins see the 403.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Network, Plus, Power, Trash2, UploadCloud, RefreshCw } from 'lucide-react';
|
import { Network, Plus, Power, Trash2, UploadCloud, RefreshCw, Skull } from 'lucide-react';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { clearLayout } from '../MazeNET/useMazeLayoutStore';
|
import { clearLayout } from '../MazeNET/useMazeLayoutStore';
|
||||||
import CreateTopologyWizard from './CreateTopologyWizard';
|
import CreateTopologyWizard from './CreateTopologyWizard';
|
||||||
@@ -47,6 +47,8 @@ const TopologyList: React.FC = () => {
|
|||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
const [busy, setBusy] = useState<string | null>(null);
|
const [busy, setBusy] = useState<string | null>(null);
|
||||||
const [armed, setArmed] = useState<string | null>(null);
|
const [armed, setArmed] = useState<string | null>(null);
|
||||||
|
const [reaping, setReaping] = useState(false);
|
||||||
|
const [reapMsg, setReapMsg] = useState<string | null>(null);
|
||||||
|
|
||||||
const arm = (key: string) => {
|
const arm = (key: string) => {
|
||||||
setArmed(key);
|
setArmed(key);
|
||||||
@@ -92,6 +94,34 @@ const TopologyList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onReapOrphans = async () => {
|
||||||
|
setReaping(true);
|
||||||
|
setReapMsg(null);
|
||||||
|
try {
|
||||||
|
const { data } = await api.post<{
|
||||||
|
orphan_prefixes: string[];
|
||||||
|
containers_removed: string[];
|
||||||
|
networks_removed: string[];
|
||||||
|
errors: string[];
|
||||||
|
}>('/topologies/reap-orphans', {});
|
||||||
|
const c = data.containers_removed.length;
|
||||||
|
const n = data.networks_removed.length;
|
||||||
|
const e = data.errors.length;
|
||||||
|
if (c === 0 && n === 0 && e === 0) {
|
||||||
|
setReapMsg('no orphans found');
|
||||||
|
} else {
|
||||||
|
setReapMsg(`removed ${c} container(s), ${n} network(s)${e ? `, ${e} error(s)` : ''}`);
|
||||||
|
}
|
||||||
|
await fetchRows();
|
||||||
|
} catch (e) {
|
||||||
|
setReapMsg((e as Error)?.message ?? 'reap failed');
|
||||||
|
} finally {
|
||||||
|
setReaping(false);
|
||||||
|
setArmed(null);
|
||||||
|
setTimeout(() => setReapMsg(null), 6000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onDeploy = async (id: string) => {
|
const onDeploy = async (id: string) => {
|
||||||
setBusy(id);
|
setBusy(id);
|
||||||
try {
|
try {
|
||||||
@@ -125,12 +155,24 @@ const TopologyList: React.FC = () => {
|
|||||||
<div className="tlist-sub">
|
<div className="tlist-sub">
|
||||||
{loading ? 'loading…' : `${rows.length} topology${rows.length === 1 ? '' : 'ies'}`}
|
{loading ? 'loading…' : `${rows.length} topology${rows.length === 1 ? '' : 'ies'}`}
|
||||||
{err && <span className="alert-text"> · {err}</span>}
|
{err && <span className="alert-text"> · {err}</span>}
|
||||||
|
{reapMsg && <span className="alert-text"> · reap: {reapMsg}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="tlist-actions">
|
<div className="tlist-actions">
|
||||||
<button type="button" className="tlist-btn ghost" onClick={fetchRows} title="Refresh">
|
<button type="button" className="tlist-btn ghost" onClick={fetchRows} title="Refresh">
|
||||||
<RefreshCw size={12} /> REFRESH
|
<RefreshCw size={12} /> REFRESH
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`tlist-btn ghost warn ${armed === 'reap' ? 'armed' : ''}`}
|
||||||
|
disabled={reaping}
|
||||||
|
onClick={() => armed === 'reap' ? onReapOrphans() : arm('reap')}
|
||||||
|
title={armed === 'reap'
|
||||||
|
? 'Click again to force-remove Docker resources for deleted topologies'
|
||||||
|
: 'Reap orphan Docker resources (admin)'}
|
||||||
|
>
|
||||||
|
<Skull size={12} /> {reaping ? 'REAPING…' : armed === 'reap' ? 'CONFIRM?' : 'REAP ORPHANS'}
|
||||||
|
</button>
|
||||||
<button type="button" className="tlist-btn" onClick={() => setCreating(true)}>
|
<button type="button" className="tlist-btn" onClick={() => setCreating(true)}>
|
||||||
<Plus size={12} /> NEW TOPOLOGY
|
<Plus size={12} /> NEW TOPOLOGY
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user