# Phase 6: User Management UI Implementation > **Status**: Planning Complete > **Created**: 2026-01-24 > **Estimated Effort**: L (Large) - Initially estimated 40-60 hours, **revised to 16-22 hours** > **Priority**: P2 - Feature Completeness > **Tests Targeted**: 19 skipped tests in `tests/settings/user-management.spec.ts` > **Dependencies**: Phase 5 (TestDataManager Auth Fix) - Infrastructure complete, blocked by environment config --- ## Executive Summary ### Goals Complete the User Management frontend to enable 19 currently-skipped Playwright E2E tests. This phase implements missing UI components including status badges with proper color classes, role badges, resend invite action, email validation, enhanced modal accessibility, and fixes a React anti-pattern bug. ### Key Finding **Most UI components already exist.** After thorough analysis, the work is primarily: 1. Verifying existing functionality (toast test IDs already exist) 2. Implementing resend invite action (backend endpoint missing - needs implementation) 3. Adding email format validation with visible error 4. Fixing React anti-pattern in PermissionsModal 5. Verification and unskipping tests **Revised Effort**: 16-22 hours (pending backend resend endpoint scope). **Solution**: Add missing test selectors, implement resend invite, add email validation UI, fix React bugs, and systematically unskip tests as they pass. ### Test Count Reconciliation The original plan stated 22 tests, but verification shows **19 skipped test declarations**. The discrepancy came from counting 4 conditional `test.skip()` calls inside test bodies (not actual test declarations). See Section 2 for the complete inventory. --- ## 1. Current State Analysis ### What EXISTS (in `UsersPage.tsx`) The Users page at `frontend/src/pages/UsersPage.tsx` already contains substantial functionality: | Component | Status | Notes | |-----------|--------|-------| | User list table | ✅ Complete | Columns: User, Role, Status, Permissions, Enabled, Actions | | InviteModal | ✅ Complete | Email, role, permission mode, host selection, URL preview | | PermissionsModal | ✅ Complete | Edit user permissions, host toggle | | Role badges | ✅ Complete | Purple for admin, blue for user, rounded styling | | Status indicators | ✅ Complete | Active (green), Pending (yellow), Expired (red) with icons | | Enable/Disable toggle | ✅ Complete | Switch component per user | | Delete button | ✅ Complete | Trash2 icon with confirmation | | Settings/Permissions button | ✅ Complete | For non-admin users | | React Query mutations | ✅ Complete | All CRUD operations | | Copy invite link | ✅ Complete | With clipboard API | | URL preview for invites | ✅ Complete | Shows invite URL before sending | ### What is PARTIALLY IMPLEMENTED | Item | Issue | Fix Required | |------|-------|--------------| | Status badges | Class names may not match test expectations | Add explicit color classes | | Modal keyboard nav | Escape key handling may be missing | Add keyboard event handler | | PermissionsModal state init | **React anti-pattern: useState used like useEffect** | Fix to use useEffect (see Section 3.6) | ### What is MISSING | Item | Description | Effort | |------|-------------|--------| | Email validation UI | Client-side format validation with visible error | 2 hours | | Resend invite action | Button + API for pending users | 6-10 hours (backend missing) | | Backend resend endpoint | `POST /api/v1/users/{id}/resend-invite` | See Phase 6.4 | --- ## 2. Test Analysis ### Summary: 19 Skipped Tests **File**: `tests/settings/user-management.spec.ts` | # | Test Name | Line | Category | Skip Reason | Status | |---|-----------|------|----------|-------------|--------| | 1 | should show user status badges | 70 | User List | Status badges styling | ✅ Verify | | 2 | should display role badges | 110 | User List | Role badges selectors | ✅ Verify | | 3 | should show pending invite status | 164 | User List | Complex timing | ⚠️ Complex | | 4 | should open invite user modal | 217 | Invite | Outdated skip comment | ✅ Verify | | 5 | should validate email format | 283 | Invite | No client validation | 🔧 Implement | | 6 | should copy invite link | 442 | Invite | Toast verification | ✅ Verify | | 7 | should open permissions modal | 494 | Permissions | Settings icon | 🔒 Auth blocked | | 8 | should update permission mode | 538 | Permissions | Base URL auth | 🔒 Auth blocked | | 9 | should add permitted hosts | 612 | Permissions | Settings icon | 🔒 Auth blocked | | 10 | should remove permitted hosts | 669 | Permissions | Settings icon | 🔒 Auth blocked | | 11 | should save permission changes | 725 | Permissions | Settings icon | 🔒 Auth blocked | | 12 | should enable/disable user | 781 | Actions | TestDataManager | 🔒 Auth blocked | | 13 | should change user role | 828 | Actions | Not implemented | ❌ Future | | 14 | should delete user with confirmation | 848 | Actions | Delete button | 🔒 Auth blocked | | 15 | should resend invite for pending user | 956 | Actions | Not implemented | 🔧 Implement | | 16 | should be keyboard navigable | 1014 | A11y | Known flaky | ⚠️ Flaky | | 17 | should require admin role for access | 1091 | Security | Routing design | ℹ️ By design | | 18 | should show error for regular user access | 1125 | Security | Routing design | ℹ️ By design | | 19 | should have proper ARIA labels | 1157 | A11y | ARIA incomplete | ✅ Verify | ### Legend - ✅ Verify: Likely already works, just needs verification - 🔧 Fix/Implement: Requires small code change - 🔒 Auth blocked: Blocked by Phase 5 (TestDataManager) - ⚠️ Complex/Flaky: Timing or complexity issues - ℹ️ By design: Intentional skip (routing behavior) - ❌ Future: Feature not prioritized ### Tests Addressable in Phase 6 **Without Auth Fix** (can implement now): 6 tests - Test 1: Status badges styling (verify only) - Test 2: Role badges (verify only) - Test 4: Open invite modal (verify only - button IS implemented) - Test 5: Email validation - Test 6: Copy invite link (verify only - toast test IDs already exist) - Test 19: ARIA labels (verify only) **With Resend Invite**: 1 test - Test 15: Resend invite **After Phase 5 Auth Fix**: 6 tests - Tests 7-12, 14: Permission/Action tests ### Detailed Test Requirements #### Test 1: should show user status badges (Line 70) **Test code**: ```typescript const statusCell = page.locator('td').filter({ has: page.locator('span').filter({ hasText: /active|pending.*invite|invite.*expired/i, }), }); const activeStatus = page.locator('span').filter({ hasText: /^active$/i }); // Expects class to include 'green', 'text-green-400', or 'success' const hasGreenColor = await activeStatus.first().evaluate((el) => { return el.className.includes('green') || el.className.includes('text-green-400') || el.className.includes('success'); }); ``` **Current code** (UsersPage.tsx line ~459): ```tsx {t('common.active')} ``` **Analysis**: Current code already includes `text-green-400` class. **Action**: ✅ **Verify only** - unskip and run test. #### Test 2: should display role badges (Line 110) **Test code**: ```typescript const adminBadge = page.locator('span').filter({ hasText: /^admin$/i }); // Expects 'purple', 'blue', or 'rounded' in class const hasDistinctColor = await adminBadge.evaluate((el) => { return el.className.includes('purple') || el.className.includes('blue') || el.className.includes('rounded'); }); ``` **Current code** (UsersPage.tsx line ~445): ```tsx {user.role} ``` **Analysis**: ✅ Classes include `rounded` and `purple`/`blue`. **Action**: ✅ **Verify only** - unskip and run test. #### Test 4: should open invite user modal (Line 217) **Test code**: ```typescript const inviteButton = page.getByRole('button', { name: /invite.*user/i }); await expect(inviteButton).toBeVisible(); await inviteButton.click(); // Verify modal is visible const modal = page.getByRole('dialog'); await expect(modal).toBeVisible(); ``` **Current state**: ✅ Invite button IS implemented in UsersPage.tsx. The skip comment is outdated. **Action**: ✅ **Verify only** - unskip and run test. #### Test 5: should validate email format (Line 283) **Test code**: ```typescript const sendButton = page.getByRole('button', { name: /send.*invite/i }); const isDisabled = await sendButton.isDisabled(); // OR error message shown const errorMessage = page.getByText(/invalid.*email|email.*invalid|valid.*email/i); ``` **Current code**: Button disabled when `!email`, but no format validation visible. **Action**: 🔧 **Implement** - Add email regex validation with error display. #### Test 6: should copy invite link (Line 442) **Test code**: ```typescript const copiedToast = page.locator('[data-testid="toast-success"]').filter({ hasText: /copied|clipboard/i, }); ``` **Current state**: ✅ Toast component already has `data-testid={toast-${toast.type}}` at `Toast.tsx:31`. **Action**: ✅ **Verify only** - unskip and run test. No code changes needed. #### Test 15: should resend invite for pending user (Line 956) **Test code**: ```typescript const resendButton = page.getByRole('button', { name: /resend/i }); await resendButton.first().click(); await waitForToast(page, /sent|resend/i, { type: 'success' }); ``` **Current state**: ❌ Resend action not implemented. **Action**: 🔧 **Implement** - Add resend button for pending users + API call. #### Test 19: should have proper ARIA labels (Line 1157) **Test code**: ```typescript const inviteButton = page.getByRole('button', { name: /invite.*user/i }); // Checks for accessible name on action buttons const ariaLabel = await button.getAttribute('aria-label'); const title = await button.getAttribute('title'); const text = await button.textContent(); ``` **Current state**: - Invite button: text content "Invite User" ✅ - Delete button: `aria-label={t('users.deleteUser')}` ✅ - Settings button: `aria-label={t('users.editPermissions')}` ✅ **Action**: ✅ **Verify only** - unskip and run test. --- ## 3. Implementation Phases ### Phase 6.1: Verify Existing Functionality (3 hours) **Goal**: Confirm tests 1, 2, 4, 6, 19 pass without code changes. **Tests in Batch**: - Test 1: should show user status badges - Test 2: should display role badges - Test 4: should open invite user modal - Test 6: should copy invite link (toast test IDs already exist) - Test 19: should have proper ARIA labels **Tasks**: 1. Temporarily remove `test.skip` from tests 1, 2, 4, 6, 19 2. Run tests individually 3. Document results 4. Permanently unskip passing tests **Commands**: ```bash # Test status badges npx playwright test tests/settings/user-management.spec.ts \ --grep "should show user status badges" --project=chromium # Test role badges npx playwright test tests/settings/user-management.spec.ts \ --grep "should display role badges" --project=chromium # Test invite modal opens npx playwright test tests/settings/user-management.spec.ts \ --grep "should open invite user modal" --project=chromium # Test copy invite link (toast test IDs already exist) npx playwright test tests/settings/user-management.spec.ts \ --grep "should copy invite link" --project=chromium # Test ARIA labels npx playwright test tests/settings/user-management.spec.ts \ --grep "should have proper ARIA labels" --project=chromium ``` **Expected outcome**: 4-5 tests pass immediately. --- ### Phase 6.2: Email Validation UI (2 hours) **Goal**: Add client-side email format validation with visible error. **File to modify**: `frontend/src/pages/UsersPage.tsx` (InviteModal) **Implementation**: ```tsx // Add state in InviteModal component const [emailError, setEmailError] = useState(null) // Email validation function const validateEmail = (email: string): boolean => { if (!email) { setEmailError(null) return false } const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ if (!emailRegex.test(email)) { setEmailError(t('users.invalidEmail')) return false } setEmailError(null) return true } // Update email input (replace existing Input)
{ setEmail(e.target.value) validateEmail(e.target.value) }} placeholder="user@example.com" aria-invalid={!!emailError} aria-describedby={emailError ? 'email-error' : undefined} /> {emailError && ( )}
// Update button disabled logic disabled={!email || !!emailError} ``` **Translation key to add** (to appropriate i18n file): ```json { "users.invalidEmail": "Please enter a valid email address" } ``` **Validation**: ```bash npx playwright test tests/settings/user-management.spec.ts \ --grep "should validate email format" --project=chromium ``` **Expected outcome**: Test 5 passes. --- ### Phase 6.3: Resend Invite Action (6-10 hours) **Goal**: Add resend invite button for pending users. #### Backend Verification **REQUIRED**: Check if backend endpoint exists before proceeding: ```bash grep -r "resend" backend/internal/api/handlers/ grep -r "ResendInvite" backend/internal/api/ ``` **Result of verification**: Backend endpoint **does not exist**. Both grep commands return no results. **Contingency**: If backend is missing (confirmed), effort increases to **8-10 hours** to implement: - Endpoint: `POST /api/v1/users/{id}/resend-invite` - Handler: Regenerate token, send email, return new token info - Tests: Unit tests for the new handler #### Frontend Implementation **File**: `frontend/src/api/users.ts` Add API function: ```typescript /** * Resends an invitation email to a pending user. * @param id - The user ID to resend invite to * @returns Promise resolving to InviteUserResponse with new token */ export const resendInvite = async (id: number): Promise => { const response = await client.post(`/users/${id}/resend-invite`) return response.data } ``` **File**: `frontend/src/pages/UsersPage.tsx` Add mutation: ```tsx const resendInviteMutation = useMutation({ mutationFn: resendInvite, onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['users'] }) if (data.email_sent) { toast.success(t('users.inviteResent')) } else { toast.success(t('users.inviteCreatedNoEmail')) } }, onError: (error: unknown) => { const err = error as { response?: { data?: { error?: string } } } toast.error(err.response?.data?.error || t('users.resendFailed')) }, }) ``` Add button in user row actions: ```tsx {user.invite_status === 'pending' && ( )} ``` **Translation keys**: ```json { "users.resendInvite": "Resend Invite", "users.inviteResent": "Invitation resent successfully", "users.inviteCreatedNoEmail": "New invite created. Email could not be sent.", "users.resendFailed": "Failed to resend invitation" } ``` **Validation**: ```bash npx playwright test tests/settings/user-management.spec.ts \ --grep "should resend invite" --project=chromium ``` **Expected outcome**: Test 15 passes. --- ### Phase 6.4: Modal Keyboard Navigation (2 hours) **Goal**: Ensure Escape key closes modals. **File to modify**: `frontend/src/pages/UsersPage.tsx` **Implementation** (add to InviteModal and PermissionsModal): ```tsx // Add useEffect for keyboard handler useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { handleClose() } } if (isOpen) { document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) } }, [isOpen, handleClose]) ``` **Note**: Test 16 (keyboard navigation) is marked as **known flaky** and may remain skipped. --- ### Phase 6.5: PermissionsModal useState Bug Fix (1 hour) **Goal**: Fix React anti-pattern in PermissionsModal. **File to modify**: `frontend/src/pages/UsersPage.tsx` (line 339) **Bug**: `useState` is being used like a `useEffect`, which is a React anti-pattern: ```tsx // WRONG - useState used like an effect (current code at line 339) useState(() => { if (user) { setPermissionMode(user.permission_mode || 'allow_all') setSelectedHosts(user.permitted_hosts || []) } }) ``` **Fix**: Replace with proper `useEffect` with dependency: ```tsx // CORRECT - useEffect with dependency useEffect(() => { if (user) { setPermissionMode(user.permission_mode || 'allow_all') setSelectedHosts(user.permitted_hosts || []) } }, [user]) ``` **Why this matters**: The useState initializer only runs once on mount. The current code appears to work incidentally but: 1. Will not update state when `user` prop changes 2. May cause stale data bugs 3. Violates React's data flow principles **Validation**: ```bash # Run TypeScript check cd frontend && npm run typecheck # Run related permission tests (after Phase 5 auth fix) npx playwright test tests/settings/user-management.spec.ts \ --grep "permissions" --project=chromium ``` --- ## 4. Implementation Order ``` Week 1 (10-14 hours) ├── Phase 6.1: Verify Existing (3h) → Tests 1, 2, 4, 6, 19 ├── Phase 6.2: Email Validation (2h) → Test 5 ├── Phase 6.3: Resend Invite (6-10h) → Test 15 │ └── Includes backend endpoint implementation └── Phase 6.5: PermissionsModal Bug Fix (1h) → Stability Week 2 (2-3 hours) └── Phase 6.4: Modal Keyboard Nav (2h) → Partial for Test 16 Validation & Cleanup (3 hours) └── Run full suite, update skip comments ``` --- ## 5. Files to Modify ### Priority 1: Required for Test Enablement | File | Changes | |------|---------| | `frontend/src/pages/UsersPage.tsx` | Email validation, resend invite button, keyboard nav, PermissionsModal useState→useEffect fix | | `frontend/src/api/users.ts` | Add `resendInvite` function | | `tests/settings/user-management.spec.ts` | Unskip verified tests | ### Priority 2: Backend (REQUIRED - endpoint missing) | File | Changes | |------|---------| | `backend/internal/api/handlers/user_handler.go` | Add resend-invite endpoint | | `backend/internal/api/routes.go` | Register new route | | `backend/internal/api/handlers/user_handler_test.go` | Add tests for resend endpoint | ### Priority 3: Translations | File | Keys to Add | |------|-------------| | `frontend/src/i18n/locales/en.json` | `invalidEmail`, `resendInvite`, `inviteResent`, etc. | ### NOT Required (Already Implemented) | File | Status | |------|--------| | `frontend/src/components/Toast.tsx` | ✅ Already has `data-testid={toast-${toast.type}}` | --- ## 6. Validation Strategy ### After Each Phase ```bash # Run specific tests npx playwright test tests/settings/user-management.spec.ts \ --grep "" --project=chromium ``` ### Final Validation ```bash # Run all user management tests npx playwright test tests/settings/user-management.spec.ts --project=chromium # Expected: # - ~12-14 tests passing (up from ~5) # - ~6-8 tests still skipped (auth blocked or by design) ``` ### Test Coverage ```bash cd frontend && npm run test:coverage # Verify UsersPage.tsx >= 85% ``` --- ## 7. Expected Outcomes ### Tests to Unskip After Phase 6 | Test | Expected Outcome | |------|------------------| | should show user status badges | ✅ Pass | | should display role badges | ✅ Pass | | should open invite user modal | ✅ Pass | | should validate email format | ✅ Pass | | should copy invite link | ✅ Pass | | should resend invite for pending user | ✅ Pass | | should have proper ARIA labels | ✅ Pass | **Total**: 7 tests enabled ### Tests Remaining Skipped | Test | Reason | |------|--------| | Tests 7-12, 14 (7 tests) | 🔒 TestDataManager auth (Phase 5) | | Test 3: pending invite status | ⚠️ Complex timing | | Test 13: change user role | ❌ Feature not implemented | | Test 16: keyboard navigation | ⚠️ Known flaky | | Tests 17-18: admin access | ℹ️ Routing design (intentional) | **Total remaining skipped**: ~12 tests (down from 22) --- ## 8. Risk Assessment | Risk | Likelihood | Impact | Mitigation | |------|------------|--------|------------| | Backend resend endpoint missing | **CONFIRMED** | High | Backend implementation included in Phase 6.3 (6-10h) | | Tests pass locally, fail in CI | Medium | Medium | Run in both environments | | Translation keys missing | Low | Low | Add to all locale files | | PermissionsModal bug causes regressions | Low | Medium | Fix early in Phase 6.5 with testing | --- ## 9. Success Metrics | Metric | Before Phase 6 | After Phase 6 | Target | |--------|----------------|---------------|--------| | User management tests passing | ~5 | ~12-14 | 15+ | | User management tests skipped | 19 | 10-12 | <10 | | Frontend coverage (UsersPage) | TBD | ≥85% | 85% | --- ## 10. Timeline | Phase | Effort | Cumulative | |-------|--------|------------| | 6.1: Verify Existing (5 tests) | 3h | 3h | | 6.2: Email Validation | 2h | 5h | | 6.3: Resend Invite (backend included) | 6-10h | 11-15h | | 6.4: Modal Keyboard Nav | 2h | 13-17h | | 6.5: PermissionsModal Bug Fix | 1h | 14-18h | | Validation & Cleanup | 3h | 17-21h | | Buffer | 3h | **16-22h** | **Total**: 16-22 hours (range depends on backend complexity) --- ## Change Log | Date | Author | Change | |------|--------|--------| | 2026-01-24 | Planning Agent | Initial plan created with detailed test analysis | | 2026-01-24 | Planning Agent | **REVISION**: Applied Supervisor corrections: | | | | - Toast test IDs already exist (Phase 6.2 removed) | | | | - Updated test line numbers to actual values (70, 110, 217, 283, 442, 956, 1014, 1157) | | | | - Added Test 4 and Test 6 to Phase 6.1 verification batch (5 tests total) | | | | - Added Phase 6.5: PermissionsModal useState bug fix | | | | - Backend resend endpoint confirmed missing (grep verification) | | | | - Corrected test count: 19 skipped tests (not 22) | | | | - Updated effort estimates: 16-22h (was 17h) |