From 7739c3367dab485ccd91b266171f06a6ea0b29e0 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sat, 30 May 2026 20:43:40 +0200 Subject: [PATCH] refactor(frontend): extract inbound sockopt + external-proxy blocks Move the inbound Sockopt (~250 lines) and External Proxy stream blocks out of InboundFormModal into presentational components under inbounds/form/transport/, mirroring the outbound extraction. Each takes its toggle handler (toggleSockopt / toggleExternalProxy) as a prop and keeps its render-prop getFieldValue gate. InboundFormModal drops from 1708 to 1332 lines. Extend inbound-form-blocks.test.tsx with isolated render-snapshot coverage for both (SockoptForm seeded enabled + happyEyeballs; ExternalProxyForm seeded with one TLS entry). No behavior change. --- .../pages/inbounds/form/InboundFormModal.tsx | 386 +----------------- .../form/transport/external-proxy.tsx | 136 ++++++ .../pages/inbounds/form/transport/index.ts | 2 + .../pages/inbounds/form/transport/sockopt.tsx | 270 ++++++++++++ .../inbound-form-blocks.test.tsx.snap | 36 ++ .../src/test/inbound-form-blocks.test.tsx | 47 ++- 6 files changed, 492 insertions(+), 385 deletions(-) create mode 100644 frontend/src/pages/inbounds/form/transport/external-proxy.tsx create mode 100644 frontend/src/pages/inbounds/form/transport/sockopt.tsx diff --git a/frontend/src/pages/inbounds/form/InboundFormModal.tsx b/frontend/src/pages/inbounds/form/InboundFormModal.tsx index f8364ff9..018c1970 100644 --- a/frontend/src/pages/inbounds/form/InboundFormModal.tsx +++ b/frontend/src/pages/inbounds/form/InboundFormModal.tsx @@ -22,7 +22,6 @@ import { ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined, - MinusOutlined, PlusOutlined, } from '@ant-design/icons'; @@ -47,18 +46,10 @@ import { } from '@/schemas/forms/inbound-form'; import { antdRule } from '@/utils/zodForm'; import { - ALPN_OPTION, - Address_Port_Strategy, - DOMAIN_STRATEGY_OPTION, Protocols, SNIFFING_OPTION, - TCP_CONGESTION_OPTION, - UTLS_FINGERPRINT, } from '@/schemas/primitives'; -import { - HappyEyeballsSchema, - SockoptStreamSettingsSchema, -} from '@/schemas/protocols/stream/sockopt'; +import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt'; import { HysteriaStreamSettingsSchema } from '@/schemas/protocols/stream/hysteria'; import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls'; import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality'; @@ -86,10 +77,12 @@ import { WireguardFields, } from './protocols'; import { + ExternalProxyForm, GrpcForm, HttpUpgradeForm, KcpForm, RawForm, + SockoptForm, WsForm, XhttpForm, } from './transport'; @@ -1058,378 +1051,9 @@ export default function InboundFormModal({ {network === 'kcp' && } - { - const a = (prev.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy; - const b = (curr.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy; - return (Array.isArray(a) ? a.length : 0) !== (Array.isArray(b) ? b.length : 0); - }} - > - {({ getFieldValue }) => { - const arr = getFieldValue(['streamSettings', 'externalProxy']); - const on = Array.isArray(arr) && arr.length > 0; - return ( - <> - - - - {on && ( - - {(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 ( - - - - - - ({ - value: a, - label: a, - }))} - /> - - - ); - }} - -
- ))} -
- - )} -
- )} - - ); - }} -
+ - { - const a = (prev.streamSettings as { sockopt?: object } | undefined)?.sockopt; - const b = (curr.streamSettings as { sockopt?: object } | undefined)?.sockopt; - return !!a !== !!b; - }} - > - {({ getFieldValue }) => { - const sock = getFieldValue(['streamSettings', 'sockopt']); - const on = !!sock && typeof sock === 'object' && Object.keys(sock).length > 0; - return ( - <> - - - - {on && ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ({ value: c, label: c }))} - /> - - - - - - - - - ({ value: v, label: v }))} - /> - - - {({ getFieldValue, setFieldValue }) => { - const he = getFieldValue(['streamSettings', 'sockopt', 'happyEyeballs']); - const hasHe = he != null; - return ( - <> - - { - setFieldValue( - ['streamSettings', 'sockopt', 'happyEyeballs'], - v ? HappyEyeballsSchema.parse({}) : undefined, - ); - }} - /> - - {hasHe && ( - <> - - - - - - - - - - - - - - )} - - ); - }} - - - {(fields, { add, remove }) => ( - <> - - - - {fields.map((field) => ( - - - - - - - - - - - - - - - - ))} - - )} - - - )} - - ); - }} - + void; +}) { + const { t } = useTranslation(); + return ( + { + const a = (prev.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy; + const b = (curr.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy; + return (Array.isArray(a) ? a.length : 0) !== (Array.isArray(b) ? b.length : 0); + }} + > + {({ getFieldValue }) => { + const arr = getFieldValue(['streamSettings', 'externalProxy']); + const on = Array.isArray(arr) && arr.length > 0; + return ( + <> + + + + {on && ( + + {(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 ( + + + + + + ({ + value: a, + label: a, + }))} + /> + + + ); + }} + +
+ ))} +
+ + )} +
+ )} + + ); + }} +
+ ); +} diff --git a/frontend/src/pages/inbounds/form/transport/index.ts b/frontend/src/pages/inbounds/form/transport/index.ts index ebcfb747..b582c45c 100644 --- a/frontend/src/pages/inbounds/form/transport/index.ts +++ b/frontend/src/pages/inbounds/form/transport/index.ts @@ -4,3 +4,5 @@ export { default as GrpcForm } from './grpc'; export { default as XhttpForm } from './xhttp'; export { default as HttpUpgradeForm } from './httpupgrade'; export { default as KcpForm } from './kcp'; +export { default as ExternalProxyForm } from './external-proxy'; +export { default as SockoptForm } from './sockopt'; diff --git a/frontend/src/pages/inbounds/form/transport/sockopt.tsx b/frontend/src/pages/inbounds/form/transport/sockopt.tsx new file mode 100644 index 00000000..f139f1ee --- /dev/null +++ b/frontend/src/pages/inbounds/form/transport/sockopt.tsx @@ -0,0 +1,270 @@ +import { useTranslation } from 'react-i18next'; +import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd'; + +import { + Address_Port_Strategy, + DOMAIN_STRATEGY_OPTION, + TCP_CONGESTION_OPTION, +} from '@/schemas/primitives'; +import { HappyEyeballsSchema } from '@/schemas/protocols/stream/sockopt'; + +export default function SockoptForm({ + toggleSockopt, +}: { + toggleSockopt: (on: boolean) => void; +}) { + const { t } = useTranslation(); + return ( + { + const a = (prev.streamSettings as { sockopt?: object } | undefined)?.sockopt; + const b = (curr.streamSettings as { sockopt?: object } | undefined)?.sockopt; + return !!a !== !!b; + }} + > + {({ getFieldValue }) => { + const sock = getFieldValue(['streamSettings', 'sockopt']); + const on = !!sock && typeof sock === 'object' && Object.keys(sock).length > 0; + return ( + <> + + + + {on && ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ({ value: c, label: c }))} + /> + + + + + + + + + ({ value: v, label: v }))} + /> + + + {({ getFieldValue, setFieldValue }) => { + const he = getFieldValue(['streamSettings', 'sockopt', 'happyEyeballs']); + const hasHe = he != null; + return ( + <> + + { + setFieldValue( + ['streamSettings', 'sockopt', 'happyEyeballs'], + v ? HappyEyeballsSchema.parse({}) : undefined, + ); + }} + /> + + {hasHe && ( + <> + + + + + + + + + + + + + + )} + + ); + }} + + + {(fields, { add, remove }) => ( + <> + + + + {fields.map((field) => ( + + + + + + + + + + + + + + + + ))} + + )} + + + )} + + ); + }} + + ); +} diff --git a/frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap b/frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap index b5f850da..53facfe2 100644 --- a/frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap +++ b/frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap @@ -36,6 +36,12 @@ exports[`inbound security forms > TlsForm field structure is stable 1`] = ` ] `; +exports[`inbound transport forms > ExternalProxyForm field structure is stable (one TLS entry) 1`] = ` +[ + "External Proxy", +] +`; + exports[`inbound transport forms > GrpcForm field structure is stable 1`] = ` [ "Service Name", @@ -71,6 +77,36 @@ exports[`inbound transport forms > RawForm field structure is stable 1`] = ` ] `; +exports[`inbound transport forms > SockoptForm field structure is stable (enabled + happy eyeballs) 1`] = ` +[ + "Sockopt", + "Route Mark", + "TCP Keep Alive Interval", + "TCP Keep Alive Idle", + "TCP Max Seg", + "TCP User Timeout", + "TCP Window Clamp", + "Proxy Protocol", + "TCP Fast Open", + "Multipath TCP", + "Penetrate", + "V6 Only", + "Domain Strategy", + "TCP Congestion", + "TProxy", + "Dialer Proxy", + "Interface name", + "Trusted X-Forwarded-For", + "Address+port strategy", + "Happy Eyeballs", + "Try delay (ms)", + "Prioritize IPv6", + "Interleave", + "Max concurrent try", + "Custom sockopt", +] +`; + exports[`inbound transport forms > WsForm field structure is stable 1`] = ` [ "Proxy Protocol", diff --git a/frontend/src/test/inbound-form-blocks.test.tsx b/frontend/src/test/inbound-form-blocks.test.tsx index 15d376f2..7b523575 100644 --- a/frontend/src/test/inbound-form-blocks.test.tsx +++ b/frontend/src/test/inbound-form-blocks.test.tsx @@ -3,10 +3,12 @@ import { Form, type FormInstance } from 'antd'; import type { ReactNode } from 'react'; import { + ExternalProxyForm, GrpcForm, HttpUpgradeForm, KcpForm, RawForm, + SockoptForm, WsForm, XhttpForm, } from '@/pages/inbounds/form/transport'; @@ -14,13 +16,22 @@ import { RealityForm, TlsForm } from '@/pages/inbounds/form/security'; import type { InboundFormValues } from '@/schemas/forms/inbound-form'; import { renderWithProviders, fieldLabels } from './test-utils'; -function FormHarness({ children }: { children: (form: FormInstance) => ReactNode }) { +function FormHarness({ + children, + initialValues, +}: { + children: (form: FormInstance) => ReactNode; + initialValues?: Record; +}) { const [form] = Form.useForm(); - return
{children(form)}
; + return
{children(form)}
; } -function renderInForm(node: (form: FormInstance) => ReactNode) { - return renderWithProviders({node}); +function renderInForm( + node: (form: FormInstance) => ReactNode, + initialValues?: Record, +) { + return renderWithProviders({node}); } const noop = () => {}; @@ -55,6 +66,34 @@ describe('inbound transport forms', () => { renderInForm((form) => ); expect(fieldLabels()).toMatchSnapshot(); }); + + it('ExternalProxyForm field structure is stable (one TLS entry)', () => { + renderInForm( + () => , + { + streamSettings: { + externalProxy: [{ + forceTls: 'tls', + dest: '', + port: 443, + remark: '', + sni: '', + fingerprint: '', + alpn: [], + }], + }, + }, + ); + expect(fieldLabels()).toMatchSnapshot(); + }); + + it('SockoptForm field structure is stable (enabled + happy eyeballs)', () => { + renderInForm( + () => , + { streamSettings: { sockopt: { happyEyeballs: {} } } }, + ); + expect(fieldLabels()).toMatchSnapshot(); + }); }); describe('inbound security forms', () => {