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 type { FormInstance } from 'antd/es/form';
import type { NamePath } from 'antd/es/form/interface';
@ -638,7 +638,7 @@ function ItemEditor({
if (type === 'base64') {
return (
<Form.Item label="Packet">
<Input.Group compact>
<Space.Compact block>
<Form.Item name={[fieldName, 'packet']} noStyle>
<Input placeholder="binary data" style={{ width: 'calc(100% - 32px)' }} />
</Form.Item>
@ -646,7 +646,7 @@ function ItemEditor({
icon={<ReloadOutlined />}
onClick={() => form.setFieldValue([...absoluteItemPath, 'packet'], RandomUtil.randomBase64())}
/>
</Input.Group>
</Space.Compact>
</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
// dead weight in the wire payload. Parsing through the protocol's schema
// gives us the canonical projection.
function clientSchemaForProtocol(protocol: string): z.ZodTypeAny | null {
function clientSchemaForProtocol(protocol: string): z.ZodType | null {
switch (protocol) {
case 'vless': return VlessClientSchema;
case 'vmess': return VmessClientSchema;

View file

@ -62,10 +62,10 @@ export default function ClientBulkAddModal({
useEffect(() => {
if (!open) return;
setForm(emptyForm());
setDelayedStart(false);
}, [open]);
function update<K extends keyof FormState>(key: K, value: FormState[K]) {
@ -87,7 +87,7 @@ export default function ClientBulkAddModal({
useEffect(() => {
if (!showFlow && form.flow) {
update('flow', '');
}
}, [showFlow, form.flow]);
@ -186,130 +186,131 @@ export default function ClientBulkAddModal({
open={open}
title={t('pages.clients.bulk')}
okText={t('create')}
cancelText={t('close')}
confirmLoading={saving}
mask={{ closable: false }}
width={640}
onOk={submit}
onCancel={() => onOpenChange(false)}
>
<Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
<Form.Item label={t('pages.clients.attachedInbounds')} required>
<Select
mode="multiple"
value={form.inboundIds}
onChange={(v) => update('inboundIds', v)}
options={inboundOptions}
placeholder={t('pages.clients.selectInbound')}
showSearch
filterOption={(input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase())}
/>
</Form.Item>
<Form.Item label={t('pages.clients.method')}>
<Select
value={form.emailMethod}
onChange={(v) => update('emailMethod', v)}
options={[
{ value: 0, label: 'Random' },
{ value: 1, label: 'Random + Prefix' },
{ value: 2, label: 'Random + Prefix + Num' },
{ value: 3, label: 'Random + Prefix + Num + Postfix' },
{ value: 4, label: 'Prefix + Num + Postfix' },
]}
/>
</Form.Item>
{form.emailMethod > 1 && (
<>
<Form.Item label={t('pages.clients.first')}>
<InputNumber value={form.firstNum} min={1} onChange={(v) => update('firstNum', Number(v) || 1)} />
</Form.Item>
<Form.Item label={t('pages.clients.last')}>
<InputNumber value={form.lastNum} min={form.firstNum} onChange={(v) => update('lastNum', Number(v) || 1)} />
</Form.Item>
</>
)}
{form.emailMethod > 0 && (
<Form.Item label={t('pages.clients.prefix')}>
<Input value={form.emailPrefix} onChange={(e) => update('emailPrefix', e.target.value)} />
</Form.Item>
)}
{form.emailMethod > 2 && (
<Form.Item label={t('pages.clients.postfix')}>
<Input value={form.emailPostfix} onChange={(e) => update('emailPostfix', e.target.value)} />
</Form.Item>
)}
{form.emailMethod < 2 && (
<Form.Item label={t('pages.clients.clientCount')}>
<InputNumber value={form.quantity} min={1} max={100} onChange={(v) => update('quantity', Number(v) || 1)} />
</Form.Item>
)}
<Form.Item label={
<>
{t('subscription.title')}
<SyncOutlined
className="random-icon"
onClick={() => update('subId', RandomUtil.randomLowerAndNum(16))}
/>
</>
}>
<Input value={form.subId} onChange={(e) => update('subId', e.target.value)} />
</Form.Item>
<Form.Item label={t('comment')}>
<Input value={form.comment} onChange={(e) => update('comment', e.target.value)} />
</Form.Item>
{showFlow && (
<Form.Item label={t('pages.clients.flow')}>
cancelText={t('close')}
confirmLoading={saving}
mask={{ closable: false }}
width={640}
onOk={submit}
onCancel={() => onOpenChange(false)}
>
<Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
<Form.Item label={t('pages.clients.attachedInbounds')} required>
<Select
value={form.flow}
onChange={(v) => update('flow', v)}
style={{ width: 220 }}
mode="multiple"
value={form.inboundIds}
onChange={(v) => update('inboundIds', v)}
options={inboundOptions}
placeholder={t('pages.clients.selectInbound')}
showSearch={{
filterOption: (input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
}}
/>
</Form.Item>
<Form.Item label={t('pages.clients.method')}>
<Select
value={form.emailMethod}
onChange={(v) => update('emailMethod', v)}
options={[
{ value: '', label: t('none') },
...FLOW_OPTIONS.map((k) => ({ value: k, label: k })),
{ value: 0, label: 'Random' },
{ value: 1, label: 'Random + Prefix' },
{ value: 2, label: 'Random + Prefix + Num' },
{ value: 3, label: 'Random + Prefix + Num + Postfix' },
{ value: 4, label: 'Prefix + Num + Postfix' },
]}
/>
</Form.Item>
)}
{ipLimitEnable && (
<Form.Item label={t('pages.clients.limitIp')}>
<InputNumber value={form.limitIp} min={0} onChange={(v) => update('limitIp', Number(v) || 0)} />
{form.emailMethod > 1 && (
<>
<Form.Item label={t('pages.clients.first')}>
<InputNumber value={form.firstNum} min={1} onChange={(v) => update('firstNum', Number(v) || 1)} />
</Form.Item>
<Form.Item label={t('pages.clients.last')}>
<InputNumber value={form.lastNum} min={form.firstNum} onChange={(v) => update('lastNum', Number(v) || 1)} />
</Form.Item>
</>
)}
{form.emailMethod > 0 && (
<Form.Item label={t('pages.clients.prefix')}>
<Input value={form.emailPrefix} onChange={(e) => update('emailPrefix', e.target.value)} />
</Form.Item>
)}
{form.emailMethod > 2 && (
<Form.Item label={t('pages.clients.postfix')}>
<Input value={form.emailPostfix} onChange={(e) => update('emailPostfix', e.target.value)} />
</Form.Item>
)}
{form.emailMethod < 2 && (
<Form.Item label={t('pages.clients.clientCount')}>
<InputNumber value={form.quantity} min={1} max={100} onChange={(v) => update('quantity', Number(v) || 1)} />
</Form.Item>
)}
<Form.Item label={
<>
{t('subscription.title')}
<SyncOutlined
className="random-icon"
onClick={() => update('subId', RandomUtil.randomLowerAndNum(16))}
/>
</>
}>
<Input value={form.subId} onChange={(e) => update('subId', e.target.value)} />
</Form.Item>
)}
<Form.Item label={t('pages.clients.totalGB')}>
<InputNumber value={form.totalGB} min={0} step={1} onChange={(v) => update('totalGB', Number(v) || 0)} />
</Form.Item>
<Form.Item label={t('comment')}>
<Input value={form.comment} onChange={(e) => update('comment', e.target.value)} />
</Form.Item>
<Form.Item label={t('pages.clients.delayedStart')}>
<Switch
checked={delayedStart}
onClick={() => { setDelayedStart(!delayedStart); update('expiryTime', 0); }}
/>
</Form.Item>
{showFlow && (
<Form.Item label={t('pages.clients.flow')}>
<Select
value={form.flow}
onChange={(v) => update('flow', v)}
style={{ width: 220 }}
options={[
{ value: '', label: t('none') },
...FLOW_OPTIONS.map((k) => ({ value: k, label: k })),
]}
/>
</Form.Item>
)}
{delayedStart ? (
<Form.Item label={t('pages.clients.expireDays')}>
<InputNumber
value={delayedExpireDays}
min={0}
onChange={(v) => update('expiryTime', -86400000 * (Number(v) || 0))}
{ipLimitEnable && (
<Form.Item label={t('pages.clients.limitIp')}>
<InputNumber value={form.limitIp} min={0} onChange={(v) => update('limitIp', Number(v) || 0)} />
</Form.Item>
)}
<Form.Item label={t('pages.clients.totalGB')}>
<InputNumber value={form.totalGB} min={0} step={1} onChange={(v) => update('totalGB', Number(v) || 0)} />
</Form.Item>
<Form.Item label={t('pages.clients.delayedStart')}>
<Switch
checked={delayedStart}
onClick={() => { setDelayedStart(!delayedStart); update('expiryTime', 0); }}
/>
</Form.Item>
) : (
<Form.Item label={t('pages.inbounds.expireDate')}>
<DateTimePicker
value={expiryDate}
onChange={(next) => update('expiryTime', next ? next.valueOf() : 0)}
/>
</Form.Item>
)}
</Form>
{delayedStart ? (
<Form.Item label={t('pages.clients.expireDays')}>
<InputNumber
value={delayedExpireDays}
min={0}
onChange={(v) => update('expiryTime', -86400000 * (Number(v) || 0))}
/>
</Form.Item>
) : (
<Form.Item label={t('pages.inbounds.expireDate')}>
<DateTimePicker
value={expiryDate}
onChange={(next) => update('expiryTime', next ? next.valueOf() : 0)}
/>
</Form.Item>
)}
</Form>
</Modal>
</>
);

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 type { ClientRecord, InboundOption } from '@/hooks/useClients';
import { ClientFormSchema, ClientCreateFormSchema } from '@/schemas/client';
import './ClientFormModal.css';
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
@ -145,7 +144,7 @@ export default function ClientFormModal({
useEffect(() => {
if (!open) return;
if (isEdit && client) {
const et = Number(client.expiryTime) || 0;
const next: FormState = {
@ -185,7 +184,7 @@ export default function ClientFormModal({
auth: RandomUtil.randomLowerAndNum(16),
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, isEdit]);
@ -217,14 +216,14 @@ export default function ClientFormModal({
useEffect(() => {
if (!showFlow && form.flow) {
update('flow', '');
}
}, [showFlow, form.flow]);
useEffect(() => {
if (!showReverseTag && form.reverseTag) {
update('reverseTag', '');
}
}, [showReverseTag, form.reverseTag]);
@ -347,193 +346,194 @@ export default function ClientFormModal({
open={open}
title={isEdit ? t('pages.clients.editTitle') : t('pages.clients.addTitle')}
destroyOnHidden
okText={isEdit ? t('save') : t('create')}
cancelText={t('cancel')}
okButtonProps={{ loading: submitting }}
width={720}
onOk={onSubmit}
onCancel={close}
>
<Form layout="vertical">
<Row gutter={16}>
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.email')} required>
<Space.Compact style={{ display: 'flex' }}>
<Input
value={form.email}
placeholder={t('pages.clients.email')}
style={{ flex: 1 }}
onChange={(e) => update('email', e.target.value)}
/>
<Button onClick={() => update('email', RandomUtil.randomLowerAndNum(12))}></Button>
</Space.Compact>
</Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.subId')}>
<Space.Compact style={{ display: 'flex' }}>
<Input value={form.subId} style={{ flex: 1 }} onChange={(e) => update('subId', e.target.value)} />
<Button onClick={() => update('subId', RandomUtil.randomLowerAndNum(16))}></Button>
</Space.Compact>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.hysteriaAuth')}>
<Space.Compact style={{ display: 'flex' }}>
<Input value={form.auth} style={{ flex: 1 }} onChange={(e) => update('auth', e.target.value)} />
<Button onClick={() => update('auth', RandomUtil.randomLowerAndNum(16))}></Button>
</Space.Compact>
</Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.password')}>
<Space.Compact style={{ display: 'flex' }}>
<Input value={form.password} style={{ flex: 1 }} onChange={(e) => update('password', e.target.value)} />
<Button onClick={() => update('password', RandomUtil.randomLowerAndNum(16))}></Button>
</Space.Compact>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.uuid')}>
<Space.Compact style={{ display: 'flex' }}>
<Input value={form.uuid} style={{ flex: 1 }} onChange={(e) => update('uuid', e.target.value)} />
<Button onClick={() => update('uuid', RandomUtil.randomUUID())}></Button>
</Space.Compact>
</Form.Item>
</Col>
<Col xs={24} md={ipLimitEnable ? 8 : 12}>
<Form.Item label={t('pages.clients.totalGB')}>
<InputNumber value={form.totalGB} min={0} step={1} style={{ width: '100%' }}
onChange={(v) => update('totalGB', Number(v) || 0)} />
</Form.Item>
</Col>
{ipLimitEnable && (
<Col xs={24} md={4}>
<Form.Item label={t('pages.clients.limitIp')}>
<InputNumber value={form.limitIp} min={0} style={{ width: '100%' }}
onChange={(v) => update('limitIp', Number(v) || 0)} />
okText={isEdit ? t('save') : t('create')}
cancelText={t('cancel')}
okButtonProps={{ loading: submitting }}
width={720}
onOk={onSubmit}
onCancel={close}
>
<Form layout="vertical">
<Row gutter={16}>
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.email')} required>
<Space.Compact style={{ display: 'flex' }}>
<Input
value={form.email}
placeholder={t('pages.clients.email')}
style={{ flex: 1 }}
onChange={(e) => update('email', e.target.value)}
/>
<Button onClick={() => update('email', RandomUtil.randomLowerAndNum(12))}></Button>
</Space.Compact>
</Form.Item>
</Col>
)}
</Row>
<Row gutter={16}>
<Col xs={24} md={12}>
{form.delayedStart ? (
<Form.Item label={t('pages.clients.expireDays')}>
<InputNumber value={form.delayedDays} min={0} style={{ width: '100%' }}
onChange={(v) => update('delayedDays', Number(v) || 0)} />
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.subId')}>
<Space.Compact style={{ display: 'flex' }}>
<Input value={form.subId} style={{ flex: 1 }} onChange={(e) => update('subId', e.target.value)} />
<Button onClick={() => update('subId', RandomUtil.randomLowerAndNum(16))}></Button>
</Space.Compact>
</Form.Item>
) : (
<Form.Item label={t('pages.clients.expiryTime')}>
<DateTimePicker
value={form.expiryDate}
onChange={(d) => update('expiryDate', d || null)}
/>
</Form.Item>
)}
</Col>
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.delayedStart')}>
<Switch
checked={form.delayedStart}
onChange={(v) => {
update('delayedStart', v);
if (v) update('expiryDate', null);
else update('delayedDays', 0);
}}
/>
</Form.Item>
</Col>
</Row>
</Col>
</Row>
{(showFlow || showReverseTag) && (
<Row gutter={16}>
{showFlow && (
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.flow')}>
<Select
value={form.flow}
onChange={(v) => update('flow', v)}
options={[
{ value: '', label: t('none') },
...FLOW_OPTIONS.map((k) => ({ value: k, label: k })),
]}
/>
</Form.Item>
</Col>
)}
{showReverseTag && (
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.reverseTag')}>
<Input value={form.reverseTag} placeholder={t('pages.clients.reverseTagPlaceholder')}
onChange={(e) => update('reverseTag', e.target.value)} />
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.hysteriaAuth')}>
<Space.Compact style={{ display: 'flex' }}>
<Input value={form.auth} style={{ flex: 1 }} onChange={(e) => update('auth', e.target.value)} />
<Button onClick={() => update('auth', RandomUtil.randomLowerAndNum(16))}></Button>
</Space.Compact>
</Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.password')}>
<Space.Compact style={{ display: 'flex' }}>
<Input value={form.password} style={{ flex: 1 }} onChange={(e) => update('password', e.target.value)} />
<Button onClick={() => update('password', RandomUtil.randomLowerAndNum(16))}></Button>
</Space.Compact>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.uuid')}>
<Space.Compact style={{ display: 'flex' }}>
<Input value={form.uuid} style={{ flex: 1 }} onChange={(e) => update('uuid', e.target.value)} />
<Button onClick={() => update('uuid', RandomUtil.randomUUID())}></Button>
</Space.Compact>
</Form.Item>
</Col>
<Col xs={24} md={ipLimitEnable ? 8 : 12}>
<Form.Item label={t('pages.clients.totalGB')}>
<InputNumber value={form.totalGB} min={0} step={1} style={{ width: '100%' }}
onChange={(v) => update('totalGB', Number(v) || 0)} />
</Form.Item>
</Col>
{ipLimitEnable && (
<Col xs={24} md={4}>
<Form.Item label={t('pages.clients.limitIp')}>
<InputNumber value={form.limitIp} min={0} style={{ width: '100%' }}
onChange={(v) => update('limitIp', Number(v) || 0)} />
</Form.Item>
</Col>
)}
</Row>
)}
<Row gutter={16}>
{tgBotEnable && (
<Row gutter={16}>
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.telegramId')}>
<InputNumber value={form.tgId} min={0} controls={false}
placeholder={t('pages.clients.telegramIdPlaceholder')} style={{ width: '100%' }}
onChange={(v) => update('tgId', Number(v) || 0)} />
{form.delayedStart ? (
<Form.Item label={t('pages.clients.expireDays')}>
<InputNumber value={form.delayedDays} min={0} style={{ width: '100%' }}
onChange={(v) => update('delayedDays', Number(v) || 0)} />
</Form.Item>
) : (
<Form.Item label={t('pages.clients.expiryTime')}>
<DateTimePicker
value={form.expiryDate}
onChange={(d) => update('expiryDate', d || null)}
/>
</Form.Item>
)}
</Col>
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.delayedStart')}>
<Switch
checked={form.delayedStart}
onChange={(v) => {
update('delayedStart', v);
if (v) update('expiryDate', null);
else update('delayedDays', 0);
}}
/>
</Form.Item>
</Col>
</Row>
{(showFlow || showReverseTag) && (
<Row gutter={16}>
{showFlow && (
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.flow')}>
<Select
value={form.flow}
onChange={(v) => update('flow', v)}
options={[
{ value: '', label: t('none') },
...FLOW_OPTIONS.map((k) => ({ value: k, label: k })),
]}
/>
</Form.Item>
</Col>
)}
{showReverseTag && (
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.reverseTag')}>
<Input value={form.reverseTag} placeholder={t('pages.clients.reverseTagPlaceholder')}
onChange={(e) => update('reverseTag', e.target.value)} />
</Form.Item>
</Col>
)}
</Row>
)}
<Col xs={24} md={tgBotEnable ? 12 : 24}>
<Form.Item label={t('pages.clients.comment')}>
<Input value={form.comment} onChange={(e) => update('comment', e.target.value)} />
</Form.Item>
</Col>
</Row>
<Form.Item label={t('pages.clients.attachedInbounds')} required={!isEdit}>
<Select
mode="multiple"
value={form.inboundIds}
onChange={(v) => update('inboundIds', v)}
options={inboundOptions}
showSearch
placeholder={t('pages.clients.selectInbound')}
filterOption={(input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase())}
/>
</Form.Item>
<Form.Item>
<Switch checked={form.enable} onChange={(v) => update('enable', v)} />
<span style={{ marginLeft: 8 }}>{t('enable')}</span>
</Form.Item>
{isEdit && ipLimitEnable && (
<Form.Item label={t('pages.clients.ipLog')}>
<Space style={{ marginBottom: 8 }}>
<Button size="small" loading={ipsLoading} onClick={loadIps}>{t('refresh')}</Button>
<Button size="small" danger loading={ipsClearing} disabled={clientIps.length === 0} onClick={clearIps}>
{t('pages.clients.clearAll')}
</Button>
</Space>
{clientIps.length > 0 ? (
<div>
{clientIps.map((ip, idx) => (
<Tag key={idx} color="blue" style={{ marginBottom: 4 }}>{ip}</Tag>
))}
</div>
) : (
<Tag>{t('tgbot.noIpRecord')}</Tag>
<Row gutter={16}>
{tgBotEnable && (
<Col xs={24} md={12}>
<Form.Item label={t('pages.clients.telegramId')}>
<InputNumber value={form.tgId} min={0} controls={false}
placeholder={t('pages.clients.telegramIdPlaceholder')} style={{ width: '100%' }}
onChange={(v) => update('tgId', Number(v) || 0)} />
</Form.Item>
</Col>
)}
<Col xs={24} md={tgBotEnable ? 12 : 24}>
<Form.Item label={t('pages.clients.comment')}>
<Input value={form.comment} onChange={(e) => update('comment', e.target.value)} />
</Form.Item>
</Col>
</Row>
<Form.Item label={t('pages.clients.attachedInbounds')} required={!isEdit}>
<Select
mode="multiple"
value={form.inboundIds}
onChange={(v) => update('inboundIds', v)}
options={inboundOptions}
placeholder={t('pages.clients.selectInbound')}
showSearch={{
filterOption: (input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
}}
/>
</Form.Item>
)}
</Form>
<Form.Item>
<Switch checked={form.enable} onChange={(v) => update('enable', v)} />
<span style={{ marginLeft: 8 }}>{t('enable')}</span>
</Form.Item>
{isEdit && ipLimitEnable && (
<Form.Item label={t('pages.clients.ipLog')}>
<Space style={{ marginBottom: 8 }}>
<Button size="small" loading={ipsLoading} onClick={loadIps}>{t('refresh')}</Button>
<Button size="small" danger loading={ipsClearing} disabled={clientIps.length === 0} onClick={clearIps}>
{t('pages.clients.clearAll')}
</Button>
</Space>
{clientIps.length > 0 ? (
<div>
{clientIps.map((ip, idx) => (
<Tag key={idx} color="blue" style={{ marginBottom: 4 }}>{ip}</Tag>
))}
</div>
) : (
<Tag>{t('tgbot.noIpRecord')}</Tag>
)}
</Form.Item>
)}
</Form>
</Modal>
</>
);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -318,8 +318,7 @@ export default function NordModal({
<Form.Item label="Country">
<Select
value={countryId ?? undefined}
showSearch
optionFilterProp="label"
showSearch={{ optionFilterProp: 'label' }}
onChange={(v) => fetchServers(v)}
options={countries.map((c) => ({
value: c.id,
@ -332,8 +331,7 @@ export default function NordModal({
<Form.Item label="City">
<Select
value={cityId}
showSearch
optionFilterProp="label"
showSearch={{ optionFilterProp: 'label' }}
onChange={setCityId}
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">
<Select
value={serverId}
showSearch
optionFilterProp="label"
showSearch={{ optionFilterProp: 'label' }}
onChange={setServerId}
options={filteredServers.map((s) => ({
value: s.id,

View file

@ -1,10 +1,10 @@
import { z } from 'zod';
export const msgSchema = <T extends z.ZodTypeAny>(obj: T) =>
export const msgSchema = <T extends z.ZodType>(obj: T) =>
z.object({
success: z.boolean(),
msg: z.string().default(''),
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.select();
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 {}
host.removeChild(textarea);

View file

@ -2,7 +2,7 @@ import type { Rule } from 'antd/es/form';
import type { TFunction } from 'i18next';
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 {
validator: async (_rule, value) => {
const result = schema.safeParse(value);

View file

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