From 8a34eeedc94915c20141f8edc8193aa0ae1ce054 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sat, 30 May 2026 20:56:31 +0200 Subject: [PATCH] refactor(frontend): break down OutboundsTab into sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract OutboundsTab's pieces: outbounds-tab-types.ts (OutboundRow), outbounds-tab-helpers.ts (address/untestable/security/breakdown + traffic/testing/result accessors), useOutboundColumns.tsx (desktop table columns hook) and OutboundCardList.tsx (mobile card view). OutboundsTab stays the orchestrator for outbound state, mutate, reorder and the toolbar, and drops from 516 to 238 lines. No behavior change. This completes plan section 2.4.5 — all four oversized Xray tabs (Basics/Routing/Dns/Outbounds) are now broken into sections + hooks. --- .../pages/xray/outbounds/OutboundCardList.tsx | 127 +++++++ .../src/pages/xray/outbounds/OutboundsTab.tsx | 334 ++---------------- .../xray/outbounds/outbounds-tab-helpers.ts | 62 ++++ .../xray/outbounds/outbounds-tab-types.ts | 7 + .../xray/outbounds/useOutboundColumns.tsx | 217 ++++++++++++ 5 files changed, 441 insertions(+), 306 deletions(-) create mode 100644 frontend/src/pages/xray/outbounds/OutboundCardList.tsx create mode 100644 frontend/src/pages/xray/outbounds/outbounds-tab-helpers.ts create mode 100644 frontend/src/pages/xray/outbounds/outbounds-tab-types.ts create mode 100644 frontend/src/pages/xray/outbounds/useOutboundColumns.tsx diff --git a/frontend/src/pages/xray/outbounds/OutboundCardList.tsx b/frontend/src/pages/xray/outbounds/OutboundCardList.tsx new file mode 100644 index 00000000..e438dbbb --- /dev/null +++ b/frontend/src/pages/xray/outbounds/OutboundCardList.tsx @@ -0,0 +1,127 @@ +import { useTranslation } from 'react-i18next'; +import { Button, Dropdown, Tag, Tooltip } from 'antd'; +import { + RetweetOutlined, + MoreOutlined, + EditOutlined, + DeleteOutlined, + VerticalAlignTopOutlined, + ThunderboltOutlined, + CheckCircleFilled, + CloseCircleFilled, + LoadingOutlined, +} from '@ant-design/icons'; + +import { SizeFormatter } from '@/utils'; +import { OutboundProtocols as Protocols } from '@/schemas/primitives'; +import type { OutboundTestState, OutboundTrafficRow } from '@/hooks/useXraySetting'; + +import type { OutboundRow } from './outbounds-tab-types'; +import { + isTesting, + isUntestable, + outboundAddresses, + showSecurity, + testResult, + trafficFor, +} from './outbounds-tab-helpers'; + +interface OutboundCardListProps { + rows: OutboundRow[]; + testMode: 'tcp' | 'http'; + outboundsTraffic: OutboundTrafficRow[]; + outboundTestStates: Record; + setFirst: (idx: number) => void; + openEdit: (idx: number) => void; + onResetTraffic: (tag: string) => void; + confirmDelete: (idx: number) => void; + onTest: (index: number, mode: string) => void; +} + +export default function OutboundCardList({ + rows, + testMode, + outboundsTraffic, + outboundTestStates, + setFirst, + openEdit, + onResetTraffic, + confirmDelete, + onTest, +}: OutboundCardListProps) { + const { t } = useTranslation(); + if (rows.length === 0) { + return
; + } + return ( + <> + {rows.map((record, index) => ( +
+
+
+ {index + 1} + + {record.tag} + + {record.protocol} + {[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(record.protocol as never) && ( + <> + {record.streamSettings?.network} + {showSecurity(record.streamSettings?.security) && {record.streamSettings?.security}} + + )} +
+ 0 + ? [{ key: 'top', label: , onClick: () => setFirst(index) }] + : []), + { key: 'edit', label: <> {t('edit')}, onClick: () => openEdit(index) }, + { key: 'reset', label: <> {t('pages.inbounds.resetTraffic')}, onClick: () => onResetTraffic(record.tag || '') }, + { key: 'del', danger: true, label: <> {t('delete')}, onClick: () => confirmDelete(index) }, + ], + }} + > +
+ {outboundAddresses(record).length > 0 && ( +
+ {outboundAddresses(record).map((addr) => ( + + {addr} + + ))} +
+ )} +
+ ↑ {SizeFormatter.sizeFormat(trafficFor(outboundsTraffic, record).up)} + + ↓ {SizeFormatter.sizeFormat(trafficFor(outboundsTraffic, record).down)} + + {testResult(outboundTestStates, index) ? ( + + {testResult(outboundTestStates, index)!.success ? : } + {testResult(outboundTestStates, index)!.success ? {testResult(outboundTestStates, index)!.delay} ms : failed} + + ) : isTesting(outboundTestStates, index) ? ( + + ) : null} +
+
+ ))} + + ); +} diff --git a/frontend/src/pages/xray/outbounds/OutboundsTab.tsx b/frontend/src/pages/xray/outbounds/OutboundsTab.tsx index bfa77601..a68a65e9 100644 --- a/frontend/src/pages/xray/outbounds/OutboundsTab.tsx +++ b/frontend/src/pages/xray/outbounds/OutboundsTab.tsx @@ -3,15 +3,12 @@ import { useTranslation } from 'react-i18next'; import { Button, Col, - Dropdown, Modal, Popconfirm, - Popover, Radio, Row, Space, Table, - Tag, Tooltip, } from 'antd'; import { @@ -19,27 +16,17 @@ import { CloudOutlined, ApiOutlined, RetweetOutlined, - MoreOutlined, - EditOutlined, - DeleteOutlined, - VerticalAlignTopOutlined, - ThunderboltOutlined, - CheckCircleFilled, - CloseCircleFilled, - LoadingOutlined, - ArrowUpOutlined, - ArrowDownOutlined, PlayCircleOutlined, } from '@ant-design/icons'; -import type { ColumnsType } from 'antd/es/table'; -import { SizeFormatter } from '@/utils'; -import { OutboundProtocols as Protocols } from '@/schemas/primitives'; import OutboundFormModal from './OutboundFormModal'; -import { isUdpOutbound } from '@/hooks/useXraySetting'; import type { XraySettingsValue, SetTemplate, OutboundTestState, OutboundTrafficRow } from '@/hooks/useXraySetting'; import './OutboundsTab.css'; +import type { OutboundRow } from './outbounds-tab-types'; +import { useOutboundColumns } from './useOutboundColumns'; +import OutboundCardList from './OutboundCardList'; + interface OutboundsTabProps { templateSettings: XraySettingsValue | null; setTemplateSettings: SetTemplate; @@ -55,59 +42,6 @@ interface OutboundsTabProps { onShowNord: () => void; } -interface OutboundRow { - key: number; - tag?: string; - protocol?: string; - streamSettings?: { network?: string; security?: string }; - settings?: Record; -} - -function outboundAddresses(o: OutboundRow): string[] { - const settings = o.settings as Record | undefined; - switch (o.protocol) { - case Protocols.VMess: { - const serverObj = settings?.vnext as Array<{ address: string; port: number }> | undefined; - return serverObj ? serverObj.map((s) => `${s.address}:${s.port}`) : []; - } - case Protocols.VLESS: - return [`${settings?.address || ''}:${settings?.port || ''}`]; - case Protocols.HTTP: - case Protocols.Socks: - case Protocols.Shadowsocks: - case Protocols.Trojan: { - const serverObj = settings?.servers as Array<{ address: string; port: number }> | undefined; - return serverObj ? serverObj.map((s) => `${s.address}:${s.port}`) : []; - } - case Protocols.DNS: { - const addr = (settings?.rewriteAddress as string) || (settings?.address as string) || ''; - const port = (settings?.rewritePort as string | number) || (settings?.port as string | number) || ''; - return addr || port ? [`${addr}:${port}`] : []; - } - case Protocols.Wireguard: - return (((settings?.peers as Array<{ endpoint?: string }>) || []).map((p) => p.endpoint || '').filter(Boolean)); - default: - return []; - } -} - -function isUntestable(o: OutboundRow, mode: string): boolean { - if (!o) return true; - if (o.protocol === Protocols.Blackhole || o.protocol === Protocols.Loopback || o.tag === 'blocked') return true; - if (mode === 'tcp' && (o.protocol === Protocols.Freedom || o.protocol === Protocols.DNS)) return true; - return false; -} - -function showSecurity(security?: string): boolean { - return security === 'tls' || security === 'reality'; -} - -function hasBreakdown(r: { endpoints?: unknown[]; error?: string } | null | undefined): boolean { - if (!r) return false; - if (r.endpoints?.length) return true; - return !!r.error; -} - export default function OutboundsTab({ templateSettings, setTemplateSettings, @@ -213,171 +147,19 @@ export default function OutboundsTab({ }); } - function trafficFor(o: OutboundRow): { up: number; down: number } { - const tr = outboundsTraffic.find((x) => x.tag === o.tag); - return { up: tr?.up || 0, down: tr?.down || 0 }; - } - function isTesting(idx: number): boolean { - return !!outboundTestStates?.[idx]?.testing; - } - function testResult(idx: number) { - return outboundTestStates?.[idx]?.result || null; - } - - const columns: ColumnsType = useMemo( - () => [ - { - title: '#', - key: 'action', - align: 'center', - width: 100, - render: (_v, _record, index) => ( -
- {index + 1} -
-
-
- ), - }, - { - title: t('pages.xray.outbound.tag'), - key: 'identity', - align: 'left', - render: (_v, record) => ( -
- - {record.tag} - -
- {record.protocol} - {[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(record.protocol as never) && ( - <> - {record.streamSettings?.network} - {showSecurity(record.streamSettings?.security) && {record.streamSettings?.security}} - - )} -
-
- ), - }, - { - title: t('pages.inbounds.address'), - key: 'address', - align: 'left', - render: (_v, record) => { - const addrs = outboundAddresses(record); - return ( -
- {addrs.length === 0 ? ( - - ) : ( - addrs.map((addr) => ( - - {addr} - - )) - )} -
- ); - }, - }, - { - title: t('pages.inbounds.traffic'), - key: 'traffic', - align: 'left', - width: 200, - render: (_v, record) => { - const tr = trafficFor(record); - return ( - <> - ↑ {SizeFormatter.sizeFormat(tr.up)} - - ↓ {SizeFormatter.sizeFormat(tr.down)} - - ); - }, - }, - { - title: t('pages.nodes.latency'), - key: 'testResult', - align: 'left', - width: 140, - render: (_v, _record, index) => { - const r = testResult(index); - if (!r) return isTesting(index) ? : ; - return ( - -
- {r.success ? {r.delay} ms : {r.error || 'failed'}} - {r.mode && {String(r.mode).toUpperCase()}} -
- {hasBreakdown(r) && ( - <> - {(r.endpoints || []).map((ep) => ( -
- - {ep.address} - {ep.success ? `${ep.delay} ms` : ep.error || 'failed'} -
- ))} - - )} - - } - > - - {r.success ? : } - {r.success ? {r.delay} ms : failed} - -
- ); - }, - }, - { - title: t('check'), - key: 'test', - align: 'center', - width: 80, - render: (_v, record, index) => ( - -