swarm-ui: move agent creation into the agents page as a dialog
Adds a generic ui/dialog/Dialog primitive (native <dialog>, no third-party modal lib and no shadow-DOM custom element per the esbuild gap on those) and wires a "+ agent" button into AgentsPage that opens the existing create-agent form inside it, content unchanged from its former life as a standalone /create-agent route/nav item. Removes the "new agent" top-level nav entry and the /create-agent route entirely -- creation now only reachable from the roster that gets populated by it. CreateAgentPage.tsx/css renamed to CreateAgentForm.tsx/css to match its new role as a mounted component rather than a page. Screenshot-verified the dialog open/closed states against a mock server.
This commit is contained in:
parent
dabd0fc823
commit
13d7c73c12
7 changed files with 165 additions and 23 deletions
|
|
@ -11,9 +11,18 @@
|
|||
// 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 { Button } from '../ui/button/Button.js';
|
||||
import { Dialog } from '../ui/dialog/Dialog.js';
|
||||
import { Panel } from '../ui/panel/Panel.js';
|
||||
import {
|
||||
RefreshIntervalPicker,
|
||||
|
|
@ -22,6 +31,7 @@ import {
|
|||
} from '../ui/refresh-interval/RefreshInterval.js';
|
||||
import { StatusChip } from '../ui/status-chip/StatusChip.js';
|
||||
import { Table, type TableColumn } from '../ui/table/Table.js';
|
||||
import { CreateAgentForm } from './CreateAgentForm.js';
|
||||
|
||||
interface ConfigPrStatus {
|
||||
pr_number: number;
|
||||
|
|
@ -67,6 +77,7 @@ 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 () => {
|
||||
|
|
@ -93,11 +104,21 @@ export function AgentsPage() {
|
|||
<Panel
|
||||
title="agents"
|
||||
icon="👥"
|
||||
actions={<RefreshIntervalPicker id="agents-refresh" value={intervalMs} onChange={setIntervalMs} />}
|
||||
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} /> : null}
|
||||
<Dialog open={createOpen} onClose={() => setCreateOpen(false)} label="create agent">
|
||||
<CreateAgentForm />
|
||||
</Dialog>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
/* <CreateAgentPage> — the create-agent form. Input/button chrome now
|
||||
comes from the shared `ui/` form kit (`TextField`/`SelectField`/
|
||||
`Button`); this file only owns the page's own layout + copy. Column,
|
||||
/* <CreateAgentForm> — the create-agent form, rendered inside a `Dialog`
|
||||
from `AgentsPage` (see that component's file-top comment for why).
|
||||
Input/button chrome comes from the shared `ui/` form kit
|
||||
(`TextField`/`SelectField`/`Button`); this file only owns this
|
||||
component's own layout + copy. Column,
|
||||
not row inside the form itself: with a second field (hive) added, a
|
||||
row layout put fields of different natural widths on one baseline and
|
||||
looked misaligned — mara: "make it a col". Reuses the same base16
|
||||
|
|
@ -1,20 +1,19 @@
|
|||
// <CreateAgentPage> — minimal agent creation form, POSTs to
|
||||
// <CreateAgentForm> — minimal agent creation form, POSTs to
|
||||
// swarm-controller's `POST /api/agents`. That endpoint only queues a
|
||||
// `CreateIdentity` job and returns its node id — the identity isn't
|
||||
// guaranteed to exist yet when this page gets a response — so this
|
||||
// page shows the queued confirmation and links to the Jobs page to
|
||||
// watch it settle, rather than duplicating JobqGraph's per-node
|
||||
// polling here. Scope matches the originating issue exactly: name
|
||||
// field only, no forge/deploy options (those aren't wired server-side
|
||||
// yet either).
|
||||
// guaranteed to exist yet when this gets a response — so this shows
|
||||
// the queued confirmation and links to the Jobs page to watch it
|
||||
// settle, rather than duplicating JobqGraph's per-node polling here.
|
||||
// Scope matches the originating issue exactly: name field only, no
|
||||
// forge/deploy options (those aren't wired server-side yet either).
|
||||
//
|
||||
// Lives at `/create-agent`, not `/agents` — a future agent *roster*
|
||||
// (listing existing agents) is the natural owner of the bare `/agents`
|
||||
// path, and this creation form is a distinct action from that list,
|
||||
// not a variant of it. Flat, not `/agents/new`: `index.html`'s asset
|
||||
// links are relative (`static/main.js`), which only resolve correctly
|
||||
// one path segment deep — a real bug, filed separately rather than
|
||||
// fixed here, but reason enough to avoid a nested route today.
|
||||
// Rendered inside a `Dialog` from `AgentsPage`'s own "+ agent" button,
|
||||
// not a standalone route — the roster it feeds is the natural home for
|
||||
// the action that populates it, and a distinct top-level nav item next
|
||||
// to it was one click of indirection for no benefit. Kept as its own
|
||||
// component (not inlined into AgentsPage) since the two-panel layout
|
||||
// below is unchanged from its former life as a page: same content, same
|
||||
// review history, just a different mount point.
|
||||
//
|
||||
// `hive` field added because agent creation had no way to record which
|
||||
// hive an agent runs on. `POST /api/agents` now requires it, so the
|
||||
|
|
@ -34,7 +33,7 @@ import { Panel } from '../ui/panel/Panel.js';
|
|||
import { TextField } from '../ui/text-field/TextField.js';
|
||||
import { SelectField, type SelectOption } from '../ui/select-field/SelectField.js';
|
||||
import { Button } from '../ui/button/Button.js';
|
||||
import './CreateAgentPage.css';
|
||||
import './CreateAgentForm.css';
|
||||
|
||||
// Mirrors swarm-controller's `HiveEntry` — same shape `HivesPage`
|
||||
// consumes off `/api/hives/status`, but this page hits the plain
|
||||
|
|
@ -70,7 +69,7 @@ type SubmitState =
|
|||
// exception report on this page.
|
||||
const NAME_PATTERN = '[a-z0-9\\-]{1,63}';
|
||||
|
||||
export function CreateAgentPage() {
|
||||
export function CreateAgentForm() {
|
||||
const [name, setName] = useState('');
|
||||
const [hive, setHive] = useState('');
|
||||
// `null` = still loading, `[]` = loaded but empty (a real, if unusual,
|
||||
Loading…
Reference in a new issue