Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: link a gitlab account to an agent

This commit is contained in:
atlas 2026-11-02 17:27:48 +01:00 • committed by mara
commit 3e5b5b54a2
4 changed files with 240 additions and 56 deletions

View file

@ -8,11 +8,12 @@ import { Badge } from "@hive/shared/badge.js";
import { Dropdown, type DropdownOption } from "@hive/shared/dropdown.js";
import { LinkIcon } from "@hive/shared/icons.js";
export type LinkKind = "matrix" | "forge" | "github" | "backend";
export type LinkKind = "matrix" | "forge" | "gitlab" | "github" | "backend";
const OPTIONS: DropdownOption[] = [
{ value: "matrix", label: "matrix account" },
{ value: "forge", label: "forge account" },
{ value: "gitlab", label: "gitlab account" },
{ value: "github", label: "github account" },
{ value: "backend", label: "API-key environment" },
];
@ -41,7 +42,7 @@ export function LinkAccountMenu({
disabled={!hasHive}
title={
hasHive
? `link a matrix, forge or github account, or set the API-key environment, for ${agent}`
? `link a matrix, forge, gitlab or github account, or set the API-key environment, for ${agent}`
: "no hive on record for this agent — nothing to link against"
}
/>

View file

@ -16,6 +16,7 @@ import { Badge } from "@hive/shared/badge.js";
import { CheckboxField } from "../../ui/checkbox-field/CheckboxField.js";
import { ConfirmDialog } from "../../ui/confirm-dialog/ConfirmDialog.js";
import { accountKindLabel } from "./account-kind.js";
import { deleteUrl, listUrl } from "./account-api.js";
import "./LinkedAccounts.css";
type AccountKind = "matrix" | "forgejo" | "gitlab" | "github" | "backend";
@ -28,23 +29,6 @@ interface LinkedAccount {
reserved: boolean;
}
// The DELETE route for one account, beside the PUT that links it.
function accountUrl(hive: string, agent: string, a: LinkedAccount): string {
const base = `/api/hives/${encodeURIComponent(hive)}/agents/${encodeURIComponent(agent)}`;
switch (a.kind) {
case "matrix":
return `${base}/matrix-accounts/${encodeURIComponent(a.name)}`;
case "forgejo":
return `${base}/forge-accounts/${encodeURIComponent(a.name)}`;
case "gitlab":
return `${base}/gitlab-accounts/${encodeURIComponent(a.name)}`;
case "github":
return `${base}/github-account`;
case "backend":
return `${base}/backend-env`;
}
}
export function LinkedAccounts({
hive,
agent,
@ -68,9 +52,7 @@ export function LinkedAccounts({
useEffect(() => {
let cancelled = false;
(async () => {
const r = await fetch(
`/api/hives/${encodeURIComponent(hive)}/agents/${encodeURIComponent(agent)}/linked-accounts`,
);
const r = await fetch(listUrl(hive, agent));
if (!r.ok) {
if (!cancelled) setError(await readApiError(r));
return;
@ -97,7 +79,7 @@ export function LinkedAccounts({
if (!deleteTarget) return;
setDeleting(true);
try {
const url = accountUrl(hive, agent, deleteTarget);
const url = deleteUrl(hive, agent, deleteTarget);
const r = await fetch(
deleteTarget.kind === "matrix" && revoke ? `${url}?revoke=true` : url,
{ method: "DELETE" },

View file

@ -0,0 +1,72 @@
// The routes and request body behind linking, listing and deleting an
// external forge account. Pure, so `npm test` runs them without a browser.
/** The kinds that share the label + URL + token shape. */
export type ForgeLinkKind = "forge" | "gitlab";
/** `hive_types::Ident`, which the controller checks for the label. */
export const LABEL_PATTERN = "[a-z0-9\\-]{1,63}";
function agentBase(hive: string, agent: string): string {
return `/api/hives/${encodeURIComponent(hive)}/agents/${encodeURIComponent(agent)}`;
}
/** PUT target for linking `label` under `kind`. */
export function linkUrl(
hive: string,
agent: string,
kind: ForgeLinkKind,
label: string,
): string {
const route = kind === "gitlab" ? "gitlab-accounts" : "forge-accounts";
return `${agentBase(hive, agent)}/${route}/${encodeURIComponent(label)}`;
}
/** The PUT body; the token is sent as typed, the URL trimmed. */
export function linkBody(url: string, token: string): string {
return JSON.stringify({ url: url.trim(), token });
}
/** GET target for the agent's linked accounts. */
export function listUrl(hive: string, agent: string): string {
return `${agentBase(hive, agent)}/linked-accounts`;
}
/** DELETE target for one linked account, as listed by `listUrl`. */
export function deleteUrl(
hive: string,
agent: string,
a: { kind: string; name: string },
): string {
const base = agentBase(hive, agent);
const name = encodeURIComponent(a.name);
switch (a.kind) {
case "matrix":
return `${base}/matrix-accounts/${name}`;
case "forgejo":
return `${base}/forge-accounts/${name}`;
case "gitlab":
return `${base}/gitlab-accounts/${name}`;
case "github":
return `${base}/github-account`;
default:
return `${base}/backend-env`;
}
}
/** A message for input the controller would refuse anyway, or null. The
* controller's own URL rule stays the gate and its 400 is shown as is. */
export function checkLink(
label: string,
url: string,
token: string,
): string | null {
if (!new RegExp(`^${LABEL_PATTERN}$`).test(label)) {
return "label: 1-63 chars, lowercase letters, digits, hyphen";
}
if (!/^https?:\/\/\S+$/.test(url.trim())) {
return "URL must start with http:// or https://";
}
if (token === "") return "token is required";
return null;
}

View file

@ -98,6 +98,7 @@ export function useAgentActions(
const [customTarget, setCustomTarget] = useState<AgentRow | null>(null);
const [matrixTarget, setMatrixTarget] = useState<AgentRow | null>(null);
const [forgeTarget, setForgeTarget] = useState<AgentRow | null>(null);
const [gitlabTarget, setGitlabTarget] = useState<AgentRow | null>(null);
const [githubTarget, setGithubTarget] = useState<AgentRow | null>(null);
const [backendTarget, setBackendTarget] = useState<AgentRow | null>(null);
const [linkVersion, setLinkVersion] = useState(0);
@ -111,6 +112,10 @@ export function useAgentActions(
setForgeTarget(null);
linkClosed();
};
const closeGitlab = () => {
setGitlabTarget(null);
linkClosed();
};
const closeGithub = () => {
setGithubTarget(null);
linkClosed();
@ -233,12 +238,28 @@ export function useAgentActions(
>
{forgeTarget?.hive ? (
<LinkForgeAccountForm
kind="forge"
hive={forgeTarget.hive}
agent={forgeTarget.name}
onClose={closeForge}
/>
) : null}
</Dialog>
<Dialog
open={gitlabTarget !== null}
onClose={closeGitlab}
label="link a gitlab account"
plain
>
{gitlabTarget?.hive ? (
<LinkForgeAccountForm
kind="gitlab"
hive={gitlabTarget.hive}
agent={gitlabTarget.name}
onClose={closeGitlab}
/>
) : null}
</Dialog>
<Dialog
open={githubTarget !== null}
onClose={closeGithub}
@ -330,6 +351,7 @@ export function useAgentActions(
openLink: (row, kind) => {
if (kind === "matrix") setMatrixTarget(row);
else if (kind === "forge") setForgeTarget(row);
else if (kind === "gitlab") setGitlabTarget(row);
else if (kind === "github") setGithubTarget(row);
else setBackendTarget(row);
},