Barazo default frontend barazo.forum
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(admin): replace error states with P3 placeholders on users and plugins pages (#161)

The admin users and plugins pages showed "API may be unreachable" errors
because their backend endpoints are planned for P3.2 and not yet
implemented. Replace with clear "Coming in P3" placeholder states.

authored by

Guido X Jansen and committed by
GitHub
5e6031a7 8ad9ad1b

+33 -312
+8 -100
src/app/admin/plugins/page.test.tsx
··· 1 1 /** 2 - * Tests for admin plugins page. 2 + * Tests for admin plugins page (P3 placeholder). 3 3 * @see specs/prd-web.md Section M13 4 4 */ 5 5 6 - import { describe, it, expect } from 'vitest' 7 - import { render, screen, waitFor, within } from '@testing-library/react' 8 - import userEvent from '@testing-library/user-event' 6 + import { describe, it, expect, vi } from 'vitest' 7 + import { render, screen } from '@testing-library/react' 9 8 import { axe } from 'vitest-axe' 10 9 import AdminPluginsPage from './page' 11 10 12 - // Mock next/navigation 13 11 vi.mock('next/navigation', () => ({ 14 12 usePathname: () => '/admin/plugins', 15 13 })) ··· 33 31 return { useAuth: () => mockAuth } 34 32 }) 35 33 36 - vi.mock('@/hooks/use-toast', () => ({ 37 - useToast: () => ({ toast: vi.fn(), dismiss: vi.fn() }), 38 - })) 39 - 40 34 describe('AdminPluginsPage', () => { 41 - it('renders page heading', async () => { 42 - render(<AdminPluginsPage />) 43 - await waitFor(() => { 44 - expect(screen.getByRole('heading', { name: /plugins/i, level: 1 })).toBeInTheDocument() 45 - }) 46 - }) 47 - 48 - it('renders plugin list from API', async () => { 49 - render(<AdminPluginsPage />) 50 - await waitFor(() => { 51 - expect(screen.getByText('Full-Text Search')).toBeInTheDocument() 52 - }) 53 - expect(screen.getByText('Markdown Editor')).toBeInTheDocument() 54 - expect(screen.getByText('Code Highlighting')).toBeInTheDocument() 55 - expect(screen.getByText('Community Analytics')).toBeInTheDocument() 56 - expect(screen.getByText('Webhook Notifications')).toBeInTheDocument() 57 - }) 58 - 59 - it('shows source badges (Core, Official, Community, Experimental)', async () => { 60 - render(<AdminPluginsPage />) 61 - await waitFor(() => { 62 - expect(screen.getAllByText('Core').length).toBeGreaterThanOrEqual(2) 63 - }) 64 - expect(screen.getByText('Official')).toBeInTheDocument() 65 - expect(screen.getByText('Community')).toBeInTheDocument() 66 - expect(screen.getByText('Experimental')).toBeInTheDocument() 67 - }) 68 - 69 - it('shows version numbers', async () => { 70 - render(<AdminPluginsPage />) 71 - await waitFor(() => { 72 - expect(screen.getByText('v1.0.0')).toBeInTheDocument() 73 - }) 74 - expect(screen.getByText('v1.2.0')).toBeInTheDocument() 75 - expect(screen.getByText('v0.2.0-beta')).toBeInTheDocument() 76 - }) 77 - 78 - it('shows enabled/disabled state for plugins', async () => { 79 - render(<AdminPluginsPage />) 80 - await waitFor(() => { 81 - expect(screen.getByText('Full-Text Search')).toBeInTheDocument() 82 - }) 83 - // Find toggle buttons - enabled plugins should have checked toggles 84 - const toggles = screen.getAllByRole('switch') 85 - expect(toggles.length).toBeGreaterThanOrEqual(5) 86 - }) 87 - 88 - it('shows dependency warning when attempting to disable a plugin with dependents', async () => { 89 - const user = userEvent.setup() 90 - render(<AdminPluginsPage />) 91 - await waitFor(() => { 92 - expect(screen.getByText('Markdown Editor')).toBeInTheDocument() 93 - }) 94 - 95 - // Markdown Editor has dependents: ['barazo-plugin-code-highlight'] 96 - // Find the Markdown Editor card and its toggle 97 - const markdownCard = screen.getByText('Markdown Editor').closest('article') 98 - expect(markdownCard).toBeTruthy() 99 - const toggle = within(markdownCard!).getByRole('switch') 100 - await user.click(toggle) 101 - 102 - // Should show dependency warning dialog 103 - await waitFor(() => { 104 - expect(screen.getByText('Dependency warning')).toBeInTheDocument() 105 - }) 106 - expect(screen.getByText('Disable anyway')).toBeInTheDocument() 107 - }) 108 - 109 - it('opens settings modal when settings button is clicked', async () => { 110 - const user = userEvent.setup() 35 + it('renders page heading', () => { 111 36 render(<AdminPluginsPage />) 112 - await waitFor(() => { 113 - expect(screen.getByText('Full-Text Search')).toBeInTheDocument() 114 - }) 115 - 116 - // Click settings button on Full-Text Search plugin 117 - const searchCard = screen.getByText('Full-Text Search').closest('article') 118 - expect(searchCard).toBeTruthy() 119 - const settingsBtn = within(searchCard!).getByRole('button', { name: /settings/i }) 120 - await user.click(settingsBtn) 121 - 122 - // Should show settings modal with schema fields 123 - await waitFor(() => { 124 - expect(screen.getByText('Enable semantic search')).toBeInTheDocument() 125 - }) 37 + expect(screen.getByRole('heading', { name: /plugins/i, level: 1 })).toBeInTheDocument() 126 38 }) 127 39 128 - it('shows plugin descriptions', async () => { 40 + it('shows coming in P3 message', () => { 129 41 render(<AdminPluginsPage />) 130 - await waitFor(() => { 131 - expect(screen.getByText(/full-text search for topics and replies/i)).toBeInTheDocument() 132 - }) 42 + expect(screen.getByRole('heading', { name: /coming in p3/i })).toBeInTheDocument() 43 + expect(screen.getByText(/planned for the p3\.2 milestone/i)).toBeInTheDocument() 133 44 }) 134 45 135 46 it('passes axe accessibility check', async () => { 136 47 const { container } = render(<AdminPluginsPage />) 137 - await waitFor(() => { 138 - expect(screen.getByText('Full-Text Search')).toBeInTheDocument() 139 - }) 140 48 const results = await axe(container) 141 49 expect(results).toHaveNoViolations() 142 50 })
+10 -73
src/app/admin/plugins/page.tsx
··· 2 2 * Admin plugin management page. 3 3 * URL: /admin/plugins 4 4 * Lists installed plugins with enable/disable, settings, and uninstall controls. 5 - * Phase 1: manage installed plugins. Phase 2: marketplace search/install. 5 + * Backend endpoints not yet implemented (planned for P3.2). 6 6 * @see specs/prd-web.md Section M13 7 7 */ 8 8 9 - 'use client' 10 - 9 + import { PuzzlePiece } from '@phosphor-icons/react/dist/ssr' 11 10 import { AdminLayout } from '@/components/admin/admin-layout' 12 - import { ErrorAlert } from '@/components/error-alert' 13 - import { PluginCard } from '@/components/admin/plugins/plugin-card' 14 - import { PluginSettingsModal } from '@/components/admin/plugins/plugin-settings-modal' 15 - import { DependencyWarningDialog } from '@/components/admin/plugins/dependency-warning-dialog' 16 - import { usePluginManagement } from '@/hooks/admin/use-plugin-management' 17 11 18 12 export default function AdminPluginsPage() { 19 - const { 20 - plugins, 21 - loading, 22 - settingsPlugin, 23 - setSettingsPlugin, 24 - dependencyWarning, 25 - setDependencyWarning, 26 - loadError, 27 - actionError, 28 - setActionError, 29 - fetchPlugins, 30 - handleToggle, 31 - confirmDisable, 32 - handleSaveSettings, 33 - handleUninstall, 34 - } = usePluginManagement() 35 - 36 13 return ( 37 14 <AdminLayout> 38 15 <div className="space-y-6"> 39 - <div className="flex items-center justify-between"> 40 - <h1 className="text-2xl font-bold text-foreground">Plugins</h1> 41 - <p className="text-sm text-muted-foreground"> 42 - {plugins.filter((p) => p.enabled).length} of {plugins.length} enabled 16 + <h1 className="text-2xl font-bold text-foreground">Plugins</h1> 17 + 18 + <div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center"> 19 + <PuzzlePiece className="mb-4 h-12 w-12 text-muted-foreground/50" /> 20 + <h2 className="text-lg font-semibold text-foreground">Coming in P3</h2> 21 + <p className="mt-1 max-w-sm text-sm text-muted-foreground"> 22 + Plugin management (install, enable/disable, configure) is planned for the P3.2 23 + milestone. The plugin API endpoints are not yet available. 43 24 </p> 44 25 </div> 45 - 46 - {loadError && ( 47 - <ErrorAlert message={loadError} variant="page" onRetry={() => void fetchPlugins()} /> 48 - )} 49 - 50 - {actionError && <ErrorAlert message={actionError} onDismiss={() => setActionError(null)} />} 51 - 52 - {loading && <p className="text-sm text-muted-foreground">Loading plugins...</p>} 53 - 54 - {!loading && plugins.length === 0 && ( 55 - <p className="py-8 text-center text-muted-foreground">No plugins installed.</p> 56 - )} 57 - 58 - {!loading && plugins.length > 0 && ( 59 - <div className="space-y-3"> 60 - {plugins.map((plugin) => ( 61 - <PluginCard 62 - key={plugin.id} 63 - plugin={plugin} 64 - allPlugins={plugins} 65 - onOpenSettings={setSettingsPlugin} 66 - onToggle={(p) => void handleToggle(p)} 67 - onUninstall={(p) => void handleUninstall(p)} 68 - /> 69 - ))} 70 - </div> 71 - )} 72 - 73 - {dependencyWarning && ( 74 - <DependencyWarningDialog 75 - pluginName={dependencyWarning.plugin.displayName} 76 - dependents={dependencyWarning.dependents} 77 - onConfirm={() => void confirmDisable()} 78 - onCancel={() => setDependencyWarning(null)} 79 - /> 80 - )} 81 - 82 - {settingsPlugin && ( 83 - <PluginSettingsModal 84 - plugin={settingsPlugin} 85 - onClose={() => setSettingsPlugin(null)} 86 - onSave={(settings) => void handleSaveSettings(settings)} 87 - /> 88 - )} 89 26 </div> 90 27 </AdminLayout> 91 28 )
+5 -46
src/app/admin/users/page.test.tsx
··· 1 1 /** 2 - * Tests for admin user management page. 2 + * Tests for admin user management page (P3 placeholder). 3 3 */ 4 4 5 5 import { describe, it, expect, vi } from 'vitest' 6 - import { render, screen, waitFor } from '@testing-library/react' 6 + import { render, screen } from '@testing-library/react' 7 7 import { axe } from 'vitest-axe' 8 8 import AdminUsersPage from './page' 9 9 ··· 50 50 return { useAuth: () => mockAuth } 51 51 }) 52 52 53 - vi.mock('@/hooks/use-toast', () => ({ 54 - useToast: () => ({ toast: vi.fn(), dismiss: vi.fn() }), 55 - })) 56 - 57 53 describe('AdminUsersPage', () => { 58 54 it('renders user management heading', () => { 59 55 render(<AdminUsersPage />) 60 56 expect(screen.getByRole('heading', { name: /user management/i })).toBeInTheDocument() 61 57 }) 62 58 63 - it('renders user list from API', async () => { 64 - render(<AdminUsersPage />) 65 - await waitFor(() => { 66 - expect(screen.getByText('Jay Admin')).toBeInTheDocument() 67 - }) 68 - expect(screen.getByText('Alex Moderator')).toBeInTheDocument() 69 - expect(screen.getByText('Sam Member')).toBeInTheDocument() 70 - }) 71 - 72 - it('shows user roles', async () => { 59 + it('shows coming in P3 message', () => { 73 60 render(<AdminUsersPage />) 74 - await waitFor(() => { 75 - expect(screen.getByText('admin')).toBeInTheDocument() 76 - }) 77 - expect(screen.getByText('moderator')).toBeInTheDocument() 78 - }) 79 - 80 - it('shows banned status', async () => { 81 - render(<AdminUsersPage />) 82 - await waitFor(() => { 83 - expect(screen.getByText('Morgan Banned')).toBeInTheDocument() 84 - }) 85 - // Morgan should show as banned 86 - expect(screen.getByText('Banned')).toBeInTheDocument() 87 - }) 88 - 89 - it('shows cross-community ban warning', async () => { 90 - render(<AdminUsersPage />) 91 - await waitFor(() => { 92 - expect(screen.getByText(/banned from 2 other communities/i)).toBeInTheDocument() 93 - }) 94 - }) 95 - 96 - it('shows ban/unban buttons', async () => { 97 - render(<AdminUsersPage />) 98 - await waitFor(() => { 99 - expect(screen.getAllByRole('button', { name: /ban/i }).length).toBeGreaterThan(0) 100 - }) 61 + expect(screen.getByRole('heading', { name: /coming in p3/i })).toBeInTheDocument() 62 + expect(screen.getByText(/planned for the p3\.2 milestone/i)).toBeInTheDocument() 101 63 }) 102 64 103 65 it('passes axe accessibility check', async () => { 104 66 const { container } = render(<AdminUsersPage />) 105 - await waitFor(() => { 106 - expect(screen.getByText('Jay Admin')).toBeInTheDocument() 107 - }) 108 67 const results = await axe(container) 109 68 expect(results).toHaveNoViolations() 110 69 })
+10 -93
src/app/admin/users/page.tsx
··· 2 2 * Admin user management page. 3 3 * URL: /admin/users 4 4 * User list with ban controls and cross-community ban warnings. 5 + * Backend endpoint not yet implemented (planned for P3.2). 5 6 * @see specs/prd-web.md Section M11 6 7 */ 7 8 8 - 'use client' 9 - 10 - import { useState, useEffect, useCallback } from 'react' 9 + import { Users } from '@phosphor-icons/react/dist/ssr' 11 10 import { AdminLayout } from '@/components/admin/admin-layout' 12 - import { ErrorAlert } from '@/components/error-alert' 13 - import { UserCard } from '@/components/admin/users/user-card' 14 - import { getAdminUsers, banUser, unbanUser } from '@/lib/api/client' 15 - import type { AdminUser } from '@/lib/api/types' 16 - import { useAuth } from '@/hooks/use-auth' 17 - import { useToast } from '@/hooks/use-toast' 18 11 19 12 export default function AdminUsersPage() { 20 - const { getAccessToken } = useAuth() 21 - const { toast } = useToast() 22 - const [users, setUsers] = useState<AdminUser[]>([]) 23 - const [loading, setLoading] = useState(true) 24 - const [loadError, setLoadError] = useState<string | null>(null) 25 - const [actionError, setActionError] = useState<string | null>(null) 26 - 27 - const fetchUsers = useCallback(async () => { 28 - setLoadError(null) 29 - try { 30 - const response = await getAdminUsers(getAccessToken() ?? '') 31 - setUsers(response.users) 32 - } catch { 33 - setLoadError('Failed to load users. The API may be unreachable.') 34 - } finally { 35 - setLoading(false) 36 - } 37 - }, [getAccessToken]) 38 - 39 - useEffect(() => { 40 - void fetchUsers() 41 - }, [fetchUsers]) 42 - 43 - const handleBan = async (did: string) => { 44 - setActionError(null) 45 - try { 46 - await banUser(did, 'Banned by admin', getAccessToken() ?? '') 47 - setUsers((prev) => 48 - prev.map((u) => 49 - u.did === did 50 - ? { 51 - ...u, 52 - isBanned: true, 53 - bannedAt: new Date().toISOString(), 54 - banReason: 'Banned by admin', 55 - } 56 - : u 57 - ) 58 - ) 59 - toast({ title: 'User banned' }) 60 - } catch { 61 - setActionError('Failed to ban user. Please try again.') 62 - } 63 - } 64 - 65 - const handleUnban = async (did: string) => { 66 - setActionError(null) 67 - try { 68 - await unbanUser(did, getAccessToken() ?? '') 69 - setUsers((prev) => 70 - prev.map((u) => 71 - u.did === did ? { ...u, isBanned: false, bannedAt: null, banReason: null } : u 72 - ) 73 - ) 74 - toast({ title: 'User unbanned' }) 75 - } catch { 76 - setActionError('Failed to unban user. Please try again.') 77 - } 78 - } 79 - 80 13 return ( 81 14 <AdminLayout> 82 15 <div className="space-y-6"> 83 16 <h1 className="text-2xl font-bold text-foreground">User management</h1> 84 17 85 - {loadError && ( 86 - <ErrorAlert message={loadError} variant="page" onRetry={() => void fetchUsers()} /> 87 - )} 88 - 89 - {actionError && <ErrorAlert message={actionError} onDismiss={() => setActionError(null)} />} 90 - 91 - {loading && <p className="text-sm text-muted-foreground">Loading users...</p>} 92 - 93 - {!loading && users.length === 0 && ( 94 - <p className="py-8 text-center text-muted-foreground">No users found.</p> 95 - )} 96 - 97 - {!loading && users.length > 0 && ( 98 - <div className="space-y-2"> 99 - {users.map((user) => ( 100 - <UserCard 101 - key={user.did} 102 - user={user} 103 - onBan={(did) => void handleBan(did)} 104 - onUnban={(did) => void handleUnban(did)} 105 - /> 106 - ))} 107 - </div> 108 - )} 18 + <div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center"> 19 + <Users className="mb-4 h-12 w-12 text-muted-foreground/50" /> 20 + <h2 className="text-lg font-semibold text-foreground">Coming in P3</h2> 21 + <p className="mt-1 max-w-sm text-sm text-muted-foreground"> 22 + User management (search, role assignment, ban controls) is planned for the P3.2 23 + milestone. The admin API endpoint is not yet available. 24 + </p> 25 + </div> 109 26 </div> 110 27 </AdminLayout> 111 28 )