Files
clawstor/dashboard-v2/src/pages/NodeDetail.tsx
T
Omar Sobh f33468b7c2
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 16s
dashboard-v2 PR 2: frontend SPA + serve integration
React 19 + Vite + Tailwind + wouter (tiny router, no external
state library). Consumes the /api/v2/* endpoints shipped in PR 1.
Serves under /v2/* so the legacy dashboard at / stays live.

Pages:
* CommandCenter (/)      — fleet strip + this-node stat tiles
* NodeDetail (/nodes/:name) — per-node deep dive
* StorageBrowser (/storage/{blobs,tags,refs,snapshots}) — tables
  with prefix filter
* RefTrackingPage (/refs/tracking) — grouped by repo

Backend changes:
* claw-store serve grows --v2-static-dir <path>
* build_app split into build_app_with_v2 for the extra static
  mount
* /v2/* falls through to index.html so wouter client routing works

New systemd unit: clawstor-dashboard.service. Points at both
static dirs; installs on any node.

dashboard/ (legacy) untouched. dashboard-v2/ built to
target/dashboard-v2/dist for deploy.

Deploy sequence per node:
1. cp target/release/claw-store  ~/clawstor-deploy/
2. rsync dashboard-v2/dist/      ~/clawstor-deploy/dashboard-v2/
3. cp deploy/systemd/clawstor-dashboard.service ~/.config/systemd/user/
4. systemctl --user daemon-reload && enable --now clawstor-dashboard.service

Cross-node fan-out for /api/v2/node/:name/status is PR 3.
Action POSTs (scrub/gc/snapshot/pin) are PR 4.
2026-07-14 15:59:47 -07:00

75 lines
2.4 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Link } from 'wouter';
import { api, NodeStatusV2, fmtBytes } from '../lib/api';
import { StatTile } from '../components/StatTile';
interface Props {
name: string;
}
export function NodeDetail({ name }: Props) {
const [status, setStatus] = useState<NodeStatusV2 | null>(null);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
api
.nodeStatus(name)
.then((n) => {
setStatus(n);
setErr(null);
})
.catch((e) => setErr(String(e)));
}, [name]);
return (
<div className="space-y-6">
<div>
<Link href="/">
<a className="text-sm text-slate-500 hover:text-slate-300">
← fleet
</a>
</Link>
<h1 className="text-2xl font-semibold text-slate-100 mt-2">
{name}
</h1>
</div>
{err && (
<div className="rounded border border-amber-800 bg-amber-950/40 p-3 text-amber-300 text-sm">
{err}
<div className="mt-2 text-xs text-slate-400">
Cross-node lookup lands in a follow-on PR. Until then this
page shows detail only when you're already viewing the
dashboard hosted by {name}. Try opening{' '}
<span className="font-mono">http://{name}:7700/v2/#/nodes/{name}</span>{' '}
directly.
</div>
</div>
)}
{status && (
<>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
<StatTile label="blobs" value={status.blob_count.toLocaleString()} color="ok" />
<StatTile label="tags" value={status.tag_count} color="ok" />
<StatTile label="refs" value={status.ref_count} color="ok" />
<StatTile label="snapshots" value={status.snapshot_count} color="ok" />
<StatTile label="ref-tracking" value={status.ref_tracking_count} color="ok" />
<StatTile
label="store size"
value={fmtBytes(status.blob_store_bytes)}
color="ok"
/>
</div>
<div className="rounded border border-slate-800 bg-slate-900/40 p-4 text-sm">
<div className="text-slate-500 uppercase text-xs tracking-wider">
blob store root
</div>
<div className="font-mono mt-1">{status.blob_store_root ?? '—'}</div>
</div>
</>
)}
</div>
);
}