feat(mcp): подготовить сервер и доступ к приёмке
This commit is contained in:
commit
bf3d50e1ff
3835 files changed
+537122
No files matched your search
@@ -0,0 +1,269 @@
|
||||
"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<ColumnDef<DataGridFeatures, SpeedTestRunRow>[]>(
|
||||
() => [
|
||||
{
|
||||
id: "startedAt",
|
||||
accessorKey: "startedAt",
|
||||
header: ({ column }) => <DataGridSortHeader column={column} title="Время" className="ml-1" />,
|
||||
cell: ({ row }) => (
|
||||
<span className="text-muted-foreground whitespace-nowrap tabular-nums font-mono text-xs">
|
||||
{new Date(row.original.startedAt).toLocaleString("ru-RU", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
),
|
||||
meta: {
|
||||
headerTitle: "Время",
|
||||
headerClassName: DATA_GRID_CELL_PAD_FIRST,
|
||||
cellClassName: DATA_GRID_CELL_PAD_FIRST,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "route",
|
||||
accessorFn: (row) => `${row.srcServerId}-${row.dstServerId}`,
|
||||
header: ({ column }) => <DataGridSortHeader column={column} title="Маршрут" />,
|
||||
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 (
|
||||
<div className="font-mono whitespace-nowrap text-xs">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Flag code={src?.country ?? "UN"} size={13} />
|
||||
<span>{src?.name ?? run.srcServerId}</span>
|
||||
<ArrowRightIcon className="size-3 text-muted-foreground" />
|
||||
<Flag code={dst?.country ?? "UN"} size={13} />
|
||||
<span>{dst?.name ?? run.dstServerId}</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground mt-0.5 font-mono">
|
||||
{run.srcInterfaceAddress && run.dstInterfaceAddress
|
||||
? `${run.srcInterfaceAddress} → ${run.dstInterfaceAddress}`
|
||||
: "внутренние IP: auto/не указаны"}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
meta: { headerTitle: "Маршрут", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
|
||||
},
|
||||
{
|
||||
id: "params",
|
||||
accessorFn: (row) => `${row.protocol}-${row.direction}-${row.durationSec}`,
|
||||
header: ({ column }) => <DataGridSortHeader column={column} title="Параметры" />,
|
||||
cell: ({ row }) => {
|
||||
const run = row.original
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-muted-foreground whitespace-nowrap text-xs">
|
||||
<span className="inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-semibold bg-muted/60 border-border/60">
|
||||
{run.protocol.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-muted-foreground/60">·</span>
|
||||
<span>{run.direction}</span>
|
||||
<span className="text-muted-foreground/60">·</span>
|
||||
<span>{run.durationSec}s</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
meta: { headerTitle: "Параметры", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => <DataGridSortHeader column={column} title="Статус" />,
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status
|
||||
if (status === "running") {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 text-[var(--status-degraded-fg)] text-xs">
|
||||
<RefreshCwIcon className="size-3 animate-spin" />
|
||||
running
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (status === "error") {
|
||||
return <span className="text-[var(--status-offline-fg)] text-xs">error</span>
|
||||
}
|
||||
if (status === "unknown") {
|
||||
return (
|
||||
<div className="max-w-64 text-xs text-[var(--status-degraded-fg)]">
|
||||
<span>Сохранение не подтверждено</span>
|
||||
<div className="mt-1 font-mono wrap-anywhere">runId: {row.original.id}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <span className="text-[var(--status-online-fg)] text-xs">done</span>
|
||||
},
|
||||
meta: { headerTitle: "Статус", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
|
||||
},
|
||||
{
|
||||
id: "txAvgMbps",
|
||||
accessorFn: row => row.txAvgMbps ?? undefined,
|
||||
sortUndefined: "last",
|
||||
header: ({ column }) => <DataGridSortHeader column={column} title="TX avg" />,
|
||||
cell: ({ row }) => {
|
||||
const run = row.original
|
||||
if (run.txAvgMbps === null) return <span className="text-xs text-muted-foreground">Неизвестно</span>
|
||||
const maxVal = Math.max(run.txAvgMbps ?? 0, run.rxAvgMbps ?? 0, 1)
|
||||
return (
|
||||
<div className="flex items-center gap-2 min-w-[120px]">
|
||||
<div className="w-16 h-1.5 rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-[var(--chart-tx)]"
|
||||
style={{ width: `${(run.txAvgMbps / maxVal) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-mono tabular-nums text-[var(--chart-tx)] font-medium whitespace-nowrap text-xs">
|
||||
{run.txAvgMbps} Мбит/с
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
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 }) => <DataGridSortHeader column={column} title="RX avg" />,
|
||||
cell: ({ row }) => {
|
||||
const run = row.original
|
||||
if (run.rxAvgMbps === null) return <span className="text-xs text-muted-foreground">Неизвестно</span>
|
||||
const maxVal = Math.max(run.txAvgMbps ?? 0, run.rxAvgMbps ?? 0, 1)
|
||||
return (
|
||||
<div className="flex items-center gap-2 min-w-[120px]">
|
||||
<div className="w-16 h-1.5 rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-[var(--chart-rx)]"
|
||||
style={{ width: `${(run.rxAvgMbps / maxVal) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-mono tabular-nums text-[var(--chart-rx)] font-medium whitespace-nowrap text-xs">
|
||||
{run.rxAvgMbps} Мбит/с
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
meta: { headerTitle: "RX avg", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
|
||||
},
|
||||
{
|
||||
id: "afterBtPing",
|
||||
accessorFn: (row) => row.afterBtPing?.rttMs ?? -1,
|
||||
header: ({ column }) => <DataGridSortHeader column={column} title="Ping после BT" />,
|
||||
cell: ({ row }) => {
|
||||
const run = row.original
|
||||
if (run.status !== "done") return <span className="text-xs">—</span>
|
||||
if (!run.afterBtPing) return <span className="text-xs text-muted-foreground">Нет данных</span>
|
||||
if (run.afterBtPing.error) {
|
||||
return (
|
||||
<span className="text-[var(--status-offline-fg)] text-xs font-mono" title={run.afterBtPing.error}>
|
||||
ошибка
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (run.afterBtPing?.rttMs != null) {
|
||||
return (
|
||||
<span className="font-mono tabular-nums whitespace-nowrap text-xs text-violet-600 dark:text-violet-400">
|
||||
{run.afterBtPing.rttMs} мс
|
||||
{run.afterBtPing.lossPct != null && run.afterBtPing.lossPct > 0 && (
|
||||
<span className="text-amber-600 dark:text-amber-400"> · {run.afterBtPing.lossPct}%</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span className="font-mono tabular-nums whitespace-nowrap text-xs text-amber-600 dark:text-amber-400">
|
||||
{run.afterBtPing.lossPct === 100 ? "Нет ответов" : "RTT неизвестен"}
|
||||
{run.afterBtPing?.lossPct != null && <span> · {run.afterBtPing.lossPct}%</span>}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
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 (
|
||||
<DataGridShell
|
||||
table={table}
|
||||
recordCount={runs.length}
|
||||
pagination={runs.length > 50 || pageSize !== 50}
|
||||
tableClassNames={{ bodyRow: cn("group/row text-xs") }}
|
||||
>
|
||||
<DataGridContainer border={false} className="w-full rounded-none border-0">
|
||||
<DataGridScrollArea orientation="horizontal">
|
||||
<DataGridTable />
|
||||
</DataGridScrollArea>
|
||||
</DataGridContainer>
|
||||
</DataGridShell>
|
||||
)
|
||||
}
|
||||
|
||||
export { UptimeSpeedHistoryDataGrid, type UptimeSpeedHistoryDataGridProps }
|
||||
Reference in new issue
Block a user