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:
+103
-41
@@ -1,13 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { Route, Switch, Link, Router, useLocation } from 'wouter';
|
||||
import { CommandCenter } from './pages/CommandCenter';
|
||||
import { NodeDetail } from './pages/NodeDetail';
|
||||
import { StorageBrowser } from './pages/StorageBrowser';
|
||||
import { RefTrackingPage } from './pages/RefTrackingPage';
|
||||
|
||||
// Base path — matches the deploy mount. When served locally at
|
||||
// :7700/v2/ the base is /v2; via Tailscale it's /clawstor.
|
||||
// Detected from the browser's current pathname so a single SPA
|
||||
// build works both places without env-var wiring.
|
||||
// Base path — matches the deploy mount. Detected from
|
||||
// window.location so a single SPA build serves both local
|
||||
// (:7700/v2/) and Tailscale (/clawstor).
|
||||
const BASE = (() => {
|
||||
if (typeof window === 'undefined') return '';
|
||||
const p = window.location.pathname;
|
||||
@@ -25,53 +25,22 @@ export default function App() {
|
||||
}
|
||||
|
||||
function Shell() {
|
||||
const [loc] = useLocation();
|
||||
const tab = (path: string, label: string) => {
|
||||
const active = loc === path || (path !== '/' && loc.startsWith(path));
|
||||
return (
|
||||
<Link href={path}>
|
||||
<a
|
||||
className={[
|
||||
'px-3 py-2 text-sm border-b-2 transition-colors',
|
||||
active
|
||||
? 'border-emerald-400 text-emerald-300'
|
||||
: 'border-transparent text-slate-400 hover:text-slate-100',
|
||||
].join(' ')}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<header className="border-b border-slate-800 bg-slate-950/80 backdrop-blur sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-4 flex items-center gap-6">
|
||||
<Link href="/">
|
||||
<a className="font-mono text-lg text-emerald-300 py-3">
|
||||
clawstor · command center
|
||||
</a>
|
||||
</Link>
|
||||
<nav className="flex">
|
||||
{tab('/', 'Overview')}
|
||||
{tab('/storage/blobs', 'Blobs')}
|
||||
{tab('/storage/tags', 'Tags')}
|
||||
{tab('/storage/refs', 'Refs')}
|
||||
{tab('/storage/snapshots', 'Snapshots')}
|
||||
{tab('/refs/tracking', 'Ref-tracking')}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<NavBar />
|
||||
<main className="max-w-7xl mx-auto p-4">
|
||||
<Switch>
|
||||
<Route path="/" component={CommandCenter} />
|
||||
<Route path="/nodes/:name">
|
||||
{(params) => <NodeDetail name={params.name} />}
|
||||
</Route>
|
||||
<Route path="/storage/:tab">
|
||||
<Route path="/advanced">
|
||||
<AdvancedRedirect />
|
||||
</Route>
|
||||
<Route path="/advanced/:tab">
|
||||
{(params) => <StorageBrowser tab={params.tab as any} />}
|
||||
</Route>
|
||||
<Route path="/refs/tracking" component={RefTrackingPage} />
|
||||
<Route path="/advanced/refs/tracking" component={RefTrackingPage} />
|
||||
<Route>
|
||||
<div className="text-slate-400 py-12 text-center">404</div>
|
||||
</Route>
|
||||
@@ -80,3 +49,96 @@ function Shell() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavBar() {
|
||||
const [loc] = useLocation();
|
||||
const [advOpen, setAdvOpen] = useState(false);
|
||||
const advActive = loc.startsWith('/advanced');
|
||||
const primary = [{ path: '/', label: 'Fleet health' }];
|
||||
const advanced = [
|
||||
{ path: '/advanced/blobs', label: 'Blobs' },
|
||||
{ path: '/advanced/tags', label: 'Tags' },
|
||||
{ path: '/advanced/refs', label: 'Refs' },
|
||||
{ path: '/advanced/snapshots', label: 'Snapshots' },
|
||||
{ path: '/advanced/refs/tracking', label: 'Ref-tracking' },
|
||||
];
|
||||
return (
|
||||
<header className="border-b border-slate-800 bg-slate-950/80 backdrop-blur sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-4 flex items-center gap-6">
|
||||
<Link href="/">
|
||||
<a className="font-mono text-lg text-emerald-300 py-3">
|
||||
clawstor · command center
|
||||
</a>
|
||||
</Link>
|
||||
<nav className="flex items-center">
|
||||
{primary.map((t) => (
|
||||
<Tab key={t.path} path={t.path} label={t.label} active={loc === t.path} />
|
||||
))}
|
||||
<div
|
||||
className="relative"
|
||||
onMouseEnter={() => setAdvOpen(true)}
|
||||
onMouseLeave={() => setAdvOpen(false)}
|
||||
>
|
||||
<span
|
||||
className={[
|
||||
'px-3 py-2 text-sm border-b-2 cursor-pointer transition-colors select-none',
|
||||
advActive
|
||||
? 'border-emerald-400 text-emerald-300'
|
||||
: 'border-transparent text-slate-400 hover:text-slate-100',
|
||||
].join(' ')}
|
||||
>
|
||||
View Advanced ▾
|
||||
</span>
|
||||
{advOpen && (
|
||||
<div className="absolute left-0 top-full mt-0 bg-slate-900 border border-slate-800 rounded shadow-lg min-w-[12rem] z-20">
|
||||
{advanced.map((t) => (
|
||||
<Link key={t.path} href={t.path}>
|
||||
<a
|
||||
className={[
|
||||
'block px-3 py-2 text-sm hover:bg-slate-800',
|
||||
loc === t.path ? 'text-emerald-300' : 'text-slate-300',
|
||||
].join(' ')}
|
||||
>
|
||||
{t.label}
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function Tab({
|
||||
path,
|
||||
label,
|
||||
active,
|
||||
}: {
|
||||
path: string;
|
||||
label: string;
|
||||
active: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Link href={path}>
|
||||
<a
|
||||
className={[
|
||||
'px-3 py-2 text-sm border-b-2 transition-colors',
|
||||
active
|
||||
? 'border-emerald-400 text-emerald-300'
|
||||
: 'border-transparent text-slate-400 hover:text-slate-100',
|
||||
].join(' ')}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function AdvancedRedirect() {
|
||||
const [, nav] = useLocation();
|
||||
nav('/advanced/blobs', { replace: true });
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user