useSubmit holds the idle, sending, done and error state and the request flow that six forms each wrote out; FormResult shows the success line and the problem panel; FormStack is the column the link, claude-login, inference and limits forms each styled by hand. The create-agent, link account, backend-env and agent-input forms use the submit flow; the link, claude-login, inference and limits forms use FormStack.
153 lines
4.9 KiB
TypeScript
153 lines
4.9 KiB
TypeScript
// <LinkForgeAccountForm> — writes an external forge account (base URL +
|
|
// token) for one agent into the swarm secret store. `kind` picks the
|
|
// route and the wording: "forge" (Forgejo) or "gitlab".
|
|
// PUTs `/api/hives/{hive}/agents/{agent}/{forge,gitlab}-accounts/{label}` — 200
|
|
// (`{ url }`) on success, 400/409/500 as `problem+json`, shown via
|
|
// `ApiErrorPanel` like `LinkMatrixAccountForm`.
|
|
//
|
|
// A forge label is what the agent passes to `hive-forge -f <label>`. A blind
|
|
// set: no route hands a token back. Linked labels and URLs show on
|
|
// the agent panel (`LinkedAccounts`).
|
|
import { useState } from "preact/hooks";
|
|
import { Panel } from "../ui/panel/Panel.js";
|
|
import { TextField } from "../ui/text-field/TextField.js";
|
|
import { Button } from "../ui/button/Button.js";
|
|
import { FormStack } from "../ui/form-stack/FormStack.js";
|
|
import { FormResult } from "../ui/submit/FormResult.js";
|
|
import { useSubmit } from "../ui/submit/useSubmit.js";
|
|
import {
|
|
checkLink,
|
|
LABEL_PATTERN,
|
|
linkBody,
|
|
linkUrl,
|
|
type ForgeLinkKind,
|
|
} from "../agent/detail/account-api.js";
|
|
|
|
// Prefilled for gitlab; the field stays editable for a self-hosted instance.
|
|
const GITLAB_DEFAULT_URL = "https://gitlab.com";
|
|
|
|
export function LinkForgeAccountForm({
|
|
kind,
|
|
hive,
|
|
agent,
|
|
onClose,
|
|
}: {
|
|
kind: ForgeLinkKind;
|
|
hive: string;
|
|
agent: string;
|
|
onClose?: () => void;
|
|
}) {
|
|
const [label, setLabel] = useState("");
|
|
const [url, setUrl] = useState(kind === "gitlab" ? GITLAB_DEFAULT_URL : "");
|
|
const [token, setToken] = useState("");
|
|
// The label is kept with the result, so the success message reads against
|
|
// what was sent even if the field is edited afterwards.
|
|
const { state, submitting, run, fail } = useSubmit<{
|
|
label: string;
|
|
url: string;
|
|
}>();
|
|
|
|
function submit(e: Event) {
|
|
e.preventDefault();
|
|
const invalid = checkLink(label, url, token);
|
|
if (invalid) {
|
|
fail({ detail: invalid });
|
|
return;
|
|
}
|
|
const sent = label;
|
|
void run(
|
|
() =>
|
|
fetch(linkUrl(hive, agent, kind, sent), {
|
|
method: "PUT",
|
|
headers: { "content-type": "application/json" },
|
|
body: linkBody(url, token),
|
|
}),
|
|
{
|
|
parse: async (r) => {
|
|
const body = (await r.json().catch(() => ({}))) as { url?: string };
|
|
return { label: sent, url: body.url ?? url.trim() };
|
|
},
|
|
// The store holds the token now; nothing here needs it.
|
|
onDone: () => setToken(""),
|
|
},
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Panel
|
|
title={`link a ${kind === "gitlab" ? "gitlab" : "forge"} account — ${agent}`}
|
|
icon="🔗"
|
|
onClose={onClose}
|
|
>
|
|
{kind === "gitlab" ? (
|
|
<>
|
|
<p>
|
|
Writes the account to the swarm secret store. The agent fetches it
|
|
within two minutes and can then use <code>glab</code> and{" "}
|
|
<code>git push</code> against the instance. Create the token on
|
|
GitLab first; nothing is created there.
|
|
</p>
|
|
<p>
|
|
Scope the token <code>api</code> and <code>write_repository</code>:
|
|
the first is for <code>glab</code>, the second for pushing over
|
|
HTTPS. Use a dedicated bot account; the agent can act within the
|
|
token's scopes.
|
|
</p>
|
|
</>
|
|
) : (
|
|
<p>
|
|
Writes the account to the swarm secret store. The agent fetches it
|
|
within two minutes and can then use{" "}
|
|
<code>hive-forge -f {label || "<label>"}</code>. Create the token on
|
|
the external forge first; nothing is created there.
|
|
</p>
|
|
)}
|
|
<FormStack onSubmit={submit}>
|
|
<TextField
|
|
id={`${kind}-account-label`}
|
|
label="label"
|
|
value={label}
|
|
pattern={LABEL_PATTERN}
|
|
title="1-63 chars: lowercase letters, digits, hyphen"
|
|
placeholder={kind === "gitlab" ? "e.g. gitlab-com" : "e.g. myforge"}
|
|
required
|
|
onInput={setLabel}
|
|
/>
|
|
<TextField
|
|
id={`${kind}-account-url`}
|
|
label={kind === "gitlab" ? "instance URL" : "forge URL"}
|
|
type="url"
|
|
value={url}
|
|
placeholder={
|
|
kind === "gitlab"
|
|
? "https://gitlab.example.org"
|
|
: "https://forge.example.org"
|
|
}
|
|
required
|
|
onInput={setUrl}
|
|
/>
|
|
<TextField
|
|
id={`${kind}-account-token`}
|
|
label="access token"
|
|
type="password"
|
|
value={token}
|
|
required
|
|
onInput={setToken}
|
|
/>
|
|
<Button variant="primary" type="submit" disabled={submitting}>
|
|
{submitting ? "linking…" : "link account"}
|
|
</Button>
|
|
</FormStack>
|
|
<FormResult
|
|
state={state}
|
|
done={(linked) => (
|
|
<>
|
|
linked <strong>{linked.label}</strong> ({linked.url}) to{" "}
|
|
<strong>{agent}</strong>
|
|
</>
|
|
)}
|
|
failed="failed to link the account"
|
|
/>
|
|
</Panel>
|
|
);
|
|
}
|