mirror of
https://github.com/woodchen-ink/clash-and-dashboard.git
synced 2025-07-18 14:01:56 +08:00
Chore: refactor connections table
This commit is contained in:
parent
68881b8840
commit
31b2439f21
@ -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,21 +23,23 @@ 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])
|
|
||||||
|
|
||||||
function formatTraffic (num: number) {
|
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) {
|
||||||
const s = ['B', 'KB', 'MB', 'GB', 'TB']
|
const s = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
let idx = 0
|
let idx = 0
|
||||||
while (~~(num / 1024) && idx < s.length) {
|
while (~~(num / 1024) && idx < s.length) {
|
||||||
@ -49,7 +50,7 @@ function formatTraffic (num: number) {
|
|||||||
return `${idx === 0 ? num : num.toFixed(2)} ${s[idx]}`
|
return `${idx === 0 ? num : num.toFixed(2)} ${s[idx]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSpeed (upload: number, download: number) {
|
function formatSpeed(upload: number, download: number) {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case upload === 0 && download === 0:
|
case upload === 0 && download === 0:
|
||||||
return '-'
|
return '-'
|
||||||
@ -62,7 +63,24 @@ function formatSpeed (upload: number, download: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 { translation, lang } = useI18n()
|
||||||
const t = useMemo(() => translation('Connections').t, [translation])
|
const t = useMemo(() => translation('Connections').t, [translation])
|
||||||
|
|
||||||
@ -72,81 +90,62 @@ 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() {
|
||||||
API.closeAllConnections().finally(() => hide())
|
API.closeAllConnections().finally(() => hide())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
function handleConnection (snapshots: API.Snapshot[]) {
|
function handleConnection(snapshots: API.Snapshot[]) {
|
||||||
for (const snapshot of snapshots) {
|
for (const snapshot of snapshots) {
|
||||||
setTraffic({
|
setTraffic({
|
||||||
uploadTotal: snapshot.uploadTotal,
|
uploadTotal: snapshot.uploadTotal,
|
||||||
@ -177,39 +176,38 @@ 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">
|
||||||
<Header title={t('title')}>
|
<Header title={t('title')}>
|
||||||
<span className="connections-filter total">
|
<span className="connections-filter total">
|
||||||
{ `(${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)})`}
|
||||||
</span>
|
</span>
|
||||||
<Checkbox className="connections-filter" checked={save} onChange={toggleSave}>{ t('keepClosed') }</Checkbox>
|
<Checkbox className="connections-filter" checked={save} onChange={toggleSave}>{t('keepClosed')}</Checkbox>
|
||||||
<Icon className="connections-filter dangerous" onClick={show} type="close-all" size={20} />
|
<Icon className="connections-filter dangerous" onClick={show} type="close-all" size={20} />
|
||||||
</Header>
|
</Header>
|
||||||
<Card className="connections-card">
|
<Card className="connections-card">
|
||||||
@ -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
|
||||||
{ column.render('Header') }
|
{...realColumn.getHeaderProps()}
|
||||||
|
className={classnames('connections-th', { resizing: realColumn.isResizing })}
|
||||||
|
key={id}>
|
||||||
|
<div {...realColumn.getSortByToggleProps()}>
|
||||||
|
{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,11 +240,32 @@ 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>
|
||||||
<Modal title={ t('closeAll.title') } show={visible} onClose={hide} onOk={handleCloseConnections}>{ t('closeAll.content') }</Modal>
|
<Modal title={t('closeAll.title')} show={visible} onClose={hide} onOk={handleCloseConnections}>{t('closeAll.content')}</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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[]) {
|
||||||
|
@ -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: '';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user