diff --git a/ui/src/pages/admin/rbac/DashboardTab.tsx b/ui/src/pages/admin/rbac/DashboardTab.tsx index df6985d6..6e5abfb5 100644 --- a/ui/src/pages/admin/rbac/DashboardTab.tsx +++ b/ui/src/pages/admin/rbac/DashboardTab.tsx @@ -1,38 +1,72 @@ -import { useRbacStats, useGroups, useRoles } from '../../../api/queries/admin/rbac'; -import type { GroupDetail, RoleDetail } from '../../../api/queries/admin/rbac'; +import { useMemo } from 'react'; +import { useRbacStats, useGroups } from '../../../api/queries/admin/rbac'; +import type { GroupDetail } from '../../../api/queries/admin/rbac'; import styles from './RbacPage.module.css'; export function DashboardTab() { const stats = useRbacStats(); const groups = useGroups(); - const roles = useRoles(); + + const groupList: GroupDetail[] = groups.data ?? []; + + // Build inheritance diagram data: top-level groups sorted alphabetically, + // children sorted alphabetically and indented below their parent. + const { topLevelGroups, childMap } = useMemo(() => { + const sorted = [...groupList].sort((a, b) => a.name.localeCompare(b.name)); + const top = sorted.filter((g) => !g.parentGroupId); + const cMap = new Map(); + for (const g of sorted) { + if (g.parentGroupId) { + const children = cMap.get(g.parentGroupId) ?? []; + children.push(g); + cMap.set(g.parentGroupId, children); + } + } + return { topLevelGroups: top, childMap: cMap }; + }, [groupList]); + + // Derive roles from groups in tree order (top-level then children), collecting + // each group's directRoles, deduplicating by id and preserving first-seen order. + const roleList = useMemo(() => { + const seen = new Set(); + const result: { id: string; name: string }[] = []; + for (const g of topLevelGroups) { + for (const r of g.directRoles) { + if (!seen.has(r.id)) { + seen.add(r.id); + result.push(r); + } + } + for (const child of childMap.get(g.id) ?? []) { + for (const r of child.directRoles) { + if (!seen.has(r.id)) { + seen.add(r.id); + result.push(r); + } + } + } + } + return result; + }, [topLevelGroups, childMap]); + + // Collect unique users from all groups, sorted alphabetically by displayName. + const allUsers = useMemo(() => { + const userMap = new Map(); + for (const g of groupList) { + for (const m of g.members) { + userMap.set(m.userId, m.displayName); + } + } + return new Map( + [...userMap.entries()].sort((a, b) => a[1].localeCompare(b[1])) + ); + }, [groupList]); if (stats.isLoading) { return
Loading...
; } const s = stats.data; - const groupList: GroupDetail[] = groups.data ?? []; - const roleList: RoleDetail[] = roles.data ?? []; - - // Build inheritance diagram data - const topLevelGroups = groupList.filter((g) => !g.parentGroupId); - const childMap = new Map(); - for (const g of groupList) { - if (g.parentGroupId) { - const children = childMap.get(g.parentGroupId) ?? []; - children.push(g); - childMap.set(g.parentGroupId, children); - } - } - - // Collect unique users from all groups - const allUsers = new Map(); - for (const g of groupList) { - for (const m of g.members) { - allUsers.set(m.userId, m.displayName); - } - } return (