Files
MikrotikManager/components/data-grids/snapshot-data-grid.tsx
T
Denozordec d3a2d38b37
Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m37s
Docker images / frontend-image (push) Successful in 1m50s
Docker images / updater-image (push) Successful in 44s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 7s
fix(ui): заменить таблицы на карточки данных и улучшить функциональность поиска
2026-06-30 22:22:51 +07:00

499 lines
15 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.
"use client"
import { Badge } from "@/components/ui/badge"
import { cn } from "@/lib/utils"
import {
CompactDataGrid,
type CompactDataGridColumn,
type CompactDataGridProps,
} from "@/components/data-grids/compact-data-grid"
import type {
AlertEngineRuleDiagSnapshot,
PingProbeSnapshot,
ResourceServerSnapshot,
ServerRestPingSnapshot,
SpeedRunSnapshot,
TrafficServerSnapshot,
} from "@/lib/scheduler-run-snapshot"
type SnapshotRow = { id: string }
function withRowId<T extends { serverId?: number; probeId?: string; ruleId?: string; target?: string }>(
rows: T[],
idFn: (row: T, index: number) => string,
): (T & SnapshotRow)[] {
return rows.map((row, index) => ({ ...row, id: idFn(row, index) }))
}
function SnapshotOkBadge({
ok,
okLabel = "ok",
errLabel = "ошибка",
}: {
ok: boolean
okLabel?: string
errLabel?: string
}) {
return ok ? (
<Badge variant="outline" className="text-[10px] border-emerald-500/40 text-emerald-700 dark:text-emerald-400">
{okLabel}
</Badge>
) : (
<Badge variant="outline" className="text-[10px] border-destructive/50 text-destructive">
{errLabel}
</Badge>
)
}
function SnapshotErrorCell({ error, className }: { error?: string; className?: string }) {
return (
<span
className={cn("text-destructive max-w-[220px] truncate block", className)}
title={error}
>
{error ?? "—"}
</span>
)
}
function SnapshotDataGrid<T extends SnapshotRow>(
props: Omit<CompactDataGridProps<T>, "compact">,
) {
return <CompactDataGrid {...props} compact />
}
function TrafficSnapshotGrid({ servers }: { servers: TrafficServerSnapshot[] }) {
const data = withRowId(servers, (s) => String(s.serverId))
const columns: CompactDataGridColumn<(typeof data)[number]>[] = [
{ id: "name", header: "Сервер", accessorKey: "name", cell: (r) => <span className="font-medium">{r.name}</span> },
{
id: "host",
header: "Хост",
accessorKey: "host",
cell: (r) => <span className="font-mono text-muted-foreground">{r.host}</span>,
},
{
id: "ok",
header: "Результат",
enableSorting: false,
cell: (r) => <SnapshotOkBadge ok={r.ok} />,
},
{
id: "interfaces",
header: "IF",
accessorKey: "interfaces",
headerClassName: "text-right",
cellClassName: "text-right tabular-nums",
cell: (r) => r.interfaces ?? "—",
},
{
id: "sumRxMbps",
header: "Σ RX",
headerClassName: "text-right",
cellClassName: "text-right tabular-nums",
cell: (r) => (r.sumRxMbps != null ? `${r.sumRxMbps} Мбит/с` : "—"),
},
{
id: "sumTxMbps",
header: "Σ TX",
headerClassName: "text-right",
cellClassName: "text-right tabular-nums",
cell: (r) => (r.sumTxMbps != null ? `${r.sumTxMbps} Мбит/с` : "—"),
},
{
id: "error",
header: "Ошибка",
enableSorting: false,
cell: (r) => <SnapshotErrorCell error={r.error} />,
},
]
return <SnapshotDataGrid data={data} columns={columns} emptyTitle="Нет сэмплов трафика" />
}
function ResourcesSnapshotGrid({ servers }: { servers: ResourceServerSnapshot[] }) {
const data = withRowId(servers, (s) => String(s.serverId))
const columns: CompactDataGridColumn<(typeof data)[number]>[] = [
{
id: "name",
header: "Сервер",
enableSorting: false,
cell: (r) => (
<div>
<span className="font-medium">{r.name}</span>
<span className="block font-mono text-[10px] text-muted-foreground">{r.host}</span>
</div>
),
},
{
id: "status",
header: "Статус",
enableSorting: false,
cell: (r) => (
<Badge
variant="outline"
className={cn(
"text-[10px]",
r.status === "online" && "border-emerald-500/40 text-emerald-700 dark:text-emerald-400",
r.status === "offline" && "border-destructive/50 text-destructive",
)}
>
{r.status}
</Badge>
),
},
{
id: "cpuLoadPct",
header: "CPU %",
headerClassName: "text-right",
cellClassName: "text-right tabular-nums",
cell: (r) => r.cpuLoadPct ?? "—",
},
{
id: "memory",
header: "Память",
enableSorting: false,
headerClassName: "text-right",
cellClassName: "text-right tabular-nums whitespace-nowrap",
cell: (r) =>
r.memUsedMb != null && r.memTotalMb != null ? `${r.memUsedMb} / ${r.memTotalMb} МБ` : "—",
},
{
id: "memUsedPct",
header: "% RAM",
headerClassName: "text-right",
cellClassName: "text-right tabular-nums",
cell: (r) => (r.memUsedPct != null ? `${r.memUsedPct}%` : "—"),
},
{
id: "disk",
header: "Диск своб.",
enableSorting: false,
headerClassName: "text-right",
cellClassName: "text-right tabular-nums whitespace-nowrap",
cell: (r) =>
r.diskFreeMb != null && r.diskTotalMb != null ? `${r.diskFreeMb} / ${r.diskTotalMb} МБ` : "—",
},
{
id: "uptimeSeconds",
header: "Uptime",
cellClassName: "tabular-nums",
cell: (r) => (r.uptimeSeconds != null ? fmtUptimeSec(r.uptimeSeconds) : "—"),
},
{
id: "board",
header: "Плата / ROS",
enableSorting: false,
cell: (r) => (
<div className="max-w-[140px]">
<span className="block truncate" title={r.boardName}>{r.boardName || "—"}</span>
<span className="block truncate text-muted-foreground font-mono text-[10px]" title={r.rosVersion}>
{r.rosVersion || ""}
</span>
</div>
),
},
{
id: "error",
header: "Ошибка",
enableSorting: false,
cell: (r) => <SnapshotErrorCell error={r.error} className="max-w-[160px]" />,
},
]
return <SnapshotDataGrid data={data} columns={columns} emptyTitle="Нет сэмплов ресурсов" />
}
function ServersRestPingSnapshotGrid({ servers }: { servers: ServerRestPingSnapshot[] }) {
const data = withRowId(servers, (s) => String(s.serverId))
const columns: CompactDataGridColumn<(typeof data)[number]>[] = [
{ id: "name", header: "Сервер", accessorKey: "name", cell: (r) => <span className="font-medium">{r.name}</span> },
{
id: "host",
header: "Хост",
accessorKey: "host",
cell: (r) => <span className="font-mono text-muted-foreground">{r.host}</span>,
},
{
id: "ok",
header: "Результат",
enableSorting: false,
cell: (r) => <SnapshotOkBadge ok={r.ok} errLabel="недоступен" />,
},
{
id: "latencyMs",
header: "RTT REST",
headerClassName: "text-right",
cellClassName: "text-right tabular-nums",
cell: (r) => (r.latencyMs != null ? `${r.latencyMs} мс` : "—"),
},
{
id: "error",
header: "Ошибка",
enableSorting: false,
cell: (r) => <SnapshotErrorCell error={r.error} />,
},
]
return <SnapshotDataGrid data={data} columns={columns} emptyTitle="Нет сэмплов REST ping" />
}
function PingSnapshotGrid({ probes }: { probes: PingProbeSnapshot[] }) {
const data = withRowId(probes, (p) => `${p.probeId}-${p.target}`)
const columns: CompactDataGridColumn<(typeof data)[number]>[] = [
{
id: "name",
header: "Проба",
enableSorting: false,
cell: (r) => (
<div>
<span className="font-medium">{r.name}</span>
<span className="block font-mono text-[10px] text-muted-foreground">{r.probeId}</span>
</div>
),
},
{
id: "target",
header: "Цель",
accessorKey: "target",
cell: (r) => <span className="font-mono">{r.target}</span>,
},
{ id: "srcServerName", header: "Источник", accessorKey: "srcServerName" },
{
id: "srcInterface",
header: "IF",
accessorKey: "srcInterface",
cell: (r) => <span className="font-mono text-muted-foreground">{r.srcInterface || "—"}</span>,
},
{
id: "rttMs",
header: "RTT",
headerClassName: "text-right",
cellClassName: "text-right tabular-nums",
cell: (r) => (r.rttMs != null ? `${r.rttMs} мс` : "—"),
},
{
id: "lossPct",
header: "Loss",
headerClassName: "text-right",
cellClassName: "text-right tabular-nums",
cell: (r) => `${r.lossPct}%`,
},
{
id: "status",
header: "Статус",
enableSorting: false,
cell: (r) => <Badge variant="outline" className="text-[10px]">{r.status}</Badge>,
},
{
id: "error",
header: "Ошибка",
enableSorting: false,
cell: (r) => <SnapshotErrorCell error={r.error} className="max-w-[180px]" />,
},
]
return <SnapshotDataGrid data={data} columns={columns} emptyTitle="Нет сэмплов ping" />
}
function SpeedSnapshotGrid({ runs }: { runs: SpeedRunSnapshot[] }) {
const data = withRowId(runs, (r) => r.probeId)
const columns: CompactDataGridColumn<(typeof data)[number]>[] = [
{
id: "probeId",
header: "Проба",
accessorKey: "probeId",
cell: (r) => <span className="font-mono">{r.probeId}</span>,
},
{
id: "route",
header: "Маршрут",
enableSorting: false,
cell: (r) => (
<span className="whitespace-nowrap">
{r.srcServerName} <span className="text-muted-foreground"></span> {r.dstServerName}
</span>
),
},
{
id: "interfaces",
header: "Интерфейсы",
enableSorting: false,
cell: (r) => (
<span className="font-mono text-[10px]">
<span className="block">{r.srcInterface || "—"}</span>
<span className="block text-muted-foreground">{r.dstInterface || "—"}</span>
</span>
),
},
{
id: "protocol",
header: "Протокол",
enableSorting: false,
cell: (r) => `${r.protocol} / ${r.direction} / ${r.durationSec}s`,
},
{
id: "txAvgMbps",
header: "TX",
headerClassName: "text-right",
cellClassName: "text-right tabular-nums",
cell: (r) => (r.txAvgMbps != null ? `${Number(r.txAvgMbps).toFixed(1)}` : "—"),
},
{
id: "rxAvgMbps",
header: "RX",
headerClassName: "text-right",
cellClassName: "text-right tabular-nums",
cell: (r) => (r.rxAvgMbps != null ? `${Number(r.rxAvgMbps).toFixed(1)}` : "—"),
},
{
id: "pingRttMs",
header: "Ping RTT",
headerClassName: "text-right",
cellClassName: "text-right tabular-nums",
cell: (r) => (r.pingRttMs != null ? `${r.pingRttMs} мс` : "—"),
},
{
id: "pingLossPct",
header: "Loss",
headerClassName: "text-right",
cellClassName: "text-right tabular-nums",
cell: (r) => (r.pingLossPct != null ? `${r.pingLossPct}%` : "—"),
},
{
id: "ok",
header: "Результат",
enableSorting: false,
cell: (r) => <SnapshotOkBadge ok={r.ok} />,
},
{
id: "error",
header: "Ошибка",
enableSorting: false,
cell: (r) => (
<div className="max-w-[200px]">
<span className="text-destructive block truncate" title={r.error}>{r.error ?? ""}</span>
{r.pingError ? (
<span className="text-[10px] text-amber-600 dark:text-amber-400 block truncate" title={r.pingError ?? ""}>
ping: {r.pingError}
</span>
) : null}
</div>
),
},
]
return <SnapshotDataGrid data={data} columns={columns} emptyTitle="Нет прогонов speed" />
}
function transitionRu(t: AlertEngineRuleDiagSnapshot["hitTransition"]): string {
switch (t) {
case "problem":
return "проблема"
case "recovery":
return "восстановление"
case "neutral":
return "нейтрально"
default:
return "—"
}
}
function blockedRu(b: AlertEngineRuleDiagSnapshot["blocked"]): string {
switch (b) {
case "no_hit":
return "условие не выполнено"
case "stability":
return "стабильность (confirmStabilitySec)"
case "cooldown":
return "cooldown"
case "no_telegram":
return "нет Telegram"
case "dedupe_positive":
return "дедуп восстановления"
case "in_group":
return "в группе (отдельно не шлём)"
default:
return "—"
}
}
function AlertEngineRuleDiagGrid({ ruleDiag }: { ruleDiag: AlertEngineRuleDiagSnapshot[] }) {
const data = withRowId(ruleDiag, (d) => d.ruleId)
const columns: CompactDataGridColumn<(typeof data)[number]>[] = [
{
id: "ruleId",
header: "ID правила",
accessorKey: "ruleId",
cell: (r) => (
<span className="font-mono max-w-[140px] truncate block" title={r.ruleId}>
{r.ruleId}
</span>
),
},
{
id: "evalHit",
header: "Сработало",
cell: (r) => (r.evalHit ? "Да" : "Нет"),
},
{
id: "hitTransition",
header: "Тип срабатывания",
cell: (r) => transitionRu(r.hitTransition),
},
{
id: "stabilityOk",
header: "Стабильность",
cell: (r) => (r.stabilityOk ? "Да" : "Нет"),
},
{
id: "cooldownOk",
header: "Кулдаун",
cell: (r) => (r.cooldownOk ? "Да" : "Нет"),
},
{
id: "telegramOk",
header: "Telegram",
cell: (r) => (r.telegramOk ? "Да" : "Нет"),
},
{
id: "hitMessage",
header: "Сообщение",
enableSorting: false,
cell: (r) => (
<span className="font-mono max-w-[280px] truncate text-muted-foreground block" title={r.hitMessage ?? ""}>
{r.hitMessage ?? "—"}
</span>
),
},
{
id: "blocked",
header: "Причина блока",
cell: (r) => <span className="text-muted-foreground">{blockedRu(r.blocked)}</span>,
},
]
return (
<SnapshotDataGrid
data={data}
columns={columns}
emptyTitle="Нет диагностики по правилам"
/>
)
}
function fmtUptimeSec(sec: number): string {
if (sec <= 0) return "—"
const d = Math.floor(sec / 86400)
const h = Math.floor((sec % 86400) / 3600)
const m = Math.floor((sec % 3600) / 60)
if (d > 0) return `${d}д ${h}ч`
if (h > 0) return `${h}ч ${m}м`
return `${m}м`
}
export {
SnapshotDataGrid,
TrafficSnapshotGrid,
ResourcesSnapshotGrid,
ServersRestPingSnapshotGrid,
PingSnapshotGrid,
SpeedSnapshotGrid,
AlertEngineRuleDiagGrid,
type SnapshotRow,
}