"use client" import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" import { useMemo, useEffect } from "react" import { type ColumnDef, useTable, } from "@tanstack/react-table" import type { Server } from "@/lib/data" import { Flag } from "@/components/flag" 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 { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sort-header" import { cn } from "@/lib/utils" import { ArrowRightIcon, RefreshCwIcon } from "lucide-react" export interface SpeedTestRunRow { id: string startedAt: number srcServerId: string dstServerId: string srcInterface?: string dstInterface?: string protocol: "tcp" | "udp" direction: "transmit" | "receive" | "both" durationSec: number txAvgMbps: number | null rxAvgMbps: number | null status: "running" | "done" | "error" | "unknown" afterBtPing?: { rttMs: number | null; lossPct: number | null; error: string | null } | null srcInterfaceAddress?: string | null dstInterfaceAddress?: string | null } interface UptimeSpeedHistoryDataGridProps { runs: SpeedTestRunRow[] servers: Server[] } function UptimeSpeedHistoryDataGrid({ runs, servers }: UptimeSpeedHistoryDataGridProps) { const columns = useMemo[]>( () => [ { id: "startedAt", accessorKey: "startedAt", header: ({ column }) => , cell: ({ row }) => ( {new Date(row.original.startedAt).toLocaleString("ru-RU", { hour: "2-digit", minute: "2-digit", second: "2-digit", day: "2-digit", month: "2-digit", })} ), meta: { headerTitle: "Время", headerClassName: DATA_GRID_CELL_PAD_FIRST, cellClassName: DATA_GRID_CELL_PAD_FIRST, }, }, { id: "route", accessorFn: (row) => `${row.srcServerId}-${row.dstServerId}`, header: ({ column }) => , cell: ({ row }) => { const run = row.original const src = servers.find((s) => s.id === run.srcServerId) const dst = servers.find((s) => s.id === run.dstServerId) return (
{src?.name ?? run.srcServerId} {dst?.name ?? run.dstServerId}
{run.srcInterfaceAddress && run.dstInterfaceAddress ? `${run.srcInterfaceAddress} → ${run.dstInterfaceAddress}` : "внутренние IP: auto/не указаны"}
) }, meta: { headerTitle: "Маршрут", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "params", accessorFn: (row) => `${row.protocol}-${row.direction}-${row.durationSec}`, header: ({ column }) => , cell: ({ row }) => { const run = row.original return (
{run.protocol.toUpperCase()} · {run.direction} · {run.durationSec}s
) }, meta: { headerTitle: "Параметры", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "status", accessorKey: "status", header: ({ column }) => , cell: ({ row }) => { const status = row.original.status if (status === "running") { return ( running ) } if (status === "error") { return error } if (status === "unknown") { return (
Сохранение не подтверждено
runId: {row.original.id}
) } return done }, meta: { headerTitle: "Статус", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "txAvgMbps", accessorFn: row => row.txAvgMbps ?? undefined, sortUndefined: "last", header: ({ column }) => , cell: ({ row }) => { const run = row.original if (run.txAvgMbps === null) return Неизвестно const maxVal = Math.max(run.txAvgMbps ?? 0, run.rxAvgMbps ?? 0, 1) return (
{run.txAvgMbps} Мбит/с
) }, meta: { headerTitle: "TX avg", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "rxAvgMbps", accessorFn: row => row.rxAvgMbps ?? undefined, sortUndefined: "last", header: ({ column }) => , cell: ({ row }) => { const run = row.original if (run.rxAvgMbps === null) return Неизвестно const maxVal = Math.max(run.txAvgMbps ?? 0, run.rxAvgMbps ?? 0, 1) return (
{run.rxAvgMbps} Мбит/с
) }, meta: { headerTitle: "RX avg", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "afterBtPing", accessorFn: (row) => row.afterBtPing?.rttMs ?? -1, header: ({ column }) => , cell: ({ row }) => { const run = row.original if (run.status !== "done") return — if (!run.afterBtPing) return Нет данных if (run.afterBtPing.error) { return ( ошибка ) } if (run.afterBtPing?.rttMs != null) { return ( {run.afterBtPing.rttMs} мс {run.afterBtPing.lossPct != null && run.afterBtPing.lossPct > 0 && ( · {run.afterBtPing.lossPct}% )} ) } return ( {run.afterBtPing.lossPct === 100 ? "Нет ответов" : "RTT неизвестен"} {run.afterBtPing?.lossPct != null && · {run.afterBtPing.lossPct}%} ) }, meta: { headerTitle: "Ping после BT", headerClassName: DATA_GRID_CELL_PAD_LAST, cellClassName: DATA_GRID_CELL_PAD_LAST, }, }, ], [servers], ) const table = useTable({ features: dataGridFeatures, manualPagination: false, autoResetPageIndex: false, data: runs, columns, getRowId: (row) => row.id, initialState: { pagination: { pageIndex: 0, pageSize: 50 }, sorting: [{ id: "startedAt", desc: true }] }, }) const { pageIndex, pageSize } = table.state.pagination useEffect(() => { const lastPage = Math.max(0, Math.ceil(runs.length / pageSize) - 1) if (pageIndex > lastPage) table.setPageIndex(lastPage) }, [runs.length, pageSize, pageIndex, table]) return ( 50 || pageSize !== 50} tableClassNames={{ bodyRow: cn("group/row text-xs") }} > ) } export { UptimeSpeedHistoryDataGrid, type UptimeSpeedHistoryDataGridProps }