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.
145 lines
4.4 KiB
TypeScript
145 lines
4.4 KiB
TypeScript
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. 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;
|
|
if (p === '/clawstor' || p.startsWith('/clawstor/')) return '/clawstor';
|
|
if (p === '/v2' || p.startsWith('/v2/')) return '/v2';
|
|
return '';
|
|
})();
|
|
|
|
export default function App() {
|
|
return (
|
|
<Router base={BASE}>
|
|
<Shell />
|
|
</Router>
|
|
);
|
|
}
|
|
|
|
function Shell() {
|
|
return (
|
|
<div className="min-h-screen">
|
|
<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="/advanced">
|
|
<AdvancedRedirect />
|
|
</Route>
|
|
<Route path="/advanced/:tab">
|
|
{(params) => <StorageBrowser tab={params.tab as any} />}
|
|
</Route>
|
|
<Route path="/advanced/refs/tracking" component={RefTrackingPage} />
|
|
<Route>
|
|
<div className="text-slate-400 py-12 text-center">404</div>
|
|
</Route>
|
|
</Switch>
|
|
</main>
|
|
</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;
|
|
}
|