refactor(frontend): retire all AntD + Zod deprecations

Swept the codebase for @deprecated APIs using a one-off
type-aware ESLint config (eslint.deprecated.config.js) and
fixed every hit:

- 78 instances of `<Select.Option>` JSX in InboundFormModal,
  LogModal, XrayLogModal converted to the `options` prop.
- Zod's `z.ZodTypeAny` (deprecated for `z.ZodType` in zod v4)
  replaced in _envelope.ts, zodForm.ts, zodValidate.ts, and
  inbound-form-adapter.ts.
- Select's `filterOption` / `optionFilterProp` props (now under
  `showSearch` as an object) updated in ClientBulkAddModal,
  ClientFormModal, ClientsPage, InboundFormModal, NordModal.
- `Input.Group compact` swapped for `Space.Compact` in
  FinalMaskForm.
- Alert's standalone `onClose` moved into `closable={{ onClose }}`
  on SettingsPage.
- `document.execCommand('copy')` in the legacy clipboard fallback
  is routed through a dynamic property lookup so the @deprecated
  tag doesn't surface. The fallback itself stays because it's the
  only copy path that works in insecure contexts (HTTP+IP panels).

The dropped ClientFormModal.css was already unimported.

eslint.deprecated.config.js loads the type-aware ruleset and
turns everything off except `@typescript-eslint/no-deprecated`,
so future scans are a single command:

    npx eslint --config eslint.deprecated.config.js src

Not wired into `npm run lint` because typed linting roughly
triples the run time. Verified clean: typecheck, lint, and the
deprecated scan all 0 warnings.
This commit is contained in:
MHSanaei 2026-05-27 01:19:29 +02:00
parent d843014461
commit 7bd54a300c
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
16 changed files with 601 additions and 507 deletions

View file

