"use client" import { useMemo } from "react" import { type ColumnDef, useTable, } from "@tanstack/react-table" import type { Server, ServerType } from "@/lib/data" import { StatusBadge } from "@/components/status-badge" import { Flag } from "@/components/flag" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell" import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid/data-grid" 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 { ServerExpandedDetail } from "@/components/data-grids/server-expanded-detail" import { ChevronRightIcon, ChevronDownIcon, HomeIcon, ServerIcon, NetworkIcon, ShieldIcon, WifiIcon, MoreHorizontalIcon, ExternalLinkIcon, PencilIcon, RefreshCwIcon, PowerIcon, Trash2Icon, } from "lucide-react" const TYPE_LABELS: Record = { "jump-host": "JumpHost", "exit-node": "Exit Node", "home-router": "Home Router", } const TYPE_STYLES: Record = { "jump-host": "bg-violet-500/10 text-violet-400 border-violet-500/20", "exit-node": "bg-sky-500/10 text-sky-400 border-sky-500/20", "home-router": "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", } function rosVer(os: string): number { const m = os.match(/(\d+)\.(\d+)/) if (!m) return 0 return parseInt(m[1], 10) * 100 + parseInt(m[2], 10) } function TypeBadge({ type }: { type: ServerType }) { const icon = type === "jump-host" ? : type === "exit-node" ? : return ( {icon}{TYPE_LABELS[type]} ) } function RosBadge({ os }: { os: string }) { const v = rosVer(os) const cls = v >= 715 ? "bg-[var(--status-online-bg)] text-[var(--status-online-fg)] border-current/20" : v >= 710 ? "bg-[var(--status-degraded-bg)] text-[var(--status-degraded-fg)] border-current/20" : "bg-[var(--status-offline-bg)] text-[var(--status-offline-fg)] border-current/20" return {os} } interface ServerRowActionsProps { canWrite: boolean busy: boolean server: Server isLive: boolean isPolling: boolean onEdit: (server: Server) => void onDelete: (id: string) => void onPoll: (id: string) => void onToggleStatus: (id: string) => void } function ServerRowActions({ canWrite, busy, server, isLive, isPolling, onEdit, onDelete, onPoll, onToggleStatus, }: ServerRowActionsProps) { return (
e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} > e.stopPropagation()} aria-label={`Действия: ${server.name}`} > } /> window.open(`https://${server.host}`, "_blank")}> Открыть WebFig onEdit(server)}> Редактировать {isLive && ( onPoll(server.id)} disabled={!canWrite || isPolling}> {isPolling ? "Опрос…" : "Опросить"} )} onToggleStatus(server.id)}> {server.enabled ? "Отключить" : "Включить"} onDelete(server.id)}> Удалить
) } interface ServersDataGridProps { hasUnfilteredRows?: boolean canWrite: boolean busy?: boolean servers: Server[] isLive: boolean pollingIds: Set onPoll: (id: string) => void onEdit: (server: Server) => void onDelete: (id: string) => void onToggleStatus: (id: string) => void } function ServersDataGrid({ canWrite, hasUnfilteredRows = false, busy = false, servers, isLive, pollingIds, onPoll, onEdit, onDelete, onToggleStatus, }: ServersDataGridProps) { const columns = useMemo[]>( () => [ { id: "name", accessorKey: "name", header: ({ column }) => ( ), cell: ({ row }) => { const s = row.original const expanded = row.getIsExpanded() return (

{s.name}

{s.host}

{s.ipv6Address && (

{s.ipv6Address}

)}
) }, meta: { headerTitle: "Имя / Хост", headerClassName: DATA_GRID_CELL_PAD_FIRST, cellClassName: DATA_GRID_CELL_PAD_FIRST, expandedContent: (row: Server) => ( onPoll(row.id)} /> ), }, }, { id: "type", accessorKey: "type", header: ({ column }) => , cell: ({ row }) => , meta: { headerTitle: "Тип", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "model", accessorKey: "model", header: ({ column }) => , cell: ({ row }) => {row.original.model}, meta: { headerTitle: "Модель", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "os", accessorKey: "os", header: ({ column }) => , cell: ({ row }) => , meta: { headerTitle: "RouterOS", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "site", accessorKey: "site", header: ({ column }) => , cell: ({ row }) => (
{row.original.site}
), meta: { headerTitle: "Площадка", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "wan", header: () => ( WAN / LAN ), enableSorting: false, cell: ({ row }) => { const s = row.original if (s.type === "home-router" && s.wanUplinks?.length) { return (
{s.wanUplinks.map((w) => (
{w.name} {w.isp} ↓{w.maxDl}↑{w.maxUl}
))} {s.lanSubnet && (
LAN {s.lanSubnet}
)}
) } return (
{s.wireGuardIfaces && s.wireGuardIfaces.length > 0 && (
WG: {s.wireGuardIfaces.length} iface · {s.wireGuardIfaces.reduce((n, i) => n + i.peers.length, 0)} peers
)} {s.rpkiEnabled && (
RPKI ✓
)} {!s.wireGuardIfaces?.length && !s.rpkiEnabled && ( — )}
) }, meta: { headerTitle: "WAN / LAN", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "latency", accessorKey: "latency", header: ({ column }) => ( ), cell: ({ row }) => { const s = row.original return ( 60 ? "text-[var(--status-degraded-fg)]" : "", )}> {s.latency == null ? "—" : `${s.latency} мс`} ) }, meta: { headerTitle: "Задержка", headerClassName: cn(DATA_GRID_CELL_PAD, "text-right"), cellClassName: cn(DATA_GRID_CELL_PAD, "text-right"), }, }, { id: "status", accessorKey: "status", header: ({ column }) => , cell: ({ row }) =>
{!row.original.enabled && Отключён}
, meta: { headerTitle: "Статус", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "actions", header: () => Действия, cell: ({ row }) => { const s = row.original return ( ) }, enableSorting: false, size: 56, meta: { headerClassName: DATA_GRID_CELL_PAD_LAST, cellClassName: DATA_GRID_CELL_PAD_LAST, }, }, ], [canWrite, busy, isLive, onDelete, onEdit, onPoll, onToggleStatus, pollingIds], ) const table = useTable({ features: dataGridFeatures, data: servers, columns, manualPagination: false, initialState: { pagination: { pageIndex: 0, pageSize: 50 } }, getRowId: (row) => row.id, getRowCanExpand: () => true, }) if (servers.length === 0) { return ( } title={hasUnfilteredRows ? "Нет совпадений" : "Нет серверов"} description={hasUnfilteredRows ? "Измените поиск или сбросьте фильтры" : canWrite ? "Добавьте первый MikroTik-сервер для мониторинга" : "Серверов нет. Добавление доступно пользователю с правом записи."} className="border-0 py-12" /> ) } return ( 50 || table.state.pagination.pageSize !== 50} onRowClick={(row) => table.getRow(row.id).toggleExpanded()} /> ) } export { ServersDataGrid, rosVer, RosBadge, TypeBadge }