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:
+39
-14
@@ -1,13 +1,13 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
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 '';
|
||||
@@ -22,15 +22,40 @@ export default function App() {
|
||||
return (_jsx(Router, { base: BASE, children: _jsx(Shell, {}) }));
|
||||
}
|
||||
function Shell() {
|
||||
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(NavBar, {}), _jsx("main", { className: "max-w-7xl mx-auto p-4", children: _jsxs(Switch, { children: [_jsx(Route, { path: "/", component: CommandCenter }), _jsx(Route, { path: "/nodes/:name", children: (params) => _jsx(NodeDetail, { name: params.name }) }), _jsx(Route, { path: "/advanced", children: _jsx(AdvancedRedirect, {}) }), _jsx(Route, { path: "/advanced/:tab", children: (params) => _jsx(StorageBrowser, { tab: params.tab }) }), _jsx(Route, { path: "/advanced/refs/tracking", component: RefTrackingPage }), _jsx(Route, { children: _jsx("div", { className: "text-slate-400 py-12 text-center", children: "404" }) })] }) })] }));
|
||||
}
|
||||
function NavBar() {
|
||||
const [loc] = useLocation();
|
||||
const tab = (path, label) => {
|
||||
const active = loc === path || (path !== '/' && loc.startsWith(path));
|
||||
return (_jsx(Link, { href: path, children: _jsx("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(' '), children: label }) }));
|
||||
};
|
||||
return (_jsxs("div", { className: "min-h-screen", children: [_jsx("header", { className: "border-b border-slate-800 bg-slate-950/80 backdrop-blur sticky top-0 z-10", children: _jsxs("div", { className: "max-w-7xl mx-auto px-4 flex items-center gap-6", children: [_jsx(Link, { href: "/", children: _jsx("a", { className: "font-mono text-lg text-emerald-300 py-3", children: "clawstor \u00B7 command center" }) }), _jsxs("nav", { className: "flex", children: [tab('/', 'Overview'), tab('/storage/blobs', 'Blobs'), tab('/storage/tags', 'Tags'), tab('/storage/refs', 'Refs'), tab('/storage/snapshots', 'Snapshots'), tab('/refs/tracking', 'Ref-tracking')] })] }) }), _jsx("main", { className: "max-w-7xl mx-auto p-4", children: _jsxs(Switch, { children: [_jsx(Route, { path: "/", component: CommandCenter }), _jsx(Route, { path: "/nodes/:name", children: (params) => _jsx(NodeDetail, { name: params.name }) }), _jsx(Route, { path: "/storage/:tab", children: (params) => _jsx(StorageBrowser, { tab: params.tab }) }), _jsx(Route, { path: "/refs/tracking", component: RefTrackingPage }), _jsx(Route, { children: _jsx("div", { className: "text-slate-400 py-12 text-center", children: "404" }) })] }) })] }));
|
||||
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 (_jsx("header", { className: "border-b border-slate-800 bg-slate-950/80 backdrop-blur sticky top-0 z-10", children: _jsxs("div", { className: "max-w-7xl mx-auto px-4 flex items-center gap-6", children: [_jsx(Link, { href: "/", children: _jsx("a", { className: "font-mono text-lg text-emerald-300 py-3", children: "clawstor \u00B7 command center" }) }), _jsxs("nav", { className: "flex items-center", children: [primary.map((t) => (_jsx(Tab, { path: t.path, label: t.label, active: loc === t.path }, t.path))), _jsxs("div", { className: "relative", onMouseEnter: () => setAdvOpen(true), onMouseLeave: () => setAdvOpen(false), children: [_jsx("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(' '), children: "View Advanced \u25BE" }), advOpen && (_jsx("div", { className: "absolute left-0 top-full mt-0 bg-slate-900 border border-slate-800 rounded shadow-lg min-w-[12rem] z-20", children: advanced.map((t) => (_jsx(Link, { href: t.path, children: _jsx("a", { className: [
|
||||
'block px-3 py-2 text-sm hover:bg-slate-800',
|
||||
loc === t.path ? 'text-emerald-300' : 'text-slate-300',
|
||||
].join(' '), children: t.label }) }, t.path))) }))] })] })] }) }));
|
||||
}
|
||||
function Tab({ path, label, active, }) {
|
||||
return (_jsx(Link, { href: path, children: _jsx("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(' '), children: label }) }));
|
||||
}
|
||||
function AdvancedRedirect() {
|
||||
const [, nav] = useLocation();
|
||||
nav('/advanced/blobs', { replace: true });
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user