swarm-ui: link a gitlab account to an agent
This commit is contained in:
parent
b7d8c0dfc0
commit
3e5b5b54a2
4 changed files with 240 additions and 56 deletions
|
|
@ -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"
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
72
frontend/packages/swarm-ui/src/agent/detail/account-api.ts
Normal file
72
frontend/packages/swarm-ui/src/agent/detail/account-api.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue