From a4dae566cedca14384ad2b2b5b4325d20b03a36c Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 3 Jun 2026 09:57:45 +0200 Subject: [PATCH] feat(xray): merge basic routing into the routing rules section Move the basic routing presets (block torrent/IPs/domains, direct IPs/domains, IPv4) out of the Basics page into a Basic tab in the Routing section, next to the advanced Rules table; both edit the same routing.rules so existing rules stay in sync. Drop the WARP and Nord routing preset rows - WARP/Nord outbounds are still added from the Outbounds page and any existing rules remain editable in the Rules tab. Hide the Source and Balancers columns in the rules table when no rule populates them. --- frontend/src/pages/xray/XrayPage.tsx | 7 - frontend/src/pages/xray/basics/BasicsTab.tsx | 191 ------------------ .../src/pages/xray/routing/RoutingBasic.tsx | 160 +++++++++++++++ .../src/pages/xray/routing/RoutingTab.css | 4 + .../src/pages/xray/routing/RoutingTab.tsx | 128 +++++++----- .../pages/xray/routing/useRoutingColumns.tsx | 9 +- 6 files changed, 252 insertions(+), 247 deletions(-) create mode 100644 frontend/src/pages/xray/routing/RoutingBasic.tsx diff --git a/frontend/src/pages/xray/XrayPage.tsx b/frontend/src/pages/xray/XrayPage.tsx index 08d4ed14..53b2909e 100644 --- a/frontend/src/pages/xray/XrayPage.tsx +++ b/frontend/src/pages/xray/XrayPage.tsx @@ -94,9 +94,6 @@ export default function XrayPage() { [setTemplateSettings], ); - const warpExist = !!templateSettings?.outbounds?.find((o) => o?.tag === 'warp'); - const nordExist = !!templateSettings?.outbounds?.find((o) => o?.tag?.startsWith?.('nord-')); - async function onTestOutbound(idx: number, mode: string) { const outbound = templateSettings?.outbounds?.[idx]; if (outbound) await testOutbound(idx, outbound, mode); @@ -287,10 +284,6 @@ export default function XrayPage() { setTemplateSettings={setTemplateSettings} outboundTestUrl={outboundTestUrl} onChangeOutboundTestUrl={setOutboundTestUrl} - warpExist={warpExist} - nordExist={nordExist} - onShowWarp={() => setWarpOpen(true)} - onShowNord={() => setNordOpen(true)} onResetDefault={resetToDefault} /> ); diff --git a/frontend/src/pages/xray/basics/BasicsTab.tsx b/frontend/src/pages/xray/basics/BasicsTab.tsx index 1e872d01..67fad5b5 100644 --- a/frontend/src/pages/xray/basics/BasicsTab.tsx +++ b/frontend/src/pages/xray/basics/BasicsTab.tsx @@ -2,13 +2,10 @@ import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, Button, Input, Modal, Select, Space, Switch, Tabs } from 'antd'; import { - ApiOutlined, BarChartOutlined, - CloudOutlined, FileTextOutlined, ReloadOutlined, SettingOutlined, - SwapOutlined, } from '@ant-design/icons'; import { OutboundDomainStrategies } from '@/schemas/primitives'; @@ -20,29 +17,17 @@ import './BasicsTab.css'; import { ACCESS_LOG, - BITTORRENT_PROTOCOLS, - BLOCK_DOMAINS_OPTIONS, - DOMAINS_OPTIONS, ERROR_LOG, - IPS_OPTIONS, LOG_LEVELS, MASK_ADDRESS, ROUTING_DOMAIN_STRATEGIES, - SERVICES_OPTIONS, - directSettings, - ipv4Settings, } from './constants'; -import { ruleGetter, ruleSetter, syncOutbound } from './helpers'; interface BasicsTabProps { templateSettings: XraySettingsValue | null; setTemplateSettings: SetTemplate; outboundTestUrl: string; onChangeOutboundTestUrl: (v: string) => void; - warpExist: boolean; - nordExist: boolean; - onShowWarp: () => void; - onShowNord: () => void; onResetDefault: () => void; } @@ -51,10 +36,6 @@ export default function BasicsTab({ setTemplateSettings, outboundTestUrl, onChangeOutboundTestUrl, - warpExist, - nordExist, - onShowWarp, - onShowNord, onResetDefault, }: BasicsTabProps) { const { t } = useTranslation(); @@ -92,19 +73,6 @@ export default function BasicsTab({ const log = (templateSettings?.log || {}) as Record; const policy = (templateSettings?.policy?.system || {}) as Record; - const blockedIPs = ruleGetter(templateSettings, 'blocked', 'ip'); - const blockedDomains = ruleGetter(templateSettings, 'blocked', 'domain'); - const blockedProtocols = ruleGetter(templateSettings, 'blocked', 'protocol'); - const directIPs = ruleGetter(templateSettings, 'direct', 'ip'); - const directDomains = ruleGetter(templateSettings, 'direct', 'domain'); - const ipv4Domains = ruleGetter(templateSettings, 'IPv4', 'domain'); - const warpDomains = ruleGetter(templateSettings, 'warp', 'domain'); - const nordTag = - templateSettings?.outbounds?.find((o) => o?.tag?.startsWith?.('nord-'))?.tag || 'nord'; - const nordDomains = ruleGetter(templateSettings, nordTag, 'domain'); - - const torrentActive = BITTORRENT_PROTOCOLS.every((p) => blockedProtocols.includes(p)); - const items = [ { key: '1', @@ -277,165 +245,6 @@ export default function BasicsTab({ ), }, - { - key: '4', - label: catTabLabel(, t('pages.xray.basicRouting'), isMobile), - children: ( - <> - - - mutate((tt) => { - const next = checked - ? [...blockedProtocols, ...BITTORRENT_PROTOCOLS] - : blockedProtocols.filter((d) => !BITTORRENT_PROTOCOLS.includes(d)); - ruleSetter(tt, 'blocked', 'protocol', next); - })} - /> - } - /> - - mutate((tt) => ruleSetter(tt, 'blocked', 'ip', v))} - /> - } - /> - - mutate((tt) => ruleSetter(tt, 'blocked', 'domain', v))} - /> - } - /> - - - - mutate((tt) => { - ruleSetter(tt, 'direct', 'ip', v); - syncOutbound(tt, 'direct', directSettings); - })} - /> - } - /> - - mutate((tt) => { - ruleSetter(tt, 'direct', 'domain', v); - syncOutbound(tt, 'direct', directSettings); - })} - /> - } - /> - - mutate((tt) => { - ruleSetter(tt, 'IPv4', 'domain', v); - syncOutbound(tt, 'IPv4', ipv4Settings); - })} - /> - } - /> - - mutate((tt) => ruleSetter(tt, 'warp', 'domain', v))} - /> - ) : ( - - ) - } - /> - - mutate((tt) => ruleSetter(tt, nordTag, 'domain', v))} - /> - ) : ( - - ) - } - /> - - ), - }, { key: 'reset', label: catTabLabel(, t('pages.settings.resetDefaultConfig'), isMobile), diff --git a/frontend/src/pages/xray/routing/RoutingBasic.tsx b/frontend/src/pages/xray/routing/RoutingBasic.tsx new file mode 100644 index 00000000..a6e60179 --- /dev/null +++ b/frontend/src/pages/xray/routing/RoutingBasic.tsx @@ -0,0 +1,160 @@ +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Alert, Select, Switch } from 'antd'; + +import { SettingListItem } from '@/components/ui'; +import type { XraySettingsValue, SetTemplate } from '@/hooks/useXraySetting'; +import { + BITTORRENT_PROTOCOLS, + BLOCK_DOMAINS_OPTIONS, + DOMAINS_OPTIONS, + IPS_OPTIONS, + SERVICES_OPTIONS, + directSettings, + ipv4Settings, +} from '../basics/constants'; +import { ruleGetter, ruleSetter, syncOutbound } from '../basics/helpers'; + +interface RoutingBasicProps { + templateSettings: XraySettingsValue | null; + setTemplateSettings: SetTemplate; +} + +export default function RoutingBasic({ templateSettings, setTemplateSettings }: RoutingBasicProps) { + const { t } = useTranslation(); + + const mutate = useCallback( + (mutator: (next: XraySettingsValue) => void) => { + setTemplateSettings((prev) => { + if (!prev) return prev; + const clone = JSON.parse(JSON.stringify(prev)) as XraySettingsValue; + mutator(clone); + return clone; + }); + }, + [setTemplateSettings], + ); + + const blockedIPs = ruleGetter(templateSettings, 'blocked', 'ip'); + const blockedDomains = ruleGetter(templateSettings, 'blocked', 'domain'); + const blockedProtocols = ruleGetter(templateSettings, 'blocked', 'protocol'); + const directIPs = ruleGetter(templateSettings, 'direct', 'ip'); + const directDomains = ruleGetter(templateSettings, 'direct', 'domain'); + const ipv4Domains = ruleGetter(templateSettings, 'IPv4', 'domain'); + + const torrentActive = BITTORRENT_PROTOCOLS.every((p) => blockedProtocols.includes(p)); + + return ( + <> + + + mutate((tt) => { + const next = checked + ? [...blockedProtocols, ...BITTORRENT_PROTOCOLS] + : blockedProtocols.filter((d) => !BITTORRENT_PROTOCOLS.includes(d)); + ruleSetter(tt, 'blocked', 'protocol', next); + })} + /> + } + /> + + mutate((tt) => ruleSetter(tt, 'blocked', 'ip', v))} + /> + } + /> + + mutate((tt) => ruleSetter(tt, 'blocked', 'domain', v))} + /> + } + /> + + + + mutate((tt) => { + ruleSetter(tt, 'direct', 'ip', v); + syncOutbound(tt, 'direct', directSettings); + })} + /> + } + /> + + mutate((tt) => { + ruleSetter(tt, 'direct', 'domain', v); + syncOutbound(tt, 'direct', directSettings); + })} + /> + } + /> + + mutate((tt) => { + ruleSetter(tt, 'IPv4', 'domain', v); + syncOutbound(tt, 'IPv4', ipv4Settings); + })} + /> + } + /> + + ); +} diff --git a/frontend/src/pages/xray/routing/RoutingTab.css b/frontend/src/pages/xray/routing/RoutingTab.css index d79b2f17..1d3590f4 100644 --- a/frontend/src/pages/xray/routing/RoutingTab.css +++ b/frontend/src/pages/xray/routing/RoutingTab.css @@ -231,3 +231,7 @@ opacity: 0.4; } +.hint-alert { + text-align: center; +} + diff --git a/frontend/src/pages/xray/routing/RoutingTab.tsx b/frontend/src/pages/xray/routing/RoutingTab.tsx index feeffece..15aad661 100644 --- a/frontend/src/pages/xray/routing/RoutingTab.tsx +++ b/frontend/src/pages/xray/routing/RoutingTab.tsx @@ -1,8 +1,10 @@ import { useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Modal, Space, Table } from 'antd'; -import { PlusOutlined } from '@ant-design/icons'; +import { Button, Modal, Space, Table, Tabs } from 'antd'; +import { ControlOutlined, PlusOutlined, UnorderedListOutlined } from '@ant-design/icons'; +import { catTabLabel } from '@/pages/settings/catTabLabel'; +import RoutingBasic from './RoutingBasic'; import RuleFormModal from './RuleFormModal'; import type { RoutingRule } from './RuleFormModal'; import RuleCardList from './RuleCardList'; @@ -226,9 +228,14 @@ export default function RoutingTab({ document.addEventListener('pointercancel', onUp); } + const hasSource = rows.some((r) => r.sourceIP || r.sourcePort || r.vlessRoute); + const hasBalancer = rows.some((r) => r.balancerTag); + const desktopColumns = useRoutingColumns({ isMobile, rowsLength: rows.length, + showSource: hasSource, + showBalancer: hasBalancer, onHandlePointerDown, openEdit, moveUp, @@ -236,56 +243,81 @@ export default function RoutingTab({ confirmDelete, }); + const tableScrollX = desktopColumns.reduce((sum, c) => { + const col = c as { width?: number; hidden?: boolean }; + return col.hidden ? sum : sum + (typeof col.width === 'number' ? col.width : 0); + }, 0); + return ( <> {modalContextHolder} - - + , t('pages.xray.basicRouting'), isMobile), + children: ( + + ), + }, + { + key: 'rules', + label: catTabLabel(, t('pages.xray.Routings'), isMobile), + children: ( + + - {isMobile ? ( - - ) : ( - r.key} - pagination={false} - scroll={{ x: 1150 }} - size="small" - className="routing-table" - onRow={(_record, index) => { - const classes: string[] = []; - const i = index ?? -1; - if (draggedIndex === i) classes.push('row-dragging'); - if (dropTargetIndex === i && draggedIndex !== i && draggedIndex != null) { - classes.push(i > draggedIndex ? 'drop-after' : 'drop-before'); - } - return { className: classes.join(' '), 'data-row-key': i } as React.HTMLAttributes; - }} - /> - )} - - setRuleModalOpen(false)} - onConfirm={onRuleConfirm} - /> - + {isMobile ? ( + + ) : ( +
r.key} + pagination={false} + scroll={{ x: tableScrollX }} + size="small" + className="routing-table" + onRow={(_record, index) => { + const classes: string[] = []; + const i = index ?? -1; + if (draggedIndex === i) classes.push('row-dragging'); + if (dropTargetIndex === i && draggedIndex !== i && draggedIndex != null) { + classes.push(i > draggedIndex ? 'drop-after' : 'drop-before'); + } + return { className: classes.join(' '), 'data-row-key': i } as React.HTMLAttributes; + }} + /> + )} + + ), + }, + ]} + /> + setRuleModalOpen(false)} + onConfirm={onRuleConfirm} + /> ); } diff --git a/frontend/src/pages/xray/routing/useRoutingColumns.tsx b/frontend/src/pages/xray/routing/useRoutingColumns.tsx index ec0b8e27..ea9ef984 100644 --- a/frontend/src/pages/xray/routing/useRoutingColumns.tsx +++ b/frontend/src/pages/xray/routing/useRoutingColumns.tsx @@ -19,6 +19,8 @@ import type { RuleRow } from './types'; interface RoutingColumnsParams { isMobile: boolean; rowsLength: number; + showSource: boolean; + showBalancer: boolean; onHandlePointerDown: (idx: number, ev: React.PointerEvent) => void; openEdit: (idx: number) => void; moveUp: (idx: number) => void; @@ -29,6 +31,8 @@ interface RoutingColumnsParams { export function useRoutingColumns({ isMobile, rowsLength, + showSource, + showBalancer, onHandlePointerDown, openEdit, moveUp, @@ -84,6 +88,7 @@ export function useRoutingColumns({ align: 'left', width: 180, key: 'source', + hidden: !showSource, render: (_v, record) => (
{record.sourceIP && } @@ -110,6 +115,7 @@ export function useRoutingColumns({ { title: t('pages.xray.rules.dest'), align: 'left', + width: 200, key: 'destination', render: (_v, record) => (
@@ -153,6 +159,7 @@ export function useRoutingColumns({ align: 'left', width: 150, key: 'balancer', + hidden: !showBalancer, render: (_v, record) => record.balancerTag ? (
@@ -165,6 +172,6 @@ export function useRoutingColumns({ }, ], // eslint-disable-next-line react-hooks/exhaustive-deps - [t, isMobile, rowsLength], + [t, isMobile, rowsLength, showSource, showBalancer], ); }