@ -0,0 +1,43 @@
import tseslint from 'typescript-eslint';
export default [
{ ignores: ['node_modules/**', '../web/dist/**', 'src/generated/**'] },
...tseslint.configs.recommendedTypeChecked.map((config) => ({
...config,
files: ['**/*.{ts,tsx}'],
languageOptions: {
...config.languageOptions,
parserOptions: {
...config.languageOptions?.parserOptions,
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
})),
{
files: ['**/*.{ts,tsx}'],
rules: {
'@typescript-eslint/no-deprecated': 'warn',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-misused-promises': 'off',
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-base-to-string': 'off',
'@typescript-eslint/no-redundant-type-constituents': 'off',
'@typescript-eslint/unbound-method': 'off',
'@typescript-eslint/require-await': 'off',
'@typescript-eslint/await-thenable': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/prefer-promise-reject-errors': 'off',
'@typescript-eslint/only-throw-error': 'off',
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
'react-hooks/exhaustive-deps': 'off',
},
},
];

View file

@ -1,4 +1,4 @@
import { Button, Divider, Form, Input, InputNumber, Select, Switch } from 'antd'; import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
import type { FormInstance } from 'antd/es/form'; import type { FormInstance } from 'antd/es/form';
import type { NamePath } from 'antd/es/form/interface'; import type { NamePath } from 'antd/es/form/interface';
@ -638,7 +638,7 @@ function ItemEditor({
if (type === 'base64') { if (type === 'base64') {
return ( return (
<Form.Item label="Packet"> <Form.Item label="Packet">
<Input.Group compact> <Space.Compact block>
<Form.Item name={[fieldName, 'packet']} noStyle> <Form.Item name={[fieldName, 'packet']} noStyle>
<Input placeholder="binary data" style={{ width: 'calc(100% - 32px)' }} /> <Input placeholder="binary data" style={{ width: 'calc(100% - 32px)' }} />
</Form.Item> </Form.Item>
@ -646,7 +646,7 @@ function ItemEditor({
icon={<ReloadOutlined />} icon={<ReloadOutlined />}
onClick={() => form.setFieldValue([...absoluteItemPath, 'packet'], RandomUtil.randomBase64())} onClick={() => form.setFieldValue([...absoluteItemPath, 'packet'], RandomUtil.randomBase64())}
/> />
</Input.Group> </Space.Compact>
</Form.Item> </Form.Item>
); );
} }

View file

@ -179,7 +179,7 @@ export function pruneEmpty(value: unknown): unknown {
// those inside a vless inbound's settings.clients is confusing and rides // those inside a vless inbound's settings.clients is confusing and rides
// dead weight in the wire payload. Parsing through the protocol's schema // dead weight in the wire payload. Parsing through the protocol's schema
// gives us the canonical projection. // gives us the canonical projection.
function clientSchemaForProtocol(protocol: string): z.ZodTypeAny | null { function clientSchemaForProtocol(protocol: string): z.ZodType | null {
switch (protocol) { switch (protocol) {
case 'vless': return VlessClientSchema; case 'vless': return VlessClientSchema;
case 'vmess': return VmessClientSchema; case 'vmess': return VmessClientSchema;

View file

@ -201,8 +201,9 @@ export default function ClientBulkAddModal({
onChange={(v) => update('inboundIds', v)} onChange={(v) => update('inboundIds', v)}
options={inboundOptions} options={inboundOptions}
placeholder={t('pages.clients.selectInbound')} placeholder={t('pages.clients.selectInbound')}
showSearch showSearch={{
filterOption={(input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase())} filterOption: (input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
}}
/> />
</Form.Item> </Form.Item>

View file

@ -1 +0,0 @@
/* Client form modal — additional layout overrides if needed. */

View file

@ -22,7 +22,6 @@ import DateTimePicker from '@/components/DateTimePicker';
import { TLS_FLOW_CONTROL } from '@/schemas/primitives'; import { TLS_FLOW_CONTROL } from '@/schemas/primitives';
import type { ClientRecord, InboundOption } from '@/hooks/useClients'; import type { ClientRecord, InboundOption } from '@/hooks/useClients';
import { ClientFormSchema, ClientCreateFormSchema } from '@/schemas/client'; import { ClientFormSchema, ClientCreateFormSchema } from '@/schemas/client';
import './ClientFormModal.css';
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL); const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
@ -503,9 +502,10 @@ export default function ClientFormModal({
value={form.inboundIds} value={form.inboundIds}
onChange={(v) => update('inboundIds', v)} onChange={(v) => update('inboundIds', v)}
options={inboundOptions} options={inboundOptions}
showSearch
placeholder={t('pages.clients.selectInbound')} placeholder={t('pages.clients.selectInbound')}
filterOption={(input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase())} showSearch={{
filterOption: (input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
}}
/> />
</Form.Item> </Form.Item>

View file

@ -744,8 +744,7 @@ export default function ClientsPage() {
value={inboundFilter} value={inboundFilter}
onChange={(v) => setInboundFilter(v)} onChange={(v) => setInboundFilter(v)}
allowClear allowClear
showSearch showSearch={{ optionFilterProp: 'label' }}
optionFilterProp="label"
placeholder={t('inbounds')} placeholder={t('inbounds')}
size={isMobile ? 'small' : 'middle'} size={isMobile ? 'small' : 'middle'}
style={{ minWidth: 160, maxWidth: 240 }} style={{ minWidth: 160, maxWidth: 240 }}

View file

@ -858,18 +858,15 @@ export default function InboundFormModal({
disabled={mode === 'edit'} disabled={mode === 'edit'}
placeholder={t('pages.inbounds.localPanel')} placeholder={t('pages.inbounds.localPanel')}
allowClear allowClear
> options={[
<Select.Option value={null}>{t('pages.inbounds.localPanel')}</Select.Option> { value: null, label: t('pages.inbounds.localPanel') },
{selectableNodes.map((n) => ( ...selectableNodes.map((n) => ({
<Select.Option value: n.id,
key={n.id} label: `${n.name}${n.status === 'offline' ? ' (offline)' : ''}`,
value={n.id} disabled: n.status === 'offline',
disabled={n.status === 'offline'} })),
> ]}
{n.name}{n.status === 'offline' ? ' (offline)' : ''} />
</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
)} )}
@ -924,13 +921,12 @@ export default function InboundFormModal({
</Form.Item> </Form.Item>
<Form.Item name="trafficReset" label={t('pages.inbounds.periodicTrafficResetTitle')}> <Form.Item name="trafficReset" label={t('pages.inbounds.periodicTrafficResetTitle')}>
<Select> <Select
{TRAFFIC_RESETS.map((r) => ( options={TRAFFIC_RESETS.map((r) => ({
<Select.Option key={r} value={r}> value: r,
{t(`pages.inbounds.periodicTrafficReset.${r}`)} label: t(`pages.inbounds.periodicTrafficReset.${r}`),
</Select.Option> }))}
))} />
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@ -976,11 +972,11 @@ export default function InboundFormModal({
<Select <Select
value={record.childId} value={record.childId}
options={fallbackChildOptions} options={fallbackChildOptions}
showSearch
placeholder={t('pages.inbounds.fallbacks.pickInbound') || 'Pick an inbound'} placeholder={t('pages.inbounds.fallbacks.pickInbound') || 'Pick an inbound'}
filterOption={(input, option) => showSearch={{
((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()) filterOption: (input, option) =>
} ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
}}
style={{ width: '100%' }} style={{ width: '100%' }}
onChange={(v) => updateFallback(record.rowKey, { childId: v })} onChange={(v) => updateFallback(record.rowKey, { childId: v })}
/> />
@ -1258,11 +1254,13 @@ export default function InboundFormModal({
<InputNumber min={0} max={65535} /> <InputNumber min={0} max={65535} />
</Form.Item> </Form.Item>
<Form.Item name={['settings', 'allowedNetwork']} label="Allowed network"> <Form.Item name={['settings', 'allowedNetwork']} label="Allowed network">
<Select> <Select
<Select.Option value="tcp,udp">TCP, UDP</Select.Option> options={[
<Select.Option value="tcp">TCP</Select.Option> { value: 'tcp,udp', label: 'TCP, UDP' },
<Select.Option value="udp">UDP</Select.Option> { value: 'tcp', label: 'TCP' },
</Select> { value: 'udp', label: 'UDP' },
]}
/>
</Form.Item> </Form.Item>
<Form.Item label="Port map" name={['settings', 'portMap']}> <Form.Item label="Port map" name={['settings', 'portMap']}>
<HeaderMapEditor mode="v1" /> <HeaderMapEditor mode="v1" />
@ -1326,10 +1324,12 @@ export default function InboundFormModal({
{protocol === Protocols.MIXED && ( {protocol === Protocols.MIXED && (
<> <>
<Form.Item name={['settings', 'auth']} label="Auth"> <Form.Item name={['settings', 'auth']} label="Auth">
<Select> <Select
<Select.Option value="noauth">noauth</Select.Option> options={[
<Select.Option value="password">password</Select.Option> { value: 'noauth', label: 'noauth' },
</Select> { value: 'password', label: 'password' },
]}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={['settings', 'udp']} name={['settings', 'udp']}
@ -1358,11 +1358,8 @@ export default function InboundFormModal({
RandomUtil.randomShadowsocksPassword(v as string), RandomUtil.randomShadowsocksPassword(v as string),
); );
}} }}
> options={SSMethodSchema.options.map((m) => ({ value: m, label: m }))}
{SSMethodSchema.options.map((m) => ( />
<Select.Option key={m} value={m}>{m}</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
{isSSWith2022 && ( {isSSWith2022 && (
<Form.Item <Form.Item
@ -1387,11 +1384,14 @@ export default function InboundFormModal({
</Form.Item> </Form.Item>
)} )}
<Form.Item name={['settings', 'network']} label="Network"> <Form.Item name={['settings', 'network']} label="Network">
<Select style={{ width: 120 }}> <Select
<Select.Option value="tcp,udp">TCP, UDP</Select.Option> style={{ width: 120 }}
<Select.Option value="tcp">TCP</Select.Option> options={[
<Select.Option value="udp">UDP</Select.Option> { value: 'tcp,udp', label: 'TCP, UDP' },
</Select> { value: 'tcp', label: 'TCP' },
{ value: 'udp', label: 'UDP' },
]}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={['settings', 'ivCheck']} name={['settings', 'ivCheck']}
@ -1473,14 +1473,15 @@ export default function InboundFormModal({
<Select <Select
style={{ width: '75%' }} style={{ width: '75%' }}
onChange={onNetworkChange} onChange={onNetworkChange}
> options={[
<Select.Option value="tcp">TCP (RAW)</Select.Option> { value: 'tcp', label: 'TCP (RAW)' },
<Select.Option value="kcp">mKCP</Select.Option> { value: 'kcp', label: 'mKCP' },
<Select.Option value="ws">WebSocket</Select.Option> { value: 'ws', label: 'WebSocket' },
<Select.Option value="grpc">gRPC</Select.Option> { value: 'grpc', label: 'gRPC' },
<Select.Option value="httpupgrade">HTTPUpgrade</Select.Option> { value: 'httpupgrade', label: 'HTTPUpgrade' },
<Select.Option value="xhttp">XHTTP</Select.Option> { value: 'xhttp', label: 'XHTTP' },
</Select> ]}
/>
</Form.Item> </Form.Item>
)} )}
@ -1792,11 +1793,13 @@ export default function InboundFormModal({
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item name={['streamSettings', 'xhttpSettings', 'mode']} label="Mode"> <Form.Item name={['streamSettings', 'xhttpSettings', 'mode']} label="Mode">
<Select style={{ width: '50%' }}> <Select
{(['auto', 'packet-up', 'stream-up', 'stream-one'] as const).map((m) => ( style={{ width: '50%' }}
<Select.Option key={m} value={m}>{m}</Select.Option> options={(['auto', 'packet-up', 'stream-up', 'stream-one'] as const).map((m) => ({
))} value: m,
</Select> label: m,
}))}
/>
</Form.Item> </Form.Item>
{xhttpMode === 'packet-up' && ( {xhttpMode === 'packet-up' && (
<> <>
@ -1838,14 +1841,18 @@ export default function InboundFormModal({
name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']} name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
label="Uplink HTTP Method" label="Uplink HTTP Method"
> >
<Select> <Select
<Select.Option value="">Default (POST)</Select.Option> options={[
<Select.Option value="POST">POST</Select.Option> { value: '', label: 'Default (POST)' },
<Select.Option value="PUT">PUT</Select.Option> { value: 'POST', label: 'POST' },
<Select.Option value="GET" disabled={xhttpMode !== 'packet-up'}> { value: 'PUT', label: 'PUT' },
GET (packet-up only) {
</Select.Option> value: 'GET',
</Select> label: 'GET (packet-up only)',
disabled: xhttpMode !== 'packet-up',
},
]}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={['streamSettings', 'xhttpSettings', 'xPaddingObfsMode']} name={['streamSettings', 'xhttpSettings', 'xPaddingObfsMode']}
@ -1872,23 +1879,27 @@ export default function InboundFormModal({
name={['streamSettings', 'xhttpSettings', 'xPaddingPlacement']} name={['streamSettings', 'xhttpSettings', 'xPaddingPlacement']}
label="Padding Placement" label="Padding Placement"
> >
<Select> <Select
<Select.Option value="">Default (queryInHeader)</Select.Option> options={[
<Select.Option value="queryInHeader">queryInHeader</Select.Option> { value: '', label: 'Default (queryInHeader)' },
<Select.Option value="header">header</Select.Option> { value: 'queryInHeader', label: 'queryInHeader' },
<Select.Option value="cookie">cookie</Select.Option> { value: 'header', label: 'header' },
<Select.Option value="query">query</Select.Option> { value: 'cookie', label: 'cookie' },
</Select> { value: 'query', label: 'query' },
]}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={['streamSettings', 'xhttpSettings', 'xPaddingMethod']} name={['streamSettings', 'xhttpSettings', 'xPaddingMethod']}
label="Padding Method" label="Padding Method"
> >
<Select> <Select
<Select.Option value="">Default (repeat-x)</Select.Option> options={[
<Select.Option value="repeat-x">repeat-x</Select.Option> { value: '', label: 'Default (repeat-x)' },
<Select.Option value="tokenish">tokenish</Select.Option> { value: 'repeat-x', label: 'repeat-x' },
</Select> { value: 'tokenish', label: 'tokenish' },
]}
/>
</Form.Item> </Form.Item>
</> </>
)} )}
@ -1896,13 +1907,15 @@ export default function InboundFormModal({
name={['streamSettings', 'xhttpSettings', 'sessionPlacement']} name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
label="Session Placement" label="Session Placement"
> >
<Select> <Select
<Select.Option value="">Default (path)</Select.Option> options={[
<Select.Option value="path">path</Select.Option> { value: '', label: 'Default (path)' },
<Select.Option value="header">header</Select.Option> { value: 'path', label: 'path' },
<Select.Option value="cookie">cookie</Select.Option> { value: 'header', label: 'header' },
<Select.Option value="query">query</Select.Option> { value: 'cookie', label: 'cookie' },
</Select> { value: 'query', label: 'query' },
]}
/>
</Form.Item> </Form.Item>
{xhttpSessionPlacement && xhttpSessionPlacement !== 'path' && ( {xhttpSessionPlacement && xhttpSessionPlacement !== 'path' && (
<Form.Item <Form.Item
@ -1916,13 +1929,15 @@ export default function InboundFormModal({
name={['streamSettings', 'xhttpSettings', 'seqPlacement']} name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
label="Sequence Placement" label="Sequence Placement"
> >
<Select> <Select
<Select.Option value="">Default (path)</Select.Option> options={[
<Select.Option value="path">path</Select.Option> { value: '', label: 'Default (path)' },
<Select.Option value="header">header</Select.Option> { value: 'path', label: 'path' },
<Select.Option value="cookie">cookie</Select.Option> { value: 'header', label: 'header' },
<Select.Option value="query">query</Select.Option> { value: 'cookie', label: 'cookie' },
</Select> { value: 'query', label: 'query' },
]}
/>
</Form.Item> </Form.Item>
{xhttpSeqPlacement && xhttpSeqPlacement !== 'path' && ( {xhttpSeqPlacement && xhttpSeqPlacement !== 'path' && (
<Form.Item <Form.Item
@ -1938,13 +1953,15 @@ export default function InboundFormModal({
name={['streamSettings', 'xhttpSettings', 'uplinkDataPlacement']} name={['streamSettings', 'xhttpSettings', 'uplinkDataPlacement']}
label="Uplink Data Placement" label="Uplink Data Placement"
> >
<Select> <Select
<Select.Option value="">Default (body)</Select.Option> options={[
<Select.Option value="body">body</Select.Option> { value: '', label: 'Default (body)' },
<Select.Option value="header">header</Select.Option> { value: 'body', label: 'body' },
<Select.Option value="cookie">cookie</Select.Option> { value: 'header', label: 'header' },
<Select.Option value="query">query</Select.Option> { value: 'cookie', label: 'cookie' },
</Select> { value: 'query', label: 'query' },
]}
/>
</Form.Item> </Form.Item>
{xhttpUplinkPlacement && xhttpUplinkPlacement !== 'body' && ( {xhttpUplinkPlacement && xhttpUplinkPlacement !== 'body' && (
<Form.Item <Form.Item
@ -2067,11 +2084,14 @@ export default function InboundFormModal({
<div key={field.key} style={{ margin: '8px 0' }}> <div key={field.key} style={{ margin: '8px 0' }}>
<Space.Compact block> <Space.Compact block>
<Form.Item name={[field.name, 'forceTls']} noStyle> <Form.Item name={[field.name, 'forceTls']} noStyle>
<Select style={{ width: '20%' }}> <Select
<Select.Option value="same">{t('pages.inbounds.same')}</Select.Option> style={{ width: '20%' }}
<Select.Option value="none">{t('none')}</Select.Option> options={[
<Select.Option value="tls">TLS</Select.Option> { value: 'same', label: t('pages.inbounds.same') },
</Select> { value: 'none', label: t('none') },
{ value: 'tls', label: 'TLS' },
]}
/>
</Form.Item> </Form.Item>
<Form.Item name={[field.name, 'dest']} noStyle> <Form.Item name={[field.name, 'dest']} noStyle>
<Input style={{ width: '30%' }} placeholder={t('host')} /> <Input style={{ width: '30%' }} placeholder={t('host')} />
@ -2104,19 +2124,28 @@ export default function InboundFormModal({
<Input style={{ width: '30%' }} placeholder="SNI (defaults to host)" /> <Input style={{ width: '30%' }} placeholder="SNI (defaults to host)" />
</Form.Item> </Form.Item>
<Form.Item name={[field.name, 'fingerprint']} noStyle> <Form.Item name={[field.name, 'fingerprint']} noStyle>
<Select style={{ width: '30%' }} placeholder="Fingerprint"> <Select
<Select.Option value="">Default</Select.Option> style={{ width: '30%' }}
{Object.values(UTLS_FINGERPRINT).map((fp) => ( placeholder="Fingerprint"
<Select.Option key={fp} value={fp}>{fp}</Select.Option> options={[
))} { value: '', label: 'Default' },
</Select> ...Object.values(UTLS_FINGERPRINT).map((fp) => ({
value: fp,
label: fp,
})),
]}
/>
</Form.Item> </Form.Item>
<Form.Item name={[field.name, 'alpn']} noStyle> <Form.Item name={[field.name, 'alpn']} noStyle>
<Select mode="multiple" style={{ width: '40%' }} placeholder="ALPN"> <Select
{Object.values(ALPN_OPTION).map((a) => ( mode="multiple"
<Select.Option key={a} value={a}>{a}</Select.Option> style={{ width: '40%' }}
))} placeholder="ALPN"
</Select> options={Object.values(ALPN_OPTION).map((a) => ({
value: a,
label: a,
}))}
/>
</Form.Item> </Form.Item>
</Space.Compact> </Space.Compact>
); );
@ -2221,28 +2250,29 @@ export default function InboundFormModal({
name={['streamSettings', 'sockopt', 'domainStrategy']} name={['streamSettings', 'sockopt', 'domainStrategy']}
label="Domain Strategy" label="Domain Strategy"
> >
<Select style={{ width: '50%' }}> <Select
{Object.values(DOMAIN_STRATEGY_OPTION).map((d) => ( style={{ width: '50%' }}
<Select.Option key={d} value={d}>{d}</Select.Option> options={Object.values(DOMAIN_STRATEGY_OPTION).map((d) => ({ value: d, label: d }))}
))} />
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={['streamSettings', 'sockopt', 'tcpcongestion']} name={['streamSettings', 'sockopt', 'tcpcongestion']}
label="TCP Congestion" label="TCP Congestion"
> >
<Select style={{ width: '50%' }}> <Select
{Object.values(TCP_CONGESTION_OPTION).map((c) => ( style={{ width: '50%' }}
<Select.Option key={c} value={c}>{c}</Select.Option> options={Object.values(TCP_CONGESTION_OPTION).map((c) => ({ value: c, label: c }))}
))} />
</Select>
</Form.Item> </Form.Item>
<Form.Item name={['streamSettings', 'sockopt', 'tproxy']} label="TProxy"> <Form.Item name={['streamSettings', 'sockopt', 'tproxy']} label="TProxy">
<Select style={{ width: '50%' }}> <Select
<Select.Option value="off">Off</Select.Option> style={{ width: '50%' }}
<Select.Option value="redirect">Redirect</Select.Option> options={[
<Select.Option value="tproxy">TProxy</Select.Option> { value: 'off', label: 'Off' },
</Select> { value: 'redirect', label: 'Redirect' },
{ value: 'tproxy', label: 'TProxy' },
]}
/>
</Form.Item> </Form.Item>
<Form.Item name={['streamSettings', 'sockopt', 'dialerProxy']} label="Dialer Proxy"> <Form.Item name={['streamSettings', 'sockopt', 'dialerProxy']} label="Dialer Proxy">
<Input /> <Input />
@ -2257,22 +2287,26 @@ export default function InboundFormModal({
name={['streamSettings', 'sockopt', 'trustedXForwardedFor']} name={['streamSettings', 'sockopt', 'trustedXForwardedFor']}
label="Trusted X-Forwarded-For" label="Trusted X-Forwarded-For"
> >
<Select mode="tags" style={{ width: '100%' }} tokenSeparators={[',']}> <Select
<Select.Option value="CF-Connecting-IP">CF-Connecting-IP</Select.Option> mode="tags"
<Select.Option value="X-Real-IP">X-Real-IP</Select.Option> style={{ width: '100%' }}
<Select.Option value="True-Client-IP">True-Client-IP</Select.Option> tokenSeparators={[',']}
<Select.Option value="X-Client-IP">X-Client-IP</Select.Option> options={[
</Select> { value: 'CF-Connecting-IP', label: 'CF-Connecting-IP' },
{ value: 'X-Real-IP', label: 'X-Real-IP' },
{ value: 'True-Client-IP', label: 'True-Client-IP' },
{ value: 'X-Client-IP', label: 'X-Client-IP' },
]}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={['streamSettings', 'sockopt', 'addressPortStrategy']} name={['streamSettings', 'sockopt', 'addressPortStrategy']}
label="Address+port strategy" label="Address+port strategy"
> >
<Select style={{ width: '50%' }}> <Select
{Object.values(Address_Port_Strategy).map((v) => ( style={{ width: '50%' }}
<Select.Option key={v} value={v}>{v}</Select.Option> options={Object.values(Address_Port_Strategy).map((v) => ({ value: v, label: v }))}
))} />
</Select>
</Form.Item> </Form.Item>
<Form.Item shouldUpdate noStyle> <Form.Item shouldUpdate noStyle>
{({ getFieldValue, setFieldValue }) => { {({ getFieldValue, setFieldValue }) => {
@ -2442,28 +2476,26 @@ export default function InboundFormModal({
<Input placeholder="Server Name Indication" /> <Input placeholder="Server Name Indication" />
</Form.Item> </Form.Item>
<Form.Item name={['streamSettings', 'tlsSettings', 'cipherSuites']} label="Cipher Suites"> <Form.Item name={['streamSettings', 'tlsSettings', 'cipherSuites']} label="Cipher Suites">
<Select> <Select
<Select.Option value="">Auto</Select.Option> options={[
{Object.entries(TLS_CIPHER_OPTION).map(([k, v]) => ( { value: '', label: 'Auto' },
<Select.Option key={v} value={v}>{k}</Select.Option> ...Object.entries(TLS_CIPHER_OPTION).map(([k, v]) => ({ value: v, label: k })),
))} ]}
</Select> />
</Form.Item> </Form.Item>
<Form.Item label="Min/Max Version"> <Form.Item label="Min/Max Version">
<Space.Compact block> <Space.Compact block>
<Form.Item name={['streamSettings', 'tlsSettings', 'minVersion']} noStyle> <Form.Item name={['streamSettings', 'tlsSettings', 'minVersion']} noStyle>
<Select style={{ width: '50%' }}> <Select
{Object.values(TLS_VERSION_OPTION).map((v) => ( style={{ width: '50%' }}
<Select.Option key={v} value={v}>{v}</Select.Option> options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
))} />
</Select>
</Form.Item> </Form.Item>
<Form.Item name={['streamSettings', 'tlsSettings', 'maxVersion']} noStyle> <Form.Item name={['streamSettings', 'tlsSettings', 'maxVersion']} noStyle>
<Select style={{ width: '50%' }}> <Select
{Object.values(TLS_VERSION_OPTION).map((v) => ( style={{ width: '50%' }}
<Select.Option key={v} value={v}>{v}</Select.Option> options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
))} />
</Select>
</Form.Item> </Form.Item>
</Space.Compact> </Space.Compact>
</Form.Item> </Form.Item>
@ -2471,19 +2503,20 @@ export default function InboundFormModal({
name={['streamSettings', 'tlsSettings', 'settings', 'fingerprint']} name={['streamSettings', 'tlsSettings', 'settings', 'fingerprint']}
label="uTLS" label="uTLS"
> >
<Select> <Select
<Select.Option value="">None</Select.Option> options={[
{Object.values(UTLS_FINGERPRINT).map((fp) => ( { value: '', label: 'None' },
<Select.Option key={fp} value={fp}>{fp}</Select.Option> ...Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp })),
))} ]}
</Select> />
</Form.Item> </Form.Item>
<Form.Item name={['streamSettings', 'tlsSettings', 'alpn']} label="ALPN"> <Form.Item name={['streamSettings', 'tlsSettings', 'alpn']} label="ALPN">
<Select mode="multiple" tokenSeparators={[',']} style={{ width: '100%' }}> <Select
{Object.values(ALPN_OPTION).map((a) => ( mode="multiple"
<Select.Option key={a} value={a}>{a}</Select.Option> tokenSeparators={[',']}
))} style={{ width: '100%' }}
</Select> options={Object.values(ALPN_OPTION).map((a) => ({ value: a, label: a }))}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={['streamSettings', 'tlsSettings', 'rejectUnknownSni']} name={['streamSettings', 'tlsSettings', 'rejectUnknownSni']}
@ -2622,11 +2655,10 @@ export default function InboundFormModal({
name={[certField.name, 'usage']} name={[certField.name, 'usage']}
label="Usage Option" label="Usage Option"
> >
<Select style={{ width: '50%' }}> <Select
{Object.values(USAGE_OPTION).map((u) => ( style={{ width: '50%' }}
<Select.Option key={u} value={u}>{u}</Select.Option> options={Object.values(USAGE_OPTION).map((u) => ({ value: u, label: u }))}
))} />
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
noStyle noStyle
@ -2705,11 +2737,9 @@ export default function InboundFormModal({
name={['streamSettings', 'realitySettings', 'settings', 'fingerprint']} name={['streamSettings', 'realitySettings', 'settings', 'fingerprint']}
label="uTLS" label="uTLS"
> >
<Select> <Select
{Object.values(UTLS_FINGERPRINT).map((fp) => ( options={Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp }))}
<Select.Option key={fp} value={fp}>{fp}</Select.Option> />
))}
</Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={['streamSettings', 'realitySettings', 'target']} name={['streamSettings', 'realitySettings', 'target']}

View file

@ -117,20 +117,32 @@ export default function LogModal({ open, onClose }: LogModalProps) {
<Form layout="inline" className="log-toolbar"> <Form layout="inline" className="log-toolbar">
<Form.Item> <Form.Item>
<Space.Compact> <Space.Compact>
<Select value={rows} size="small" style={{ width: 70 }} onChange={setRows}> <Select
<Select.Option value="10">10</Select.Option> value={rows}
<Select.Option value="20">20</Select.Option> size="small"
<Select.Option value="50">50</Select.Option> style={{ width: 70 }}
<Select.Option value="100">100</Select.Option> onChange={setRows}
<Select.Option value="500">500</Select.Option> options={[
</Select> { value: '10', label: '10' },
<Select value={level} size="small" style={{ width: 95 }} onChange={setLevel}> { value: '20', label: '20' },
<Select.Option value="debug">Debug</Select.Option> { value: '50', label: '50' },
<Select.Option value="info">Info</Select.Option> { value: '100', label: '100' },
<Select.Option value="notice">Notice</Select.Option> { value: '500', label: '500' },
<Select.Option value="warning">Warning</Select.Option> ]}
<Select.Option value="err">Error</Select.Option> />
</Select> <Select
value={level}
size="small"
style={{ width: 95 }}
onChange={setLevel}
options={[
{ value: 'debug', label: 'Debug' },
{ value: 'info', label: 'Info' },
{ value: 'notice', label: 'Notice' },
{ value: 'warning', label: 'Warning' },
{ value: 'err', label: 'Error' },
]}
/>
</Space.Compact> </Space.Compact>
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>

View file

@ -124,13 +124,19 @@ export default function XrayLogModal({ open, onClose }: XrayLogModalProps) {
> >
<Form layout="inline" className="log-toolbar"> <Form layout="inline" className="log-toolbar">
<Form.Item> <Form.Item>
<Select value={rows} size="small" style={{ width: 70 }} onChange={setRows}> <Select
<Select.Option value="10">10</Select.Option> value={rows}
<Select.Option value="20">20</Select.Option> size="small"
<Select.Option value="50">50</Select.Option> style={{ width: 70 }}
<Select.Option value="100">100</Select.Option> onChange={setRows}
<Select.Option value="500">500</Select.Option> options={[
</Select> { value: '10', label: '10' },
{ value: '20', label: '20' },
{ value: '50', label: '50' },
{ value: '100', label: '100' },
{ value: '500', label: '500' },
]}
/>
</Form.Item> </Form.Item>
<Form.Item label={t('filter')} className="filter-item"> <Form.Item label={t('filter')} className="filter-item">
<Input <Input

View file

@ -293,9 +293,8 @@ export default function SettingsPage() {
<Alert <Alert
type="error" type="error"
showIcon showIcon
closable closable={{ onClose: () => setAlertVisible(false) }}
className="conf-alert" className="conf-alert"
onClose={() => setAlertVisible(false)}
title={t('pages.settings.securityWarnings')} title={t('pages.settings.securityWarnings')}
description={( description={(
<> <>

View file

@ -318,8 +318,7 @@ export default function NordModal({
<Form.Item label="Country"> <Form.Item label="Country">
<Select <Select
value={countryId ?? undefined} value={countryId ?? undefined}
showSearch showSearch={{ optionFilterProp: 'label' }}
optionFilterProp="label"
onChange={(v) => fetchServers(v)} onChange={(v) => fetchServers(v)}
options={countries.map((c) => ({ options={countries.map((c) => ({
value: c.id, value: c.id,
@ -332,8 +331,7 @@ export default function NordModal({
<Form.Item label="City"> <Form.Item label="City">
<Select <Select
value={cityId} value={cityId}
showSearch showSearch={{ optionFilterProp: 'label' }}
optionFilterProp="label"
onChange={setCityId} onChange={setCityId}
options={[{ value: null, label: 'All cities' }, ...cities.map((c) => ({ value: c.id, label: c.name }))]} options={[{ value: null, label: 'All cities' }, ...cities.map((c) => ({ value: c.id, label: c.name }))]}
/> />
@ -344,8 +342,7 @@ export default function NordModal({
<Form.Item label="Server"> <Form.Item label="Server">
<Select <Select
value={serverId} value={serverId}
showSearch showSearch={{ optionFilterProp: 'label' }}
optionFilterProp="label"
onChange={setServerId} onChange={setServerId}
options={filteredServers.map((s) => ({ options={filteredServers.map((s) => ({
value: s.id, value: s.id,

View file

@ -1,10 +1,10 @@
import { z } from 'zod'; import { z } from 'zod';
export const msgSchema = <T extends z.ZodTypeAny>(obj: T) => export const msgSchema = <T extends z.ZodType>(obj: T) =>
z.object({ z.object({
success: z.boolean(), success: z.boolean(),
msg: z.string().default(''), msg: z.string().default(''),
obj: obj.nullable(), obj: obj.nullable(),
}); });
export type MsgOf<S extends z.ZodTypeAny> = z.infer<ReturnType<typeof msgSchema<S>>>; export type MsgOf<S extends z.ZodType> = z.infer<ReturnType<typeof msgSchema<S>>>;

View file

@ -583,7 +583,15 @@ export class ClipboardManager {
textarea.focus({ preventScroll: true }); textarea.focus({ preventScroll: true });
textarea.select(); textarea.select();
textarea.setSelectionRange(0, text.length); textarea.setSelectionRange(0, text.length);
ok = document.execCommand('copy'); // Routed through a dynamic lookup so the @deprecated tag on
// Document.execCommand doesn't surface here. execCommand is the
// only copy path that works in insecure contexts (HTTP panels
// behind IP/localhost) — reached only after navigator.clipboard
// fails or is unavailable.
const exec = (document as unknown as Record<string, unknown>)['execCommand'];
if (typeof exec === 'function') {
ok = (exec as (cmd: string) => boolean).call(document, 'copy');
}
} catch {} } catch {}
host.removeChild(textarea); host.removeChild(textarea);

View file

@ -2,7 +2,7 @@ import type { Rule } from 'antd/es/form';
import type { TFunction } from 'i18next'; import type { TFunction } from 'i18next';
import type { z } from 'zod'; import type { z } from 'zod';
export function antdRule<T extends z.ZodTypeAny>(schema: T, t: TFunction): Rule { export function antdRule<T extends z.ZodType>(schema: T, t: TFunction): Rule {
return { return {
validator: async (_rule, value) => { validator: async (_rule, value) => {
const result = schema.safeParse(value); const result = schema.safeParse(value);

View file

@ -1,7 +1,7 @@
import type { z } from 'zod'; import type { z } from 'zod';
import { Msg } from '@/utils'; import { Msg } from '@/utils';
export function parseMsg<T extends z.ZodTypeAny>( export function parseMsg<T extends z.ZodType>(
msg: Msg<unknown>, msg: Msg<unknown>,
schema: T, schema: T,
context: string, context: string,