From bd4e22eafb32fb8db61708e1ebe32aa8c8cd4b34 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:22:56 +0100 Subject: [PATCH] refactor: use SplitPane and EntityList in Admin RBAC tabs Co-Authored-By: Claude Opus 4.6 (1M context) --- src/pages/Admin/UserManagement/GroupsTab.tsx | 361 +++++++------ src/pages/Admin/UserManagement/RolesTab.tsx | 245 +++++---- .../UserManagement/UserManagement.module.css | 78 --- src/pages/Admin/UserManagement/UsersTab.tsx | 477 +++++++++--------- 4 files changed, 519 insertions(+), 642 deletions(-) diff --git a/src/pages/Admin/UserManagement/GroupsTab.tsx b/src/pages/Admin/UserManagement/GroupsTab.tsx index 4714ea9..480375c 100644 --- a/src/pages/Admin/UserManagement/GroupsTab.tsx +++ b/src/pages/Admin/UserManagement/GroupsTab.tsx @@ -11,8 +11,10 @@ import { InlineEdit } from '../../../design-system/primitives/InlineEdit/InlineE import { MultiSelect } from '../../../design-system/composites/MultiSelect/MultiSelect' import { ConfirmDialog } from '../../../design-system/composites/ConfirmDialog/ConfirmDialog' import { AlertDialog } from '../../../design-system/composites/AlertDialog/AlertDialog' +import { SplitPane } from '../../../design-system/composites/SplitPane/SplitPane' +import { EntityList } from '../../../design-system/composites/EntityList/EntityList' import { useToast } from '../../../design-system/composites/Toast/Toast' -import { MOCK_GROUPS, MOCK_USERS, MOCK_ROLES, getChildGroups, type MockGroup } from './rbacMocks' +import { MOCK_GROUPS, MOCK_USERS, MOCK_ROLES, type MockGroup } from './rbacMocks' import styles from './UserManagement.module.css' export function GroupsTab() { @@ -83,207 +85,190 @@ export function GroupsTab() { return ( <> -
-
-
- setSearch(e.target.value)} - onClear={() => setSearch('')} - className={styles.listHeaderSearch} - /> - -
- - {creating && ( -
- setNewName(e.target.value)} /> - {duplicateGroupName && Group name already exists} - setNewName(e.target.value)} /> + {duplicateGroupName && Group name already exists} + setSearch(e.target.value)} - onClear={() => setSearch('')} - className={styles.listHeaderSearch} - /> - -
- - {creating && ( -
- setNewName(e.target.value)} /> - {duplicateRoleName && Role name already exists} - setNewDesc(e.target.value)} /> -
- - -
-
- )} - -
- {filtered.map((role) => ( -
setSelectedId(role.id)} - role="option" - tabIndex={0} - aria-selected={selectedId === role.id} - onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); 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) => )} -
+ + {creating && ( +
+ setNewName(e.target.value)} /> + {duplicateRoleName && Role name already exists} + setNewDesc(e.target.value)} /> +
+ +
- ))} - {filtered.length === 0 && ( -
No roles match your search
)} -
-
-
- {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 - + ( + <> + +
+
+ {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) => )} +
+
+ )} - - ) : ( -
Select a role to view details
- )} -
-
+ 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={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 + + )} + + ) : null} + emptyMessage="Select a role to view details" + /> -
-
-
- setSearch(e.target.value)} - onClear={() => setSearch('')} - className={styles.listHeaderSearch} - /> - -
- - {creating && ( -
- setNewProvider(v as 'local' | 'oidc')} orientation="horizontal"> - - - -
- setNewUsername(e.target.value)} /> - setNewDisplay(e.target.value)} /> -
- {duplicateUsername && Username already exists} - setNewEmail(e.target.value)} /> - {newProvider === 'local' && ( - setNewPassword(e.target.value)} /> - )} - {newProvider === 'oidc' && ( - - OIDC users authenticate via the configured identity provider. Pre-register to assign roles/groups before their first login. - - )} -
- - -
-
- )} - -
- {filtered.map((user) => ( -
{ setSelectedId(user.id); setResettingPassword(false) }} - role="option" - tabIndex={0} - aria-selected={selectedId === user.id} - onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSelectedId(user.id); setResettingPassword(false) } }} - > - -
-
- {user.displayName} - {user.provider !== 'local' && ( - - )} -
-
- {user.email} · {getUserGroupPath(user)} -
-
- {user.directRoles.map((r) => )} - {user.directGroups.map((gId) => { - const g = MOCK_GROUPS.find((gr) => gr.id === gId) - return g ? : null - })} -
+ + {creating && ( +
+ setNewProvider(v as 'local' | 'oidc')} orientation="horizontal"> + + + +
+ setNewUsername(e.target.value)} /> + setNewDisplay(e.target.value)} /> +
+ {duplicateUsername && Username already exists} + setNewEmail(e.target.value)} /> + {newProvider === 'local' && ( + setNewPassword(e.target.value)} /> + )} + {newProvider === 'oidc' && ( + + OIDC users authenticate via the configured identity provider. Pre-register to assign roles/groups before their first login. + + )} +
+ +
- ))} - {filtered.length === 0 && ( -
No users match your search
)} -
-
-
- {selected ? ( - <> -
- -
-
- updateUser(selected.id, { displayName: v })} - /> -
-
{selected.email}
-
- -
- - Status -
- -
- -
- ID - {selected.id} - Created - {new Date(selected.createdAt).toLocaleDateString()} - Provider - {selected.provider} -
- - Security -
- {selected.provider === 'local' ? ( - <> -
- Password - •••••••• - {!resettingPassword && ( - + ( + <> + +
+
+ {user.displayName} + {user.provider !== 'local' && ( + )}
- {resettingPassword && ( -
- setNewPw(e.target.value)} - className={styles.resetInput} - /> - - -
- )} - - ) : ( - <> -
- Authentication - OIDC ({selected.provider}) +
+ {user.email} · {getUserGroupPath(user)}
- - Password managed by the identity provider. - - - )} -
- - Group membership (direct only) -
- {selected.directGroups.map((gId) => { - const g = MOCK_GROUPS.find((gr) => gr.id === gId) - return g ? ( - { - const group = MOCK_GROUPS.find((gr) => gr.id === gId) - if (group && group.directRoles.length > 0) { - setRemoveGroupTarget(gId) - } else { - updateUser(selected.id, { directGroups: selected.directGroups.filter((id) => id !== gId) }) - toast({ title: 'Group removed', variant: 'success' }) - } - }} - /> - ) : null - })} - {selected.directGroups.length === 0 && ( - (no groups) - )} - { - updateUser(selected.id, { directGroups: [...selected.directGroups, ...ids] }) - toast({ title: `${ids.length} group(s) added`, variant: 'success' }) - }} - placeholder="+ Add" - /> -
- - Effective roles (direct + inherited) -
- {effectiveRoles.map(({ role, source }) => - source === 'direct' ? ( - { - updateUser(selected.id, { directRoles: selected.directRoles.filter((r) => r !== role) }) - toast({ title: 'Role removed', description: role, variant: 'success' }) - }} - /> - ) : ( - - ) - )} - {effectiveRoles.length === 0 && ( - (no roles) - )} - { - updateUser(selected.id, { directRoles: [...selected.directRoles, ...roles] }) - toast({ title: `${roles.length} role(s) added`, variant: 'success' }) - }} - placeholder="+ Add" - /> -
- {effectiveRoles.some((r) => r.source !== 'direct') && ( - - Roles with ↑ are inherited through group membership - +
+ {user.directRoles.map((r) => )} + {user.directGroups.map((gId) => { + const g = MOCK_GROUPS.find((gr) => gr.id === gId) + return g ? : null + })} +
+
+ )} - - ) : ( -
Select a user to view details
- )} -
-
+ getItemId={(user) => user.id} + selectedId={selectedId ?? undefined} + onSelect={(id) => { setSelectedId(id); setResettingPassword(false) }} + searchPlaceholder="Search users..." + onSearch={setSearch} + addLabel="+ Add user" + onAdd={() => setCreating(true)} + emptyMessage="No users match your search" + /> + + } + detail={selected ? ( + <> +
+ +
+
+ updateUser(selected.id, { displayName: v })} + /> +
+
{selected.email}
+
+ +
+ + Status +
+ +
+ +
+ ID + {selected.id} + Created + {new Date(selected.createdAt).toLocaleDateString()} + Provider + {selected.provider} +
+ + Security +
+ {selected.provider === 'local' ? ( + <> +
+ Password + •••••••• + {!resettingPassword && ( + + )} +
+ {resettingPassword && ( +
+ setNewPw(e.target.value)} + className={styles.resetInput} + /> + + +
+ )} + + ) : ( + <> +
+ Authentication + OIDC ({selected.provider}) +
+ + Password managed by the identity provider. + + + )} +
+ + Group membership (direct only) +
+ {selected.directGroups.map((gId) => { + const g = MOCK_GROUPS.find((gr) => gr.id === gId) + return g ? ( + { + const group = MOCK_GROUPS.find((gr) => gr.id === gId) + if (group && group.directRoles.length > 0) { + setRemoveGroupTarget(gId) + } else { + updateUser(selected.id, { directGroups: selected.directGroups.filter((id) => id !== gId) }) + toast({ title: 'Group removed', variant: 'success' }) + } + }} + /> + ) : null + })} + {selected.directGroups.length === 0 && ( + (no groups) + )} + { + updateUser(selected.id, { directGroups: [...selected.directGroups, ...ids] }) + toast({ title: `${ids.length} group(s) added`, variant: 'success' }) + }} + placeholder="+ Add" + /> +
+ + Effective roles (direct + inherited) +
+ {effectiveRoles.map(({ role, source }) => + source === 'direct' ? ( + { + updateUser(selected.id, { directRoles: selected.directRoles.filter((r) => r !== role) }) + toast({ title: 'Role removed', description: role, variant: 'success' }) + }} + /> + ) : ( + + ) + )} + {effectiveRoles.length === 0 && ( + (no roles) + )} + { + updateUser(selected.id, { directRoles: [...selected.directRoles, ...roles] }) + toast({ title: `${roles.length} role(s) added`, variant: 'success' }) + }} + placeholder="+ Add" + /> +
+ {effectiveRoles.some((r) => r.source !== 'direct') && ( + + Roles with ↑ are inherited through group membership + + )} + + ) : null} + emptyMessage="Select a user to view details" + />