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.
This commit is contained in:
MHSanaei
2026-06-21 16:29:46 +02:00
parent 7c8889466b
commit 97c02ef69f
3 changed files with 43 additions and 26 deletions
@@ -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
View File
@@ -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
/>
</>
);
}
@@ -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>