Files
MikrotikManager/app/(main)/dashboard/page.tsx
T
2026-05-02 01:17:08 +07:00

243 lines
12 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 { PageHeader } from "@/components/page-header"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { StatusDot } from "@/components/status-dot"
import { StatusBadge } from "@/components/status-badge"
import { Sparkline } from "@/components/sparkline"
import { LatencyChart } from "@/components/dashboard/latency-chart"
import { BandwidthChart } from "@/components/dashboard/bandwidth-chart"
import { servers, pingProbes, systemEvents, dashLatency, traffic } from "@/lib/data"
import { Flag } from "@/components/flag"
import { AlertCircleIcon, AlertTriangleIcon, InfoIcon, FilterIcon, DownloadIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
function StatCard({
label, value, unit, delta, deltaDir, spark, sparkColor,
}: {
label: string; value: string; unit?: string; delta?: string
deltaDir?: "up" | "down"; spark?: number[]; sparkColor?: string
}) {
return (
<Card className="relative overflow-hidden">
<CardContent className="pt-5 pb-4 px-5">
<p className="text-sm font-medium text-muted-foreground">{label}</p>
<div className="flex items-baseline gap-1.5 mt-1">
<span className="text-3xl font-semibold tracking-tight tabular-nums">{value}</span>
{unit && <span className="text-sm text-muted-foreground">{unit}</span>}
</div>
{delta && (
<p className={`text-xs mt-1 flex items-center gap-1 ${deltaDir === "up" ? "text-emerald-600 dark:text-emerald-400" : deltaDir === "down" ? "text-red-500 dark:text-red-400" : "text-muted-foreground"}`}>
{delta}
</p>
)}
{spark && spark.length > 1 && (
<div className="absolute right-4 bottom-4 opacity-60">
<Sparkline data={spark} width={80} height={32} color={sparkColor ?? "currentColor"} filled />
</div>
)}
</CardContent>
</Card>
)
}
export default function DashboardPage() {
return (
<div className="flex flex-col h-full">
<PageHeader
crumbs={[{ label: "Обзор" }, { label: "Дашборд" }]}
actions={
<>
<Button variant="outline" size="sm"><DownloadIcon className="size-4" />Экспорт</Button>
</>
}
/>
<div className="flex-1 overflow-y-auto">
<div className="p-6 flex flex-col gap-6">
{/* KPI row */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
label="Серверы онлайн" value="6" unit="/ 7"
delta="+1 со вчерашнего" deltaDir="up"
spark={[5,5,6,6,5,6,6,6,7,6,6,6,6,6,6,6,6]}
sparkColor="var(--chart-line-1)"
/>
<StatCard
label="Активные фильтры" value="5" unit="/ 6"
delta="2.4к доменов синхронизировано" deltaDir="up"
spark={[3,4,4,5,4,5,5,5,5,5,5,5,5,5,5]}
sparkColor="var(--chart-line-2)"
/>
<StatCard
label="BGP-префиксы" value="8 432"
delta="+412 в последнем обновлении" deltaDir="up"
spark={[7800,7900,8000,8100,8050,8120,8200,8240,8300,8350,8380,8400,8420,8430,8432]}
sparkColor="var(--chart-line-4)"
/>
<StatCard
label="Активные алерты" value="4"
delta="2 критических · 2 предупреждения" deltaDir="down"
spark={[1,2,2,3,3,4,5,4,4,4,3,3,4,4,4]}
sparkColor="var(--chart-5)"
/>
</div>
{/* Latency chart + Events */}
<div className="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-4">
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base">Задержка до серверов</CardTitle>
<p className="text-sm text-muted-foreground mt-0.5">Последние 60 минут · ping от монитора серверы MikroTik</p>
</div>
</div>
</CardHeader>
<CardContent className="pt-0 px-3">
<LatencyChart series={dashLatency} />
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base">Последние события</CardTitle>
<Button variant="ghost" size="sm" className="text-xs h-7">Все </Button>
</div>
<p className="text-sm text-muted-foreground">Система и BGP-активность</p>
</CardHeader>
<CardContent className="pt-0 px-0">
<div className="divide-y divide-border">
{systemEvents.map((e) => (
<div key={e.id} className="grid grid-cols-[20px_1fr_auto] gap-3 px-5 py-3 items-start">
<div className="mt-0.5">
{e.sev === "destructive" && <AlertCircleIcon className="size-4 text-red-500" />}
{e.sev === "warning" && <AlertTriangleIcon className="size-4 text-amber-500" />}
{e.sev === "info" && <InfoIcon className="size-4 text-blue-500" />}
</div>
<div>
<p className="text-[13px] font-medium leading-tight">{e.title}</p>
<p className="text-xs text-muted-foreground mt-0.5 leading-snug">{e.message}</p>
</div>
<span className="text-[11px] font-mono text-muted-foreground">{e.when}</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Bandwidth + Server status */}
<div className="grid grid-cols-1 lg:grid-cols-[3fr_2fr] gap-4">
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base">Пропускная способность</CardTitle>
<p className="text-sm text-muted-foreground mt-0.5">Суммарный RX / TX по всем серверам</p>
</div>
<div className="flex items-center gap-3 text-xs">
<span className="flex items-center gap-1.5"><span className="w-3 h-0.5 bg-foreground/80 rounded inline-block" />RX 318 Мбит/с</span>
<span className="flex items-center gap-1.5"><span className="w-3 h-0.5 bg-blue-500 rounded inline-block" />TX 244 Мбит/с</span>
</div>
</div>
</CardHeader>
<CardContent className="pt-0 px-3">
<BandwidthChart rx={traffic.rx} tx={traffic.tx} />
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base">Состояние серверов</CardTitle>
<p className="text-sm text-muted-foreground mt-0.5">7 узлов MikroTik</p>
</div>
<Button variant="outline" size="sm" className="h-7 text-xs">Управление </Button>
</div>
</CardHeader>
<CardContent className="pt-0 px-0">
<div className="divide-y divide-border">
{servers.slice(0, 5).map((s) => (
<div key={s.id} className="grid grid-cols-[10px_1fr_auto_auto] gap-3 px-5 py-2.5 items-center">
<StatusDot status={s.status} pulse />
<div>
<p className="text-[13px] font-medium leading-tight">{s.name}</p>
<p className="text-[11px] font-mono text-muted-foreground flex items-center gap-1">
<Flag code={s.country} className="not-mono" />{s.host} · {s.site} · {s.asn}
</p>
</div>
<span className={`text-xs font-mono ${s.latency == null ? "text-red-500" : s.latency > 60 ? "text-amber-500" : "text-muted-foreground"}`}>
{s.latency == null ? "недоступен" : `${s.latency}мс`}
</span>
<StatusBadge status={s.status} />
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Ping probes table */}
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base">Активные пробы</CardTitle>
<p className="text-sm text-muted-foreground mt-0.5">8 ping-зондов · опрос раз в секунду</p>
</div>
<Button variant="outline" size="sm" className="h-7 text-xs">Открыть монитор </Button>
</div>
</CardHeader>
<CardContent className="pt-0 px-0">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border text-xs text-muted-foreground">
<th className="text-left font-medium px-5 py-2.5">Проба</th>
<th className="text-left font-medium px-4 py-2.5">Цель</th>
<th className="text-left font-medium px-4 py-2.5">Фильтр</th>
<th className="text-right font-medium px-4 py-2.5">RTT</th>
<th className="text-right font-medium px-4 py-2.5">Потери</th>
<th className="text-left font-medium px-4 py-2.5 w-36">60с</th>
<th className="text-left font-medium px-4 py-2.5">Статус</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{pingProbes.map((p) => {
const sparkColor = p.status === "down" ? "hsl(0 84% 60%)" : p.status === "warn" ? "hsl(32 94% 44%)" : "hsl(142 76% 36%)"
return (
<tr key={p.id} className="hover:bg-muted/40 transition-colors">
<td className="px-5 py-2.5 font-medium">{p.name}</td>
<td className="px-4 py-2.5 font-mono text-xs text-muted-foreground">{p.target}</td>
<td className="px-4 py-2.5">
<span className="inline-flex items-center gap-1 text-xs border border-border rounded px-2 py-0.5">
<FilterIcon className="size-3 text-muted-foreground" />{p.filter}
</span>
</td>
<td className="px-4 py-2.5 font-mono text-right">{p.rtt == null ? "—" : `${p.rtt} мс`}</td>
<td className={`px-4 py-2.5 font-mono text-right ${p.loss > 5 ? "text-red-500" : p.loss > 0 ? "text-amber-500" : "text-muted-foreground"}`}>
{p.loss}%
</td>
<td className="px-4 py-2.5">
<Sparkline data={p.series} width={120} height={24} color={sparkColor} />
</td>
<td className="px-4 py-2.5">
<StatusBadge status={p.status === "up" ? "online" : p.status === "warn" ? "degraded" : "offline"} />
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
)
}