Files
MikrotikManager/components/data-grids/route-optimizer-ospf-preview-data-grid.tsx
T

192 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { useEffect, useMemo, useState } 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 { Input } from "@/components/ui/input"
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 [query, setQuery] = useState("")
const filteredRows = useMemo(() => {
const value = query.trim().toLowerCase()
return value ? rows.filter(row => `${row.interface} ${row.id}`.toLowerCase().includes(value)) : rows
}, [rows, query])
const columns = useMemo<ColumnDef<DataGridFeatures, OspfPreviewInterfaceRow>[]>(
() => [
{
id: "interface",
size: 120,
accessorKey: "interface",
header: ({ column }) => (
<DataGridSortHeader column={column} title="Интерфейс" className="ml-1" />
),
cell: ({ row }) => <span className="block truncate font-mono" title={row.original.interface}>{row.original.interface}</span>,
meta: {
headerTitle: "Интерфейс",
headerClassName: DATA_GRID_CELL_PAD_FIRST,
cellClassName: DATA_GRID_CELL_PAD_FIRST,
},
},
{
id: "cost",
size: 144,
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",
size: 96,
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",
size: 288,
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",
size: 336,
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: false,
autoResetPageIndex: false,
initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: ["interface"], end: [] } },
data: filteredRows,
columns,
getRowId: (row) => row.id,
})
const { pageIndex, pageSize } = table.state.pagination
useEffect(() => {
const lastPage = Math.max(0, Math.ceil(filteredRows.length / pageSize) - 1)
if (pageIndex > lastPage) table.setPageIndex(lastPage)
}, [filteredRows.length, pageIndex, pageSize, table])
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 (
<section aria-label="Preview интерфейсов OSPF" className="min-w-0">
{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>}
<div className="flex min-w-0 flex-col gap-2 px-3 py-3">
<Input aria-label="Поиск интерфейсов preview OSPF" placeholder="Имя интерфейса…" value={query} onChange={event => { setQuery(event.target.value); table.setPageIndex(0) }} />
<p role="status" className="text-xs text-muted-foreground">Найдено {filteredRows.length} из {rows.length} интерфейсов. Фильтр меняет только отображение preview.</p>
</div>
<DataGridShell
table={table}
recordCount={filteredRows.length}
pagination={filteredRows.length > 50 || pageSize !== 50}
emptyMessage="Нет интерфейсов по запросу"
isLoading={loading && rows.length === 0}
loadingMode="skeleton"
tableClassNames={{ headerRow: "border-b bg-muted/30", bodyRow: "group/row" }}
tableLayout={{ dense: true, width: "fixed", columnsPinnable: true }}
>
<DataGridContainer border={false}>
<DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea>
</DataGridContainer>
</DataGridShell>
</section>
)
}
export { RouteOptimizerOspfPreviewDataGrid, type RouteOptimizerOspfPreviewDataGridProps }