Le dépôt sortait 7 erreurs eslint au début de cette série de correctifs, dont certaines antérieures. Il n'en reste aucune. - Dashboard et Users appelaient setState de façon synchrone dans un effet, ce qui provoque des rendus en cascade. Le chargement initial passe par une fonction asynchrone qui n'écrit qu'après l'await, avec annulation au démontage. Sur le Dashboard, l'indicateur de chargement est allumé par les gestionnaires d'événement, et le bouton Actualiser relance l'effet via un compteur plutôt qu'en appelant le chargement. - L'import ExternalLink de Documentation.jsx n'était pas utilisé. - AuthContext.jsx exportait à la fois un composant, le contexte et un hook, ce qui désactivait le rafraîchissement à chaud de Vite pour ce fichier. Le contexte et le hook vivent désormais dans leurs propres modules ; les 5 fichiers concernés sont mis à jour.
183 lines
6.0 KiB
React
183 lines
6.0 KiB
React
/* Page principale du dashboard : KPI cards + grille de monitorings */
|
|
|
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
import { RefreshCw } from 'lucide-react';
|
|
import { getDashboard, getDashboardSummary, getDashboardFiltres } from '../../services/api';
|
|
import MonitoringCard from '../../components/common/MonitoringCard';
|
|
import KpiCard from '../../components/common/KpiCard';
|
|
import LoadingSpinner from '../../components/common/LoadingSpinner';
|
|
import ErrorMessage from '../../components/common/ErrorMessage';
|
|
import styles from './Dashboard.module.css';
|
|
|
|
export default function Dashboard() {
|
|
const [summary, setSummary] = useState(null);
|
|
const [monitorings, setMonitorings] = useState([]);
|
|
const [services, setServices] = useState([]);
|
|
const [combinaisons, setCombinaisons] = useState([]);
|
|
const [filtreService, setFiltreService] = useState('');
|
|
const [filtreCategorie, setFiltreCategorie] = useState('');
|
|
const [loading, setLoading] = useState(true);
|
|
const [erreur, setErreur] = useState('');
|
|
const [rafraichissement, setRafraichissement] = useState(0);
|
|
|
|
/* Chargement initial des filtres.
|
|
On n'utilise pas les référentiels SERVICE / CATEGORIE complets : ils
|
|
contiennent des entrées sans aucun monitoring rattaché (ex. « Business
|
|
Intelligence »), qui mèneraient sur un dashboard vide. */
|
|
useEffect(() => {
|
|
async function loadFiltres() {
|
|
try {
|
|
const filtres = await getDashboardFiltres();
|
|
setServices(filtres.services);
|
|
setCombinaisons(filtres.combinaisons);
|
|
} catch {
|
|
/* Non bloquant, les filtres restent vides */
|
|
}
|
|
}
|
|
loadFiltres();
|
|
}, []);
|
|
|
|
/* Catégories proposées : restreintes au service sélectionné, le cas échéant */
|
|
const categoriesDisponibles = useCallback(service => {
|
|
const pertinentes = service
|
|
? combinaisons.filter(c => c.service === service)
|
|
: combinaisons;
|
|
return [...new Set(pertinentes.map(c => c.categorie))].sort();
|
|
}, [combinaisons]);
|
|
|
|
const categories = useMemo(
|
|
() => categoriesDisponibles(filtreService),
|
|
[categoriesDisponibles, filtreService],
|
|
);
|
|
|
|
/* Changer de service peut rendre la catégorie sélectionnée sans résultat :
|
|
on la remet à zéro dans le même rendu, pour ne déclencher qu'un seul appel. */
|
|
function handleServiceChange(service) {
|
|
setLoading(true);
|
|
setFiltreService(service);
|
|
if (filtreCategorie && !categoriesDisponibles(service).includes(filtreCategorie)) {
|
|
setFiltreCategorie('');
|
|
}
|
|
}
|
|
|
|
function handleCategorieChange(categorie) {
|
|
setLoading(true);
|
|
setFiltreCategorie(categorie);
|
|
}
|
|
|
|
/* Le bouton Actualiser relance l'effet en changeant cette valeur. */
|
|
function handleActualiser() {
|
|
setLoading(true);
|
|
setRafraichissement(compteur => compteur + 1);
|
|
}
|
|
|
|
/* Les données sont rechargées à chaque changement de filtre. L'indicateur de
|
|
chargement est allumé par les gestionnaires d'événement ci-dessus, jamais
|
|
dans l'effet : y appeler setState de façon synchrone provoquerait un rendu
|
|
en cascade. */
|
|
useEffect(() => {
|
|
let annule = false;
|
|
|
|
async function charger() {
|
|
try {
|
|
const [sum, data] = await Promise.all([
|
|
getDashboardSummary(),
|
|
getDashboard(filtreService, filtreCategorie),
|
|
]);
|
|
if (annule) return;
|
|
setSummary(sum);
|
|
setMonitorings(Array.isArray(data) ? data : []);
|
|
setErreur('');
|
|
} catch (e) {
|
|
if (!annule) setErreur(e.message || 'Erreur lors du chargement du dashboard.');
|
|
} finally {
|
|
if (!annule) setLoading(false);
|
|
}
|
|
}
|
|
|
|
charger();
|
|
return () => { annule = true; };
|
|
}, [filtreService, filtreCategorie, rafraichissement]);
|
|
|
|
return (
|
|
<div className={styles.page}>
|
|
<div className={styles.pageHeader}>
|
|
<h1 className={styles.title}>Dashboard</h1>
|
|
<button className={styles.refreshBtn} onClick={handleActualiser} disabled={loading}>
|
|
<RefreshCw size={14} className={loading ? styles.spinning : ''} />
|
|
Actualiser
|
|
</button>
|
|
</div>
|
|
|
|
{/* KPI Cards */}
|
|
{summary && (
|
|
<div className={styles.kpiGrid}>
|
|
<KpiCard
|
|
label="Total monitorings"
|
|
value={summary.total ?? 0}
|
|
color="var(--color-text)"
|
|
/>
|
|
<KpiCard
|
|
label="Sans erreur"
|
|
value={summary.ok ?? 0}
|
|
color="var(--color-ok)"
|
|
/>
|
|
<KpiCard
|
|
label="En erreur"
|
|
value={summary.critical ?? 0}
|
|
color="var(--color-primary)"
|
|
/>
|
|
<KpiCard
|
|
label="Total erreurs"
|
|
value={summary.total_erreurs ?? 0}
|
|
color="var(--color-warning)"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Filtres */}
|
|
<div className={styles.filtres}>
|
|
<select
|
|
className={styles.select}
|
|
value={filtreService}
|
|
onChange={e => handleServiceChange(e.target.value)}
|
|
>
|
|
<option value="">Tous les services</option>
|
|
{services.map(s => (
|
|
<option key={s} value={s}>{s}</option>
|
|
))}
|
|
</select>
|
|
<select
|
|
className={styles.select}
|
|
value={filtreCategorie}
|
|
onChange={e => handleCategorieChange(e.target.value)}
|
|
>
|
|
<option value="">Toutes les catégories</option>
|
|
{categories.map(c => (
|
|
<option key={c} value={c}>{c}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Contenu principal */}
|
|
{loading ? (
|
|
<LoadingSpinner message="Chargement du dashboard..." />
|
|
) : erreur ? (
|
|
<ErrorMessage message={erreur} onRetry={handleActualiser} />
|
|
) : (
|
|
<>
|
|
{monitorings.length === 0 ? (
|
|
<p className={styles.empty}>Aucun monitoring trouvé pour ces filtres.</p>
|
|
) : (
|
|
<div className={styles.grid}>
|
|
{monitorings.map(m => (
|
|
<MonitoringCard key={m.id} monitoring={m} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|