Closes #3448. New ui/ primitives: FormField (shared label+control wrapper), TextField, SelectField, Button — each with a min-height touch target (2.75em ~= 44px, WCAG 2.5.5) per mara's #3447 ask, and a max-width instead of a fixed width so the control caps on desktop without overflowing a narrow/touch viewport. CreateAgentPage's name field + submit button now come from the kit instead of page-scoped CSS; ComponentsPage gets a section for each new primitive with an editable sample.
112 lines
4.6 KiB
TypeScript
112 lines
4.6 KiB
TypeScript
// <CreateAgentPage> — 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).
|
|
//
|
|
// 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.
|
|
//
|
|
// First real form in this package — its name field + submit button now
|
|
// come from the shared `ui/` form kit (`TextField`/`Button`) rather than
|
|
// page-scoped input/button chrome, so a hive-picker soon landing on this
|
|
// same page has something to reuse instead of copying this page's CSS.
|
|
import { useState } from 'preact/hooks';
|
|
import { Link } from 'wouter-preact';
|
|
import { ApiErrorPanel } from '@hive/shared/api-error-panel.js';
|
|
import { readApiError, type ProblemDetails } from '@hive/shared/api-error.js';
|
|
import { Panel } from '../ui/panel/Panel.js';
|
|
import { TextField } from '../ui/text-field/TextField.js';
|
|
import { Button } from '../ui/button/Button.js';
|
|
import './CreateAgentPage.css';
|
|
|
|
interface CreateAgentResponse {
|
|
node_id: number;
|
|
}
|
|
|
|
type SubmitState =
|
|
| { status: 'idle' }
|
|
| { status: 'submitting' }
|
|
| { status: 'done'; nodeId: number }
|
|
| { status: 'error'; problem: ProblemDetails };
|
|
|
|
// Client-side mirror of `hive_types::Ident` (1-63 chars of `[a-z0-9-]`) —
|
|
// a `pattern` hint only, not a substitute for the server's own
|
|
// validation; a name this rejects would fail server-side anyway, so
|
|
// catching it before a round-trip is a pure UX win, not a new gate.
|
|
//
|
|
// Hyphen escaped (`\-`) rather than left trailing in the class: modern
|
|
// browsers validate the `pattern` attribute's regex in Unicode-set (`v`)
|
|
// mode, which is far stricter about hyphen placement than classic mode —
|
|
// `[a-z0-9-]` throws "Invalid character class" under `v` mode even though
|
|
// it's valid classic-mode regex (a trailing `-` is unambiguous there).
|
|
// Reproduced directly: `new RegExp('[a-z0-9-]', 'v')` throws,
|
|
// `new RegExp('[a-z0-9\\-]', 'v')` doesn't — confirmed via mara's console
|
|
// exception report on this page.
|
|
const NAME_PATTERN = '[a-z0-9\\-]{1,63}';
|
|
|
|
export function CreateAgentPage() {
|
|
const [name, setName] = useState('');
|
|
const [result, setResult] = useState<SubmitState>({ status: 'idle' });
|
|
|
|
async function submit(e: Event) {
|
|
e.preventDefault();
|
|
setResult({ status: 'submitting' });
|
|
try {
|
|
const r = await fetch('/api/agents', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ name }),
|
|
});
|
|
if (!r.ok) {
|
|
setResult({ status: 'error', problem: await readApiError(r) });
|
|
return;
|
|
}
|
|
const data = (await r.json()) as CreateAgentResponse;
|
|
setResult({ status: 'done', nodeId: data.node_id });
|
|
setName('');
|
|
} catch (err) {
|
|
setResult({ status: 'error', problem: { detail: String(err) } });
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Panel title="create agent">
|
|
<p class="create-agent-intro">
|
|
Create a new agent's swarm-level identity. This only queues the job — check{' '}
|
|
<Link href="/jobs">jobs</Link> to watch it settle.
|
|
</p>
|
|
<form class="create-agent-form" onSubmit={submit}>
|
|
<TextField
|
|
id="agent-name"
|
|
label="agent name"
|
|
value={name}
|
|
pattern={NAME_PATTERN}
|
|
title="1-63 chars: lowercase letters, digits, hyphens"
|
|
required
|
|
onInput={setName}
|
|
/>
|
|
<Button variant="primary" type="submit" disabled={result.status === 'submitting'}>
|
|
{result.status === 'submitting' ? 'creating…' : 'create'}
|
|
</Button>
|
|
</form>
|
|
{result.status === 'done' && (
|
|
<p class="create-agent-result create-agent-result-ok">
|
|
queued as job node #{result.nodeId} — <Link href="/jobs">watch it in jobs</Link>
|
|
</p>
|
|
)}
|
|
{result.status === 'error' && (
|
|
<ApiErrorPanel context="failed to queue" problem={result.problem} />
|
|
)}
|
|
</Panel>
|
|
);
|
|
}
|