"use client"; import * as React from "react"; import { AnimatePresence, motion } from "framer-motion"; import { Ban, Check, Plus, ShieldCheck, Trash2, Undo2, X } from "lucide-react"; import { api, bytes, type AdminUser } from "@/lib/api"; import { useToast } from "@/components/toast"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export function UsersTable({ users, roles, me, refresh, }: { users: AdminUser[]; roles: string[]; me: string | null; refresh: () => void; }) { const [adding, setAdding] = React.useState(false); return (

Users

Adding someone here is the invitation — no email, no token. They sign in with that provider and they’re in.

{adding && ( { setAdding(false); refresh(); }} /> )}
{users.map((u) => ( ))}
); } function UserRow({ u, roles, isMe, refresh, }: { u: AdminUser; roles: string[]; isMe: boolean; refresh: () => void; }) { const toast = useToast(); const [busy, setBusy] = React.useState(false); const [confirmDel, setConfirmDel] = React.useState(false); const run = async (fn: () => Promise, ok: string) => { setBusy(true); try { await fn(); toast(ok); refresh(); } catch (e) { toast((e as Error).message, true); } setBusy(false); }; const quotaPct = u.quota ? (u.usage.requests / u.quota) * 100 : null; return (
{u.avatar && ( /* eslint-disable-next-line @next/next/no-img-element */ (e.currentTarget.style.visibility = "hidden")} /> )}
{u.display || u.username} {u.role === "admin" && } {isMe && (you)}
{u.username} · {u.provider} {u.last_seen && ` · seen ${u.last_seen.slice(0, 10)}`} {u.disabled && " · disabled"}
{confirmDel ? (
) : ( )}
run(() => api.admin.patchUser(u.username, { quota: v }), "Quota updated")} /> run(() => api.admin.patchUser(u.username, { storage_quota_gb: v }), "Quota updated") } /> {u.bootstrap && ( bootstrap account — disabling won’t lock them out )}
); } /** Number input that only commits on blur or Enter, so a quota isn't PATCHed * once per keystroke while you type "120". */ function QuotaField({ label, value, suffix, pct, onSave, }: { label: string; value: number; suffix: string; pct: number | null; onSave: (v: number) => void; }) { const [v, setV] = React.useState(String(value)); React.useEffect(() => setV(String(value)), [value]); const commit = () => { const n = Number(v); if (!Number.isFinite(n) || n < 0) { setV(String(value)); return; } if (n !== value) onSave(n); }; return ( ); } function InviteForm({ roles, onDone }: { roles: string[]; onDone: () => void }) { const toast = useToast(); const [username, setUsername] = React.useState(""); const [provider, setProvider] = React.useState("github"); const [role, setRole] = React.useState("user"); const [quota, setQuota] = React.useState("0"); const [storage, setStorage] = React.useState("0"); const [busy, setBusy] = React.useState(false); const submit = async () => { if (!username.trim()) { toast("Username required", true); return; } setBusy(true); try { await api.admin.addUser({ username: username.trim(), provider, role, quota: Number(quota) || 0, storage_quota_gb: Number(storage) || 0, }); toast(`Invited ${username.trim()}`); onDone(); } catch (e) { toast((e as Error).message, true); } setBusy(false); }; const input = "rounded-glass border border-edge-hi bg-transparent px-3 py-2 text-[13px] " + "text-ink outline-none focus:border-accent-line"; return (
0 means unlimited.
); }