Chore: refactor connections table

This commit is contained in:
Dreamacro 2020-11-01 23:27:28 +08:00
parent 68881b8840
commit 31b2439f21
3 changed files with 160 additions and 128 deletions

View File

@ -1,12 +1,11 @@
import React, { useMemo, useLayoutEffect } from 'react' import React, { useMemo, useLayoutEffect, useCallback } from 'react'
import { useBlockLayout, useResizeColumns, useTable } from 'react-table' import { Cell, Column, ColumnInstance, TableOptions, useBlockLayout, useResizeColumns, UseResizeColumnsColumnProps, UseResizeColumnsOptions, useSortBy, UseSortByColumnOptions, UseSortByColumnProps, UseSortByOptions, useTable } from 'react-table'
import classnames from 'classnames' import classnames from 'classnames'
import { Header, Card, Checkbox, Modal, Icon } from '@components' import { Header, Card, Checkbox, Modal, Icon } from '@components'
import { useI18n } from '@stores' import { useI18n } from '@stores'
import * as API from '@lib/request' import * as API from '@lib/request'
import { StreamReader } from '@lib/streamer' import { StreamReader } from '@lib/streamer'
import { useObject, useVisible } from '@lib/hook' import { useObject, useVisible } from '@lib/hook'
import { noop } from '@lib/helper'
import { fromNow } from '@lib/date' import { fromNow } from '@lib/date'
import { RuleType } from '@models' import { RuleType } from '@models'
import { useConnections } from './store' import { useConnections } from './store'
@ -24,19 +23,21 @@ enum Columns {
Time = 'time' 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<string>([Columns.Network, Columns.Type, Columns.Rule, Columns.Speed, Columns.Upload, Columns.Download, Columns.Time]) const shouldCenter = new Set<string>([Columns.Network, Columns.Type, Columns.Rule, Columns.Speed, Columns.Upload, Columns.Download, Columns.Time])
const couldSort = new Set<string>([Columns.Host, Columns.Network, Columns.Type, Columns.Rule, Columns.Upload, Columns.Download])
interface TableColumn<D extends object = {}> extends
ColumnInstance<D>,
UseSortByColumnProps<D>,
UseResizeColumnsColumnProps<D> {}
type TableColumnOption<D extends object = {}> =
Column<D> &
UseResizeColumnsOptions<D> &
UseSortByColumnOptions<D>
interface ITableOptions<D extends object = {}> extends
TableOptions<D>,
UseSortByOptions<D> {}
function formatTraffic(num: number) { function formatTraffic(num: number) {
const s = ['B', 'KB', 'MB', 'GB', 'TB'] const s = ['B', 'KB', 'MB', 'GB', 'TB']
@ -62,6 +63,23 @@ function formatSpeed (upload: number, download: number) {
} }
} }
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() { export default function Connections() {
const { translation, lang } = useI18n() const { translation, lang } = useI18n()
const t = useMemo(() => translation('Connections').t, [translation]) const t = useMemo(() => translation('Connections').t, [translation])
@ -72,21 +90,6 @@ export default function Connections () {
downloadTotal: 0 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 // close all connections
const { visible, show, hide } = useVisible() const { visible, show, hide } = useVisible()
function handleCloseConnections() { function handleCloseConnections() {
@ -95,53 +98,49 @@ export default function Connections () {
// connections // connections
const { connections, feed, save, toggleSave } = useConnections() const { connections, feed, save, toggleSave } = useConnections()
const data = useMemo(() => { const data: formatConnection[] = useMemo(() => connections.map(
return connections c => ({
.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, id: c.id,
host: `${c.metadata.host || c.metadata.destinationIP}:${c.metadata.destinationPort}`, host: `${c.metadata.host || c.metadata.destinationIP}:${c.metadata.destinationPort}`,
chains: c.chains.slice().reverse().join(' --> '), chains: c.chains.slice().reverse().join(' / '),
rule: c.rule === RuleType.RuleSet ? `${c.rule}(${c.rulePayload})` : c.rule, rule: c.rule === RuleType.RuleSet ? `${c.rule}(${c.rulePayload})` : c.rule,
time: fromNow(new Date(c.start), lang), time: new Date(c.start).getTime(),
upload: formatTraffic(c.upload), upload: c.upload,
download: formatTraffic(c.download), download: c.download,
type: c.metadata.type, type: c.metadata.type,
network: c.metadata.network.toUpperCase(), network: c.metadata.network.toUpperCase(),
speed: formatSpeed(c.speed.upload, c.speed.download), speed: { upload: c.uploadSpeed, download: c.downloadSpeed },
completed: !!c.completed 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]) ), [connections])
// table // table
const columns = useMemo(() => columnsPair.map( const columns: TableColumnOption<formatConnection>[] = useMemo(() => [
c => ({ { Header: t(`columns.${Columns.Host}`), accessor: 'host', minWidth: 260, width: 260 },
Header: t(`columns.${c[0]}`), { Header: t(`columns.${Columns.Network}`), accessor: 'network', minWidth: 80, width: 80 },
accessor: c[0], { Header: t(`columns.${Columns.Type}`), accessor: 'type', minWidth: 120, width: 120 },
minWidth: c[1], { Header: t(`columns.${Columns.Chains}`), accessor: 'chains', minWidth: 200, width: 200 },
width: c[1] { Header: t(`columns.${Columns.Rule}`), accessor: 'rule', minWidth: 140, width: 140 },
}) {
), [t]) 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<formatConnection>[], [t])
useLayoutEffect(() => { useLayoutEffect(() => {
let streamReader: StreamReader<API.Snapshot> | null = null let streamReader: StreamReader<API.Snapshot> | null = null
@ -177,31 +176,30 @@ export default function Connections () {
rows, rows,
prepareRow prepareRow
} = useTable( } = useTable(
{ columns: columns as any, data }, {
columns,
data,
autoResetSortBy: false,
initialState: { sortBy: [{ id: Columns.Time, desc: false }] }
} as ITableOptions<formatConnection>,
useResizeColumns,
useBlockLayout, useBlockLayout,
useResizeColumns useSortBy
) )
const headerGroup = useMemo(() => headerGroups[0], [headerGroups]) const headerGroup = useMemo(() => headerGroups[0], [headerGroups])
const renderItem = useMemo(() => rows.map((row, i) => { const renderCell = useCallback(function (cell: Cell<formatConnection>) {
prepareRow(row) switch (cell.column.id) {
return ( case Columns.Speed:
<div {...row.getRowProps()} className="connections-item" key={i}> return formatSpeed(cell.value[0], cell.value[1])
{ case Columns.Upload:
row.cells.map((cell, j) => { case Columns.Download:
const classname = classnames( return formatTraffic(cell.value)
'connections-block', case Columns.Time:
{ center: shouldCenter.has(cell.column.id), completed: !!(row.original as any).completed } return fromNow(new Date(cell.value), lang)
) default:
return ( return cell.value
<div {...cell.getCellProps()} className={classname} key={j}>
{ cell.render('Cell') }
</div>
)
})
} }
</div> }, [lang])
)
}), [prepareRow, rows])
return ( return (
<div className="page"> <div className="page">
@ -217,16 +215,23 @@ export default function Connections () {
<div {...headerGroup.getHeaderGroupProps()} className="connections-header"> <div {...headerGroup.getHeaderGroupProps()} className="connections-header">
{ {
headerGroup.headers.map((column, idx) => { headerGroup.headers.map((column, idx) => {
const id = column.id const realColumn = column as unknown as TableColumn<formatConnection>
const handleClick = couldSort.has(id) ? () => handleSort(id) : noop const id = realColumn.id
return ( return (
<div {...column.getHeaderProps()} className="connections-th" onClick={handleClick} key={id}> <div
{...realColumn.getHeaderProps()}
className={classnames('connections-th', { resizing: realColumn.isResizing })}
key={id}>
<div {...realColumn.getSortByToggleProps()}>
{column.render('Header')} {column.render('Header')}
{ {
sort.column === id && (sort.asc ? ' ↑' : ' ↓') realColumn.isSorted
? realColumn.isSortedDesc ? ' ↓' : ' ↑'
: null
} }
</div>
{ idx !== headerGroup.headers.length - 1 && { idx !== headerGroup.headers.length - 1 &&
<div {...(column as any).getResizerProps()} className="connections-resizer" /> <div {...realColumn.getResizerProps()} className="connections-resizer" />
} }
</div> </div>
) )
@ -235,7 +240,28 @@ export default function Connections () {
</div> </div>
<div {...getTableBodyProps()} className="connections-body"> <div {...getTableBodyProps()} className="connections-body">
{ renderItem } {
rows.map((row, i) => {
prepareRow(row)
return (
<div {...row.getRowProps()} className="connections-item" key={i}>
{
row.cells.map((cell, j) => {
const classname = classnames(
'connections-block',
{ center: shouldCenter.has(cell.column.id), completed: row.original.completed }
)
return (
<div {...cell.getCellProps()} className={classname} key={j}>
{ renderCell(cell)}
</div>
)
})
}
</div>
)
})
}
</div> </div>
</div> </div>
</Card> </Card>

View File

@ -1,10 +1,10 @@
import * as API from '@lib/request' import * as API from '@lib/request'
import { useState, useMemo, useRef, useCallback } from 'react' 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 { class Store {
protected connections = new Map<string, Connections>() protected connections = new Map<string, Connection>()
protected saveDisconnection = false protected saveDisconnection = false
appendToSet (connections: API.Connections[]) { appendToSet (connections: API.Connections[]) {
@ -20,7 +20,8 @@ class Store {
const connection = this.connections.get(id) const connection = this.connections.get(id)
if (connection) { if (connection) {
connection.completed = true 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()) { for (const id of mapping.keys()) {
if (!this.connections.has(id)) { 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 continue
} }
const c = this.connections.get(id)! const c = this.connections.get(id)!
const n = mapping.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 () { export function useConnections () {
const store = useMemo(() => new Store(), []) const store = useMemo(() => new Store(), [])
const shouldFlush = useRef(true) const shouldFlush = useRef(true)
const [connections, setConnections] = useState<Connections[]>([]) const [connections, setConnections] = useState<Connection[]>([])
const [save, setSave] = useState<boolean>(false) const [save, setSave] = useState<boolean>(false)
const feed = useCallback(function (connections: API.Connections[]) { const feed = useCallback(function (connections: API.Connections[]) {

View File

@ -34,6 +34,10 @@
font-size: 14px; font-size: 14px;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
&.resizing .connections-resizer {
opacity: 1;
}
} }
.connections-resizer { .connections-resizer {
@ -50,6 +54,7 @@
z-index: 10; z-index: 10;
font-size: 14px; font-size: 14px;
font-weight: 300; font-weight: 300;
touch-action: none;
&::before { &::before {
content: ''; content: '';