refactor: admin section UX/UI redesign
All checks were successful
Build & Publish / publish (push) Successful in 43s
All checks were successful
Build & Publish / publish (push) Successful in 43s
- Fix critical --bg-base token bug (dark mode broken), replace with --bg-surface - Replace hand-rolled admin nav with Tabs composite (proper ARIA) - Migrate AuditLog from custom table to DataTable with sorting, row accents, card wrapper - Remove duplicate h2 page titles (breadcrumb + tab already identify the page) - Rework user creation with provider-aware form (Local/OIDC RadioGroup) - Add Security section with password reset for local users, OIDC info for external - Add toast notifications to all RBAC mutations (create/delete/add/remove) - Add confirmation dialogs for cascading removals (group/role) - Add keyboard accessibility to entity lists (role/tabIndex/aria-selected) - Add empty search states, duplicate name validation - Replace lock emoji with Badge, fix radii/shadow/padding consistency - Badge dashed variant keeps background color - Inherited roles shown with dashed outline + reduced opacity - Inline MultiSelect (+Add) for groups, roles, members, child groups - Center OIDC form, replace inline styles with CSS modules Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,10 +7,12 @@ 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 { useToast } from '../../../design-system/composites/Toast/Toast'
|
||||
import { MOCK_ROLES, MOCK_GROUPS, MOCK_USERS, getEffectiveRoles, type MockRole } from './rbacMocks'
|
||||
import styles from './UserManagement.module.css'
|
||||
|
||||
export function RolesTab() {
|
||||
const { toast } = useToast()
|
||||
const [roles, setRoles] = useState(MOCK_ROLES)
|
||||
const [search, setSearch] = useState('')
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
@@ -43,6 +45,7 @@ export function RolesTab() {
|
||||
setCreating(false)
|
||||
setNewName(''); setNewDesc('')
|
||||
setSelectedId(newRole.id)
|
||||
toast({ title: 'Role created', description: newRole.name, variant: 'success' })
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
@@ -50,8 +53,11 @@ export function RolesTab() {
|
||||
setRoles((prev) => prev.filter((r) => r.id !== deleteTarget.id))
|
||||
if (selectedId === deleteTarget.id) setSelectedId(null)
|
||||
setDeleteTarget(null)
|
||||
toast({ title: 'Role deleted', description: deleteTarget.name, variant: 'warning' })
|
||||
}
|
||||
|
||||
const duplicateRoleName = newName.trim() !== '' && roles.some((r) => r.name === newName.trim().toUpperCase())
|
||||
|
||||
// Role assignments
|
||||
const assignedGroups = selected
|
||||
? MOCK_GROUPS.filter((g) => g.directRoles.includes(selected.name))
|
||||
@@ -91,26 +97,31 @@ export function RolesTab() {
|
||||
{creating && (
|
||||
<div className={styles.createForm}>
|
||||
<Input placeholder="Role name *" value={newName} onChange={(e) => setNewName(e.target.value)} />
|
||||
{duplicateRoleName && <span style={{ color: 'var(--error)', fontSize: 11 }}>Role name already exists</span>}
|
||||
<Input placeholder="Description" value={newDesc} onChange={(e) => setNewDesc(e.target.value)} />
|
||||
<div className={styles.createFormActions}>
|
||||
<Button size="sm" variant="ghost" onClick={() => setCreating(false)}>Cancel</Button>
|
||||
<Button size="sm" variant="primary" onClick={handleCreate} disabled={!newName.trim()}>Create</Button>
|
||||
<Button size="sm" variant="primary" onClick={handleCreate} disabled={!newName.trim() || duplicateRoleName}>Create</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.entityList}>
|
||||
<div className={styles.entityList} role="listbox" aria-label="Roles">
|
||||
{filtered.map((role) => (
|
||||
<div
|
||||
key={role.id}
|
||||
className={`${styles.entityItem} ${selectedId === role.id ? styles.entityItemSelected : ''}`}
|
||||
onClick={() => 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) } }}
|
||||
>
|
||||
<Avatar name={role.name} size="sm" />
|
||||
<div className={styles.entityInfo}>
|
||||
<div className={styles.entityName}>
|
||||
{role.name}
|
||||
{role.system && <span title="System role"> 🔒</span>}
|
||||
{role.system && <Badge label="system" color="auto" variant="outlined" className={styles.providerBadge} />}
|
||||
</div>
|
||||
<div className={styles.entityMeta}>
|
||||
{role.description} · {getAssignmentCount(role)} assignments
|
||||
@@ -124,6 +135,9 @@ export function RolesTab() {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{filtered.length === 0 && (
|
||||
<div className={styles.emptySearch}>No roles match your search</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user