Les select de service et catégorie utilisaient getServices()/getCategories(), soit les référentiels complets — dont certaines entrées (ex. "Business Intelligence") n'ont aucun monitoring rattaché et menaient sur un dashboard vide. Bascule vers le nouvel endpoint GET /dashboard/filtres (VUE_CONSO), avec filtrage en cascade : sélectionner un service restreint la liste des catégories proposées à celles qui ont réellement des résultats pour ce service.
173 lines
5.5 KiB
React
173 lines
5.5 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 LoadingSpinner from '../../components/common/LoadingSpinner';
|
|
import ErrorMessage from '../../components/common/ErrorMessage';
|
|
import styles from './Dashboard.module.css';
|
|
|
|
/* Carte KPI affichant un indicateur clé */
|
|
function KpiCard({ label, value, color }) {
|
|
return (
|
|
<div className={styles.kpiCard}>
|
|
<span className={styles.kpiValue} style={{ color }}>{value}</span>
|
|
<span className={styles.kpiLabel}>{label}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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('');
|
|
|
|
/* 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) {
|
|
setFiltreService(service);
|
|
if (filtreCategorie && !categoriesDisponibles(service).includes(filtreCategorie)) {
|
|
setFiltreCategorie('');
|
|
}
|
|
}
|
|
|
|
/* Chargement des données dashboard */
|
|
const loadData = useCallback(async () => {
|
|
setLoading(true);
|
|
setErreur('');
|
|
try {
|
|
const [sum, data] = await Promise.all([
|
|
getDashboardSummary(),
|
|
getDashboard(filtreService, filtreCategorie),
|
|
]);
|
|
setSummary(sum);
|
|
setMonitorings(Array.isArray(data) ? data : []);
|
|
} catch (e) {
|
|
setErreur(e.message || 'Erreur lors du chargement du dashboard.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [filtreService, filtreCategorie]);
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, [loadData]);
|
|
|
|
return (
|
|
<div className={styles.page}>
|
|
<div className={styles.pageHeader}>
|
|
<h1 className={styles.title}>Dashboard</h1>
|
|
<button className={styles.refreshBtn} onClick={loadData} 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 => setFiltreCategorie(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={loadData} />
|
|
) : (
|
|
<>
|
|
{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>
|
|
);
|
|
}
|