import React, { useMemo, useLayoutEffect, useCallback, useRef, useState } from 'react' import { Cell, Column, ColumnInstance, TableInstance, TableOptions, useBlockLayout, useFilters, UseFiltersInstanceProps, UseFiltersOptions, useResizeColumns, UseResizeColumnsColumnProps, UseResizeColumnsOptions, useSortBy, UseSortByColumnOptions, UseSortByColumnProps, UseSortByOptions, useTable } from 'react-table' import classnames from 'classnames' import { useScroll } from 'react-use' import { groupBy } from 'lodash' 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 { fromNow } from '@lib/date' import { RuleType } from '@models' import { Devices } from './Devices' import { useConnections } from './store' import './style.scss' enum Columns { Host = 'host', Network = 'network', Type = 'type', Chains = 'chains', Rule = 'rule', Speed = 'speed', Upload = 'upload', Download = 'download', SourceIP = 'sourceIP', Time = 'time' } const shouldCenter = new Set([Columns.Network, Columns.Type, Columns.Rule, Columns.Speed, Columns.Upload, Columns.Download, Columns.SourceIP, Columns.Time]) interface TableColumn extends ColumnInstance, UseSortByColumnProps, UseResizeColumnsColumnProps {} type TableColumnOption = Column & UseResizeColumnsOptions & UseSortByColumnOptions interface ITableOptions extends TableOptions, UseSortByOptions, UseFiltersOptions {} interface ITableInstance extends TableInstance, UseFiltersInstanceProps {} function formatTraffic(num: number) { const s = ['B', 'KB', 'MB', 'GB', 'TB'] let idx = 0 while (~~(num / 1024) && idx < s.length) { num /= 1024 idx++ } return `${idx === 0 ? num : num.toFixed(2)} ${s[idx]}` } 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` } } interface formatConnection { id: string host: string chains: string rule: string time: number upload: number download: number type: string network: string sourceIP: string speed: { upload: number download: number } completed: boolean } export default function Connections() { const { translation, lang } = useI18n() const t = useMemo(() => translation('Connections').t, [translation]) // total const [traffic, setTraffic] = useObject({ uploadTotal: 0, downloadTotal: 0 }) // close all connections const { visible, show, hide } = useVisible() function handleCloseConnections() { API.closeAllConnections().finally(() => hide()) } // connections const { connections, feed, save, toggleSave } = useConnections() 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, sourceIP: c.metadata.sourceIP, type: c.metadata.type, network: c.metadata.network.toUpperCase(), speed: { upload: c.uploadSpeed, download: c.downloadSpeed }, completed: !!c.completed }) ), [connections]) const devices = useMemo(() => { const gb = groupBy(connections, 'metadata.sourceIP') return Object.keys(gb).map(key => ({ label: key, number: gb[key].length })).sort((a, b) => a.label.localeCompare(b.label)) }, [connections]) // table const tableRef = useRef(null) const { x: scrollX } = useScroll(tableRef) const columns: TableColumnOption[] = useMemo(() => [ { Header: t(`columns.${Columns.Host}`), accessor: Columns.Host, minWidth: 260, width: 260 }, { Header: t(`columns.${Columns.Network}`), accessor: Columns.Network, minWidth: 80, width: 80 }, { Header: t(`columns.${Columns.Type}`), accessor: Columns.Type, minWidth: 120, width: 120 }, { Header: t(`columns.${Columns.Chains}`), accessor: Columns.Chains, minWidth: 200, width: 200 }, { Header: t(`columns.${Columns.Rule}`), accessor: Columns.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: Columns.Upload, minWidth: 100, width: 100, sortDescFirst: true }, { Header: t(`columns.${Columns.Download}`), accessor: Columns.Download, minWidth: 100, width: 100, sortDescFirst: true }, { Header: t(`columns.${Columns.SourceIP}`), accessor: Columns.SourceIP, minWidth: 140, width: 140 }, { Header: t(`columns.${Columns.Time}`), accessor: Columns.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[]) { for (const snapshot of snapshots) { setTraffic({ uploadTotal: snapshot.uploadTotal, downloadTotal: snapshot.downloadTotal }) feed(snapshot.connections) } } (async function () { streamReader = await API.getConnectionStreamReader() streamReader.subscribe('data', handleConnection) }()) return () => { if (streamReader) { streamReader.unsubscribe('data', handleConnection) streamReader.destory() } } }, [feed, setTraffic]) const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow, setFilter } = useTable( { columns, data, autoResetSortBy: false, autoResetFilters: false, initialState: { sortBy: [{ id: Columns.Time, desc: false }] } } as ITableOptions, useResizeColumns, useBlockLayout, useFilters, useSortBy ) as ITableInstance const headerGroup = useMemo(() => headerGroups[0], [headerGroups]) 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]) // filter const [device, setDevice] = useState('') function handleDeviceSelected (label: string) { setDevice(label) setFilter?.(Columns.SourceIP, label) } return (
{`(${t('total.text')}: ${t('total.upload')} ${formatTraffic(traffic.uploadTotal)} ${t('total.download')} ${formatTraffic(traffic.downloadTotal)})`} {t('keepClosed')}
{ devices.length > 1 && }
{ headerGroup.headers.map((column, idx) => { const realColumn = column as unknown as TableColumn const id = realColumn.id return (
0 && realColumn.id === Columns.Host })} key={id}>
{column.render('Header')} { realColumn.isSorted ? realColumn.isSortedDesc ? ' ↓' : ' ↑' : null }
{ idx !== headerGroup.headers.length - 1 &&
}
) }) }
{ rows.map(row => { prepareRow(row) return (
{ row.cells.map(cell => { const classname = classnames( 'connections-block', { center: shouldCenter.has(cell.column.id), completed: row.original.completed }, { fixed: scrollX > 0 && cell.column.id === Columns.Host } ) return (
{ renderCell(cell)}
) }) }
) }) }
{t('closeAll.content')}
) }