Skip to content

Commit b5522ff

Browse files
Copilothotlong
andcommitted
feat: add SystemHubPage, AppManagementPage, PermissionManagementPage with routes and sidebar updates
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 2c3c9c6 commit b5522ff

6 files changed

Lines changed: 798 additions & 1 deletion

File tree

apps/console/src/App.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,12 @@ const RegisterPage = lazy(() => import('./pages/RegisterPage').then(m => ({ defa
4040
const ForgotPasswordPage = lazy(() => import('./pages/ForgotPasswordPage').then(m => ({ default: m.ForgotPasswordPage })));
4141

4242
// System Admin Pages (lazy — rarely accessed)
43+
const SystemHubPage = lazy(() => import('./pages/system/SystemHubPage').then(m => ({ default: m.SystemHubPage })));
44+
const AppManagementPage = lazy(() => import('./pages/system/AppManagementPage').then(m => ({ default: m.AppManagementPage })));
4345
const UserManagementPage = lazy(() => import('./pages/system/UserManagementPage').then(m => ({ default: m.UserManagementPage })));
4446
const OrgManagementPage = lazy(() => import('./pages/system/OrgManagementPage').then(m => ({ default: m.OrgManagementPage })));
4547
const RoleManagementPage = lazy(() => import('./pages/system/RoleManagementPage').then(m => ({ default: m.RoleManagementPage })));
48+
const PermissionManagementPage = lazy(() => import('./pages/system/PermissionManagementPage').then(m => ({ default: m.PermissionManagementPage })));
4649
const AuditLogPage = lazy(() => import('./pages/system/AuditLogPage').then(m => ({ default: m.AuditLogPage })));
4750
const ProfilePage = lazy(() => import('./pages/system/ProfilePage').then(m => ({ default: m.ProfilePage })));
4851

@@ -363,9 +366,12 @@ export function AppContent() {
363366
<Route path="edit-app/:editAppName" element={<EditAppPage />} />
364367

365368
{/* System Administration Routes */}
369+
<Route path="system" element={<SystemHubPage />} />
370+
<Route path="system/apps" element={<AppManagementPage />} />
366371
<Route path="system/users" element={<UserManagementPage />} />
367372
<Route path="system/organizations" element={<OrgManagementPage />} />
368373
<Route path="system/roles" element={<RoleManagementPage />} />
374+
<Route path="system/permissions" element={<PermissionManagementPage />} />
369375
<Route path="system/audit-log" element={<AuditLogPage />} />
370376
<Route path="system/profile" element={<ProfilePage />} />
371377
</Routes>

apps/console/src/__tests__/SystemPages.test.tsx

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,41 @@ vi.mock('sonner', () => ({
3333
toast: { success: vi.fn(), error: vi.fn() },
3434
}));
3535

36+
const mockNavigate = vi.fn();
37+
vi.mock('react-router-dom', async () => {
38+
const actual = await vi.importActual('react-router-dom');
39+
return {
40+
...actual,
41+
useNavigate: () => mockNavigate,
42+
useParams: () => ({ appName: 'test-app' }),
43+
};
44+
});
45+
46+
const mockRefresh = vi.fn().mockResolvedValue(undefined);
47+
vi.mock('../context/MetadataProvider', () => ({
48+
useMetadata: () => ({
49+
apps: [
50+
{ name: 'crm', label: 'CRM', description: 'Customer management', active: true, isDefault: true },
51+
{ name: 'hr', label: 'HR', description: 'Human resources', active: false, isDefault: false },
52+
],
53+
objects: [],
54+
dashboards: [],
55+
reports: [],
56+
pages: [],
57+
loading: false,
58+
error: null,
59+
refresh: mockRefresh,
60+
}),
61+
}));
62+
3663
// Import after mocks
3764
import { UserManagementPage } from '../pages/system/UserManagementPage';
3865
import { OrgManagementPage } from '../pages/system/OrgManagementPage';
3966
import { RoleManagementPage } from '../pages/system/RoleManagementPage';
4067
import { AuditLogPage } from '../pages/system/AuditLogPage';
68+
import { SystemHubPage } from '../pages/system/SystemHubPage';
69+
import { AppManagementPage } from '../pages/system/AppManagementPage';
70+
import { PermissionManagementPage } from '../pages/system/PermissionManagementPage';
4171

4272
function wrap(ui: React.ReactElement) {
4373
return render(<MemoryRouter>{ui}</MemoryRouter>);
@@ -143,3 +173,123 @@ describe('AuditLogPage', () => {
143173
});
144174
});
145175
});
176+
177+
describe('SystemHubPage', () => {
178+
it('should render System Settings heading and all hub cards', async () => {
179+
mockFind.mockResolvedValue({ data: [] });
180+
wrap(<SystemHubPage />);
181+
expect(screen.getByText('System Settings')).toBeInTheDocument();
182+
await waitFor(() => {
183+
expect(screen.getByTestId('hub-card-applications')).toBeInTheDocument();
184+
expect(screen.getByTestId('hub-card-users')).toBeInTheDocument();
185+
expect(screen.getByTestId('hub-card-organizations')).toBeInTheDocument();
186+
expect(screen.getByTestId('hub-card-roles')).toBeInTheDocument();
187+
expect(screen.getByTestId('hub-card-permissions')).toBeInTheDocument();
188+
expect(screen.getByTestId('hub-card-audit-log')).toBeInTheDocument();
189+
expect(screen.getByTestId('hub-card-profile')).toBeInTheDocument();
190+
});
191+
});
192+
193+
it('should fetch counts from dataSource on mount', async () => {
194+
mockFind.mockResolvedValue({ data: [{ id: '1' }] });
195+
wrap(<SystemHubPage />);
196+
await waitFor(() => {
197+
expect(mockFind).toHaveBeenCalledWith('sys_user');
198+
expect(mockFind).toHaveBeenCalledWith('sys_org');
199+
expect(mockFind).toHaveBeenCalledWith('sys_role');
200+
expect(mockFind).toHaveBeenCalledWith('sys_permission');
201+
expect(mockFind).toHaveBeenCalledWith('sys_audit_log');
202+
});
203+
});
204+
205+
it('should navigate to section when card is clicked', async () => {
206+
mockFind.mockResolvedValue({ data: [] });
207+
wrap(<SystemHubPage />);
208+
await waitFor(() => {
209+
expect(screen.getByTestId('hub-card-users')).toBeInTheDocument();
210+
});
211+
fireEvent.click(screen.getByTestId('hub-card-users'));
212+
expect(mockNavigate).toHaveBeenCalledWith('/apps/test-app/system/users');
213+
});
214+
});
215+
216+
describe('AppManagementPage', () => {
217+
it('should render app list from metadata', () => {
218+
wrap(<AppManagementPage />);
219+
expect(screen.getByText('Applications')).toBeInTheDocument();
220+
expect(screen.getByTestId('app-card-crm')).toBeInTheDocument();
221+
expect(screen.getByTestId('app-card-hr')).toBeInTheDocument();
222+
});
223+
224+
it('should filter apps by search query', () => {
225+
wrap(<AppManagementPage />);
226+
fireEvent.change(screen.getByTestId('app-search-input'), { target: { value: 'CRM' } });
227+
expect(screen.getByTestId('app-card-crm')).toBeInTheDocument();
228+
expect(screen.queryByTestId('app-card-hr')).not.toBeInTheDocument();
229+
});
230+
231+
it('should show empty state when no matching apps', () => {
232+
wrap(<AppManagementPage />);
233+
fireEvent.change(screen.getByTestId('app-search-input'), { target: { value: 'nonexistent' } });
234+
expect(screen.getByTestId('no-apps-message')).toBeInTheDocument();
235+
});
236+
237+
it('should navigate to create-app on New App button click', () => {
238+
wrap(<AppManagementPage />);
239+
fireEvent.click(screen.getByTestId('create-app-btn'));
240+
expect(mockNavigate).toHaveBeenCalledWith('/apps/test-app/create-app');
241+
});
242+
});
243+
244+
describe('PermissionManagementPage', () => {
245+
it('should call dataSource.find("sys_permission") on mount', async () => {
246+
mockFind.mockResolvedValueOnce({
247+
data: [{ id: '1', name: 'manage_users', resource: 'user', action: 'manage', description: 'Full user access' }],
248+
});
249+
wrap(<PermissionManagementPage />);
250+
await waitFor(() => {
251+
expect(mockFind).toHaveBeenCalledWith('sys_permission');
252+
});
253+
expect(screen.getByText('manage_users')).toBeInTheDocument();
254+
});
255+
256+
it('should show empty state when no permissions', async () => {
257+
mockFind.mockResolvedValueOnce({ data: [] });
258+
wrap(<PermissionManagementPage />);
259+
await waitFor(() => {
260+
expect(screen.getByText('No permissions found.')).toBeInTheDocument();
261+
});
262+
});
263+
264+
it('should call create when Add Permission is clicked', async () => {
265+
mockFind.mockResolvedValue({ data: [] });
266+
mockCreate.mockResolvedValueOnce({ id: 'new-perm' });
267+
wrap(<PermissionManagementPage />);
268+
await waitFor(() => {
269+
expect(screen.getByText('No permissions found.')).toBeInTheDocument();
270+
});
271+
fireEvent.click(screen.getByText('Add Permission'));
272+
await waitFor(() => {
273+
expect(mockCreate).toHaveBeenCalledWith('sys_permission', expect.objectContaining({ name: 'New Permission' }));
274+
});
275+
});
276+
277+
it('should filter permissions by search query', async () => {
278+
mockFind.mockResolvedValue({
279+
data: [
280+
{ id: '1', name: 'manage_users', resource: 'user', action: 'manage', description: '' },
281+
{ id: '2', name: 'read_reports', resource: 'report', action: 'read', description: '' },
282+
],
283+
});
284+
wrap(<PermissionManagementPage />);
285+
await waitFor(() => {
286+
expect(screen.getByText('manage_users')).toBeInTheDocument();
287+
expect(screen.getByText('read_reports')).toBeInTheDocument();
288+
});
289+
fireEvent.change(screen.getByTestId('permission-search-input'), { target: { value: 'report' } });
290+
await waitFor(() => {
291+
expect(screen.queryByText('manage_users')).not.toBeInTheDocument();
292+
expect(screen.getByText('read_reports')).toBeInTheDocument();
293+
});
294+
});
295+
});

apps/console/src/components/AppSidebar.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,12 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
312312
</div>
313313
<div className="font-medium text-muted-foreground">Edit App</div>
314314
</DropdownMenuItem>
315+
<DropdownMenuItem className="gap-2 p-2" onClick={() => navigate(`/apps/${activeAppName}/system/apps`)} data-testid="manage-all-apps-btn">
316+
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
317+
<Settings className="size-4" />
318+
</div>
319+
<div className="font-medium text-muted-foreground">Manage All Apps</div>
320+
</DropdownMenuItem>
315321
</DropdownMenuContent>
316322
</DropdownMenu>
317323
</SidebarMenuItem>
@@ -480,7 +486,7 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
480486
<DropdownMenuSeparator />
481487
<DropdownMenuGroup>
482488
<DropdownMenuItem
483-
onClick={() => navigate(`/apps/${activeAppName}/system/profile`)}
489+
onClick={() => navigate(`/apps/${activeAppName}/system`)}
484490
>
485491
<Settings className="mr-2 h-4 w-4" />
486492
Settings

0 commit comments

Comments
 (0)