feat(clustering): animation de calcul + scroll indépendant des colonnes
Layout fix (App.tsx): - Création de MainContent avec useLocation pour layout adaptatif - Route /clustering : main sans padding, overflow-hidden, height=calc(100vh-3.5rem) → ClusteringView remplit exactement la fenêtre, colonnes scrollables indépendamment - Autres routes : comportement inchangé (px-6 py-5 overflow-auto) Animation de calcul (ClusteringView.tsx): - Overlay absolu z-20 sur le canvas pendant computing || loading - 2 anneaux concentriques contra-rotatifs (accent-primary + blue-500) - 8 noeuds orbitaux avec animate-ping colorés selon taxonomie menace - Emoji 🔬 pulsant au centre - Texte : 'Clustering en cours…' + détails (31 features, toutes les IPs) - Mise à jour toutes les 3s (texte animé) Scroll indépendant: - Panneau gauche : style height:100% explicite - Sidebar droite : style height:100% explicite - Canvas : overflow-hidden ajouté - La main a overflow-hidden → les colonnes scrollent sans bouger les voisines Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@ -321,6 +321,57 @@ function RouteTracker() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── MainContent : layout adaptatif selon la route ───────────────────────────
|
||||
// Les vues "canvas" ont besoin d'une hauteur fixe sans padding
|
||||
// pour que leurs colonnes scroll indépendamment.
|
||||
const FULLHEIGHT_ROUTES = ['/clustering'];
|
||||
|
||||
function MainContent({ counts: _counts }: { counts: AlertCounts | null }) {
|
||||
const location = useLocation();
|
||||
const isFullHeight = FULLHEIGHT_ROUTES.some(r => location.pathname.startsWith(r));
|
||||
|
||||
if (isFullHeight) {
|
||||
return (
|
||||
<main className="mt-14 overflow-hidden" style={{ height: 'calc(100vh - 3.5rem)' }}>
|
||||
<Routes>
|
||||
<Route path="/clustering" element={<ClusteringView />} />
|
||||
</Routes>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex-1 px-6 py-5 mt-14 overflow-auto">
|
||||
<Routes>
|
||||
<Route path="/" element={<IncidentsView />} />
|
||||
<Route path="/incidents" element={<IncidentsView />} />
|
||||
<Route path="/pivot" element={<PivotView />} />
|
||||
<Route path="/fingerprints" element={<FingerprintsView />} />
|
||||
<Route path="/campaigns" element={<CampaignsView />} />
|
||||
<Route path="/threat-intel" element={<ThreatIntelView />} />
|
||||
<Route path="/bruteforce" element={<BruteForceView />} />
|
||||
<Route path="/tcp-spoofing" element={<TcpSpoofingView />} />
|
||||
<Route path="/headers" element={<HeaderFingerprintView />} />
|
||||
<Route path="/heatmap" element={<Navigate to="/" replace />} />
|
||||
<Route path="/botnets" element={<Navigate to="/campaigns" replace />} />
|
||||
<Route path="/rotation" element={<Navigate to="/fingerprints" replace />} />
|
||||
<Route path="/ml-features" element={<MLFeaturesView />} />
|
||||
<Route path="/detections" element={<DetectionsList />} />
|
||||
<Route path="/detections/:type/:value" element={<DetailsView />} />
|
||||
<Route path="/investigate" element={<DetectionsList />} />
|
||||
<Route path="/investigate/:type/:value" element={<InvestigateRoute />} />
|
||||
<Route path="/investigation/ja4/:ja4" element={<JA4InvestigationView />} />
|
||||
<Route path="/investigation/:ip" element={<InvestigationView />} />
|
||||
<Route path="/entities/subnet/:subnet" element={<SubnetInvestigation />} />
|
||||
<Route path="/entities/:type/:value" element={<EntityInvestigationView />} />
|
||||
<Route path="/bulk-classify" element={<BulkClassificationRoute />} />
|
||||
<Route path="/tools/correlation-graph/:ip" element={<CorrelationGraphRoute />} />
|
||||
<Route path="/tools/timeline/:ip?" element={<TimelineRoute />} />
|
||||
</Routes>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── App ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function App() {
|
||||
@ -362,36 +413,8 @@ export default function App() {
|
||||
{/* Fixed top header */}
|
||||
<TopHeader counts={counts} />
|
||||
|
||||
{/* Scrollable page content */}
|
||||
<main className="flex-1 px-6 py-5 mt-14 overflow-auto">
|
||||
<Routes>
|
||||
<Route path="/" element={<IncidentsView />} />
|
||||
<Route path="/incidents" element={<IncidentsView />} />
|
||||
<Route path="/pivot" element={<PivotView />} />
|
||||
<Route path="/fingerprints" element={<FingerprintsView />} />
|
||||
<Route path="/campaigns" element={<CampaignsView />} />
|
||||
<Route path="/threat-intel" element={<ThreatIntelView />} />
|
||||
<Route path="/bruteforce" element={<BruteForceView />} />
|
||||
<Route path="/tcp-spoofing" element={<TcpSpoofingView />} />
|
||||
<Route path="/clustering" element={<ClusteringView />} />
|
||||
<Route path="/headers" element={<HeaderFingerprintView />} />
|
||||
<Route path="/heatmap" element={<Navigate to="/" replace />} />
|
||||
<Route path="/botnets" element={<Navigate to="/campaigns" replace />} />
|
||||
<Route path="/rotation" element={<Navigate to="/fingerprints" replace />} />
|
||||
<Route path="/ml-features" element={<MLFeaturesView />} />
|
||||
<Route path="/detections" element={<DetectionsList />} />
|
||||
<Route path="/detections/:type/:value" element={<DetailsView />} />
|
||||
<Route path="/investigate" element={<DetectionsList />} />
|
||||
<Route path="/investigate/:type/:value" element={<InvestigateRoute />} />
|
||||
<Route path="/investigation/ja4/:ja4" element={<JA4InvestigationView />} />
|
||||
<Route path="/investigation/:ip" element={<InvestigationView />} />
|
||||
<Route path="/entities/subnet/:subnet" element={<SubnetInvestigation />} />
|
||||
<Route path="/entities/:type/:value" element={<EntityInvestigationView />} />
|
||||
<Route path="/bulk-classify" element={<BulkClassificationRoute />} />
|
||||
<Route path="/tools/correlation-graph/:ip" element={<CorrelationGraphRoute />} />
|
||||
<Route path="/tools/timeline/:ip?" element={<TimelineRoute />} />
|
||||
</Routes>
|
||||
</main>
|
||||
{/* Page content — full-height sans padding pour les vues canvas */}
|
||||
<MainContent counts={counts} />
|
||||
</div>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
|
||||
@ -330,8 +330,8 @@ export default function ClusteringView() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full overflow-hidden bg-background text-text-primary">
|
||||
{/* ── Panneau gauche ── */}
|
||||
<div className="flex flex-col w-72 flex-shrink-0 border-r border-gray-700 overflow-y-auto p-4 gap-4 z-10">
|
||||
{/* ── Panneau gauche (scroll indépendant) ── */}
|
||||
<div className="flex flex-col w-72 flex-shrink-0 border-r border-gray-700 overflow-y-auto p-4 gap-4 z-10" style={{ height: '100%' }}>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold mb-1">🔬 Clustering IPs</h2>
|
||||
<p className="text-xs text-text-secondary">Rendu WebGL · K-means++ sur toutes les IPs</p>
|
||||
@ -440,12 +440,55 @@ export default function ClusteringView() {
|
||||
</div>
|
||||
|
||||
{/* ── Canvas WebGL (deck.gl) ── */}
|
||||
<div className="flex-1 relative">
|
||||
{!data && !loading && !computing && (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-text-secondary">
|
||||
Cliquez sur <strong className="mx-1 text-white">Recalculer</strong> pour démarrer
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
|
||||
{/* Animation de calcul — remplace le canvas pendant le traitement */}
|
||||
{(computing || loading) && (
|
||||
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-background">
|
||||
{/* Noeuds pulsants animés */}
|
||||
<div className="relative w-56 h-56 mb-2">
|
||||
{/* Anneau tournant */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-32 h-32 rounded-full border-2 border-accent-primary/20 border-t-accent-primary animate-spin" style={{ animationDuration: '1.4s' }} />
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-20 h-20 rounded-full border-2 border-blue-500/20 border-b-blue-500/80 animate-spin" style={{ animationDuration: '2.1s', animationDirection: 'reverse' }} />
|
||||
</div>
|
||||
{/* Noeuds orbitaux représentant les clusters */}
|
||||
{([0,1,2,3,4,5,6,7] as const).map((i) => {
|
||||
const angle = (i / 8) * 2 * Math.PI;
|
||||
const r = 88;
|
||||
const x = 50 + (r / 1.12) * Math.cos(angle);
|
||||
const y = 50 + (r / 1.12) * Math.sin(angle);
|
||||
const colors = ['#dc2626','#f97316','#eab308','#22c55e','#3b82f6','#8b5cf6','#ec4899','#14b8a6'];
|
||||
return (
|
||||
<div key={i} className="absolute w-3 h-3 rounded-full animate-ping"
|
||||
style={{
|
||||
left: `${x}%`, top: `${y}%`, transform: 'translate(-50%,-50%)',
|
||||
background: colors[i], opacity: 0.75,
|
||||
animationDelay: `${i * 0.18}s`, animationDuration: '1.6s',
|
||||
}} />
|
||||
);
|
||||
})}
|
||||
{/* Centre */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-2xl select-none animate-pulse">🔬</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-white font-semibold text-lg tracking-wide">Clustering en cours…</p>
|
||||
<p className="text-text-secondary text-sm mt-1">K-means++ · 31 features · toutes les IPs</p>
|
||||
<p className="text-text-disabled text-xs mt-2 animate-pulse">Mise à jour automatique toutes les 3 secondes</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Message état vide */}
|
||||
{!data && !loading && !computing && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 text-text-secondary">
|
||||
<span className="text-4xl">🔬</span>
|
||||
<span>Cliquez sur <strong className="text-white">Recalculer</strong> pour démarrer</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DeckGL
|
||||
views={new OrthographicView({ id: 'ortho', controller: true })}
|
||||
viewState={viewState}
|
||||
@ -528,7 +571,7 @@ function ClusterSidebar({ node, ipDetails, ipTotal, ipPage, clusterPoints, onClo
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-96 flex-shrink-0 border-l border-gray-700 bg-background-secondary flex flex-col overflow-hidden">
|
||||
<div className="w-96 flex-shrink-0 border-l border-gray-700 bg-background-secondary flex flex-col overflow-hidden" style={{ height: '100%' }}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user