Files
dipl-edr/frontend/src/components/Overview.tsx
T
kilyabin 42535528eb feat: redesign Overview with active threats, severity donut, top processes bar
- Add /api/active-anomalies endpoint — latest snapshot per PID where is_anomaly=1
- Active threats card with pulsing indicator, CPU/RAM/TCP per process
- Alerts by severity donut chart (high/medium/low)
- Top processes horizontal bar chart weighted by alert count
2026-05-28 14:43:03 +04:00

336 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useQuery } from '@tanstack/react-query'
import { Activity, AlertTriangle, Bell, Cpu, Database, ShieldAlert } from 'lucide-react'
import {
PieChart,
Pie,
Cell,
Tooltip as ChartTooltip,
BarChart,
Bar,
XAxis,
YAxis,
ResponsiveContainer,
} from 'recharts'
import { api, type Alert, type ProcessMetric } from '@/lib/api'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { SystemChart } from '@/components/SystemChart'
// ── shared tooltip style ──────────────────────────────────────────────────────
const TOOLTIP_STYLE = {
contentStyle: {
background: '#111118',
border: '1px solid #00ff9d33',
borderRadius: 6,
fontSize: 12,
fontFamily: 'JetBrains Mono, monospace',
},
labelStyle: { color: '#6b7280' },
}
// ── stat card ─────────────────────────────────────────────────────────────────
function StatCard({
title,
value,
icon: Icon,
accent = '#00ff9d',
}: {
title: string
value: string | number
icon: React.ElementType
accent?: string
}) {
return (
<Card className="relative overflow-hidden">
<div
className="absolute inset-0 opacity-5"
style={{ background: `radial-gradient(circle at top right, ${accent}, transparent 70%)` }}
/>
<CardHeader className="pb-2">
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-end justify-between">
<span className="text-3xl font-bold" style={{ color: accent }}>
{value}
</span>
<Icon className="w-8 h-8 opacity-20" style={{ color: accent }} />
</div>
</CardContent>
</Card>
)
}
// ── active anomalies list ─────────────────────────────────────────────────────
function ActiveAnomaliesList({ procs }: { procs: ProcessMetric[] }) {
return (
<Card className="flex flex-col">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<ShieldAlert className="w-4 h-4 text-red-500" />
Активные угрозы
</CardTitle>
{procs.length > 0 && (
<Badge variant="destructive" className="text-xs">
{procs.length}
</Badge>
)}
</div>
</CardHeader>
<CardContent className="flex-1 overflow-auto p-0">
{procs.length === 0 ? (
<div className="flex flex-col items-center justify-center h-40 gap-2 text-muted-foreground text-sm">
<span className="text-[#00ff9d] text-2xl"></span>
Аномалий не обнаружено
</div>
) : (
<ul className="divide-y divide-white/5">
{procs.map((p) => (
<li key={p.pid} className="flex items-start gap-3 px-5 py-3">
<span className="mt-1.5 w-2 h-2 rounded-full bg-red-500 shadow-[0_0_6px_#ff3366] shrink-0 animate-pulse" />
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<span className="font-mono text-sm truncate">{p.name}</span>
<span className="font-mono text-xs text-muted-foreground shrink-0">
PID {p.pid}
</span>
</div>
<div className="flex gap-3 mt-1 text-xs font-mono text-muted-foreground">
<span style={{ color: '#00ff9d' }}>CPU {p.cpu_percent}%</span>
<span style={{ color: '#00d4ff' }}>RAM {Math.round(p.memory_mb)} МБ</span>
{p.connections > 0 && (
<span style={{ color: '#a78bfa' }}>{p.connections} TCP</span>
)}
</div>
</div>
</li>
))}
</ul>
)}
</CardContent>
</Card>
)
}
// ── severity donut ────────────────────────────────────────────────────────────
const SEV_META = [
{ key: 'high', label: 'Высокий', color: '#ff3366' },
{ key: 'medium', label: 'Средний', color: '#ff6b35' },
{ key: 'low', label: 'Низкий', color: '#00d4ff' },
] as const
function SeverityDonut({ alerts }: { alerts: Alert[] }) {
const counts = { high: 0, medium: 0, low: 0 }
for (const a of alerts) counts[a.severity] = (counts[a.severity] || 0) + 1
const data = SEV_META.map((m) => ({ name: m.label, value: counts[m.key], color: m.color }))
.filter((d) => d.value > 0)
const renderLegend = () => (
<ul className="flex flex-col gap-1.5 pl-2">
{data.map((d) => (
<li key={d.name} className="flex items-center gap-2 text-xs font-mono">
<span className="w-2.5 h-2.5 rounded-sm shrink-0" style={{ background: d.color }} />
<span className="text-muted-foreground">{d.name}</span>
<span className="ml-auto" style={{ color: d.color }}>{d.value}</span>
</li>
))}
</ul>
)
return (
<Card>
<CardHeader className="pb-2">
<CardTitle>Алерты по серьёзности</CardTitle>
</CardHeader>
<CardContent>
{data.length === 0 ? (
<div className="flex items-center justify-center h-44 text-muted-foreground text-sm">
Алертов нет
</div>
) : (
<div className="flex items-center gap-4">
<ResponsiveContainer width="60%" height={180}>
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius={48}
outerRadius={72}
paddingAngle={3}
dataKey="value"
stroke="none"
>
{data.map((d) => (
<Cell key={d.name} fill={d.color} />
))}
</Pie>
<ChartTooltip
{...TOOLTIP_STYLE}
formatter={(v: number, n: string) => [v, n]}
/>
</PieChart>
</ResponsiveContainer>
<div className="flex-1">{renderLegend()}</div>
</div>
)}
</CardContent>
</Card>
)
}
// ── top processes bar ─────────────────────────────────────────────────────────
function TopProcessesBar({ alerts }: { alerts: Alert[] }) {
const counts: Record<string, number> = {}
for (const a of alerts) {
counts[a.process_name] = (counts[a.process_name] || 0) + (a.count ?? 1)
}
const data = Object.entries(counts)
.sort(([, a], [, b]) => b - a)
.slice(0, 6)
.map(([name, value]) => ({ name, value }))
return (
<Card>
<CardHeader className="pb-2">
<CardTitle>Топ процессов по алертам</CardTitle>
</CardHeader>
<CardContent>
{data.length === 0 ? (
<div className="flex items-center justify-center h-44 text-muted-foreground text-sm">
Нет данных
</div>
) : (
<ResponsiveContainer width="100%" height={Math.max(180, data.length * 36)}>
<BarChart
data={data}
layout="vertical"
margin={{ top: 4, right: 32, left: 8, bottom: 4 }}
>
<XAxis
type="number"
tick={{ fontSize: 10, fill: '#6b7280', fontFamily: 'JetBrains Mono, monospace' }}
tickLine={false}
axisLine={{ stroke: '#ffffff18' }}
/>
<YAxis
type="category"
dataKey="name"
width={100}
tick={{ fontSize: 11, fill: '#e5e7eb', fontFamily: 'JetBrains Mono, monospace' }}
tickLine={false}
axisLine={false}
/>
<ChartTooltip
{...TOOLTIP_STYLE}
formatter={(v: number) => [v, 'алертов']}
/>
<Bar dataKey="value" fill="#ff6b35" radius={[0, 3, 3, 0]} maxBarSize={18} />
</BarChart>
</ResponsiveContainer>
)}
</CardContent>
</Card>
)
}
// ── main component ────────────────────────────────────────────────────────────
export function Overview() {
const { data: stats } = useQuery({
queryKey: ['stats'],
queryFn: api.stats,
refetchInterval: 5_000,
})
const { data: sysRows } = useQuery({
queryKey: ['system-metrics'],
queryFn: () => api.systemMetrics(60),
refetchInterval: 5_000,
})
const { data: activeProcs } = useQuery({
queryKey: ['active-anomalies'],
queryFn: api.activeAnomalies,
refetchInterval: 5_000,
})
const { data: alerts } = useQuery({
queryKey: ['alerts'],
queryFn: () => api.alerts(200),
refetchInterval: 10_000,
})
const chartData = (sysRows ?? []).map((r) => ({
t: r.timestamp.slice(11, 19),
cpu: Math.round(r.cpu_percent * 10) / 10,
ram: Math.round(r.ram_percent * 10) / 10,
}))
return (
<div className="space-y-4">
{/* Stat cards */}
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-5 gap-4">
<StatCard
title="Всего процессов"
value={stats?.total_processes ?? '—'}
icon={Activity}
accent="#00d4ff"
/>
<StatCard
title="Аномалий сегодня"
value={stats?.anomalies_today ?? '—'}
icon={AlertTriangle}
accent="#ff6b35"
/>
<StatCard
title="Алертов сегодня"
value={stats?.alerts_today ?? '—'}
icon={Bell}
accent="#ff3366"
/>
<StatCard
title="Средний CPU"
value={stats ? `${stats.cpu_avg}%` : '—'}
icon={Cpu}
accent="#00ff9d"
/>
<StatCard
title="Средний RAM"
value={stats ? `${stats.ram_avg}%` : '—'}
icon={Database}
accent="#a78bfa"
/>
</div>
{/* Active anomalies + system chart */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<ActiveAnomaliesList procs={activeProcs ?? []} />
<Card className="lg:col-span-2">
<CardHeader className="pb-2">
<CardTitle>Нагрузка системы реальное время (60 точек)</CardTitle>
</CardHeader>
<CardContent>
<SystemChart data={chartData} />
</CardContent>
</Card>
</div>
{/* Severity donut + top processes */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<SeverityDonut alerts={alerts ?? []} />
<div className="lg:col-span-2">
<TopProcessesBar alerts={alerts ?? []} />
</div>
</div>
</div>
)
}