init projet
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
// src/pages/MonitoringDetail/MonitoringDetail.jsx
|
||||
// Page de détail d'un monitoring avec fil d'Ariane, tableau d'erreurs, export CSV
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Download, TrendingUp } from 'lucide-react';
|
||||
import { getMonitoringById, getMonitoringDetails, getMonitoringCount, getMonitoringColumns, exportToCSV } from '../../services/api';
|
||||
import DataTable from '../../components/common/DataTable';
|
||||
import LoadingSpinner from '../../components/common/LoadingSpinner';
|
||||
import ErrorMessage from '../../components/common/ErrorMessage';
|
||||
import Badge from '../../components/common/Badge';
|
||||
import Button from '../../components/common/Button';
|
||||
import styles from './MonitoringDetail.module.css';
|
||||
|
||||
/* Détermine le statut selon le nombre d'erreurs */
|
||||
function getStatut(nbErreurs) {
|
||||
if (nbErreurs === 0) return { label: 'OK', variant: 'ok' };
|
||||
if (nbErreurs < 10) return { label: 'Attention', variant: 'warning' };
|
||||
return { label: 'Critique', variant: 'critical' };
|
||||
}
|
||||
|
||||
export default function MonitoringDetail() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [monitoring, setMonitoring] = useState(null);
|
||||
const [details, setDetails] = useState([]);
|
||||
const [columns, setColumns] = useState([]);
|
||||
const [count, setCount] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [erreur, setErreur] = useState('');
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
setLoading(true);
|
||||
setErreur('');
|
||||
try {
|
||||
const [monit, det, cnt, cols] = await Promise.all([
|
||||
getMonitoringById(id),
|
||||
getMonitoringDetails(id, { limit: 5000 }), // Charge maximum pour export complet
|
||||
getMonitoringCount(id),
|
||||
getMonitoringColumns(id)
|
||||
]);
|
||||
setMonitoring(monit);
|
||||
setDetails(Array.isArray(det) ? det : []);
|
||||
setCount(cnt.count ?? cnt.nb_erreurs ?? 0);
|
||||
|
||||
// Traite les colonnes de l'API
|
||||
if (cols?.columns) {
|
||||
const colsList = cols.columns.map(col => ({
|
||||
key: col.COLUMN_NAME,
|
||||
label: col.COLUMN_NAME
|
||||
}));
|
||||
setColumns(colsList);
|
||||
}
|
||||
} catch (e) {
|
||||
setErreur(e.message || 'Erreur lors du chargement du monitoring.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
loadData();
|
||||
}, [id]);
|
||||
|
||||
/* Filtrage des détails par recherche */
|
||||
const filteredDetails = details.filter(row => {
|
||||
if (!search) return true;
|
||||
return Object.values(row).some(val =>
|
||||
String(val || '').toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
if (loading) return <LoadingSpinner message="Chargement du monitoring..." />;
|
||||
if (erreur) return <ErrorMessage message={erreur} />;
|
||||
|
||||
const statut = getStatut(count);
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
{/* Fil d'Ariane */}
|
||||
<div className={styles.breadcrumb}>
|
||||
<span onClick={() => navigate('/dashboard')} className={styles.breadcrumbLink}>Dashboard</span>
|
||||
<span className={styles.separator}>›</span>
|
||||
<span>{monitoring?.service || 'Service'}</span>
|
||||
<span className={styles.separator}>›</span>
|
||||
<span>{monitoring?.nom || 'Monitoring'}</span>
|
||||
</div>
|
||||
|
||||
{/* En-tête */}
|
||||
<div className={styles.header}>
|
||||
<div className={styles.titleSection}>
|
||||
<h1 className={styles.title}>{monitoring?.nom || monitoring?.monito_intitule || 'Monitoring'}</h1>
|
||||
<div className={styles.badges}>
|
||||
{monitoring?.bdd_source && <Badge variant={monitoring.bdd_source.toLowerCase() === 'sage' ? 'sage' : 'crm'}>{monitoring.bdd_source}</Badge>}
|
||||
{monitoring?.id_categorie && <Badge variant="info">Catégorie {monitoring.id_categorie}</Badge>}
|
||||
<Badge variant={statut.variant}>{statut.label}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.count}>
|
||||
<span className={styles.countNumber}>{count}</span>
|
||||
<span className={styles.countLabel}>erreur{count !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className={styles.actions}>
|
||||
<Button variant="secondary" onClick={() => exportToCSV(filteredDetails, `monitoring_${id}_${new Date().toISOString().split('T')[0]}.csv`)}>
|
||||
<Download size={14} />
|
||||
Exporter CSV ({filteredDetails.length} lignes)
|
||||
</Button>
|
||||
<Button variant="primary" onClick={() => navigate(`/historique?id=${id}`)}>
|
||||
<TrendingUp size={14} />
|
||||
Voir l'évolution
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Filtres */}
|
||||
<div className={styles.filtres}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher dans les erreurs..."
|
||||
className={styles.searchInput}
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Résumé des résultats */}
|
||||
<div style={{ marginBottom: '1rem', fontSize: '0.9rem', color: 'var(--color-text-secondary)' }}>
|
||||
{filteredDetails.length} / {details.length} erreur{details.length !== 1 ? 's' : ''}
|
||||
{search && ` (filtré: ${search})`}
|
||||
</div>
|
||||
|
||||
{/* Tableau */}
|
||||
{columns.length > 0 && (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={filteredDetails}
|
||||
pageSize={50}
|
||||
emptyMessage="Aucune erreur trouvée."
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Encart aide */}
|
||||
<div className={styles.helpBox}>
|
||||
<p><strong>Aide :</strong> Ce tableau affiche les erreurs détectées pour ce monitoring. Utilisez la barre de recherche pour filtrer. L'export CSV permet de télécharger toutes les données filtrées.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/* src/pages/MonitoringDetail/MonitoringDetail.module.css */
|
||||
|
||||
.page {
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.breadcrumbLink {
|
||||
color: var(--color-link);
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumbLink:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: var(--color-primary);
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--color-gray-border);
|
||||
}
|
||||
|
||||
.titleSection h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.badges {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.count {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.countNumber {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: var(--color-primary);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.countLabel {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.filtres {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.searchInput,
|
||||
.filterInput {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-gray-border);
|
||||
border-radius: var(--radius);
|
||||
font-size: 14px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.helpBox {
|
||||
margin-top: 32px;
|
||||
padding: 16px;
|
||||
background-color: var(--color-danger-bg);
|
||||
border-left: 4px solid var(--color-primary);
|
||||
border-radius: var(--radius);
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
}
|
||||
Reference in New Issue
Block a user