import './ConnectionTable.scss'; import cx from 'clsx'; import { formatDistance, Locale } from 'date-fns'; import { enUS, zhCN, zhTW } from 'date-fns/locale'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ArrowDown, ArrowUp, ChevronDown, Sliders, XCircle } from '~/components/shared/FeatherIcons'; import { useTranslation } from 'react-i18next'; import { useSortBy, useTable } from 'react-table'; import { List as VirtualList, RowComponentProps } from 'react-window'; import { FormattedConn } from '~/store/connections'; import * as connAPI from '../api/connections'; import prettyBytes from '../misc/pretty-bytes'; import ConnectionCard from './ConnectionCard'; import s from './ConnectionTable.module.scss'; import MOdalCloseConnection from './ModalCloseAllConnections'; import ModalConnectionDetails from './ModalConnectionDetails'; const sortById = { id: 'id', desc: true }; const COLUMN_WIDTHS = { ctrl: 50, start: 100, type: 120, host: 300, rule: 200, chains: 250, download: 100, upload: 100, downloadSpeedCurr: 100, uploadSpeedCurr: 100, source: 170, destinationIP: 170, process: 130, sniffHost: 150, }; const TOTAL_WIDTH = Object.values(COLUMN_WIDTHS).reduce((a, b) => a + b, 0); const getColumnStyle = (columnId: string) => { const width = COLUMN_WIDTHS[columnId] || 100; const style: React.CSSProperties = { width, minWidth: width, flex: `0 0 ${width}px`, flexShrink: 0, }; if (['download', 'upload', 'downloadSpeedCurr', 'uploadSpeedCurr', 'start'].includes(columnId)) { style.justifyContent = 'flex-end'; } if (columnId === 'ctrl') { style.justifyContent = 'center'; } return style; }; function Table({ data, columns, hiddenColumns, apiConfig, height }) { const { t, i18n } = useTranslation(); const [operationId, setOperationId] = useState(''); const [showModalDisconnect, setShowModalDisconnect] = useState(false); const [selectedConn, setSelectedConn] = useState(null); const [isMobile, setIsMobile] = useState(false); const headerRef = React.useRef(null); useEffect(() => { const mql = window.matchMedia('(max-width: 768px)'); setIsMobile(mql.matches); const listener = (e) => setIsMobile(e.matches); mql.addEventListener('change', listener); return () => mql.removeEventListener('change', listener); }, []); // 从本地存储加载排序状态 const tableState = useMemo(() => { const savedSortBy = JSON.parse(localStorage.getItem('tableSortBy')) || [sortById]; return { sortBy: savedSortBy, hiddenColumns, }; }, [hiddenColumns]); const table = useTable( { columns, data, initialState: tableState, autoResetSortBy: false, }, useSortBy ); const { setHiddenColumns, headerGroups, rows, prepareRow, toggleSortBy } = table; const state = table.state; const sortOptions = useMemo(() => { return columns .filter((c) => c.accessor !== 'id' && c.accessor !== 'ctrl') .map((c) => ({ label: t(c.Header), value: c.accessor, })); }, [columns, t]); const currentSort = state.sortBy[0] || sortById; useEffect(() => { setHiddenColumns(hiddenColumns); }, [setHiddenColumns, hiddenColumns]); let locale: Locale; if (i18n.language === 'zh-CN') { locale = zhCN; } else if (i18n.language === 'zh-TW') { locale = zhTW; } else { locale = enUS; } const disconnectOperation = useCallback(() => { connAPI.closeConnById(apiConfig, operationId); setShowModalDisconnect(false); }, [apiConfig, operationId]); const handlerDisconnect = useCallback((id, e) => { e.stopPropagation(); setOperationId(id); setShowModalDisconnect(true); }, []); const renderCell = useCallback( (cell, locale) => { switch (cell.column.id) { case 'ctrl': return ( handlerDisconnect(cell.row.original.id, e)} > ); case 'start': return formatDistance(cell.value, 0, { locale: locale }); case 'download': case 'upload': return prettyBytes(cell.value); case 'downloadSpeedCurr': case 'uploadSpeedCurr': return prettyBytes(cell.value) + '/s'; default: return cell.value; } }, [handlerDisconnect] ); // 当排序状态改变时,将新状态保存到本地存储 useEffect(() => { localStorage.setItem('tableSortBy', JSON.stringify(state.sortBy)); }, [state.sortBy]); const MobileRow = useCallback( ({ index, style }: RowComponentProps) => { const row = rows[index]; const conn = row.original as FormattedConn; return (
setSelectedConn(conn)} />
); }, [rows, handlerDisconnect] ); const DesktopRow = useCallback( ({ index, style }: RowComponentProps) => { const row = rows[index]; prepareRow(row); return (
setSelectedConn((row as any).original)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSelectedConn((row as any).original); } }} > {row.cells.map((cell) => { const columnStyle = getColumnStyle(cell.column.id); return (
{renderCell(cell, locale)}
); })}
); }, [prepareRow, rows, renderCell, locale] ); const handleDesktopListScroll = useCallback((e: React.UIEvent) => { if (headerRef.current) { headerRef.current.scrollLeft = e.currentTarget.scrollLeft; } }, []); return (
{isMobile ? (
{t('Sort')}: {sortOptions.find((opt) => opt.value === currentSort.id)?.label}
) : (
{headerGroups.map((headerGroup, trindex) => (
{headerGroup.headers.map((column) => { const columnStyle = getColumnStyle(column.id); return (
{t(column.render('Header'))} {column.id !== 'ctrl' ? ( {column.isSorted ? ( ) : null} ) : null}
); })}
))}
)} setShowModalDisconnect(false)} primaryButtonOnTap={disconnectOperation} > setSelectedConn(null)} connection={selectedConn} />
); } export default Table;