import { useMemo, type ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; import { Popover, Switch, Tag, Tooltip, type TableColumnType } from 'antd'; import { TeamOutlined } from '@ant-design/icons'; import { SizeFormatter, IntlUtil, ColorUtils } from '@/utils'; import { InfinityIcon } from '@/components/ui'; import { useDatepicker } from '@/hooks/useDatepicker'; import type { NodeRecord } from '@/api/queries/useNodesQuery'; import { coerceInboundJsonField } from '@/models/dbinbound'; import { RowActionsCell } from './RowActions'; import { SPEED_COLUMN_WIDTH, SPEED_TAG_CLASS_NAME, SPEED_TAG_STYLE } from '@/components/utility/speedTagStyle'; import { InboundSpeedTag, isActiveSpeed } from './InboundSpeedTag'; import { readStreamHints, networkLabel, networkL4, shadowsocksNetworkLabel, tunnelNetworkLabel, mixedNetworkLabel, } from './helpers'; import type { ClientCountEntry, DBInboundRecord, InboundSpeedEntry, RowAction } from './types'; interface UseInboundColumnsParams { hasAnyRemark: boolean; hasAnySubSortIndex: boolean; hasActiveNode: boolean; nodesById: Map; clientCount: Record; inboundSpeed: Record; subEnable: boolean; expireDiff: number; trafficDiff: number; onRowAction: (action: { key: RowAction; dbInbound: DBInboundRecord }) => void; onSwitchEnable: (dbInbound: DBInboundRecord, next: boolean) => void; } export function useInboundColumns({ hasAnyRemark, hasAnySubSortIndex, hasActiveNode, nodesById, clientCount, inboundSpeed, subEnable, expireDiff, trafficDiff, onRowAction, onSwitchEnable, }: UseInboundColumnsParams): TableColumnType[] { const { t } = useTranslation(); const { datepicker } = useDatepicker(); return useMemo(() => { const compareText = (a: string | undefined | null, b: string | undefined | null) => ( (a || '').localeCompare(b || '', undefined, { numeric: true, sensitivity: 'base' }) ); const nodeName = (record: DBInboundRecord) => { if (record.nodeId == null) return t('pages.inbounds.localPanel'); return nodesById.get(record.nodeId)?.name || `node #${record.nodeId}`; }; const clientTotal = (record: DBInboundRecord) => ( (clientCount[record.id] || fallbackClientCount(record))?.clients ?? 0 ); const speedTotal = (record: DBInboundRecord) => { const speed = inboundSpeed[record.id]; return speed ? speed.up + speed.down : 0; }; const expirySortValue = (record: DBInboundRecord) => ( record.expiryTime > 0 ? record.expiryTime : Number.MAX_SAFE_INTEGER ); const fallbackClientCount = (record: DBInboundRecord): ClientCountEntry | null => { const settings = coerceInboundJsonField(record.settings) as { clients?: { email?: string; enable?: boolean }[]; }; const clients = Array.isArray(settings.clients) ? settings.clients : []; if (clients.length === 0) return null; const active = clients .filter((client) => client.email && client.enable !== false) .map((client) => client.email!); const deactive = clients .filter((client) => client.email && client.enable === false) .map((client) => client.email!); return { clients: clients.length, active, deactive, depleted: [], expiring: [], online: [], }; }; const cols: TableColumnType[] = [ { title: 'ID', dataIndex: 'id', key: 'id', align: 'right', width: 60, sorter: (a, b) => a.id - b.id, }, { title: t('pages.inbounds.operate'), key: 'action', align: 'center', width: 70, render: (_, record) => ( 0} onClick={(key) => onRowAction({ key, dbInbound: record })} /> ), }, { title: t('pages.inbounds.enable'), key: 'enable', align: 'center', width: 80, render: (_, record) => ( onSwitchEnable(record, next)} /> ), }, ]; if (hasAnyRemark) { cols.push({ title: t('pages.inbounds.remark'), dataIndex: 'remark', key: 'remark', align: 'center', width: 90, sorter: (a, b) => compareText(a.remark, b.remark), }); } if (hasActiveNode) { cols.push({ title: t('pages.inbounds.node'), key: 'node', align: 'center', width: 130, sorter: (a, b) => compareText(nodeName(a), nodeName(b)), render: (_, record) => { if (record.nodeId == null) { return {t('pages.inbounds.localPanel')}; } const node = nodesById.get(record.nodeId); if (!node) { return node #{record.nodeId}; } return ( {node.name} ); }, }); } if (hasAnySubSortIndex) { cols.push({ title: ( {t('pages.inbounds.subSortIndex')} ), dataIndex: 'subSortIndex', key: 'subSortIndex', align: 'right', width: 90, sorter: (a, b) => (a.subSortIndex ?? 1) - (b.subSortIndex ?? 1), }); } cols.push( { title: t('pages.inbounds.port'), dataIndex: 'port', key: 'port', align: 'center', width: 80, sorter: (a, b) => a.port - b.port, }, { title: t('pages.inbounds.protocol'), key: 'protocol', align: 'left', width: 190, sorter: (a, b) => compareText(a.protocol, b.protocol), render: (_, record) => { const tags: ReactElement[] = [{record.protocol}]; if (record.isWireguard || record.isHysteria) { tags.push(UDP); } else if (record.isSS) { const stream = readStreamHints(record.streamSettings); tags.push({shadowsocksNetworkLabel(record.settings)}); if (stream.isTls) tags.push(TLS); } else if (record.isTunnel) { tags.push({tunnelNetworkLabel(record.settings)}); } else if (record.isMixed) { tags.push({mixedNetworkLabel(record.settings)}); } else if (record.isVMess || record.isVLess || record.isTrojan) { const stream = readStreamHints(record.streamSettings); tags.push({networkLabel(stream.network)}); const l4 = networkL4(stream.network); if (l4) tags.push({l4}); if (stream.isTls) tags.push(TLS); if (stream.isReality) tags.push(Reality); } return
{tags}
; }, }, { title: t('clients'), key: 'clients', align: 'left', width: 200, sorter: (a, b) => clientTotal(a) - clientTotal(b), render: (_, record) => { const cc = clientCount[record.id] || fallbackClientCount(record); if (!cc) return null; return ( <> {cc.clients} {cc.active.length > 0 ? ( {cc.active.map((e) =>
{e}
)} )} > {cc.active.length}
) : ( 0 )} {cc.deactive.length > 0 && ( {cc.deactive.map((e) =>
{e}
)} )} > {cc.deactive.length}
)} {cc.depleted.length > 0 && ( {cc.depleted.map((e) =>
{e}
)} )} > {cc.depleted.length}
)} {cc.online.length > 0 && ( {cc.online.map((e) =>
{e}
)} )} > {cc.online.length}
)} ); }, }, { title: t('pages.inbounds.traffic'), key: 'traffic', align: 'center', width: 140, sorter: (a, b) => (a.up + a.down) - (b.up + b.down), render: (_, record) => ( ↑ {SizeFormatter.sizeFormat(record.up)} ↓ {SizeFormatter.sizeFormat(record.down)} {record.total > 0 && record.up + record.down < record.total && ( {t('remained')} {SizeFormatter.sizeFormat(record.total - record.up - record.down)} )} )} > {SizeFormatter.sizeFormat(record.up + record.down)} / {' '} {record.total > 0 ? SizeFormatter.sizeFormat(record.total) : } ), }, { title: t('pages.inbounds.speed'), key: 'speed', align: 'center', width: SPEED_COLUMN_WIDTH, sorter: (a, b) => speedTotal(a) - speedTotal(b), render: (_, record) => { const speed = inboundSpeed[record.id]; if (!isActiveSpeed(speed)) { return ; } return ; }, }, { title: t('pages.inbounds.expireDate'), key: 'expiryTime', align: 'center', width: 100, sorter: (a, b) => expirySortValue(a) - expirySortValue(b), render: (_, record) => { if (record.expiryTime > 0) { return ( {IntlUtil.formatRelativeTime(record.expiryTime)} ); } return ; }, }, ); return cols; }, [t, hasAnyRemark, hasAnySubSortIndex, hasActiveNode, nodesById, clientCount, inboundSpeed, subEnable, expireDiff, trafficDiff, datepicker, onRowAction, onSwitchEnable]); }