mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-07 13:44:24 +00:00
refactor(frontend): split inbound vless/http/mixed/hysteria protocol forms
Extract the remaining inbound protocol blocks into inbounds/form/protocols/: vless (auth handlers/state as props), http + mixed (shared accounts-list), hysteria. Drop now-unused HysteriaMasqueradeForm/Typography/Text imports from the modal. InboundFormModal.tsx 2841 -> 2478. Inbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
This commit is contained in:
parent
e8381564a6
commit
52cbcfb99e
7 changed files with 205 additions and 133 deletions
|
|
@ -16,7 +16,6 @@ import {
|
||||||
Switch,
|
Switch,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
|
||||||
message,
|
message,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
|
|
@ -77,12 +76,20 @@ import { XHttpStreamSettingsSchema } from '@/schemas/protocols/stream/xhttp';
|
||||||
import { DateTimePicker } from '@/components/form';
|
import { DateTimePicker } from '@/components/form';
|
||||||
import { FinalMaskForm } from '@/lib/xray/forms/transport';
|
import { FinalMaskForm } from '@/lib/xray/forms/transport';
|
||||||
import { HeaderMapEditor } from '@/components/form';
|
import { HeaderMapEditor } from '@/components/form';
|
||||||
import { HysteriaMasqueradeForm } from '@/lib/xray/forms/protocols/shared';
|
|
||||||
import { InputAddon } from '@/components/ui';
|
import { InputAddon } from '@/components/ui';
|
||||||
import './InboundFormModal.css';
|
import './InboundFormModal.css';
|
||||||
|
|
||||||
import { AdvancedAllEditor, AdvancedSliceEditor } from './advanced-editors';
|
import { AdvancedAllEditor, AdvancedSliceEditor } from './advanced-editors';
|
||||||
import { ShadowsocksFields, TunFields, TunnelFields, WireguardFields } from './protocols';
|
import {
|
||||||
|
HttpFields,
|
||||||
|
HysteriaFields,
|
||||||
|
MixedFields,
|
||||||
|
ShadowsocksFields,
|
||||||
|
TunFields,
|
||||||
|
TunnelFields,
|
||||||
|
VlessFields,
|
||||||
|
WireguardFields,
|
||||||
|
} from './protocols';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
|
import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
|
||||||
|
|
@ -93,8 +100,6 @@ import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||||
// InboundsPage continues to render the old InboundFormModal.tsx until the
|
// InboundsPage continues to render the old InboundFormModal.tsx until the
|
||||||
// atomic swap at the end (Core Decision 7).
|
// atomic swap at the end (Core Decision 7).
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -952,119 +957,12 @@ export default function InboundFormModal({
|
||||||
|
|
||||||
{protocol === Protocols.TUNNEL && <TunnelFields />}
|
{protocol === Protocols.TUNNEL && <TunnelFields />}
|
||||||
|
|
||||||
{(protocol === Protocols.HTTP || protocol === Protocols.MIXED) && (
|
{protocol === Protocols.HTTP && <HttpFields />}
|
||||||
<>
|
{protocol === Protocols.MIXED && <MixedFields mixedUdpOn={mixedUdpOn} />}
|
||||||
<Form.List name={['settings', 'accounts']}>
|
|
||||||
{(fields, { add, remove }) => (
|
|
||||||
<>
|
|
||||||
<Form.Item label={t('pages.inbounds.form.accounts')}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={() => add({
|
|
||||||
user: RandomUtil.randomLowerAndNum(8),
|
|
||||||
pass: RandomUtil.randomLowerAndNum(12),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<PlusOutlined /> {t('add')}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
{fields.length > 0 && (
|
|
||||||
<Form.Item wrapperCol={{ span: 24 }}>
|
|
||||||
{fields.map((field, idx) => (
|
|
||||||
<Space.Compact key={field.key} className="mb-8" block>
|
|
||||||
<InputAddon>{String(idx + 1)}</InputAddon>
|
|
||||||
<Form.Item name={[field.name, 'user']} noStyle>
|
|
||||||
<Input placeholder={t('username')} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name={[field.name, 'pass']} noStyle>
|
|
||||||
<Input placeholder={t('password')} />
|
|
||||||
</Form.Item>
|
|
||||||
<Button onClick={() => remove(field.name)}>
|
|
||||||
<MinusOutlined />
|
|
||||||
</Button>
|
|
||||||
</Space.Compact>
|
|
||||||
))}
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Form.List>
|
|
||||||
{protocol === Protocols.HTTP && (
|
|
||||||
<Form.Item
|
|
||||||
name={['settings', 'allowTransparent']}
|
|
||||||
label={t('pages.inbounds.form.allowTransparent')}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
{protocol === Protocols.MIXED && (
|
|
||||||
<>
|
|
||||||
<Form.Item name={['settings', 'auth']} label={t('pages.inbounds.info.auth')}>
|
|
||||||
<Select
|
|
||||||
options={[
|
|
||||||
{ value: 'noauth', label: 'noauth' },
|
|
||||||
{ value: 'password', label: 'password' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['settings', 'udp']}
|
|
||||||
label="UDP"
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
{mixedUdpOn && (
|
|
||||||
<Form.Item name={['settings', 'ip']} label="UDP IP">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{protocol === Protocols.SHADOWSOCKS && <ShadowsocksFields form={form} isSSWith2022={isSSWith2022} />}
|
{protocol === Protocols.SHADOWSOCKS && <ShadowsocksFields form={form} isSSWith2022={isSSWith2022} />}
|
||||||
|
|
||||||
{protocol === Protocols.VLESS && (
|
{protocol === Protocols.VLESS && <VlessFields saving={saving} selectedVlessAuth={selectedVlessAuth} network={network} security={security} getNewVlessEnc={getNewVlessEnc} clearVlessEnc={clearVlessEnc} />}
|
||||||
<>
|
|
||||||
<Form.Item name={['settings', 'decryption']} label={t('pages.inbounds.decryption')}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name={['settings', 'encryption']} label={t('pages.inbounds.encryption')}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label=" ">
|
|
||||||
<Space size={8} wrap>
|
|
||||||
<Button type="primary" loading={saving} onClick={() => getNewVlessEnc('x25519')}>
|
|
||||||
{t('pages.inbounds.vlessAuthX25519')}
|
|
||||||
</Button>
|
|
||||||
<Button type="primary" loading={saving} onClick={() => getNewVlessEnc('mlkem768')}>
|
|
||||||
{t('pages.inbounds.vlessAuthMlkem768')}
|
|
||||||
</Button>
|
|
||||||
<Button danger onClick={clearVlessEnc}>{t('clear')}</Button>
|
|
||||||
</Space>
|
|
||||||
<Text type="secondary" className="vless-auth-state">
|
|
||||||
{t('pages.inbounds.vlessAuthSelected', { auth: selectedVlessAuth })}
|
|
||||||
</Text>
|
|
||||||
</Form.Item>
|
|
||||||
{network === 'tcp' && (security === 'tls' || security === 'reality') && (
|
|
||||||
<Form.Item
|
|
||||||
label={t('pages.inbounds.form.visionTestseed')}
|
|
||||||
extra="Applies only to clients using the xtls-rprx-vision flow; ignored otherwise."
|
|
||||||
>
|
|
||||||
<Space.Compact block>
|
|
||||||
{[900, 500, 900, 256].map((def, i) => (
|
|
||||||
<Form.Item key={i} name={['settings', 'testseed', i]} noStyle initialValue={def}>
|
|
||||||
<InputNumber min={1} style={{ width: '25%' }} />
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
</Space.Compact>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isFallbackHost && fallbacksCard}
|
{isFallbackHost && fallbacksCard}
|
||||||
</>
|
</>
|
||||||
|
|
@ -1148,24 +1046,7 @@ export default function InboundFormModal({
|
||||||
auth + udpIdleTimeout are required, masquerade is an optional
|
auth + udpIdleTimeout are required, masquerade is an optional
|
||||||
sub-object that lets xray-core disguise the listener as an
|
sub-object that lets xray-core disguise the listener as an
|
||||||
HTTP server when probed. */}
|
HTTP server when probed. */}
|
||||||
{protocol === Protocols.HYSTERIA && (
|
{protocol === Protocols.HYSTERIA && <HysteriaFields form={form} />}
|
||||||
<>
|
|
||||||
<Form.Item
|
|
||||||
label={t('pages.inbounds.form.version')}
|
|
||||||
name={['streamSettings', 'hysteriaSettings', 'version']}
|
|
||||||
>
|
|
||||||
<InputNumber min={2} max={2} disabled />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t('pages.inbounds.form.udpIdleTimeout')}
|
|
||||||
name={['streamSettings', 'hysteriaSettings', 'udpIdleTimeout']}
|
|
||||||
>
|
|
||||||
<InputNumber min={1} style={{ width: '100%' }} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<HysteriaMasqueradeForm form={form} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{network === 'tcp' && (
|
{network === 'tcp' && (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
47
frontend/src/pages/inbounds/form/protocols/accounts-list.tsx
Normal file
47
frontend/src/pages/inbounds/form/protocols/accounts-list.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button, Form, Input, Space } from 'antd';
|
||||||
|
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
import { RandomUtil } from '@/utils';
|
||||||
|
import { InputAddon } from '@/components/ui';
|
||||||
|
|
||||||
|
export default function AccountsList() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Form.List name={['settings', 'accounts']}>
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
<Form.Item label={t('pages.inbounds.form.accounts')}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() => add({
|
||||||
|
user: RandomUtil.randomLowerAndNum(8),
|
||||||
|
pass: RandomUtil.randomLowerAndNum(12),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<PlusOutlined /> {t('add')}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
{fields.length > 0 && (
|
||||||
|
<Form.Item wrapperCol={{ span: 24 }}>
|
||||||
|
{fields.map((field, idx) => (
|
||||||
|
<Space.Compact key={field.key} className="mb-8" block>
|
||||||
|
<InputAddon>{String(idx + 1)}</InputAddon>
|
||||||
|
<Form.Item name={[field.name, 'user']} noStyle>
|
||||||
|
<Input placeholder={t('username')} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={[field.name, 'pass']} noStyle>
|
||||||
|
<Input placeholder={t('password')} />
|
||||||
|
</Form.Item>
|
||||||
|
<Button onClick={() => remove(field.name)}>
|
||||||
|
<MinusOutlined />
|
||||||
|
</Button>
|
||||||
|
</Space.Compact>
|
||||||
|
))}
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
frontend/src/pages/inbounds/form/protocols/http.tsx
Normal file
20
frontend/src/pages/inbounds/form/protocols/http.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Form, Switch } from 'antd';
|
||||||
|
|
||||||
|
import AccountsList from './accounts-list';
|
||||||
|
|
||||||
|
export default function HttpFields() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AccountsList />
|
||||||
|
<Form.Item
|
||||||
|
name={['settings', 'allowTransparent']}
|
||||||
|
label={t('pages.inbounds.form.allowTransparent')}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
frontend/src/pages/inbounds/form/protocols/hysteria.tsx
Normal file
27
frontend/src/pages/inbounds/form/protocols/hysteria.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Form, InputNumber, type FormInstance } from 'antd';
|
||||||
|
|
||||||
|
import { HysteriaMasqueradeForm } from '@/lib/xray/forms/protocols/shared';
|
||||||
|
import type { InboundFormValues } from '@/schemas/forms/inbound-form';
|
||||||
|
|
||||||
|
export default function HysteriaFields({ form }: { form: FormInstance<InboundFormValues> }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label={t('pages.inbounds.form.version')}
|
||||||
|
name={['streamSettings', 'hysteriaSettings', 'version']}
|
||||||
|
>
|
||||||
|
<InputNumber min={2} max={2} disabled />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t('pages.inbounds.form.udpIdleTimeout')}
|
||||||
|
name={['streamSettings', 'hysteriaSettings', 'udpIdleTimeout']}
|
||||||
|
>
|
||||||
|
<InputNumber min={1} style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<HysteriaMasqueradeForm form={form} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,3 +2,7 @@ export { default as TunFields } from './tun';
|
||||||
export { default as TunnelFields } from './tunnel';
|
export { default as TunnelFields } from './tunnel';
|
||||||
export { default as ShadowsocksFields } from './shadowsocks';
|
export { default as ShadowsocksFields } from './shadowsocks';
|
||||||
export { default as WireguardFields } from './wireguard';
|
export { default as WireguardFields } from './wireguard';
|
||||||
|
export { default as HysteriaFields } from './hysteria';
|
||||||
|
export { default as HttpFields } from './http';
|
||||||
|
export { default as MixedFields } from './mixed';
|
||||||
|
export { default as VlessFields } from './vless';
|
||||||
|
|
|
||||||
33
frontend/src/pages/inbounds/form/protocols/mixed.tsx
Normal file
33
frontend/src/pages/inbounds/form/protocols/mixed.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Form, Input, Select, Switch } from 'antd';
|
||||||
|
|
||||||
|
import AccountsList from './accounts-list';
|
||||||
|
|
||||||
|
export default function MixedFields({ mixedUdpOn }: { mixedUdpOn: boolean }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AccountsList />
|
||||||
|
<Form.Item name={['settings', 'auth']} label={t('pages.inbounds.info.auth')}>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: 'noauth', label: 'noauth' },
|
||||||
|
{ value: 'password', label: 'password' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={['settings', 'udp']}
|
||||||
|
label="UDP"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
{mixedUdpOn && (
|
||||||
|
<Form.Item name={['settings', 'ip']} label="UDP IP">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
frontend/src/pages/inbounds/form/protocols/vless.tsx
Normal file
60
frontend/src/pages/inbounds/form/protocols/vless.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button, Form, Input, InputNumber, Space, Typography } from 'antd';
|
||||||
|
|
||||||
|
interface VlessFieldsProps {
|
||||||
|
saving: boolean;
|
||||||
|
selectedVlessAuth: string;
|
||||||
|
network: string;
|
||||||
|
security: string;
|
||||||
|
getNewVlessEnc: (kind: 'x25519' | 'mlkem768') => void;
|
||||||
|
clearVlessEnc: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VlessFields({
|
||||||
|
saving,
|
||||||
|
selectedVlessAuth,
|
||||||
|
network,
|
||||||
|
security,
|
||||||
|
getNewVlessEnc,
|
||||||
|
clearVlessEnc,
|
||||||
|
}: VlessFieldsProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item name={['settings', 'decryption']} label={t('pages.inbounds.decryption')}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={['settings', 'encryption']} label={t('pages.inbounds.encryption')}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label=" ">
|
||||||
|
<Space size={8} wrap>
|
||||||
|
<Button type="primary" loading={saving} onClick={() => getNewVlessEnc('x25519')}>
|
||||||
|
{t('pages.inbounds.vlessAuthX25519')}
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" loading={saving} onClick={() => getNewVlessEnc('mlkem768')}>
|
||||||
|
{t('pages.inbounds.vlessAuthMlkem768')}
|
||||||
|
</Button>
|
||||||
|
<Button danger onClick={clearVlessEnc}>{t('clear')}</Button>
|
||||||
|
</Space>
|
||||||
|
<Typography.Text type="secondary" className="vless-auth-state">
|
||||||
|
{t('pages.inbounds.vlessAuthSelected', { auth: selectedVlessAuth })}
|
||||||
|
</Typography.Text>
|
||||||
|
</Form.Item>
|
||||||
|
{network === 'tcp' && (security === 'tls' || security === 'reality') && (
|
||||||
|
<Form.Item
|
||||||
|
label={t('pages.inbounds.form.visionTestseed')}
|
||||||
|
extra="Applies only to clients using the xtls-rprx-vision flow; ignored otherwise."
|
||||||
|
>
|
||||||
|
<Space.Compact block>
|
||||||
|
{[900, 500, 900, 256].map((def, i) => (
|
||||||
|
<Form.Item key={i} name={['settings', 'testseed', i]} noStyle initialValue={def}>
|
||||||
|
<InputNumber min={1} style={{ width: '25%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
</Space.Compact>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue