Sfoglia il codice sorgente

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.
MHSanaei 1 giorno fa
parent
commit
a4dae566ce

+ 0 - 7
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}
           />
         );

+ 0 - 191
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<string, unknown>;
   const policy = (templateSettings?.policy?.system || {}) as Record<string, boolean>;
 
-  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(<SwapOutlined />, t('pages.xray.basicRouting'), isMobile),
-      children: (
-        <>
-          <Alert
-            type="warning"
-            showIcon
-            className="mb-12 hint-alert"
-            title={t('pages.xray.blockConnectionsConfigsDesc')}
-          />
-
-          <SettingListItem
-            title={t('pages.xray.Torrent')}
-            paddings="small"
-            control={
-              <Switch
-                checked={torrentActive}
-                onChange={(checked) => mutate((tt) => {
-                  const next = checked
-                    ? [...blockedProtocols, ...BITTORRENT_PROTOCOLS]
-                    : blockedProtocols.filter((d) => !BITTORRENT_PROTOCOLS.includes(d));
-                  ruleSetter(tt, 'blocked', 'protocol', next);
-                })}
-              />
-            }
-          />
-
-          <SettingListItem
-            title={t('pages.xray.blockips')}
-            paddings="small"
-            control={
-              <Select
-                mode="tags"
-                value={blockedIPs}
-                style={{ width: '100%' }}
-                options={IPS_OPTIONS}
-                onChange={(v) => mutate((tt) => ruleSetter(tt, 'blocked', 'ip', v))}
-              />
-            }
-          />
-
-          <SettingListItem
-            title={t('pages.xray.blockdomains')}
-            paddings="small"
-            control={
-              <Select
-                mode="tags"
-                value={blockedDomains}
-                style={{ width: '100%' }}
-                options={BLOCK_DOMAINS_OPTIONS}
-                onChange={(v) => mutate((tt) => ruleSetter(tt, 'blocked', 'domain', v))}
-              />
-            }
-          />
-
-          <Alert
-            type="warning"
-            showIcon
-            className="mb-12 hint-alert"
-            title={t('pages.xray.directConnectionsConfigsDesc')}
-          />
-
-          <SettingListItem
-            title={t('pages.xray.directips')}
-            paddings="small"
-            control={
-              <Select
-                mode="tags"
-                value={directIPs}
-                style={{ width: '100%' }}
-                options={IPS_OPTIONS}
-                onChange={(v) => mutate((tt) => {
-                  ruleSetter(tt, 'direct', 'ip', v);
-                  syncOutbound(tt, 'direct', directSettings);
-                })}
-              />
-            }
-          />
-
-          <SettingListItem
-            title={t('pages.xray.directdomains')}
-            paddings="small"
-            control={
-              <Select
-                mode="tags"
-                value={directDomains}
-                style={{ width: '100%' }}
-                options={DOMAINS_OPTIONS}
-                onChange={(v) => mutate((tt) => {
-                  ruleSetter(tt, 'direct', 'domain', v);
-                  syncOutbound(tt, 'direct', directSettings);
-                })}
-              />
-            }
-          />
-
-          <SettingListItem
-            title={t('pages.xray.ipv4Routing')}
-            description={t('pages.xray.ipv4RoutingDesc')}
-            paddings="small"
-            control={
-              <Select
-                mode="tags"
-                value={ipv4Domains}
-                style={{ width: '100%' }}
-                options={SERVICES_OPTIONS}
-                onChange={(v) => mutate((tt) => {
-                  ruleSetter(tt, 'IPv4', 'domain', v);
-                  syncOutbound(tt, 'IPv4', ipv4Settings);
-                })}
-              />
-            }
-          />
-
-          <SettingListItem
-            title={t('pages.xray.warpRouting')}
-            description={t('pages.xray.warpRoutingDesc')}
-            paddings="small"
-            control={
-              warpExist ? (
-                <Select
-                  mode="tags"
-                  value={warpDomains}
-                  style={{ width: '100%' }}
-                  options={SERVICES_OPTIONS}
-                  onChange={(v) => mutate((tt) => ruleSetter(tt, 'warp', 'domain', v))}
-                />
-              ) : (
-                <Button type="primary" onClick={onShowWarp} icon={<CloudOutlined />}>
-                  WARP
-                </Button>
-              )
-            }
-          />
-
-          <SettingListItem
-            title={t('pages.xray.nordRouting')}
-            description={t('pages.xray.nordRoutingDesc')}
-            paddings="small"
-            control={
-              nordExist ? (
-                <Select
-                  mode="tags"
-                  value={nordDomains}
-                  style={{ width: '100%' }}
-                  options={SERVICES_OPTIONS}
-                  onChange={(v) => mutate((tt) => ruleSetter(tt, nordTag, 'domain', v))}
-                />
-              ) : (
-                <Button type="primary" onClick={onShowNord} icon={<ApiOutlined />}>
-                  NordVPN
-                </Button>
-              )
-            }
-          />
-        </>
-      ),
-    },
     {
       key: 'reset',
       label: catTabLabel(<ReloadOutlined />, t('pages.settings.resetDefaultConfig'), isMobile),

+ 160 - 0
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 (
+    <>
+      <Alert
+        type="warning"
+        showIcon
+        className="mb-12 hint-alert"
+        title={t('pages.xray.blockConnectionsConfigsDesc')}
+      />
+
+      <SettingListItem
+        title={t('pages.xray.Torrent')}
+        paddings="small"
+        control={
+          <Switch
+            checked={torrentActive}
+            onChange={(checked) => mutate((tt) => {
+              const next = checked
+                ? [...blockedProtocols, ...BITTORRENT_PROTOCOLS]
+                : blockedProtocols.filter((d) => !BITTORRENT_PROTOCOLS.includes(d));
+              ruleSetter(tt, 'blocked', 'protocol', next);
+            })}
+          />
+        }
+      />
+
+      <SettingListItem
+        title={t('pages.xray.blockips')}
+        paddings="small"
+        control={
+          <Select
+            mode="tags"
+            value={blockedIPs}
+            style={{ width: '100%' }}
+            options={IPS_OPTIONS}
+            onChange={(v) => mutate((tt) => ruleSetter(tt, 'blocked', 'ip', v))}
+          />
+        }
+      />
+
+      <SettingListItem
+        title={t('pages.xray.blockdomains')}
+        paddings="small"
+        control={
+          <Select
+            mode="tags"
+            value={blockedDomains}
+            style={{ width: '100%' }}
+            options={BLOCK_DOMAINS_OPTIONS}
+            onChange={(v) => mutate((tt) => ruleSetter(tt, 'blocked', 'domain', v))}
+          />
+        }
+      />
+
+      <Alert
+        type="warning"
+        showIcon
+        className="mb-12 hint-alert"
+        title={t('pages.xray.directConnectionsConfigsDesc')}
+      />
+
+      <SettingListItem
+        title={t('pages.xray.directips')}
+        paddings="small"
+        control={
+          <Select
+            mode="tags"
+            value={directIPs}
+            style={{ width: '100%' }}
+            options={IPS_OPTIONS}
+            onChange={(v) => mutate((tt) => {
+              ruleSetter(tt, 'direct', 'ip', v);
+              syncOutbound(tt, 'direct', directSettings);
+            })}
+          />
+        }
+      />
+
+      <SettingListItem
+        title={t('pages.xray.directdomains')}
+        paddings="small"
+        control={
+          <Select
+            mode="tags"
+            value={directDomains}
+            style={{ width: '100%' }}
+            options={DOMAINS_OPTIONS}
+            onChange={(v) => mutate((tt) => {
+              ruleSetter(tt, 'direct', 'domain', v);
+              syncOutbound(tt, 'direct', directSettings);
+            })}
+          />
+        }
+      />
+
+      <SettingListItem
+        title={t('pages.xray.ipv4Routing')}
+        description={t('pages.xray.ipv4RoutingDesc')}
+        paddings="small"
+        control={
+          <Select
+            mode="tags"
+            value={ipv4Domains}
+            style={{ width: '100%' }}
+            options={SERVICES_OPTIONS}
+            onChange={(v) => mutate((tt) => {
+              ruleSetter(tt, 'IPv4', 'domain', v);
+              syncOutbound(tt, 'IPv4', ipv4Settings);
+            })}
+          />
+        }
+      />
+    </>
+  );
+}

