import React, { useMemo, useLayoutEffect } from 'react' import { useBlockLayout, useResizeColumns, 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 { useConnections } from './store' import './style.scss' enum Columns { Host = 'host', Network = 'network', Type = 'type', Chains = 'chains', Rule = 'rule', Speed = 'speed', Upload = 'upload', Download = 'download', 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) { 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` } } export default function Connections () { const { useTranslation, lang } = useI18n() const t = useMemo(() => useTranslation('Connections').t, [useTranslation]) // total const [traffic, setTraffic] = useObject({ uploadTotal: 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 const { visible, show, hide } = useVisible() 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, 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 !== '') { return sort.asc ? a[sort.column].localeCompare(b[sort.column]) : b[sort.column].localeCompare(a[sort.column]) } return 0 }) }, [connections, sort]) // table const columns = useMemo(() => columnsPair.map( c => ({ Header: t(`columns.${c[0]}`), accessor: c[0], minWidth: c[1], width: c[1] }) ), [t]) useLayoutEffect(() => { const streamReader: StreamReader = null function handleConnection (snapshots: API.Snapshot[]) { for (const snapshot of snapshots) { setTraffic({ uploadTotal: snapshot.uploadTotal, downloadTotal: snapshot.downloadTotal }) feed(snapshot.connections) } } (async function () { const streamReader = await API.getConnectionStreamReader() streamReader.subscribe('data', handleConnection) }()) return () => { if (streamReader) { streamReader.unsubscribe('data', handleConnection) streamReader.destory() } } }, []) const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable( { columns, data }, useBlockLayout, useResizeColumns ) 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]) return (
{ `(${t('total.text')}: ${t('total.upload')} ${formatTraffic(traffic.uploadTotal)} ${t('total.download')} ${formatTraffic(traffic.downloadTotal)})` } { t('keepClosed') }
{ headerGroup.headers.map((column, idx) => { const id = column.id const handleClick = couldSort.has(id) ? () => handleSort(id) : noop return (
{ column.render('Header') } { sort.column === id && (sort.asc ? ' ↑' : ' ↓') } { idx !== headerGroup.headers.length - 1 &&
}
) }) }
{ renderItem }
{ t('closeAll.content') }
) }