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

294 lines
9.7 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 { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid"
import { 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-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
: "bg-muted text-muted-foreground border-border"
}
>
{prob} б.
</Chip>
)
}
function ConfChip({ conf }: { conf: string }) {
const map: Record<string, string> = {
HIGH: "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20",
MEDIUM: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20",
LOW: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/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",
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 w-4">
{row.original._index + 1}
</span>
{isBest && (
<span className="text-[9px] font-bold uppercase tracking-wide text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 border border-emerald-500/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",
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-sky-600 dark:text-sky-400">{r.wan.name}</span>
<ArrowRightIcon 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",
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",
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-emerald-600 dark:text-emerald-400"
: totalPing < 80
? "text-amber-600 dark:text-amber-400"
: "text-red-500",
)}
>
{totalPing} мс
</span>
)
},
meta: {
headerTitle: "Ping (итого)",
headerClassName: cn(DATA_GRID_CELL_PAD, "text-center"),
cellClassName: cn(DATA_GRID_CELL_PAD, "text-center"),
},
},
{
id: "bw",
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",
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",
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",
accessorKey: "confidence",
header: ({ column }) => (
<DataGridSortHeader column={column} title="Conf." className="mx-auto" />
),
cell: ({ row }) => (
<div className="flex justify-center">
<ConfChip conf={row.original.confidence} />
</div>
),
meta: {
headerTitle: "Conf.",
headerClassName: DATA_GRID_CELL_PAD_LAST,
cellClassName: cn(DATA_GRID_CELL_PAD_LAST, "text-center"),
},
},
],
[bestId],
)
const table = useTable({
features: dataGridFeatures,
manualPagination: true,
data: indexed,
columns,
getRowId: (row) => row.id,
})
return (
<div>
<DataGridShell
table={table}
recordCount={visibleRoutes.length}
tableClassNames={{
headerRow: "border-b bg-muted/20",
bodyRow: cn("group/row hover:bg-muted/30", "has-[[data-route-best=true]]:bg-emerald-500/5"),
}}
tableLayout={{ dense: true }}
/>
{routes.length > 5 && (
<button
type="button"
onClick={() => setExpanded((v) => !v)}
className="w-full py-2 text-xs text-muted-foreground hover:text-foreground transition-colors border-t flex items-center justify-center gap-1"
>
{expanded ? (
<>
<ChevronUpIcon className="size-3" />
Свернуть
</>
) : (
<>
<ChevronDownIcon className="size-3" />
Показать все {routes.length} комбинаций
</>
)}
</button>
)}
</div>
)
}
export { RouteOptimizerFullRoutesDataGrid, type RouteOptimizerFullRoutesDataGridProps }