dashboard-v2 PR 2: frontend SPA + serve integration
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 16s
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 16s
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.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api, fmtBytes, fmtAge, } from '../lib/api';
|
||||
export function StorageBrowser({ tab }) {
|
||||
return (_jsxs("div", { className: "space-y-4", children: [_jsxs("h1", { className: "text-2xl font-semibold text-slate-100", children: ["Storage \u00B7 ", tab] }), tab === 'blobs' && _jsx(BlobsList, {}), tab === 'tags' && _jsx(TagsList, {}), tab === 'refs' && _jsx(RefsList, {}), tab === 'snapshots' && _jsx(SnapshotsList, {})] }));
|
||||
}
|
||||
function BlobsList() {
|
||||
const [rows, setRows] = useState([]);
|
||||
const [err, setErr] = useState(null);
|
||||
useEffect(() => {
|
||||
api.blobs().then(setRows).catch((e) => setErr(String(e)));
|
||||
}, []);
|
||||
return (_jsxs(_Fragment, { children: [err && _jsx(ErrorBox, { msg: err }), _jsx(Table, { headers: ['blob-id', 'size', 'chunks'], rows: rows.map((r) => [
|
||||
_jsxs("span", { className: "font-mono text-xs", children: [r.blob_id_hex.slice(0, 24), "\u2026"] }, "hex"),
|
||||
fmtBytes(r.size_bytes),
|
||||
r.chunk_count.toString(),
|
||||
]) }), _jsx(Footer, { count: rows.length })] }));
|
||||
}
|
||||
function TagsList() {
|
||||
const [rows, setRows] = useState([]);
|
||||
const [prefix, setPrefix] = useState('');
|
||||
const [err, setErr] = useState(null);
|
||||
useEffect(() => {
|
||||
api.tags(prefix).then(setRows).catch((e) => setErr(String(e)));
|
||||
}, [prefix]);
|
||||
return (_jsxs(_Fragment, { children: [_jsx("input", { value: prefix, onChange: (e) => setPrefix(e.target.value), placeholder: "filter prefix \u2014 e.g. clawverse:", className: "w-full sm:w-96 rounded bg-slate-900 border border-slate-800 px-3 py-2 text-sm font-mono" }), err && _jsx(ErrorBox, { msg: err }), _jsx(Table, { headers: ['key', 'blob-id'], rows: rows.map((r) => [
|
||||
_jsx("span", { className: "font-mono text-sm", children: r.key }, "k"),
|
||||
_jsxs("span", { className: "font-mono text-xs text-slate-400", children: [r.value_hex.slice(0, 24), "\u2026"] }, "v"),
|
||||
]) }), _jsx(Footer, { count: rows.length })] }));
|
||||
}
|
||||
function RefsList() {
|
||||
const [rows, setRows] = useState([]);
|
||||
const [err, setErr] = useState(null);
|
||||
useEffect(() => {
|
||||
api.refs().then(setRows).catch((e) => setErr(String(e)));
|
||||
}, []);
|
||||
return (_jsxs(_Fragment, { children: [err && _jsx(ErrorBox, { msg: err }), _jsx(Table, { headers: ['fingerprint', 'blob-id'], rows: rows.map((r) => [
|
||||
_jsxs("span", { className: "font-mono text-xs", children: [r.fingerprint_hex.slice(0, 24), "\u2026"] }, "fp"),
|
||||
_jsxs("span", { className: "font-mono text-xs text-slate-400", children: [r.blob_id_hex.slice(0, 24), "\u2026"] }, "b"),
|
||||
]) }), _jsx(Footer, { count: rows.length })] }));
|
||||
}
|
||||
function SnapshotsList() {
|
||||
const [rows, setRows] = useState([]);
|
||||
const [err, setErr] = useState(null);
|
||||
useEffect(() => {
|
||||
api.snapshots().then(setRows).catch((e) => setErr(String(e)));
|
||||
}, []);
|
||||
return (_jsxs(_Fragment, { children: [err && _jsx(ErrorBox, { msg: err }), _jsx(Table, { headers: ['name', 'created', 'blobs', 'json size'], rows: rows.map((r) => [
|
||||
_jsx("span", { className: "font-mono text-sm", children: r.name }, "n"),
|
||||
fmtAge(r.created_at_unix),
|
||||
r.blob_count.toString(),
|
||||
fmtBytes(r.file_bytes),
|
||||
]) }), _jsx(Footer, { count: rows.length })] }));
|
||||
}
|
||||
function Table({ headers, rows, }) {
|
||||
return (_jsx("div", { className: "rounded border border-slate-800 overflow-hidden", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-slate-900/60 text-slate-500 uppercase text-xs tracking-wider", children: _jsx("tr", { children: headers.map((h) => (_jsx("th", { className: "text-left px-4 py-2 font-normal", children: h }, h))) }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { className: "px-4 py-6 text-center text-slate-500", colSpan: headers.length, children: "nothing here yet" }) })), rows.map((row, i) => (_jsx("tr", { className: "border-t border-slate-900 hover:bg-slate-900/40", children: row.map((cell, j) => (_jsx("td", { className: "px-4 py-2", children: cell }, j))) }, i)))] })] }) }));
|
||||
}
|
||||
function Footer({ count }) {
|
||||
return (_jsxs("div", { className: "text-xs text-slate-500 mt-2 font-mono", children: [count, " row", count === 1 ? '' : 's'] }));
|
||||
}
|
||||
function ErrorBox({ msg }) {
|
||||
return (_jsx("div", { className: "rounded border border-red-800 bg-red-950/40 p-3 text-red-300 text-sm mb-2", children: msg }));
|
||||
}
|
||||
Reference in New Issue
Block a user