From 31b2439f21a6d157d4e94232b923b36532318a53 Mon Sep 17 00:00:00 2001 From: Dreamacro <8615343+Dreamacro@users.noreply.github.com> Date: Sun, 1 Nov 2020 23:27:28 +0800 Subject: [PATCH] Chore: refactor connections table --- src/containers/Connections/index.tsx | 270 ++++++++++++++------------ src/containers/Connections/store.ts | 13 +- src/containers/Connections/style.scss | 5 + 3 files changed, 160 insertions(+), 128 deletions(-) diff --git a/src/containers/Connections/index.tsx b/src/containers/Connections/index.tsx index 7b32264..79fa1b8 100644 --- a/src/containers/Connections/index.tsx +++ b/src/containers/Connections/index.tsx @@ -1,12 +1,11 @@ -import React, { useMemo, useLayoutEffect } from 'react' -import { useBlockLayout, useResizeColumns, useTable } from 'react-table' +import React, { useMemo, useLayoutEffect, useCallback } from 'react' +import { Cell, Column, ColumnInstance, TableOptions, useBlockLayout, useResizeColumns, UseResizeColumnsColumnProps, UseResizeColumnsOptions, useSortBy, UseSortByColumnOptions, UseSortByColumnProps, UseSortByOptions, useTable } from 'react-table' import classnames from 'classnames' import { Header, Card, Checkbox, Modal, Icon } from '@components' import { useI18n } from '@stores' import * as API from '@lib/request' import { StreamReader } from '@lib/streamer' import { useObject, useVisible } from '@lib/hook' -import { noop } from '@lib/helper' import { fromNow } from '@lib/date' import { RuleType } from '@models' import { useConnections } from './store' @@ -24,21 +23,23 @@ enum Columns { Time = 'time' } -const columnsPair: [string, number][] = [ - [Columns.Host, 260], - [Columns.Network, 80], - [Columns.Type, 120], - [Columns.Chains, 200], - [Columns.Rule, 140], - [Columns.Speed, 200], - [Columns.Upload, 100], - [Columns.Download, 100], - [Columns.Time, 120] -] const shouldCenter = new Set([Columns.Network, Columns.Type, Columns.Rule, Columns.Speed, Columns.Upload, Columns.Download, Columns.Time]) -const couldSort = new Set([Columns.Host, Columns.Network, Columns.Type, Columns.Rule, Columns.Upload, Columns.Download]) -function formatTraffic (num: number) { +interface TableColumn extends + ColumnInstance, + UseSortByColumnProps, + UseResizeColumnsColumnProps {} + +type TableColumnOption = + Column & + UseResizeColumnsOptions & + UseSortByColumnOptions + +interface ITableOptions extends + TableOptions, + UseSortByOptions {} + +function formatTraffic(num: number) { const s = ['B', 'KB', 'MB', 'GB', 'TB'] let idx = 0 while (~~(num / 1024) && idx < s.length) { @@ -49,20 +50,37 @@ function formatTraffic (num: number) { return `${idx === 0 ? num : num.toFixed(2)} ${s[idx]}` } -function formatSpeed (upload: number, download: number) { +function formatSpeed(upload: number, download: number) { switch (true) { - case upload === 0 && download === 0: - return '-' - case upload !== 0 && download !== 0: - return `↑ ${formatTraffic(upload)}/s ↓ ${formatTraffic(download)}/s` - case upload !== 0: - return `↑ ${formatTraffic(upload)}/s` - default: - return `↓ ${formatTraffic(download)}/s` + case upload === 0 && download === 0: + return '-' + case upload !== 0 && download !== 0: + return `↑ ${formatTraffic(upload)}/s ↓ ${formatTraffic(download)}/s` + case upload !== 0: + return `↑ ${formatTraffic(upload)}/s` + default: + return `↓ ${formatTraffic(download)}/s` } } -export default function Connections () { +interface formatConnection { + id: string + host: string + chains: string + rule: string + time: number + upload: number + download: number + type: string + network: string + speed: { + upload: number + download: number + } + completed: boolean +} + +export default function Connections() { const { translation, lang } = useI18n() const t = useMemo(() => translation('Connections').t, [translation]) @@ -72,81 +90,62 @@ export default function Connections () { downloadTotal: 0 }) - // sort - const [sort, setSort] = useObject({ - column: '', - asc: true - }) - function handleSort (column: string) { - if (column === sort.column) { - sort.asc - ? setSort('asc', false) - : setSort({ column: '', asc: true }) - } else { - setSort('column', column) - } - } - // close all connections const { visible, show, hide } = useVisible() - function handleCloseConnections () { + function handleCloseConnections() { API.closeAllConnections().finally(() => hide()) } // connections const { connections, feed, save, toggleSave } = useConnections() - const data = useMemo(() => { - return connections - .sort((a, b) => { - if (a.completed !== b.completed) { - return a.completed ? 1 : -1 - } - - const diffTime = new Date(a.start).getTime() - new Date(b.start).getTime() - if (diffTime !== 0) { - return diffTime - } - return a.id.localeCompare(b.id) - }) - .map(c => ({ - id: c.id, - host: `${c.metadata.host || c.metadata.destinationIP}:${c.metadata.destinationPort}`, - chains: c.chains.slice().reverse().join(' --> '), - rule: c.rule === RuleType.RuleSet ? `${c.rule}(${c.rulePayload})` : c.rule, - time: fromNow(new Date(c.start), lang), - upload: formatTraffic(c.upload), - download: formatTraffic(c.download), - type: c.metadata.type, - network: c.metadata.network.toUpperCase(), - speed: formatSpeed(c.speed.upload, c.speed.download), - completed: !!c.completed - })) - .sort((a, b) => { - if (sort.column !== '') { - const aValue = a[sort.column as keyof typeof a] as string - const bValue = b[sort.column as keyof typeof b] as string - return sort.asc - ? aValue.localeCompare(bValue) - : bValue.localeCompare(aValue) - } - return 0 - }) - }, [connections, lang, sort.asc, sort.column]) + const data: formatConnection[] = useMemo(() => connections.map( + c => ({ + id: c.id, + host: `${c.metadata.host || c.metadata.destinationIP}:${c.metadata.destinationPort}`, + chains: c.chains.slice().reverse().join(' / '), + rule: c.rule === RuleType.RuleSet ? `${c.rule}(${c.rulePayload})` : c.rule, + time: new Date(c.start).getTime(), + upload: c.upload, + download: c.download, + type: c.metadata.type, + network: c.metadata.network.toUpperCase(), + speed: { upload: c.uploadSpeed, download: c.downloadSpeed }, + completed: !!c.completed + }) + ), [connections]) // table - const columns = useMemo(() => columnsPair.map( - c => ({ - Header: t(`columns.${c[0]}`), - accessor: c[0], - minWidth: c[1], - width: c[1] - }) - ), [t]) + const columns: TableColumnOption[] = useMemo(() => [ + { Header: t(`columns.${Columns.Host}`), accessor: 'host', minWidth: 260, width: 260 }, + { Header: t(`columns.${Columns.Network}`), accessor: 'network', minWidth: 80, width: 80 }, + { Header: t(`columns.${Columns.Type}`), accessor: 'type', minWidth: 120, width: 120 }, + { Header: t(`columns.${Columns.Chains}`), accessor: 'chains', minWidth: 200, width: 200 }, + { Header: t(`columns.${Columns.Rule}`), accessor: 'rule', minWidth: 140, width: 140 }, + { + id: Columns.Speed, + Header: t(`columns.${Columns.Speed}`), + accessor(originalRow: formatConnection) { + return [originalRow.speed.upload, originalRow.speed.download] + }, + sortType(rowA, rowB) { + const speedA = rowA.original.speed + const speedB = rowB.original.speed + return speedA.download === speedB.download + ? speedA.upload - speedB.upload + : speedA.download - speedB.download + }, + minWidth: 200, width: 200, + sortDescFirst: true + }, + { Header: t(`columns.${Columns.Upload}`), accessor: 'upload', minWidth: 100, width: 100, sortDescFirst: true }, + { Header: t(`columns.${Columns.Download}`), accessor: 'download', minWidth: 100, width: 100, sortDescFirst: true }, + { Header: t(`columns.${Columns.Time}`), accessor: 'time', minWidth: 120, width: 120, sortType(rowA, rowB) { return rowB.original.time - rowA.original.time } }, + ] as TableColumnOption[], [t]) useLayoutEffect(() => { let streamReader: StreamReader | null = null - function handleConnection (snapshots: API.Snapshot[]) { + function handleConnection(snapshots: API.Snapshot[]) { for (const snapshot of snapshots) { setTraffic({ uploadTotal: snapshot.uploadTotal, @@ -177,39 +176,38 @@ export default function Connections () { rows, prepareRow } = useTable( - { columns: columns as any, data }, + { + columns, + data, + autoResetSortBy: false, + initialState: { sortBy: [{ id: Columns.Time, desc: false }] } + } as ITableOptions, + useResizeColumns, useBlockLayout, - useResizeColumns + useSortBy ) const headerGroup = useMemo(() => headerGroups[0], [headerGroups]) - const renderItem = useMemo(() => rows.map((row, i) => { - prepareRow(row) - return ( -
- { - row.cells.map((cell, j) => { - const classname = classnames( - 'connections-block', - { center: shouldCenter.has(cell.column.id), completed: !!(row.original as any).completed } - ) - return ( -
- { cell.render('Cell') } -
- ) - }) - } -
- ) - }), [prepareRow, rows]) + const renderCell = useCallback(function (cell: Cell) { + switch (cell.column.id) { + case Columns.Speed: + return formatSpeed(cell.value[0], cell.value[1]) + case Columns.Upload: + case Columns.Download: + return formatTraffic(cell.value) + case Columns.Time: + return fromNow(new Date(cell.value), lang) + default: + return cell.value + } + }, [lang]) return (
- { `(${t('total.text')}: ${t('total.upload')} ${formatTraffic(traffic.uploadTotal)} ${t('total.download')} ${formatTraffic(traffic.downloadTotal)})` } + {`(${t('total.text')}: ${t('total.upload')} ${formatTraffic(traffic.uploadTotal)} ${t('total.download')} ${formatTraffic(traffic.downloadTotal)})`} - { t('keepClosed') } + {t('keepClosed')}
@@ -217,16 +215,23 @@ export default function Connections () {
{ headerGroup.headers.map((column, idx) => { - const id = column.id - const handleClick = couldSort.has(id) ? () => handleSort(id) : noop + const realColumn = column as unknown as TableColumn + const id = realColumn.id return ( -
- { column.render('Header') } - { - sort.column === id && (sort.asc ? ' ↑' : ' ↓') - } +
+
+ {column.render('Header')} + { + realColumn.isSorted + ? realColumn.isSortedDesc ? ' ↓' : ' ↑' + : null + } +
{ idx !== headerGroup.headers.length - 1 && -
+
}
) @@ -235,11 +240,32 @@ export default function Connections () {
- { renderItem } + { + rows.map((row, i) => { + prepareRow(row) + return ( +
+ { + row.cells.map((cell, j) => { + const classname = classnames( + 'connections-block', + { center: shouldCenter.has(cell.column.id), completed: row.original.completed } + ) + return ( +
+ { renderCell(cell)} +
+ ) + }) + } +
+ ) + }) + }
- { t('closeAll.content') } + {t('closeAll.content')}
) } diff --git a/src/containers/Connections/store.ts b/src/containers/Connections/store.ts index c3e2d57..919daaf 100644 --- a/src/containers/Connections/store.ts +++ b/src/containers/Connections/store.ts @@ -1,10 +1,10 @@ import * as API from '@lib/request' import { useState, useMemo, useRef, useCallback } from 'react' -type Connections = API.Connections & { completed?: boolean, speed: { upload: number, download: number } } +export type Connection = API.Connections & { completed?: boolean, uploadSpeed: number, downloadSpeed: number } class Store { - protected connections = new Map() + protected connections = new Map() protected saveDisconnection = false appendToSet (connections: API.Connections[]) { @@ -20,7 +20,8 @@ class Store { const connection = this.connections.get(id) if (connection) { connection.completed = true - connection.speed = { upload: 0, download: 0 } + connection.uploadSpeed = 0 + connection.downloadSpeed = 0 } } } @@ -28,13 +29,13 @@ class Store { for (const id of mapping.keys()) { if (!this.connections.has(id)) { - this.connections.set(id, { ...mapping.get(id)!, speed: { upload: 0, download: 0 } }) + this.connections.set(id, { ...mapping.get(id)!, uploadSpeed: 0, downloadSpeed: 0 }) continue } const c = this.connections.get(id)! const n = mapping.get(id)! - this.connections?.set(id, { ...n, speed: { upload: n.upload - c.upload, download: n.download - c.download } }) + this.connections?.set(id, { ...n, uploadSpeed: n.upload - c.upload, downloadSpeed: n.download - c.download }) } } @@ -61,7 +62,7 @@ class Store { export function useConnections () { const store = useMemo(() => new Store(), []) const shouldFlush = useRef(true) - const [connections, setConnections] = useState([]) + const [connections, setConnections] = useState([]) const [save, setSave] = useState(false) const feed = useCallback(function (connections: API.Connections[]) { diff --git a/src/containers/Connections/style.scss b/src/containers/Connections/style.scss index 4616a6c..030c03e 100644 --- a/src/containers/Connections/style.scss +++ b/src/containers/Connections/style.scss @@ -34,6 +34,10 @@ font-size: 14px; cursor: pointer; user-select: none; + + &.resizing .connections-resizer { + opacity: 1; + } } .connections-resizer { @@ -50,6 +54,7 @@ z-index: 10; font-size: 14px; font-weight: 300; + touch-action: none; &::before { content: '';