+ 4 - 0
frontend/src/pages/xray/routing/RoutingTab.css

@@ -231,3 +231,7 @@
   opacity: 0.4;
 }
 
+.hint-alert {
+  text-align: center;
+}
+

+ 80 - 48
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}
-      <Space orientation="vertical" size="middle" style={{ width: '100%' }}>
-        <Button type="primary" icon={<PlusOutlined />} onClick={openAdd}>
-          {t('pages.xray.Routings')}
-        </Button>
-
-        {isMobile ? (
-          <RuleCardList
-            rows={rows}
-            draggedIndex={draggedIndex}
-            dropTargetIndex={dropTargetIndex}
-            onHandlePointerDown={onHandlePointerDown}
-            openEdit={openEdit}
-            moveUp={moveUp}
-            moveDown={moveDown}
-            confirmDelete={confirmDelete}
-          />
-        ) : (
-          <Table
-            columns={desktopColumns}
-            dataSource={rows}
-            rowKey={(r) => 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<HTMLElement>;
-            }}
-          />
-        )}
+      <Tabs
+        defaultActiveKey="basic"
+        items={[
+          {
+            key: 'basic',
+            label: catTabLabel(<ControlOutlined />, t('pages.xray.basicRouting'), isMobile),
+            children: (
+              <RoutingBasic
+                templateSettings={templateSettings}
+                setTemplateSettings={setTemplateSettings}
+              />
+            ),
+          },
+          {
+            key: 'rules',
+            label: catTabLabel(<UnorderedListOutlined />, t('pages.xray.Routings'), isMobile),
+            children: (
+              <Space orientation="vertical" size="middle" style={{ width: '100%' }}>
+                <Button type="primary" icon={<PlusOutlined />} onClick={openAdd}>
+                  {t('pages.xray.Routings')}
+                </Button>
 
-        <RuleFormModal
-          open={ruleModalOpen}
-          rule={editingRule}
-          inboundTags={inboundTagOptions}
-          outboundTags={outboundTagOptions}
-          balancerTags={balancerTagOptions}
-          onClose={() => setRuleModalOpen(false)}
-          onConfirm={onRuleConfirm}
-        />
-      </Space>
+                {isMobile ? (
+                  <RuleCardList
+                    rows={rows}
+                    draggedIndex={draggedIndex}
+                    dropTargetIndex={dropTargetIndex}
+                    onHandlePointerDown={onHandlePointerDown}
+                    openEdit={openEdit}
+                    moveUp={moveUp}
+                    moveDown={moveDown}
+                    confirmDelete={confirmDelete}
+                  />
+                ) : (
+                  <Table
+                    columns={desktopColumns}
+                    dataSource={rows}
+                    rowKey={(r) => 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<HTMLElement>;
+                    }}
+                  />
+                )}
+              </Space>
+            ),
+          },
+        ]}
+      />
+      <RuleFormModal
+        open={ruleModalOpen}
+        rule={editingRule}
+        inboundTags={inboundTagOptions}
+        outboundTags={outboundTagOptions}
+        balancerTags={balancerTagOptions}
+        onClose={() => setRuleModalOpen(false)}
+        onConfirm={onRuleConfirm}
+      />
     </>
   );
 }

+ 8 - 1
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) => (
           <div className="criterion-flow">
             {record.sourceIP && <CriterionRow label="IP" value={record.sourceIP} title={`Source IP: ${record.sourceIP}`} />}
@@ -110,6 +115,7 @@ export function useRoutingColumns({
       {
         title: t('pages.xray.rules.dest'),
         align: 'left',
+        width: 200,
         key: 'destination',
         render: (_v, record) => (
           <div className="criterion-flow">
@@ -153,6 +159,7 @@ export function useRoutingColumns({
         align: 'left',
         width: 150,
         key: 'balancer',
+        hidden: !showBalancer,
         render: (_v, record) =>
           record.balancerTag ? (
             <div className="target-row">
@@ -165,6 +172,6 @@ export function useRoutingColumns({
       },
     ],
     // eslint-disable-next-line react-hooks/exhaustive-deps
-    [t, isMobile, rowsLength],
+    [t, isMobile, rowsLength, showSource, showBalancer],
   );
 }