Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Failing after 3s
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).
102 lines
3.8 KiB
TypeScript
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>
|
|
);
|
|
}
|