Files
clawstor/dashboard-v2/src/pages/RefTrackingPage.tsx
T
Omar Sobh b672037e42
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Failing after 3s
dashboard-v2 frontend: node column + clickable pills on storage tabs
All storage rows now carry a 'node' field from the aggregator.
Adds a small green-pill component that renders the source node
name; click drills into that node's detail page.

Also fixes the TS interfaces to match the new backend shape
(node field added to Blob/Tag/Ref/Snapshot/RefTracking types).
2026-07-14 16:44:06 -07:00

102 lines
3.8 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { api, RefTrackingItem, fmtAge } from '../lib/api';
export function RefTrackingPage() {
const [rows, setRows] = useState<RefTrackingItem[]>([]);
const [repoFilter, setRepoFilter] = useState('');
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
api
.refTracking(repoFilter)
.then(setRows)
.catch((e) => setErr(String(e)));
}, [repoFilter]);
// Group by repo for a cleaner display.
const grouped = useMemo(() => {
const g = new Map<string, RefTrackingItem[]>();
for (const r of rows) {
const arr = g.get(r.repo) ?? [];
arr.push(r);
g.set(r.repo, arr);
}
return [...g.entries()].sort((a, b) => a[0].localeCompare(b[0]));
}, [rows]);
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-semibold text-slate-100">Ref-tracking</h1>
<p className="text-sm text-slate-500 mt-1">
Every cached fingerprint's producing <span className="font-mono">(repo, git-ref)</span>.
Feeds the nightly <span className="font-mono">cluster-ref-sweep</span> that reaps
fingerprints whose refs are gone from Gitea.
</p>
</div>
<input
value={repoFilter}
onChange={(e) => setRepoFilter(e.target.value)}
placeholder="filter repo — e.g. clawverse/clawstor"
className="w-full sm:w-96 rounded bg-slate-900 border border-slate-800 px-3 py-2 text-sm font-mono"
/>
{err && (
<div className="rounded border border-red-800 bg-red-950/40 p-3 text-red-300 text-sm">
{err}
</div>
)}
{grouped.length === 0 && (
<div className="text-slate-500 text-sm py-6">no ref-tracking entries yet</div>
)}
{grouped.map(([repo, items]) => (
<div key={repo} className="rounded border border-slate-800 overflow-hidden">
<div className="bg-slate-900/60 px-4 py-2 flex items-baseline justify-between">
<span className="font-mono text-sm text-emerald-300">{repo}</span>
<span className="text-xs text-slate-500">
{items.length} fingerprint{items.length === 1 ? '' : 's'}
</span>
</div>
<table className="w-full text-sm">
<thead className="text-slate-500 uppercase text-xs tracking-wider">
<tr>
<th className="text-left px-4 py-2 font-normal">node</th>
<th className="text-left px-4 py-2 font-normal">fingerprint</th>
<th className="text-left px-4 py-2 font-normal">refs</th>
<th className="text-left px-4 py-2 font-normal">first seen</th>
<th className="text-left px-4 py-2 font-normal">last seen</th>
</tr>
</thead>
<tbody>
{items.map((it) => (
<tr key={it.node + ':' + it.fingerprint_hex} className="border-t border-slate-900">
<td className="px-4 py-2">
<span className="inline-block rounded bg-slate-800 text-emerald-300 text-xs font-mono px-2 py-0.5">
{it.node}
</span>
</td>
<td className="px-4 py-2 font-mono text-xs">
{it.fingerprint_hex.slice(0, 24)}…
</td>
<td className="px-4 py-2 font-mono text-xs">
{it.refs.join(', ')}
</td>
<td className="px-4 py-2 text-slate-400">
{fmtAge(it.first_seen_unix)}
</td>
<td className="px-4 py-2 text-slate-400">
{fmtAge(it.last_seen_unix)}
</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
);
}