mara: non-interactive Badge and StatusChip render identically (same padding/radius/font, same tone-to-color mapping) and StatusChip's 4-tone/single-label shape is a strict subset of Badge's — no real reason to keep both. Deletes StatusChip.tsx/.css, migrates AgentsPage's config-PR chip, HivesPage's freshness chip, and ComponentsPage's own table-status sample to Badge (tone/value, no onClick). Also drops the now-redundant standalone StatusChip demo section on /components (the Badge section already covers all 4 former chip tones plus accent). Updated the two stale StatusChip references outside swarm-ui too: design-guide.md's component-list example and colors.css's WCAG- contrast-rationale comment.
131 lines
4.7 KiB
TypeScript
131 lines
4.7 KiB
TypeScript
// <AgentsPage> — the swarm's agent roster, merged with each agent's open
|
|
// config-PR status in the same table. Two separate asks (a roster listing,
|
|
// and a per-agent config-PR indicator) that turned out to be one page: a
|
|
// roster with no per-row detail is thin, and a config-PR panel with no
|
|
// roster to embed it in has nothing to render against.
|
|
//
|
|
// Two fetches, both on a refresh-interval cadence like HivesPage: `GET
|
|
// /api/agents` (just names — the identity store is the roster, and
|
|
// deliberately says nothing about health) and `GET /api/config-prs`
|
|
// (agent name -> open PR, only agents with one present). Merged
|
|
// client-side into one row per agent rather than N per-agent config-PR
|
|
// calls — exactly why the bulk endpoint exists instead of looping the
|
|
// single-agent one.
|
|
//
|
|
// Owns the "+ agent" trigger too: creation used to be its own
|
|
// `/create-agent` route + nav item, but the roster this populates is
|
|
// the natural home for the action that populates it — a separate top-
|
|
// level nav entry was one click of indirection for no benefit. The form
|
|
// itself (`CreateAgentForm`) is unchanged from its page days, just
|
|
// mounted inside a `Dialog` instead of a route.
|
|
import { useState } from 'preact/hooks';
|
|
import { ApiErrorPanel } from '@hive/shared/api-error-panel.js';
|
|
import { readApiError, type ProblemDetails } from '@hive/shared/api-error.js';
|
|
import { Badge } from '@hive/shared/badge.js';
|
|
import { Button } from '../ui/button/Button.js';
|
|
import { Dialog } from '../ui/dialog/Dialog.js';
|
|
import { Panel } from '../ui/panel/Panel.js';
|
|
import {
|
|
RefreshIntervalPicker,
|
|
useRefreshInterval,
|
|
type RefreshIntervalMs,
|
|
} from '../ui/refresh-interval/RefreshInterval.js';
|
|
import { Table, type TableColumn } from '../ui/table/Table.js';
|
|
import { CreateAgentForm } from './CreateAgentForm.js';
|
|
|
|
interface ConfigPrStatus {
|
|
pr_number: number;
|
|
html_url: string | null;
|
|
}
|
|
|
|
interface AgentRow {
|
|
name: string;
|
|
configPr: ConfigPrStatus | null;
|
|
}
|
|
|
|
const COLUMNS: TableColumn<AgentRow>[] = [
|
|
{ key: 'name', header: 'name', render: (a) => a.name },
|
|
{
|
|
key: 'config-pr',
|
|
header: 'config PR',
|
|
render: (a) =>
|
|
a.configPr ? (
|
|
<Badge
|
|
tone="warning"
|
|
value={
|
|
a.configPr.html_url ? (
|
|
<a href={a.configPr.html_url} target="_blank" rel="noreferrer">
|
|
#{a.configPr.pr_number}
|
|
</a>
|
|
) : (
|
|
`#${a.configPr.pr_number}`
|
|
)
|
|
}
|
|
/>
|
|
) : (
|
|
'—'
|
|
),
|
|
},
|
|
];
|
|
|
|
// Same 30s default + same reasoning as HivesPage: no inputs on this page
|
|
// for a refresh to clobber, so the out-of-the-box behaviour should just
|
|
// solve staleness rather than require an opt-in every visit.
|
|
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
|
|
|
|
export function AgentsPage() {
|
|
const [rows, setRows] = useState<AgentRow[] | null>(null);
|
|
const [error, setError] = useState<ProblemDetails | null>(null);
|
|
const [intervalMs, setIntervalMs] = useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
|
const [createOpen, setCreateOpen] = useState(false);
|
|
|
|
useRefreshInterval(intervalMs, () => {
|
|
(async () => {
|
|
const [namesRes, prsRes] = await Promise.all([fetch('/api/agents'), fetch('/api/config-prs')]);
|
|
if (!namesRes.ok) {
|
|
setError(await readApiError(namesRes));
|
|
return;
|
|
}
|
|
if (!prsRes.ok) {
|
|
setError(await readApiError(prsRes));
|
|
return;
|
|
}
|
|
const names = (await namesRes.json()) as string[];
|
|
const prs = (await prsRes.json()) as Record<string, ConfigPrStatus>;
|
|
setRows(names.map((name) => ({ name, configPr: prs[name] ?? null })));
|
|
// A refresh that succeeds clears a previous failure — otherwise a
|
|
// transient error would sit on screen forever after the data itself
|
|
// has recovered.
|
|
setError(null);
|
|
})().catch((e: unknown) => setError({ detail: String(e) }));
|
|
});
|
|
|
|
return (
|
|
<Panel
|
|
title="agents"
|
|
icon="👥"
|
|
actions={
|
|
<>
|
|
<Button variant="primary" onClick={() => setCreateOpen(true)}>
|
|
+ agent
|
|
</Button>
|
|
<RefreshIntervalPicker id="agents-refresh" value={intervalMs} onChange={setIntervalMs} />
|
|
</>
|
|
}
|
|
>
|
|
{error ? <ApiErrorPanel context="failed to load the agent roster" problem={error} /> : null}
|
|
{!error && rows === null ? <p>loading…</p> : null}
|
|
{rows ? (
|
|
<Table
|
|
columns={COLUMNS}
|
|
rows={rows}
|
|
rowKey={(a) => a.name}
|
|
emptyMessage="no agents yet — the swarm-wide identity store has no agents registered on any hive"
|
|
/>
|
|
) : null}
|
|
<Dialog open={createOpen} onClose={() => setCreateOpen(false)} label="create agent">
|
|
<CreateAgentForm />
|
|
</Dialog>
|
|
</Panel>
|
|
);
|
|
}
|