"use client" import { useMemo, useState } from "react" import { type ColumnDef, getCoreRowModel, useReactTable } from "@tanstack/react-table" import type { FlowMapEdge } from "@mmapp/contracts/traffic-flow" import { Frame, FramePanel } from "@/components/reui/frame" 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 { Flag } from "@/components/flag" import { fmtRate } from "@/lib/fmt-rate" const W = 640 const H = 280 const STROKES = [ "var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--color-success)", "var(--chart-5)", ] function formatBytes(n: number): string { if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)} ГБ` if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)} МБ` if (n >= 1000) return `${(n / 1000).toFixed(1)} КБ` return `${n} Б` } function FlowTrafficMap({ edges, onSelectCountry, }: { edges: FlowMapEdge[] onSelectCountry?: (country: string) => void }) { const [hover, setHover] = useState(null) const layout = useMemo(() => { const sources = [...new Map(edges.map((e) => [e.fromId, e])).values()] const dests = [...new Map(edges.map((e) => [e.toCountry, e])).values()] const srcY = (i: number) => sources.length <= 1 ? H / 2 : 36 + (i * (H - 72)) / Math.max(1, sources.length - 1) const dstY = (i: number) => dests.length <= 1 ? H / 2 : 36 + (i * (H - 72)) / Math.max(1, dests.length - 1) const srcPos = new Map(sources.map((s, i) => [s.fromId, { x: 88, y: srcY(i), label: s.fromLabel, country: s.fromCountry }])) const dstPos = new Map(dests.map((d, i) => [d.toCountry, { x: 552, y: dstY(i), country: d.toCountry }])) const maxBytes = Math.max(...edges.map((e) => e.bytes), 1) return { srcPos, dstPos, maxBytes } }, [edges]) const columns = useMemo[]>( () => [ { id: "from", accessorKey: "fromLabel", header: () => Источник, cell: ({ row }) => ( {row.original.fromCountry && row.original.fromCountry !== "UN" ? : null} {row.original.fromLabel} ), meta: { headerClassName: DATA_GRID_CELL_PAD_FIRST, cellClassName: DATA_GRID_CELL_PAD_FIRST }, }, { id: "to", accessorKey: "toCountry", header: () => Назначение, cell: ({ row }) => ( {/^[a-z]{2}$/i.test(row.original.toCountry) ? : null} {row.original.toCountry} {row.original.toAsn ? AS{row.original.toAsn} : null} ), meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "cat", accessorKey: "category", header: () => Категория, cell: ({ row }) => {row.original.category}, meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "rate", accessorFn: (r) => r.bps, header: () => Скорость, cell: ({ row }) => {fmtRate(row.original.bps / 1_000_000)}, meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "bytes", accessorKey: "bytes", header: () => Байты, cell: ({ row }) => {formatBytes(row.original.bytes)}, meta: { headerClassName: DATA_GRID_CELL_PAD_LAST, cellClassName: DATA_GRID_CELL_PAD_LAST }, }, ], [], ) const table = useReactTable({ data: edges, columns, getCoreRowModel: getCoreRowModel(), getRowId: (row, i) => `${row.fromId}-${row.toCountry}-${i}`, }) if (!edges.length) { return (

Страны подтянутся из кэша RIPEstat

) } return (
{edges.map((e, i) => { const from = layout.srcPos.get(e.fromId) const to = layout.dstPos.get(e.toCountry) if (!from || !to) return null const id = `${e.fromId}-${e.toCountry}` const midX = (from.x + to.x) / 2 const d = `M ${from.x} ${from.y} C ${midX} ${from.y}, ${midX} ${to.y}, ${to.x} ${to.y}` const sw = 1.25 + 7 * (e.bytes / layout.maxBytes) const active = hover === id return ( setHover(id)} onMouseLeave={() => setHover(null)} onClick={() => onSelectCountry?.(e.toCountry)} > {`${e.fromLabel} → ${e.toCountry} · ${e.category} · ${formatBytes(e.bytes)}`} ) })} {[...layout.srcPos.values()].map((n) => ( {n.label} ))} {[...layout.dstPos.values()].map((n) => ( {n.country} ))} {hover ? (

Нажмите дугу, чтобы отфильтровать сессии по стране назначения

) : null}
onSelectCountry?.(row.toCountry)} />
) } export { FlowTrafficMap }