Files
MikrotikManager/components/data-grids/traffic-flows-data-grid.tsx
T
DenozordecandCursor 90c8c393e5
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Failing after 1m31s
Docker images / frontend-image (push) Successful in 2m36s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 42s
Docker images / publish-release (push) Skipped
feat(traffic): разделить плоскости IPFIX и показать путь JH→EN
Считать payload отдельно от overlay GRE/ESP и mesh; вкладка Пути и KPI Wire из счётчиков iface.

Co-authored-by: Cursor <[email protected]>
2026-09-07 11:39:14 +07:00

151 lines
6.0 KiB
TypeScript

"use client"
import { useMemo } from "react"
import { type ColumnDef, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import type { FlowTalkerDto } from "@mmapp/contracts/traffic-flow"
import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell"
import {
DATA_GRID_CELL_PAD,
DATA_GRID_CELL_PAD_FIRST,
DATA_GRID_CELL_PAD_LAST,
} from "@/components/data-grids/shared/data-grid-layout"
import { fmtRate } from "@/lib/fmt-rate"
import { cn } from "@/lib/utils"
function formatBytes(n: number): string {
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)} ГБ`
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)} МБ`
if (n >= 1000) return `${(n / 1000).toFixed(1)} КБ`
return `${n} Б`
}
function TrafficFlowsDataGrid({
rows,
emptyHint,
}: {
rows: FlowTalkerDto[]
emptyHint?: string
}) {
const columns = useMemo<ColumnDef<FlowTalkerDto>[]>(
() => [
{
id: "client",
accessorFn: (r) => r.clientName ?? "",
header: () => <span className="text-xs font-medium text-muted-foreground">Клиент</span>,
cell: ({ row }) => <span className="text-xs">{row.original.clientName || "—"}</span>,
meta: { headerClassName: DATA_GRID_CELL_PAD_FIRST, cellClassName: DATA_GRID_CELL_PAD_FIRST },
},
{
id: "server",
accessorKey: "serverName",
header: () => <span className="text-xs font-medium text-muted-foreground">JH</span>,
cell: ({ row }) => <span className="text-sm font-medium">{row.original.serverName}</span>,
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
},
{
id: "src",
accessorKey: "src",
header: () => <span className="text-xs font-medium text-muted-foreground">Src</span>,
cell: ({ row }) => (
<span className="font-mono text-xs">
{row.original.src}
{row.original.srcPort ? `:${row.original.srcPort}` : ""}
</span>
),
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
},
{
id: "dst",
accessorKey: "dst",
header: () => <span className="text-xs font-medium text-muted-foreground">Dst</span>,
cell: ({ row }) => (
<span className="font-mono text-xs">
{row.original.dst}
{row.original.dstPort ? `:${row.original.dstPort}` : ""}
</span>
),
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
},
{
id: "app",
accessorFn: (r) => r.application ?? r.protoName,
header: () => <span className="text-xs font-medium text-muted-foreground">App</span>,
cell: ({ row }) => (
<span className="flex min-w-0 flex-col gap-0.5 text-xs">
<span>{row.original.application ?? row.original.protoName}</span>
{row.original.category || row.original.service ? (
<span className="text-[10px] text-muted-foreground truncate">
{[row.original.category, row.original.service].filter(Boolean).join(" · ")}
</span>
) : null}
</span>
),
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
},
{
id: "proto",
accessorKey: "protoName",
header: () => <span className="text-xs font-medium text-muted-foreground">Proto</span>,
cell: ({ row }) => <span className="text-xs">{row.original.protoName}</span>,
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
},
{
id: "rate",
accessorFn: (r) => r.bps,
header: () => <span className="text-xs font-medium text-muted-foreground">Скорость</span>,
cell: ({ row }) => <span className="text-xs tabular-nums">{fmtRate(row.original.bps / 1_000_000)}</span>,
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
},
{
id: "bytes",
accessorKey: "bytes",
header: () => <span className="text-xs font-medium text-muted-foreground">Байты</span>,
cell: ({ row }) => <span className="text-xs tabular-nums">{formatBytes(row.original.bytes)}</span>,
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
},
{
id: "en",
accessorFn: (r) => r.enName ?? "",
header: () => <span className="text-xs font-medium text-muted-foreground">EN</span>,
cell: ({ row }) => <span className="text-xs">{row.original.enName || "—"}</span>,
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
},
{
id: "plane",
accessorFn: (r) => r.plane ?? "",
header: () => <span className="text-xs font-medium text-muted-foreground">Плоскость</span>,
cell: ({ row }) => <span className="text-xs text-muted-foreground">{row.original.plane || "—"}</span>,
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
},
{
id: "iface",
accessorKey: "inIface",
header: () => <span className="text-xs font-medium text-muted-foreground">Iface</span>,
cell: ({ row }) => <span className="font-mono text-xs text-muted-foreground">{row.original.inIface || "—"}</span>,
meta: { headerClassName: DATA_GRID_CELL_PAD_LAST, cellClassName: cn(DATA_GRID_CELL_PAD_LAST) },
},
],
[],
)
const table = useReactTable({
data: rows,
columns,
getCoreRowModel: getCoreRowModel(),
getRowId: (row, i) => `${row.serverId}-${row.src}-${row.dst}-${row.proto}-${row.srcPort}-${row.dstPort}-${i}`,
})
return (
<DataGridShell
table={table}
recordCount={rows.length}
emptyMessage={
emptyHint
|| "Пока нет IPFIX. Поднимите wg-flow на хосте MM и подключите jump-host одним кликом."
}
/>
)
}
export { TrafficFlowsDataGrid }