import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import { Button, Card, Checkbox, Empty, Form, Input, InputNumber, Modal, Radio, Select, Space, Switch, Tabs, Tooltip, Typography, message, } from 'antd'; import { ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined, MinusOutlined, PlusOutlined, SyncOutlined, } from '@ant-design/icons'; import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils'; import { rawInboundToFormValues, formValuesToWirePayload, } from '@/lib/xray/inbound-form-adapter'; import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults'; import { canEnableReality, canEnableStream, canEnableTls, isSS2022, } from '@/lib/xray/protocol-capabilities'; import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks'; import { getRandomRealityTarget } from '@/models/reality-targets'; import { InboundFormBaseSchema, InboundFormSchema, type FallbackRow, type InboundFormValues, } from '@/schemas/forms/inbound-form'; import { antdRule } from '@/utils/zodForm'; import { ALPN_OPTION, DOMAIN_STRATEGY_OPTION, Protocols, SNIFFING_OPTION, TCP_CONGESTION_OPTION, TLS_CIPHER_OPTION, TLS_VERSION_OPTION, USAGE_OPTION, UTLS_FINGERPRINT, } from '@/schemas/primitives'; import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt'; import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls'; import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality'; import DateTimePicker from '@/components/DateTimePicker'; import HeaderMapEditor from '@/components/HeaderMapEditor'; 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; import type { DBInbound } from '@/models/dbinbound'; import type { NodeRecord } from '@/api/queries/useNodesQuery'; // Pattern A rewrite of InboundFormModal. Built as a sibling file so the // build stays green while the rewrite progresses section by section. // InboundsPage continues to render the old InboundFormModal.tsx until the // atomic swap at the end (Core Decision 7). 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; path: NamePath; minHeight?: string; maxHeight?: string; }) { const [text, setText] = useState(() => JSON.stringify(form.getFieldValue(path) ?? {}, null, 2), ); return ( { setText(next); try { form.setFieldValue(path, JSON.parse(next)); } catch { } }} /> ); } const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p })); const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const; const NODE_ELIGIBLE_PROTOCOLS = new Set([ Protocols.VLESS, Protocols.VMESS, Protocols.TROJAN, Protocols.SHADOWSOCKS, Protocols.HYSTERIA, Protocols.WIREGUARD, ]); interface InboundFormModalProps { open: boolean; onClose: () => void; onSaved: () => void; mode: 'add' | 'edit'; dbInbound: DBInbound | null; dbInbounds: DBInbound[]; availableNodes?: NodeRecord[]; } function buildAddModeValues(): InboundFormValues { const settings = createDefaultInboundSettings('vless') ?? undefined; return rawInboundToFormValues({ protocol: 'vless', settings, streamSettings: { network: 'tcp', security: 'none' }, sniffing: {}, port: RandomUtil.randomInteger(10000, 60000), listen: '', tag: '', enable: true, trafficReset: 'never', }); } export default function InboundFormModal({ open, onClose, onSaved, mode, dbInbound, dbInbounds, availableNodes, }: InboundFormModalProps) { const { t } = useTranslation(); const [messageApi, messageContextHolder] = message.useMessage(); const [form] = Form.useForm(); const [saving, setSaving] = useState(false); const fallbackKeyRef = useRef(0); const [fallbacks, setFallbacks] = useState([]); const selectableNodes = (availableNodes || []).filter((n) => n.enable); const protocol = (Form.useWatch('protocol', form) ?? '') as string; const isNodeEligible = NODE_ELIGIBLE_PROTOCOLS.has(protocol); const sniffingEnabled = Form.useWatch(['sniffing', 'enabled'], form) ?? false; const vlessEncryption = Form.useWatch(['settings', 'encryption'], form) ?? ''; const ssMethod = Form.useWatch(['settings', 'method'], form); const isSSWith2022 = isSS2022({ protocol, settings: typeof ssMethod === 'string' ? { method: ssMethod } : {}, }); const mixedUdpOn = Form.useWatch(['settings', 'udp'], form) ?? false; const network = Form.useWatch(['streamSettings', 'network'], form) ?? ''; const security = Form.useWatch(['streamSettings', 'security'], form) ?? 'none'; const streamEnabled = canEnableStream({ protocol }); const tlsAllowed = canEnableTls({ protocol, streamSettings: { network, security } }); const realityAllowed = canEnableReality({ protocol, streamSettings: { network, security } }); const isFallbackHost = (protocol === Protocols.VLESS || protocol === Protocols.TROJAN) && network === 'tcp' && (security === 'tls' || security === 'reality'); const fallbackChildOptions = (dbInbounds || []) .filter((ib) => ib.id !== dbInbound?.id) .map((ib) => ({ label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`, value: ib.id, })); const loadFallbacks = async (masterId: number | null) => { if (!masterId) { setFallbacks([]); return; } const msg = await HttpUtil.get(`/panel/api/inbounds/${masterId}/fallbacks`); if (!msg?.success || !Array.isArray(msg.obj)) { setFallbacks([]); return; } setFallbacks( (msg.obj as { childId: number; name?: string; alpn?: string; path?: string; xver?: number }[]) .map((r) => ({ rowKey: `fb-${++fallbackKeyRef.current}`, childId: r.childId, name: r.name || '', alpn: r.alpn || '', path: r.path || '', xver: r.xver || 0, })), ); }; const saveFallbacks = async (masterId: number) => { if (!masterId) return true; const payload = { fallbacks: fallbacks.filter((c) => c.childId).map((c, i) => ({ childId: c.childId, name: c.name, alpn: c.alpn, path: c.path, xver: Number(c.xver) || 0, sortOrder: i, })), }; const msg = await HttpUtil.post( `/panel/api/inbounds/${masterId}/fallbacks`, payload, { headers: { 'Content-Type': 'application/json' } }, ); return !!msg?.success; }; const addFallback = () => { setFallbacks((prev) => [...prev, { rowKey: `fb-${++fallbackKeyRef.current}`, childId: null, name: '', alpn: '', path: '', xver: 0, }]); }; const updateFallback = (rowKey: string, patch: Partial) => { setFallbacks((prev) => prev.map((r) => r.rowKey === rowKey ? { ...r, ...patch } : r)); }; const removeFallback = (idx: number) => { setFallbacks((prev) => prev.filter((_, i) => i !== idx)); }; // Move a fallback row up/down by swapping adjacent indices. The order // is persisted via the fallback row's sortOrder (rebuilt by index on // save), so reordering survives reloads. const moveFallback = (idx: number, direction: -1 | 1) => { setFallbacks((prev) => { const target = idx + direction; if (target < 0 || target >= prev.length) return prev; const next = prev.slice(); [next[idx], next[target]] = [next[target], next[idx]]; return next; }); }; // One-shot: add a fresh fallback row for every eligible inbound (i.e. // every option in fallbackChildOptions) that is not already wired up. // Convenient for operators who want catch-all routing to every host // they manage on the panel. const addAllFallbacks = () => { setFallbacks((prev) => { const alreadyHave = new Set(prev.map((r) => r.childId)); const additions = fallbackChildOptions .filter((opt) => !alreadyHave.has(opt.value)) .map((opt) => ({ rowKey: `fb-${++fallbackKeyRef.current}`, childId: opt.value, name: '', alpn: '', path: '', xver: 0, })); if (additions.length === 0) return prev; return [...prev, ...additions]; }); }; const genRealityKeypair = async () => { setSaving(true); try { const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert'); if (msg?.success) { const obj = msg.obj as { privateKey: string; publicKey: string }; form.setFieldValue(['streamSettings', 'realitySettings', 'privateKey'], obj.privateKey); form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'publicKey'], obj.publicKey); } } finally { setSaving(false); } }; const clearRealityKeypair = () => { form.setFieldValue(['streamSettings', 'realitySettings', 'privateKey'], ''); form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'publicKey'], ''); }; const genMldsa65 = async () => { setSaving(true); try { const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65'); if (msg?.success) { const obj = msg.obj as { seed: string; verify: string }; form.setFieldValue(['streamSettings', 'realitySettings', 'mldsa65Seed'], obj.seed); form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify'], obj.verify); } } finally { setSaving(false); } }; const clearMldsa65 = () => { form.setFieldValue(['streamSettings', 'realitySettings', 'mldsa65Seed'], ''); form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify'], ''); }; const randomizeRealityTarget = () => { const tgt = getRandomRealityTarget() as { target: string; sni: string }; form.setFieldValue(['streamSettings', 'realitySettings', 'target'], tgt.target); form.setFieldValue( ['streamSettings', 'realitySettings', 'serverNames'], tgt.sni.split(',').map((s) => s.trim()).filter(Boolean), ); }; const randomizeShortIds = () => { form.setFieldValue( ['streamSettings', 'realitySettings', 'shortIds'], RandomUtil.randomShortIds().split(',').map((s) => s.trim()).filter(Boolean), ); }; const getNewEchCert = async () => { const sni = form.getFieldValue(['streamSettings', 'tlsSettings', 'serverName']); setSaving(true); try { const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni }); if (msg?.success) { const obj = msg.obj as { echServerKeys: string; echConfigList: string }; form.setFieldValue(['streamSettings', 'tlsSettings', 'echServerKeys'], obj.echServerKeys); form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], obj.echConfigList); } } finally { setSaving(false); } }; const clearEchCert = () => { form.setFieldValue(['streamSettings', 'tlsSettings', 'echServerKeys'], ''); form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], ''); }; const onSecurityChange = (next: string) => { const current = (form.getFieldValue('streamSettings') as Record) ?? {}; const cleaned: Record = { ...current, security: next }; delete cleaned.tlsSettings; delete cleaned.realitySettings; if (next === 'tls') cleaned.tlsSettings = TlsStreamSettingsSchema.parse({}); if (next === 'reality') cleaned.realitySettings = RealityStreamSettingsSchema.parse({}); form.setFieldValue('streamSettings', cleaned); }; const xhttpMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'mode'], form); const xhttpObfsMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'xPaddingObfsMode'], form) ?? false; const xhttpSessionPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'sessionPlacement'], form); const xhttpSeqPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'seqPlacement'], form); const xhttpUplinkPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'uplinkDataPlacement'], form); const externalProxyArr = Form.useWatch(['streamSettings', 'externalProxy'], form); const externalProxyOn = Array.isArray(externalProxyArr) && externalProxyArr.length > 0; const sockoptValue = Form.useWatch(['streamSettings', 'sockopt'], form); const sockoptOn = !!sockoptValue && typeof sockoptValue === 'object' && Object.keys(sockoptValue as object).length > 0; const toggleExternalProxy = (on: boolean) => { if (on) { const port = (form.getFieldValue('port') as number) ?? 443; form.setFieldValue(['streamSettings', 'externalProxy'], [{ forceTls: 'same', dest: typeof window !== 'undefined' ? window.location.hostname : '', port, remark: '', sni: '', fingerprint: '', alpn: [], }]); } else { form.setFieldValue(['streamSettings', 'externalProxy'], []); } }; const toggleSockopt = (on: boolean) => { if (on) { form.setFieldValue( ['streamSettings', 'sockopt'], SockoptStreamSettingsSchema.parse({}), ); } else { form.setFieldValue(['streamSettings', 'sockopt'], undefined); } }; const wgSecretKey = Form.useWatch(['settings', 'secretKey'], form); const wgPubKey = typeof wgSecretKey === 'string' && wgSecretKey.length > 0 ? Wireguard.generateKeypair(wgSecretKey).publicKey : ''; const regenInboundWg = () => { const kp = Wireguard.generateKeypair(); form.setFieldValue(['settings', 'secretKey'], kp.privateKey); }; const regenWgPeerKeypair = (peerName: number) => { const kp = Wireguard.generateKeypair(); form.setFieldValue(['settings', 'peers', peerName, 'privateKey'], kp.privateKey); form.setFieldValue(['settings', 'peers', peerName, 'publicKey'], kp.publicKey); }; const matchesVlessAuth = ( block: { id?: string; label?: string } | undefined | null, authId: string, ) => { if (block?.id === authId) return true; const label = (block?.label || '').toLowerCase().replace(/[-_\s]/g, ''); if (authId === 'mlkem768') return label.includes('mlkem768'); if (authId === 'x25519') return label.includes('x25519'); return false; }; const getNewVlessEnc = async (authId: string) => { if (!authId) return; setSaving(true); try { const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc'); if (!msg?.success) return; const obj = msg.obj as { auths?: { decryption: string; encryption: string; label?: string; id?: string }[]; }; const block = (obj.auths || []).find((a) => matchesVlessAuth(a, authId)); if (!block) return; form.setFieldValue(['settings', 'decryption'], block.decryption); form.setFieldValue(['settings', 'encryption'], block.encryption); } finally { setSaving(false); } }; const clearVlessEnc = () => { form.setFieldValue(['settings', 'decryption'], 'none'); form.setFieldValue(['settings', 'encryption'], 'none'); }; const selectedVlessAuth = (() => { const enc = typeof vlessEncryption === 'string' ? vlessEncryption : ''; if (!enc || enc === 'none') return 'None'; const parts = enc.split('.').filter(Boolean); const authKey = parts[parts.length - 1] || ''; if (!authKey) return t('pages.inbounds.vlessAuthCustom'); return authKey.length > 300 ? t('pages.inbounds.vlessAuthMlkem768') : t('pages.inbounds.vlessAuthX25519'); })(); useEffect(() => { if (!open) return; const initial = mode === 'edit' && dbInbound ? rawInboundToFormValues(dbInbound) : buildAddModeValues(); form.resetFields(); form.setFieldsValue(initial); if ( mode === 'edit' && dbInbound && (dbInbound.protocol === Protocols.VLESS || dbInbound.protocol === Protocols.TROJAN) ) { loadFallbacks(dbInbound.id); } else { setFallbacks([]); } }, [open, mode, dbInbound, form]); // Why: protocol picker reset cascades through the form — clearing the // settings DU branch and dropping a nodeId that no longer applies. The // legacy modal did this imperatively in onProtocolChange; here we hook // into AntD's onValuesChange and let setFieldValue keep the rest of // the form state intact. const onValuesChange = (changed: Partial) => { if (mode === 'edit') return; if ('protocol' in changed && typeof changed.protocol === 'string') { const next = changed.protocol; const settings = createDefaultInboundSettings(next) ?? undefined; form.setFieldValue('settings', settings); if (!NODE_ELIGIBLE_PROTOCOLS.has(next)) { form.setFieldValue('nodeId', null); } } }; const submit = async () => { let values: InboundFormValues; try { values = await form.validateFields(); } catch { return; } const parsed = InboundFormSchema.safeParse(values); if (!parsed.success) { const issue = parsed.error.issues[0]; messageApi.error( t(issue?.message ?? 'somethingWentWrong', { defaultValue: issue?.message ?? 'invalid', }), ); return; } setSaving(true); try { const payload = formValuesToWirePayload(parsed.data); const url = mode === 'edit' && dbInbound ? `/panel/api/inbounds/update/${dbInbound.id}` : '/panel/api/inbounds/add'; const msg = await HttpUtil.post(url, payload); if (msg?.success) { if (isFallbackHost) { const obj = msg.obj as { id?: number; Id?: number } | null; const masterId = mode === 'edit' ? dbInbound!.id : (obj?.id ?? obj?.Id ?? 0); if (masterId) await saveFallbacks(masterId); } onSaved(); onClose(); } } finally { setSaving(false); } }; const title = mode === 'edit' ? t('pages.inbounds.modifyInbound') : t('pages.inbounds.addInbound'); const okText = mode === 'edit' ? t('pages.clients.submitEdit') : t('create'); const basicTab = ( <> {selectableNodes.length > 0 && isNodeEligible && ( )} {t('pages.inbounds.totalFlow')} } > prev.total !== curr.total} > {({ getFieldValue, setFieldValue }) => { const totalBytes = (getFieldValue('total') as number) ?? 0; const totalGB = totalBytes ? Math.round((totalBytes / SizeFormatter.ONE_GB) * 100) / 100 : 0; return ( { const bytes = NumberFormatter.toFixed( (Number(v) || 0) * SizeFormatter.ONE_GB, 0, ); setFieldValue('total', bytes); }} /> ); }} {t('pages.inbounds.expireDate')} } > prev.expiryTime !== curr.expiryTime} > {({ getFieldValue, setFieldValue }) => { const expiry = (getFieldValue('expiryTime') as number) ?? 0; return ( 0 ? dayjs(expiry) : null} onChange={(d) => setFieldValue('expiryTime', d ? d.valueOf() : 0)} /> ); }} ); const fallbacksCard = ( {fallbacks.length === 0 && ( )} {fallbacks.map((record, idx) => (
updateFallback(record.rowKey, { name: e.target.value })} /> ALPN updateFallback(record.rowKey, { alpn: e.target.value })} /> Path updateFallback(record.rowKey, { path: e.target.value })} /> xver updateFallback(record.rowKey, { xver: Number(v) || 0 })} />
))}
); const protocolTab = ( <> {protocol === Protocols.WIREGUARD && ( <> Secret key{' '} } > {(fields, { add, remove }) => ( <> {fields.map((field, idx) => (
{fields.length > 1 && ( )} Secret key{' '} regenWgPeerKeypair(field.name)} /> } > {(ipFields, { add: addIp, remove: removeIp }) => ( {ipFields.map((ipField) => ( {ipFields.length > 1 && ( )} ))} )}
))} )}
)} {protocol === Protocols.TUN && ( <> {(fields, { add, remove }) => ( {fields.map((field, j) => ( ))} )} {(fields, { add, remove }) => ( {fields.map((field, j) => ( ))} )} {(fields, { add, remove }) => ( Auto system routes } > {fields.map((field, j) => ( ))} )} Auto outbounds interface } > )} {protocol === Protocols.TUNNEL && ( <> {(fields, { add, remove }) => ( <> {fields.length > 0 && ( {fields.map((field, idx) => ( {String(idx + 1)} ))} )} )} )} {(protocol === Protocols.HTTP || protocol === Protocols.MIXED) && ( <> {(fields, { add, remove }) => ( <> {fields.length > 0 && ( {fields.map((field, idx) => ( {String(idx + 1)} ))} )} )} {protocol === Protocols.HTTP && ( )} {protocol === Protocols.MIXED && ( <> {mixedUdpOn && ( )} )} )} {protocol === Protocols.SHADOWSOCKS && ( <> {isSSWith2022 && ( Password{' '} { const method = form.getFieldValue(['settings', 'method']); form.setFieldValue( ['settings', 'password'], RandomUtil.randomShadowsocksPassword(method as string), ); }} /> } > )} )} {protocol === Protocols.VLESS && ( <> {t('pages.inbounds.vlessAuthSelected', { auth: selectedVlessAuth })} )} {isFallbackHost && fallbacksCard} ); // Switching `network` swaps which per-network key (tcpSettings, wsSettings, // grpcSettings, ...) appears on the wire. We clear the previously selected // network's settings blob and seed a default empty object for the new one // so AntD's Form.Items aren't pointed at undefined nested paths. const onNetworkChange = (next: string) => { const ALL = ['tcpSettings', 'kcpSettings', 'wsSettings', 'grpcSettings', 'httpupgradeSettings', 'xhttpSettings']; const current = (form.getFieldValue('streamSettings') as Record) ?? {}; const cleaned: Record = { ...current, network: next }; for (const k of ALL) { if (k !== `${next}Settings`) delete cleaned[k]; } cleaned[`${next}Settings`] = {}; form.setFieldValue('streamSettings', cleaned); }; const streamTab = ( <> {protocol !== Protocols.HYSTERIA && ( )} {network === 'tcp' && ( <> prev.streamSettings?.tcpSettings?.header?.type !== curr.streamSettings?.tcpSettings?.header?.type } > {({ getFieldValue, setFieldValue }) => { const headerType = getFieldValue( ['streamSettings', 'tcpSettings', 'header', 'type'], ) as string | undefined; return ( { setFieldValue( ['streamSettings', 'tcpSettings', 'header'], v ? { type: 'http', request: { version: '1.1', method: 'GET', path: ['/'], headers: {}, }, response: { version: '1.1', status: '200', reason: 'OK', headers: {}, }, } : { type: 'none' }, ); }} /> ); }} {/* Host + path camouflage inputs only render when the Switch above is on. Both are string[] on the wire; normalize + getValueProps translate to/from comma-joined input. Mirrors the symmetric outbound side. */} prev.streamSettings?.tcpSettings?.header?.type !== curr.streamSettings?.tcpSettings?.header?.type } > {({ getFieldValue }) => { const headerType = getFieldValue( ['streamSettings', 'tcpSettings', 'header', 'type'], ) as string | undefined; if (headerType !== 'http') return null; return ( <> typeof v === 'string' ? v.split(',').map((s) => s.trim()).filter(Boolean) : Array.isArray(v) ? v : [] } getValueProps={(v: unknown) => ({ value: Array.isArray(v) ? v.join(',') : '', })} > typeof v === 'string' ? v.split(',').map((s) => s.trim()).filter(Boolean) : Array.isArray(v) ? v : ['/'] } getValueProps={(v: unknown) => ({ value: Array.isArray(v) ? v.join(',') : '/', })} > {/* Response side: shaped as a separate sub-object on the wire ({version, status, reason, headers}). Inbound is the server, so the response side is the one the panel sends back to clients during HTTP camouflage. */} ); }} )} {network === 'ws' && ( <> )} {network === 'grpc' && ( <> )} {network === 'xhttp' && ( <> {xhttpMode === 'packet-up' && ( <> )} {xhttpMode === 'stream-up' && ( )} {xhttpObfsMode && ( <> )} {xhttpSessionPlacement && xhttpSessionPlacement !== 'path' && ( )} {xhttpSeqPlacement && xhttpSeqPlacement !== 'path' && ( )} {xhttpMode === 'packet-up' && ( <> {xhttpUplinkPlacement && xhttpUplinkPlacement !== 'body' && ( )} )} )} {network === 'httpupgrade' && ( <> )} {network === 'kcp' && ( <> )} {externalProxyOn && ( {(fields, { add, remove }) => ( <> {fields.map((field) => (
remove(field.name)}> prev.streamSettings?.externalProxy?.[field.name]?.forceTls !== curr.streamSettings?.externalProxy?.[field.name]?.forceTls } > {({ getFieldValue }) => { const ft = getFieldValue([ 'streamSettings', 'externalProxy', field.name, 'forceTls', ]); if (ft !== 'tls') return null; return ( ); }}
))}
)}
)} {sockoptOn && ( <> )} ); const securityTab = ( <> prev.streamSettings?.security !== curr.streamSettings?.security } > {({ getFieldValue }) => { const sec = getFieldValue(['streamSettings', 'security']) ?? 'none'; return ( ); }} {security === 'tls' && ( <> {(certFields, { add, remove }) => ( <> {certFields.map((certField, idx) => (
{t('pages.inbounds.certificatePath')} {t('pages.inbounds.certificateContent')} {certFields.length > 1 && ( )} prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile !== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile } > {({ getFieldValue }) => { const useFile = getFieldValue([ 'streamSettings', 'tlsSettings', 'certificates', certField.name, 'useFile', ]); return useFile ? ( <> ) : ( <> typeof v === 'string' ? v.split('\n') : v} getValueProps={(v) => ({ value: Array.isArray(v) ? v.join('\n') : v, })} >