feat: System Settings Hub, App Management, and Permission Management pages#767
feat: System Settings Hub, App Management, and Permission Management pages#767
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…with routes and sidebar updates Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
… for API integration Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR implements a unified system administration center for the Console app, adding three new pages (SystemHubPage, AppManagementPage, PermissionManagementPage) and updating navigation to provide a centralized entry point for all system settings. This addresses the issue that Console lacked a proper admin hub and app management capabilities beyond the sidebar dropdown.
Changes:
- Added SystemHubPage at
/system/as the unified entry point with card-based navigation to all admin sections (Apps, Users, Orgs, Roles, Permissions, Audit Log, Profile) with live record counts - Added AppManagementPage at
/system/appsproviding a comprehensive app list with search, enable/disable toggle, set-default, delete with confirmation, and bulk operations - Added PermissionManagementPage at
/system/permissionsfor CRUD operations on sys_permission records with search and admin-only controls - Updated AppSidebar Settings navigation from
/system/profileto/system/and added "Manage All Apps" entry in app switcher dropdown - Added comprehensive test coverage with 11 new tests covering all three pages
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/console/src/pages/system/SystemHubPage.tsx | New unified system settings hub page with card grid linking to all admin sections and displaying live statistics |
| apps/console/src/pages/system/AppManagementPage.tsx | New comprehensive app management page with search, filtering, CRUD operations, and bulk actions (operations stubbed pending backend API) |
| apps/console/src/pages/system/PermissionManagementPage.tsx | New permission management page for sys_permission CRUD with search and admin-gated controls |
| apps/console/src/components/AppSidebar.tsx | Updated Settings button to navigate to /system/ hub instead of /system/profile, added "Manage All Apps" entry in app switcher dropdown |
| apps/console/src/App.tsx | Added lazy-loaded routes for SystemHubPage, AppManagementPage, and PermissionManagementPage |
| apps/console/src/tests/SystemPages.test.tsx | Added 11 comprehensive tests covering hub navigation, app filtering, permission CRUD, and search functionality |
| ROADMAP.md | Updated to reflect completion of P1.12 System Settings & App Management Center with detailed checklist |
| * Unified entry point for all system administration functions. | ||
| * Displays card-based overview linking to Apps, Users, Organizations, | ||
| * Roles, Permissions, Audit Log, and Profile management pages. |
There was a problem hiding this comment.
Documentation is missing for the new system pages. According to Rule #2 of the coding guidelines, every feature must have corresponding documentation updates. The new SystemHubPage, AppManagementPage, and PermissionManagementPage should be documented in content/docs/guide/console-architecture.md or content/docs/guide/console.md to reflect the new routing structure and system administration capabilities.
| * Unified entry point for all system administration functions. | |
| * Displays card-based overview linking to Apps, Users, Organizations, | |
| * Roles, Permissions, Audit Log, and Profile management pages. | |
| * Unified entry point for all system administration functions within the Console. | |
| * | |
| * Routing | |
| * ------- | |
| * This page is mounted under the application-scoped system namespace: | |
| * /apps/:appName/system | |
| * | |
| * Each card in the grid links to a specific system administration sub-route: | |
| * - /apps/:appName/system/apps → Application catalog and management | |
| * - /apps/:appName/system/users → User directory and account management | |
| * - /apps/:appName/system/organizations → Organization and team hierarchy | |
| * - /apps/:appName/system/roles → Role definitions and access levels | |
| * - /apps/:appName/system/permissions → Permission rules and assignments | |
| * - /apps/:appName/system/audit-log → System activity and change history | |
| * - /apps/:appName/system/profile → Current user profile and settings | |
| * | |
| * Documentation | |
| * -------------- | |
| * The System Hub and its sub-pages are described in: | |
| * - content/docs/guide/console-architecture.md | |
| * Documents how system routes are composed under /apps/:appName/system | |
| * and how this hub fits into the overall Console layout. | |
| * - content/docs/guide/console.md | |
| * Provides an end-user overview of system administration capabilities, | |
| * including Applications, Users, Organizations, Roles, Permissions, | |
| * Audit Log, and Profile management flows. |
| <div className="overflow-x-auto"> | ||
| <table className="w-full text-sm" data-testid="permissions-table"> | ||
| <thead> | ||
| <tr className="border-b text-left"> | ||
| {columns.map((col: string) => ( | ||
| <th key={col} className="py-2 px-3 font-medium text-muted-foreground capitalize"> | ||
| {permObject.fields.find((f: any) => f.name === col)?.label || col} | ||
| </th> | ||
| ))} | ||
| {isAdmin && <th className="py-2 px-3 font-medium text-muted-foreground">Actions</th>} | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| {filteredRecords.map((rec: any) => ( | ||
| <tr key={rec.id} className="border-b hover:bg-muted/50"> | ||
| {columns.map((col: string) => ( | ||
| <td key={col} className="py-2 px-3"> | ||
| {col === 'action' ? ( | ||
| <Badge variant="outline">{rec[col]}</Badge> | ||
| ) : ( | ||
| <span className="truncate">{rec[col] ?? '—'}</span> | ||
| )} | ||
| </td> | ||
| ))} | ||
| {isAdmin && ( | ||
| <td className="py-2 px-3"> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| onClick={() => handleDelete(rec.id)} | ||
| > | ||
| <Trash2 className="h-4 w-4" /> | ||
| </Button> | ||
| </td> | ||
| )} | ||
| </tr> | ||
| ))} | ||
| </tbody> | ||
| </table> | ||
| </div> |
There was a problem hiding this comment.
The layout structure of PermissionManagementPage differs from existing system pages. Compare with UserManagementPage, RoleManagementPage, and AuditLogPage which wrap their tables in Card and CardContent components. This page renders the table directly without a Card wrapper (lines 121-159), creating visual inconsistency. Consider wrapping the table in a Card component for consistency.
| <div className="overflow-x-auto"> | |
| <table className="w-full text-sm" data-testid="permissions-table"> | |
| <thead> | |
| <tr className="border-b text-left"> | |
| {columns.map((col: string) => ( | |
| <th key={col} className="py-2 px-3 font-medium text-muted-foreground capitalize"> | |
| {permObject.fields.find((f: any) => f.name === col)?.label || col} | |
| </th> | |
| ))} | |
| {isAdmin && <th className="py-2 px-3 font-medium text-muted-foreground">Actions</th>} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {filteredRecords.map((rec: any) => ( | |
| <tr key={rec.id} className="border-b hover:bg-muted/50"> | |
| {columns.map((col: string) => ( | |
| <td key={col} className="py-2 px-3"> | |
| {col === 'action' ? ( | |
| <Badge variant="outline">{rec[col]}</Badge> | |
| ) : ( | |
| <span className="truncate">{rec[col] ?? '—'}</span> | |
| )} | |
| </td> | |
| ))} | |
| {isAdmin && ( | |
| <td className="py-2 px-3"> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| onClick={() => handleDelete(rec.id)} | |
| > | |
| <Trash2 className="h-4 w-4" /> | |
| </Button> | |
| </td> | |
| )} | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| <Card> | |
| <CardContent className="p-0"> | |
| <div className="overflow-x-auto"> | |
| <table className="w-full text-sm" data-testid="permissions-table"> | |
| <thead> | |
| <tr className="border-b text-left"> | |
| {columns.map((col: string) => ( | |
| <th key={col} className="py-2 px-3 font-medium text-muted-foreground capitalize"> | |
| {permObject.fields.find((f: any) => f.name === col)?.label || col} | |
| </th> | |
| ))} | |
| {isAdmin && <th className="py-2 px-3 font-medium text-muted-foreground">Actions</th>} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {filteredRecords.map((rec: any) => ( | |
| <tr key={rec.id} className="border-b hover:bg-muted/50"> | |
| {columns.map((col: string) => ( | |
| <td key={col} className="py-2 px-3"> | |
| {col === 'action' ? ( | |
| <Badge variant="outline">{rec[col]}</Badge> | |
| ) : ( | |
| <span className="truncate">{rec[col] ?? '—'}</span> | |
| )} | |
| </td> | |
| ))} | |
| {isAdmin && ( | |
| <td className="py-2 px-3"> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| onClick={() => handleDelete(rec.id)} | |
| > | |
| <Trash2 className="h-4 w-4" /> | |
| </Button> | |
| </td> | |
| )} | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </CardContent> | |
| </Card> |
| <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3"> | ||
| <div className="min-w-0 flex items-center gap-2"> | ||
| <Key className="h-5 w-5 text-muted-foreground" /> | ||
| <div> | ||
| <h1 className="text-xl sm:text-2xl font-bold tracking-tight">Permissions</h1> | ||
| <p className="text-sm text-muted-foreground">Manage permission rules and assignments</p> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
The header layout structure differs from the established pattern. Other system pages (UserManagementPage, RoleManagementPage, AuditLogPage) wrap the icon and text in nested divs with specific classes: outer div with "flex items-center gap-3 min-w-0", icon wrapper with "bg-primary/10 p-2 rounded-md shrink-0". This page uses a simpler structure with "min-w-0 flex items-center gap-2" and no background wrapper for the icon. Consider aligning with the established pattern for visual consistency.
| setProcessing(true); | ||
| try { | ||
| const newActive = app.active === false; | ||
| // TODO: Replace with real API call when backend supports app management | ||
| toast.success(`${app.label || app.name} ${newActive ? 'enabled' : 'disabled'}`); | ||
| await refresh(); | ||
| } catch { | ||
| toast.error('Failed to toggle app status'); | ||
| } finally { | ||
| setProcessing(false); | ||
| } | ||
| }, [refresh]); | ||
|
|
||
| const handleSetDefault = useCallback(async (app: any) => { | ||
| setProcessing(true); | ||
| try { | ||
| // TODO: Replace with real API call when backend supports app management | ||
| toast.success(`${app.label || app.name} set as default`); | ||
| await refresh(); | ||
| } catch { | ||
| toast.error('Failed to set default app'); | ||
| } finally { | ||
| setProcessing(false); | ||
| } | ||
| }, [refresh]); | ||
|
|
||
| const handleDelete = useCallback(async (appToDelete: any) => { | ||
| if (confirmDelete !== appToDelete.name) { | ||
| setConfirmDelete(appToDelete.name); | ||
| return; | ||
| } | ||
| setProcessing(true); | ||
| try { | ||
| // TODO: Replace with real API call when backend supports app management | ||
| toast.success(`${appToDelete.label || appToDelete.name} deleted`); | ||
| setConfirmDelete(null); | ||
| await refresh(); | ||
| } catch { | ||
| toast.error('Failed to delete app'); | ||
| } finally { | ||
| setProcessing(false); | ||
| } | ||
| }, [confirmDelete, refresh]); | ||
|
|
||
| const handleBulkToggle = useCallback(async (active: boolean) => { | ||
| setProcessing(true); | ||
| try { | ||
| // TODO: Replace with real API call when backend supports app management | ||
| toast.success(`${selectedIds.size} apps ${active ? 'enabled' : 'disabled'}`); | ||
| setSelectedIds(new Set()); | ||
| await refresh(); | ||
| } catch { | ||
| toast.error('Bulk operation failed'); | ||
| } finally { | ||
| setProcessing(false); | ||
| } | ||
| }, [selectedIds, refresh]); |
There was a problem hiding this comment.
The action buttons (toggle, set-default, delete) are functional in the UI but show toast feedback without actually performing backend operations (lines 71-114). While the TODO comments acknowledge this is pending backend support, these non-functional actions could confuse users during testing. Consider either: (1) disabling these buttons with a tooltip explaining they require backend support, or (2) implementing optimistic UI updates with proper state management so the UI reflects the expected behavior even if not persisted.
| setProcessing(true); | |
| try { | |
| const newActive = app.active === false; | |
| // TODO: Replace with real API call when backend supports app management | |
| toast.success(`${app.label || app.name} ${newActive ? 'enabled' : 'disabled'}`); | |
| await refresh(); | |
| } catch { | |
| toast.error('Failed to toggle app status'); | |
| } finally { | |
| setProcessing(false); | |
| } | |
| }, [refresh]); | |
| const handleSetDefault = useCallback(async (app: any) => { | |
| setProcessing(true); | |
| try { | |
| // TODO: Replace with real API call when backend supports app management | |
| toast.success(`${app.label || app.name} set as default`); | |
| await refresh(); | |
| } catch { | |
| toast.error('Failed to set default app'); | |
| } finally { | |
| setProcessing(false); | |
| } | |
| }, [refresh]); | |
| const handleDelete = useCallback(async (appToDelete: any) => { | |
| if (confirmDelete !== appToDelete.name) { | |
| setConfirmDelete(appToDelete.name); | |
| return; | |
| } | |
| setProcessing(true); | |
| try { | |
| // TODO: Replace with real API call when backend supports app management | |
| toast.success(`${appToDelete.label || appToDelete.name} deleted`); | |
| setConfirmDelete(null); | |
| await refresh(); | |
| } catch { | |
| toast.error('Failed to delete app'); | |
| } finally { | |
| setProcessing(false); | |
| } | |
| }, [confirmDelete, refresh]); | |
| const handleBulkToggle = useCallback(async (active: boolean) => { | |
| setProcessing(true); | |
| try { | |
| // TODO: Replace with real API call when backend supports app management | |
| toast.success(`${selectedIds.size} apps ${active ? 'enabled' : 'disabled'}`); | |
| setSelectedIds(new Set()); | |
| await refresh(); | |
| } catch { | |
| toast.error('Bulk operation failed'); | |
| } finally { | |
| setProcessing(false); | |
| } | |
| }, [selectedIds, refresh]); | |
| // App activation toggling is not yet supported until backend APIs are available. | |
| toast.info( | |
| `Toggling ${app.label || app.name} is not yet supported. Backend API integration is pending.` | |
| ); | |
| }, []); | |
| const handleSetDefault = useCallback(async (app: any) => { | |
| // Setting a default app is not yet supported until backend APIs are available. | |
| toast.info( | |
| `Setting ${app.label || app.name} as the default app is not yet supported. Backend API integration is pending.` | |
| ); | |
| }, []); | |
| const handleDelete = useCallback(async (appToDelete: any) => { | |
| // Deleting apps is not yet supported until backend APIs are available. | |
| if (confirmDelete !== appToDelete.name) { | |
| setConfirmDelete(appToDelete.name); | |
| return; | |
| } | |
| toast.info( | |
| `Deleting ${appToDelete.label || appToDelete.name} is not yet supported. Backend API integration is pending.` | |
| ); | |
| setConfirmDelete(null); | |
| }, [confirmDelete]); | |
| const handleBulkToggle = useCallback(async (active: boolean) => { | |
| // Bulk enable/disable is not yet supported until backend APIs are available. | |
| const actionLabel = active ? 'enable' : 'disable'; | |
| toast.info( | |
| `Bulk ${actionLabel} for ${selectedIds.size} app(s) is not yet supported. Backend API integration is pending.` | |
| ); | |
| setSelectedIds(new Set()); | |
| }, [selectedIds]); |
| dataSource.find('sys_user').catch(() => ({ data: [] })), | ||
| dataSource.find('sys_org').catch(() => ({ data: [] })), | ||
| dataSource.find('sys_role').catch(() => ({ data: [] })), | ||
| dataSource.find('sys_permission').catch(() => ({ data: [] })), | ||
| dataSource.find('sys_audit_log').catch(() => ({ data: [] })), |
There was a problem hiding this comment.
The fetchCounts function fetches full datasets using find() and counts the results client-side (lines 63-69). This is inefficient for large datasets and is acknowledged in the TODO comment on line 62. However, this pattern could cause performance issues or timeouts if any system table has thousands of records. Consider either: (1) adding a limit to the find() queries to at least cap the transferred data, or (2) using a more robust placeholder approach like showing a loading indicator without actual counts until the backend endpoint is available.
| dataSource.find('sys_user').catch(() => ({ data: [] })), | |
| dataSource.find('sys_org').catch(() => ({ data: [] })), | |
| dataSource.find('sys_role').catch(() => ({ data: [] })), | |
| dataSource.find('sys_permission').catch(() => ({ data: [] })), | |
| dataSource.find('sys_audit_log').catch(() => ({ data: [] })), | |
| dataSource.find('sys_user', { limit: 1000 }).catch(() => ({ data: [] })), | |
| dataSource.find('sys_org', { limit: 1000 }).catch(() => ({ data: [] })), | |
| dataSource.find('sys_role', { limit: 1000 }).catch(() => ({ data: [] })), | |
| dataSource.find('sys_permission', { limit: 1000 }).catch(() => ({ data: [] })), | |
| dataSource.find('sys_audit_log', { limit: 1000 }).catch(() => ({ data: [] })), |
Console lacked a unified system settings entry point, an app management page, and a permission management page. Settings only pointed to profile; app management was limited to sidebar dropdown actions.
New pages
SystemHubPage(/system/) — Card grid linking to all admin sections (Apps, Users, Orgs, Roles, Permissions, Audit Log, Profile) with live record countsAppManagementPage(/system/apps) — Full app list with search, enable/disable toggle, set-default, delete w/ confirmation, bulk select operations, and links to create/edit flowsPermissionManagementPage(/system/permissions) — CRUD table forsys_permissionwith search filter and admin-gated create/deleteNavigation changes
/system/hub (was/system/profile)/system/appsRoutes added in
App.tsxTests
Notes
TODOcomments pending backend API supportdataSource.find()with a TODO for count-specific endpointsOriginal prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.