Initial commit
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { api } from '../../api.js';
|
||||
import { fmt, StatusBadge } from './adminHelpers.jsx';
|
||||
|
||||
const KNOWN_JOBS = [
|
||||
{ name: 'auto_statut_retard', label: 'Passage automatique en retard' },
|
||||
];
|
||||
|
||||
export default function JobLogsSection() {
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [running, setRunning] = useState(null);
|
||||
const [runResult, setRunResult] = useState(null);
|
||||
const [err, setErr] = useState(null);
|
||||
const [page, setPage] = useState(0);
|
||||
const PER_PAGE = 20;
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await api.get('/admin/job-logs', { limit: PER_PAGE, offset: page * PER_PAGE });
|
||||
setLogs(data.rows);
|
||||
setTotal(data.total);
|
||||
} catch (e) { setErr(e.message); }
|
||||
finally { setLoading(false); }
|
||||
}, [page]);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const runJob = async (jobName) => {
|
||||
setRunning(jobName); setRunResult(null);
|
||||
try {
|
||||
const r = await api.post(`/admin/jobs/${jobName}/run`, {});
|
||||
setRunResult({ ok: true, msg: `Exécution terminée — ${r.nb_changes} modification(s)` });
|
||||
load();
|
||||
} catch (e) {
|
||||
setRunResult({ ok: false, msg: e.message });
|
||||
} finally { setRunning(null); }
|
||||
};
|
||||
|
||||
const pages = Math.ceil(total / PER_PAGE);
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<h3 style={{ margin: '0 0 4px' }}>Logs des jobs automatiques</h3>
|
||||
<p className="text-muted" style={{ margin: '0 0 20px', fontSize: 'var(--fs-sm)' }}>
|
||||
Historique d'exécution des tâches planifiées et lancement manuel.
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'center', marginBottom: 20, flexWrap: 'wrap' }}>
|
||||
{KNOWN_JOBS.map(j => (
|
||||
<button
|
||||
key={j.name}
|
||||
className="btn btn-outline"
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 7 }}
|
||||
disabled={running === j.name}
|
||||
onClick={() => runJob(j.name)}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor"
|
||||
strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polygon points="4,2 14,8 4,14"/>
|
||||
</svg>
|
||||
{running === j.name ? 'Exécution…' : `Lancer : ${j.label}`}
|
||||
</button>
|
||||
))}
|
||||
{runResult && (
|
||||
<span style={{
|
||||
fontSize: 13, padding: '4px 12px', borderRadius: 6,
|
||||
background: runResult.ok ? 'rgba(34,197,94,.1)' : 'rgba(239,68,68,.1)',
|
||||
color: runResult.ok ? '#16a34a' : '#dc2626',
|
||||
border: `1px solid ${runResult.ok ? 'rgba(34,197,94,.3)' : 'rgba(239,68,68,.3)'}`,
|
||||
}}>
|
||||
{runResult.msg}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading && <p style={{ color: 'var(--text-muted)' }}>Chargement…</p>}
|
||||
{err && <p style={{ color: '#ef4444' }}>{err}</p>}
|
||||
{!loading && !err && !logs.length && <p style={{ color: 'var(--text-muted)' }}>Aucun log disponible.</p>}
|
||||
|
||||
{logs.length > 0 && (
|
||||
<>
|
||||
<p style={{ fontSize: 13, color: 'var(--text-muted)', marginBottom: 12 }}>
|
||||
{total} entrée{total > 1 ? 's' : ''} au total
|
||||
</p>
|
||||
<table style={{ fontSize: 13 }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th><th>Job</th><th>Statut</th>
|
||||
<th className="num">Modifs</th><th>Détails</th><th>Erreur</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{logs.map(l => (
|
||||
<tr key={l.id}>
|
||||
<td style={{ whiteSpace: 'nowrap', color: 'var(--text-muted)' }}>{fmt(l.run_at)}</td>
|
||||
<td style={{ fontFamily: 'monospace', fontSize: 12 }}>{l.job_name}</td>
|
||||
<td><StatusBadge status={l.status} /></td>
|
||||
<td className="num">
|
||||
{l.nb_changes > 0
|
||||
? <span style={{ fontWeight: 700, color: '#f97316' }}>{l.nb_changes}</span>
|
||||
: <span style={{ color: 'var(--text-muted)' }}>0</span>
|
||||
}
|
||||
</td>
|
||||
<td style={{ maxWidth: 280, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={l.details || ''}>
|
||||
{l.details || <span style={{ color: 'var(--text-muted)' }}>—</span>}
|
||||
</td>
|
||||
<td style={{ color: '#ef4444', fontSize: 12 }}>
|
||||
{l.error_msg || <span style={{ color: 'var(--text-muted)' }}>—</span>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{pages > 1 && (
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 16, alignItems: 'center' }}>
|
||||
<button className="btn btn-sm btn-outline" disabled={page === 0} onClick={() => setPage(p => p - 1)}>← Précédent</button>
|
||||
<span style={{ fontSize: 13, color: 'var(--text-muted)' }}>Page {page + 1} / {pages}</span>
|
||||
<button className="btn btn-sm btn-outline" disabled={page >= pages - 1} onClick={() => setPage(p => p + 1)}>Suivant →</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user