From c95e63014f15bd259393e752f6291722235a7564 Mon Sep 17 00:00:00 2001 From: coolcoala Date: Sat, 9 Aug 2025 02:38:32 +0300 Subject: [PATCH] the connections page has been slightly revised. --- .../connection/connection-table.tsx | 473 +++++++++++------- src/pages/connections.tsx | 315 ++++++------ src/services/types.d.ts | 2 +- 3 files changed, 452 insertions(+), 338 deletions(-) diff --git a/src/components/connection/connection-table.tsx b/src/components/connection/connection-table.tsx index d7577c72..2cf1d2e1 100644 --- a/src/components/connection/connection-table.tsx +++ b/src/components/connection/connection-table.tsx @@ -1,15 +1,14 @@ import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; -import React, { useMemo, useState, useEffect, RefObject } from "react"; +import React, { useMemo, useState, useEffect, useRef } from "react"; import { ColumnDef, flexRender, getCoreRowModel, useReactTable, - Row, + Header, ColumnSizingState, } from "@tanstack/react-table"; -import { TableVirtuoso, TableComponents } from "react-virtuoso"; import { Table, @@ -27,7 +26,30 @@ import { cn } from "@root/lib/utils"; dayjs.extend(relativeTime); -// Интерфейс для строки данных, которую использует react-table +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; @@ -45,29 +67,81 @@ interface ConnectionRow { 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) : {}; + 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 {}; + 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), + "connection-table-widths", + JSON.stringify(columnSizing) ); }, [columnSizing]); @@ -77,13 +151,13 @@ export const ConnectionTable = (props: Props) => { 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}`; + ? `${metadata.destinationIP}:${metadata.destinationPort}` + : `${metadata.remoteDestination}:${metadata.destinationPort}`; return { id: each.id, host: metadata.host - ? `${metadata.host}:${metadata.destinationPort}` - : `${metadata.remoteDestination}:${metadata.destinationPort}`, + ? `${metadata.host}:${metadata.destinationPort}` + : `${metadata.remoteDestination}:${metadata.destinationPort}`, download: each.download, upload: each.upload, dlSpeed: each.curDownload ?? 0, @@ -101,102 +175,118 @@ export const ConnectionTable = (props: Props) => { }, [connections]); const columns = useMemo[]>( - () => [ - { - accessorKey: "host", - header: () => t("Host"), - size: columnSizing?.host || 220, - minSize: 180, - }, - { - accessorKey: "download", - header: () => t("Downloaded"), - size: columnSizing?.download || 88, - cell: ({ getValue }) => ( -
- {parseTraffic(getValue()).join(" ")} -
- ), - }, - { - accessorKey: "upload", - header: () => t("Uploaded"), - size: columnSizing?.upload || 88, - cell: ({ getValue }) => ( -
- {parseTraffic(getValue()).join(" ")} -
- ), - }, - { - accessorKey: "dlSpeed", - header: () => t("DL Speed"), - size: columnSizing?.dlSpeed || 88, - cell: ({ getValue }) => ( -
- {parseTraffic(getValue()).join(" ")}/s -
- ), - }, - { - accessorKey: "ulSpeed", - header: () => t("UL Speed"), - size: columnSizing?.ulSpeed || 88, - cell: ({ getValue }) => ( -
- {parseTraffic(getValue()).join(" ")}/s -
- ), - }, - { - accessorKey: "chains", - header: () => t("Chains"), - size: columnSizing?.chains || 340, - minSize: 180, - }, - { - accessorKey: "rule", - header: () => t("Rule"), - size: columnSizing?.rule || 280, - minSize: 180, - }, - { - accessorKey: "process", - header: () => t("Process"), - size: columnSizing?.process || 220, - minSize: 180, - }, - { - accessorKey: "time", - header: () => t("Time"), - size: columnSizing?.time || 120, - minSize: 100, - cell: ({ getValue }) => ( -
- {dayjs(getValue()).fromNow()} -
- ), - }, - { - accessorKey: "source", - header: () => t("Source"), - size: columnSizing?.source || 200, - minSize: 130, - }, - { - accessorKey: "remoteDestination", - header: () => t("Destination"), - size: columnSizing?.remoteDestination || 200, - minSize: 130, - }, - { - accessorKey: "type", - header: () => t("Type"), - size: columnSizing?.type || 160, - minSize: 100, - }, - ], - [columnSizing], + () => [ + { + 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({ @@ -206,92 +296,91 @@ export const ConnectionTable = (props: Props) => { onColumnSizingChange: setColumnSizing, getCoreRowModel: getCoreRowModel(), columnResizeMode: "onChange", + enableColumnResizing: true, }); - const VirtuosoTableComponents = useMemo>>( - () => ({ - // Явно типизируем `ref` для каждого компонента - Scroller: React.forwardRef((props, ref) => ( -
- )), - Table: (props) => , - TableHead: React.forwardRef((props, ref) => ( - - )), - // Явно типизируем пропсы и `ref` для TableRow - TableRow: React.forwardRef< - HTMLTableRowElement, - { item: Row } & React.HTMLAttributes - >(({ item: row, ...props }, ref) => { - // `Virtuoso` передает нам готовую строку `row` в пропсе `item`. - // Больше не нужно искать ее по индексу! - return ( - onShowDetail(row.original.connectionData)} - /> - ); - }), - TableBody: React.forwardRef((props, ref) => ( - - )), - }), - [], - ); + const totalTableWidth = useMemo(() => { + return table.getCenterTotalSize(); + }, [table.getState().columnSizing]); + + if (connRows.length === 0) { + return ( +
+

{t("No connections")}

+
+ ); + } return ( -
- {connRows.length > 0 ? ( - - table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ))} - - )) - } - itemContent={(index, row) => ( - <> - {row.getVisibleCells().map((cell) => ( - onShowDetail(row.original.connectionData)} +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )} - /> - ) : ( -
-

No results.

-
- )} - + {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())} + + ))} + + ))} + +
+
); -}; +}; \ No newline at end of file diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index 2597ef00..19714aea 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -49,18 +49,37 @@ import { PauseCircle, ArrowDown, ArrowUp, - Menu, } from "lucide-react"; import {SidebarTrigger} from "@/components/ui/sidebar"; +interface IConnectionsItem { + id: string; + metadata: { + host: string; + destinationIP: string; + process?: string; + }; + start?: string; + curUpload?: number; + curDownload?: number; +} + +interface IConnections { + uploadTotal: number; + downloadTotal: number; + connections: IConnectionsItem[]; + data: IConnectionsItem[]; +} + +type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]; + const initConn: IConnections = { uploadTotal: 0, downloadTotal: 0, connections: [], + data: [], }; -type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]; - const ConnectionsPage = () => { const { t } = useTranslation(); const pageVisible = useVisibility(); @@ -72,14 +91,14 @@ const ConnectionsPage = () => { const orderOpts: Record = { Default: (list) => - list.sort( - (a, b) => - new Date(b.start || "0").getTime()! - - new Date(a.start || "0").getTime()!, - ), - "Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!), + list.sort( + (a, b) => + new Date(b.start || "0").getTime()! - + new Date(a.start || "0").getTime()!, + ), + "Upload Speed": (list) => list.sort((a, b) => (b.curUpload ?? 0) - (a.curUpload ?? 0)), "Download Speed": (list) => - list.sort((a, b) => b.curDownload! - a.curDownload!), + list.sort((a, b) => (b.curDownload ?? 0) - (a.curDownload ?? 0)), }; const [isPaused, setIsPaused] = useState(false); @@ -91,6 +110,7 @@ const ConnectionsPage = () => { uploadTotal: connections.uploadTotal, downloadTotal: connections.downloadTotal, connections: connections.data, + data: connections.data, }; if (isPaused) return frozenData ?? currentData; return currentData; @@ -101,7 +121,7 @@ const ConnectionsPage = () => { let conns = displayData.connections.filter((conn) => { const { host, destinationIP, process } = conn.metadata; return ( - match(host || "") || match(destinationIP || "") || match(process || "") + match(host || "") || match(destinationIP || "") || match(process || "") ); }); if (orderFunc) conns = orderFunc(conns); @@ -109,24 +129,24 @@ const ConnectionsPage = () => { }, [displayData, match, curOrderOpt]); const [scrollingElement, setScrollingElement] = useState< - HTMLElement | Window | null + HTMLElement | Window | null >(null); const [isScrolled, setIsScrolled] = useState(false); const scrollerRefCallback = useCallback( - (node: HTMLElement | Window | null) => { - setScrollingElement(node); - }, - [], + (node: HTMLElement | Window | null) => { + setScrollingElement(node); + }, + [], ); useEffect(() => { if (!scrollingElement) return; const handleScroll = () => { const scrollTop = - scrollingElement instanceof Window - ? scrollingElement.scrollY - : scrollingElement.scrollTop; + scrollingElement instanceof Window + ? scrollingElement.scrollY + : scrollingElement.scrollTop; setIsScrolled(scrollTop > 5); }; @@ -137,8 +157,8 @@ const ConnectionsPage = () => { const onCloseAll = useLockFn(closeAllConnections); const detailRef = useRef(null!); const handleSearch = useCallback( - (m: (content: string) => boolean) => setMatch(() => m), - [], + (m: (content: string) => boolean) => setMatch(() => m), + [], ); const handlePauseToggle = useCallback(() => { setIsPaused((prev) => { @@ -147,6 +167,7 @@ const ConnectionsPage = () => { uploadTotal: connections.uploadTotal, downloadTotal: connections.downloadTotal, connections: connections.data, + data: connections.data, }); } else { setFrozenData(null); @@ -155,134 +176,138 @@ const ConnectionsPage = () => { }); }, [connections]); - return ( -
-
-
-
- -
-

- {t("Connections")} -

- -
-
-
- - {parseTraffic(displayData.downloadTotal)} -
-
- - {parseTraffic(displayData.uploadTotal)} -
-
- - - - - - -

{isTableLayout ? t("List View") : t("Table View")}

-
-
- - - - - -

{isPaused ? t("Resume") : t("Pause")}

-
-
- -
-
-
-
- {!isTableLayout && ( - - )} -
- -
-
-
+ const headerHeight = "7rem"; -
- {filterConn.length === 0 ? ( - - ) : isTableLayout ? ( -
- detailRef.current?.open(detail)} - scrollerRef={scrollerRefCallback} - /> + return ( +
+
+
+
+ +
+

+ {t("Connections")} +

+ +
+
+
+ + {parseTraffic(displayData.downloadTotal)} +
+
+ + {parseTraffic(displayData.uploadTotal)} +
+
+ + + + + + +

{isTableLayout ? t("List View") : t("Table View")}

+
+
+ + + + + +

{isPaused ? t("Resume") : t("Pause")}

+
+
+ +
+
- ) : ( - ( - detailRef.current?.open(item)} - /> +
+ {!isTableLayout && ( + )} - /> - )} - +
+ +
+
+
+ +
+ {filterConn.length === 0 ? ( + + ) : isTableLayout ? ( +
+ detailRef.current?.open(detail)} + scrollerRef={scrollerRefCallback} + /> +
+ ) : ( + ( + detailRef.current?.open(item)} + /> + )} + /> + )} + +
-
); }; -export default ConnectionsPage; +export default ConnectionsPage; \ No newline at end of file diff --git a/src/services/types.d.ts b/src/services/types.d.ts index e4878211..92181038 100644 --- a/src/services/types.d.ts +++ b/src/services/types.d.ts @@ -154,7 +154,7 @@ interface IConnectionsItem { start: string; chains: string[]; rule: string; - rulePayload: string; + rulePayload?: string; curUpload?: number; // upload speed, calculate at runtime curDownload?: number; // download speed, calculate at runtime }