163 lines
5.7 KiB
TypeScript
163 lines
5.7 KiB
TypeScript
"use client"
|
||
|
||
import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
|
||
|
||
import { useMemo } from "react"
|
||
import type {OspfOptimizerObserved} from "@mmapp/contracts"
|
||
import {
|
||
type ColumnDef,
|
||
useTable,
|
||
} from "@tanstack/react-table"
|
||
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 { EmptyState } from "@/components/empty-state"
|
||
import { NetworkIcon } from "lucide-react"
|
||
|
||
function describeObserved(metric:OspfOptimizerObserved["ping"]|undefined,unit:string){
|
||
if(!metric || metric.value===null)return "Измерение: —"
|
||
const source=metric.source==="speed-probe"?"Проба":"Трафик интерфейса"
|
||
const date=metric.measuredAt?new Date(metric.measuredAt).toLocaleString("ru-RU"):"дата неизвестна"
|
||
return source+": "+metric.value+" "+unit+" · "+date
|
||
}
|
||
|
||
export interface OspfPreviewInterfaceRow {
|
||
observed?: OspfOptimizerObserved
|
||
id: string
|
||
interface: string
|
||
currentCost: number
|
||
optimalCost: number
|
||
score: number
|
||
pingMs: number
|
||
dlMbps: number
|
||
ulMbps: number
|
||
}
|
||
|
||
interface RouteOptimizerOspfPreviewDataGridProps {
|
||
rows: OspfPreviewInterfaceRow[]
|
||
error?: string | null
|
||
loading?: boolean
|
||
}
|
||
|
||
function RouteOptimizerOspfPreviewDataGrid({
|
||
rows,
|
||
error,
|
||
loading,
|
||
}: RouteOptimizerOspfPreviewDataGridProps) {
|
||
const columns = useMemo<ColumnDef<DataGridFeatures, OspfPreviewInterfaceRow>[]>(
|
||
() => [
|
||
{
|
||
id: "interface",
|
||
accessorKey: "interface",
|
||
header: ({ column }) => (
|
||
<DataGridSortHeader column={column} title="Интерфейс" className="ml-1" />
|
||
),
|
||
cell: ({ row }) => <span className="font-mono">{row.original.interface}</span>,
|
||
meta: {
|
||
headerTitle: "Интерфейс",
|
||
headerClassName: DATA_GRID_CELL_PAD_FIRST,
|
||
cellClassName: DATA_GRID_CELL_PAD_FIRST,
|
||
},
|
||
},
|
||
{
|
||
id: "cost",
|
||
accessorKey: "currentCost",
|
||
header: ({ column }) => <DataGridSortHeader column={column} title="Cost" />,
|
||
cell: ({ row }) => {
|
||
const r = row.original
|
||
return (
|
||
<span className="font-mono">
|
||
<span className="text-info ">{r.currentCost}</span>
|
||
{" → "}
|
||
<span
|
||
className={
|
||
r.currentCost === r.optimalCost
|
||
? "text-success "
|
||
: "text-warning "
|
||
}
|
||
>
|
||
{r.optimalCost}
|
||
</span>
|
||
</span>
|
||
)
|
||
},
|
||
meta: { headerTitle: "Cost", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
|
||
},
|
||
{
|
||
id: "score",
|
||
accessorKey: "score",
|
||
header: ({ column }) => <DataGridSortHeader column={column} title="Score" />,
|
||
cell: ({ row }) => <span className="font-mono">{row.original.score}</span>,
|
||
meta: { headerTitle: "Score", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
|
||
},
|
||
{
|
||
id: "pingMs",
|
||
accessorKey: "pingMs",
|
||
header: ({ column }) => <DataGridSortHeader column={column} title="Ping модели" />,
|
||
cell: ({ row }) => <div><span className="font-mono">{row.original.pingMs}ms</span><p className="text-[10px] text-muted-foreground">{describeObserved(row.original.observed?.ping,"мс")}</p></div>,
|
||
meta: { headerTitle: "Ping модели", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
|
||
},
|
||
{
|
||
id: "speed",
|
||
header: () => <span className="text-xs font-medium text-muted-foreground">Speed (dl/ul) · модель</span>,
|
||
enableSorting: false,
|
||
cell: ({ row }) => {
|
||
const r = row.original
|
||
return (
|
||
<span className="font-mono">
|
||
↓{r.dlMbps} / ↑{r.ulMbps}<span className="block text-[10px] text-muted-foreground">↓ {describeObserved(r.observed?.dl,"Мбит/с")}<br />↑ {describeObserved(r.observed?.ul,"Мбит/с")}</span>
|
||
</span>
|
||
)
|
||
},
|
||
meta: { headerClassName: DATA_GRID_CELL_PAD_LAST, cellClassName: DATA_GRID_CELL_PAD_LAST },
|
||
},
|
||
],
|
||
[],
|
||
)
|
||
|
||
const table = useTable({
|
||
features: dataGridFeatures,
|
||
manualPagination: true,
|
||
data: rows,
|
||
columns,
|
||
getRowId: (row) => row.id,
|
||
})
|
||
|
||
if (error && rows.length === 0) {
|
||
return (
|
||
<p role="alert" className="px-3 py-2 text-xs text-destructive">Ошибка preview: {error}</p>
|
||
)
|
||
}
|
||
|
||
if (!loading && rows.length === 0) {
|
||
return (
|
||
<EmptyState
|
||
icon={<NetworkIcon aria-hidden="true" className="size-4" />}
|
||
title="Интерфейсы OSPF не найдены для выбранного сервера"
|
||
className="border-0 py-6 text-xs"
|
||
/>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<>
|
||
{error && <p role="alert" className="px-3 py-2 text-xs text-warning">Ошибка preview: {error}. Последний снимок; актуальность неизвестна.</p>}
|
||
{loading && rows.length > 0 && <p role="status" className="px-3 py-2 text-xs text-muted-foreground">Обновление preview OSPF…</p>}
|
||
<DataGridShell
|
||
table={table}
|
||
recordCount={rows.length}
|
||
isLoading={loading && rows.length === 0}
|
||
loadingMode="skeleton"
|
||
tableClassNames={{ headerRow: "border-b bg-muted/30", bodyRow: "group/row" }}
|
||
tableLayout={{ dense: true }}
|
||
/>
|
||
</>
|
||
)
|
||
}
|
||
|
||
export { RouteOptimizerOspfPreviewDataGrid, type RouteOptimizerOspfPreviewDataGridProps }
|