swarm-ui: minimal agent creation page
Adds /agents: a name field that POSTs to swarm-controller's POST /api/agents (from the CreateIdentity work), shows the queued job node id, and links to /jobs to watch it settle. Scope matches the issue exactly - no forge/deploy options, those aren't wired server-side yet.
This commit is contained in:
parent
abf3a9e530
commit
d044281040
4 changed files with 151 additions and 0 deletions
|
|
@ -2,6 +2,7 @@
|
|||
// page component under `./pages/`; this file just maps paths to them.
|
||||
import { Route, Switch } from 'wouter-preact';
|
||||
import { Shell } from './shell/Shell.js';
|
||||
import { AgentsPage } from './pages/AgentsPage.js';
|
||||
import { ComponentsPage } from './pages/ComponentsPage.js';
|
||||
import { JobsPage } from './pages/JobsPage.js';
|
||||
import { OverviewPage } from './pages/OverviewPage.js';
|
||||
|
|
@ -20,6 +21,7 @@ export function App() {
|
|||
<Shell>
|
||||
<Switch>
|
||||
<Route path="/" component={OverviewPage} />
|
||||
<Route path="/agents" component={AgentsPage} />
|
||||
<Route path="/jobs" component={JobsPage} />
|
||||
<Route path="/components" component={ComponentsPage} />
|
||||
<Route component={NotFound} />
|
||||
|
|
|
|||
54
frontend/packages/swarm-ui/src/pages/AgentsPage.css
Normal file
54
frontend/packages/swarm-ui/src/pages/AgentsPage.css
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/* <AgentsPage> — the create-agent form. First form in this package, so
|
||||
its input/button chrome lives here rather than in `ui/` — see the
|
||||
component's own comment for why. Reuses the same base16 slots
|
||||
(../theme.css) every other component draws from. */
|
||||
.agents-intro {
|
||||
margin: 0 0 1.5em;
|
||||
color: var(--muted);
|
||||
}
|
||||
.agents-form {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.75em;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.agents-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3em;
|
||||
}
|
||||
.agents-label {
|
||||
font-size: 0.85em;
|
||||
color: var(--muted);
|
||||
}
|
||||
.agents-input {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--purple-dim);
|
||||
border-radius: 0.3em;
|
||||
padding: 0.4em 0.6em;
|
||||
font: inherit;
|
||||
min-width: 16em;
|
||||
}
|
||||
.agents-submit {
|
||||
background: var(--purple-dim);
|
||||
color: var(--fg);
|
||||
border: none;
|
||||
border-radius: 0.3em;
|
||||
padding: 0.5em 1.2em;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.agents-submit:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
.agents-result {
|
||||
margin-top: 1em;
|
||||
}
|
||||
.agents-result-ok {
|
||||
color: var(--green);
|
||||
}
|
||||
.agents-result-error {
|
||||
color: var(--red);
|
||||
}
|
||||
94
frontend/packages/swarm-ui/src/pages/AgentsPage.tsx
Normal file
94
frontend/packages/swarm-ui/src/pages/AgentsPage.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// <AgentsPage> — 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).
|
||||
//
|
||||
// First real form in this package — no shared form kit exists yet, so
|
||||
// the input/button styling below is scoped to `AgentsPage.css` rather
|
||||
// than promoted into `ui/`. Promote the day a second page needs one,
|
||||
// same "don't build ahead of a second caller" rule `Panel`'s own
|
||||
// comment states.
|
||||
import { useState } from 'preact/hooks';
|
||||
import { Link } from 'wouter-preact';
|
||||
import { Panel } from '../ui/panel/Panel.js';
|
||||
import './AgentsPage.css';
|
||||
|
||||
interface CreateAgentResponse {
|
||||
node_id: number;
|
||||
}
|
||||
|
||||
type SubmitState =
|
||||
| { status: 'idle' }
|
||||
| { status: 'submitting' }
|
||||
| { status: 'done'; nodeId: number }
|
||||
| { status: 'error'; message: string };
|
||||
|
||||
// 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.
|
||||
const NAME_PATTERN = '[a-z0-9-]{1,63}';
|
||||
|
||||
export function AgentsPage() {
|
||||
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) throw new Error((await r.text()) || `http ${r.status}`);
|
||||
const data = (await r.json()) as CreateAgentResponse;
|
||||
setResult({ status: 'done', nodeId: data.node_id });
|
||||
setName('');
|
||||
} catch (err) {
|
||||
setResult({ status: 'error', message: String(err) });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel title="agents">
|
||||
<p class="agents-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="agents-form" onSubmit={submit}>
|
||||
<div class="agents-field">
|
||||
<label class="agents-label" for="agent-name">
|
||||
agent name
|
||||
</label>
|
||||
<input
|
||||
id="agent-name"
|
||||
class="agents-input"
|
||||
type="text"
|
||||
value={name}
|
||||
pattern={NAME_PATTERN}
|
||||
title="1-63 chars: lowercase letters, digits, hyphens"
|
||||
required
|
||||
onInput={(e) => setName(e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
<button class="agents-submit" type="submit" disabled={result.status === 'submitting'}>
|
||||
{result.status === 'submitting' ? 'creating…' : 'create'}
|
||||
</button>
|
||||
</form>
|
||||
{result.status === 'done' && (
|
||||
<p class="agents-result agents-result-ok">
|
||||
queued as job node #{result.nodeId} — <Link href="/jobs">watch it in jobs</Link>
|
||||
</p>
|
||||
)}
|
||||
{result.status === 'error' && (
|
||||
<p class="agents-result agents-result-error">failed to queue: {result.message}</p>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import './Shell.css';
|
|||
|
||||
const NAV_ITEMS: { href: string; label: string }[] = [
|
||||
{ href: '/', label: 'overview' },
|
||||
{ href: '/agents', label: 'agents' },
|
||||
{ href: '/jobs', label: 'jobs' },
|
||||
{ href: '/components', label: 'components' },
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in a new issue