diff --git a/src/pages/Admin/UserManagement/RolesTab.tsx b/src/pages/Admin/UserManagement/RolesTab.tsx new file mode 100644 index 0000000..f7d10e4 --- /dev/null +++ b/src/pages/Admin/UserManagement/RolesTab.tsx @@ -0,0 +1,213 @@ +import { useState, useMemo } from 'react' +import { Avatar } from '../../../design-system/primitives/Avatar/Avatar' +import { Badge } from '../../../design-system/primitives/Badge/Badge' +import { Button } from '../../../design-system/primitives/Button/Button' +import { Input } from '../../../design-system/primitives/Input/Input' +import { MonoText } from '../../../design-system/primitives/MonoText/MonoText' +import { SectionHeader } from '../../../design-system/primitives/SectionHeader/SectionHeader' +import { Tag } from '../../../design-system/primitives/Tag/Tag' +import { ConfirmDialog } from '../../../design-system/composites/ConfirmDialog/ConfirmDialog' +import { MOCK_ROLES, MOCK_GROUPS, MOCK_USERS, getEffectiveRoles, type MockRole } from './rbacMocks' +import styles from './UserManagement.module.css' + +export function RolesTab() { + const [roles, setRoles] = useState(MOCK_ROLES) + const [search, setSearch] = useState('') + const [selectedId, setSelectedId] = useState(null) + const [creating, setCreating] = useState(false) + const [deleteTarget, setDeleteTarget] = useState(null) + + const [newName, setNewName] = useState('') + const [newDesc, setNewDesc] = useState('') + + const filtered = useMemo(() => { + if (!search) return roles + const q = search.toLowerCase() + return roles.filter((r) => + r.name.toLowerCase().includes(q) || r.description.toLowerCase().includes(q) + ) + }, [roles, search]) + + const selected = roles.find((r) => r.id === selectedId) ?? null + + function handleCreate() { + if (!newName.trim()) return + const newRole: MockRole = { + id: `role-${Date.now()}`, + name: newName.trim().toUpperCase(), + description: newDesc.trim(), + scope: 'custom', + system: false, + } + setRoles((prev) => [...prev, newRole]) + setCreating(false) + setNewName(''); setNewDesc('') + setSelectedId(newRole.id) + } + + function handleDelete() { + if (!deleteTarget) return + setRoles((prev) => prev.filter((r) => r.id !== deleteTarget.id)) + if (selectedId === deleteTarget.id) setSelectedId(null) + setDeleteTarget(null) + } + + // Role assignments + const assignedGroups = selected + ? MOCK_GROUPS.filter((g) => g.directRoles.includes(selected.name)) + : [] + + const directUsers = selected + ? MOCK_USERS.filter((u) => u.directRoles.includes(selected.name)) + : [] + + const effectivePrincipals = selected + ? MOCK_USERS.filter((u) => getEffectiveRoles(u).some((r) => r.role === selected.name)) + : [] + + function getAssignmentCount(role: MockRole): number { + const groups = MOCK_GROUPS.filter((g) => g.directRoles.includes(role.name)).length + const users = MOCK_USERS.filter((u) => u.directRoles.includes(role.name)).length + return groups + users + } + + return ( + <> +
+
+
+ setSearch(e.target.value)} + onClear={() => setSearch('')} + className={styles.listHeaderSearch} + /> + +
+ + {creating && ( +
+ setNewName(e.target.value)} /> + setNewDesc(e.target.value)} /> +
+ + +
+
+ )} + +
+ {filtered.map((role) => ( +
setSelectedId(role.id)} + > + +
+
+ {role.name} + {role.system && ๐Ÿ”’} +
+
+ {role.description} ยท {getAssignmentCount(role)} assignments +
+
+ {MOCK_GROUPS.filter((g) => g.directRoles.includes(role.name)) + .map((g) => )} + {MOCK_USERS.filter((u) => u.directRoles.includes(role.name)) + .map((u) => )} +
+
+
+ ))} +
+
+ +
+ {selected ? ( + <> +
+ +
+
{selected.name}
+ {selected.description && ( +
{selected.description}
+ )} +
+ {!selected.system && ( + + )} +
+ +
+ ID + {selected.id} + Scope + {selected.scope} + {selected.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 = u.directRoles.includes(selected.name) + return ( + + ) + })} + {effectivePrincipals.length === 0 && (none)} +
+ {effectivePrincipals.some((u) => !u.directRoles.includes(selected.name)) && ( + + Dashed entries inherit this role through group membership + + )} + + ) : ( +
Select a role to view details
+ )} +
+
+ + setDeleteTarget(null)} + onConfirm={handleDelete} + message={`Delete role "${deleteTarget?.name}"? This cannot be undone.`} + confirmText={deleteTarget?.name ?? ''} + /> + + ) +}