Browse Source

feat(xray): preview export in a modal and switch rule enable toggle

Routing and Outbounds export now opens a TextModal showing the JSON with
copy/download buttons instead of auto-downloading the file. Routing import
and export are collapsed into a "More" dropdown to match the Outbounds tab.
The rule form Enabled field becomes a Switch instead of an Enabled/Disabled
Select.
MHSanaei 19 giờ trước cách đây
mục cha
commit
97c02ef69f

+ 14 - 4
frontend/src/pages/xray/outbounds/OutboundsTab.tsx

@@ -37,8 +37,9 @@ import {
   ImportOutlined,
 } from '@ant-design/icons';
 
-import { FileManager, HttpUtil } from '@/utils';
+import { HttpUtil } from '@/utils';
 import PromptModal from '@/components/feedback/PromptModal';
+import TextModal from '@/components/feedback/TextModal';
 
 import OutboundFormModal from './OutboundFormModal';
 import { propagateOutboundTagRename } from '../basics/helpers';
@@ -226,11 +227,12 @@ export default function OutboundsTab({
   }
 
   const [importOpen, setImportOpen] = useState(false);
+  const [exportOpen, setExportOpen] = useState(false);
+  const [exportContent, setExportContent] = useState('');
 
   function exportOutbounds() {
-    FileManager.downloadTextFile(JSON.stringify(outbounds, null, 2), 'outbounds.json', {
-      type: 'application/json',
-    });
+    setExportContent(JSON.stringify(outbounds, null, 2));
+    setExportOpen(true);
   }
 
   function importOutbounds(value: string) {
@@ -531,6 +533,14 @@ export default function OutboundsTab({
           json
           onConfirm={importOutbounds}
         />
+        <TextModal
+          open={exportOpen}
+          onClose={() => setExportOpen(false)}
+          title={t('pages.xray.exportOutbounds')}
+          content={exportContent}
+          fileName="outbounds.json"
+          json
+        />
 
         {/* Subscription outbounds (read-only, merged at runtime) */}
         {Array.isArray(subscriptionOutbounds) && subscriptionOutbounds.length > 0 && (

+ 25 - 14
frontend/src/pages/xray/routing/RoutingTab.tsx

@@ -1,18 +1,19 @@
 import { useCallback, useMemo, useRef, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Button, Modal, Space, Table, Tabs, message } from 'antd';
+import { Button, Dropdown, Modal, Space, Table, Tabs, message } from 'antd';
 import {
   AimOutlined,
   ControlOutlined,
   ExportOutlined,
   ImportOutlined,
+  MoreOutlined,
   PlusOutlined,
   UnorderedListOutlined,
 } from '@ant-design/icons';
 
 import { catTabLabel } from '@/pages/settings/catTabLabel';
-import { FileManager } from '@/utils';
 import PromptModal from '@/components/feedback/PromptModal';
+import TextModal from '@/components/feedback/TextModal';
 import RoutingBasic from './RoutingBasic';
 import RouteTester from './RouteTester';
 import RuleFormModal from './RuleFormModal';
@@ -144,11 +145,12 @@ export default function RoutingTab({
   }, [templateSettings?.routing?.balancers]);
 
   const [importOpen, setImportOpen] = useState(false);
+  const [exportOpen, setExportOpen] = useState(false);
+  const [exportContent, setExportContent] = useState('');
 
   function exportRules() {
-    FileManager.downloadTextFile(JSON.stringify(rules, null, 2), 'routing-rules.json', {
-      type: 'application/json',
-    });
+    setExportContent(JSON.stringify(rules, null, 2));
+    setExportOpen(true);
   }
 
   function importRules(value: string) {
@@ -333,16 +335,17 @@ export default function RoutingTab({
                   <Button type="primary" icon={<PlusOutlined />} onClick={openAdd}>
                     {t('pages.xray.Routings')}
                   </Button>
-                  <Button icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>
-                    {t('pages.xray.importRules')}
-                  </Button>
-                  <Button
-                    icon={<ExportOutlined />}
-                    onClick={exportRules}
-                    disabled={rules.length === 0}
+                  <Dropdown
+                    trigger={['click']}
+                    menu={{
+                      items: [
+                        { key: 'import', icon: <ImportOutlined />, label: t('pages.xray.importRules'), onClick: () => setImportOpen(true) },
+                        { key: 'export', icon: <ExportOutlined />, label: t('pages.xray.exportRules'), disabled: rules.length === 0, onClick: exportRules },
+                      ],
+                    }}
                   >
-                    {t('pages.xray.exportRules')}
-                  </Button>
+                    <Button icon={<MoreOutlined />}>{t('more')}</Button>
+                  </Dropdown>
                 </Space>
 
                 {isMobile ? (
@@ -405,6 +408,14 @@ export default function RoutingTab({
         json
         onConfirm={importRules}
       />
+      <TextModal
+        open={exportOpen}
+        onClose={() => setExportOpen(false)}
+        title={t('pages.xray.exportRules')}
+        content={exportContent}
+        fileName="routing-rules.json"
+        json
+      />
     </>
   );
 }

+ 4 - 8
frontend/src/pages/xray/routing/RuleFormModal.tsx

@@ -1,6 +1,6 @@
 import { useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Button, Form, Input, Modal, Select, Space, Tooltip } from 'antd';
+import { Button, Form, Input, Modal, Select, Space, Switch, Tooltip } from 'antd';
 import { PlusOutlined, MinusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
 import { InputAddon } from '@/components/ui';
 import { useInboundOptions } from '@/api/queries/useInboundOptions';
@@ -156,14 +156,10 @@ export default function RuleFormModal({
     >
       <Form colon={false} labelCol={{ md: { span: 8 } }} wrapperCol={{ md: { span: 14 } }}>
         <Form.Item label={t('enable')}>
-          <Select
-            value={form.enabled}
-            onChange={(v) => update('enabled', v)}
+          <Switch
+            checked={form.enabled}
+            onChange={(checked) => update('enabled', checked)}
             disabled={isApiRule(rule ?? {})}
-            options={[
-              { value: true, label: t('enable') },
-              { value: false, label: t('disable') },
-            ]}
           />
         </Form.Item>