"use client"; import { useEffect, useMemo, useState } from "react"; import { AlertCircle, CheckCircle2, Loader2, Save } from "lucide-react"; import { fetchAdminResources, fetchUserGrant, saveUserGrant } from "../api"; import type { GrantPayload, McpToolOption, MultiUserResources } from "../types"; type SaveState = "idle" | "saving" | "saved" | "error"; function emptyGrant(userId: string): GrantPayload { return { version: 2, user_id: userId, models: { llm: [] }, knowledge_bases: [], skills: [], partners: [], enabled_tools: null, mcp_tools: null, exec_enabled: null, }; } function hasModel(grant: GrantPayload, profileId: string, modelId?: string) { return grant.models.llm.some((item) => { if (item.profile_id !== profileId) return false; if (!modelId) return true; return Array.isArray(item.model_ids) && item.model_ids.includes(modelId); }); } function grantFingerprint(grant: GrantPayload): string { return JSON.stringify(grant); } function SectionTitle({ children }: { children: React.ReactNode }) { return (

{children}

); } function CheckRow({ label, description, checked, disabled, onToggle, }: { label: string; description?: string; checked: boolean; disabled: boolean; onToggle: () => void; }) { return ( ); } /** * Default-vs-custom switch for a whitelist field. ``null`` selects the * "default" mode; what that resolves to server-side depends on the field — * built-in tools default to *all*, MCP tools default to *none* (deny until * explicitly granted) — so the default-mode label is caller-supplied. */ function ModeSwitch({ isCustom, disabled, onDefault, onCustom, defaultLabel = "Default · all", }: { isCustom: boolean; disabled: boolean; onDefault: () => void; onCustom: () => void; defaultLabel?: string; }) { const base = "rounded-md px-2 py-0.5 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-45"; return (
); } export function GrantEditor({ userId }: { userId: string }) { const [resources, setResources] = useState(null); const [grant, setGrant] = useState(() => emptyGrant(userId)); const [loading, setLoading] = useState(true); const [savedFingerprint, setSavedFingerprint] = useState(""); const [saveState, setSaveState] = useState("idle"); const [message, setMessage] = useState(""); useEffect(() => { let cancelled = false; Promise.all([fetchAdminResources(), fetchUserGrant(userId)]) .then(([nextResources, nextGrant]) => { if (cancelled) return; setResources(nextResources); setGrant(nextGrant); setSavedFingerprint(grantFingerprint(nextGrant)); }) .catch((error) => { setSaveState("error"); setMessage( error instanceof Error ? error.message : "Failed to load grants", ); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [userId]); const currentFingerprint = useMemo(() => grantFingerprint(grant), [grant]); const dirty = Boolean(savedFingerprint) && currentFingerprint !== savedFingerprint; const kbIds = useMemo( () => new Set( grant.knowledge_bases.map((item) => String(item.resource_id || item.id || ""), ), ), [grant.knowledge_bases], ); const skillIds = useMemo( () => new Set( grant.skills.map((item) => String(item.skill_id || item.id || "")), ), [grant.skills], ); const partnerIds = useMemo( () => new Set( grant.partners.map((item) => String(item.partner_id || item.id || "")), ), [grant.partners], ); const selectedModelCount = useMemo( () => grant.models.llm.reduce((total, item) => { if (Array.isArray(item.model_ids)) return total + item.model_ids.length; return total + 1; }, 0), [grant.models.llm], ); const saving = saveState === "saving"; const controlsDisabled = loading || saving; function toggleModel(profileId: string, modelId: string) { setGrant((current) => { const next = structuredClone(current) as GrantPayload; const items = next.models.llm; const existing = items.find((item) => item.profile_id === profileId); if (!existing) { items.push({ profile_id: profileId, model_ids: [modelId], source: "admin", }); return next; } const modelIds = new Set( Array.isArray(existing.model_ids) ? existing.model_ids : [], ); if (modelIds.has(modelId)) modelIds.delete(modelId); else modelIds.add(modelId); existing.model_ids = Array.from(modelIds); next.models.llm = items.filter((item) => Array.isArray(item.model_ids) ? item.model_ids.length > 0 : true, ); return next; }); } function toggleKb(resourceId: string, name: string) { setGrant((current) => { const next = structuredClone(current) as GrantPayload; const exists = kbIds.has(resourceId); next.knowledge_bases = exists ? next.knowledge_bases.filter( (item) => String(item.resource_id || item.id || "") !== resourceId, ) : [ ...next.knowledge_bases, { resource_id: resourceId, name, access: "read", source: "admin" }, ]; return next; }); } function toggleSkill(name: string) { setGrant((current) => { const next = structuredClone(current) as GrantPayload; const exists = skillIds.has(name); next.skills = exists ? next.skills.filter( (item) => String(item.skill_id || item.id || "") !== name, ) : [...next.skills, { skill_id: name, access: "use", source: "admin" }]; return next; }); } function togglePartner(partnerId: string, name: string) { setGrant((current) => { const next = structuredClone(current) as GrantPayload; const exists = partnerIds.has(partnerId); next.partners = exists ? next.partners.filter( (item) => String(item.partner_id || item.id || "") !== partnerId, ) : [...next.partners, { partner_id: partnerId, name, source: "admin" }]; return next; }); } function setToolList( key: "enabled_tools" | "mcp_tools", value: string[] | null, ) { setGrant((current) => ({ ...current, [key]: value })); } function toggleToolName(key: "enabled_tools" | "mcp_tools", name: string) { setGrant((current) => { const list = current[key]; if (list === null) return current; const next = list.includes(name) ? list.filter((item) => item !== name) : [...list, name]; return { ...current, [key]: next }; }); } async function save() { setSaveState("saving"); setMessage(""); try { const saved = await saveUserGrant(userId, grant); setGrant(saved); setSavedFingerprint(grantFingerprint(saved)); setSaveState("saved"); setMessage("Saved just now"); } catch (error) { setSaveState("error"); setMessage(error instanceof Error ? error.message : "Failed to save"); } } const status = loading ? "Loading assignments..." : saveState === "saving" ? "Saving changes..." : saveState === "error" ? message || "Failed to save" : saveState === "saved" && !dirty ? message || "Saved just now" : dirty ? "Unsaved changes" : "Ready"; const statusTone = saveState === "error" ? "text-red-600 dark:text-red-400" : saveState === "saved" && !dirty ? "text-emerald-700 dark:text-emerald-300" : "text-[var(--muted-foreground)]"; const toolsSummary = grant.enabled_tools === null ? "all tools" : `${grant.enabled_tools.length} tools`; // MCP tools deny-by-default for non-admin users: ``null`` grants none until // the admin switches to Custom and picks specific tool names. const mcpSummary = grant.mcp_tools === null ? "no MCP" : `${grant.mcp_tools.length} MCP`; const mcpByServer = useMemo(() => { const groups = new Map(); for (const tool of resources?.mcp_tools || []) { const key = tool.server || "other"; groups.set(key, [...(groups.get(key) ?? []), tool]); } return groups; }, [resources?.mcp_tools]); if (loading && !resources) { return (
Loading assignments...
); } return (

Assign access

Admin resources stay linked server-side; users only receive allowed access.

{selectedModelCount} models {kbIds.size} KBs {skillIds.size} skills {partnerIds.size} partners {toolsSummary} {mcpSummary}
Models
{(resources?.models.llm || []).map((profile) => (
{profile.name}
{(profile.models || []).map((model) => ( ))}
))}
Knowledge
{(resources?.knowledge_bases || []).map((kb) => ( toggleKb(kb.resource_id, kb.name)} /> ))}
Skills
{(resources?.skills || []).map((skill) => ( toggleSkill(skill.name)} /> ))}
Partners
{(resources?.partners || []).length === 0 ? (

No partners yet. Create one under Partners to assign it.

) : ( (resources?.partners || []).map((partner) => ( togglePartner( partner.partner_id, partner.name || partner.partner_id, ) } /> )) )}
System tools setToolList("enabled_tools", null)} onCustom={() => setToolList( "enabled_tools", (resources?.tools || []).map((tool) => tool.name), ) } /> {grant.enabled_tools !== null && (
{(resources?.tools || []).map((tool) => ( toggleToolName("enabled_tools", tool.name) } /> ))}
)}
MCP tools setToolList("mcp_tools", null)} onCustom={() => setToolList( "mcp_tools", (resources?.mcp_tools || []).map((tool) => tool.name), ) } /> {grant.mcp_tools === null ? (

MCP tools proxy host-side capabilities, so they stay denied by default. Switch to Custom to grant specific tools.

) : null} {grant.mcp_tools !== null && (resources?.mcp_tools?.length ? (
{[...mcpByServer.entries()].map(([server, tools]) => (

{server}

{tools.map((tool) => ( toggleToolName("mcp_tools", tool.name) } /> ))}
))}
) : (

No MCP servers configured.

))}
Code execution
setGrant((current) => ({ ...current, exec_enabled: current.exec_enabled === false ? null : false, })) } />
{saveState === "error" ? ( ) : saveState === "saved" && !dirty ? ( ) : null} {status}
); }