import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import React, { useMemo, useState, useEffect, useRef } from "react"; import { ColumnDef, flexRender, getCoreRowModel, useReactTable, Header, ColumnSizingState, } from "@tanstack/react-table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { truncateStr } from "@/utils/truncate-str"; import parseTraffic from "@/utils/parse-traffic"; import { t } from "i18next"; import { cn } from "@root/lib/utils"; dayjs.extend(relativeTime); interface IConnectionsItem { id: string; metadata: { host: string; destinationIP: string; destinationPort: string; remoteDestination: string; process?: string; processPath?: string; sourceIP: string; sourcePort: string; type: string; network: string; }; rule: string; rulePayload?: string; chains: string[]; download: number; upload: number; curDownload?: number; curUpload?: number; start: string; } interface ConnectionRow { id: string; host: string; download: number; upload: number; dlSpeed: number; ulSpeed: number; chains: string; rule: string; process: string; time: string; source: string; remoteDestination: string; type: string; connectionData: IConnectionsItem; } interface Props { connections: IConnectionsItem[]; onShowDetail: (data: IConnectionsItem) => void; scrollerRef: (element: HTMLElement | Window | null) => void; } const ColumnResizer = ({ header, }: { header: Header; }) => { return (
); }; export const ConnectionTable = (props: Props) => { const { connections, onShowDetail, scrollerRef } = props; const tableContainerRef = useRef(null); useEffect(() => { if (tableContainerRef.current && scrollerRef) { scrollerRef(tableContainerRef.current); } }, [scrollerRef]); const [columnSizing, setColumnSizing] = useState(() => { try { const saved = localStorage.getItem("connection-table-widths"); return saved ? JSON.parse(saved) : { host: 220, download: 88, upload: 88, dlSpeed: 88, ulSpeed: 88, chains: 340, rule: 280, process: 220, time: 120, source: 200, remoteDestination: 200, type: 160, }; } catch { return { host: 220, download: 88, upload: 88, dlSpeed: 88, ulSpeed: 88, chains: 340, rule: 280, process: 220, time: 120, source: 200, remoteDestination: 200, type: 160, }; } }); useEffect(() => { localStorage.setItem( "connection-table-widths", JSON.stringify(columnSizing), ); }, [columnSizing]); const connRows = useMemo((): ConnectionRow[] => { return connections.map((each) => { const { metadata, rulePayload } = each; const chains = [...each.chains].reverse().join(" / "); const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule; const Destination = metadata.destinationIP ? `${metadata.destinationIP}:${metadata.destinationPort}` : `${metadata.remoteDestination}:${metadata.destinationPort}`; return { id: each.id, host: metadata.host ? `${metadata.host}:${metadata.destinationPort}` : `${metadata.remoteDestination}:${metadata.destinationPort}`, download: each.download, upload: each.upload, dlSpeed: each.curDownload ?? 0, ulSpeed: each.curUpload ?? 0, chains, rule, process: truncateStr(metadata.process || metadata.processPath) ?? "", time: each.start, source: `${metadata.sourceIP}:${metadata.sourcePort}`, remoteDestination: Destination, type: `${metadata.type}(${metadata.network})`, connectionData: each, }; }); }, [connections]); const columns = useMemo[]>( () => [ { accessorKey: "host", header: () => t("Host"), size: columnSizing?.host || 220, minSize: 180, maxSize: 400, }, { accessorKey: "download", header: () => t("Downloaded"), size: columnSizing?.download || 88, minSize: 80, maxSize: 150, cell: ({ getValue }) => (
{parseTraffic(getValue()).join(" ")}
), }, { accessorKey: "upload", header: () => t("Uploaded"), size: columnSizing?.upload || 88, minSize: 80, maxSize: 150, cell: ({ getValue }) => (
{parseTraffic(getValue()).join(" ")}
), }, { accessorKey: "dlSpeed", header: () => t("DL Speed"), size: columnSizing?.dlSpeed || 88, minSize: 80, maxSize: 150, cell: ({ getValue }) => (
{parseTraffic(getValue()).join(" ")}/s
), }, { accessorKey: "ulSpeed", header: () => t("UL Speed"), size: columnSizing?.ulSpeed || 88, minSize: 80, maxSize: 150, cell: ({ getValue }) => (
{parseTraffic(getValue()).join(" ")}/s
), }, { accessorKey: "chains", header: () => t("Chains"), size: columnSizing?.chains || 340, minSize: 180, maxSize: 500, }, { accessorKey: "rule", header: () => t("Rule"), size: columnSizing?.rule || 280, minSize: 180, maxSize: 400, }, { accessorKey: "process", header: () => t("Process"), size: columnSizing?.process || 220, minSize: 180, maxSize: 350, }, { accessorKey: "time", header: () => t("Time"), size: columnSizing?.time || 120, minSize: 100, maxSize: 180, cell: ({ getValue }) => (
{dayjs(getValue()).fromNow()}
), }, { accessorKey: "source", header: () => t("Source"), size: columnSizing?.source || 200, minSize: 130, maxSize: 300, }, { accessorKey: "remoteDestination", header: () => t("Destination"), size: columnSizing?.remoteDestination || 200, minSize: 130, maxSize: 300, }, { accessorKey: "type", header: () => t("Type"), size: columnSizing?.type || 160, minSize: 100, maxSize: 220, }, ], [columnSizing], ); const table = useReactTable({ data: connRows, columns, state: { columnSizing }, onColumnSizingChange: setColumnSizing, getCoreRowModel: getCoreRowModel(), columnResizeMode: "onChange", enableColumnResizing: true, }); const totalTableWidth = useMemo(() => { return table.getCenterTotalSize(); }, [table.getState().columnSizing]); if (connRows.length === 0) { return (

{t("No connections")}

); } return (
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => (
{header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )}
{header.column.getCanResize() && ( )}
))}
))}
{table.getRowModel().rows.map((row) => ( onShowDetail(row.original.connectionData)} > {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} ))}
); };