import { useState, useMemo } from 'react'; import { useUsers } from '../../../api/queries/admin/rbac'; import type { UserDetail, GroupDetail } from '../../../api/queries/admin/rbac'; import { useGroups } from '../../../api/queries/admin/rbac'; import styles from './RbacPage.module.css'; function getInitials(name: string): string { const parts = name.trim().split(/\s+/); if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); return name.slice(0, 2).toUpperCase(); } function formatDate(iso: string): string { try { return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: '2-digit', day: '2-digit', }); } catch { return iso; } } function buildGroupPath(user: UserDetail, groupMap: Map): string { if (user.directGroups.length === 0) return '(no groups)'; const names = user.directGroups.map((g) => g.name); // Try to find a parent -> child path for (const g of user.directGroups) { const detail = groupMap.get(g.id); if (detail?.parentGroupId) { const parent = groupMap.get(detail.parentGroupId); if (parent) return `${parent.name} > ${g.name}`; } } return names.join(', '); } export function UsersTab() { const users = useUsers(); const groups = useGroups(); const [selected, setSelected] = useState(null); const [filter, setFilter] = useState(''); const groupMap = useMemo(() => { const map = new Map(); for (const g of groups.data ?? []) { map.set(g.id, g); } return map; }, [groups.data]); const filtered = useMemo(() => { const list = users.data ?? []; if (!filter) return list; const lower = filter.toLowerCase(); return list.filter( (u) => u.displayName.toLowerCase().includes(lower) || u.email.toLowerCase().includes(lower) || u.userId.toLowerCase().includes(lower) ); }, [users.data, filter]); const selectedUser = useMemo( () => (users.data ?? []).find((u) => u.userId === selected) ?? null, [users.data, selected] ); if (users.isLoading) { return
Loading...
; } return ( <>
Users
Manage identities, group membership and direct roles
setFilter(e.target.value)} />
{filtered.map((user) => { const isSelected = user.userId === selected; return (
setSelected(user.userId)} >
{getInitials(user.displayName)}
{user.displayName} {user.provider !== 'local' && ( {user.provider} )}
{user.email} ยท {buildGroupPath(user, groupMap)}
{user.directRoles.map((r) => ( {r.name} ))} {user.effectiveRoles .filter((er) => !user.directRoles.some((dr) => dr.id === er.id)) .map((r) => ( {r.name} ))} {user.directGroups.map((g) => ( {g.name} ))}
); })}
{!selectedUser ? (
Select a user to view details
) : ( )}
); } function UserDetail({ user, groupMap, }: { user: UserDetail; groupMap: Map; }) { // Build group tree for this user const groupTree = useMemo(() => { const tree: { name: string; depth: number; annotation: string }[] = []; for (const g of user.directGroups) { const detail = groupMap.get(g.id); if (detail?.parentGroupId) { const parent = groupMap.get(detail.parentGroupId); if (parent && !tree.some((t) => t.name === parent.name)) { tree.push({ name: parent.name, depth: 0, annotation: '' }); } tree.push({ name: g.name, depth: 1, annotation: 'child group' }); } else { tree.push({ name: g.name, depth: 0, annotation: '' }); } } return tree; }, [user, groupMap]); const inheritedRoles = user.effectiveRoles.filter( (er) => !user.directRoles.some((dr) => dr.id === er.id) ); return ( <>
{getInitials(user.displayName)}
{user.displayName} {user.provider !== 'local' && ( {user.provider} )}
{user.email}
Status Active
ID {user.userId}
Created {formatDate(user.createdAt)}

Group membership direct only
{user.directGroups.length === 0 ? ( No group membership ) : ( user.directGroups.map((g) => ( {g.name} )) )}
Effective roles direct + inherited
{user.directRoles.map((r) => ( {r.name} ))} {inheritedRoles.map((r) => ( {r.name} {r.source ? `\u2191 ${r.source}` : ''} ))} {inheritedRoles.length > 0 && (
Dashed roles are inherited transitively through group membership.
)}
{groupTree.length > 0 && (
Group tree
{groupTree.map((node, i) => (
{node.depth > 0 && (
)} {node.name} {node.annotation && ( {node.annotation} )}
))}
)} ); }