"use client" import { useMemo } from "react" import { type ColumnDef, getCoreRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table" import type { FirewallRule } from "@/lib/data" import { FormToggle } from "@/components/form-kit" 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 { DATA_GRID_CELL_PAD, DATA_GRID_CELL_PAD_FIRST, DATA_GRID_CELL_PAD_LAST, DATA_GRID_CONTAINER_CLASS, } from "@/components/data-grids/shared/data-grid-layout" import { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sort-header" import { DataGridContainer, DataGridTableDndRowHandle, DataGridTableDndRows } from "@/components/reui/data-grid" import type { DragEndEvent } from "@dnd-kit/core" import { EmptyState } from "@/components/empty-state" import { CopyIcon, MoreHorizontalIcon, PencilIcon, PowerIcon, ShieldOffIcon, Trash2Icon, } from "lucide-react" const ACTION_STYLES: Record = { accept: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20", drop: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20", reject: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20", masquerade: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20", "mark-routing": "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20", "mark-conn": "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20", "fasttrack-connection": "bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20", "dst-nat": "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20", "src-nat": "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20", "add-src-to-address-list": "bg-orange-500/10 text-orange-600 dark:text-orange-400 border-orange-500/20", } const CHAIN_STYLES: Record = { forward: "bg-foreground/5 text-foreground/70", input: "bg-violet-500/10 text-violet-600 dark:text-violet-400", output: "bg-sky-500/10 text-sky-600 dark:text-sky-400", srcnat: "bg-blue-500/10 text-blue-600 dark:text-blue-400", dstnat: "bg-blue-500/10 text-blue-600 dark:text-blue-400", prerouting: "bg-amber-500/10 text-amber-600 dark:text-amber-400", postrouting: "bg-amber-500/10 text-amber-600 dark:text-amber-400", "ip6-input": "bg-violet-500/10 text-violet-600 dark:text-violet-400", "ip6-forward": "bg-foreground/5 text-foreground/70", "ip6-output": "bg-sky-500/10 text-sky-600 dark:text-sky-400", } function fmtHits(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}М` if (n >= 1_000) return `${(n / 1_000).toFixed(0)}к` return String(n) } function ActionBadge({ action }: { action: string }) { const cls = ACTION_STYLES[action] ?? "bg-muted text-muted-foreground border-border" return ( {action} ) } function ChainBadge({ chain }: { chain: string }) { const cls = CHAIN_STYLES[chain] ?? "bg-muted text-muted-foreground" return ( {chain} ) } interface FirewallRulesDataGridProps { rules: FirewallRule[] onToggle: (id: string) => void onEdit: (rule: FirewallRule) => void onDelete?: (rule: FirewallRule) => void onReorder?: (activeId: string, overId: string) => void showServer?: boolean } function FirewallRulesDataGrid({ rules, onToggle, onEdit, onDelete, onReorder, showServer = false, }: FirewallRulesDataGridProps) { const indexedRules = useMemo( () => rules.map((rule, index) => ({ ...rule, _index: index + 1 })), [rules], ) const reorderable = Boolean(onReorder) const indexPad = reorderable ? DATA_GRID_CELL_PAD : DATA_GRID_CELL_PAD_FIRST const columns = useMemo[]>( () => [ ...(reorderable ? [{ id: "drag", header: () => Порядок, enableSorting: false, cell: () => , size: 40, meta: { headerClassName: DATA_GRID_CELL_PAD_FIRST, cellClassName: DATA_GRID_CELL_PAD_FIRST, }, } satisfies ColumnDef] : []), { id: "index", accessorKey: "_index", header: () => #, enableSorting: false, cell: ({ row }) => ( {row.original._index} ), size: 48, meta: { headerClassName: indexPad, cellClassName: indexPad, }, }, ...(showServer ? [{ id: "server", accessorFn: (row: FirewallRule & { _index: number }) => row.serverName ?? row.serverId ?? "", header: ({ column }) => , cell: ({ row }) => ( {row.original.serverName || row.original.serverId || "—"} ), meta: { headerTitle: "Сервер", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, } satisfies ColumnDef] : []), { id: "chain", accessorKey: "chain", header: ({ column }) => , cell: ({ row }) => , meta: { headerTitle: "Цепочка", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "action", accessorKey: "action", header: ({ column }) => , cell: ({ row }) => , meta: { headerTitle: "Действие", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "src", accessorKey: "src", header: ({ column }) => , cell: ({ row }) => ( {row.original.src || "any"} ), meta: { headerTitle: "Источник", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "dst", accessorKey: "dst", header: ({ column }) => , cell: ({ row }) => ( {row.original.dst || "any"} ), meta: { headerTitle: "Назначение", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "proto", accessorKey: "proto", header: ({ column }) => , cell: ({ row }) => {row.original.proto}, meta: { headerTitle: "Протокол", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "port", accessorKey: "port", header: ({ column }) => , cell: ({ row }) => ( {row.original.port || "—"} ), meta: { headerTitle: "Порт", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "iface", accessorKey: "iface", header: ({ column }) => , cell: ({ row }) => ( {row.original.iface || "—"} ), meta: { headerTitle: "Интерфейс", headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "hits", accessorKey: "hits", header: ({ column }) => ( ), cell: ({ row }) => { const hits = row.original.hits return ( 1_000_000 ? "text-emerald-600 dark:text-emerald-400 font-semibold" : hits > 10_000 ? "text-foreground" : "text-muted-foreground", )} > {fmtHits(hits)} ) }, meta: { headerTitle: "Пакетов", headerClassName: cn(DATA_GRID_CELL_PAD, "text-right"), cellClassName: cn(DATA_GRID_CELL_PAD, "text-right"), }, }, { id: "enabled", accessorKey: "enabled", header: () => Вкл, enableSorting: false, cell: ({ row }) => (
e.stopPropagation()}> onToggle(row.original.id)} />
), meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD }, }, { id: "actions", header: () => Действия, enableSorting: false, cell: ({ row }) => { const r = row.original return (
e.stopPropagation()}> } /> onEdit(r)}> Редактировать Дублировать onToggle(r.id)}> {r.enabled ? "Отключить" : "Включить"} onDelete?.(r)}> Удалить правило
) }, size: 56, meta: { headerClassName: DATA_GRID_CELL_PAD_LAST, cellClassName: DATA_GRID_CELL_PAD_LAST }, }, ], [onEdit, onToggle, onDelete, showServer, reorderable, indexPad], ) const table = useReactTable({ data: indexedRules, columns, getCoreRowModel: getCoreRowModel(), ...(reorderable ? {} : { getSortedRowModel: getSortedRowModel() }), enableSorting: !reorderable, getRowId: (row) => row.id, }) function handleDragEnd(event: DragEndEvent) { const { active, over } = event if (!over || active.id === over.id || !onReorder) return onReorder(String(active.id), String(over.id)) } if (rules.length === 0) { return ( } title="Правила не найдены" description="Попробуйте изменить фильтр или добавьте новое правило" className="border-0 py-16" /> ) } return ( {reorderable ? ( r.id)} handleDragEnd={handleDragEnd} /> ) : undefined} ) } export { FirewallRulesDataGrid, type FirewallRulesDataGridProps, ActionBadge, ChainBadge }