import { useState, useMemo } from 'react'; import { Avatar, Badge, Button, Input, MonoText, SectionHeader, Tag, ConfirmDialog, SplitPane, EntityList, Spinner, useToast, } from '@cameleer/design-system'; import { useRoles, useRole, useCreateRole, useDeleteRole, } from '../../api/queries/admin/rbac'; import type { RoleDetail } from '../../api/queries/admin/rbac'; import styles from './UserManagement.module.css'; export default function RolesTab() { const { toast } = useToast(); const { data: roles, isLoading } = useRoles(); const [search, setSearch] = useState(''); const [selectedId, setSelectedId] = useState(null); const [creating, setCreating] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); // Create form state const [newName, setNewName] = useState(''); const [newDesc, setNewDesc] = useState(''); // Detail query const { data: detail, isLoading: detailLoading } = useRole(selectedId); // Mutations const createRole = useCreateRole(); const deleteRole = useDeleteRole(); const filtered = useMemo(() => { const list = roles ?? []; if (!search) return list; const q = search.toLowerCase(); return list.filter( (r) => r.name.toLowerCase().includes(q) || r.description.toLowerCase().includes(q), ); }, [roles, search]); const duplicateRoleName = newName.trim() !== '' && (roles ?? []).some((r) => r.name === newName.trim().toUpperCase()); function handleCreate() { if (!newName.trim()) return; createRole.mutate( { name: newName.trim().toUpperCase(), description: newDesc.trim() || undefined }, { onSuccess: () => { toast({ title: 'Role created', description: newName.trim().toUpperCase(), variant: 'success', }); setCreating(false); setNewName(''); setNewDesc(''); }, onError: () => { toast({ title: 'Failed to create role', variant: 'error' }); }, }, ); } function handleDelete() { if (!deleteTarget) return; deleteRole.mutate(deleteTarget.id, { onSuccess: () => { toast({ title: 'Role deleted', description: deleteTarget.name, variant: 'warning', }); if (selectedId === deleteTarget.id) setSelectedId(null); setDeleteTarget(null); }, onError: () => { toast({ title: 'Failed to delete role', variant: 'error' }); setDeleteTarget(null); }, }); } function getAssignmentCount(role: RoleDetail): number { return ( (role.assignedGroups?.length ?? 0) + (role.directUsers?.length ?? 0) ); } if (isLoading) return ; return ( <> {creating && (
setNewName(e.target.value)} /> {duplicateRoleName && ( Role name already exists )} setNewDesc(e.target.value)} />
)} ( <>
{role.name} {role.system && ( )}
{role.description || '\u2014'} \u00b7{' '} {getAssignmentCount(role)} assignments
{(role.assignedGroups ?? []).map((g) => ( ))} {(role.directUsers ?? []).map((u) => ( ))}
)} getItemId={(role) => role.id} selectedId={selectedId ?? undefined} onSelect={setSelectedId} searchPlaceholder="Search roles..." onSearch={setSearch} addLabel="+ Add role" onAdd={() => setCreating(true)} emptyMessage="No roles match your search" /> } detail={ selectedId && (detailLoading || !detail) ? ( ) : detail ? ( setDeleteTarget(detail)} /> ) : null } emptyMessage="Select a role to view details" /> setDeleteTarget(null)} onConfirm={handleDelete} message={`Delete role "${deleteTarget?.name}"? This cannot be undone.`} confirmText={deleteTarget?.name ?? ''} loading={deleteRole.isPending} /> ); } // ── Detail panel ────────────────────────────────────────────────────────────── interface RoleDetailPanelProps { role: RoleDetail; onDeleteRequest: () => void; } function RoleDetailPanel({ role, onDeleteRequest }: RoleDetailPanelProps) { const directUserIds = new Set( (role.directUsers ?? []).map((u) => u.userId), ); const assignedGroups = role.assignedGroups ?? []; const directUsers = role.directUsers ?? []; const effectivePrincipals = role.effectivePrincipals ?? []; return ( <>
{role.name}
{role.description && (
{role.description}
)}
{!role.system && ( )}
ID {role.id} Scope {role.scope || '\u2014'} {role.system && ( <> Type System role (read-only) )}
Assigned to groups
{assignedGroups.map((g) => ( ))} {assignedGroups.length === 0 && ( (none) )}
Assigned to users (direct)
{directUsers.map((u) => ( ))} {directUsers.length === 0 && ( (none) )}
Effective principals
{effectivePrincipals.map((u) => { const isDirect = directUserIds.has(u.userId); return isDirect ? ( ) : ( ); })} {effectivePrincipals.length === 0 && ( (none) )}
{effectivePrincipals.some((u) => !directUserIds.has(u.userId)) && ( Dashed entries inherit this role through group membership )} ); }