Skip to content

Commit 5d8dcd2

Browse files
committed
Implement management dashboard navigation cards and enhance user permissions filtering
- Created a new `NavCardGrid` component for displaying management dashboard quick-link cards, aligning with the Podverse pattern. - Updated the dashboard page to utilize the new `NavCardGrid`, filtering links based on user permissions for super-admin and limited-admin roles. - Added internationalization support for card descriptions in both English and Spanish. - Enhanced end-to-end tests to verify visibility of dashboard links according to user roles. - Introduced new styles for the navigation card grid to improve layout and user experience.
1 parent eaf1afa commit 5d8dcd2

16 files changed

Lines changed: 338 additions & 181 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Management Dashboard Nav Cards - History
2+
3+
Started: 2026-04-23
4+
Author: GitHub Copilot (GPT-5.3-Codex)
5+
Context: Align Metaboost management dashboard quick-link card layout and behavior with Podverse pattern.
6+
7+
### Session 1 - 2026-04-23
8+
#### Prompt (Developer)
9+
the metaboost management dashboard should follow the same pattern as the podverse management dashboard. create a metaboost ui component if needed. display the linked pages available in a clickable button type of format (reuse podverse layout and styles for this)
10+
11+
#### Prompt (Developer)
12+
Start implementation
13+
14+
#### Key Decisions
15+
- Build a reusable @metaboost/ui navigation card grid component (Podverse-style structure) instead of page-local dashboard markup.
16+
- Show only links the current management user can access by reusing existing main-nav permission filtering.
17+
- Use title + description cards and add dashboard description i18n keys in en-US and es.
18+
19+
#### Files Created/Modified
20+
- .llm/history/active/management-dashboard-nav-cards/management-dashboard-nav-cards-part-01.md
21+
- apps/management-web/src/app/(main)/dashboard/page.tsx
22+
- apps/management-web/src/lib/main-nav.ts
23+
- apps/management-web/i18n/originals/en-US.json
24+
- apps/management-web/i18n/originals/es.json
25+
- apps/management-web/e2e/dashboard-super-admin-full-crud.spec.ts
26+
- apps/management-web/e2e/dashboard-limited-admin.spec.ts
27+
- packages/ui/src/components/navigation/NavCardGrid/NavCardGrid.tsx
28+
- packages/ui/src/components/navigation/NavCardGrid/NavCardGrid.module.scss
29+
- packages/ui/src/components/navigation/NavCardGrid/NavCardGrid.stories.tsx
30+
- packages/ui/src/components/navigation/NavCardGrid/index.ts
31+
- packages/ui/src/index.ts

apps/management-web/e2e/dashboard-limited-admin.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,19 @@ test.describe('Management dashboard-page for the admin (admins users events:own)
2525
testInfo,
2626
'The dashboard-page is visible for admin (admins users events:own).'
2727
);
28+
29+
await actionAndCapture(
30+
page,
31+
testInfo,
32+
'Dashboard quick-link cards are filtered to only pages available for admin (admins users events:own).',
33+
async () => {
34+
await expect(page.getByRole('link', { name: 'Events' })).toBeVisible();
35+
await expect(page.getByRole('link', { name: 'Admins' })).toHaveCount(0);
36+
await expect(page.getByRole('link', { name: 'Global blocked apps' })).toHaveCount(0);
37+
await expect(page.getByRole('link', { name: 'Terms versions' })).toHaveCount(0);
38+
await expect(page.getByRole('link', { name: 'Users' })).toHaveCount(0);
39+
await expect(page.getByRole('link', { name: 'Buckets' })).toHaveCount(0);
40+
}
41+
);
2842
});
2943
});

apps/management-web/e2e/dashboard-super-admin-full-crud.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,19 @@ test.describe('Management dashboard-page for the super-admin user', () => {
2323
testInfo,
2424
'The management dashboard-page is visible with the primary heading after successful login.'
2525
);
26+
27+
await actionAndCapture(
28+
page,
29+
testInfo,
30+
'Dashboard quick-link cards for all management pages are visible to the super-admin user.',
31+
async () => {
32+
await expect(page.getByRole('link', { name: 'Admins' })).toBeVisible();
33+
await expect(page.getByRole('link', { name: 'Global blocked apps' })).toBeVisible();
34+
await expect(page.getByRole('link', { name: 'Events' })).toBeVisible();
35+
await expect(page.getByRole('link', { name: 'Terms versions' })).toBeVisible();
36+
await expect(page.getByRole('link', { name: 'Users' })).toBeVisible();
37+
await expect(page.getByRole('link', { name: 'Buckets' })).toBeVisible();
38+
}
39+
);
2640
});
2741
});

