import { useEffect, useMemo, useState } from "react"; import { Search, Pencil, Plus, Trash2 } from "lucide-react"; import type { ApiSettings } from "../../types"; import Button from "../../components/Button"; import ConfirmDialog from "../../components/ConfirmDialog"; import DataGrid, { type DataGridColumn } from "../../components/table/DataGrid"; import Dialog from "../../components/Dialog"; import FormField from "../../components/FormField"; import StatusBadge from "../../components/StatusBadge"; import { createGovernanceTemplate, deleteGovernanceTemplate, fetchGovernanceTemplates, fetchPermissionCatalog, fetchTenants, updateGovernanceTemplate, type GovernanceAssignment, type GovernanceTemplateItem, type PermissionItem, type TenantAdminItem } from "../../api/admin"; import AdminIconButton from "./components/AdminIconButton"; import AdminPageLayout from "./components/AdminPageLayout"; import AdminSelectionList from "./components/AdminSelectionList"; import { adminErrorMessage, joinLabels } from "./adminUtils"; const emptyDraft = { slug: "", name: "", description: "", isActive: true, permissions: [] as string[], assignments: [] as GovernanceAssignment[] }; export default function GovernanceTemplatesPanel({ settings, kind, canWrite, onAuthRefresh }: { settings: ApiSettings; kind: "group" | "role"; canWrite: boolean; onAuthRefresh: () => Promise; }) { const [items, setItems] = useState([]); const [tenants, setTenants] = useState([]); const [permissions, setPermissions] = useState([]); const [editing, setEditing] = useState(null); const [viewing, setViewing] = useState(null); const [deleting, setDeleting] = useState(null); const [draft, setDraft] = useState(emptyDraft); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); async function load() { setLoading(true); setError(""); try { const [nextItems, nextTenants, nextPermissions] = await Promise.all([ fetchGovernanceTemplates(settings, kind), fetchTenants(settings), kind === "role" ? fetchPermissionCatalog(settings) : Promise.resolve([]) ]); setItems(nextItems); setTenants(nextTenants); setPermissions(nextPermissions.filter((item) => item.level === "tenant")); } catch (err) { setError(adminErrorMessage(err)); } finally { setLoading(false); } } useEffect(() => { void load(); }, [settings.accessToken, settings.apiBaseUrl, kind]); function openCreate() { setDraft(emptyDraft); setEditing("new"); } function openEdit(item: GovernanceTemplateItem) { setDraft({ slug: item.slug, name: item.name, description: item.description || "", isActive: item.is_active, permissions: item.permissions, assignments: item.assignments }); setEditing(item); } function assignmentMode(tenantId: string): "none" | "available" | "required" { return draft.assignments.find((item) => item.tenant_id === tenantId)?.mode ?? "none"; } function setAssignment(tenantId: string, mode: "none" | "available" | "required") { setDraft((current) => ({ ...current, assignments: mode === "none" ? current.assignments.filter((item) => item.tenant_id !== tenantId) : [...current.assignments.filter((item) => item.tenant_id !== tenantId), { tenant_id: tenantId, mode }] })); } async function save() { setBusy(true); setError(""); try { if (editing === "new") { await createGovernanceTemplate(settings, { kind, slug: draft.slug, name: draft.name, description: draft.description || null, permissions: kind === "role" ? draft.permissions : [], is_active: draft.isActive, assignments: draft.assignments }); setSuccess(`${kind === "group" ? "Group" : "Role"} template created.`); } else if (editing) { await updateGovernanceTemplate(settings, editing.id, { name: draft.name, description: draft.description || null, permissions: kind === "role" ? draft.permissions : [], is_active: draft.isActive, assignments: draft.assignments }); setSuccess(`${draft.name} updated and synchronized to assigned tenants.`); } setEditing(null); await load(); await onAuthRefresh(); } catch (err) { setError(adminErrorMessage(err)); } finally { setBusy(false); } } async function remove() { if (!deleting) return; setBusy(true); setError(""); try { await deleteGovernanceTemplate(settings, deleting.id); setSuccess(`${deleting.name} deleted.`); setDeleting(null); await load(); await onAuthRefresh(); } catch (err) { setError(adminErrorMessage(err)); } finally { setBusy(false); } } const columns = useMemo[]>(() => [ { id: "template", header: kind === "group" ? "Group template" : "Tenant role", width: "minmax(240px, 1.2fr)", minWidth: 210, resizable: true, sticky: "start", sortable: true, filterable: true, value: (row) => `${row.name} ${row.slug}`, render: (row) =>
{row.name}
{row.slug}
}, { id: "tenants", header: "Tenant availability", width: 320, minWidth: 210, maxWidth: 640, resizable: true, fill: true, sortable: true, filterable: true, value: (row) => row.assignments.map((assignment) => tenants.find((tenant) => tenant.id === assignment.tenant_id)?.name || assignment.tenant_id).join(", ") || "—", render: (row) => row.assignments.length ? row.assignments.map((assignment) => { const tenant = tenants.find((item) => item.id === assignment.tenant_id); return `${tenant?.name || assignment.tenant_id} (${assignment.mode})`; }).join(", ") : "—" }, ...(kind === "role" ? [{ id: "permissions", header: "Permissions", width: 120, resizable: false, sortable: true, filterable: true, filterType: "integer" as const, value: (row: GovernanceTemplateItem) => row.effective_permission_count }] : []), { id: "status", header: "Status", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.is_active ? "active" : "inactive", render: (row) => }, { id: "actions", header: "Actions", width: 150, sticky: "end", resizable: false, align: "right", render: (row) =>
} onClick={() => setViewing(row)} /> } onClick={() => openEdit(row)} disabled={!canWrite} /> } variant="danger" onClick={() => setDeleting(row)} disabled={!canWrite} />
} ], [canWrite, kind, tenants]); const title = kind === "group" ? "Central groups" : "Tenant roles"; const description = kind === "group" ? "Centrally defined group identities that are provisioned into selected tenants as available or required definitions. Membership remains tenant-local." : "Centrally governed tenant roles that are provisioned into selected tenants as available or required definitions."; return ( <> } variant="primary" onClick={openCreate} disabled={!canWrite} />} >
row.id} emptyText={`No central ${kind} templates found.`} />
!busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<>} >
setDraft({ ...draft, name: event.target.value })} /> setDraft({ ...draft, slug: event.target.value })} />