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
499 lines
15 KiB
TypeScript
499 lines
15 KiB
TypeScript
"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,
|
||
}
|