swarm-ui: wire hive overview to the real status aggregate
App.tsx now fetches GET /api/hives/status (the swarm-controller aggregate: one row per roster hive, freshness derived at read time from the status bucket) instead of GET /api/hives + a static 'configured' chip. Renders fresh/stale/never_reported/unknown as StatusChip tones with a relative age, per the placeholder comment that was already waiting on this endpoint to exist. Adds a small local fmtAgo helper (src/util.ts) mirroring the dashboard package's near-identical formatter — not worth sharing across a vanilla-JS and a Preact/TS call site.
This commit is contained in:
parent
4f8b78a4d4
commit
2460d7fec7
2 changed files with 62 additions and 22 deletions
|
|
@ -1,49 +1,74 @@
|
||||||
// Root shell component. `/` is now the real hive-roster overview page —
|
// Root shell component. `/` is the real hive-roster overview page —
|
||||||
// fetches swarm-controller's `GET /api/hives` and renders it through the
|
// fetches swarm-controller's `GET /api/hives/status`, the swarm-wide
|
||||||
// shared primitives. Status starts as a static "configured" chip; grows
|
// status aggregate: one row per roster hive, freshness derived at read
|
||||||
// into a real online/stale/offline tone once a status rollup exists
|
// time from the status bucket. Supersedes the earlier `GET /api/hives`
|
||||||
// server-side — same component, richer data later, no rebuild.
|
// + static "configured" chip placeholder now that the real rollup
|
||||||
|
// exists — same component, richer data, no rebuild, exactly the plan
|
||||||
|
// that placeholder's comment laid out.
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
import { Route, Switch } from 'wouter-preact';
|
import { Route, Switch } from 'wouter-preact';
|
||||||
import { Shell } from './shell/Shell.js';
|
import { Shell } from './shell/Shell.js';
|
||||||
import { ComponentsPage } from './pages/ComponentsPage.js';
|
import { ComponentsPage } from './pages/ComponentsPage.js';
|
||||||
import { JobsPage } from './pages/JobsPage.js';
|
import { JobsPage } from './pages/JobsPage.js';
|
||||||
import { Panel } from './ui/panel/Panel.js';
|
import { Panel } from './ui/panel/Panel.js';
|
||||||
import { StatusChip } from './ui/status-chip/StatusChip.js';
|
import { StatusChip, type ChipTone } from './ui/status-chip/StatusChip.js';
|
||||||
import { Table, type TableColumn } from './ui/table/Table.js';
|
import { Table, type TableColumn } from './ui/table/Table.js';
|
||||||
|
import { fmtAgo } from './util.js';
|
||||||
|
|
||||||
interface Hive {
|
type Freshness = 'fresh' | 'stale' | 'never_reported' | 'unknown';
|
||||||
|
|
||||||
|
interface HiveStatus {
|
||||||
name: string;
|
name: string;
|
||||||
domain: string;
|
domain: string | null;
|
||||||
|
freshness: Freshness;
|
||||||
|
last_seen_unix: number | null;
|
||||||
|
age_seconds: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLUMNS: TableColumn<Hive>[] = [
|
// One tone + label per freshness value. `unknown` (reporting but absent
|
||||||
|
// from the roster) reads `negative` — it's the one case this endpoint
|
||||||
|
// cannot vouch for at all, per `status.rs`'s doc comment.
|
||||||
|
const FRESHNESS: Record<Freshness, { tone: ChipTone; label: string }> = {
|
||||||
|
fresh: { tone: 'positive', label: 'fresh' },
|
||||||
|
stale: { tone: 'warning', label: 'stale' },
|
||||||
|
never_reported: { tone: 'neutral', label: 'never reported' },
|
||||||
|
unknown: { tone: 'negative', label: 'unknown' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLUMNS: TableColumn<HiveStatus>[] = [
|
||||||
{ key: 'name', header: 'name', render: (h) => h.name },
|
{ key: 'name', header: 'name', render: (h) => h.name },
|
||||||
{
|
{
|
||||||
key: 'domain',
|
key: 'domain',
|
||||||
header: 'domain',
|
header: 'domain',
|
||||||
render: (h) => (
|
render: (h) =>
|
||||||
<a href={`https://${h.domain}/`} target="_blank" rel="noreferrer">
|
h.domain ? (
|
||||||
{h.domain}
|
<a href={`https://${h.domain}/`} target="_blank" rel="noreferrer">
|
||||||
</a>
|
{h.domain}
|
||||||
),
|
</a>
|
||||||
|
) : (
|
||||||
|
'—'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
header: 'status',
|
||||||
|
render: (h) => {
|
||||||
|
const { tone, label } = FRESHNESS[h.freshness];
|
||||||
|
const age = h.age_seconds !== null ? ` (${fmtAgo(h.age_seconds)})` : '';
|
||||||
|
return <StatusChip tone={tone} label={label + age} />;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// Static "configured" tone until a swarm-wide status rollup exists —
|
|
||||||
// there is no data source for online/stale/offline yet, and a chip
|
|
||||||
// that always renders "positive" would misreport a genuinely offline
|
|
||||||
// hive. See StatusChip's own doc comment for the same reasoning.
|
|
||||||
{ key: 'status', header: 'status', render: () => <StatusChip label="configured" /> },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const [hives, setHives] = useState<Hive[] | null>(null);
|
const [hives, setHives] = useState<HiveStatus[] | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/hives')
|
fetch('/api/hives/status')
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
if (!r.ok) throw new Error(`http ${r.status}`);
|
if (!r.ok) throw new Error(`http ${r.status}`);
|
||||||
return r.json() as Promise<Hive[]>;
|
return r.json() as Promise<HiveStatus[]>;
|
||||||
})
|
})
|
||||||
.then(setHives)
|
.then(setHives)
|
||||||
.catch((e: unknown) => setError(String(e)));
|
.catch((e: unknown) => setError(String(e)));
|
||||||
|
|
|
||||||
15
frontend/packages/swarm-ui/src/util.ts
Normal file
15
frontend/packages/swarm-ui/src/util.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Small pure render helpers shared across swarm-ui pages. Not folded
|
||||||
|
// into `@hive/shared` — these are swarm-ui's own formatting choices
|
||||||
|
// (compact, one unit deep), not a cross-package contract, and the
|
||||||
|
// dashboard package already has its own near-identical `fmtAgo` in
|
||||||
|
// `util.js` for the same reason: two small vanilla-vs-Preact call
|
||||||
|
// sites don't justify a shared abstraction over a five-line function.
|
||||||
|
|
||||||
|
// Relative age in whole seconds, coarsened to one unit ("5m ago").
|
||||||
|
export function fmtAgo(ageSeconds: number): string {
|
||||||
|
const age = Math.max(0, Math.floor(ageSeconds));
|
||||||
|
if (age < 60) return age + 's ago';
|
||||||
|
if (age < 3600) return Math.floor(age / 60) + 'm ago';
|
||||||
|
if (age < 86400) return Math.floor(age / 3600) + 'h ago';
|
||||||
|
return Math.floor(age / 86400) + 'd ago';
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue