import { useState } from 'react'; import { Avatar, Badge, Button, ConfirmDialog, Input, MonoText, Spinner, Tag, 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 { data: roles, isLoading } = useRoles(); const [selectedId, setSelectedId] = useState(null); const [search, setSearch] = useState(''); const [showCreate, setShowCreate] = useState(false); const [newName, setNewName] = useState(''); const [newDescription, setNewDescription] = useState(''); const [confirmDelete, setConfirmDelete] = useState(false); const { data: detail, isLoading: detailLoading } = useRole(selectedId); const createRole = useCreateRole(); const deleteRole = useDeleteRole(); const { toast } = useToast(); const filtered = (roles ?? []).filter((r) => r.name.toLowerCase().includes(search.toLowerCase()), ); function handleCreate() { if (!newName.trim()) return; createRole.mutate( { name: newName.trim(), description: newDescription.trim() || undefined }, { onSuccess: () => { toast({ title: 'Role created', variant: 'success' }); setShowCreate(false); setNewName(''); setNewDescription(''); }, onError: () => { toast({ title: 'Failed to create role', variant: 'error' }); }, }, ); } function handleDelete() { if (!selectedId) return; deleteRole.mutate(selectedId, { onSuccess: () => { toast({ title: 'Role deleted', variant: 'success' }); setSelectedId(null); setConfirmDelete(false); }, onError: () => { toast({ title: 'Failed to delete role', variant: 'error' }); setConfirmDelete(false); }, }); } return (
{/* Left pane — list */}
setSearch(e.target.value)} />
{showCreate && (
setNewName(e.target.value.toUpperCase())} style={{ marginBottom: 8 }} /> setNewDescription(e.target.value)} />
)} {isLoading ? ( ) : (
{filtered.map((role) => { const assignmentCount = (role.assignedGroups?.length ?? 0) + (role.directUsers?.length ?? 0); return (
setSelectedId(role.id)} >
{role.name} {role.system && }
{role.description || '—'} · {assignmentCount} assignment {assignmentCount !== 1 ? 's' : ''}
{((role.assignedGroups?.length ?? 0) > 0 || (role.directUsers?.length ?? 0) > 0) && (
{(role.assignedGroups ?? []).map((g) => ( ))} {(role.directUsers ?? []).map((u) => ( ))}
)}
); })}
)}
{/* Right pane — detail */}
{!selectedId ? (
Select a role to view details
) : detailLoading || !detail ? ( ) : ( setConfirmDelete(true)} /> )}
{detail && ( setConfirmDelete(false)} onConfirm={handleDelete} title="Delete role" message={`Delete role "${detail.name}"? This cannot be undone.`} confirmText={detail.name} confirmLabel="Delete" variant="danger" loading={deleteRole.isPending} /> )}
); } // ── Detail panel ────────────────────────────────────────────────────────────── interface RoleDetailPanelProps { role: RoleDetail; onDeleteRequest: () => void; } function RoleDetailPanel({ role, onDeleteRequest }: RoleDetailPanelProps) { // Build a set of directly-assigned user IDs for distinguishing inherited principals const directUserIds = new Set((role.directUsers ?? []).map((u) => u.userId)); return (
{/* Header */}
{role.name}
{role.description && (
{role.description}
)}
{/* Metadata */}
ID {role.id} Scope {role.scope || '—'} Type {role.system ? 'System role (read-only)' : 'Custom role'}
{/* Assigned to groups */}
Assigned to groups
{(role.assignedGroups ?? []).length === 0 ? ( None ) : ( (role.assignedGroups ?? []).map((g) => ( )) )}
{/* Assigned to users (direct) */}
Assigned to users (direct)
{(role.directUsers ?? []).length === 0 ? ( None ) : ( (role.directUsers ?? []).map((u) => ( )) )}
{/* Effective principals */}
Effective principals
{(role.effectivePrincipals ?? []).length === 0 ? ( None ) : ( (role.effectivePrincipals ?? []).map((u) => { const isDirect = directUserIds.has(u.userId); return isDirect ? ( ) : ( ); }) )}
{(role.effectivePrincipals ?? []).some((u) => !directUserIds.has(u.userId)) && (
Dashed entries inherit this role through group membership
)}
); }