mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
feat(frontend): advanced JSON tab on InboundFormModal.new.tsx (Pattern A)
Adds the advanced JSON tab. Each sub-tab (settings / streamSettings / sniffing) renders an AdvancedSliceEditor — a small CodeMirror-backed JsonEditor that holds a local text buffer and forwards parsed JSON to form state on every valid edit. Invalid JSON sits silently in the local buffer; once the user finishes balancing braces / quoting, the next valid parse pushes through to the form. No stamping ref, no apply-on-tab-switch ceremony — the form is the single source of truth. The buffer seeds once from form state on mount. The Modal's destroyOnHidden means each open is a fresh editor instance, so external form mutations during a single open session can't desync the editor either. The streamSettings sub-tab is omitted when streamEnabled is false (matching the legacy modal's behavior for protocols like Http / Mixed that have no stream layer).
This commit is contained in:
parent
40d17b5e59
commit
d6d0c3bb41
1 changed files with 87 additions and 0 deletions
|
|
@ -55,6 +55,9 @@ import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
|
||||||
import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
|
import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
|
||||||
import DateTimePicker from '@/components/DateTimePicker';
|
import DateTimePicker from '@/components/DateTimePicker';
|
||||||
import InputAddon from '@/components/InputAddon';
|
import InputAddon from '@/components/InputAddon';
|
||||||
|
import JsonEditor from '@/components/JsonEditor';
|
||||||
|
import type { FormInstance } from 'antd';
|
||||||
|
import type { NamePath } from 'antd/es/form/interface';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
import type { DBInbound } from '@/models/dbinbound';
|
import type { DBInbound } from '@/models/dbinbound';
|
||||||
|
|
@ -67,6 +70,44 @@ import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
// Sub-editor for one slice of the form (settings, streamSettings, sniffing).
|
||||||
|
// Holds a local text buffer so the user can type freely; on every keystroke
|
||||||
|
// we try to JSON.parse and forward the result to form state. Invalid JSON
|
||||||
|
// is held in the buffer until the next valid moment — no panic on partial
|
||||||
|
// input. The buffer seeds once on mount; the modal's destroyOnHidden makes
|
||||||
|
// each open a fresh editor instance, so we don't need to re-sync on outer
|
||||||
|
// form changes.
|
||||||
|
function AdvancedSliceEditor({
|
||||||
|
form,
|
||||||
|
path,
|
||||||
|
minHeight,
|
||||||
|
maxHeight,
|
||||||
|
}: {
|
||||||
|
form: FormInstance<InboundFormValues>;
|
||||||
|
path: NamePath;
|
||||||
|
minHeight?: string;
|
||||||
|
maxHeight?: string;
|
||||||
|
}) {
|
||||||
|
const [text, setText] = useState(() =>
|
||||||
|
JSON.stringify(form.getFieldValue(path) ?? {}, null, 2),
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<JsonEditor
|
||||||
|
value={text}
|
||||||
|
minHeight={minHeight}
|
||||||
|
maxHeight={maxHeight}
|
||||||
|
onChange={(next) => {
|
||||||
|
setText(next);
|
||||||
|
try {
|
||||||
|
form.setFieldValue(path, JSON.parse(next));
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
|
const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
|
||||||
const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const;
|
const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const;
|
||||||
const NODE_ELIGIBLE_PROTOCOLS = new Set<string>([
|
const NODE_ELIGIBLE_PROTOCOLS = new Set<string>([
|
||||||
|
|
@ -1855,6 +1896,51 @@ export default function InboundFormModalNew({
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const advancedTab = (
|
||||||
|
<Tabs
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'settings',
|
||||||
|
label: t('pages.inbounds.advanced.settings'),
|
||||||
|
children: (
|
||||||
|
<AdvancedSliceEditor
|
||||||
|
form={form}
|
||||||
|
path="settings"
|
||||||
|
minHeight="320px"
|
||||||
|
maxHeight="540px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
...(streamEnabled
|
||||||
|
? [{
|
||||||
|
key: 'stream',
|
||||||
|
label: t('pages.inbounds.advanced.stream'),
|
||||||
|
children: (
|
||||||
|
<AdvancedSliceEditor
|
||||||
|
form={form}
|
||||||
|
path="streamSettings"
|
||||||
|
minHeight="320px"
|
||||||
|
maxHeight="540px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
key: 'sniffing',
|
||||||
|
label: t('pages.inbounds.advanced.sniffing'),
|
||||||
|
children: (
|
||||||
|
<AdvancedSliceEditor
|
||||||
|
form={form}
|
||||||
|
path="sniffing"
|
||||||
|
minHeight="240px"
|
||||||
|
maxHeight="420px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const sniffingTab = (
|
const sniffingTab = (
|
||||||
<>
|
<>
|
||||||
<Form.Item name={['sniffing', 'enabled']} label={t('enable')} valuePropName="checked">
|
<Form.Item name={['sniffing', 'enabled']} label={t('enable')} valuePropName="checked">
|
||||||
|
|
@ -1957,6 +2043,7 @@ export default function InboundFormModalNew({
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
{ key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: sniffingTab },
|
{ key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: sniffingTab },
|
||||||
|
{ key: 'advanced', label: t('pages.xray.advancedTemplate'), children: advancedTab },
|
||||||
]} />
|
]} />
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue