# Admin Redesign Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Overhaul admin section UX/UI to match the design language of the rest of the application, fix critical bugs, and improve usability. **Architecture:** Mostly file edits — replacing custom implementations with design system composites (DataTable, Tabs), fixing tokens, reworking the user creation flow with provider awareness, and adding toast feedback + accessibility. **Tech Stack:** React 18, TypeScript, CSS Modules, Vitest + RTL **Spec:** `docs/superpowers/specs/2026-03-18-admin-redesign.md` --- ## File Map ### Modified files ``` src/pages/Admin/Admin.tsx — Replace custom nav with Tabs, import Tabs src/pages/Admin/Admin.module.css — Remove nav styles, fix padding src/pages/Admin/AuditLog/AuditLog.tsx — Rewrite to use DataTable src/pages/Admin/AuditLog/AuditLog.module.css — Replace with card + filter styles only src/pages/Admin/AuditLog/auditMocks.ts — Change id to string src/pages/Admin/OidcConfig/OidcConfig.tsx — Remove h2 title, add toolbar src/pages/Admin/OidcConfig/OidcConfig.module.css — Remove header styles, center form src/pages/Admin/UserManagement/UserManagement.tsx — Replace inline style src/pages/Admin/UserManagement/UserManagement.module.css — Fix tokens, radii, shadow, add tabContent + empty/security styles src/pages/Admin/UserManagement/UsersTab.tsx — Rework create form, add security section, toasts, accessibility, confirmations src/pages/Admin/UserManagement/GroupsTab.tsx — Add toasts, accessibility, confirmations, empty state src/pages/Admin/UserManagement/RolesTab.tsx — Replace emoji, add toasts, accessibility, empty state src/pages/Admin/UserManagement/rbacMocks.ts — No changes needed (provider field already exists) ``` --- ### Task 1: Fix critical token bug + visual polish **Files:** - Modify: `src/pages/Admin/Admin.module.css` - Modify: `src/pages/Admin/UserManagement/UserManagement.module.css` - [ ] **Step 1: Fix `--bg-base` token in Admin.module.css** In `Admin.module.css`, replace `var(--bg-base)` with `var(--bg-surface)` on line 6. Also fix the content padding on line 35: change `padding: 20px` to `padding: 20px 24px 40px`. - [ ] **Step 2: Fix `--bg-base` token and visual polish in UserManagement.module.css** Replace both `var(--bg-base)` occurrences (lines 12, 19) with `var(--bg-surface)`. Change `border-radius: var(--radius-md)` to `var(--radius-lg)` on `.splitPane` (line 7), `.listPane` (line 15), and `.detailPane` (line 22). Add `box-shadow: var(--shadow-card)` to `.splitPane`. Add these new classes at the end of the file: ```css .tabContent { margin-top: 16px; } .emptySearch { padding: 32px; text-align: center; color: var(--text-faint); font-size: 12px; font-family: var(--font-body); } .securitySection { margin-top: 8px; margin-bottom: 8px; } .securityRow { display: flex; align-items: center; gap: 12px; font-size: 12px; font-family: var(--font-body); color: var(--text-primary); } .passwordDots { font-family: var(--font-mono); letter-spacing: 2px; } .resetForm { display: flex; gap: 8px; align-items: center; margin-top: 8px; } .resetInput { width: 200px; } ``` - [ ] **Step 3: Commit** ```bash git add src/pages/Admin/Admin.module.css src/pages/Admin/UserManagement/UserManagement.module.css git commit -m "fix: replace nonexistent --bg-base token with --bg-surface, fix radii and padding" ``` --- ### Task 2: Replace admin nav with Tabs composite **Files:** - Modify: `src/pages/Admin/Admin.tsx` - Modify: `src/pages/Admin/Admin.module.css` - [ ] **Step 1: Rewrite Admin.tsx** Replace the entire file with: ```tsx import { useNavigate, useLocation } from 'react-router-dom' import { AppShell } from '../../design-system/layout/AppShell/AppShell' import { Sidebar } from '../../design-system/layout/Sidebar/Sidebar' import { TopBar } from '../../design-system/layout/TopBar/TopBar' import { Tabs } from '../../design-system/composites/Tabs/Tabs' import { SIDEBAR_APPS } from '../../mocks/sidebar' import styles from './Admin.module.css' import type { ReactNode } from 'react' const ADMIN_TABS = [ { label: 'User Management', value: '/admin/rbac' }, { label: 'Audit Log', value: '/admin/audit' }, { label: 'OIDC', value: '/admin/oidc' }, ] interface AdminLayoutProps { title: string children: ReactNode } export function AdminLayout({ title, children }: AdminLayoutProps) { const navigate = useNavigate() const location = useLocation() return ( }> navigate(path)} />
{children}
) } ``` - [ ] **Step 2: Clean up Admin.module.css** Remove `.adminNav`, `.adminTab`, `.adminTab:hover`, `.adminTabActive` styles entirely. Keep only `.adminContent`. - [ ] **Step 3: Commit** ```bash git add src/pages/Admin/Admin.tsx src/pages/Admin/Admin.module.css git commit -m "refactor: replace custom admin nav with Tabs composite" ``` --- ### Task 3: Fix AuditLog mock data + migrate to DataTable **Files:** - Modify: `src/pages/Admin/AuditLog/auditMocks.ts` - Modify: `src/pages/Admin/AuditLog/AuditLog.tsx` - Modify: `src/pages/Admin/AuditLog/AuditLog.module.css` - [ ] **Step 1: Change AuditEvent id to string in auditMocks.ts** Change `id: number` to `id: string` in the `AuditEvent` interface. Change all mock IDs from numbers to strings: `id: 1` → `id: 'audit-1'`, `id: 2` → `id: 'audit-2'`, etc. through all 25 events. - [ ] **Step 2: Rewrite AuditLog.tsx to use DataTable** Replace the entire file with: ```tsx import { useState, useMemo } from 'react' import { AdminLayout } from '../Admin' import { Badge } from '../../../design-system/primitives/Badge/Badge' import { DateRangePicker } from '../../../design-system/primitives/DateRangePicker/DateRangePicker' import { Input } from '../../../design-system/primitives/Input/Input' import { Select } from '../../../design-system/primitives/Select/Select' import { MonoText } from '../../../design-system/primitives/MonoText/MonoText' import { CodeBlock } from '../../../design-system/primitives/CodeBlock/CodeBlock' import { DataTable } from '../../../design-system/composites/DataTable/DataTable' import type { Column } from '../../../design-system/composites/DataTable/types' import type { DateRange } from '../../../design-system/utils/timePresets' import { AUDIT_EVENTS, type AuditEvent } from './auditMocks' import styles from './AuditLog.module.css' const CATEGORIES = [ { value: '', label: 'All categories' }, { value: 'INFRA', label: 'INFRA' }, { value: 'AUTH', label: 'AUTH' }, { value: 'USER_MGMT', label: 'USER_MGMT' }, { value: 'CONFIG', label: 'CONFIG' }, ] function formatTimestamp(iso: string): string { return new Date(iso).toLocaleString('en-GB', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }) } const COLUMNS: Column[] = [ { key: 'timestamp', header: 'Timestamp', width: '170px', sortable: true, render: (_, row) => {formatTimestamp(row.timestamp)}, }, { key: 'username', header: 'User', sortable: true, render: (_, row) => {row.username}, }, { key: 'category', header: 'Category', width: '110px', sortable: true, render: (_, row) => , }, { key: 'action', header: 'Action' }, { key: 'target', header: 'Target', render: (_, row) => {row.target}, }, { key: 'result', header: 'Result', width: '90px', sortable: true, render: (_, row) => ( ), }, ] const now = Date.now() const INITIAL_RANGE: DateRange = { from: new Date(now - 7 * 24 * 3600_000).toISOString().slice(0, 16), to: new Date(now).toISOString().slice(0, 16), } export function AuditLog() { const [dateRange, setDateRange] = useState(INITIAL_RANGE) const [userFilter, setUserFilter] = useState('') const [categoryFilter, setCategoryFilter] = useState('') const [searchFilter, setSearchFilter] = useState('') const filtered = useMemo(() => { const from = new Date(dateRange.from).getTime() const to = new Date(dateRange.to).getTime() return AUDIT_EVENTS.filter((e) => { const ts = new Date(e.timestamp).getTime() if (ts < from || ts > to) return false if (userFilter && !e.username.toLowerCase().includes(userFilter.toLowerCase())) return false if (categoryFilter && e.category !== categoryFilter) return false if (searchFilter) { const q = searchFilter.toLowerCase() if (!e.action.toLowerCase().includes(q) && !e.target.toLowerCase().includes(q)) return false } return true }) }, [dateRange, userFilter, categoryFilter, searchFilter]) return (
setUserFilter(e.target.value)} onClear={() => setUserFilter('')} className={styles.filterInput} /> setSearchFilter(e.target.value)} onClear={() => setSearchFilter('')} className={styles.filterInput} />
Audit Log
{filtered.length} events
row.result === 'FAILURE' ? 'error' : undefined} expandedContent={(row) => (
IP Address {row.ipAddress}
User Agent {row.userAgent}
Detail
)} />
) } ``` - [ ] **Step 3: Rewrite AuditLog.module.css** Replace the entire file with: ```css .filters { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; } .filterInput { width: 200px; } .filterSelect { width: 160px; } .tableSection { background: var(--bg-surface); border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); overflow: hidden; } .tableHeader { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--border-subtle); } .tableTitle { font-size: 13px; font-weight: 600; color: var(--text-primary); } .tableRight { display: flex; align-items: center; gap: 10px; } .tableMeta { font-size: 11px; color: var(--text-muted); } .target { display: inline-block; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .expandedDetail { padding: 4px 0; } .detailGrid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; } .detailField { display: flex; flex-direction: column; gap: 4px; } .detailLabel { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; color: var(--text-muted); font-family: var(--font-body); } .detailValue { font-size: 12px; color: var(--text-secondary); } ``` - [ ] **Step 4: Verify build** Run: `npx vite build 2>&1 | tail -5` Expected: Build succeeds - [ ] **Step 5: Commit** ```bash git add src/pages/Admin/AuditLog/ git commit -m "refactor: migrate AuditLog to DataTable with card wrapper" ``` --- ### Task 4: Remove duplicate titles from OidcConfig **Files:** - Modify: `src/pages/Admin/OidcConfig/OidcConfig.tsx` - Modify: `src/pages/Admin/OidcConfig/OidcConfig.module.css` - [ ] **Step 1: Replace h2 header with toolbar in OidcConfig.tsx** Replace the `.header` div (lines 74-84) with a compact toolbar: ```tsx
``` - [ ] **Step 2: Update OidcConfig.module.css** Remove `.header`, `.title`, `.headerActions`. Add: ```css .toolbar { display: flex; gap: 8px; justify-content: flex-end; margin-bottom: 20px; } ``` Also add `margin: 0 auto` to the `.page` class to center the form. - [ ] **Step 3: Commit** ```bash git add src/pages/Admin/OidcConfig/ git commit -m "refactor: remove duplicate title from OIDC page, center form" ``` --- ### Task 5: Replace inline style + fix UserManagement **Files:** - Modify: `src/pages/Admin/UserManagement/UserManagement.tsx` - [ ] **Step 1: Replace inline style with CSS class** Change line 20 from `
` to `
`. Add the `styles` import if not already present (it already imports from `./UserManagement.module.css` — wait, it doesn't currently. Add: ```tsx import styles from './UserManagement.module.css' ``` - [ ] **Step 2: Commit** ```bash git add src/pages/Admin/UserManagement/UserManagement.tsx git commit -m "refactor: replace inline style with CSS module class" ``` --- ### Task 6: Add toasts to all RBAC tabs **Files:** - Modify: `src/pages/Admin/UserManagement/UsersTab.tsx` - Modify: `src/pages/Admin/UserManagement/GroupsTab.tsx` - Modify: `src/pages/Admin/UserManagement/RolesTab.tsx` - [ ] **Step 1: Add useToast to UsersTab** Add import: `import { useToast } from '../../../design-system/composites/Toast/Toast'` Add `const { toast } = useToast()` at the top of the `UsersTab` function body. Add toast calls: - After `setSelectedId(newUser.id)` in `handleCreate`: `toast({ title: 'User created', description: newUser.displayName, variant: 'success' })` - After `setDeleteTarget(null)` in `handleDelete`: `toast({ title: 'User deleted', description: deleteTarget.username, variant: 'warning' })` - [ ] **Step 2: Add useToast to GroupsTab** Same pattern. Add toast calls: - After create: `toast({ title: 'Group created', description: newGroup.name, variant: 'success' })` - After delete: `toast({ title: 'Group deleted', description: deleteTarget.name, variant: 'warning' })` - [ ] **Step 3: Add useToast to RolesTab** Same pattern. Add toast calls: - After create: `toast({ title: 'Role created', description: newRole.name, variant: 'success' })` - After delete: `toast({ title: 'Role deleted', description: deleteTarget.name, variant: 'warning' })` - [ ] **Step 4: Commit** ```bash git add src/pages/Admin/UserManagement/UsersTab.tsx src/pages/Admin/UserManagement/GroupsTab.tsx src/pages/Admin/UserManagement/RolesTab.tsx git commit -m "feat: add toast notifications to all RBAC mutations" ``` --- ### Task 7: Rework user creation form + password management **Files:** - Modify: `src/pages/Admin/UserManagement/UsersTab.tsx` This is the largest single task. It covers spec items 3.2 (provider-aware create form), 3.3 (password management in detail pane), and 3.6 (remove unused password field). - [ ] **Step 1: Rework the create form section** Replace the create form state variables (lines 23-26): ```tsx const [newUsername, setNewUsername] = useState('') const [newDisplay, setNewDisplay] = useState('') const [newEmail, setNewEmail] = useState('') const [newPassword, setNewPassword] = useState('') ``` with: ```tsx const [newUsername, setNewUsername] = useState('') const [newDisplay, setNewDisplay] = useState('') const [newEmail, setNewEmail] = useState('') const [newPassword, setNewPassword] = useState('') const [newProvider, setNewProvider] = useState<'local' | 'oidc'>('local') ``` Add imports for RadioGroup, RadioItem, and InfoCallout: ```tsx import { RadioGroup, RadioItem } from '../../../design-system/primitives/Radio/Radio' import { InfoCallout } from '../../../design-system/primitives/InfoCallout/InfoCallout' ``` Update `handleCreate` to use the provider selection and validate password for local: ```tsx function handleCreate() { if (!newUsername.trim()) return if (newProvider === 'local' && !newPassword.trim()) return const newUser: MockUser = { id: `usr-${Date.now()}`, username: newUsername.trim(), displayName: newDisplay.trim() || newUsername.trim(), email: newEmail.trim(), provider: newProvider, createdAt: new Date().toISOString(), directRoles: [], directGroups: [], } setUsers((prev) => [...prev, newUser]) setCreating(false) setNewUsername(''); setNewDisplay(''); setNewEmail(''); setNewPassword(''); setNewProvider('local') setSelectedId(newUser.id) toast({ title: 'User created', description: newUser.displayName, variant: 'success' }) } ``` Replace the create form JSX (lines 100-114) with: ```tsx {creating && (
setNewProvider(v as 'local' | 'oidc')} orientation="horizontal">
setNewUsername(e.target.value)} /> setNewDisplay(e.target.value)} />
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. )}
)} ``` - [ ] **Step 2: Add Security section to detail pane** Add password reset state at the top of the component: ```tsx const [resettingPassword, setResettingPassword] = useState(false) const [newPw, setNewPw] = useState('') ``` Add the Security section after the metadata grid (after the `
` closing the `.metaGrid`), before the "Group membership" SectionHeader: ```tsx Security
{selected.provider === 'local' ? ( <>
Password •••••••• {!resettingPassword && ( )}
{resettingPassword && (
setNewPw(e.target.value)} className={styles.resetInput} />
)} ) : ( <>
Authentication OIDC ({selected.provider})
Password managed by the identity provider. )}
``` - [ ] **Step 3: Verify build** Run: `npx vite build 2>&1 | tail -5` Expected: Build succeeds - [ ] **Step 4: Commit** ```bash git add src/pages/Admin/UserManagement/UsersTab.tsx git commit -m "feat: rework user creation with provider selection, add password management" ``` --- ### Task 8: Keyboard accessibility for entity lists **Files:** - Modify: `src/pages/Admin/UserManagement/UsersTab.tsx` - Modify: `src/pages/Admin/UserManagement/GroupsTab.tsx` - Modify: `src/pages/Admin/UserManagement/RolesTab.tsx` - [ ] **Step 1: Add keyboard support to UsersTab entity list** On the `.entityList` wrapper div, add: ```tsx
``` On each `.entityItem` div, add `role`, `tabIndex`, `aria-selected`, and `onKeyDown`: ```tsx
setSelectedId(user.id)} role="option" tabIndex={0} aria-selected={selectedId === user.id} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSelectedId(user.id) } }} > ``` Add empty-search state after the list map: ```tsx {filtered.length === 0 && (
No users match your search
)} ``` - [ ] **Step 2: Add keyboard support to GroupsTab entity list** Same pattern — add `role="listbox"` to container, `role="option"` + `tabIndex={0}` + `aria-selected` + `onKeyDown` to items, and empty-search state. - [ ] **Step 3: Add keyboard support to RolesTab entity list** Same pattern. Also replace the lock emoji on line 113: ```tsx {role.system && 🔒} ``` with: ```tsx {role.system && } ``` Add empty-search state. - [ ] **Step 4: Commit** ```bash git add src/pages/Admin/UserManagement/UsersTab.tsx src/pages/Admin/UserManagement/GroupsTab.tsx src/pages/Admin/UserManagement/RolesTab.tsx git commit -m "feat: add keyboard accessibility and empty states to entity lists" ``` --- ### Task 9: Add confirmation for cascading removals **Files:** - Modify: `src/pages/Admin/UserManagement/UsersTab.tsx` - Modify: `src/pages/Admin/UserManagement/GroupsTab.tsx` - [ ] **Step 1: Add group removal confirmation in UsersTab** Add state for tracking the removal target: ```tsx const [removeGroupTarget, setRemoveGroupTarget] = useState(null) ``` Replace the direct `onRemove` on group Tags (in the "Group membership" section) with: ```tsx onRemove={() => { 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' }) } }} ``` Add an AlertDialog (import from composites) for the confirmation: ```tsx setRemoveGroupTarget(null)} onConfirm={() => { if (removeGroupTarget && selected) { updateUser(selected.id, { directGroups: selected.directGroups.filter((id) => id !== removeGroupTarget) }) toast({ title: 'Group removed', variant: 'success' }) } setRemoveGroupTarget(null) }} title="Remove group membership" description={`Removing this group will also revoke inherited roles: ${MOCK_GROUPS.find((g) => g.id === removeGroupTarget)?.directRoles.join(', ') ?? ''}. Continue?`} confirmLabel="Remove" variant="warning" /> ``` Add import: `import { AlertDialog } from '../../../design-system/composites/AlertDialog/AlertDialog'` - [ ] **Step 2: Add role removal confirmation in GroupsTab** Add state: ```tsx const [removeRoleTarget, setRemoveRoleTarget] = useState(null) ``` Replace direct `onRemove` on role Tags with: ```tsx onRemove={() => { const memberCount = MOCK_USERS.filter((u) => u.directGroups.includes(selected.id)).length if (memberCount > 0) { setRemoveRoleTarget(r) } else { updateGroup(selected.id, { directRoles: selected.directRoles.filter((role) => role !== r) }) toast({ title: 'Role removed', variant: 'success' }) } }} ``` Add AlertDialog: ```tsx setRemoveRoleTarget(null)} onConfirm={() => { if (removeRoleTarget && selected) { updateGroup(selected.id, { directRoles: selected.directRoles.filter((role) => role !== removeRoleTarget) }) toast({ title: 'Role removed', variant: 'success' }) } setRemoveRoleTarget(null) }} title="Remove role from group" description={`Removing ${removeRoleTarget} from ${selected?.name} will affect ${members.length} member(s) who inherit this role. Continue?`} confirmLabel="Remove" variant="warning" /> ``` Add import: `import { AlertDialog } from '../../../design-system/composites/AlertDialog/AlertDialog'` - [ ] **Step 3: Commit** ```bash git add src/pages/Admin/UserManagement/UsersTab.tsx src/pages/Admin/UserManagement/GroupsTab.tsx git commit -m "feat: add confirmation dialogs for cascading removals" ``` --- ### Task 10: Add duplicate name validation to create forms **Files:** - Modify: `src/pages/Admin/UserManagement/UsersTab.tsx` - Modify: `src/pages/Admin/UserManagement/GroupsTab.tsx` - Modify: `src/pages/Admin/UserManagement/RolesTab.tsx` - [ ] **Step 1: Add duplicate check in UsersTab** Add a computed `duplicateUsername`: ```tsx const duplicateUsername = newUsername.trim() && users.some((u) => u.username.toLowerCase() === newUsername.trim().toLowerCase()) ``` Update the Create button `disabled` to include `|| duplicateUsername`. Show error text below the username Input when duplicate: ```tsx {duplicateUsername && Username already exists} ``` - [ ] **Step 2: Add duplicate check in GroupsTab** Similar pattern with `duplicateGroupName` check. Disable Create button when duplicate. - [ ] **Step 3: Add duplicate check in RolesTab** Similar pattern with `duplicateRoleName` check (compare uppercase). Disable Create button when duplicate. - [ ] **Step 4: Commit** ```bash git add src/pages/Admin/UserManagement/UsersTab.tsx src/pages/Admin/UserManagement/GroupsTab.tsx src/pages/Admin/UserManagement/RolesTab.tsx git commit -m "feat: add duplicate name validation to create forms" ``` --- ### Task 11: Final verification - [ ] **Step 1: Run full test suite** Run: `npx vitest run` Expected: All tests pass - [ ] **Step 2: Build the project** Run: `npx vite build` Expected: Build succeeds - [ ] **Step 3: Fix any issues** If build fails, fix TypeScript errors. Common issues: - Import path typos - Missing props on components - InfoCallout `variant` prop — check the actual prop name (may be `color` instead) - [ ] **Step 4: Commit fixes if needed** ```bash git add -A git commit -m "fix: resolve build issues from admin redesign" ```