apps/management-web/i18n/originals/en-US.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,27 @@
472472
"dashboard": {
473473
"title": "Dashboard",
474474
"hello": "Hello, {name}.",
475-
"signedInAs": "Signed in as {email}"
475+
"signedInAs": "Signed in as {email}",
476+
"links": {
477+
"admins": {
478+
"description": "Manage management admins and permissions."
479+
},
480+
"globalBlockedApps": {
481+
"description": "Review and manage globally blocked app IDs."
482+
},
483+
"events": {
484+
"description": "Review management activity and event logs."
485+
},
486+
"termsVersions": {
487+
"description": "Manage published terms and version history."
488+
},
489+
"users": {
490+
"description": "Browse users and inspect account details."
491+
},
492+
"buckets": {
493+
"description": "Manage buckets, admins, roles, and messages."
494+
}
495+
}
476496
},
477497
"profile": {
478498
"title": "Profile",

apps/management-web/i18n/originals/es.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,27 @@
472472
"dashboard": {
473473
"title": "Panel",
474474
"hello": "Hola, {name}.",
475-
"signedInAs": "Conectado como {email}"
475+
"signedInAs": "Conectado como {email}",
476+
"links": {
477+
"admins": {
478+
"description": "Administra los administradores de gestión y sus permisos."
479+
},
480+
"globalBlockedApps": {
481+
"description": "Revisa y administra los IDs de apps bloqueadas globalmente."
482+
},
483+
"events": {
484+
"description": "Revisa la actividad de gestión y los registros de eventos."
485+
},
486+
"termsVersions": {
487+
"description": "Administra los términos publicados y su historial de versiones."
488+
},
489+
"users": {
490+
"description": "Explora usuarios e inspecciona los detalles de sus cuentas."
491+
},
492+
"buckets": {
493+
"description": "Administra buckets, administradores, roles y mensajes."
494+
}
495+
}
476496
},
477497
"profile": {
478498
"title": "Perfil",

apps/management-web/i18n/overrides/es.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,27 @@
472472
"dashboard": {
473473
"title": "",
474474
"hello": "",
475-
"signedInAs": ""
475+
"signedInAs": "",
476+
"links": {
477+
"admins": {
478+
"description": ""
479+
},
480+
"globalBlockedApps": {
481+
"description": ""
482+
},
483+
"events": {
484+
"description": ""
485+
},
486+
"termsVersions": {
487+
"description": ""
488+
},
489+
"users": {
490+
"description": ""
491+
},
492+
"buckets": {
493+
"description": ""
494+
}
495+
}
476496
},
477497
"profile": {
478498
"title": "",
Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { getTranslations } from 'next-intl/server';
2+
import Link from 'next/link';
23
import { redirect } from 'next/navigation';
34

4-
import { Container, SectionWithHeading, Text } from '@metaboost/ui';
5+
import { Container, NavCardGrid, Text } from '@metaboost/ui';
56

7+
import { getVisibleNavEntries } from '../../../lib/main-nav';
68
import { ROUTES } from '../../../lib/routes';
79
import { getServerUser } from '../../../lib/server-auth';
810

@@ -13,14 +15,33 @@ export default async function DashboardPage() {
1315
redirect(ROUTES.LOGIN);
1416
}
1517

16-
const t = await getTranslations('dashboard');
18+
const tDashboard = await getTranslations('dashboard');
19+
const tCommon = await getTranslations('common');
1720
const displayName = user.displayName ?? user.username;
1821

22+
const getDescriptionByLabelKey = (labelKey: string) => {
23+
if (labelKey === 'admins') return tDashboard('links.admins.description');
24+
if (labelKey === 'globalBlockedApps') return tDashboard('links.globalBlockedApps.description');
25+
if (labelKey === 'events') return tDashboard('links.events.description');
26+
if (labelKey === 'termsVersions') return tDashboard('links.termsVersions.description');
27+
if (labelKey === 'users') return tDashboard('links.users.description');
28+
if (labelKey === 'buckets') return tDashboard('links.buckets.description');
29+
return '';
30+
};
31+
32+
const cards = getVisibleNavEntries(user.isSuperAdmin, user.permissions)
33+
.filter((entry) => entry.href !== ROUTES.DASHBOARD)
34+
.map((entry) => ({
35+
href: entry.href,
36+
title: tCommon(entry.labelKey),
37+
description: getDescriptionByLabelKey(entry.labelKey),
38+
}));
39+
1940
return (
2041
<Container>
21-
<SectionWithHeading title={t('title')}>
22-
<Text>{t('hello', { name: displayName })}</Text>
23-
</SectionWithHeading>
42+
<h1>{tDashboard('title')}</h1>
43+
<Text>{tDashboard('hello', { name: displayName })}</Text>
44+
<NavCardGrid cards={cards} LinkComponent={Link} />
2445
</Container>
2546
);
2647
}

apps/management-web/src/lib/main-nav.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,22 @@ export type VisibleNavItem = {
7979
label: string;
8080
};
8181

82+
/**
83+
* Returns nav entries that the user is allowed to see (read permission or no permission required).
84+
* Keeps labelKey so callers can resolve labels/descriptions from i18n namespaces.
85+
*/
86+
export function getVisibleNavEntries(
87+
isSuperAdmin: boolean,
88+
permissions: ManagementUserPermissions | null | undefined
89+
): MainNavEntry[] {
90+
return MAIN_NAV_ENTRIES.filter((entry) => {
91+
if (entry.superAdminOnly === true) return isSuperAdmin;
92+
if (entry.readPermission === undefined) return true;
93+
if (isSuperAdmin) return true;
94+
return hasReadPermission(permissions, entry.readPermission);
95+
});
96+
}
97+
8298
/**
8399
* Returns nav items that the user is allowed to see (read permission or no permission required).
84100
* Labels are translated using the provided t function (e.g. getTranslations('common')).
@@ -88,12 +104,7 @@ export function getVisibleNavItems(
88104
permissions: ManagementUserPermissions | null | undefined,
89105
t: (key: string) => string
90106
): VisibleNavItem[] {
91-
return MAIN_NAV_ENTRIES.filter((entry) => {
92-
if (entry.superAdminOnly === true) return isSuperAdmin;
93-
if (entry.readPermission === undefined) return true;
94-
if (isSuperAdmin) return true;
95-
return hasReadPermission(permissions, entry.readPermission);
96-
}).map((entry) => ({
107+
return getVisibleNavEntries(isSuperAdmin, permissions).map((entry) => ({
97108
href: entry.href,
98109
label: t(entry.labelKey),
99110
}));

0 commit comments

Comments
 (0)