The success line of both link forms read the live text field, so editing the name after a link changed the sentence. The result now carries the name that was sent. The matrix form held the typed token and password after a failed submit, and kept them across a switch of credential mode. It now drops them when the request ends and when the mode changes. The linked-accounts list and the delete dialog said forgejo where the link menu and forms say forge; they now say forge.
131 lines
4.3 KiB
TypeScript
131 lines
4.3 KiB
TypeScript
// <LinkForgeAccountForm> — writes an external forge account (base URL +
|
|
// token) for one agent into the swarm secret store.
|
|
// PUTs `/api/hives/{hive}/agents/{agent}/forge-accounts/{label}` — 200
|
|
// (`{ url }`) on success, 400/409/500 as `problem+json`, shown via
|
|
// `ApiErrorPanel` like `LinkMatrixAccountForm`.
|
|
//
|
|
// The 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 { 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 "./LinkMatrixAccountForm.css";
|
|
|
|
// `hive_types::Ident`, which the server checks: `hive-forge -f` accepts
|
|
// nothing wider. A UX hint only; the server is the gate.
|
|
const LABEL_PATTERN = "[a-z0-9\\-]{1,63}";
|
|
|
|
type SubmitState =
|
|
| { status: "idle" }
|
|
| { status: "submitting" }
|
|
| { status: "done"; label: string; url: string }
|
|
| { status: "error"; problem: ProblemDetails };
|
|
|
|
export function LinkForgeAccountForm({
|
|
hive,
|
|
agent,
|
|
onClose,
|
|
}: {
|
|
hive: string;
|
|
agent: string;
|
|
onClose?: () => void;
|
|
}) {
|
|
const [label, setLabel] = useState("");
|
|
const [url, setUrl] = useState("");
|
|
const [token, setToken] = useState("");
|
|
const [result, setResult] = useState<SubmitState>({ status: "idle" });
|
|
|
|
async function submit(e: Event) {
|
|
e.preventDefault();
|
|
setResult({ status: "submitting" });
|
|
try {
|
|
const r = await fetch(
|
|
`/api/hives/${encodeURIComponent(hive)}/agents/${encodeURIComponent(agent)}/forge-accounts/${encodeURIComponent(label)}`,
|
|
{
|
|
method: "PUT",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ url: url.trim(), token }),
|
|
},
|
|
);
|
|
if (!r.ok) {
|
|
setResult({ status: "error", problem: await readApiError(r) });
|
|
return;
|
|
}
|
|
const body = (await r.json().catch(() => ({}))) as { url?: string };
|
|
// The label is stored with the result, so the success message reads
|
|
// against what was sent even if the field is edited afterwards.
|
|
setResult({ status: "done", label, url: body.url ?? url.trim() });
|
|
// The store holds the token now; nothing here needs it.
|
|
setToken("");
|
|
} catch (err) {
|
|
setResult({ status: "error", problem: { detail: String(err) } });
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Panel
|
|
title={`link a forge account — ${agent}`}
|
|
icon="🔗"
|
|
onClose={onClose}
|
|
>
|
|
<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>
|
|
<form class="link-matrix-account-form" onSubmit={submit}>
|
|
<TextField
|
|
id="forge-account-label"
|
|
label="label"
|
|
value={label}
|
|
pattern={LABEL_PATTERN}
|
|
title="1-63 chars: lowercase letters, digits, hyphen"
|
|
placeholder="e.g. myforge"
|
|
required
|
|
onInput={setLabel}
|
|
/>
|
|
<TextField
|
|
id="forge-account-url"
|
|
label="forge URL"
|
|
type="url"
|
|
value={url}
|
|
placeholder="https://forge.example.org"
|
|
required
|
|
onInput={setUrl}
|
|
/>
|
|
<TextField
|
|
id="forge-account-token"
|
|
label="access token"
|
|
type="password"
|
|
value={token}
|
|
required
|
|
onInput={setToken}
|
|
/>
|
|
<Button
|
|
variant="primary"
|
|
type="submit"
|
|
disabled={result.status === "submitting"}
|
|
>
|
|
{result.status === "submitting" ? "linking…" : "link account"}
|
|
</Button>
|
|
</form>
|
|
{result.status === "done" && (
|
|
<p class="link-matrix-account-result-ok">
|
|
linked <strong>{result.label}</strong> ({result.url}) to{" "}
|
|
<strong>{agent}</strong>
|
|
</p>
|
|
)}
|
|
{result.status === "error" && (
|
|
<ApiErrorPanel
|
|
context="failed to link the account"
|
|
problem={result.problem}
|
|
/>
|
|
)}
|
|
</Panel>
|
|
);
|
|
}
|