mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-09 22:55:00 +03:00
b54325dcc9
Use valid translation keys on six panel screens, add the missing copy-failure and empty-value strings across all locales, and reject unresolved literal translation keys in the regression test.
260 lines
8.0 KiB
TypeScript
260 lines
8.0 KiB
TypeScript
import { NativeSelect } from '@/components/form/NativeSelect';
|
|
import { useState, useId } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
AutoComplete,
|
|
Button,
|
|
Collapse,
|
|
Divider,
|
|
Form,
|
|
Input,
|
|
InputNumber,
|
|
Select,
|
|
Space,
|
|
Switch,
|
|
message,
|
|
} from 'antd';
|
|
import { CloudDownloadOutlined, SyncOutlined } from '@ant-design/icons';
|
|
import { useFormContext, useWatch } from 'react-hook-form';
|
|
|
|
import { selectTagRender } from '@/components/form';
|
|
import { FormField } from '@/components/form/rhf';
|
|
import { HttpUtil } from '@/utils';
|
|
|
|
export default function TuicFields() {
|
|
const { t } = useTranslation();
|
|
const uid = useId();
|
|
const [messageApi, messageContextHolder] = message.useMessage();
|
|
const { control, setValue } = useFormContext();
|
|
const [loadingPanelCert, setLoadingPanelCert] = useState(false);
|
|
|
|
const sni = (useWatch({ control, name: 'settings.server.sni' }) ?? '') as string;
|
|
const certificate = (useWatch({ control, name: 'settings.server.certificate' }) ?? '') as string;
|
|
const privateKey = (useWatch({ control, name: 'settings.server.private_key' }) ?? '') as string;
|
|
const nodeId = useWatch({ control, name: 'nodeId' }) as number | null | undefined;
|
|
|
|
const handleSniChange = (newSni: string) => {
|
|
setValue('settings.server.sni', newSni);
|
|
};
|
|
|
|
const autofillFromSni = () => {
|
|
const cleanSni = (sni || '').trim();
|
|
if (!cleanSni) {
|
|
messageApi.warning(t('pages.xray.tuic.sniRequired'));
|
|
return;
|
|
}
|
|
setValue('settings.server.certificate', `/root/cert/${cleanSni}/fullchain.pem`);
|
|
setValue('settings.server.private_key', `/root/cert/${cleanSni}/privkey.pem`);
|
|
};
|
|
|
|
const setCertFromPanel = async () => {
|
|
setLoadingPanelCert(true);
|
|
try {
|
|
const msg =
|
|
typeof nodeId === 'number'
|
|
? await HttpUtil.get(`/panel/api/nodes/webCert/${nodeId}`, undefined, { silent: true })
|
|
: await HttpUtil.post('/panel/api/setting/all', undefined, { silent: true });
|
|
if (!msg?.success) {
|
|
messageApi.warning(msg?.msg || t('pages.inbounds.setDefaultCertEmpty'));
|
|
return;
|
|
}
|
|
const obj = msg.obj as { webCertFile?: string; webKeyFile?: string };
|
|
if (!obj?.webCertFile && !obj?.webKeyFile) {
|
|
messageApi.warning(t('pages.inbounds.setDefaultCertEmpty'));
|
|
return;
|
|
}
|
|
if (obj.webCertFile) {
|
|
setValue('settings.server.certificate', obj.webCertFile);
|
|
}
|
|
if (obj.webKeyFile) {
|
|
setValue('settings.server.private_key', obj.webKeyFile);
|
|
}
|
|
messageApi.success(t('success'));
|
|
} catch {
|
|
messageApi.error(t('somethingWentWrong'));
|
|
} finally {
|
|
setLoadingPanelCert(false);
|
|
}
|
|
};
|
|
|
|
const certOptions = sni
|
|
? [
|
|
{ value: `/root/cert/${sni}/fullchain.pem` },
|
|
{ value: `/etc/letsencrypt/live/${sni}/fullchain.pem` },
|
|
{ value: '/root/cert.pem' },
|
|
]
|
|
: [{ value: '/root/cert.pem' }];
|
|
|
|
const keyOptions = sni
|
|
? [
|
|
{ value: `/root/cert/${sni}/privkey.pem` },
|
|
{ value: `/etc/letsencrypt/live/${sni}/privkey.pem` },
|
|
{ value: '/root/privkey.pem' },
|
|
]
|
|
: [{ value: '/root/privkey.pem' }];
|
|
|
|
const advancedItems = [
|
|
{
|
|
key: 'advanced',
|
|
label: t('pages.inbounds.advancedTitle'),
|
|
children: (
|
|
<>
|
|
<FormField
|
|
name={['settings', 'server', 'zero_rtt_handshake']}
|
|
label={t('pages.xray.tuic.zeroRttHandshake')}
|
|
valueProp="checked"
|
|
>
|
|
<Switch />
|
|
</FormField>
|
|
|
|
<FormField
|
|
name={['settings', 'server', 'log_level']}
|
|
label={t('pages.xray.tuic.logLevel')}
|
|
tooltip={t('pages.xray.tuic.logLevelHint')}
|
|
>
|
|
<NativeSelect
|
|
options={[
|
|
{ label: 'Info', value: 'info' },
|
|
{ label: 'Warn', value: 'warn' },
|
|
{ label: 'Error', value: 'error' },
|
|
{ label: 'Debug', value: 'debug' },
|
|
]}
|
|
/>
|
|
</FormField>
|
|
|
|
<FormField
|
|
name={['settings', 'server', 'max_idle_time']}
|
|
label={t('pages.xray.tuic.maxIdleTime')}
|
|
>
|
|
<InputNumber min={1} style={{ width: '100%' }} />
|
|
</FormField>
|
|
|
|
<FormField
|
|
name={['settings', 'server', 'authentication_timeout']}
|
|
label={t('pages.xray.tuic.authTimeout')}
|
|
>
|
|
<InputNumber min={1} style={{ width: '100%' }} />
|
|
</FormField>
|
|
|
|
<FormField
|
|
name={['settings', 'server', 'max_udp_relay_packet_size']}
|
|
label={t('pages.xray.tuic.maxUdpRelayPacketSize')}
|
|
>
|
|
<InputNumber min={1} max={65245} style={{ width: '100%' }} />
|
|
</FormField>
|
|
</>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
{messageContextHolder}
|
|
<Form.Item htmlFor={`${uid}-certificate`} label={t('pages.inbounds.publicKey')}>
|
|
<AutoComplete
|
|
id={`${uid}-certificate`}
|
|
value={certificate}
|
|
options={certOptions}
|
|
onChange={(v) => setValue('settings.server.certificate', v)}
|
|
placeholder="/root/cert.pem"
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item htmlFor={`${uid}-privateKey`} label={t('pages.inbounds.privatekey')}>
|
|
<AutoComplete
|
|
id={`${uid}-privateKey`}
|
|
value={privateKey}
|
|
options={keyOptions}
|
|
onChange={(v) => setValue('settings.server.private_key', v)}
|
|
placeholder="/root/privkey.pem"
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item label=" ">
|
|
<Space wrap style={{ display: 'flex', flexWrap: 'wrap', width: '100%' }}>
|
|
<Button
|
|
type="primary"
|
|
icon={<CloudDownloadOutlined aria-hidden />}
|
|
loading={loadingPanelCert}
|
|
onClick={setCertFromPanel}
|
|
>
|
|
{t('pages.inbounds.setDefaultCert')}
|
|
</Button>
|
|
<Button icon={<SyncOutlined aria-hidden />} onClick={autofillFromSni}>
|
|
{t('pages.inbounds.form.autoFill')}
|
|
</Button>
|
|
<Button
|
|
danger
|
|
onClick={() => {
|
|
setValue('settings.server.certificate', '');
|
|
setValue('settings.server.private_key', '');
|
|
}}
|
|
>
|
|
{t('clear')}
|
|
</Button>
|
|
</Space>
|
|
</Form.Item>
|
|
|
|
<FormField
|
|
name={['settings', 'server', 'congestion_control']}
|
|
label={t('pages.xray.tuic.congestionControl')}
|
|
tooltip={t('pages.xray.tuic.congestionControlHint')}
|
|
>
|
|
<NativeSelect
|
|
options={[
|
|
{ label: 'BBR', value: 'bbr' },
|
|
{ label: 'CUBIC', value: 'cubic' },
|
|
{ label: 'New Reno', value: 'new_reno' },
|
|
]}
|
|
/>
|
|
</FormField>
|
|
|
|
<FormField name={['settings', 'server', 'alpn']} label={t('pages.xray.tuic.alpn')}>
|
|
<Select
|
|
mode="tags"
|
|
tagRender={selectTagRender}
|
|
tokenSeparators={[',']}
|
|
options={[
|
|
{ label: 'h3', value: 'h3' },
|
|
{ label: 'spdy/3.1', value: 'spdy/3.1' },
|
|
]}
|
|
/>
|
|
</FormField>
|
|
|
|
<Divider titlePlacement="start">{t('pages.xray.tuic.profileOptions')}</Divider>
|
|
|
|
<Form.Item
|
|
htmlFor={`${uid}-sni`}
|
|
label={t('pages.xray.tuic.sni')}
|
|
tooltip={t('pages.xray.tuic.sniHint')}
|
|
>
|
|
<Input
|
|
id={`${uid}-sni`}
|
|
aria-describedby={`${uid}-sni-hint`}
|
|
value={sni}
|
|
placeholder="example.com"
|
|
onChange={(e) => handleSniChange(e.target.value)}
|
|
/>
|
|
<span id={`${uid}-sni-hint`} hidden>
|
|
{t('pages.xray.tuic.sniHint')}
|
|
</span>
|
|
</Form.Item>
|
|
|
|
<FormField
|
|
name={['settings', 'server', 'udp_relay_mode']}
|
|
label={t('pages.xray.tuic.udpRelayMode')}
|
|
tooltip={t('pages.xray.tuic.udpRelayModeHint')}
|
|
>
|
|
<NativeSelect
|
|
options={[
|
|
{ label: 'Native (Recommended)', value: 'native' },
|
|
{ label: 'QUIC', value: 'quic' },
|
|
]}
|
|
/>
|
|
</FormField>
|
|
|
|
<Collapse style={{ marginTop: 16, marginBottom: 8 }} items={advancedItems} />
|
|
</>
|
|
);
|
|
}
|