refactor(admin): replace static dashboard stats with dynamic widget registry
This commit is contained in:
@@ -1,30 +1,9 @@
|
||||
/**
|
||||
* Admin Stats Actions
|
||||
* Server-side actions for core dashboard statistics
|
||||
*
|
||||
* Usage in your Next.js app:
|
||||
*
|
||||
* ```javascript
|
||||
* // 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}
|
||||
* />
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
* @deprecated Utiliser collectAllDashboardData() de serverRegistry à la place.
|
||||
* Ce fichier est conservé pour la rétrocompatibilité avec les projets qui
|
||||
* appellent getDashboardStats() directement depuis leur page admin.
|
||||
*/
|
||||
|
||||
'use server';
|
||||
@@ -33,40 +12,16 @@ import { query } from '@zen/core/database';
|
||||
import { fail } from '@zen/core/shared/logger';
|
||||
|
||||
/**
|
||||
* Get total number of users
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
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>}
|
||||
* @deprecated Utiliser collectAllDashboardData() de serverRegistry à la place.
|
||||
* @returns {Promise<{ success: boolean, stats?: { totalUsers: number }, error?: string }>}
|
||||
*/
|
||||
export async function getDashboardStats() {
|
||||
try {
|
||||
const totalUsers = await getTotalUsersCount();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stats: {
|
||||
totalUsers,
|
||||
}
|
||||
};
|
||||
const result = await query(`SELECT COUNT(*) as count FROM zen_auth_users`);
|
||||
const totalUsers = parseInt(result.rows[0].count) || 0;
|
||||
return { success: true, stats: { totalUsers } };
|
||||
} catch (error) {
|
||||
fail(`Error getting dashboard stats: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Failed to get dashboard statistics'
|
||||
};
|
||||
return { success: false, error: error.message || 'Failed to get dashboard statistics' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { StatCard } from '@zen/core/shared/components';
|
||||
import { UserMultiple02Icon } from '@zen/core/shared/icons';
|
||||
import { getClientWidgets } from '../../dashboard/clientRegistry.js';
|
||||
import '../../../dashboard.client.js';
|
||||
|
||||
export default function DashboardPage({ user, stats }) {
|
||||
const loading = !stats;
|
||||
// Évalué après tous les imports : les auto-registrations sont complètes
|
||||
const sortedWidgets = getClientWidgets();
|
||||
|
||||
export default function DashboardPage({ stats }) {
|
||||
const loading = stats === null || stats === undefined;
|
||||
|
||||
return (
|
||||
<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 className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
||||
<StatCard
|
||||
title="Nombre d'utilisateurs"
|
||||
value={loading ? '-' : String(stats?.totalUsers || 0)}
|
||||
icon={UserMultiple02Icon}
|
||||
color="text-purple-400"
|
||||
bgColor="bg-purple-500/10"
|
||||
loading={loading}
|
||||
/>
|
||||
{sortedWidgets.map(({ id, Component }) => (
|
||||
<Component
|
||||
key={id}
|
||||
data={loading ? null : (stats[id] ?? null)}
|
||||
loading={loading}
|
||||
/>
|
||||
))}
|
||||
</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 { protectAdmin } from '@zen/core/features/admin';
|
||||
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 { getAppName } from '@zen/core';
|
||||
|
||||
@@ -17,8 +17,7 @@ export default async function AdminPage({ params }) {
|
||||
const session = await protectAdmin();
|
||||
const appName = getAppName();
|
||||
|
||||
const statsResult = await getDashboardStats();
|
||||
const dashboardStats = statsResult.success ? statsResult.stats : null;
|
||||
const dashboardStats = await collectAllDashboardData();
|
||||
|
||||
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/admin/page.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'],
|
||||
dts: false,
|
||||
|
||||
Reference in New Issue
Block a user