FleetHealth PR 2 (frontend): human-oriented landing + advanced menu
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 18s
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 18s
Reworks the SPA around the new DashboardStatus fields. The landing is now a fleet-health dashboard aimed at a layperson — disk gauges, mount ✓/✗, cache hit rate, next scheduled job. No hex, no primitives. Nav restructure: * Primary: 'Fleet health' (just the landing). * 'View Advanced ▾' dropdown reveals: Blobs / Tags / Refs / Snapshots / Ref-tracking. Routes moved under /advanced/*. New components: * StorageBar — horizontal used/total bar with pinned/evictable split, health-color threshold at 60/85%. * NodeCard — traffic-light dot + disk + hot tier bars + mount state + cache hit rate + next-timer countdown. Whole card is a link into node detail. New CommandCenter: * Fleet-wide storage roll-up card (sum of every node's disk). * Grid of NodeCards. * 10s poll cadence retained from previous version. Existing StorageBrowser + RefTrackingPage moved behind /advanced/* routes; internal component code untouched.
This commit is contained in:
@@ -1,13 +1,9 @@
|
||||
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api, fmtAge, fmtBytes } from '../lib/api';
|
||||
import { api, fmtBytes, fmtAge } from '../lib/api';
|
||||
import { NodeCard } from '../components/NodeCard';
|
||||
import { StatTile } from '../components/StatTile';
|
||||
// Fleet command center — the landing pane.
|
||||
//
|
||||
// Aggregator polls each peer via cluster RPC every 10 s. Numbers
|
||||
// are ground truth from the daemons themselves; no local
|
||||
// blob-store reads on the aggregator.
|
||||
// FleetHealth landing — human-oriented single-pane-of-glass.
|
||||
// Polls the aggregator's /api/v2/fleet every 10 s.
|
||||
export function CommandCenter() {
|
||||
const [fleet, setFleet] = useState(null);
|
||||
const [err, setErr] = useState(null);
|
||||
@@ -27,14 +23,14 @@ export function CommandCenter() {
|
||||
}, []);
|
||||
const totals = fleet
|
||||
? fleet.nodes.reduce((a, n) => ({
|
||||
blobs: a.blobs + n.blob_count,
|
||||
tags: a.tags + n.tag_count,
|
||||
refs: a.refs + n.ref_count,
|
||||
snapshots: a.snapshots + n.snapshot_count,
|
||||
bytes: a.bytes + n.blob_store_bytes,
|
||||
diskUsed: a.diskUsed + (n.filesystem?.used_bytes ?? 0),
|
||||
diskTotal: a.diskTotal + (n.filesystem?.total_bytes ?? 0),
|
||||
hotUsed: a.hotUsed + (n.hot?.used_bytes ?? 0),
|
||||
hotMax: a.hotMax + (n.hot?.max_bytes ?? 0),
|
||||
online: a.online + (n.online ? 1 : 0),
|
||||
}), { blobs: 0, tags: 0, refs: 0, snapshots: 0, bytes: 0, online: 0 })
|
||||
mounted: a.mounted + (n.mount?.active ? 1 : 0),
|
||||
}), { diskUsed: 0, diskTotal: 0, hotUsed: 0, hotMax: 0, online: 0, mounted: 0 })
|
||||
: null;
|
||||
return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold text-slate-100", children: "Fleet" }), _jsxs("div", { className: "text-sm text-slate-500", children: ["aggregated by", ' ', _jsx("span", { className: "font-mono text-slate-300", children: fleet?.aggregator_name ?? '…' }), fleet && (_jsxs(_Fragment, { children: [' · updated ', _jsx("span", { className: "font-mono", children: fmtAge(fleet.fetched_at_unix) }), ' · ', _jsx("span", { className: "text-emerald-300", children: totals?.online }), "/", fleet.nodes.length, " online"] }))] })] }), err && (_jsx("div", { className: "rounded border border-red-800 bg-red-950/40 p-3 text-red-300 text-sm", children: err })), totals && fleet && (_jsxs("section", { children: [_jsx("h2", { className: "text-lg font-semibold text-slate-100 mb-3", children: "Totals (fleet-wide)" }), _jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3", children: [_jsx(StatTile, { label: "online nodes", value: `${totals.online}/${fleet.nodes.length}`, color: totals.online === fleet.nodes.length ? 'ok' : 'warn' }), _jsx(StatTile, { label: "blobs", value: totals.blobs.toLocaleString(), color: "ok" }), _jsx(StatTile, { label: "tags", value: totals.tags, color: "ok" }), _jsx(StatTile, { label: "refs", value: totals.refs, color: "ok" }), _jsx(StatTile, { label: "total bytes", value: fmtBytes(totals.bytes), color: "ok" })] })] })), _jsxs("section", { children: [_jsx("h2", { className: "text-lg font-semibold text-slate-100 mb-3", children: "Nodes" }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4", children: [fleet?.nodes.map((n) => (_jsx(NodeCard, { node: n }, n.node_name))), !fleet &&
|
||||
[1, 2, 3].map((i) => (_jsx("div", { className: "rounded-lg border border-slate-800 bg-slate-900/40 p-5 h-40 animate-pulse" }, i)))] })] })] }));
|
||||
return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold text-slate-100", children: "Fleet health" }), _jsx("div", { className: "text-sm text-slate-500 mt-1", children: fleet && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-emerald-300", children: totals?.online }), "/", fleet.nodes.length, " nodes online", ' · ', totals?.mounted, "/", fleet.nodes.length, " mounted", ' · ', "updated ", _jsx("span", { className: "font-mono", children: fmtAge(fleet.fetched_at_unix) }), ' · ', "hosted by ", _jsx("span", { className: "font-mono text-slate-300", children: fleet.aggregator_name })] })) })] }), err && (_jsx("div", { className: "rounded border border-red-800 bg-red-950/40 p-3 text-red-300 text-sm", children: err })), totals && totals.diskTotal > 0 && (_jsxs("div", { className: "rounded-lg border border-slate-800 bg-slate-900/60 p-4 flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-xs uppercase tracking-wider text-slate-500", children: "Fleet-wide storage" }), _jsxs("div", { className: "text-2xl font-semibold font-mono mt-1", children: [fmtBytes(totals.diskUsed), ' ', _jsxs("span", { className: "text-slate-500 text-lg", children: ["/ ", fmtBytes(totals.diskTotal)] })] })] }), _jsxs("div", { className: "text-right text-slate-400 text-sm", children: [_jsxs("div", { children: [Math.round((totals.diskUsed / totals.diskTotal) * 100), "% used"] }), _jsxs("div", { className: "text-xs text-slate-500 mt-1", children: ["hot tier: ", fmtBytes(totals.hotUsed), " / ", fmtBytes(totals.hotMax)] })] })] })), _jsxs("section", { children: [_jsx("h2", { className: "text-lg font-semibold text-slate-100 mb-3", children: "Nodes" }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4", children: [fleet?.nodes.map((n) => (_jsx(NodeCard, { node: n }, n.node_name))), !fleet &&
|
||||
[1, 2, 3].map((i) => (_jsx("div", { className: "rounded-lg border border-slate-800 bg-slate-900/40 p-5 h-64 animate-pulse" }, i)))] })] })] }));
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api, FleetSnapshot, fmtAge, fmtBytes } from '../lib/api';
|
||||
import { api, FleetSnapshot, fmtBytes, fmtAge } from '../lib/api';
|
||||
import { NodeCard } from '../components/NodeCard';
|
||||
import { StatTile } from '../components/StatTile';
|
||||
|
||||
// Fleet command center — the landing pane.
|
||||
//
|
||||
// Aggregator polls each peer via cluster RPC every 10 s. Numbers
|
||||
// are ground truth from the daemons themselves; no local
|
||||
// blob-store reads on the aggregator.
|
||||
// FleetHealth landing — human-oriented single-pane-of-glass.
|
||||
// Polls the aggregator's /api/v2/fleet every 10 s.
|
||||
export function CommandCenter() {
|
||||
const [fleet, setFleet] = useState<FleetSnapshot | null>(null);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
@@ -31,33 +27,32 @@ export function CommandCenter() {
|
||||
const totals = fleet
|
||||
? fleet.nodes.reduce(
|
||||
(a, n) => ({
|
||||
blobs: a.blobs + n.blob_count,
|
||||
tags: a.tags + n.tag_count,
|
||||
refs: a.refs + n.ref_count,
|
||||
snapshots: a.snapshots + n.snapshot_count,
|
||||
bytes: a.bytes + n.blob_store_bytes,
|
||||
diskUsed: a.diskUsed + (n.filesystem?.used_bytes ?? 0),
|
||||
diskTotal: a.diskTotal + (n.filesystem?.total_bytes ?? 0),
|
||||
hotUsed: a.hotUsed + (n.hot?.used_bytes ?? 0),
|
||||
hotMax: a.hotMax + (n.hot?.max_bytes ?? 0),
|
||||
online: a.online + (n.online ? 1 : 0),
|
||||
mounted: a.mounted + (n.mount?.active ? 1 : 0),
|
||||
}),
|
||||
{ blobs: 0, tags: 0, refs: 0, snapshots: 0, bytes: 0, online: 0 }
|
||||
{ diskUsed: 0, diskTotal: 0, hotUsed: 0, hotMax: 0, online: 0, mounted: 0 }
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-slate-100">Fleet</h1>
|
||||
<div className="text-sm text-slate-500">
|
||||
aggregated by{' '}
|
||||
<span className="font-mono text-slate-300">
|
||||
{fleet?.aggregator_name ?? '…'}
|
||||
</span>
|
||||
<h1 className="text-2xl font-semibold text-slate-100">Fleet health</h1>
|
||||
<div className="text-sm text-slate-500 mt-1">
|
||||
{fleet && (
|
||||
<>
|
||||
{' · updated '}
|
||||
<span className="font-mono">{fmtAge(fleet.fetched_at_unix)}</span>
|
||||
{' · '}
|
||||
<span className="text-emerald-300">{totals?.online}</span>
|
||||
/{fleet.nodes.length} online
|
||||
/{fleet.nodes.length} nodes online
|
||||
{' · '}
|
||||
{totals?.mounted}/{fleet.nodes.length} mounted
|
||||
{' · '}
|
||||
updated <span className="font-mono">{fmtAge(fleet.fetched_at_unix)}</span>
|
||||
{' · '}
|
||||
hosted by <span className="font-mono text-slate-300">{fleet.aggregator_name}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -69,19 +64,24 @@ export function CommandCenter() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{totals && fleet && (
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-slate-100 mb-3">
|
||||
Totals (fleet-wide)
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||
<StatTile label="online nodes" value={`${totals.online}/${fleet.nodes.length}`} color={totals.online === fleet.nodes.length ? 'ok' : 'warn'} />
|
||||
<StatTile label="blobs" value={totals.blobs.toLocaleString()} color="ok" />
|
||||
<StatTile label="tags" value={totals.tags} color="ok" />
|
||||
<StatTile label="refs" value={totals.refs} color="ok" />
|
||||
<StatTile label="total bytes" value={fmtBytes(totals.bytes)} color="ok" />
|
||||
{totals && totals.diskTotal > 0 && (
|
||||
<div className="rounded-lg border border-slate-800 bg-slate-900/60 p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-wider text-slate-500">
|
||||
Fleet-wide storage
|
||||
</div>
|
||||
<div className="text-2xl font-semibold font-mono mt-1">
|
||||
{fmtBytes(totals.diskUsed)}{' '}
|
||||
<span className="text-slate-500 text-lg">/ {fmtBytes(totals.diskTotal)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div className="text-right text-slate-400 text-sm">
|
||||
<div>{Math.round((totals.diskUsed / totals.diskTotal) * 100)}% used</div>
|
||||
<div className="text-xs text-slate-500 mt-1">
|
||||
hot tier: {fmtBytes(totals.hotUsed)} / {fmtBytes(totals.hotMax)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section>
|
||||
@@ -94,7 +94,7 @@ export function CommandCenter() {
|
||||
[1, 2, 3].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-lg border border-slate-800 bg-slate-900/40 p-5 h-40 animate-pulse"
|
||||
className="rounded-lg border border-slate-800 bg-slate-900/40 p-5 h-64 animate-pulse"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user