From 53f6ed394f88bd6d639661dc19f515aafa48b0f7 Mon Sep 17 00:00:00 2001 From: Abdalrahman Date: Mon, 15 Jun 2026 01:43:49 +0300 Subject: [PATCH] Add Enable/Disable Toggle for Xray Routing Rules (#5296) * feat: add enable/disable toggle for xray routing rules * fix(routing): never let the internal api rule be disabled The Enable/Disable toggle could strip the stats api rule: its table switch was locked, but the rule-form modal's Enable dropdown was not, and stripDisabledRules had no api-rule guard (EnsureStatsRouting's delete only runs when the api rule isn't already first). A disabled api rule then dropped out of the generated config and broke traffic accounting. - stripDisabledRules now always keeps the api rule, even if marked disabled, and strips the panel-only enabled key from every rule - extract isApiRule helper (backend + frontend) and reuse it across the table switch, card switch, and form modal - disable the form-modal Enable dropdown for the api rule - add stripDisabledRules tests covering the api-rule survival path --------- Co-authored-by: Sanaei --- .../src/pages/xray/routing/RoutingTab.css | 4 + .../src/pages/xray/routing/RoutingTab.tsx | 10 +++ .../src/pages/xray/routing/RuleCardList.tsx | 17 +++- .../src/pages/xray/routing/RuleFormModal.tsx | 18 +++- frontend/src/pages/xray/routing/helpers.ts | 7 ++ frontend/src/pages/xray/routing/types.ts | 1 + .../pages/xray/routing/useRoutingColumns.tsx | 86 +++++++++++------- frontend/src/schemas/routing.ts | 1 + frontend/src/schemas/xray.ts | 1 + internal/web/service/xray.go | 54 ++++++++++++ internal/web/service/xray_setting.go | 61 +++++++------ internal/web/service/xray_strip_rules_test.go | 88 +++++++++++++++++++ 12 files changed, 287 insertions(+), 61 deletions(-) create mode 100644 internal/web/service/xray_strip_rules_test.go diff --git a/frontend/src/pages/xray/routing/RoutingTab.css b/frontend/src/pages/xray/routing/RoutingTab.css index 1d3590f4d..33176d793 100644 --- a/frontend/src/pages/xray/routing/RoutingTab.css +++ b/frontend/src/pages/xray/routing/RoutingTab.css @@ -235,3 +235,7 @@ text-align: center; } +.rule-disabled { + opacity: 0.5; + filter: grayscale(1); +} diff --git a/frontend/src/pages/xray/routing/RoutingTab.tsx b/frontend/src/pages/xray/routing/RoutingTab.tsx index 783e03e99..6da51608b 100644 --- a/frontend/src/pages/xray/routing/RoutingTab.tsx +++ b/frontend/src/pages/xray/routing/RoutingTab.tsx @@ -58,6 +58,7 @@ export default function RoutingTab({ () => rules.map((rule, idx) => { const r: RuleRow = { key: idx }; + r.enabled = rule.enabled !== false; r.domain = arrJoin(rule.domain); r.ip = arrJoin(rule.ip); r.port = rule.port; @@ -185,6 +186,13 @@ export default function RoutingTab({ [list[idx + 1], list[idx]] = [list[idx], list[idx + 1]]; }); } + function toggleRule(idx: number, enabled: boolean) { + mutate((tt) => { + const list = tt.routing?.rules; + if (!list || !list[idx]) return; + list[idx].enabled = enabled; + }); + } function onHandlePointerDown(idx: number, ev: React.PointerEvent) { if (ev.button != null && ev.button !== 0) return; @@ -247,6 +255,7 @@ export default function RoutingTab({ moveUp, moveDown, confirmDelete, + toggleRule, }); const tableScrollX = desktopColumns.reduce((sum, c) => { @@ -289,6 +298,7 @@ export default function RoutingTab({ moveUp={moveUp} moveDown={moveDown} confirmDelete={confirmDelete} + toggleRule={toggleRule} /> ) : ( void; moveDown: (idx: number) => void; confirmDelete: (idx: number) => void; + toggleRule: (idx: number, enabled: boolean) => void; } export default function RuleCardList({ @@ -36,6 +37,7 @@ export default function RuleCardList({ moveUp, moveDown, confirmDelete, + toggleRule, }: RuleCardListProps) { const { t } = useTranslation(); const { data: inboundOptions } = useInboundOptions(); @@ -50,7 +52,9 @@ export default function RuleCardList({ key={rule.key} className={`rule-card ${draggedIndex === index ? 'row-dragging' : ''} ${ dropTargetIndex === index && draggedIndex != null && index < draggedIndex ? 'drop-before' : '' - } ${dropTargetIndex === index && draggedIndex != null && index > draggedIndex ? 'drop-after' : ''}`} + } ${dropTargetIndex === index && draggedIndex != null && index > draggedIndex ? 'drop-after' : ''} ${ + rule.enabled === false ? 'rule-disabled' : '' + }`} data-row-key={index} >
@@ -72,6 +76,13 @@ export default function RuleCardList({ >
diff --git a/frontend/src/pages/xray/routing/RuleFormModal.tsx b/frontend/src/pages/xray/routing/RuleFormModal.tsx index eafd1547e..a92ce665d 100644 --- a/frontend/src/pages/xray/routing/RuleFormModal.tsx +++ b/frontend/src/pages/xray/routing/RuleFormModal.tsx @@ -5,9 +5,10 @@ import { PlusOutlined, MinusOutlined, QuestionCircleOutlined } from '@ant-design import { InputAddon } from '@/components/ui'; import { useInboundOptions } from '@/api/queries/useInboundOptions'; import { RuleFormSchema, type RuleFormValues } from '@/schemas/xray'; -import { buildRemarkByTag, formatInboundTag } from './helpers'; +import { buildRemarkByTag, formatInboundTag, isApiRule } from './helpers'; export interface RoutingRule { + enabled?: boolean; type?: string; domain?: string | string[]; ip?: string | string[]; @@ -38,6 +39,7 @@ interface RuleFormModalProps { type FormState = RuleFormValues; const initialForm = (): FormState => ({ + enabled: true, domain: '', ip: '', port: '', @@ -81,6 +83,7 @@ export default function RuleFormModal({ if (!open) return; if (rule) { setForm({ + enabled: rule.enabled !== false, domain: Array.isArray(rule.domain) ? rule.domain.join(',') : rule.domain || '', ip: Array.isArray(rule.ip) ? rule.ip.join(',') : rule.ip || '', port: rule.port || '', @@ -109,6 +112,7 @@ export default function RuleFormModal({ const v = validated.data; const built: Record = { type: 'field', + enabled: v.enabled, domain: csv(v.domain), ip: csv(v.ip), port: v.port, @@ -151,6 +155,18 @@ export default function RuleFormModal({ onCancel={onClose} >
+ +