swarm-ui: move create-agent page off /agents, rename to CreateAgentPage
mara: the route should reflect creating an agent, and stay separate from a future agent list page. Renamed AgentsPage -> CreateAgentPage (file, component, css classes) and moved the route from /agents to /create-agent - flat, not /agents/new, since index.html's relative asset links only resolve correctly one path segment deep (filed separately as a real bug, not fixed here). Leaves the bare /agents path free for a future roster page.
This commit is contained in:
parent
d044281040
commit
47e89c1c93
4 changed files with 48 additions and 37 deletions
|
|
@ -2,8 +2,8 @@
|
||||||
// page component under `./pages/`; this file just maps paths to them.
|
// page component under `./pages/`; this file just maps paths to them.
|
||||||
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 { AgentsPage } from './pages/AgentsPage.js';
|
|
||||||
import { ComponentsPage } from './pages/ComponentsPage.js';
|
import { ComponentsPage } from './pages/ComponentsPage.js';
|
||||||
|
import { CreateAgentPage } from './pages/CreateAgentPage.js';
|
||||||
import { JobsPage } from './pages/JobsPage.js';
|
import { JobsPage } from './pages/JobsPage.js';
|
||||||
import { OverviewPage } from './pages/OverviewPage.js';
|
import { OverviewPage } from './pages/OverviewPage.js';
|
||||||
import { Panel } from './ui/panel/Panel.js';
|
import { Panel } from './ui/panel/Panel.js';
|
||||||
|
|
@ -21,7 +21,7 @@ export function App() {
|
||||||
<Shell>
|
<Shell>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" component={OverviewPage} />
|
<Route path="/" component={OverviewPage} />
|
||||||
<Route path="/agents" component={AgentsPage} />
|
<Route path="/create-agent" component={CreateAgentPage} />
|
||||||
<Route path="/jobs" component={JobsPage} />
|
<Route path="/jobs" component={JobsPage} />
|
||||||
<Route path="/components" component={ComponentsPage} />
|
<Route path="/components" component={ComponentsPage} />
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
/* <AgentsPage> — the create-agent form. First form in this package, so
|
/* <CreateAgentPage> — the create-agent form. First form in this package,
|
||||||
its input/button chrome lives here rather than in `ui/` — see the
|
so its input/button chrome lives here rather than in `ui/` — see the
|
||||||
component's own comment for why. Reuses the same base16 slots
|
component's own comment for why. Reuses the same base16 slots
|
||||||
(../theme.css) every other component draws from. */
|
(../theme.css) every other component draws from. */
|
||||||
.agents-intro {
|
.create-agent-intro {
|
||||||
margin: 0 0 1.5em;
|
margin: 0 0 1.5em;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
.agents-form {
|
.create-agent-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
gap: 0.75em;
|
gap: 0.75em;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.agents-field {
|
.create-agent-field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.3em;
|
gap: 0.3em;
|
||||||
}
|
}
|
||||||
.agents-label {
|
.create-agent-label {
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
.agents-input {
|
.create-agent-input {
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
border: 1px solid var(--purple-dim);
|
border: 1px solid var(--purple-dim);
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
font: inherit;
|
font: inherit;
|
||||||
min-width: 16em;
|
min-width: 16em;
|
||||||
}
|
}
|
||||||
.agents-submit {
|
.create-agent-submit {
|
||||||
background: var(--purple-dim);
|
background: var(--purple-dim);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -39,16 +39,16 @@
|
||||||
font: inherit;
|
font: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.agents-submit:disabled {
|
.create-agent-submit:disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
.agents-result {
|
.create-agent-result {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
.agents-result-ok {
|
.create-agent-result-ok {
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
}
|
}
|
||||||
.agents-result-error {
|
.create-agent-result-error {
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
}
|
}
|
||||||
|
|
@ -1,21 +1,30 @@
|
||||||
// <AgentsPage> — minimal agent creation form, POSTs to swarm-controller's
|
// <CreateAgentPage> — minimal agent creation form, POSTs to
|
||||||
// `POST /api/agents`. That endpoint only queues a `CreateIdentity` job
|
// swarm-controller's `POST /api/agents`. That endpoint only queues a
|
||||||
// and returns its node id — the identity isn't guaranteed to exist yet
|
// `CreateIdentity` job and returns its node id — the identity isn't
|
||||||
// when this page gets a response — so this page shows the queued
|
// guaranteed to exist yet when this page gets a response — so this
|
||||||
// confirmation and links to the Jobs page to watch it settle, rather
|
// page shows the queued confirmation and links to the Jobs page to
|
||||||
// than duplicating JobqGraph's per-node polling here. Scope matches the
|
// watch it settle, rather than duplicating JobqGraph's per-node
|
||||||
// originating issue exactly: name field only, no forge/deploy options
|
// polling here. Scope matches the originating issue exactly: name
|
||||||
// (those aren't wired server-side yet either).
|
// 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 — no shared form kit exists yet, so
|
// First real form in this package — no shared form kit exists yet, so
|
||||||
// the input/button styling below is scoped to `AgentsPage.css` rather
|
// the input/button styling below is scoped to `CreateAgentPage.css`
|
||||||
// than promoted into `ui/`. Promote the day a second page needs one,
|
// rather than promoted into `ui/`. Promote the day a second page needs
|
||||||
// same "don't build ahead of a second caller" rule `Panel`'s own
|
// one, same "don't build ahead of a second caller" rule `Panel`'s own
|
||||||
// comment states.
|
// comment states.
|
||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
import { Link } from 'wouter-preact';
|
import { Link } from 'wouter-preact';
|
||||||
import { Panel } from '../ui/panel/Panel.js';
|
import { Panel } from '../ui/panel/Panel.js';
|
||||||
import './AgentsPage.css';
|
import './CreateAgentPage.css';
|
||||||
|
|
||||||
interface CreateAgentResponse {
|
interface CreateAgentResponse {
|
||||||
node_id: number;
|
node_id: number;
|
||||||
|
|
@ -33,7 +42,7 @@ type SubmitState =
|
||||||
// catching it before a round-trip is a pure UX win, not a new gate.
|
// catching it before a round-trip is a pure UX win, not a new gate.
|
||||||
const NAME_PATTERN = '[a-z0-9-]{1,63}';
|
const NAME_PATTERN = '[a-z0-9-]{1,63}';
|
||||||
|
|
||||||
export function AgentsPage() {
|
export function CreateAgentPage() {
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [result, setResult] = useState<SubmitState>({ status: 'idle' });
|
const [result, setResult] = useState<SubmitState>({ status: 'idle' });
|
||||||
|
|
||||||
|
|
@ -56,19 +65,19 @@ export function AgentsPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel title="agents">
|
<Panel title="create agent">
|
||||||
<p class="agents-intro">
|
<p class="create-agent-intro">
|
||||||
Create a new agent's swarm-level identity. This only queues the job — check{' '}
|
Create a new agent's swarm-level identity. This only queues the job — check{' '}
|
||||||
<Link href="/jobs">jobs</Link> to watch it settle.
|
<Link href="/jobs">jobs</Link> to watch it settle.
|
||||||
</p>
|
</p>
|
||||||
<form class="agents-form" onSubmit={submit}>
|
<form class="create-agent-form" onSubmit={submit}>
|
||||||
<div class="agents-field">
|
<div class="create-agent-field">
|
||||||
<label class="agents-label" for="agent-name">
|
<label class="create-agent-label" for="agent-name">
|
||||||
agent name
|
agent name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="agent-name"
|
id="agent-name"
|
||||||
class="agents-input"
|
class="create-agent-input"
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
pattern={NAME_PATTERN}
|
pattern={NAME_PATTERN}
|
||||||
|
|
@ -77,17 +86,19 @@ export function AgentsPage() {
|
||||||
onInput={(e) => setName(e.currentTarget.value)}
|
onInput={(e) => setName(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button class="agents-submit" type="submit" disabled={result.status === 'submitting'}>
|
<button class="create-agent-submit" type="submit" disabled={result.status === 'submitting'}>
|
||||||
{result.status === 'submitting' ? 'creating…' : 'create'}
|
{result.status === 'submitting' ? 'creating…' : 'create'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{result.status === 'done' && (
|
{result.status === 'done' && (
|
||||||
<p class="agents-result agents-result-ok">
|
<p class="create-agent-result create-agent-result-ok">
|
||||||
queued as job node #{result.nodeId} — <Link href="/jobs">watch it in jobs</Link>
|
queued as job node #{result.nodeId} — <Link href="/jobs">watch it in jobs</Link>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{result.status === 'error' && (
|
{result.status === 'error' && (
|
||||||
<p class="agents-result agents-result-error">failed to queue: {result.message}</p>
|
<p class="create-agent-result create-agent-result-error">
|
||||||
|
failed to queue: {result.message}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
|
|
@ -19,7 +19,7 @@ import './Shell.css';
|
||||||
|
|
||||||
const NAV_ITEMS: { href: string; label: string }[] = [
|
const NAV_ITEMS: { href: string; label: string }[] = [
|
||||||
{ href: '/', label: 'overview' },
|
{ href: '/', label: 'overview' },
|
||||||
{ href: '/agents', label: 'agents' },
|
{ href: '/create-agent', label: 'new agent' },
|
||||||
{ href: '/jobs', label: 'jobs' },
|
{ href: '/jobs', label: 'jobs' },
|
||||||
{ href: '/components', label: 'components' },
|
{ href: '/components', label: 'components' },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue