xtap-pool / explorer /src /components /AdminPanel.tsx
osolmaz's picture
osolmaz HF Staff
deploy: eaa4b86
879c715 verified
Raw
History Blame Contribute Delete
20.5 kB
import { useEffect, useRef, useState } from "react";
import type { ServiceAccountsSnapshot } from "@xtap-pool/shared";
import type { PoolSnapshot } from "../lib/api.js";
import {
addPoolAdmin,
addPoolMember,
addPoolMemberOrg,
fetchAdminPool,
fetchAdminEnrichment,
fetchAdminFreeLabels,
fetchAdminServiceAccounts,
issueServiceAccount,
removePoolAdmin,
removePoolMember,
removePoolMemberOrg,
repairPoolConfig,
repairServiceAccounts,
revokeServiceAccount,
revokeServiceAccountKey,
rotateServiceAccount,
} from "../lib/api.js";
import type { MemberOrgGrant } from "../lib/api.js";
type AdminState =
| { status: "loading" }
| { status: "ready"; pool: PoolSnapshot; busy?: string; error?: string }
| { status: "error"; error: string };
function sortUsers(users: readonly string[]): string[] {
return [...users].sort((a, b) => a.localeCompare(b));
}
function sortMemberOrgs(orgs: readonly MemberOrgGrant[]): MemberOrgGrant[] {
return [...orgs].sort((a, b) => a.name.localeCompare(b.name));
}
// eslint-disable-next-line complexity -- This stateful admin surface coordinates several independent controls.
export function AdminPanel(): React.JSX.Element {
const [state, setState] = useState<AdminState>({ status: "loading" });
const [memberInput, setMemberInput] = useState("");
const [adminInput, setAdminInput] = useState("");
const [orgInput, setOrgInput] = useState("");
const [enrichment, setEnrichment] = useState<Awaited<ReturnType<typeof fetchAdminEnrichment>>>();
const [freeLabels, setFreeLabels] = useState<Awaited<ReturnType<typeof fetchAdminFreeLabels>>>();
useEffect(() => {
void fetchAdminPool().then(
({ pool }) => {
setState({ status: "ready", pool });
},
(error: unknown) => {
setState({ status: "error", error: message(error) });
},
);
}, []);
useEffect(() => {
void fetchAdminEnrichment().then(setEnrichment, () => undefined);
void fetchAdminFreeLabels().then(setFreeLabels, () => undefined);
}, []);
async function mutate(label: string, action: () => Promise<PoolSnapshot>): Promise<void> {
if (state.status !== "ready") return;
setState({ status: "ready", pool: state.pool, busy: label });
try {
const pool = await action();
setState({ status: "ready", pool });
} catch (error) {
setState({ status: "ready", pool: state.pool, error: message(error) });
}
}
if (state.status === "loading") {
return <p className="p-4 text-sm text-(--x-muted)">Loading…</p>;
}
if (state.status === "error") {
return <p className="p-4 text-sm text-red-500">{state.error}</p>;
}
const { pool, busy, error } = state;
const admins = new Set(pool.admins);
const bootstrapAdmins = new Set(pool.bootstrap_admins);
return (
<div className="flex flex-col gap-6 p-4">
<header className="border-b border-(--x-border) pb-4">
<h2 className="text-lg font-bold">Pool Admin</h2>
<p className="text-sm text-(--x-muted)">
{pool.members.length.toLocaleString()} members · {pool.admins.length.toLocaleString()}{" "}
admins · {pool.member_orgs.length.toLocaleString()} org grant
</p>
</header>
{pool.config_error === undefined ? null : (
<div className="rounded-md border border-red-400 px-3 py-2 text-sm text-red-500">
<p>{pool.config_error}</p>
<button
type="button"
className="mt-2 rounded-md border border-red-400 px-2 py-1 font-semibold"
disabled={busy !== undefined}
onClick={() => {
void mutate("repair-config", repairPoolConfig);
}}
>
Replace with bootstrap membership
</button>
</div>
)}
{error === undefined ? null : <p className="text-sm text-red-500">{error}</p>}
<section>
<h3 className="mb-2 font-bold">Enrichment</h3>
{enrichment === undefined ? (
<p className="text-sm text-(--x-muted)">Loading worker status…</p>
) : (
<p className="text-sm text-(--x-muted)">
{enrichment.worker_recently_completed ? "Worker recently completed" : "No recent run"} ·
pending {enrichment.totals.pending} · retrying {enrichment.totals.retrying} · blocked{" "}
{enrichment.totals.blocked} · complete {enrichment.totals.completed}
</p>
)}
{freeLabels === undefined ? null : (
<div className="mt-1 text-sm text-(--x-muted)">
<p>
Free labels:{" "}
{freeLabels.labels.filter((label) => label.status === "candidate").length} candidate ·{" "}
{freeLabels.labels.filter((label) => label.status === "approved").length} approved ·{" "}
{freeLabels.labels.filter((label) => label.status === "rejected").length} rejected
</p>
{freeLabels.candidates.length === 0 ? null : (
<ul className="mt-2 divide-y divide-(--x-border) border-y border-(--x-border)">
{freeLabels.candidates.map((label) => (
<li key={label.name} className="py-2">
<p className="font-medium text-(--x-text)">
{label.name} · {label.unit_count} units · {label.distinct_authors} authors ·{" "}
{label.distinct_days} days
</p>
{label.representative_quotes.slice(0, 2).map((evidence) => (
<p key={`${evidence.unit_id}:${evidence.tweet_id}:${evidence.quote}`}>
{evidence.tweet_id}: {evidence.quote}
</p>
))}
</li>
))}
</ul>
)}
</div>
)}
</section>
<form
className="flex flex-wrap gap-2"
onSubmit={(event) => {
event.preventDefault();
const username = memberInput.trim();
if (username === "") return;
setMemberInput("");
void mutate(`member:${username}`, () => addPoolMember(username));
}}
>
<input
aria-label="Member username"
className="min-w-0 flex-1 rounded-md border border-(--x-border) bg-(--x-soft) px-3 py-2 text-sm outline-none focus:border-(--x-accent)"
placeholder="HF username"
value={memberInput}
onChange={(event) => {
setMemberInput(event.target.value);
}}
/>
<button
type="submit"
className="rounded-md bg-(--x-accent) px-3 py-2 text-sm font-semibold text-white"
disabled={busy !== undefined}
>
Add member
</button>
</form>
<section>
<h3 className="mb-2 font-bold">Members</h3>
<ul className="divide-y divide-(--x-border) border-y border-(--x-border)">
{sortUsers(pool.members).map((member) => (
<li key={member} className="flex items-center justify-between gap-3 py-2">
<span>@{member}</span>
<div className="flex gap-2">
{!admins.has(member) ? (
<button
type="button"
className="rounded-md border border-(--x-border) px-2 py-1 text-sm"
disabled={busy !== undefined}
onClick={() => {
void mutate(`admin:${member}`, () => addPoolAdmin(member));
}}
>
Promote
</button>
) : null}
{!admins.has(member) ? (
<button
type="button"
className="rounded-md border border-(--x-border) px-2 py-1 text-sm"
disabled={busy !== undefined}
onClick={() => {
void mutate(`member:${member}`, () => removePoolMember(member));
}}
>
Remove
</button>
) : null}
</div>
</li>
))}
</ul>
</section>
<form
className="flex flex-wrap gap-2"
onSubmit={(event) => {
event.preventDefault();
const orgName = orgInput.trim();
if (orgName === "") return;
setOrgInput("");
void mutate(`member-org:${orgName}`, () => addPoolMemberOrg(orgName));
}}
>
<input
aria-label="Member organization"
className="min-w-0 flex-1 rounded-md border border-(--x-border) bg-(--x-soft) px-3 py-2 text-sm outline-none focus:border-(--x-accent)"
placeholder="HF organization"
value={orgInput}
onChange={(event) => {
setOrgInput(event.target.value);
}}
/>
<button
type="submit"
className="rounded-md bg-(--x-accent) px-3 py-2 text-sm font-semibold text-white"
disabled={busy !== undefined}
>
Set org
</button>
</form>
<section>
<h3 className="mb-2 font-bold">Member Organization</h3>
<ul className="divide-y divide-(--x-border) border-y border-(--x-border)">
{sortMemberOrgs(pool.member_orgs).map((org) => (
<li key={org.sub} className="flex items-center justify-between gap-3 py-2">
<span>
@{org.name}
{org.display_name === undefined ? null : (
<span className="ml-2 text-sm text-(--x-muted)">{org.display_name}</span>
)}
</span>
<button
type="button"
className="rounded-md border border-(--x-border) px-2 py-1 text-sm"
disabled={busy !== undefined}
onClick={() => {
void mutate(`member-org:${org.name}`, () => removePoolMemberOrg(org.name));
}}
>
Remove
</button>
</li>
))}
</ul>
</section>
<form
className="flex flex-wrap gap-2"
onSubmit={(event) => {
event.preventDefault();
const username = adminInput.trim();
if (username === "") return;
setAdminInput("");
void mutate(`admin:${username}`, () => addPoolAdmin(username));
}}
>
<input
aria-label="Admin username"
className="min-w-0 flex-1 rounded-md border border-(--x-border) bg-(--x-soft) px-3 py-2 text-sm outline-none focus:border-(--x-accent)"
placeholder="HF username"
value={adminInput}
onChange={(event) => {
setAdminInput(event.target.value);
}}
/>
<button
type="submit"
className="rounded-md bg-(--x-accent) px-3 py-2 text-sm font-semibold text-white"
disabled={busy !== undefined}
>
Add admin
</button>
</form>
<section>
<h3 className="mb-2 font-bold">Admins</h3>
<ul className="divide-y divide-(--x-border) border-y border-(--x-border)">
{sortUsers(pool.admins).map((admin) => (
<li key={admin} className="flex items-center justify-between gap-3 py-2">
<span>@{admin}</span>
{bootstrapAdmins.has(admin) ? (
<span className="text-sm text-(--x-muted)">Bootstrap</span>
) : (
<button
type="button"
className="rounded-md border border-(--x-border) px-2 py-1 text-sm"
disabled={busy !== undefined}
onClick={() => {
void mutate(`admin:${admin}`, () => removePoolAdmin(admin));
}}
>
Demote
</button>
)}
</li>
))}
</ul>
</section>
<ServiceAccountsPanel />
</div>
);
}
type ServiceState =
| { status: "loading" }
| {
status: "ready";
snapshot: ServiceAccountsSnapshot;
busy?: string;
error?: string;
issued?: { name: string; token: string };
}
| { status: "error"; error: string };
function optionalIssued(issued: { name: string; token: string } | undefined): {
issued?: { name: string; token: string };
} {
return issued === undefined ? {} : { issued };
}
function ServiceAccountsPanel(): React.JSX.Element {
const [state, setState] = useState<ServiceState>({ status: "loading" });
const [name, setName] = useState("");
const issuedRef = useRef<{ name: string; token: string } | undefined>(undefined);
useEffect(() => {
void fetchAdminServiceAccounts().then(
(snapshot) => {
setState({ status: "ready", snapshot });
},
(error: unknown) => {
setState({ status: "error", error: message(error) });
},
);
}, []);
async function mutateServices(
label: string,
operation: () => Promise<{ name?: string; token?: string } | undefined>,
): Promise<void> {
if (state.status !== "ready") return;
setState({
status: "ready",
snapshot: state.snapshot,
busy: label,
...optionalIssued(issuedRef.current),
});
try {
const result = await operation();
if (result?.token !== undefined && result.name !== undefined) {
issuedRef.current = { name: result.name, token: result.token };
setState({
status: "ready",
snapshot: state.snapshot,
busy: label,
issued: issuedRef.current,
});
}
const snapshot = await fetchAdminServiceAccounts();
setState({
status: "ready",
snapshot,
...optionalIssued(issuedRef.current),
});
} catch (error) {
setState({
status: "ready",
snapshot: state.snapshot,
error: message(error),
...optionalIssued(issuedRef.current),
});
}
}
if (state.status === "loading") return <p className="text-sm text-(--x-muted)">Loading…</p>;
if (state.status === "error") return <p className="text-sm text-red-500">{state.error}</p>;
return (
<section className="grid gap-3 border-t border-(--x-border) pt-5">
<div>
<h3 className="font-bold">Service Accounts</h3>
<p className="text-sm text-(--x-muted)">
Read-only machine access. Credentials appear once and cannot ingest or administer.
</p>
</div>
{state.snapshot.config_error === undefined ? null : (
<div className="rounded-md border border-red-400 px-3 py-2 text-sm text-red-500">
<p>{state.snapshot.config_error}</p>
<button
type="button"
className="mt-2 rounded-md border border-red-400 px-2 py-1 font-semibold"
disabled={state.busy !== undefined}
onClick={() => {
void mutateServices("repair", async () => {
await repairServiceAccounts();
return undefined;
});
}}
>
Replace with empty registry
</button>
</div>
)}
{state.issued === undefined ? null : (
<div className="rounded-md border border-amber-400 px-3 py-2 text-sm">
<p className="font-semibold">Copy the {state.issued.name} credential now.</p>
<p className="text-(--x-muted)">It will not be shown again.</p>
<textarea
aria-label="Issued service credential"
className="mt-2 h-20 w-full rounded-md border border-(--x-border) bg-(--x-soft) p-2 font-mono text-xs"
readOnly
value={state.issued.token}
/>
<button
type="button"
className="rounded-md border border-(--x-border) px-2 py-1 text-sm"
onClick={() => {
issuedRef.current = undefined;
setState({ status: "ready", snapshot: state.snapshot });
}}
>
Dismiss
</button>
</div>
)}
{state.error === undefined ? null : <p className="text-sm text-red-500">{state.error}</p>}
<form
className="flex flex-wrap gap-2"
onSubmit={(event) => {
event.preventDefault();
const accountName = name.trim();
if (accountName === "") return;
setName("");
void mutateServices(`issue:${accountName}`, async () => {
const credential = await issueServiceAccount(accountName, [
"units:read",
"taxonomy:read",
]);
return { name: credential.account.name, token: credential.token };
});
}}
>
<input
aria-label="Service account name"
className="min-w-0 flex-1 rounded-md border border-(--x-border) bg-(--x-soft) px-3 py-2 text-sm"
placeholder="local-frontier"
value={name}
onChange={(event) => {
setName(event.target.value);
}}
/>
<button
type="submit"
className="rounded-md bg-(--x-accent) px-3 py-2 text-sm font-semibold text-white"
disabled={state.busy !== undefined || state.snapshot.config_error !== undefined}
>
Issue reader
</button>
</form>
<ul className="divide-y divide-(--x-border) border-y border-(--x-border)">
{state.snapshot.accounts.map((account) => (
<li key={account.id} className="grid gap-2 py-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<span>
<strong>{account.name}</strong>{" "}
<span className="text-sm text-(--x-muted)">
{account.status} · {account.scopes.join(", ")}
</span>
</span>
{account.status === "active" ? (
<div className="flex gap-2">
<button
type="button"
className="rounded-md border border-(--x-border) px-2 py-1 text-sm"
disabled={state.busy !== undefined}
onClick={() => {
void mutateServices(`rotate:${account.id}`, async () => {
const credential = await rotateServiceAccount(account.id);
return { name: credential.account.name, token: credential.token };
});
}}
>
Rotate
</button>
<button
type="button"
className="rounded-md border border-red-400 px-2 py-1 text-sm text-red-500"
disabled={state.busy !== undefined}
onClick={() => {
void mutateServices(`revoke:${account.id}`, async () => {
await revokeServiceAccount(account.id);
return undefined;
});
}}
>
Revoke
</button>
</div>
) : null}
</div>
{account.keys.length === 0 ? null : (
<ul className="grid gap-1 pl-3 text-xs text-(--x-muted)">
{account.keys.map((key) => (
<li key={key.id} className="flex items-center justify-between gap-2">
<span>
key {key.id} · issued {key.created_at.slice(0, 10)}
</span>
<button
type="button"
className="rounded-md border border-(--x-border) px-2 py-1"
disabled={state.busy !== undefined}
onClick={() => {
void mutateServices(`revoke-key:${key.id}`, async () => {
await revokeServiceAccountKey(account.id, key.id);
return undefined;
});
}}
>
Revoke key
</button>
</li>
))}
</ul>
)}
</li>
))}
</ul>
</section>
);
}
function message(error: unknown): string {
return error instanceof Error ? error.message : "request failed";
}