import { useMemo, useState } from "react"; import { ListTree, X, Skull } from "lucide-react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { managerPost } from "@/main-axios"; import { useManagerData, extractError } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; import { ManagerSearch } from "./ManagerToolbar"; import { Select2 } from "@/components/select2"; interface ProcessRow { pid: number; ppid: number; user: string; cpu: number; mem: number; command: string; args: string; } type SortKey = "cpu" | "mem" | "pid"; type Signal = "TERM" | "KILL"; export function ProcessInspectorCard({ hostId }: { hostId: number | null }) { const { t } = useTranslation(); const { data, loading, error, refresh } = useManagerData<{ processes: ProcessRow[]; }>(hostId, "processes"); const [filter, setFilter] = useState(""); const [sort, setSort] = useState("cpu"); const [tree, setTree] = useState(false); const [busy, setBusy] = useState(null); const all = useMemo(() => data?.processes ?? [], [data?.processes]); const filtered = useMemo(() => { const q = filter.toLowerCase(); const list = all.filter( (p) => !q || p.args.toLowerCase().includes(q) || p.command.toLowerCase().includes(q) || String(p.pid).includes(q) || p.user.toLowerCase().includes(q), ); return [...list].sort((a, b) => { if (sort === "pid") return a.pid - b.pid; return b[sort] - a[sort]; }); }, [all, filter, sort]); // Build a depth map for the tree view from ppid links (only among visible). const treeRows = useMemo(() => { if (!tree) return null; const byId = new Map(filtered.map((p) => [p.pid, p])); const children = new Map(); const roots: ProcessRow[] = []; for (const p of filtered) { if (p.ppid && byId.has(p.ppid)) { const arr = children.get(p.ppid) ?? []; arr.push(p); children.set(p.ppid, arr); } else { roots.push(p); } } const out: Array<{ proc: ProcessRow; depth: number }> = []; const walk = (p: ProcessRow, depth: number) => { out.push({ proc: p, depth }); for (const c of children.get(p.pid) ?? []) walk(c, depth + 1); }; roots.forEach((r) => walk(r, 0)); return out; }, [tree, filtered]); const kill = async (pid: number, signal: Signal) => { if (hostId == null) return; setBusy(pid); try { const res = await managerPost<{ success: boolean; output: string }>( hostId, "processes", { pid, signal }, "signal", ); if (res.success) { toast.success(t("hostMetrics.managers.signalSent", { pid })); refresh(); } else { toast.error(res.output || t("hostMetrics.managers.actionFailed")); } } catch (e) { toast.error(extractError(e).message); } finally { setBusy(null); } }; const rows = treeRows ? treeRows.slice(0, 200) : filtered.slice(0, 200).map((proc) => ({ proc, depth: 0 })); return ( } loading={loading} error={error} onRefresh={refresh} empty={!loading && filtered.length === 0} headerExtra={ <> setSort(e.target.value as SortKey)} disabled={tree} className="h-6 border border-border bg-background px-1 text-[10px] disabled:opacity-40" > } >
PID CPU MEM CMD
{rows.map(({ proc: p, depth }) => (
{p.pid} {p.cpu.toFixed(0)}% {p.mem.toFixed(0)}% {depth > 0 && ( └ )} {/* `comm` is capped at 15 chars by the kernel; the full command line is in args. */} {p.args || p.command}
))}
); }