import React, { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Network, Plus, Power, Trash2, UploadCloud, RefreshCw, Skull } from 'lucide-react'; import api from '../../utils/api'; import { clearLayout } from '../MazeNET/useMazeLayoutStore'; import CreateTopologyWizard from './CreateTopologyWizard'; import EmptyState from '../EmptyState/EmptyState'; import './TopologyList.css'; interface TopologySummary { id: string; name: string; mode: string; target_host_uuid: string | null; status: string; version: number; needs_resync?: boolean; created_at: string; status_changed_at: string | null; } interface ListResponse { total: number; limit: number | null; offset: number | null; data: TopologySummary[]; } const statusClass = (s: string): string => { switch (s) { case 'active': return 'pill-ok'; case 'pending': return 'pill-dim'; case 'deploying': case 'tearing_down': return 'pill-warn'; case 'degraded': return 'pill-warn'; case 'failed': case 'teardown_failed': return 'pill-bad'; case 'torn_down': return 'pill-dim'; default: return 'pill-dim'; } }; const TopologyList: React.FC = () => { const navigate = useNavigate(); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); const [creating, setCreating] = useState(false); const [busy, setBusy] = useState(null); const [armed, setArmed] = useState(null); const [reaping, setReaping] = useState(false); const [reapMsg, setReapMsg] = useState(null); const arm = (key: string) => { setArmed(key); setTimeout(() => setArmed((prev) => (prev === key ? null : prev)), 4000); }; const fetchRows = useCallback(async () => { try { const { data } = await api.get('/topologies/'); setRows(data.data ?? []); setErr(null); } catch (e) { setErr((e as Error)?.message ?? 'failed to list topologies'); } finally { setLoading(false); } }, []); useEffect(() => { let cancelled = false; const tick = async () => { if (!cancelled) await fetchRows(); }; tick(); const iv = setInterval(tick, 5000); return () => { cancelled = true; clearInterval(iv); }; }, [fetchRows]); const onCreated = (row: TopologySummary) => { setCreating(false); navigate(`/mazenet?topology=${row.id}`); }; const onDelete = async (id: string) => { setBusy(id); try { await api.delete(`/topologies/${id}`); clearLayout(id); await fetchRows(); } catch (e) { setErr((e as Error)?.message ?? 'delete failed'); } finally { setBusy(null); setArmed(null); } }; 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) => { setBusy(id); try { await api.post(`/topologies/${id}/deploy`, {}); await fetchRows(); } catch (e) { setErr((e as Error)?.message ?? 'deploy failed'); } finally { setBusy(null); } }; const onTeardown = async (id: string) => { setBusy(id); try { await api.post(`/topologies/${id}/teardown`, {}); await fetchRows(); } catch (e) { setErr((e as Error)?.message ?? 'teardown failed'); } finally { setBusy(null); setArmed(null); } }; return (

TOPOLOGIES

{loading ? 'LOADING…' : `${rows.length} ${rows.length === 1 ? 'TOPOLOGY' : 'TOPOLOGIES'}`} {err && · {err}} {reapMsg && · reap: {reapMsg}}
setCreating(false)} onCreated={onCreated} /> {!loading && rows.length === 0 ? (
setCreating(true) }} />
) : (
{rows.map((r) => (
navigate(`/mazenet?topology=${r.id}`)}>
{r.name}
{r.status}
mode: {r.mode} v{r.version} {new Date(r.created_at).toLocaleString()}
{r.id}
e.stopPropagation()}> {r.status === 'pending' && ( )} {['active', 'degraded', 'failed', 'deploying'].includes(r.status) && ( )} {!['active', 'degraded', 'deploying'].includes(r.status) && ( )}
))}
)}
); }; export default TopologyList;