refactor(admin): replace static dashboard stats with dynamic widget registry
This commit is contained in:
@@ -1,30 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Admin Stats Actions
|
* Admin Stats Actions
|
||||||
* Server-side actions for core dashboard statistics
|
|
||||||
*
|
*
|
||||||
* Usage in your Next.js app:
|
* @deprecated Utiliser collectAllDashboardData() de serverRegistry à la place.
|
||||||
*
|
* Ce fichier est conservé pour la rétrocompatibilité avec les projets qui
|
||||||
* ```javascript
|
* appellent getDashboardStats() directement depuis leur page admin.
|
||||||
* // app/(admin)/admin/[...admin]/page.js
|
|
||||||
* import { protectAdmin } from '@zen/core/features/admin';
|
|
||||||
* import { getDashboardStats } from '@zen/core/features/admin/actions';
|
|
||||||
* import { AdminPagesClient } from '@zen/core/features/admin/pages';
|
|
||||||
*
|
|
||||||
* export default async function AdminPage({ params }) {
|
|
||||||
* const { user } = await protectAdmin();
|
|
||||||
*
|
|
||||||
* const statsResult = await getDashboardStats();
|
|
||||||
* const dashboardStats = statsResult.success ? statsResult.stats : null;
|
|
||||||
*
|
|
||||||
* return (
|
|
||||||
* <AdminPagesClient
|
|
||||||
* params={params}
|
|
||||||
* user={user}
|
|
||||||
* dashboardStats={dashboardStats}
|
|
||||||
* />
|
|
||||||
* );
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use server';
|
'use server';
|
||||||
@@ -33,40 +12,16 @@ import { query } from '@zen/core/database';
|
|||||||
import { fail } from '@zen/core/shared/logger';
|
import { fail } from '@zen/core/shared/logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get total number of users
|
* @deprecated Utiliser collectAllDashboardData() de serverRegistry à la place.
|
||||||
* @returns {Promise<number>}
|
* @returns {Promise<{ success: boolean, stats?: { totalUsers: number }, error?: string }>}
|
||||||
*/
|
|
||||||
async function getTotalUsersCount() {
|
|
||||||
try {
|
|
||||||
const result = await query(
|
|
||||||
`SELECT COUNT(*) as count FROM zen_auth_users`
|
|
||||||
);
|
|
||||||
return parseInt(result.rows[0].count) || 0;
|
|
||||||
} catch (error) {
|
|
||||||
fail(`Error getting users count: ${error.message}`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get core dashboard statistics
|
|
||||||
* @returns {Promise<Object>}
|
|
||||||
*/
|
*/
|
||||||
export async function getDashboardStats() {
|
export async function getDashboardStats() {
|
||||||
try {
|
try {
|
||||||
const totalUsers = await getTotalUsersCount();
|
const result = await query(`SELECT COUNT(*) as count FROM zen_auth_users`);
|
||||||
|
const totalUsers = parseInt(result.rows[0].count) || 0;
|
||||||
return {
|
return { success: true, stats: { totalUsers } };
|
||||||
success: true,
|
|
||||||
stats: {
|
|
||||||
totalUsers,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
fail(`Error getting dashboard stats: ${error.message}`);
|
fail(`Error getting dashboard stats: ${error.message}`);
|
||||||
return {
|
return { success: false, error: error.message || 'Failed to get dashboard statistics' };
|
||||||
success: false,
|
|
||||||
error: error.message || 'Failed to get dashboard statistics'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { StatCard } from '@zen/core/shared/components';
|
import { getClientWidgets } from '../../dashboard/clientRegistry.js';
|
||||||
import { UserMultiple02Icon } from '@zen/core/shared/icons';
|
import '../../../dashboard.client.js';
|
||||||
|
|
||||||
export default function DashboardPage({ user, stats }) {
|
// Évalué après tous les imports : les auto-registrations sont complètes
|
||||||
const loading = !stats;
|
const sortedWidgets = getClientWidgets();
|
||||||
|
|
||||||
|
export default function DashboardPage({ stats }) {
|
||||||
|
const loading = stats === null || stats === undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 sm:gap-6 lg:gap-8">
|
<div className="flex flex-col gap-4 sm:gap-6 lg:gap-8">
|
||||||
@@ -18,14 +21,13 @@ export default function DashboardPage({ user, stats }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
||||||
<StatCard
|
{sortedWidgets.map(({ id, Component }) => (
|
||||||
title="Nombre d'utilisateurs"
|
<Component
|
||||||
value={loading ? '-' : String(stats?.totalUsers || 0)}
|
key={id}
|
||||||
icon={UserMultiple02Icon}
|
data={loading ? null : (stats[id] ?? null)}
|
||||||
color="text-purple-400"
|
loading={loading}
|
||||||
bgColor="bg-purple-500/10"
|
/>
|
||||||
loading={loading}
|
))}
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard Client Registry — API générique
|
||||||
|
*
|
||||||
|
* Les features s'enregistrent ici via registerClientWidget().
|
||||||
|
* Ce fichier ne doit jamais importer une feature directement.
|
||||||
|
* Les features dépendent de ce fichier, pas l'inverse.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const widgets = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistre un composant React pour le tableau de bord.
|
||||||
|
* Appelé en side effect par chaque feature (ex: auth/dashboard.widget.js).
|
||||||
|
* @param {string} id - Identifiant unique de la feature (doit correspondre à l'id serveur)
|
||||||
|
* @param {React.ComponentType} Component - Composant avec props { data, loading }
|
||||||
|
* @param {number} order - Ordre d'affichage (croissant). Utiliser des intervalles de 10.
|
||||||
|
*/
|
||||||
|
export function registerClientWidget(id, Component, order) {
|
||||||
|
widgets.push({ id, Component, order });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne tous les widgets enregistrés, triés par order croissant.
|
||||||
|
* @returns {Array<{ id: string, Component: React.ComponentType, order: number }>}
|
||||||
|
*/
|
||||||
|
export function getClientWidgets() {
|
||||||
|
return [...widgets].sort((a, b) => a.order - b.order);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Dashboard Server Registry — API générique
|
||||||
|
*
|
||||||
|
* Les features s'enregistrent ici via registerServerWidget().
|
||||||
|
* Ce fichier ne doit jamais importer une feature directement.
|
||||||
|
* Les features dépendent de ce fichier, pas l'inverse.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const widgets = new Map(); // id → fetcher
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistre une fonction de fetch pour le tableau de bord.
|
||||||
|
* Appelé en side effect par chaque feature (ex: auth/dashboard.server.js).
|
||||||
|
* @param {string} id - Identifiant unique de la feature
|
||||||
|
* @param {() => Promise<any>} fetcher - Fonction async retournant des données sérialisables
|
||||||
|
*/
|
||||||
|
export function registerServerWidget(id, fetcher) {
|
||||||
|
widgets.set(id, fetcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exécute tous les fetchers enregistrés en parallèle.
|
||||||
|
* Un fetcher qui échoue n'empêche pas les autres (Promise.allSettled).
|
||||||
|
* @returns {Promise<Record<string, any>>} Map featureId → données
|
||||||
|
*/
|
||||||
|
export async function collectAllDashboardData() {
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
Array.from(widgets.entries()).map(([id, fetcher]) =>
|
||||||
|
fetcher().then(data => ({ id, data }))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return results.reduce((acc, result) => {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
acc[result.value.id] = result.value.data;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Dashboard Server Registry — Point d'entrée
|
||||||
|
*
|
||||||
|
* Ré-exporte l'API du registre pur et déclenche le câblage des features
|
||||||
|
* via un import side-effect de features/dashboard.server.js.
|
||||||
|
*
|
||||||
|
* Ne jamais importer une feature directement ici.
|
||||||
|
* Pour ajouter une feature : éditer src/features/dashboard.server.js.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { collectAllDashboardData } from './registry.js';
|
||||||
|
|
||||||
|
// Side effect : initialise le registre, puis déclenche toutes les auto-registrations
|
||||||
|
import './registry.js';
|
||||||
|
import '../../dashboard.server.js';
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
import { AdminPagesLayout, AdminPagesClient } from '@zen/core/features/admin/pages';
|
import { AdminPagesLayout, AdminPagesClient } from '@zen/core/features/admin/pages';
|
||||||
import { protectAdmin } from '@zen/core/features/admin';
|
import { protectAdmin } from '@zen/core/features/admin';
|
||||||
import { buildNavigationSections } from '@zen/core/features/admin/navigation';
|
import { buildNavigationSections } from '@zen/core/features/admin/navigation';
|
||||||
import { getDashboardStats } from '@zen/core/features/admin/actions';
|
import { collectAllDashboardData } from './dashboard/serverRegistry.js';
|
||||||
import { logoutAction } from '@zen/core/features/auth/actions';
|
import { logoutAction } from '@zen/core/features/auth/actions';
|
||||||
import { getAppName } from '@zen/core';
|
import { getAppName } from '@zen/core';
|
||||||
|
|
||||||
@@ -17,8 +17,7 @@ export default async function AdminPage({ params }) {
|
|||||||
const session = await protectAdmin();
|
const session = await protectAdmin();
|
||||||
const appName = getAppName();
|
const appName = getAppName();
|
||||||
|
|
||||||
const statsResult = await getDashboardStats();
|
const dashboardStats = await collectAllDashboardData();
|
||||||
const dashboardStats = statsResult.success ? statsResult.stats : null;
|
|
||||||
|
|
||||||
const navigationSections = buildNavigationSections('/');
|
const navigationSections = buildNavigationSections('/');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Auth Feature — Contribution serveur au tableau de bord
|
||||||
|
*
|
||||||
|
* Ce module s'auto-enregistre auprès du registre admin en side effect.
|
||||||
|
* Dépendance correcte : auth → admin (feature → core).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { registerServerWidget } from '../admin/dashboard/registry.js';
|
||||||
|
import { query } from '@zen/core/database';
|
||||||
|
import { fail } from '@zen/core/shared/logger';
|
||||||
|
|
||||||
|
async function getDashboardData() {
|
||||||
|
try {
|
||||||
|
const result = await query(`SELECT COUNT(*) as count FROM zen_auth_users`);
|
||||||
|
return { totalUsers: parseInt(result.rows[0].count) || 0 };
|
||||||
|
} catch (error) {
|
||||||
|
fail(`Auth dashboard data error: ${error.message}`);
|
||||||
|
return { totalUsers: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerServerWidget('auth', getDashboardData);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth Feature — Widget client pour le tableau de bord
|
||||||
|
*
|
||||||
|
* Ce module s'auto-enregistre auprès du registre admin en side effect.
|
||||||
|
* Dépendance correcte : auth → admin (feature → core).
|
||||||
|
*
|
||||||
|
* Props du composant :
|
||||||
|
* data — { totalUsers: number } retourné par getDashboardData(), ou null
|
||||||
|
* loading — true tant que les données serveur ne sont pas disponibles
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { registerClientWidget } from '../admin/dashboard/clientRegistry.js';
|
||||||
|
import { StatCard } from '@zen/core/shared/components';
|
||||||
|
import { UserMultiple02Icon } from '@zen/core/shared/icons';
|
||||||
|
|
||||||
|
function AuthDashboardWidget({ data, loading }) {
|
||||||
|
return (
|
||||||
|
<StatCard
|
||||||
|
title="Nombre d'utilisateurs"
|
||||||
|
value={loading ? '-' : String(data?.totalUsers ?? 0)}
|
||||||
|
icon={UserMultiple02Icon}
|
||||||
|
color="text-purple-400"
|
||||||
|
bgColor="bg-purple-500/10"
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerClientWidget('auth', AuthDashboardWidget, 10);
|
||||||
|
|
||||||
|
export default AuthDashboardWidget;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Câblage client des widgets du tableau de bord
|
||||||
|
*
|
||||||
|
* Ce fichier est le SEUL à modifier pour ajouter le widget client
|
||||||
|
* d'une nouvelle feature au tableau de bord.
|
||||||
|
*
|
||||||
|
* L'import suffit : chaque module s'enregistre lui-même en side effect
|
||||||
|
* via registerClientWidget() de admin/dashboard/clientRegistry.js.
|
||||||
|
*
|
||||||
|
* Exemple pour une feature 'blog' :
|
||||||
|
* import './blog/dashboard.widget.js';
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './auth/dashboard.widget.js';
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Câblage serveur des widgets du tableau de bord
|
||||||
|
*
|
||||||
|
* Ce fichier est le SEUL à modifier pour ajouter la contribution serveur
|
||||||
|
* d'une nouvelle feature au tableau de bord.
|
||||||
|
*
|
||||||
|
* L'import suffit : chaque module s'enregistre lui-même en side effect
|
||||||
|
* via registerServerWidget() de admin/dashboard/registry.js.
|
||||||
|
*
|
||||||
|
* Exemple pour une feature 'blog' :
|
||||||
|
* import './blog/dashboard.server.js';
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './auth/dashboard.server.js';
|
||||||
@@ -58,6 +58,10 @@ export default defineConfig([
|
|||||||
'src/features/auth/page.js',
|
'src/features/auth/page.js',
|
||||||
'src/features/admin/page.js',
|
'src/features/admin/page.js',
|
||||||
'src/features/admin/navigation.server.js',
|
'src/features/admin/navigation.server.js',
|
||||||
|
'src/features/admin/dashboard/registry.js',
|
||||||
|
'src/features/admin/dashboard/serverRegistry.js',
|
||||||
|
'src/features/auth/dashboard.server.js',
|
||||||
|
'src/features/dashboard.server.js',
|
||||||
],
|
],
|
||||||
format: ['esm'],
|
format: ['esm'],
|
||||||
dts: false,
|
dts: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user