Files
MikrotikManager/components/data-grids/route-optimizer-full-routes-data-grid.tsx
T

315 lines
11 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 ColumnDef,
useTable,
} from "@tanstack/react-table"
import type { FullRoute } from "@/lib/route-optimizer-data"
import { Flag } from "@/components/flag"
import { cn } from "@/lib/utils"
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 { ArrowRightIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
function Chip({ children, color }: { children: React.ReactNode; color?: string }) {
return (
<span
className={cn(
"inline-flex items-center rounded px-1.5 py-0.5 text-[11px] font-medium border",
color ?? "bg-muted text-muted-foreground border-border",
)}
>
{children}
</span>
)
}
function ProbChip({ prob, best }: { prob: number; best?: boolean }) {
return (
<Chip
color={
best
? "bg-success/10 text-success border-success/20"
: "bg-muted text-muted-foreground border-border"
}
>
{prob} б.
</Chip>
)
}
function ConfChip({ conf }: { conf: string }) {
const map: Record<string, string> = {
HIGH: "bg-info/10 text-info border-info/20",
MEDIUM: "bg-warning/10 text-warning border-warning/20",
LOW: "bg-destructive/10 text-destructive border-destructive/20",
}
return <Chip color={map[conf] ?? map.LOW}>{conf}</Chip>
}
interface RouteOptimizerFullRoutesDataGridProps {
routes: FullRoute[]
bestId?: string
}
function RouteOptimizerFullRoutesDataGrid({
routes,
bestId,
}: RouteOptimizerFullRoutesDataGridProps) {
const [expanded, setExpanded] = useState(false)
const visibleRoutes = expanded ? routes : routes.slice(0, 5)
const indexed = useMemo(
() => visibleRoutes.map((r, index) => ({ ...r, _index: index })),
[visibleRoutes],
)
const columns = useMemo<ColumnDef<DataGridFeatures, FullRoute & { _index: number }>[]>(
() => [
{
id: "index",
size: 112,
header: () => <span className="text-[11px] font-medium text-muted-foreground"># Маршрут</span>,
enableSorting: false,
cell: ({ row }) => {
const isBest = row.original.id === bestId || row.original._index === 0
return (
<div className="flex items-center gap-2">
<span className="text-[10px] font-mono text-muted-foreground min-w-6 tabular-nums">
{row.original._index + 1}
</span>
{isBest && (
<span className="text-[9px] font-bold uppercase tracking-wide text-success bg-success/10 border border-success/20 px-1.5 py-0.5 rounded">
Лучший
</span>
)}
</div>
)
},
meta: { headerClassName: DATA_GRID_CELL_PAD_FIRST, cellClassName: DATA_GRID_CELL_PAD_FIRST },
},
{
id: "wanJh",
size: 264,
header: () => <span className="text-[11px] font-medium text-muted-foreground">WAN → JH</span>,
enableSorting: false,
cell: ({ row }) => {
const r = row.original
return (
<div className="flex items-center gap-1.5 text-xs">
<span className="font-mono font-semibold text-info ">{r.wan.name}</span>
<ArrowRightIcon aria-hidden="true" className="size-3 text-muted-foreground shrink-0" />
<div>
<div className="font-medium">{r.jh.label}</div>
<div className="font-mono text-[10px] text-muted-foreground">
Модель: {r.hw.pingMs} мс · ↓{r.hw.dlMbps} ↑{r.hw.ulMbps}
</div>
</div>
</div>
)
},
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
},
{
id: "jhExit",
size: 240,
header: () => <span className="text-[11px] font-medium text-muted-foreground">JH → Exit</span>,
enableSorting: false,
cell: ({ row }) => {
const r = row.original
return (
<div className="text-xs">
<div className="flex items-center gap-1 font-medium">
<Flag code={r.exit.country} />
{r.exit.label}
<span className="text-[10px] text-muted-foreground">({r.exit.site})</span>
</div>
<div className="font-mono text-[10px] text-muted-foreground">
Модель: {r.je.pingMs} мс · ↓{r.je.dlMbps} ↑{r.je.ulMbps}
</div>
</div>
)
},
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
},
{
id: "totalPing",
size: 144,
accessorFn: (row) => row.hw.pingMs + row.je.pingMs,
header: ({ column }) => (
<DataGridSortHeader column={column} title="Ping модели" className="mx-auto" />
),
cell: ({ row }) => {
const totalPing = row.original.hw.pingMs + row.original.je.pingMs
return (
<span
className={cn(
"font-mono text-xs text-center block",
totalPing < 40
? "text-success "
: totalPing < 80
? "text-warning "
: "text-destructive",
)}
>
{totalPing} мс
</span>
)
},
meta: {
headerTitle: "Ping модели",
headerClassName: cn(DATA_GRID_CELL_PAD, "text-center"),
cellClassName: cn(DATA_GRID_CELL_PAD, "text-center"),
},
},
{
id: "bw",
size: 144,
header: () => (
<span className="text-[11px] font-medium text-muted-foreground block text-center">
BW (мин)
</span>
),
enableSorting: false,
cell: ({ row }) => {
const r = row.original
const minDl = Math.min(r.hw.dlMbps, r.je.dlMbps)
const minUl = Math.min(r.hw.ulMbps, r.je.ulMbps)
return (
<div className="font-mono text-xs text-muted-foreground text-center">
<div>↓{minDl}</div>
<div>↑{minUl}</div>
</div>
)
},
meta: {
headerClassName: cn(DATA_GRID_CELL_PAD, "text-center"),
cellClassName: cn(DATA_GRID_CELL_PAD, "text-center"),
},
},
{
id: "score",
size: 80,
accessorKey: "score",
header: ({ column }) => (
<DataGridSortHeader column={column} title="Score" className="mx-auto" />
),
cell: ({ row }) => (
<span className="font-mono text-xs font-semibold text-center block">
{row.original.score}
</span>
),
meta: {
headerTitle: "Score",
headerClassName: cn(DATA_GRID_CELL_PAD, "text-center"),
cellClassName: cn(DATA_GRID_CELL_PAD, "text-center"),
},
},
{
id: "prob",
size: 104,
accessorKey: "probabilityOptimal",
header: ({ column }) => (
<DataGridSortHeader column={column} title="Индекс" className="mx-auto" />
),
cell: ({ row }) => {
const isBest = row.original.id === bestId || row.original._index === 0
return (
<div className="flex justify-center">
<ProbChip prob={row.original.probabilityOptimal} best={isBest} />
</div>
)
},
meta: {
headerTitle: "Индекс",
headerClassName: cn(DATA_GRID_CELL_PAD, "text-center"),
cellClassName: cn(DATA_GRID_CELL_PAD, "text-center"),
},
},
{
id: "confidence",
size: 176,
accessorKey: "confidence",
header: ({ column }) => (
<DataGridSortHeader column={column} title="Категория модели" className="mx-auto" />
),
cell: ({ row }) => (
<div className="flex justify-center">
<ConfChip conf={row.original.confidence} />
</div>
),
meta: {
headerTitle: "Категория модели",
headerClassName: DATA_GRID_CELL_PAD_LAST,
cellClassName: cn(DATA_GRID_CELL_PAD_LAST, "text-center"),
},
},
],
[bestId],
)
const table = useTable({
features: dataGridFeatures,
manualPagination: false,
autoResetPageIndex: false,
initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: ["index"], end: [] } },
data: indexed,
columns,
getRowId: (row) => row.id,
})
const { pageIndex, pageSize } = table.state.pagination
useEffect(() => {
const lastPage = Math.max(0, Math.ceil(indexed.length / pageSize) - 1)
if (pageIndex > lastPage) table.setPageIndex(lastPage)
}, [indexed.length, pageIndex, pageSize, table])
return (
<section aria-label="Полные маршруты оптимизатора" className="min-w-0">
<DataGridShell
table={table}
recordCount={visibleRoutes.length}
pagination={expanded && (visibleRoutes.length > 50 || pageSize !== 50)}
tableClassNames={{
headerRow: "border-b bg-muted/20",
bodyRow: cn("group/row hover:bg-muted/30", "has-[[data-route-best=true]]:bg-success/5"),
}}
tableLayout={{ dense: true, width: "fixed", columnsPinnable: true }}
>
<DataGridContainer border={false}>
<DataGridScrollArea orientation="horizontal"><DataGridTable /></DataGridScrollArea>
</DataGridContainer>
</DataGridShell>
{routes.length > 5 && (
<button
type="button"
onClick={() => { setExpanded((v) => !v); table.setPageIndex(0) }}
className="w-full min-h-11 py-2 text-xs text-muted-foreground hover:text-foreground transition-colors border-t flex items-center justify-center gap-1"
>
{expanded ? (
<>
<ChevronUpIcon aria-hidden="true" className="size-3" />
Свернуть
</>
) : (
<>
<ChevronDownIcon aria-hidden="true" className="size-3" />
{routes.length > 50 ? `Открыть все ${routes.length} комбинаций` : `Показать все ${routes.length} комбинаций`}
</>
)}
</button>
)}
</section>
)
}
export { RouteOptimizerFullRoutesDataGrid, type RouteOptimizerFullRoutesDataGridProps }