maj cumulative
This commit is contained in:
@ -42,8 +42,10 @@ export function DetectionsList() {
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
const modelName = searchParams.get('model_name') || undefined;
|
||||
const search = searchParams.get('search') || undefined;
|
||||
const sortField = (searchParams.get('sort_by') || searchParams.get('sort') || 'anomaly_score') as SortField;
|
||||
const sortOrder = (searchParams.get('sort_order') || searchParams.get('order') || 'asc') as SortOrder;
|
||||
const sortField = (searchParams.get('sort_by') || searchParams.get('sort') || 'detected_at') as SortField;
|
||||
const sortOrder = (searchParams.get('sort_order') || searchParams.get('order') || 'desc') as SortOrder;
|
||||
|
||||
const [groupByIP, setGroupByIP] = useState(true);
|
||||
|
||||
const { data, loading, error } = useDetections({
|
||||
page,
|
||||
@ -52,13 +54,11 @@ export function DetectionsList() {
|
||||
search,
|
||||
sort_by: sortField,
|
||||
sort_order: sortOrder,
|
||||
group_by_ip: groupByIP,
|
||||
});
|
||||
|
||||
const [searchInput, setSearchInput] = useState(search || '');
|
||||
const [showColumnSelector, setShowColumnSelector] = useState(false);
|
||||
const [groupByIP, setGroupByIP] = useState(true); // Grouper par IP par défaut
|
||||
|
||||
// Configuration des colonnes
|
||||
const [columns, setColumns] = useState<ColumnConfig[]>([
|
||||
{ key: 'ip_ja4', label: 'IP / JA4', visible: true, sortable: true },
|
||||
{ key: 'host', label: 'Host', visible: true, sortable: true },
|
||||
@ -107,6 +107,14 @@ export function DetectionsList() {
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
const handleSort = (key: string, dir: 'asc' | 'desc') => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set('sort_by', key);
|
||||
newParams.set('sort_order', dir);
|
||||
newParams.set('page', '1');
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
@ -125,59 +133,8 @@ export function DetectionsList() {
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
// Traiter les données pour le regroupement par IP
|
||||
const processedData = (() => {
|
||||
if (!groupByIP) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Grouper par IP
|
||||
const ipGroups = new Map<string, typeof data.items[0]>();
|
||||
const ipStats = new Map<string, {
|
||||
first: Date;
|
||||
last: Date;
|
||||
count: number;
|
||||
ja4s: Set<string>;
|
||||
hosts: Set<string>;
|
||||
clientHeaders: Set<string>;
|
||||
}>();
|
||||
|
||||
data.items.forEach(item => {
|
||||
if (!ipGroups.has(item.src_ip)) {
|
||||
ipGroups.set(item.src_ip, item);
|
||||
ipStats.set(item.src_ip, {
|
||||
first: new Date(item.detected_at),
|
||||
last: new Date(item.detected_at),
|
||||
count: 1,
|
||||
ja4s: new Set([item.ja4 || '']),
|
||||
hosts: new Set([item.host || '']),
|
||||
clientHeaders: new Set([item.client_headers || ''])
|
||||
});
|
||||
} else {
|
||||
const stats = ipStats.get(item.src_ip)!;
|
||||
const itemDate = new Date(item.detected_at);
|
||||
if (itemDate < stats.first) stats.first = itemDate;
|
||||
if (itemDate > stats.last) stats.last = itemDate;
|
||||
stats.count++;
|
||||
if (item.ja4) stats.ja4s.add(item.ja4);
|
||||
if (item.host) stats.hosts.add(item.host);
|
||||
if (item.client_headers) stats.clientHeaders.add(item.client_headers);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...data,
|
||||
items: Array.from(ipGroups.values()).map(item => ({
|
||||
...item,
|
||||
hits: ipStats.get(item.src_ip)!.count,
|
||||
first_seen: ipStats.get(item.src_ip)!.first.toISOString(),
|
||||
last_seen: ipStats.get(item.src_ip)!.last.toISOString(),
|
||||
unique_ja4s: Array.from(ipStats.get(item.src_ip)!.ja4s),
|
||||
unique_hosts: Array.from(ipStats.get(item.src_ip)!.hosts),
|
||||
unique_client_headers: Array.from(ipStats.get(item.src_ip)!.clientHeaders)
|
||||
}))
|
||||
};
|
||||
})();
|
||||
// Backend handles grouping — data is already grouped when groupByIP=true
|
||||
const processedData = data;
|
||||
|
||||
// Build DataTable columns from visible column configs
|
||||
const tableColumns: Column<DetectionRow>[] = columns
|
||||
@ -352,20 +309,25 @@ export function DetectionsList() {
|
||||
label: col.label,
|
||||
sortable: true,
|
||||
render: (_, row) =>
|
||||
groupByIP && row.first_seen ? (
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-text-secondary">
|
||||
<span className="font-medium">Premier:</span>{' '}
|
||||
{new Date(row.first_seen).toLocaleDateString('fr-FR')}{' '}
|
||||
{new Date(row.first_seen).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
||||
groupByIP && row.first_seen ? (() => {
|
||||
const first = new Date(row.first_seen!);
|
||||
const last = new Date(row.last_seen!);
|
||||
const sameTime = first.getTime() === last.getTime();
|
||||
const fmt = (d: Date) =>
|
||||
`${d.toLocaleDateString('fr-FR')} ${d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`;
|
||||
return sameTime ? (
|
||||
<div className="text-xs text-text-secondary">{fmt(last)}</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-text-secondary">
|
||||
<span className="font-medium">Premier:</span> {fmt(first)}
|
||||
</div>
|
||||
<div className="text-xs text-text-secondary">
|
||||
<span className="font-medium">Dernier:</span> {fmt(last)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-text-secondary">
|
||||
<span className="font-medium">Dernier:</span>{' '}
|
||||
{new Date(row.last_seen!).toLocaleDateString('fr-FR')}{' '}
|
||||
{new Date(row.last_seen!).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
);
|
||||
})() : (
|
||||
<>
|
||||
<div className="text-sm text-text-primary">
|
||||
{new Date(row.detected_at).toLocaleDateString('fr-FR')}
|
||||
@ -388,7 +350,7 @@ export function DetectionsList() {
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="text-2xl font-bold text-text-primary">Détections</h1>
|
||||
<div className="flex items-center gap-2 text-sm text-text-secondary">
|
||||
<span>{groupByIP ? processedData.items.length : data.items.length}</span>
|
||||
<span>{data.items.length}</span>
|
||||
<span>→</span>
|
||||
<span>{data.total} détections</span>
|
||||
</div>
|
||||
@ -403,8 +365,9 @@ export function DetectionsList() {
|
||||
? 'bg-accent-primary text-white border-accent-primary'
|
||||
: 'bg-background-card text-text-secondary border-background-card hover:text-text-primary'
|
||||
}`}
|
||||
title={groupByIP ? 'Passer en vue détections individuelles' : 'Passer en vue groupée par IP'}
|
||||
>
|
||||
{groupByIP ? '⊟ Détections individuelles' : '⊞ Grouper par IP'}
|
||||
{groupByIP ? '⊞ Vue : Groupé par IP' : '⊟ Vue : Individuelle'}
|
||||
</button>
|
||||
|
||||
{/* Sélecteur de colonnes */}
|
||||
@ -486,7 +449,9 @@ export function DetectionsList() {
|
||||
data={processedData.items as DetectionRow[]}
|
||||
columns={tableColumns}
|
||||
rowKey={(row) => `${row.src_ip}-${row.detected_at}-${groupByIP ? 'g' : 'i'}`}
|
||||
defaultSortKey="anomaly_score"
|
||||
defaultSortKey={sortField}
|
||||
defaultSortDir={sortOrder}
|
||||
onSort={handleSort}
|
||||
onRowClick={(row) => navigate(`/detections/ip/${encodeURIComponent(row.src_ip)}`)}
|
||||
emptyMessage="Aucune détection trouvée"
|
||||
compact
|
||||
|
||||
Reference in New Issue
Block a user