mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-31 10:15:00 +03:00
fix(frontend): make the panel usable with screen readers (#6907)
* fix(frontend): make the panel usable with screen readers * fix(frontend): keep mobile tab names and the inbound expiry hint Review of #6907 found two gaps in the screen-reader pass: - On phone-width screens catTabLabel renders only the icon and names the tab by putting aria-label on it, but callers now pass that icon with aria-hidden for the desktop label, so every Settings, Xray and Host category tab lost its name under TalkBack. catTabLabel now clears aria-hidden on the icon it uses as the name. - DateTimePicker forwarded id and aria-label but not aria-describedby, so the inbound expiry hint ("leave blank to never expire") was never attached. It now forwards it on both the Gregorian and Jalali input.
This commit is contained in:
+25
-2
@@ -9,7 +9,9 @@
|
|||||||
"jsx-a11y"
|
"jsx-a11y"
|
||||||
],
|
],
|
||||||
"jsPlugins": [
|
"jsPlugins": [
|
||||||
"./tools/oxlint/input-number-guard.mjs"
|
"./tools/oxlint/input-number-guard.mjs",
|
||||||
|
"./tools/oxlint/control-label-guard.mjs",
|
||||||
|
"./tools/oxlint/icon-a11y-guard.mjs"
|
||||||
],
|
],
|
||||||
"categories": {
|
"categories": {
|
||||||
"correctness": "error"
|
"correctness": "error"
|
||||||
@@ -48,7 +50,9 @@
|
|||||||
"react-hooks/exhaustive-deps": "error",
|
"react-hooks/exhaustive-deps": "error",
|
||||||
"jsx-a11y/no-autofocus": "off",
|
"jsx-a11y/no-autofocus": "off",
|
||||||
"input-number/no-synthetic-clear": "off",
|
"input-number/no-synthetic-clear": "off",
|
||||||
"jsx-a11y/prefer-tag-over-role": "off"
|
"jsx-a11y/prefer-tag-over-role": "off",
|
||||||
|
"control-label/has-name": "off",
|
||||||
|
"antd-icon/explicit-a11y": "off"
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
@@ -67,6 +71,25 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"input-number/no-synthetic-clear": "off"
|
"input-number/no-synthetic-clear": "off"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"src/**/*.tsx"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"control-label/has-name": "error",
|
||||||
|
"antd-icon/explicit-a11y": "error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"src/**/*.stories.tsx",
|
||||||
|
"src/test/**"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"control-label/has-name": "off",
|
||||||
|
"antd-icon/explicit-a11y": "off"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ export default function ClientHwidListModal({
|
|||||||
zIndex={zIndex}
|
zIndex={zIndex}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
footer={[
|
footer={[
|
||||||
<Button key="refresh" icon={<ReloadOutlined />} loading={loading} onClick={onRefresh}>
|
<Button
|
||||||
|
key="refresh"
|
||||||
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
|
loading={loading}
|
||||||
|
onClick={onRefresh}
|
||||||
|
>
|
||||||
{t('refresh')}
|
{t('refresh')}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
@@ -122,7 +127,7 @@ export default function ClientHwidListModal({
|
|||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
aria-label={t('pages.clients.deleteHwid')}
|
aria-label={t('pages.clients.deleteHwid')}
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined aria-hidden />}
|
||||||
loading={deletingId === entry.id}
|
loading={deletingId === entry.id}
|
||||||
/>
|
/>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
|
|||||||
@@ -38,12 +38,17 @@ export default function ConfigBlock({
|
|||||||
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
||||||
<div className="config-block-actions" onClick={(e: MouseEvent) => e.stopPropagation()}>
|
<div className="config-block-actions" onClick={(e: MouseEvent) => e.stopPropagation()}>
|
||||||
<Tooltip title={t('copy')}>
|
<Tooltip title={t('copy')}>
|
||||||
<Button size="small" icon={<CopyOutlined />} aria-label={t('copy')} onClick={copy} />
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<CopyOutlined aria-hidden />}
|
||||||
|
aria-label={t('copy')}
|
||||||
|
onClick={copy}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t('download')}>
|
<Tooltip title={t('download')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined aria-hidden />}
|
||||||
aria-label={t('download')}
|
aria-label={t('download')}
|
||||||
onClick={() => FileManager.downloadTextFile(text, fileName)}
|
onClick={() => FileManager.downloadTextFile(text, fileName)}
|
||||||
/>
|
/>
|
||||||
@@ -56,7 +61,11 @@ export default function ConfigBlock({
|
|||||||
content={<QrPanel value={text} remark={qrRemark || label} size={220} />}
|
content={<QrPanel value={text} remark={qrRemark || label} size={220} />}
|
||||||
>
|
>
|
||||||
<Tooltip title={t('pages.clients.qrCode')}>
|
<Tooltip title={t('pages.clients.qrCode')}>
|
||||||
<Button size="small" icon={<QrcodeOutlined />} aria-label={t('pages.clients.qrCode')} />
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<QrcodeOutlined aria-hidden />}
|
||||||
|
aria-label={t('pages.clients.qrCode')}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -262,9 +262,9 @@ export default function CommandPalette() {
|
|||||||
title: c.email,
|
title: c.email,
|
||||||
subtitle: `${trafficUsed} / ${trafficTotal}${c.comment ? ` · ${c.comment}` : ''}`,
|
subtitle: `${trafficUsed} / ${trafficTotal}${c.comment ? ` · ${c.comment}` : ''}`,
|
||||||
icon: isOnline ? (
|
icon: isOnline ? (
|
||||||
<CheckCircleFilled style={{ color: '#52c41a' }} />
|
<CheckCircleFilled aria-hidden style={{ color: '#52c41a' }} />
|
||||||
) : (
|
) : (
|
||||||
<CloseCircleFilled style={{ color: '#ff4d4f' }} />
|
<CloseCircleFilled aria-hidden style={{ color: '#ff4d4f' }} />
|
||||||
),
|
),
|
||||||
action: () => {
|
action: () => {
|
||||||
close();
|
close();
|
||||||
@@ -274,7 +274,7 @@ export default function CommandPalette() {
|
|||||||
c.subId && allSetting.subURI
|
c.subId && allSetting.subURI
|
||||||
? {
|
? {
|
||||||
label: t('commandPalette.copySubscription'),
|
label: t('commandPalette.copySubscription'),
|
||||||
icon: <CopyOutlined />,
|
icon: <CopyOutlined aria-hidden />,
|
||||||
execute: (e) => {
|
execute: (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
copySubscription(c);
|
copySubscription(c);
|
||||||
@@ -331,7 +331,7 @@ export default function CommandPalette() {
|
|||||||
category: 'inbounds',
|
category: 'inbounds',
|
||||||
title: ib.remark || ib.tag || `Inbound #${ib.id}`,
|
title: ib.remark || ib.tag || `Inbound #${ib.id}`,
|
||||||
subtitle: `Port ${ib.port || ''}`,
|
subtitle: `Port ${ib.port || ''}`,
|
||||||
icon: <ImportOutlined style={{ color: '#1677ff' }} />,
|
icon: <ImportOutlined aria-hidden style={{ color: '#1677ff' }} />,
|
||||||
tag:
|
tag:
|
||||||
tags.length > 0 ? (
|
tags.length > 0 ? (
|
||||||
<div style={{ display: 'inline-flex', gap: 4, flexWrap: 'wrap' }}>{tags}</div>
|
<div style={{ display: 'inline-flex', gap: 4, flexWrap: 'wrap' }}>{tags}</div>
|
||||||
@@ -348,7 +348,7 @@ export default function CommandPalette() {
|
|||||||
path: '/',
|
path: '/',
|
||||||
title: t('menu.dashboard'),
|
title: t('menu.dashboard'),
|
||||||
keywords: ['overview', 'dashboard', 'cpu', 'ram', 'memory', 'traffic', 'speed'],
|
keywords: ['overview', 'dashboard', 'cpu', 'ram', 'memory', 'traffic', 'speed'],
|
||||||
icon: <DashboardOutlined />,
|
icon: <DashboardOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/inbounds',
|
path: '/inbounds',
|
||||||
@@ -365,67 +365,67 @@ export default function CommandPalette() {
|
|||||||
'hysteria',
|
'hysteria',
|
||||||
'masque',
|
'masque',
|
||||||
],
|
],
|
||||||
icon: <ImportOutlined />,
|
icon: <ImportOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/clients',
|
path: '/clients',
|
||||||
title: t('menu.clients'),
|
title: t('menu.clients'),
|
||||||
keywords: ['clients', 'users', 'sub', 'traffic', 'quota'],
|
keywords: ['clients', 'users', 'sub', 'traffic', 'quota'],
|
||||||
icon: <TeamOutlined />,
|
icon: <TeamOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/groups',
|
path: '/groups',
|
||||||
title: t('menu.groups'),
|
title: t('menu.groups'),
|
||||||
keywords: ['groups', 'tags', 'batch'],
|
keywords: ['groups', 'tags', 'batch'],
|
||||||
icon: <TagsOutlined />,
|
icon: <TagsOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/nodes',
|
path: '/nodes',
|
||||||
title: t('menu.nodes'),
|
title: t('menu.nodes'),
|
||||||
keywords: ['nodes', 'servers', 'cluster', 'remote nodes'],
|
keywords: ['nodes', 'servers', 'cluster', 'remote nodes'],
|
||||||
icon: <ClusterOutlined />,
|
icon: <ClusterOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/hosts',
|
path: '/hosts',
|
||||||
title: t('menu.hosts'),
|
title: t('menu.hosts'),
|
||||||
keywords: ['hosts', 'sni', 'domains'],
|
keywords: ['hosts', 'sni', 'domains'],
|
||||||
icon: <GlobalOutlined />,
|
icon: <GlobalOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/outbound',
|
path: '/outbound',
|
||||||
title: t('menu.outbounds'),
|
title: t('menu.outbounds'),
|
||||||
keywords: ['outbounds', 'freedom', 'blackhole', 'socks', 'http', 'warp', 'nord', 'pia'],
|
keywords: ['outbounds', 'freedom', 'blackhole', 'socks', 'http', 'warp', 'nord', 'pia'],
|
||||||
icon: <ExportOutlined />,
|
icon: <ExportOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/routing',
|
path: '/routing',
|
||||||
title: t('menu.routing'),
|
title: t('menu.routing'),
|
||||||
keywords: ['routing', 'rules', 'geoip', 'geosite', 'direct', 'block'],
|
keywords: ['routing', 'rules', 'geoip', 'geosite', 'direct', 'block'],
|
||||||
icon: <SwapOutlined />,
|
icon: <SwapOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
title: t('menu.settings'),
|
title: t('menu.settings'),
|
||||||
keywords: ['settings', 'config', 'port', 'password', 'ssl', 'telegram'],
|
keywords: ['settings', 'config', 'port', 'password', 'ssl', 'telegram'],
|
||||||
icon: <SettingOutlined />,
|
icon: <SettingOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/xray',
|
path: '/xray',
|
||||||
title: t('menu.xray'),
|
title: t('menu.xray'),
|
||||||
keywords: ['xray', 'templates', 'balancer', 'dns'],
|
keywords: ['xray', 'templates', 'balancer', 'dns'],
|
||||||
icon: <ToolOutlined />,
|
icon: <ToolOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/api-docs',
|
path: '/api-docs',
|
||||||
title: t('menu.apiDocs'),
|
title: t('menu.apiDocs'),
|
||||||
keywords: ['api', 'api docs', 'swagger', 'rest api', 'endpoints'],
|
keywords: ['api', 'api docs', 'swagger', 'rest api', 'endpoints'],
|
||||||
icon: <ApiOutlined />,
|
icon: <ApiOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/sponsors',
|
path: '/sponsors',
|
||||||
title: t('menu.sponsors'),
|
title: t('menu.sponsors'),
|
||||||
keywords: ['sponsors', 'sponsor', 'partners'],
|
keywords: ['sponsors', 'sponsor', 'partners'],
|
||||||
icon: <CrownOutlined />,
|
icon: <CrownOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -451,63 +451,63 @@ export default function CommandPalette() {
|
|||||||
title: `${t('menu.settings')} · ${t('pages.settings.panelSettings')}`,
|
title: `${t('menu.settings')} · ${t('pages.settings.panelSettings')}`,
|
||||||
subtitle: t('pages.settings.panelSettings'),
|
subtitle: t('pages.settings.panelSettings'),
|
||||||
keywords: ['general', 'webPort', 'webBasePath', 'listenIP', 'ssl', 'certificate'],
|
keywords: ['general', 'webPort', 'webBasePath', 'listenIP', 'ssl', 'certificate'],
|
||||||
icon: <SettingOutlined />,
|
icon: <SettingOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings#security',
|
path: '/settings#security',
|
||||||
title: `${t('menu.settings')} · ${t('pages.settings.securitySettings')}`,
|
title: `${t('menu.settings')} · ${t('pages.settings.securitySettings')}`,
|
||||||
subtitle: t('pages.settings.securitySettings'),
|
subtitle: t('pages.settings.securitySettings'),
|
||||||
keywords: ['security', 'password', 'username', '2fa', 'two factor', 'login limit'],
|
keywords: ['security', 'password', 'username', '2fa', 'two factor', 'login limit'],
|
||||||
icon: <SafetyOutlined />,
|
icon: <SafetyOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings#telegram',
|
path: '/settings#telegram',
|
||||||
title: `${t('menu.settings')} · ${t('pages.settings.TGBotSettings')}`,
|
title: `${t('menu.settings')} · ${t('pages.settings.TGBotSettings')}`,
|
||||||
subtitle: t('pages.settings.TGBotSettings'),
|
subtitle: t('pages.settings.TGBotSettings'),
|
||||||
keywords: ['telegram', 'tgbot', 'bot token', 'chat id', 'notifications', 'alerts'],
|
keywords: ['telegram', 'tgbot', 'bot token', 'chat id', 'notifications', 'alerts'],
|
||||||
icon: <MessageOutlined />,
|
icon: <MessageOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings#email',
|
path: '/settings#email',
|
||||||
title: `${t('menu.settings')} · ${t('pages.settings.emailSettings')}`,
|
title: `${t('menu.settings')} · ${t('pages.settings.emailSettings')}`,
|
||||||
subtitle: t('pages.settings.emailSettings'),
|
subtitle: t('pages.settings.emailSettings'),
|
||||||
keywords: ['email', 'smtp', 'mail', 'crash alerts'],
|
keywords: ['email', 'smtp', 'mail', 'crash alerts'],
|
||||||
icon: <MailOutlined />,
|
icon: <MailOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings#discord',
|
path: '/settings#discord',
|
||||||
title: `${t('menu.settings')} · ${t('pages.settings.discordSettings')}`,
|
title: `${t('menu.settings')} · ${t('pages.settings.discordSettings')}`,
|
||||||
subtitle: t('pages.settings.discordSettings'),
|
subtitle: t('pages.settings.discordSettings'),
|
||||||
keywords: ['discord', 'bot', 'channel', 'notifications', 'alerts'],
|
keywords: ['discord', 'bot', 'channel', 'notifications', 'alerts'],
|
||||||
icon: <DiscordOutlined />,
|
icon: <DiscordOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings#subscription',
|
path: '/settings#subscription',
|
||||||
title: `${t('menu.settings')} · ${t('pages.settings.subSettings')}`,
|
title: `${t('menu.settings')} · ${t('pages.settings.subSettings')}`,
|
||||||
subtitle: t('pages.settings.subSettings'),
|
subtitle: t('pages.settings.subSettings'),
|
||||||
keywords: ['subscription', 'subPort', 'subURI', 'subDomain', 'reverse proxy'],
|
keywords: ['subscription', 'subPort', 'subURI', 'subDomain', 'reverse proxy'],
|
||||||
icon: <CloudServerOutlined />,
|
icon: <CloudServerOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings#subscription-formats',
|
path: '/settings#subscription-formats',
|
||||||
title: `${t('menu.settings')} · ${t('menu.subFormats')}`,
|
title: `${t('menu.settings')} · ${t('menu.subFormats')}`,
|
||||||
subtitle: t('menu.subFormats'),
|
subtitle: t('menu.subFormats'),
|
||||||
keywords: ['formats', 'clash', 'sing-box', 'v2ray', 'json', 'sub formats'],
|
keywords: ['formats', 'clash', 'sing-box', 'v2ray', 'json', 'sub formats'],
|
||||||
icon: <CodeOutlined />,
|
icon: <CodeOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings#subscription-balancers',
|
path: '/settings#subscription-balancers',
|
||||||
title: `${t('menu.settings')} · ${t('pages.settings.subBalancers.menu')}`,
|
title: `${t('menu.settings')} · ${t('pages.settings.subBalancers.menu')}`,
|
||||||
subtitle: t('pages.settings.subBalancers.menu'),
|
subtitle: t('pages.settings.subBalancers.menu'),
|
||||||
keywords: ['balancers', 'sub balancers', 'balancer nodes'],
|
keywords: ['balancers', 'sub balancers', 'balancer nodes'],
|
||||||
icon: <ApartmentOutlined />,
|
icon: <ApartmentOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/xray#basic',
|
path: '/xray#basic',
|
||||||
title: `${t('menu.xray')} · ${t('pages.xray.basicTemplate')}`,
|
title: `${t('menu.xray')} · ${t('pages.xray.basicTemplate')}`,
|
||||||
subtitle: t('pages.xray.basicTemplate'),
|
subtitle: t('pages.xray.basicTemplate'),
|
||||||
keywords: ['basics', 'freedom strategy', 'happy eyeballs', 'torrent', 'connection'],
|
keywords: ['basics', 'freedom strategy', 'happy eyeballs', 'torrent', 'connection'],
|
||||||
icon: <ToolOutlined />,
|
icon: <ToolOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/xray#basic',
|
path: '/xray#basic',
|
||||||
@@ -522,56 +522,56 @@ export default function CommandPalette() {
|
|||||||
'statsOutbound',
|
'statsOutbound',
|
||||||
'metrics_out',
|
'metrics_out',
|
||||||
],
|
],
|
||||||
icon: <DashboardOutlined />,
|
icon: <DashboardOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/xray#basic',
|
path: '/xray#basic',
|
||||||
title: `${t('menu.xray')} · ${t('pages.xray.connectionLimits')}`,
|
title: `${t('menu.xray')} · ${t('pages.xray.connectionLimits')}`,
|
||||||
subtitle: t('pages.xray.connectionLimits'),
|
subtitle: t('pages.xray.connectionLimits'),
|
||||||
keywords: ['limits', 'idle timeout', 'bufferSize', 'connIdle', 'timeout'],
|
keywords: ['limits', 'idle timeout', 'bufferSize', 'connIdle', 'timeout'],
|
||||||
icon: <ClockCircleOutlined />,
|
icon: <ClockCircleOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/xray#basic',
|
path: '/xray#basic',
|
||||||
title: `${t('menu.xray')} · ${t('pages.xray.logConfigs')}`,
|
title: `${t('menu.xray')} · ${t('pages.xray.logConfigs')}`,
|
||||||
subtitle: t('pages.xray.logConfigs'),
|
subtitle: t('pages.xray.logConfigs'),
|
||||||
keywords: ['logs', 'access log', 'error log', 'dns log', 'mask address', 'loglevel'],
|
keywords: ['logs', 'access log', 'error log', 'dns log', 'mask address', 'loglevel'],
|
||||||
icon: <FileTextOutlined />,
|
icon: <FileTextOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/xray#balancer',
|
path: '/xray#balancer',
|
||||||
title: `${t('menu.xray')} · ${t('pages.xray.Balancers')}`,
|
title: `${t('menu.xray')} · ${t('pages.xray.Balancers')}`,
|
||||||
subtitle: t('pages.xray.Balancers'),
|
subtitle: t('pages.xray.Balancers'),
|
||||||
keywords: ['balancers', 'leastPing', 'roundRobin', 'fallback', 'strategy'],
|
keywords: ['balancers', 'leastPing', 'roundRobin', 'fallback', 'strategy'],
|
||||||
icon: <ClusterOutlined />,
|
icon: <ClusterOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/xray#dns',
|
path: '/xray#dns',
|
||||||
title: `${t('menu.xray')} · DNS`,
|
title: `${t('menu.xray')} · DNS`,
|
||||||
subtitle: 'DNS',
|
subtitle: 'DNS',
|
||||||
keywords: ['dns', 'dns servers', 'hosts', 'doh', 'dot', 'cloudflare dns'],
|
keywords: ['dns', 'dns servers', 'hosts', 'doh', 'dot', 'cloudflare dns'],
|
||||||
icon: <DatabaseOutlined />,
|
icon: <DatabaseOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/xray#outbound',
|
path: '/xray#outbound',
|
||||||
title: `${t('menu.xray')} · ${t('pages.xray.Outbounds')}`,
|
title: `${t('menu.xray')} · ${t('pages.xray.Outbounds')}`,
|
||||||
subtitle: t('pages.xray.Outbounds'),
|
subtitle: t('pages.xray.Outbounds'),
|
||||||
keywords: ['outbound', 'freedom', 'direct', 'proxy outbounds'],
|
keywords: ['outbound', 'freedom', 'direct', 'proxy outbounds'],
|
||||||
icon: <ExportOutlined />,
|
icon: <ExportOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/xray#routing',
|
path: '/xray#routing',
|
||||||
title: `${t('menu.xray')} · ${t('pages.xray.basicRouting')}`,
|
title: `${t('menu.xray')} · ${t('pages.xray.basicRouting')}`,
|
||||||
subtitle: t('pages.xray.basicRouting'),
|
subtitle: t('pages.xray.basicRouting'),
|
||||||
keywords: ['routing', 'routing rules', 'geoip', 'geosite', 'block', 'direct'],
|
keywords: ['routing', 'routing rules', 'geoip', 'geosite', 'block', 'direct'],
|
||||||
icon: <SwapOutlined />,
|
icon: <SwapOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/xray#advanced',
|
path: '/xray#advanced',
|
||||||
title: `${t('menu.xray')} · ${t('pages.xray.advancedTemplate')}`,
|
title: `${t('menu.xray')} · ${t('pages.xray.advancedTemplate')}`,
|
||||||
subtitle: t('pages.xray.advancedTemplate'),
|
subtitle: t('pages.xray.advancedTemplate'),
|
||||||
keywords: ['advanced', 'json template', 'advanced config', 'custom json'],
|
keywords: ['advanced', 'json template', 'advanced config', 'custom json'],
|
||||||
icon: <CodeOutlined />,
|
icon: <CodeOutlined aria-hidden />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -599,7 +599,7 @@ export default function CommandPalette() {
|
|||||||
title: t('commandPalette.restartXray'),
|
title: t('commandPalette.restartXray'),
|
||||||
subtitle: t('pages.index.restartXray'),
|
subtitle: t('pages.index.restartXray'),
|
||||||
keywords: ['restart', 'xray restart', 'reboot xray'],
|
keywords: ['restart', 'xray restart', 'reboot xray'],
|
||||||
icon: <ReloadOutlined style={{ color: '#faad14' }} />,
|
icon: <ReloadOutlined aria-hidden style={{ color: '#faad14' }} />,
|
||||||
action: restartXray,
|
action: restartXray,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -608,7 +608,7 @@ export default function CommandPalette() {
|
|||||||
title: t('menu.theme'),
|
title: t('menu.theme'),
|
||||||
subtitle: isUltra ? 'Ultra Dark' : isDark ? 'Dark' : 'Light',
|
subtitle: isUltra ? 'Ultra Dark' : isDark ? 'Dark' : 'Light',
|
||||||
keywords: ['theme', 'light', 'dark', 'ultra'],
|
keywords: ['theme', 'light', 'dark', 'ultra'],
|
||||||
icon: isDark ? <SunOutlined /> : <MoonOutlined />,
|
icon: isDark ? <SunOutlined aria-hidden /> : <MoonOutlined aria-hidden />,
|
||||||
action: cycleTheme,
|
action: cycleTheme,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -617,7 +617,7 @@ export default function CommandPalette() {
|
|||||||
title: t('pages.inbounds.addInbound'),
|
title: t('pages.inbounds.addInbound'),
|
||||||
subtitle: t('menu.inbounds'),
|
subtitle: t('menu.inbounds'),
|
||||||
keywords: ['add inbound', 'create inbound', 'new port', 'new inbound'],
|
keywords: ['add inbound', 'create inbound', 'new port', 'new inbound'],
|
||||||
icon: <PlusOutlined style={{ color: '#52c41a' }} />,
|
icon: <PlusOutlined aria-hidden style={{ color: '#52c41a' }} />,
|
||||||
action: () => {
|
action: () => {
|
||||||
close();
|
close();
|
||||||
navigate('/inbounds');
|
navigate('/inbounds');
|
||||||
@@ -629,7 +629,7 @@ export default function CommandPalette() {
|
|||||||
title: t('pages.clients.addClient'),
|
title: t('pages.clients.addClient'),
|
||||||
subtitle: t('menu.clients'),
|
subtitle: t('menu.clients'),
|
||||||
keywords: ['add client', 'create user', 'new client', 'new user'],
|
keywords: ['add client', 'create user', 'new client', 'new user'],
|
||||||
icon: <PlusOutlined style={{ color: '#52c41a' }} />,
|
icon: <PlusOutlined aria-hidden style={{ color: '#52c41a' }} />,
|
||||||
action: () => {
|
action: () => {
|
||||||
close();
|
close();
|
||||||
navigate('/clients');
|
navigate('/clients');
|
||||||
@@ -705,9 +705,9 @@ export default function CommandPalette() {
|
|||||||
>
|
>
|
||||||
<div className="command-palette-header">
|
<div className="command-palette-header">
|
||||||
{isClientSearching ? (
|
{isClientSearching ? (
|
||||||
<LoadingOutlined className="command-palette-search-icon spinning" />
|
<LoadingOutlined aria-hidden className="command-palette-search-icon spinning" />
|
||||||
) : (
|
) : (
|
||||||
<SearchOutlined className="command-palette-search-icon" />
|
<SearchOutlined aria-hidden className="command-palette-search-icon" />
|
||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
|||||||
@@ -69,11 +69,11 @@ export default function TextModal({
|
|||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
{fileName && (
|
{fileName && (
|
||||||
<Button icon={<DownloadOutlined />} onClick={download}>
|
<Button icon={<DownloadOutlined aria-hidden />} onClick={download}>
|
||||||
{fileName}
|
{fileName}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button type="primary" icon={<CopyOutlined />} onClick={copy}>
|
<Button type="primary" icon={<CopyOutlined aria-hidden />} onClick={copy}>
|
||||||
{t('copy')}
|
{t('copy')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ interface DateTimePickerProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
allowClear?: boolean;
|
allowClear?: boolean;
|
||||||
maxDate?: Dayjs;
|
maxDate?: Dayjs;
|
||||||
|
id?: string;
|
||||||
|
'aria-label'?: string;
|
||||||
|
'aria-describedby'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LIGHT_THEME = {
|
const LIGHT_THEME = {
|
||||||
@@ -57,6 +60,9 @@ export default function DateTimePicker({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
allowClear = true,
|
allowClear = true,
|
||||||
maxDate,
|
maxDate,
|
||||||
|
id,
|
||||||
|
'aria-label': ariaLabel,
|
||||||
|
'aria-describedby': ariaDescribedBy,
|
||||||
}: DateTimePickerProps) {
|
}: DateTimePickerProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { datepicker } = useDatepicker();
|
const { datepicker } = useDatepicker();
|
||||||
@@ -97,7 +103,11 @@ export default function DateTimePicker({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (datepicker !== 'jalalian') return;
|
if (datepicker !== 'jalalian') return;
|
||||||
const input = jalaliRef.current?.querySelector('input');
|
const input = jalaliRef.current?.querySelector('input');
|
||||||
if (input) input.placeholder = placeholder;
|
if (!input) return;
|
||||||
|
input.placeholder = placeholder;
|
||||||
|
if (id) input.id = id;
|
||||||
|
if (ariaLabel) input.setAttribute('aria-label', ariaLabel);
|
||||||
|
if (ariaDescribedBy) input.setAttribute('aria-describedby', ariaDescribedBy);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (datepicker === 'jalalian') {
|
if (datepicker === 'jalalian') {
|
||||||
@@ -137,7 +147,7 @@ export default function DateTimePicker({
|
|||||||
setClearNonce((n) => n + 1);
|
setClearNonce((n) => n + 1);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CloseCircleFilled />
|
<CloseCircleFilled aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -156,6 +166,9 @@ export default function DateTimePicker({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
allowClear={allowClear}
|
allowClear={allowClear}
|
||||||
maxDate={maxDate}
|
maxDate={maxDate}
|
||||||
|
id={id}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
aria-describedby={ariaDescribedBy}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -125,21 +125,23 @@ export default function HeaderMapEditor({ mode, value, onChange }: HeaderMapEdit
|
|||||||
<Input
|
<Input
|
||||||
value={row.name}
|
value={row.name}
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
|
aria-label={t('pages.inbounds.form.headerName')}
|
||||||
onChange={(e) => setRow(idx, { name: e.target.value })}
|
onChange={(e) => setRow(idx, { name: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
value={row.value}
|
value={row.value}
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
|
aria-label={t('pages.inbounds.form.headerValue')}
|
||||||
onChange={(e) => setRow(idx, { value: e.target.value })}
|
onChange={(e) => setRow(idx, { value: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('remove')}
|
aria-label={t('remove')}
|
||||||
icon={<MinusOutlined />}
|
icon={<MinusOutlined aria-hidden />}
|
||||||
onClick={() => removeRow(idx)}
|
onClick={() => removeRow(idx)}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
))}
|
))}
|
||||||
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={addRow}>
|
<Button size="small" type="primary" icon={<PlusOutlined aria-hidden />} onClick={addRow}>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ interface RemarkTemplateFieldProps {
|
|||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
metadataOnly?: boolean;
|
metadataOnly?: boolean;
|
||||||
|
id?: string;
|
||||||
|
'aria-label'?: string;
|
||||||
|
'aria-labelledby'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,6 +40,9 @@ export default function RemarkTemplateField({
|
|||||||
multiline = false,
|
multiline = false,
|
||||||
rows,
|
rows,
|
||||||
metadataOnly = false,
|
metadataOnly = false,
|
||||||
|
id,
|
||||||
|
'aria-label': ariaLabel,
|
||||||
|
'aria-labelledby': ariaLabelledBy,
|
||||||
}: RemarkTemplateFieldProps) {
|
}: RemarkTemplateFieldProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const inputRef = useRef<InputRef>(null);
|
const inputRef = useRef<InputRef>(null);
|
||||||
@@ -76,7 +82,7 @@ export default function RemarkTemplateField({
|
|||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CodeOutlined />}
|
icon={<CodeOutlined aria-hidden />}
|
||||||
aria-label={t('pages.hosts.remarkVars.title')}
|
aria-label={t('pages.hosts.remarkVars.title')}
|
||||||
style={{ marginInlineEnd: -7 }}
|
style={{ marginInlineEnd: -7 }}
|
||||||
/>
|
/>
|
||||||
@@ -90,6 +96,9 @@ export default function RemarkTemplateField({
|
|||||||
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
|
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
ref={textAreaRef}
|
ref={textAreaRef}
|
||||||
|
id={id}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
aria-labelledby={ariaLabelledBy}
|
||||||
value={value}
|
value={value}
|
||||||
maxLength={maxLength}
|
maxLength={maxLength}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
@@ -101,6 +110,9 @@ export default function RemarkTemplateField({
|
|||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
id={id}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
aria-labelledby={ariaLabelledBy}
|
||||||
value={value}
|
value={value}
|
||||||
maxLength={maxLength}
|
maxLength={maxLength}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|||||||
@@ -49,8 +49,13 @@ export function FormField<T extends FieldValues = FieldValues>({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dottedName = toDotted(name) as Path<T>;
|
const dottedName = toDotted(name) as Path<T>;
|
||||||
const generatedId = useId();
|
const generatedId = useId();
|
||||||
const explicitId = (children as ReactElement<{ id?: string }>).props.id;
|
const hintId = useId();
|
||||||
const fieldId = explicitId ?? generatedId;
|
const childOwnProps = (children as ReactElement<{ id?: string; 'aria-describedby'?: string }>)
|
||||||
|
.props;
|
||||||
|
const fieldId = childOwnProps.id ?? generatedId;
|
||||||
|
const describedBy = tooltip
|
||||||
|
? [childOwnProps['aria-describedby'], hintId].filter(Boolean).join(' ')
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
@@ -74,6 +79,7 @@ export function FormField<T extends FieldValues = FieldValues>({
|
|||||||
onBlur: field.onBlur,
|
onBlur: field.onBlur,
|
||||||
ref: field.ref,
|
ref: field.ref,
|
||||||
};
|
};
|
||||||
|
if (describedBy) childProps['aria-describedby'] = describedBy;
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={label}
|
label={label}
|
||||||
@@ -88,6 +94,12 @@ export function FormField<T extends FieldValues = FieldValues>({
|
|||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
{cloneElement(children as ReactElement<Record<string, unknown>>, childProps)}
|
{cloneElement(children as ReactElement<Record<string, unknown>>, childProps)}
|
||||||
|
{/* The tooltip only opens on hover, so screen readers get its text as a description. */}
|
||||||
|
{tooltip && (
|
||||||
|
<span id={hintId} hidden>
|
||||||
|
{tooltip}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ export default function GeoBrowserModal({
|
|||||||
value={categoryQuery}
|
value={categoryQuery}
|
||||||
onChange={(event) => setCategoryQuery(event.target.value)}
|
onChange={(event) => setCategoryQuery(event.target.value)}
|
||||||
placeholder={t('pages.xray.geoBrowser.searchCategory')}
|
placeholder={t('pages.xray.geoBrowser.searchCategory')}
|
||||||
|
aria-label={t('pages.xray.geoBrowser.searchCategory')}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -392,6 +393,7 @@ export default function GeoBrowserModal({
|
|||||||
value={entryQuery}
|
value={entryQuery}
|
||||||
onChange={(event) => setEntryQuery(event.target.value)}
|
onChange={(event) => setEntryQuery(event.target.value)}
|
||||||
placeholder={t('pages.xray.geoBrowser.searchEntries')}
|
placeholder={t('pages.xray.geoBrowser.searchEntries')}
|
||||||
|
aria-label={t('pages.xray.geoBrowser.searchEntries')}
|
||||||
allowClear
|
allowClear
|
||||||
className="geo-entry-filter"
|
className="geo-entry-filter"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface GeoTokenInputProps {
|
|||||||
kind: GeoTokenKind;
|
kind: GeoTokenKind;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
'aria-describedby'?: string;
|
||||||
ref?: Ref<InputRef>;
|
ref?: Ref<InputRef>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ export default function GeoTokenInput({
|
|||||||
kind,
|
kind,
|
||||||
placeholder,
|
placeholder,
|
||||||
id,
|
id,
|
||||||
|
'aria-describedby': ariaDescribedBy,
|
||||||
ref,
|
ref,
|
||||||
}: GeoTokenInputProps) {
|
}: GeoTokenInputProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -92,6 +94,7 @@ export default function GeoTokenInput({
|
|||||||
<Input
|
<Input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={id}
|
id={id}
|
||||||
|
aria-describedby={ariaDescribedBy}
|
||||||
value={value}
|
value={value}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={(event) => onChange?.(event.target.value)}
|
onChange={(event) => onChange?.(event.target.value)}
|
||||||
@@ -99,7 +102,7 @@ export default function GeoTokenInput({
|
|||||||
/>
|
/>
|
||||||
<Tooltip title={t('pages.xray.geoBrowser.openTooltip')}>
|
<Tooltip title={t('pages.xray.geoBrowser.openTooltip')}>
|
||||||
<Button
|
<Button
|
||||||
icon={<DatabaseOutlined />}
|
icon={<DatabaseOutlined aria-hidden />}
|
||||||
aria-label={t('pages.xray.geoBrowser.openTooltip')}
|
aria-label={t('pages.xray.geoBrowser.openTooltip')}
|
||||||
onClick={() => setBrowsing(true)}
|
onClick={() => setBrowsing(true)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -63,13 +63,13 @@ export default function SponsorCard({ sponsor, variant = 'banner', onClose }: Sp
|
|||||||
<span className="sponsor-aside">
|
<span className="sponsor-aside">
|
||||||
<span className="sponsor-tag">{tag}</span>
|
<span className="sponsor-tag">{tag}</span>
|
||||||
<span className="sponsor-visit">
|
<span className="sponsor-visit">
|
||||||
{t('pages.sponsors.visit')} <ExportOutlined />
|
{t('pages.sponsors.visit')} <ExportOutlined aria-hidden />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{variant === 'card' && (
|
{variant === 'card' && (
|
||||||
<span className="sponsor-visit">
|
<span className="sponsor-visit">
|
||||||
{t('pages.sponsors.visit')} <ExportOutlined />
|
{t('pages.sponsors.visit')} <ExportOutlined aria-hidden />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
@@ -81,7 +81,7 @@ export default function SponsorCard({ sponsor, variant = 'banner', onClose }: Sp
|
|||||||
title={t('close')}
|
title={t('close')}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<CloseOutlined />
|
<CloseOutlined aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { NotificationGroupConfig } from './types';
|
|||||||
|
|
||||||
const GROUPS: NotificationGroupConfig[] = [
|
const GROUPS: NotificationGroupConfig[] = [
|
||||||
{
|
{
|
||||||
icon: <CloudServerOutlined />,
|
icon: <CloudServerOutlined aria-hidden />,
|
||||||
title: 'eventGroupOutbound',
|
title: 'eventGroupOutbound',
|
||||||
events: [
|
events: [
|
||||||
{ key: 'outbound.down', label: 'eventOutboundDown', settingKey: '' },
|
{ key: 'outbound.down', label: 'eventOutboundDown', settingKey: '' },
|
||||||
@@ -21,12 +21,12 @@ const GROUPS: NotificationGroupConfig[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <ThunderboltOutlined />,
|
icon: <ThunderboltOutlined aria-hidden />,
|
||||||
title: 'eventGroupXray',
|
title: 'eventGroupXray',
|
||||||
events: [{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' }],
|
events: [{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <DesktopOutlined />,
|
icon: <DesktopOutlined aria-hidden />,
|
||||||
title: 'eventGroupNode',
|
title: 'eventGroupNode',
|
||||||
events: [
|
events: [
|
||||||
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
|
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
|
||||||
@@ -34,7 +34,7 @@ const GROUPS: NotificationGroupConfig[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <DashboardOutlined />,
|
icon: <DashboardOutlined aria-hidden />,
|
||||||
title: 'eventGroupSystem',
|
title: 'eventGroupSystem',
|
||||||
events: [
|
events: [
|
||||||
{
|
{
|
||||||
@@ -72,7 +72,7 @@ const GROUPS: NotificationGroupConfig[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <SafetyOutlined />,
|
icon: <SafetyOutlined aria-hidden />,
|
||||||
title: 'eventGroupSecurity',
|
title: 'eventGroupSecurity',
|
||||||
events: [{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' }],
|
events: [{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' }],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { NotificationGroupConfig } from './types';
|
|||||||
|
|
||||||
const GROUPS: NotificationGroupConfig[] = [
|
const GROUPS: NotificationGroupConfig[] = [
|
||||||
{
|
{
|
||||||
icon: <CloudServerOutlined />,
|
icon: <CloudServerOutlined aria-hidden />,
|
||||||
title: 'eventGroupOutbound',
|
title: 'eventGroupOutbound',
|
||||||
events: [
|
events: [
|
||||||
{
|
{
|
||||||
@@ -36,12 +36,12 @@ const GROUPS: NotificationGroupConfig[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <ThunderboltOutlined />,
|
icon: <ThunderboltOutlined aria-hidden />,
|
||||||
title: 'eventGroupXray',
|
title: 'eventGroupXray',
|
||||||
events: [{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' }],
|
events: [{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <DesktopOutlined />,
|
icon: <DesktopOutlined aria-hidden />,
|
||||||
title: 'eventGroupNode',
|
title: 'eventGroupNode',
|
||||||
events: [
|
events: [
|
||||||
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
|
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
|
||||||
@@ -49,7 +49,7 @@ const GROUPS: NotificationGroupConfig[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <DashboardOutlined />,
|
icon: <DashboardOutlined aria-hidden />,
|
||||||
title: 'eventGroupSystem',
|
title: 'eventGroupSystem',
|
||||||
events: [
|
events: [
|
||||||
{
|
{
|
||||||
@@ -87,7 +87,7 @@ const GROUPS: NotificationGroupConfig[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <SafetyOutlined />,
|
icon: <SafetyOutlined aria-hidden />,
|
||||||
title: 'eventGroupSecurity',
|
title: 'eventGroupSecurity',
|
||||||
events: [{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' }],
|
events: [{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' }],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { NotificationGroupConfig } from './types';
|
|||||||
|
|
||||||
const GROUPS: NotificationGroupConfig[] = [
|
const GROUPS: NotificationGroupConfig[] = [
|
||||||
{
|
{
|
||||||
icon: <CloudServerOutlined />,
|
icon: <CloudServerOutlined aria-hidden />,
|
||||||
title: 'eventGroupOutbound',
|
title: 'eventGroupOutbound',
|
||||||
events: [
|
events: [
|
||||||
{
|
{
|
||||||
@@ -36,12 +36,12 @@ const GROUPS: NotificationGroupConfig[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <ThunderboltOutlined />,
|
icon: <ThunderboltOutlined aria-hidden />,
|
||||||
title: 'eventGroupXray',
|
title: 'eventGroupXray',
|
||||||
events: [{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' }],
|
events: [{ key: 'xray.crash', label: 'eventXrayCrash', settingKey: '' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <DesktopOutlined />,
|
icon: <DesktopOutlined aria-hidden />,
|
||||||
title: 'eventGroupNode',
|
title: 'eventGroupNode',
|
||||||
events: [
|
events: [
|
||||||
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
|
{ key: 'node.down', label: 'eventNodeDown', settingKey: '' },
|
||||||
@@ -49,7 +49,7 @@ const GROUPS: NotificationGroupConfig[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <DashboardOutlined />,
|
icon: <DashboardOutlined aria-hidden />,
|
||||||
title: 'eventGroupSystem',
|
title: 'eventGroupSystem',
|
||||||
events: [
|
events: [
|
||||||
{
|
{
|
||||||
@@ -87,7 +87,7 @@ const GROUPS: NotificationGroupConfig[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <SafetyOutlined />,
|
icon: <SafetyOutlined aria-hidden />,
|
||||||
title: 'eventGroupSecurity',
|
title: 'eventGroupSecurity',
|
||||||
events: [{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' }],
|
events: [{ key: 'login.attempt', label: 'eventLoginAttempt', settingKey: '' }],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'antd/dist/reset.css';
|
|||||||
import { setupHttp } from '@/api/http-init';
|
import { setupHttp } from '@/api/http-init';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import { readyI18n } from '@/i18n/react';
|
import { readyI18n } from '@/i18n/react';
|
||||||
|
import { AntdRootConfig } from '@/i18n/AntdRootConfig';
|
||||||
import { ThemeProvider } from '@/hooks/useTheme';
|
import { ThemeProvider } from '@/hooks/useTheme';
|
||||||
import { QueryProvider } from '@/api/QueryProvider';
|
import { QueryProvider } from '@/api/QueryProvider';
|
||||||
import LoginPage from '@/pages/login/LoginPage';
|
import LoginPage from '@/pages/login/LoginPage';
|
||||||
@@ -21,11 +22,13 @@ readyI18n().then(() => {
|
|||||||
const root = document.getElementById('app');
|
const root = document.getElementById('app');
|
||||||
if (root) {
|
if (root) {
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<ThemeProvider>
|
<AntdRootConfig>
|
||||||
<QueryProvider>
|
<ThemeProvider>
|
||||||
<LoginPage />
|
<QueryProvider>
|
||||||
</QueryProvider>
|
<LoginPage />
|
||||||
</ThemeProvider>,
|
</QueryProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</AntdRootConfig>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { message } from 'antd';
|
|||||||
import 'antd/dist/reset.css';
|
import 'antd/dist/reset.css';
|
||||||
|
|
||||||
import { readyI18n } from '@/i18n/react';
|
import { readyI18n } from '@/i18n/react';
|
||||||
|
import { AntdRootConfig } from '@/i18n/AntdRootConfig';
|
||||||
import { ThemeProvider } from '@/hooks/useTheme';
|
import { ThemeProvider } from '@/hooks/useTheme';
|
||||||
import { QueryProvider } from '@/api/QueryProvider';
|
import { QueryProvider } from '@/api/QueryProvider';
|
||||||
import SubPage from '@/pages/sub/SubPage';
|
import SubPage from '@/pages/sub/SubPage';
|
||||||
@@ -16,11 +17,13 @@ readyI18n('subscription').then(() => {
|
|||||||
const root = document.getElementById('app');
|
const root = document.getElementById('app');
|
||||||
if (root) {
|
if (root) {
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<ThemeProvider>
|
<AntdRootConfig>
|
||||||
<QueryProvider>
|
<ThemeProvider>
|
||||||
<SubPage />
|
<QueryProvider>
|
||||||
</QueryProvider>
|
<SubPage />
|
||||||
</ThemeProvider>,
|
</QueryProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</AntdRootConfig>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { ConfigProvider } from 'antd';
|
||||||
|
import type { ConfigProviderProps } from 'antd/es/config-provider';
|
||||||
|
import type { Locale } from 'antd/es/locale';
|
||||||
|
|
||||||
|
const antdLocaleLoaders: Record<string, () => Promise<{ default: Locale }>> = {
|
||||||
|
'ar-EG': () => import('antd/es/locale/ar_EG'),
|
||||||
|
'es-ES': () => import('antd/es/locale/es_ES'),
|
||||||
|
'fa-IR': () => import('antd/es/locale/fa_IR'),
|
||||||
|
'id-ID': () => import('antd/es/locale/id_ID'),
|
||||||
|
'ja-JP': () => import('antd/es/locale/ja_JP'),
|
||||||
|
'pt-BR': () => import('antd/es/locale/pt_BR'),
|
||||||
|
'ru-RU': () => import('antd/es/locale/ru_RU'),
|
||||||
|
'tr-TR': () => import('antd/es/locale/tr_TR'),
|
||||||
|
'uk-UA': () => import('antd/es/locale/uk_UA'),
|
||||||
|
'vi-VN': () => import('antd/es/locale/vi_VN'),
|
||||||
|
'zh-CN': () => import('antd/es/locale/zh_CN'),
|
||||||
|
'zh-TW': () => import('antd/es/locale/zh_TW'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Undefined keeps antd's built-in en_US.
|
||||||
|
let antdLocale: Locale | undefined;
|
||||||
|
|
||||||
|
export async function loadAntdLocale(language: string): Promise<void> {
|
||||||
|
antdLocale = (await antdLocaleLoaders[language]?.())?.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Form.Item tooltip icon sits inside its <label>, so its "question-circle" name
|
||||||
|
// became part of every hinted field's name. FormField exposes the hint as a description.
|
||||||
|
const FORM_CONFIG: ConfigProviderProps['form'] = {
|
||||||
|
tooltip: { icon: <QuestionCircleOutlined aria-hidden /> },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AntdRootConfig({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<ConfigProvider locale={antdLocale} form={FORM_CONFIG}>
|
||||||
|
{children}
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { initReactI18next } from 'react-i18next';
|
|||||||
|
|
||||||
import { LanguageManager } from '@/utils';
|
import { LanguageManager } from '@/utils';
|
||||||
import type { LanguageScope } from '@/utils';
|
import type { LanguageScope } from '@/utils';
|
||||||
|
import { loadAntdLocale } from './AntdRootConfig';
|
||||||
import enUS from '../../../internal/web/translation/en-US.json';
|
import enUS from '../../../internal/web/translation/en-US.json';
|
||||||
|
|
||||||
const FALLBACK = 'en-US';
|
const FALLBACK = 'en-US';
|
||||||
@@ -43,6 +44,9 @@ export async function readyI18n(scope: LanguageScope = 'panel') {
|
|||||||
await i18next.changeLanguage(active);
|
await i18next.changeLanguage(active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The html files ship lang="en"; screen readers pick their voice from it (s-ui#1293).
|
||||||
|
document.documentElement.lang = i18next.language;
|
||||||
|
await loadAntdLocale(i18next.language);
|
||||||
return i18next;
|
return i18next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ function DonateButton({ ariaLabel }: { ariaLabel: string }) {
|
|||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
title={ariaLabel}
|
title={ariaLabel}
|
||||||
>
|
>
|
||||||
<HeartOutlined />
|
<HeartOutlined aria-hidden />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -115,7 +115,7 @@ function DocsButton({ ariaLabel }: { ariaLabel: string }) {
|
|||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
title={ariaLabel}
|
title={ariaLabel}
|
||||||
>
|
>
|
||||||
<ReadOutlined />
|
<ReadOutlined aria-hidden />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@ function VersionBadge({ version, collapsed }: { version: string; collapsed?: boo
|
|||||||
aria-label={`GitHub ${label}`}
|
aria-label={`GitHub ${label}`}
|
||||||
title={label}
|
title={label}
|
||||||
>
|
>
|
||||||
<GithubOutlined />
|
<GithubOutlined aria-hidden />
|
||||||
{!collapsed && <span className="sider-version-text">{label}</span>}
|
{!collapsed && <span className="sider-version-text">{label}</span>}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
@@ -151,7 +151,13 @@ function ThemeCycleButton({
|
|||||||
onCycle: () => void;
|
onCycle: () => void;
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
}) {
|
}) {
|
||||||
const icon = !isDark ? <SunOutlined /> : !isUltra ? <MoonOutlined /> : <MoonFilled />;
|
const icon = !isDark ? (
|
||||||
|
<SunOutlined aria-hidden />
|
||||||
|
) : !isUltra ? (
|
||||||
|
<MoonOutlined aria-hidden />
|
||||||
|
) : (
|
||||||
|
<MoonFilled aria-hidden />
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
id={id}
|
id={id}
|
||||||
@@ -248,42 +254,46 @@ export default function AppSidebar() {
|
|||||||
const children: NonNullable<MenuProps['items']> = [
|
const children: NonNullable<MenuProps['items']> = [
|
||||||
{
|
{
|
||||||
key: '/settings#general',
|
key: '/settings#general',
|
||||||
icon: <SettingOutlined />,
|
icon: <SettingOutlined aria-hidden />,
|
||||||
label: t('pages.settings.panelSettings'),
|
label: t('pages.settings.panelSettings'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '/settings#security',
|
key: '/settings#security',
|
||||||
icon: <SafetyOutlined />,
|
icon: <SafetyOutlined aria-hidden />,
|
||||||
label: t('pages.settings.securitySettings'),
|
label: t('pages.settings.securitySettings'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '/settings#telegram',
|
key: '/settings#telegram',
|
||||||
icon: <MessageOutlined />,
|
icon: <MessageOutlined aria-hidden />,
|
||||||
label: t('pages.settings.TGBotSettings'),
|
label: t('pages.settings.TGBotSettings'),
|
||||||
},
|
},
|
||||||
{ key: '/settings#email', icon: <MailOutlined />, label: t('pages.settings.emailSettings') },
|
{
|
||||||
|
key: '/settings#email',
|
||||||
|
icon: <MailOutlined aria-hidden />,
|
||||||
|
label: t('pages.settings.emailSettings'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: '/settings#discord',
|
key: '/settings#discord',
|
||||||
icon: <DiscordOutlined />,
|
icon: <DiscordOutlined aria-hidden />,
|
||||||
label: t('pages.settings.discordSettings'),
|
label: t('pages.settings.discordSettings'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '/settings#subscription',
|
key: '/settings#subscription',
|
||||||
icon: <CloudServerOutlined />,
|
icon: <CloudServerOutlined aria-hidden />,
|
||||||
label: t('pages.settings.subSettings'),
|
label: t('pages.settings.subSettings'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
if (showSubFormats) {
|
if (showSubFormats) {
|
||||||
children.push({
|
children.push({
|
||||||
key: '/settings#subscription-formats',
|
key: '/settings#subscription-formats',
|
||||||
icon: <CodeOutlined />,
|
icon: <CodeOutlined aria-hidden />,
|
||||||
label: t('menu.subFormats'),
|
label: t('menu.subFormats'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (showSubBalancers) {
|
if (showSubBalancers) {
|
||||||
children.push({
|
children.push({
|
||||||
key: '/settings#subscription-balancers',
|
key: '/settings#subscription-balancers',
|
||||||
icon: <ApartmentOutlined />,
|
icon: <ApartmentOutlined aria-hidden />,
|
||||||
label: t('pages.settings.subBalancers.menu'),
|
label: t('pages.settings.subBalancers.menu'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -292,10 +302,22 @@ export default function AppSidebar() {
|
|||||||
|
|
||||||
const xrayChildren = useMemo<NonNullable<MenuProps['items']>>(
|
const xrayChildren = useMemo<NonNullable<MenuProps['items']>>(
|
||||||
() => [
|
() => [
|
||||||
{ key: '/xray#basic', icon: <SettingOutlined />, label: t('pages.xray.basicTemplate') },
|
{
|
||||||
{ key: '/xray#balancer', icon: <ClusterOutlined />, label: t('pages.xray.Balancers') },
|
key: '/xray#basic',
|
||||||
{ key: '/xray#dns', icon: <DatabaseOutlined />, label: 'DNS' },
|
icon: <SettingOutlined aria-hidden />,
|
||||||
{ key: '/xray#advanced', icon: <CodeOutlined />, label: t('pages.xray.advancedTemplate') },
|
label: t('pages.xray.basicTemplate'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '/xray#balancer',
|
||||||
|
icon: <ClusterOutlined aria-hidden />,
|
||||||
|
label: t('pages.xray.Balancers'),
|
||||||
|
},
|
||||||
|
{ key: '/xray#dns', icon: <DatabaseOutlined aria-hidden />, label: 'DNS' },
|
||||||
|
{
|
||||||
|
key: '/xray#advanced',
|
||||||
|
icon: <CodeOutlined aria-hidden />,
|
||||||
|
label: t('pages.xray.advancedTemplate'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
[t],
|
[t],
|
||||||
);
|
);
|
||||||
@@ -394,7 +416,7 @@ export default function AppSidebar() {
|
|||||||
title={t(pinned ? 'menu.unpinSidebar' : 'menu.pinSidebar')}
|
title={t(pinned ? 'menu.unpinSidebar' : 'menu.pinSidebar')}
|
||||||
onClick={togglePinned}
|
onClick={togglePinned}
|
||||||
>
|
>
|
||||||
{pinned ? <PushpinFilled /> : <PushpinOutlined />}
|
{pinned ? <PushpinFilled aria-hidden /> : <PushpinOutlined aria-hidden />}
|
||||||
</button>
|
</button>
|
||||||
<DocsButton ariaLabel={t('menu.docs') || 'Documentation'} />
|
<DocsButton ariaLabel={t('menu.docs') || 'Documentation'} />
|
||||||
<DonateButton ariaLabel={t('menu.donate') || 'Donate'} />
|
<DonateButton ariaLabel={t('menu.donate') || 'Donate'} />
|
||||||
@@ -421,7 +443,7 @@ export default function AppSidebar() {
|
|||||||
aria-label={t('commandPalette.title') || 'Command Palette (Ctrl + K)'}
|
aria-label={t('commandPalette.title') || 'Command Palette (Ctrl + K)'}
|
||||||
>
|
>
|
||||||
<span className="sidebar-command-left">
|
<span className="sidebar-command-left">
|
||||||
<SearchOutlined className="sidebar-command-icon" />
|
<SearchOutlined aria-hidden className="sidebar-command-icon" />
|
||||||
<span className="sidebar-command-text">
|
<span className="sidebar-command-text">
|
||||||
{t('commandPalette.search') || 'Search...'}
|
{t('commandPalette.search') || 'Search...'}
|
||||||
</span>
|
</span>
|
||||||
@@ -488,7 +510,7 @@ export default function AppSidebar() {
|
|||||||
aria-label={t('close')}
|
aria-label={t('close')}
|
||||||
onClick={() => setDrawerOpen(false)}
|
onClick={() => setDrawerOpen(false)}
|
||||||
>
|
>
|
||||||
<CloseOutlined />
|
<CloseOutlined aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -503,7 +525,7 @@ export default function AppSidebar() {
|
|||||||
style={{ margin: '8px 12px 4px', width: 'calc(100% - 24px)' }}
|
style={{ margin: '8px 12px 4px', width: 'calc(100% - 24px)' }}
|
||||||
>
|
>
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<SearchOutlined className="sidebar-command-icon" />
|
<SearchOutlined aria-hidden className="sidebar-command-icon" />
|
||||||
<span>{t('commandPalette.search') || 'Search...'}</span>
|
<span>{t('commandPalette.search') || 'Search...'}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="sidebar-command-kbd">
|
<span className="sidebar-command-kbd">
|
||||||
@@ -547,7 +569,7 @@ export default function AppSidebar() {
|
|||||||
aria-label={t('menu.openMenu')}
|
aria-label={t('menu.openMenu')}
|
||||||
onClick={() => setDrawerOpen(true)}
|
onClick={() => setDrawerOpen(true)}
|
||||||
>
|
>
|
||||||
<MenuOutlined />
|
<MenuOutlined aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function CustomSockoptList({
|
|||||||
<Button
|
<Button
|
||||||
type="dashed"
|
type="dashed"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
onClick={() => add({ type: 'int', level: '6', opt: '', value: '' })}
|
onClick={() => add({ type: 'int', level: '6', opt: '', value: '' })}
|
||||||
>
|
>
|
||||||
{t('pages.inbounds.form.addCustomOption')}
|
{t('pages.inbounds.form.addCustomOption')}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useId, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
AutoComplete,
|
AutoComplete,
|
||||||
Button,
|
Button,
|
||||||
@@ -271,6 +271,7 @@ export default function FinalMaskForm({
|
|||||||
showAll = false,
|
showAll = false,
|
||||||
side = 'client',
|
side = 'client',
|
||||||
}: FinalMaskFormProps) {
|
}: FinalMaskFormProps) {
|
||||||
|
const uid = useId();
|
||||||
const base = asPath(name);
|
const base = asPath(name);
|
||||||
|
|
||||||
// Migrate legacy mask shapes once on mount so configs saved before #6334 (fragment
|
// Migrate legacy mask shapes once on mount so configs saved before #6334 (fragment
|
||||||
@@ -346,8 +347,9 @@ export default function FinalMaskForm({
|
|||||||
)}
|
)}
|
||||||
{showQuic && (
|
{showQuic && (
|
||||||
<>
|
<>
|
||||||
<Form.Item label="QUIC Params">
|
<Form.Item label="QUIC Params" htmlFor={`${uid}-quic-params`}>
|
||||||
<Switch
|
<Switch
|
||||||
|
id={`${uid}-quic-params`}
|
||||||
checked={hasQuicParams}
|
checked={hasQuicParams}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
form.setFieldValue([...base, 'quicParams'], v ? defaultQuicParams() : undefined);
|
form.setFieldValue([...base, 'quicParams'], v ? defaultQuicParams() : undefined);
|
||||||
@@ -385,7 +387,7 @@ function TcpMasksList({
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('add')}
|
aria-label={t('add')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
add({ type: 'fragment', settings: defaultTcpMaskSettings('fragment') })
|
add({ type: 'fragment', settings: defaultTcpMaskSettings('fragment') })
|
||||||
@@ -531,12 +533,13 @@ function TcpMaskItem({
|
|||||||
rules={[{ required: true, message: 'Password is required' }]}
|
rules={[{ required: true, message: 'Password is required' }]}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
aria-label="Password"
|
||||||
placeholder="Obfuscation password"
|
placeholder="Obfuscation password"
|
||||||
style={{ width: 'calc(100% - 32px)' }}
|
style={{ width: 'calc(100% - 32px)' }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button
|
<Button
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
form.setFieldValue(
|
form.setFieldValue(
|
||||||
@@ -558,36 +561,47 @@ function TcpMaskItem({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FILTER_MASK_OPTION = { value: 'filter', label: 'Filter (source IPs)' };
|
const FILTER_MASK_OPTION = { value: 'filter', label: 'Filter (source IPs)' };
|
||||||
|
const FILTER_ALLOWED_HINT = 'Only these sources may connect; empty allows every source';
|
||||||
|
const FILTER_BLOCKED_HINT = 'These sources are dropped before any other mask runs';
|
||||||
|
|
||||||
// xray-core 26.10.21 (#7206) screens a listener's source IPs; a blocked match wins over an allowed
|
// xray-core 26.10.21 (#7206) screens a listener's source IPs; a blocked match wins over an allowed
|
||||||
// one, and an empty list leaves that side open. Links and JSON subscriptions drop it.
|
// one, and an empty list leaves that side open. Links and JSON subscriptions drop it.
|
||||||
function FilterMaskSettings({ fieldName }: { fieldName: number }) {
|
function FilterMaskSettings({ fieldName }: { fieldName: number }) {
|
||||||
|
const uid = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Allowed Source IPs"
|
label="Allowed Source IPs"
|
||||||
name={[fieldName, 'settings', 'allowedSourceIPs']}
|
name={[fieldName, 'settings', 'allowedSourceIPs']}
|
||||||
tooltip="Only these sources may connect; empty allows every source"
|
tooltip={FILTER_ALLOWED_HINT}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
aria-describedby={`${uid}-allowed-hint`}
|
||||||
mode="tags"
|
mode="tags"
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
tokenSeparators={[',', ' ']}
|
tokenSeparators={[',', ' ']}
|
||||||
placeholder="10.0.0.0/8, geoip:ir"
|
placeholder="10.0.0.0/8, geoip:ir"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<span id={`${uid}-allowed-hint`} hidden>
|
||||||
|
{FILTER_ALLOWED_HINT}
|
||||||
|
</span>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Blocked Source IPs"
|
label="Blocked Source IPs"
|
||||||
name={[fieldName, 'settings', 'blockedSourceIPs']}
|
name={[fieldName, 'settings', 'blockedSourceIPs']}
|
||||||
tooltip="These sources are dropped before any other mask runs"
|
tooltip={FILTER_BLOCKED_HINT}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
aria-describedby={`${uid}-blocked-hint`}
|
||||||
mode="tags"
|
mode="tags"
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
tokenSeparators={[',', ' ']}
|
tokenSeparators={[',', ' ']}
|
||||||
placeholder="203.0.113.7, geoip:cn"
|
placeholder="203.0.113.7, geoip:cn"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<span id={`${uid}-blocked-hint`} hidden>
|
||||||
|
{FILTER_BLOCKED_HINT}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -683,7 +697,7 @@ function FragmentRangeList({
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('add')}
|
aria-label={t('add')}
|
||||||
onClick={() => add('')}
|
onClick={() => add('')}
|
||||||
/>
|
/>
|
||||||
@@ -776,7 +790,7 @@ function XmcProfilesList({ tcpFieldName }: { tcpFieldName: number }) {
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('add')}
|
aria-label={t('add')}
|
||||||
onClick={() => add(defaultXmcProfile())}
|
onClick={() => add(defaultXmcProfile())}
|
||||||
/>
|
/>
|
||||||
@@ -846,6 +860,21 @@ const HEADER_CUSTOM_TCP_GROUPS = [
|
|||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const UDP_MODE_HINT =
|
||||||
|
'Prefix puts the header on every packet; Standalone exchanges it as separate handshake packets first';
|
||||||
|
const NOISE_RESET_HINT = 'Seconds before the noise is sent to a destination again; 0 sends it once';
|
||||||
|
const NOISE_SCRIPT_HINT =
|
||||||
|
'A Lua file xray-core runs to build the noise, found beside the xray binary; replaces the noise list';
|
||||||
|
const DOH_TLS_HINT =
|
||||||
|
'tlsSettings for the https:// addresses, e.g. a serverName to front the domain';
|
||||||
|
const DOH_QUIC_HINT = 'QUIC parameters for an https:// address reached over h3';
|
||||||
|
const REUSE_HINT = 'Writes the bytes an earlier item captured under this name';
|
||||||
|
const TRANSFORM_HINT =
|
||||||
|
'An expression tree, e.g. {"op": "concat", "args": [{"type": "str", "bytes": "OK "}, {"reuse": "nonce"}]}';
|
||||||
|
const CAPTURE_HINT = 'Saves the bytes this item writes or reads under a name later items can reuse';
|
||||||
|
const UDP_HOP_HINT =
|
||||||
|
'Advertised to clients as a hop range; the server listens on its own port only';
|
||||||
|
|
||||||
function HeaderCustomGroups({
|
function HeaderCustomGroups({
|
||||||
tcpFieldName,
|
tcpFieldName,
|
||||||
form,
|
form,
|
||||||
@@ -856,6 +885,7 @@ function HeaderCustomGroups({
|
|||||||
absoluteSettingsPath: (string | number)[];
|
absoluteSettingsPath: (string | number)[];
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{HEADER_CUSTOM_TCP_GROUPS.map(({ key: groupKey, label, tooltip }) => (
|
{HEADER_CUSTOM_TCP_GROUPS.map(({ key: groupKey, label, tooltip }) => (
|
||||||
@@ -866,10 +896,14 @@ function HeaderCustomGroups({
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('add')}
|
aria-label={t('add')}
|
||||||
|
aria-describedby={`${uid}-${groupKey}-hint`}
|
||||||
onClick={() => addGroup([defaultClientServerItem()])}
|
onClick={() => addGroup([defaultClientServerItem()])}
|
||||||
/>
|
/>
|
||||||
|
<span id={`${uid}-${groupKey}-hint`} hidden>
|
||||||
|
{tooltip}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{groups.map((group, gi) => (
|
{groups.map((group, gi) => (
|
||||||
<div key={group.key}>
|
<div key={group.key}>
|
||||||
@@ -890,7 +924,7 @@ function HeaderCustomGroups({
|
|||||||
<Form.Item label="Items">
|
<Form.Item label="Items">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('add')}
|
aria-label={t('add')}
|
||||||
onClick={() => addItem(defaultClientServerItem())}
|
onClick={() => addItem(defaultClientServerItem())}
|
||||||
/>
|
/>
|
||||||
@@ -950,7 +984,7 @@ function UdpMasksList({
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('add')}
|
aria-label={t('add')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const def = isHysteria || isWireguard ? 'salamander' : 'mkcp-legacy';
|
const def = isHysteria || isWireguard ? 'salamander' : 'mkcp-legacy';
|
||||||
@@ -1228,6 +1262,7 @@ function SalamanderUdpMaskSettings({
|
|||||||
absolutePath: (string | number)[];
|
absolutePath: (string | number)[];
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const packetSizePath = [...absolutePath, 'settings', 'packetSize'];
|
const packetSizePath = [...absolutePath, 'settings', 'packetSize'];
|
||||||
const packetSize = Form.useWatch(packetSizePath, { form, preserve: true });
|
const packetSize = Form.useWatch(packetSizePath, { form, preserve: true });
|
||||||
const mode = typeof packetSize === 'string' && packetSize.trim() !== '' ? 'gecko' : 'salamander';
|
const mode = typeof packetSize === 'string' && packetSize.trim() !== '' ? 'gecko' : 'salamander';
|
||||||
@@ -1235,6 +1270,7 @@ function SalamanderUdpMaskSettings({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-salamander-mode`}
|
||||||
label="Mode"
|
label="Mode"
|
||||||
extra={
|
extra={
|
||||||
mode === 'gecko'
|
mode === 'gecko'
|
||||||
@@ -1243,6 +1279,7 @@ function SalamanderUdpMaskSettings({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
id={`${uid}-salamander-mode`}
|
||||||
value={mode}
|
value={mode}
|
||||||
onChange={(next) => {
|
onChange={(next) => {
|
||||||
if (next === 'gecko') {
|
if (next === 'gecko') {
|
||||||
@@ -1274,10 +1311,14 @@ function SalamanderUdpMaskSettings({
|
|||||||
noStyle
|
noStyle
|
||||||
rules={[{ validator: validateSalamanderPassword }]}
|
rules={[{ validator: validateSalamanderPassword }]}
|
||||||
>
|
>
|
||||||
<Input placeholder="Obfuscation password" style={{ width: 'calc(100% - 32px)' }} />
|
<Input
|
||||||
|
aria-label="Password"
|
||||||
|
placeholder="Obfuscation password"
|
||||||
|
style={{ width: 'calc(100% - 32px)' }}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button
|
<Button
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
form.setFieldValue(
|
form.setFieldValue(
|
||||||
@@ -1304,9 +1345,11 @@ function SalamanderUdpMaskSettings({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function GeckoPacketSizeInput({
|
function GeckoPacketSizeInput({
|
||||||
|
id,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
|
id?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -1315,6 +1358,7 @@ function GeckoPacketSizeInput({
|
|||||||
return (
|
return (
|
||||||
<Space.Compact block>
|
<Space.Compact block>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
id={id}
|
||||||
prefix="Min"
|
prefix="Min"
|
||||||
min={GECKO_MIN_PACKET_SIZE}
|
min={GECKO_MIN_PACKET_SIZE}
|
||||||
max={GECKO_MAX_PACKET_SIZE}
|
max={GECKO_MAX_PACKET_SIZE}
|
||||||
@@ -1325,6 +1369,7 @@ function GeckoPacketSizeInput({
|
|||||||
style={{ width: '50%' }}
|
style={{ width: '50%' }}
|
||||||
/>
|
/>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
aria-label="Packet size max"
|
||||||
prefix="Max"
|
prefix="Max"
|
||||||
min={GECKO_MIN_PACKET_SIZE}
|
min={GECKO_MIN_PACKET_SIZE}
|
||||||
max={GECKO_MAX_PACKET_SIZE}
|
max={GECKO_MAX_PACKET_SIZE}
|
||||||
@@ -1348,14 +1393,12 @@ function UdpHeaderCustom({
|
|||||||
absoluteSettingsPath: (string | number)[];
|
absoluteSettingsPath: (string | number)[];
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Item
|
<Form.Item label="Mode" name={[udpFieldName, 'settings', 'mode']} tooltip={UDP_MODE_HINT}>
|
||||||
label="Mode"
|
|
||||||
name={[udpFieldName, 'settings', 'mode']}
|
|
||||||
tooltip="Prefix puts the header on every packet; Standalone exchanges it as separate handshake packets first"
|
|
||||||
>
|
|
||||||
<Select
|
<Select
|
||||||
|
aria-describedby={`${uid}-mode-hint`}
|
||||||
allowClear
|
allowClear
|
||||||
placeholder="Prefix"
|
placeholder="Prefix"
|
||||||
options={[
|
options={[
|
||||||
@@ -1364,6 +1407,9 @@ function UdpHeaderCustom({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<span id={`${uid}-mode-hint`} hidden>
|
||||||
|
{UDP_MODE_HINT}
|
||||||
|
</span>
|
||||||
{(['client', 'server'] as const).map((groupKey) => (
|
{(['client', 'server'] as const).map((groupKey) => (
|
||||||
<Form.List key={groupKey} name={[udpFieldName, 'settings', groupKey]}>
|
<Form.List key={groupKey} name={[udpFieldName, 'settings', groupKey]}>
|
||||||
{(items, { add, remove }) => (
|
{(items, { add, remove }) => (
|
||||||
@@ -1372,7 +1418,7 @@ function UdpHeaderCustom({
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('add')}
|
aria-label={t('add')}
|
||||||
onClick={() => add(defaultUdpClientServerItem())}
|
onClick={() => add(defaultUdpClientServerItem())}
|
||||||
/>
|
/>
|
||||||
@@ -1418,6 +1464,7 @@ const XDNS_RECORD_TYPE_OPTIONS = [
|
|||||||
// Resolvers and extraPoll are read by clients only; a server keeps them for the share link.
|
// Resolvers and extraPoll are read by clients only; a server keeps them for the share link.
|
||||||
function XdnsSettings({ udpFieldName }: { udpFieldName: number }) {
|
function XdnsSettings({ udpFieldName }: { udpFieldName: number }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.List name={[udpFieldName, 'settings', 'domains']}>
|
<Form.List name={[udpFieldName, 'settings', 'domains']}>
|
||||||
@@ -1427,7 +1474,7 @@ function XdnsSettings({ udpFieldName }: { udpFieldName: number }) {
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('add')}
|
aria-label={t('add')}
|
||||||
onClick={() => add({ names: [], edns0: XDNS_LEGACY_EDNS0 })}
|
onClick={() => add({ names: [], edns0: XDNS_LEGACY_EDNS0 })}
|
||||||
/>
|
/>
|
||||||
@@ -1481,7 +1528,7 @@ function XdnsSettings({ udpFieldName }: { udpFieldName: number }) {
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('add')}
|
aria-label={t('add')}
|
||||||
onClick={() => add({ addrs: [] })}
|
onClick={() => add({ addrs: [] })}
|
||||||
/>
|
/>
|
||||||
@@ -1513,17 +1560,29 @@ function XdnsSettings({ udpFieldName }: { udpFieldName: number }) {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
label="DoH TLS"
|
label="DoH TLS"
|
||||||
name={[resolver.name, 'tlsConfig']}
|
name={[resolver.name, 'tlsConfig']}
|
||||||
tooltip="tlsSettings for the https:// addresses, e.g. a serverName to front the domain"
|
tooltip={DOH_TLS_HINT}
|
||||||
>
|
>
|
||||||
<JsonObjectInput placeholder='{"serverName": "dns.google", "fingerprint": "chrome"}' />
|
<JsonObjectInput
|
||||||
|
aria-describedby={`${uid}-doh-tls-hint-${ri}`}
|
||||||
|
placeholder='{"serverName": "dns.google", "fingerprint": "chrome"}'
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<span id={`${uid}-doh-tls-hint-${ri}`} hidden>
|
||||||
|
{DOH_TLS_HINT}
|
||||||
|
</span>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="DoH QUIC"
|
label="DoH QUIC"
|
||||||
name={[resolver.name, 'quicConfig']}
|
name={[resolver.name, 'quicConfig']}
|
||||||
tooltip="QUIC parameters for an https:// address reached over h3"
|
tooltip={DOH_QUIC_HINT}
|
||||||
>
|
>
|
||||||
<JsonObjectInput placeholder='{"congestion": "bbr"}' />
|
<JsonObjectInput
|
||||||
|
aria-describedby={`${uid}-doh-quic-hint-${ri}`}
|
||||||
|
placeholder='{"congestion": "bbr"}'
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<span id={`${uid}-doh-quic-hint-${ri}`} hidden>
|
||||||
|
{DOH_QUIC_HINT}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@@ -1619,25 +1678,32 @@ function NoiseItems({
|
|||||||
form: FormInstance;
|
form: FormInstance;
|
||||||
absoluteSettingsPath: (string | number)[];
|
absoluteSettingsPath: (string | number)[];
|
||||||
}) {
|
}) {
|
||||||
|
const uid = useId();
|
||||||
const script = Form.useWatch([...absoluteSettingsPath, 'script'], form) as string | undefined;
|
const script = Form.useWatch([...absoluteSettingsPath, 'script'], form) as string | undefined;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Reset"
|
label="Reset"
|
||||||
name={[udpFieldName, 'settings', 'reset']}
|
name={[udpFieldName, 'settings', 'reset']}
|
||||||
tooltip="Seconds before the noise is sent to a destination again; 0 sends it once"
|
tooltip={NOISE_RESET_HINT}
|
||||||
rules={[{ validator: validateNoiseReset }]}
|
rules={[{ validator: validateNoiseReset }]}
|
||||||
>
|
>
|
||||||
<Input placeholder="0 or 60-120" />
|
<Input placeholder="0 or 60-120" aria-describedby={`${uid}-reset-hint`} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<span id={`${uid}-reset-hint`} hidden>
|
||||||
|
{NOISE_RESET_HINT}
|
||||||
|
</span>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Script"
|
label="Script"
|
||||||
name={[udpFieldName, 'settings', 'script']}
|
name={[udpFieldName, 'settings', 'script']}
|
||||||
tooltip="A Lua file xray-core runs to build the noise, found beside the xray binary; replaces the noise list"
|
tooltip={NOISE_SCRIPT_HINT}
|
||||||
normalize={(value: string) => value || undefined}
|
normalize={(value: string) => value || undefined}
|
||||||
>
|
>
|
||||||
<Input allowClear placeholder="noise.lua" />
|
<Input allowClear placeholder="noise.lua" aria-describedby={`${uid}-script-hint`} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<span id={`${uid}-script-hint`} hidden>
|
||||||
|
{NOISE_SCRIPT_HINT}
|
||||||
|
</span>
|
||||||
{!script && (
|
{!script && (
|
||||||
<NoiseItemList
|
<NoiseItemList
|
||||||
udpFieldName={udpFieldName}
|
udpFieldName={udpFieldName}
|
||||||
@@ -1668,7 +1734,7 @@ function NoiseItemList({
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('add')}
|
aria-label={t('add')}
|
||||||
onClick={() => add(defaultNoiseItem())}
|
onClick={() => add(defaultNoiseItem())}
|
||||||
/>
|
/>
|
||||||
@@ -1728,10 +1794,14 @@ function validateCustomVarName(_: unknown, value: unknown) {
|
|||||||
|
|
||||||
// Hands the form only a parsed JSON object, so half-typed text never reaches the config.
|
// Hands the form only a parsed JSON object, so half-typed text never reaches the config.
|
||||||
function JsonObjectInput({
|
function JsonObjectInput({
|
||||||
|
id,
|
||||||
|
'aria-describedby': ariaDescribedBy,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
}: {
|
}: {
|
||||||
|
id?: string;
|
||||||
|
'aria-describedby'?: string;
|
||||||
value?: Record<string, unknown>;
|
value?: Record<string, unknown>;
|
||||||
onChange?: (next: Record<string, unknown> | undefined) => void;
|
onChange?: (next: Record<string, unknown> | undefined) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -1741,6 +1811,8 @@ function JsonObjectInput({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
|
id={id}
|
||||||
|
aria-describedby={ariaDescribedBy}
|
||||||
value={text}
|
value={text}
|
||||||
autoSize={{ minRows: 2, maxRows: 8 }}
|
autoSize={{ minRows: 2, maxRows: 8 }}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
@@ -1783,6 +1855,7 @@ function ItemEditor({
|
|||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const [kind, setKind] = useState<ItemKind>(() =>
|
const [kind, setKind] = useState<ItemKind>(() =>
|
||||||
allowVariables ? itemKindOf(form.getFieldValue(absoluteItemPath)) : 'bytes',
|
allowVariables ? itemKindOf(form.getFieldValue(absoluteItemPath)) : 'bytes',
|
||||||
);
|
);
|
||||||
@@ -1816,8 +1889,13 @@ function ItemEditor({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{allowVariables && (
|
{allowVariables && (
|
||||||
<Form.Item label="Kind">
|
<Form.Item label="Kind" htmlFor={`${uid}-kind`}>
|
||||||
<Select value={kind} onChange={onKindChange} options={ITEM_KIND_OPTIONS} />
|
<Select
|
||||||
|
id={`${uid}-kind`}
|
||||||
|
value={kind}
|
||||||
|
onChange={onKindChange}
|
||||||
|
options={ITEM_KIND_OPTIONS}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
{kind === 'bytes' && (
|
{kind === 'bytes' && (
|
||||||
@@ -1848,27 +1926,40 @@ function ItemEditor({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{kind === 'reuse' && (
|
{kind === 'reuse' && (
|
||||||
<Form.Item
|
<>
|
||||||
label="Reuse"
|
<Form.Item
|
||||||
name={[fieldName, 'reuse']}
|
label="Reuse"
|
||||||
tooltip="Writes the bytes an earlier item captured under this name"
|
name={[fieldName, 'reuse']}
|
||||||
rules={[
|
tooltip={REUSE_HINT}
|
||||||
{ required: true, message: 'name the captured variable' },
|
rules={[
|
||||||
{ validator: validateCustomVarName },
|
{ required: true, message: 'name the captured variable' },
|
||||||
]}
|
{ validator: validateCustomVarName },
|
||||||
>
|
]}
|
||||||
<Input placeholder="variable name" />
|
>
|
||||||
</Form.Item>
|
<Input placeholder="variable name" aria-describedby={`${uid}-reuse-hint`} />
|
||||||
|
</Form.Item>
|
||||||
|
<span id={`${uid}-reuse-hint`} hidden>
|
||||||
|
{REUSE_HINT}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{kind === 'transform' && (
|
{kind === 'transform' && (
|
||||||
<Form.Item
|
<>
|
||||||
label="Transform"
|
<Form.Item
|
||||||
name={[fieldName, 'transform']}
|
label="Transform"
|
||||||
tooltip='An expression tree, e.g. {"op": "concat", "args": [{"type": "str", "bytes": "OK "}, {"reuse": "nonce"}]}'
|
name={[fieldName, 'transform']}
|
||||||
rules={[{ required: true, message: 'enter the transform expression' }]}
|
tooltip={TRANSFORM_HINT}
|
||||||
>
|
rules={[{ required: true, message: 'enter the transform expression' }]}
|
||||||
<JsonObjectInput placeholder='{"op": "concat", "args": [{"reuse": "nonce"}]}' />
|
>
|
||||||
</Form.Item>
|
<JsonObjectInput
|
||||||
|
aria-describedby={`${uid}-transform-hint`}
|
||||||
|
placeholder='{"op": "concat", "args": [{"reuse": "nonce"}]}'
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<span id={`${uid}-transform-hint`} hidden>
|
||||||
|
{TRANSFORM_HINT}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -1910,10 +2001,14 @@ function ItemEditor({
|
|||||||
<Form.Item label="Packet">
|
<Form.Item label="Packet">
|
||||||
<Space.Compact block>
|
<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
|
||||||
|
aria-label="Packet"
|
||||||
|
placeholder="binary data"
|
||||||
|
style={{ width: 'calc(100% - 32px)' }}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button
|
<Button
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
form.setFieldValue([...absoluteItemPath, 'packet'], RandomUtil.randomBase64())
|
form.setFieldValue([...absoluteItemPath, 'packet'], RandomUtil.randomBase64())
|
||||||
@@ -1932,15 +2027,20 @@ function ItemEditor({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{allowVariables && (
|
{allowVariables && (
|
||||||
<Form.Item
|
<>
|
||||||
label="Capture"
|
<Form.Item
|
||||||
name={[fieldName, 'capture']}
|
label="Capture"
|
||||||
tooltip="Saves the bytes this item writes or reads under a name later items can reuse"
|
name={[fieldName, 'capture']}
|
||||||
normalize={(v) => (v === '' ? undefined : v)}
|
tooltip={CAPTURE_HINT}
|
||||||
rules={[{ validator: validateCustomVarName }]}
|
normalize={(v) => (v === '' ? undefined : v)}
|
||||||
>
|
rules={[{ validator: validateCustomVarName }]}
|
||||||
<Input placeholder="variable name" />
|
>
|
||||||
</Form.Item>
|
<Input placeholder="variable name" aria-describedby={`${uid}-capture-hint`} />
|
||||||
|
</Form.Item>
|
||||||
|
<span id={`${uid}-capture-hint`} hidden>
|
||||||
|
{CAPTURE_HINT}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -1955,6 +2055,7 @@ function QuicParamsForm({
|
|||||||
form: FormInstance;
|
form: FormInstance;
|
||||||
showUdpHop: boolean;
|
showUdpHop: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const uid = useId();
|
||||||
const congestion = Form.useWatch([...base, 'congestion'], form) as string | undefined;
|
const congestion = Form.useWatch([...base, 'congestion'], form) as string | undefined;
|
||||||
const udpHop = Form.useWatch([...base, 'udpHop'], { form, preserve: true }) as
|
const udpHop = Form.useWatch([...base, 'udpHop'], { form, preserve: true }) as
|
||||||
| Record<string, unknown>
|
| Record<string, unknown>
|
||||||
@@ -2012,16 +2113,18 @@ function QuicParamsForm({
|
|||||||
itself listens on its own port. */}
|
itself listens on its own port. */}
|
||||||
{showUdpHop && (
|
{showUdpHop && (
|
||||||
<>
|
<>
|
||||||
<Form.Item
|
<Form.Item htmlFor={`${uid}-udp-hop`} label="UDP Hop" tooltip={UDP_HOP_HINT}>
|
||||||
label="UDP Hop"
|
|
||||||
tooltip="Advertised to clients as a hop range; the server listens on its own port only"
|
|
||||||
>
|
|
||||||
<Switch
|
<Switch
|
||||||
|
id={`${uid}-udp-hop`}
|
||||||
|
aria-describedby={`${uid}-udp-hop-hint`}
|
||||||
checked={hasUdpHop}
|
checked={hasUdpHop}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
form.setFieldValue([...base, 'udpHop'], v ? defaultUdpHop() : undefined);
|
form.setFieldValue([...base, 'udpHop'], v ? defaultUdpHop() : undefined);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<span id={`${uid}-udp-hop-hint`} hidden>
|
||||||
|
{UDP_HOP_HINT}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{hasUdpHop && (
|
{hasUdpHop && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import '@/styles/page-cards.css';
|
|||||||
|
|
||||||
import { setupHttp } from '@/api/http-init';
|
import { setupHttp } from '@/api/http-init';
|
||||||
import { readyI18n } from '@/i18n/react';
|
import { readyI18n } from '@/i18n/react';
|
||||||
|
import { AntdRootConfig } from '@/i18n/AntdRootConfig';
|
||||||
import { ThemeProvider } from '@/hooks/useTheme';
|
import { ThemeProvider } from '@/hooks/useTheme';
|
||||||
import { QueryProvider } from '@/api/QueryProvider';
|
import { QueryProvider } from '@/api/QueryProvider';
|
||||||
import { router } from '@/routes';
|
import { router } from '@/routes';
|
||||||
@@ -49,11 +50,13 @@ readyI18n().then(() => {
|
|||||||
const root = document.getElementById('app');
|
const root = document.getElementById('app');
|
||||||
if (root) {
|
if (root) {
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<ThemeProvider>
|
<AntdRootConfig>
|
||||||
<QueryProvider>
|
<ThemeProvider>
|
||||||
<RouterProvider router={router} />
|
<QueryProvider>
|
||||||
</QueryProvider>
|
<RouterProvider router={router} />
|
||||||
</ThemeProvider>,
|
</QueryProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</AntdRootConfig>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export default function BulkAttachInboundsModal({
|
|||||||
onChange={setTargetIds}
|
onChange={setTargetIds}
|
||||||
options={targetOptions}
|
options={targetOptions}
|
||||||
placeholder={t('pages.clients.attachToInboundsTargets')}
|
placeholder={t('pages.clients.attachToInboundsTargets')}
|
||||||
|
aria-label={t('pages.clients.attachToInboundsTargets')}
|
||||||
showSearch={{ optionFilterProp: 'label' }}
|
showSearch={{ optionFilterProp: 'label' }}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export default function BulkDetachInboundsModal({
|
|||||||
onChange={setTargetIds}
|
onChange={setTargetIds}
|
||||||
options={targetOptions}
|
options={targetOptions}
|
||||||
placeholder={t('pages.clients.detachFromInboundsTargets')}
|
placeholder={t('pages.clients.detachFromInboundsTargets')}
|
||||||
|
aria-label={t('pages.clients.detachFromInboundsTargets')}
|
||||||
showSearch={{ optionFilterProp: 'label' }}
|
showSearch={{ optionFilterProp: 'label' }}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useId, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
AutoComplete,
|
AutoComplete,
|
||||||
@@ -82,6 +82,7 @@ export default function ClientBulkAddModal({
|
|||||||
onSaved,
|
onSaved,
|
||||||
}: ClientBulkAddModalProps) {
|
}: ClientBulkAddModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const [messageApi, messageContextHolder] = message.useMessage();
|
const [messageApi, messageContextHolder] = message.useMessage();
|
||||||
const { bulkCreate } = useClients({ list: false });
|
const { bulkCreate } = useClients({ list: false });
|
||||||
|
|
||||||
@@ -254,13 +255,18 @@ export default function ClientBulkAddModal({
|
|||||||
>
|
>
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
|
<Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
|
||||||
<Form.Item label={t('pages.clients.attachedInbounds')} required>
|
<Form.Item
|
||||||
|
label={t('pages.clients.attachedInbounds')}
|
||||||
|
required
|
||||||
|
htmlFor={`${uid}-inbounds`}
|
||||||
|
>
|
||||||
<SelectAllClearButtons
|
<SelectAllClearButtons
|
||||||
options={inboundOptions}
|
options={inboundOptions}
|
||||||
value={inboundIds}
|
value={inboundIds}
|
||||||
onChange={(v) => methods.setValue('inboundIds', v)}
|
onChange={(v) => methods.setValue('inboundIds', v)}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
|
id={`${uid}-inbounds`}
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
value={inboundIds}
|
value={inboundIds}
|
||||||
onChange={(v) => methods.setValue('inboundIds', v)}
|
onChange={(v) => methods.setValue('inboundIds', v)}
|
||||||
@@ -323,16 +329,17 @@ export default function ClientBulkAddModal({
|
|||||||
</FormField>
|
</FormField>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form.Item label={t('pages.clients.subId')}>
|
<Form.Item label={t('pages.clients.subId')} htmlFor={`${uid}-sub-id`}>
|
||||||
<Space.Compact style={{ display: 'flex' }}>
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-sub-id`}
|
||||||
value={subId}
|
value={subId}
|
||||||
onChange={(e) => methods.setValue('subId', e.target.value)}
|
onChange={(e) => methods.setValue('subId', e.target.value)}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
onClick={() => methods.setValue('subId', RandomUtil.randomLowerAndNum(16))}
|
onClick={() => methods.setValue('subId', RandomUtil.randomLowerAndNum(16))}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
@@ -376,10 +383,11 @@ export default function ClientBulkAddModal({
|
|||||||
</FormField>
|
</FormField>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form.Item label={t('pages.clients.limitIp')}>
|
<Form.Item label={t('pages.clients.limitIp')} htmlFor={`${uid}-limit-ip`}>
|
||||||
<Tooltip title={limitIpNotice || undefined}>
|
<Tooltip title={limitIpNotice || undefined}>
|
||||||
<span style={{ display: 'inline-flex' }}>
|
<span style={{ display: 'inline-flex' }}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
id={`${uid}-limit-ip`}
|
||||||
value={limitIp}
|
value={limitIp}
|
||||||
min={0}
|
min={0}
|
||||||
disabled={limitIpDisabled}
|
disabled={limitIpDisabled}
|
||||||
@@ -399,8 +407,9 @@ export default function ClientBulkAddModal({
|
|||||||
<InputNumber min={0} step={1} />
|
<InputNumber min={0} step={1} />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<Form.Item label={t('pages.clients.delayedStart')}>
|
<Form.Item label={t('pages.clients.delayedStart')} htmlFor={`${uid}-delayed-start`}>
|
||||||
<Switch
|
<Switch
|
||||||
|
id={`${uid}-delayed-start`}
|
||||||
checked={delayedStart}
|
checked={delayedStart}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDelayedStart(!delayedStart);
|
setDelayedStart(!delayedStart);
|
||||||
@@ -410,16 +419,18 @@ export default function ClientBulkAddModal({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{delayedStart ? (
|
{delayedStart ? (
|
||||||
<Form.Item label={t('pages.clients.expireDays')}>
|
<Form.Item label={t('pages.clients.expireDays')} htmlFor={`${uid}-expire-days`}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
id={`${uid}-expire-days`}
|
||||||
value={delayedExpireDays}
|
value={delayedExpireDays}
|
||||||
min={0}
|
min={0}
|
||||||
onChange={(v) => methods.setValue('expiryTime', -86400000 * (Number(v) || 0))}
|
onChange={(v) => methods.setValue('expiryTime', -86400000 * (Number(v) || 0))}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
) : (
|
) : (
|
||||||
<Form.Item label={t('pages.inbounds.expireDate')}>
|
<Form.Item label={t('pages.inbounds.expireDate')} htmlFor={`${uid}-expiry`}>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
|
id={`${uid}-expiry`}
|
||||||
value={expiryDate}
|
value={expiryDate}
|
||||||
onChange={(next) => methods.setValue('expiryTime', next ? next.valueOf() : 0)}
|
onChange={(next) => methods.setValue('expiryTime', next ? next.valueOf() : 0)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useId, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
AutoComplete,
|
AutoComplete,
|
||||||
@@ -256,6 +256,7 @@ export default function ClientFormModal({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: ClientFormModalProps) {
|
}: ClientFormModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const [messageApi, messageContextHolder] = message.useMessage();
|
const [messageApi, messageContextHolder] = message.useMessage();
|
||||||
const isEdit = mode === 'edit';
|
const isEdit = mode === 'edit';
|
||||||
|
|
||||||
@@ -820,7 +821,7 @@ export default function ClientFormModal({
|
|||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="danger"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
icon={<RetweetOutlined />}
|
icon={<RetweetOutlined aria-hidden />}
|
||||||
loading={resetting}
|
loading={resetting}
|
||||||
>
|
>
|
||||||
{t('pages.inbounds.resetTraffic')}
|
{t('pages.inbounds.resetTraffic')}
|
||||||
@@ -848,9 +849,14 @@ export default function ClientFormModal({
|
|||||||
<>
|
<>
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Form.Item label={t('pages.clients.email')} required>
|
<Form.Item
|
||||||
|
label={t('pages.clients.email')}
|
||||||
|
required
|
||||||
|
htmlFor={`${uid}-email`}
|
||||||
|
>
|
||||||
<Space.Compact style={{ display: 'flex' }}>
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-email`}
|
||||||
value={email}
|
value={email}
|
||||||
placeholder={t('pages.clients.email')}
|
placeholder={t('pages.clients.email')}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
@@ -859,7 +865,7 @@ export default function ClientFormModal({
|
|||||||
{!isEdit && (
|
{!isEdit && (
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
methods.setValue('email', RandomUtil.randomLowerAndNum(12))
|
methods.setValue('email', RandomUtil.randomLowerAndNum(12))
|
||||||
}
|
}
|
||||||
@@ -880,6 +886,7 @@ export default function ClientFormModal({
|
|||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-limit-ip`}
|
||||||
label={t('pages.clients.limitIp')}
|
label={t('pages.clients.limitIp')}
|
||||||
tooltip={t('pages.clients.limitIpDesc')}
|
tooltip={t('pages.clients.limitIpDesc')}
|
||||||
>
|
>
|
||||||
@@ -887,6 +894,8 @@ export default function ClientFormModal({
|
|||||||
<span style={{ display: 'flex', width: '100%' }}>
|
<span style={{ display: 'flex', width: '100%' }}>
|
||||||
<Space.Compact style={{ display: 'flex', flex: 1 }}>
|
<Space.Compact style={{ display: 'flex', flex: 1 }}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
id={`${uid}-limit-ip`}
|
||||||
|
aria-describedby={`${uid}-limit-ip-hint`}
|
||||||
value={limitIp}
|
value={limitIp}
|
||||||
min={0}
|
min={0}
|
||||||
disabled={limitIpDisabled}
|
disabled={limitIpDisabled}
|
||||||
@@ -900,7 +909,7 @@ export default function ClientFormModal({
|
|||||||
<Tooltip title={t('pages.clients.ipLog')}>
|
<Tooltip title={t('pages.clients.ipLog')}>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('pages.clients.ipLog')}
|
aria-label={t('pages.clients.ipLog')}
|
||||||
icon={<EyeOutlined />}
|
icon={<EyeOutlined aria-hidden />}
|
||||||
loading={ipsLoading}
|
loading={ipsLoading}
|
||||||
onClick={openIpsModal}
|
onClick={openIpsModal}
|
||||||
>
|
>
|
||||||
@@ -911,15 +920,21 @@ export default function ClientFormModal({
|
|||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<span id={`${uid}-limit-ip-hint`} hidden>
|
||||||
|
{t('pages.clients.limitIpDesc')}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-limit-hwid`}
|
||||||
label={t('pages.clients.limitHwid')}
|
label={t('pages.clients.limitHwid')}
|
||||||
tooltip={t('pages.clients.limitHwidDesc')}
|
tooltip={t('pages.clients.limitHwidDesc')}
|
||||||
>
|
>
|
||||||
<Space.Compact style={{ display: 'flex' }}>
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
id={`${uid}-limit-hwid`}
|
||||||
|
aria-describedby={`${uid}-limit-hwid-hint`}
|
||||||
value={limitHwid}
|
value={limitHwid}
|
||||||
min={0}
|
min={0}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
@@ -929,7 +944,7 @@ export default function ClientFormModal({
|
|||||||
<Tooltip title={t('pages.clients.hwidLog')}>
|
<Tooltip title={t('pages.clients.hwidLog')}>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('pages.clients.hwidLog')}
|
aria-label={t('pages.clients.hwidLog')}
|
||||||
icon={<EyeOutlined />}
|
icon={<EyeOutlined aria-hidden />}
|
||||||
loading={hwidsLoading}
|
loading={hwidsLoading}
|
||||||
onClick={openHwidsModal}
|
onClick={openHwidsModal}
|
||||||
>
|
>
|
||||||
@@ -938,6 +953,9 @@ export default function ClientFormModal({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
|
<span id={`${uid}-limit-hwid-hint`} hidden>
|
||||||
|
{t('pages.clients.limitHwidDesc')}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -953,8 +971,12 @@ export default function ClientFormModal({
|
|||||||
<InputNumber min={0} style={{ width: '100%' }} />
|
<InputNumber min={0} style={{ width: '100%' }} />
|
||||||
</FormField>
|
</FormField>
|
||||||
) : (
|
) : (
|
||||||
<Form.Item label={t('pages.clients.expiryTime')}>
|
<Form.Item
|
||||||
|
label={t('pages.clients.expiryTime')}
|
||||||
|
htmlFor={`${uid}-expiry`}
|
||||||
|
>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
|
id={`${uid}-expiry`}
|
||||||
value={expiryDayjs}
|
value={expiryDayjs}
|
||||||
onChange={(d) =>
|
onChange={(d) =>
|
||||||
methods.setValue('expiryDate', d ? d.valueOf() : 0)
|
methods.setValue('expiryDate', d ? d.valueOf() : 0)
|
||||||
@@ -964,8 +986,12 @@ export default function ClientFormModal({
|
|||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} md={6}>
|
<Col xs={12} md={6}>
|
||||||
<Form.Item label={t('pages.clients.delayedStart')}>
|
<Form.Item
|
||||||
|
label={t('pages.clients.delayedStart')}
|
||||||
|
htmlFor={`${uid}-delayed-start`}
|
||||||
|
>
|
||||||
<Switch
|
<Switch
|
||||||
|
id={`${uid}-delayed-start`}
|
||||||
checked={delayedStart}
|
checked={delayedStart}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
methods.setValue('delayedStart', v);
|
methods.setValue('delayedStart', v);
|
||||||
@@ -1062,13 +1088,18 @@ export default function ClientFormModal({
|
|||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form.Item label={t('pages.clients.attachedInbounds')} required={!isEdit}>
|
<Form.Item
|
||||||
|
label={t('pages.clients.attachedInbounds')}
|
||||||
|
required={!isEdit}
|
||||||
|
htmlFor={`${uid}-inbounds`}
|
||||||
|
>
|
||||||
<SelectAllClearButtons
|
<SelectAllClearButtons
|
||||||
options={inboundOptions}
|
options={inboundOptions}
|
||||||
value={inboundIds}
|
value={inboundIds}
|
||||||
onChange={(v) => methods.setValue('inboundIds', v)}
|
onChange={(v) => methods.setValue('inboundIds', v)}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
|
id={`${uid}-inbounds`}
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
value={inboundIds}
|
value={inboundIds}
|
||||||
onChange={(v) => methods.setValue('inboundIds', v)}
|
onChange={(v) => methods.setValue('inboundIds', v)}
|
||||||
@@ -1102,77 +1133,96 @@ export default function ClientFormModal({
|
|||||||
label: t('pages.clients.tabCredentials'),
|
label: t('pages.clients.tabCredentials'),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<Form.Item label={t('pages.clients.uuid')}>
|
<Form.Item label={t('pages.clients.uuid')} htmlFor={`${uid}-uuid`}>
|
||||||
<Space.Compact style={{ display: 'flex' }}>
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-uuid`}
|
||||||
value={uuid}
|
value={uuid}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
onChange={(e) => methods.setValue('uuid', e.target.value)}
|
onChange={(e) => methods.setValue('uuid', e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
onClick={() => methods.setValue('uuid', RandomUtil.randomUUID())}
|
onClick={() => methods.setValue('uuid', RandomUtil.randomUUID())}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-password`}
|
||||||
label={t('pages.clients.password')}
|
label={t('pages.clients.password')}
|
||||||
tooltip={t('pages.clients.passwordDesc')}
|
tooltip={t('pages.clients.passwordDesc')}
|
||||||
>
|
>
|
||||||
<Space.Compact style={{ display: 'flex' }}>
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-password`}
|
||||||
|
aria-describedby={`${uid}-password-hint`}
|
||||||
value={password}
|
value={password}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
onChange={(e) => methods.setValue('password', e.target.value)}
|
onChange={(e) => methods.setValue('password', e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
onClick={regeneratePassword}
|
onClick={regeneratePassword}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
|
<span id={`${uid}-password-hint`} hidden>
|
||||||
|
{t('pages.clients.passwordDesc')}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-sub-id`}
|
||||||
label={t('pages.clients.subId')}
|
label={t('pages.clients.subId')}
|
||||||
tooltip={t('pages.clients.subIdDesc')}
|
tooltip={t('pages.clients.subIdDesc')}
|
||||||
>
|
>
|
||||||
<Space.Compact style={{ display: 'flex' }}>
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-sub-id`}
|
||||||
|
aria-describedby={`${uid}-sub-id-hint`}
|
||||||
value={subId}
|
value={subId}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
onChange={(e) => methods.setValue('subId', e.target.value)}
|
onChange={(e) => methods.setValue('subId', e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
methods.setValue('subId', RandomUtil.randomLowerAndNum(16))
|
methods.setValue('subId', RandomUtil.randomLowerAndNum(16))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
|
<span id={`${uid}-sub-id-hint`} hidden>
|
||||||
|
{t('pages.clients.subIdDesc')}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-auth`}
|
||||||
label={t('pages.clients.hysteriaAuth')}
|
label={t('pages.clients.hysteriaAuth')}
|
||||||
tooltip={t('pages.clients.hysteriaAuthDesc')}
|
tooltip={t('pages.clients.hysteriaAuthDesc')}
|
||||||
>
|
>
|
||||||
<Space.Compact style={{ display: 'flex' }}>
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-auth`}
|
||||||
|
aria-describedby={`${uid}-auth-hint`}
|
||||||
value={auth}
|
value={auth}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
onChange={(e) => methods.setValue('auth', e.target.value)}
|
onChange={(e) => methods.setValue('auth', e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
methods.setValue('auth', RandomUtil.randomLowerAndNum(16))
|
methods.setValue('auth', RandomUtil.randomLowerAndNum(16))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
|
<span id={`${uid}-auth-hint`} hidden>
|
||||||
|
{t('pages.clients.hysteriaAuthDesc')}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{showFlow && (
|
{showFlow && (
|
||||||
@@ -1195,6 +1245,7 @@ export default function ClientFormModal({
|
|||||||
{(showWireguard || showAmneziawg) && (
|
{(showWireguard || showAmneziawg) && (
|
||||||
<>
|
<>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-wg-private-key`}
|
||||||
label={t(
|
label={t(
|
||||||
showAmneziawg
|
showAmneziawg
|
||||||
? 'pages.clients.amneziaWgPrivateKey'
|
? 'pages.clients.amneziaWgPrivateKey'
|
||||||
@@ -1203,6 +1254,7 @@ export default function ClientFormModal({
|
|||||||
>
|
>
|
||||||
<Space.Compact style={{ display: 'flex' }}>
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-wg-private-key`}
|
||||||
value={wgPrivateKey}
|
value={wgPrivateKey}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -1216,7 +1268,7 @@ export default function ClientFormModal({
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
onClick={regenerateWireguardKeys}
|
onClick={regenerateWireguardKeys}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
@@ -1232,6 +1284,7 @@ export default function ClientFormModal({
|
|||||||
<Input disabled />
|
<Input disabled />
|
||||||
</FormField>
|
</FormField>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-wg-psk`}
|
||||||
label={t(
|
label={t(
|
||||||
showAmneziawg
|
showAmneziawg
|
||||||
? 'pages.clients.amneziaWgPreSharedKey'
|
? 'pages.clients.amneziaWgPreSharedKey'
|
||||||
@@ -1240,11 +1293,11 @@ export default function ClientFormModal({
|
|||||||
>
|
>
|
||||||
<Space.Compact style={{ display: 'flex' }}>
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
<FormField name="wgPreSharedKey" noStyle>
|
<FormField name="wgPreSharedKey" noStyle>
|
||||||
<Input style={{ flex: 1 }} />
|
<Input id={`${uid}-wg-psk`} style={{ flex: 1 }} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
onClick={regenerateWireguardPresharedKey}
|
onClick={regenerateWireguardPresharedKey}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
@@ -1305,18 +1358,20 @@ export default function ClientFormModal({
|
|||||||
{showMtproto && (
|
{showMtproto && (
|
||||||
<>
|
<>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-mtproto-secret`}
|
||||||
label={t('pages.clients.mtprotoSecret')}
|
label={t('pages.clients.mtprotoSecret')}
|
||||||
extra={t('pages.clients.mtprotoSecretHint')}
|
extra={t('pages.clients.mtprotoSecretHint')}
|
||||||
>
|
>
|
||||||
<Space.Compact style={{ display: 'flex' }}>
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-mtproto-secret`}
|
||||||
value={secret}
|
value={secret}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
onChange={(e) => methods.setValue('secret', e.target.value)}
|
onChange={(e) => methods.setValue('secret', e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
onClick={regenerateMtprotoSecret}
|
onClick={regenerateMtprotoSecret}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
@@ -1344,7 +1399,7 @@ export default function ClientFormModal({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
onClick={() => addExternalLinkRow('link')}
|
onClick={() => addExternalLinkRow('link')}
|
||||||
>
|
>
|
||||||
{t('pages.clients.addExternalLink')}
|
{t('pages.clients.addExternalLink')}
|
||||||
@@ -1364,7 +1419,7 @@ export default function ClientFormModal({
|
|||||||
valueProp="checked"
|
valueProp="checked"
|
||||||
noStyle
|
noStyle
|
||||||
>
|
>
|
||||||
<Switch size="small" />
|
<Switch size="small" aria-label={t('enable')} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<span>{t('enable')}</span>
|
<span>{t('enable')}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1378,7 +1433,7 @@ export default function ClientFormModal({
|
|||||||
<Button
|
<Button
|
||||||
aria-label={t('delete')}
|
aria-label={t('delete')}
|
||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined aria-hidden />}
|
||||||
onClick={() => removeExternalLink(index)}
|
onClick={() => removeExternalLink(index)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -1398,6 +1453,7 @@ export default function ClientFormModal({
|
|||||||
const hasSpecificExpiry = Number(expiryField.value) > 0;
|
const hasSpecificExpiry = Number(expiryField.value) > 0;
|
||||||
return (
|
return (
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
|
aria-label={t('pages.clients.expiryTime')}
|
||||||
value={displayedExpiry > 0 ? dayjs(displayedExpiry) : null}
|
value={displayedExpiry > 0 ? dayjs(displayedExpiry) : null}
|
||||||
onChange={(v) => expiryField.onChange(v ? v.valueOf() : 0)}
|
onChange={(v) => expiryField.onChange(v ? v.valueOf() : 0)}
|
||||||
placeholder={t('pages.inbounds.leaveBlankToNeverExpire')}
|
placeholder={t('pages.inbounds.leaveBlankToNeverExpire')}
|
||||||
@@ -1415,7 +1471,7 @@ export default function ClientFormModal({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
onClick={() => addExternalLinkRow('subscription')}
|
onClick={() => addExternalLinkRow('subscription')}
|
||||||
>
|
>
|
||||||
{t('pages.clients.addExternalSubscription')}
|
{t('pages.clients.addExternalSubscription')}
|
||||||
@@ -1435,7 +1491,7 @@ export default function ClientFormModal({
|
|||||||
valueProp="checked"
|
valueProp="checked"
|
||||||
noStyle
|
noStyle
|
||||||
>
|
>
|
||||||
<Switch size="small" />
|
<Switch size="small" aria-label={t('enable')} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<span>{t('enable')}</span>
|
<span>{t('enable')}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1449,7 +1505,7 @@ export default function ClientFormModal({
|
|||||||
<Button
|
<Button
|
||||||
aria-label={t('delete')}
|
aria-label={t('delete')}
|
||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined aria-hidden />}
|
||||||
onClick={() => removeExternalLink(index)}
|
onClick={() => removeExternalLink(index)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -1475,6 +1531,7 @@ export default function ClientFormModal({
|
|||||||
const hasSpecificExpiry = Number(expiryField.value) > 0;
|
const hasSpecificExpiry = Number(expiryField.value) > 0;
|
||||||
return (
|
return (
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
|
aria-label={t('pages.clients.expiryTime')}
|
||||||
value={displayedExpiry > 0 ? dayjs(displayedExpiry) : null}
|
value={displayedExpiry > 0 ? dayjs(displayedExpiry) : null}
|
||||||
onChange={(v) => expiryField.onChange(v ? v.valueOf() : 0)}
|
onChange={(v) => expiryField.onChange(v ? v.valueOf() : 0)}
|
||||||
placeholder={t('pages.inbounds.leaveBlankToNeverExpire')}
|
placeholder={t('pages.inbounds.leaveBlankToNeverExpire')}
|
||||||
@@ -1515,7 +1572,12 @@ export default function ClientFormModal({
|
|||||||
zIndex={CLIENT_IP_LOG_MODAL_Z_INDEX}
|
zIndex={CLIENT_IP_LOG_MODAL_Z_INDEX}
|
||||||
onCancel={() => setIpsModalOpen(false)}
|
onCancel={() => setIpsModalOpen(false)}
|
||||||
footer={[
|
footer={[
|
||||||
<Button key="refresh" icon={<ReloadOutlined />} loading={ipsLoading} onClick={loadIps}>
|
<Button
|
||||||
|
key="refresh"
|
||||||
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
|
loading={ipsLoading}
|
||||||
|
onClick={loadIps}
|
||||||
|
>
|
||||||
{t('refresh')}
|
{t('refresh')}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ export default function ClientInfoModal({
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyValue(client.subId!)}
|
onClick={() => copyValue(client.subId!)}
|
||||||
/>
|
/>
|
||||||
@@ -365,7 +365,7 @@ export default function ClientInfoModal({
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyValue(client.uuid!)}
|
onClick={() => copyValue(client.uuid!)}
|
||||||
/>
|
/>
|
||||||
@@ -380,7 +380,7 @@ export default function ClientInfoModal({
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyValue(client.password!)}
|
onClick={() => copyValue(client.password!)}
|
||||||
/>
|
/>
|
||||||
@@ -395,7 +395,7 @@ export default function ClientInfoModal({
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyValue(client.auth!)}
|
onClick={() => copyValue(client.auth!)}
|
||||||
/>
|
/>
|
||||||
@@ -457,7 +457,7 @@ export default function ClientInfoModal({
|
|||||||
<td>
|
<td>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<EyeOutlined />}
|
icon={<EyeOutlined aria-hidden />}
|
||||||
aria-label={t('pages.clients.ipLog')}
|
aria-label={t('pages.clients.ipLog')}
|
||||||
loading={ipsLoading}
|
loading={ipsLoading}
|
||||||
onClick={openIpsModal}
|
onClick={openIpsModal}
|
||||||
@@ -489,7 +489,7 @@ export default function ClientInfoModal({
|
|||||||
<td>
|
<td>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<EyeOutlined />}
|
icon={<EyeOutlined aria-hidden />}
|
||||||
aria-label={t('pages.clients.hwidLog')}
|
aria-label={t('pages.clients.hwidLog')}
|
||||||
loading={hwidsLoading}
|
loading={hwidsLoading}
|
||||||
onClick={openHwidsModal}
|
onClick={openHwidsModal}
|
||||||
@@ -591,7 +591,7 @@ export default function ClientInfoModal({
|
|||||||
<Tooltip title={t('copy')}>
|
<Tooltip title={t('copy')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyValue(subLink)}
|
onClick={() => copyValue(subLink)}
|
||||||
/>
|
/>
|
||||||
@@ -599,7 +599,7 @@ export default function ClientInfoModal({
|
|||||||
<Tooltip title={t('download')}>
|
<Tooltip title={t('download')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined aria-hidden />}
|
||||||
aria-label={t('download')}
|
aria-label={t('download')}
|
||||||
loading={downloadingFormat === 'standard'}
|
loading={downloadingFormat === 'standard'}
|
||||||
disabled={downloadingFormat !== null}
|
disabled={downloadingFormat !== null}
|
||||||
@@ -621,7 +621,7 @@ export default function ClientInfoModal({
|
|||||||
<Tooltip title={t('pages.clients.qrCode')}>
|
<Tooltip title={t('pages.clients.qrCode')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<QrcodeOutlined />}
|
icon={<QrcodeOutlined aria-hidden />}
|
||||||
aria-label={t('pages.clients.qrCode')}
|
aria-label={t('pages.clients.qrCode')}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -646,7 +646,7 @@ export default function ClientInfoModal({
|
|||||||
<Tooltip title={t('copy')}>
|
<Tooltip title={t('copy')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyValue(subJsonLink)}
|
onClick={() => copyValue(subJsonLink)}
|
||||||
/>
|
/>
|
||||||
@@ -654,7 +654,7 @@ export default function ClientInfoModal({
|
|||||||
<Tooltip title={t('download')}>
|
<Tooltip title={t('download')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined aria-hidden />}
|
||||||
aria-label={t('download')}
|
aria-label={t('download')}
|
||||||
loading={downloadingFormat === 'json'}
|
loading={downloadingFormat === 'json'}
|
||||||
disabled={downloadingFormat !== null}
|
disabled={downloadingFormat !== null}
|
||||||
@@ -676,7 +676,7 @@ export default function ClientInfoModal({
|
|||||||
<Tooltip title={t('pages.clients.qrCode')}>
|
<Tooltip title={t('pages.clients.qrCode')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<QrcodeOutlined />}
|
icon={<QrcodeOutlined aria-hidden />}
|
||||||
aria-label={t('pages.clients.qrCode')}
|
aria-label={t('pages.clients.qrCode')}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -704,7 +704,7 @@ export default function ClientInfoModal({
|
|||||||
<Tooltip title={t('copy')}>
|
<Tooltip title={t('copy')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyValue(subClashLink)}
|
onClick={() => copyValue(subClashLink)}
|
||||||
/>
|
/>
|
||||||
@@ -712,7 +712,7 @@ export default function ClientInfoModal({
|
|||||||
<Tooltip title={t('download')}>
|
<Tooltip title={t('download')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined aria-hidden />}
|
||||||
aria-label={t('download')}
|
aria-label={t('download')}
|
||||||
loading={downloadingFormat === 'clash'}
|
loading={downloadingFormat === 'clash'}
|
||||||
disabled={downloadingFormat !== null}
|
disabled={downloadingFormat !== null}
|
||||||
@@ -734,7 +734,7 @@ export default function ClientInfoModal({
|
|||||||
<Tooltip title={t('pages.clients.qrCode')}>
|
<Tooltip title={t('pages.clients.qrCode')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<QrcodeOutlined />}
|
icon={<QrcodeOutlined aria-hidden />}
|
||||||
aria-label={t('pages.clients.qrCode')}
|
aria-label={t('pages.clients.qrCode')}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -768,7 +768,7 @@ export default function ClientInfoModal({
|
|||||||
<Tooltip title={t('copy')}>
|
<Tooltip title={t('copy')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyValue(link)}
|
onClick={() => copyValue(link)}
|
||||||
/>
|
/>
|
||||||
@@ -783,7 +783,7 @@ export default function ClientInfoModal({
|
|||||||
<Tooltip title={t('pages.clients.qrCode')}>
|
<Tooltip title={t('pages.clients.qrCode')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<QrcodeOutlined />}
|
icon={<QrcodeOutlined aria-hidden />}
|
||||||
aria-label={t('pages.clients.qrCode')}
|
aria-label={t('pages.clients.qrCode')}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -853,7 +853,12 @@ export default function ClientInfoModal({
|
|||||||
width={440}
|
width={440}
|
||||||
onCancel={() => setIpsModalOpen(false)}
|
onCancel={() => setIpsModalOpen(false)}
|
||||||
footer={[
|
footer={[
|
||||||
<Button key="refresh" icon={<ReloadOutlined />} loading={ipsLoading} onClick={loadIps}>
|
<Button
|
||||||
|
key="refresh"
|
||||||
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
|
loading={ipsLoading}
|
||||||
|
onClick={loadIps}
|
||||||
|
>
|
||||||
{t('refresh')}
|
{t('refresh')}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ function UngroupIcon() {
|
|||||||
height: '1em',
|
height: '1em',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TagsOutlined />
|
<TagsOutlined aria-hidden />
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
style={{
|
style={{
|
||||||
@@ -1081,6 +1081,7 @@ export default function ClientsPage() {
|
|||||||
checked={!!record.enable}
|
checked={!!record.enable}
|
||||||
size="small"
|
size="small"
|
||||||
loading={togglingEmail === record.email}
|
loading={togglingEmail === record.email}
|
||||||
|
aria-label={t('pages.clients.enabled')}
|
||||||
onChange={(next) => onToggleEnable(record, next)}
|
onChange={(next) => onToggleEnable(record, next)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -1140,16 +1141,20 @@ export default function ClientsPage() {
|
|||||||
render: (_v, record) => {
|
render: (_v, record) => {
|
||||||
if (!record.group) return <Typography.Text type="secondary">—</Typography.Text>;
|
if (!record.group) return <Typography.Text type="secondary">—</Typography.Text>;
|
||||||
const isActive = filters.groups.includes(record.group);
|
const isActive = filters.groups.includes(record.group);
|
||||||
|
const filterByGroup = () => {
|
||||||
|
if (!isActive) setFilters({ ...filters, groups: [...filters.groups, record.group!] });
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
color="geekblue"
|
color="geekblue"
|
||||||
style={{ margin: 0, cursor: 'pointer', opacity: isActive ? 0.6 : 1 }}
|
style={{ margin: 0, cursor: 'pointer', opacity: isActive ? 0.6 : 1 }}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!isActive) {
|
filterByGroup();
|
||||||
setFilters({ ...filters, groups: [...filters.groups, record.group!] });
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={activateOnKey(filterByGroup)}
|
||||||
>
|
>
|
||||||
{record.group}
|
{record.group}
|
||||||
</Tag>
|
</Tag>
|
||||||
@@ -1327,7 +1332,7 @@ export default function ClientsPage() {
|
|||||||
<SummaryStat
|
<SummaryStat
|
||||||
title={t('clients')}
|
title={t('clients')}
|
||||||
value={summary.total}
|
value={summary.total}
|
||||||
prefix={<TeamOutlined />}
|
prefix={<TeamOutlined aria-hidden />}
|
||||||
onSelect={() => selectBucket(null)}
|
onSelect={() => selectBucket(null)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -1393,7 +1398,7 @@ export default function ClientsPage() {
|
|||||||
{selectedRowKeys.length === 0 ? (
|
{selectedRowKeys.length === 0 ? (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
onClick={onAdd}
|
onClick={onAdd}
|
||||||
aria-label={t('pages.clients.addClients')}
|
aria-label={t('pages.clients.addClients')}
|
||||||
>
|
>
|
||||||
@@ -1418,20 +1423,20 @@ export default function ClientsPage() {
|
|||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: 'attach',
|
key: 'attach',
|
||||||
icon: <UsergroupAddOutlined />,
|
icon: <UsergroupAddOutlined aria-hidden />,
|
||||||
label: t('pages.clients.attach'),
|
label: t('pages.clients.attach'),
|
||||||
onClick: () => setBulkAttachOpen(true),
|
onClick: () => setBulkAttachOpen(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'detach',
|
key: 'detach',
|
||||||
icon: <UsergroupDeleteOutlined />,
|
icon: <UsergroupDeleteOutlined aria-hidden />,
|
||||||
label: t('pages.clients.detach'),
|
label: t('pages.clients.detach'),
|
||||||
danger: true,
|
danger: true,
|
||||||
onClick: () => setBulkDetachOpen(true),
|
onClick: () => setBulkDetachOpen(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'addToGroup',
|
key: 'addToGroup',
|
||||||
icon: <TagsOutlined />,
|
icon: <TagsOutlined aria-hidden />,
|
||||||
label: t('pages.clients.addToGroup'),
|
label: t('pages.clients.addToGroup'),
|
||||||
onClick: () => setBulkGroupOpen(true),
|
onClick: () => setBulkGroupOpen(true),
|
||||||
},
|
},
|
||||||
@@ -1445,26 +1450,26 @@ export default function ClientsPage() {
|
|||||||
{ type: 'divider' as const },
|
{ type: 'divider' as const },
|
||||||
{
|
{
|
||||||
key: 'enable',
|
key: 'enable',
|
||||||
icon: <CheckCircleOutlined />,
|
icon: <CheckCircleOutlined aria-hidden />,
|
||||||
label: t('pages.clients.enable'),
|
label: t('pages.clients.enable'),
|
||||||
onClick: () => onBulkSetEnable(true),
|
onClick: () => onBulkSetEnable(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'disable',
|
key: 'disable',
|
||||||
icon: <StopOutlined />,
|
icon: <StopOutlined aria-hidden />,
|
||||||
label: t('pages.clients.disable'),
|
label: t('pages.clients.disable'),
|
||||||
danger: true,
|
danger: true,
|
||||||
onClick: () => onBulkSetEnable(false),
|
onClick: () => onBulkSetEnable(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'adjust',
|
key: 'adjust',
|
||||||
icon: <ClockCircleOutlined />,
|
icon: <ClockCircleOutlined aria-hidden />,
|
||||||
label: t('pages.clients.adjust'),
|
label: t('pages.clients.adjust'),
|
||||||
onClick: () => setBulkAdjustOpen(true),
|
onClick: () => setBulkAdjustOpen(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'subLinks',
|
key: 'subLinks',
|
||||||
icon: <LinkOutlined />,
|
icon: <LinkOutlined aria-hidden />,
|
||||||
label: t('pages.clients.subLinks'),
|
label: t('pages.clients.subLinks'),
|
||||||
onClick: () => setSubLinksOpen(true),
|
onClick: () => setSubLinksOpen(true),
|
||||||
},
|
},
|
||||||
@@ -1472,39 +1477,39 @@ export default function ClientsPage() {
|
|||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
key: 'bulk',
|
key: 'bulk',
|
||||||
icon: <UsergroupAddOutlined />,
|
icon: <UsergroupAddOutlined aria-hidden />,
|
||||||
label: t('pages.clients.bulk'),
|
label: t('pages.clients.bulk'),
|
||||||
onClick: () => setBulkAddOpen(true),
|
onClick: () => setBulkAddOpen(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'export',
|
key: 'export',
|
||||||
icon: <DownloadOutlined />,
|
icon: <DownloadOutlined aria-hidden />,
|
||||||
label: t('pages.clients.exportClients'),
|
label: t('pages.clients.exportClients'),
|
||||||
onClick: onExportClients,
|
onClick: onExportClients,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'import',
|
key: 'import',
|
||||||
icon: <UploadOutlined />,
|
icon: <UploadOutlined aria-hidden />,
|
||||||
label: t('pages.clients.importClients'),
|
label: t('pages.clients.importClients'),
|
||||||
onClick: onImportClients,
|
onClick: onImportClients,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'resetAll',
|
key: 'resetAll',
|
||||||
icon: <RetweetOutlined />,
|
icon: <RetweetOutlined aria-hidden />,
|
||||||
label: t('pages.clients.resetAllTraffics'),
|
label: t('pages.clients.resetAllTraffics'),
|
||||||
onClick: onResetAllTraffics,
|
onClick: onResetAllTraffics,
|
||||||
},
|
},
|
||||||
{ type: 'divider' as const },
|
{ type: 'divider' as const },
|
||||||
{
|
{
|
||||||
key: 'delDepleted',
|
key: 'delDepleted',
|
||||||
icon: <RestOutlined />,
|
icon: <RestOutlined aria-hidden />,
|
||||||
label: t('pages.clients.delDepleted'),
|
label: t('pages.clients.delDepleted'),
|
||||||
danger: true,
|
danger: true,
|
||||||
onClick: onDelDepleted,
|
onClick: onDelDepleted,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'delOrphans',
|
key: 'delOrphans',
|
||||||
icon: <DisconnectOutlined />,
|
icon: <DisconnectOutlined aria-hidden />,
|
||||||
label: t('pages.clients.delOrphans'),
|
label: t('pages.clients.delOrphans'),
|
||||||
danger: true,
|
danger: true,
|
||||||
onClick: onDeleteOrphans,
|
onClick: onDeleteOrphans,
|
||||||
@@ -1512,14 +1517,14 @@ export default function ClientsPage() {
|
|||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button icon={<MoreOutlined />} aria-label={t('more')}>
|
<Button icon={<MoreOutlined aria-hidden />} aria-label={t('more')}>
|
||||||
{!isMobile && t('more')}
|
{!isMobile && t('more')}
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
{selectedRowKeys.length > 0 && (
|
{selectedRowKeys.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined aria-hidden />}
|
||||||
onClick={onBulkDelete}
|
onClick={onBulkDelete}
|
||||||
style={{ marginInlineStart: 'auto' }}
|
style={{ marginInlineStart: 'auto' }}
|
||||||
aria-label={t('delete')}
|
aria-label={t('delete')}
|
||||||
@@ -1536,14 +1541,14 @@ export default function ClientsPage() {
|
|||||||
onChange={(e) => setSearchKey(e.target.value)}
|
onChange={(e) => setSearchKey(e.target.value)}
|
||||||
placeholder={t('pages.clients.searchPlaceholder')}
|
placeholder={t('pages.clients.searchPlaceholder')}
|
||||||
allowClear
|
allowClear
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined aria-hidden />}
|
||||||
size={isMobile ? 'small' : 'middle'}
|
size={isMobile ? 'small' : 'middle'}
|
||||||
style={{ maxWidth: 320 }}
|
style={{ maxWidth: 320 }}
|
||||||
aria-label={t('search')}
|
aria-label={t('search')}
|
||||||
/>
|
/>
|
||||||
<Badge count={activeCount} size="small" offset={[-4, 4]}>
|
<Badge count={activeCount} size="small" offset={[-4, 4]}>
|
||||||
<Button
|
<Button
|
||||||
icon={<FilterOutlined />}
|
icon={<FilterOutlined aria-hidden />}
|
||||||
size={isMobile ? 'small' : 'middle'}
|
size={isMobile ? 'small' : 'middle'}
|
||||||
onClick={() => setFilterDrawerOpen(true)}
|
onClick={() => setFilterDrawerOpen(true)}
|
||||||
type={activeCount > 0 ? 'primary' : 'default'}
|
type={activeCount > 0 ? 'primary' : 'default'}
|
||||||
@@ -1556,7 +1561,7 @@ export default function ClientsPage() {
|
|||||||
value={sortValueFor(sortColumn, sortOrder)}
|
value={sortValueFor(sortColumn, sortOrder)}
|
||||||
aria-label={t('sort')}
|
aria-label={t('sort')}
|
||||||
size={isMobile ? 'small' : 'middle'}
|
size={isMobile ? 'small' : 'middle'}
|
||||||
suffix={<SortAscendingOutlined />}
|
suffix={<SortAscendingOutlined aria-hidden />}
|
||||||
style={{ minWidth: isMobile ? 130 : 200 }}
|
style={{ minWidth: isMobile ? 130 : 200 }}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
const opt = SORT_OPTIONS.find((o) => o.value === value);
|
const opt = SORT_OPTIONS.find((o) => o.value === value);
|
||||||
@@ -1709,7 +1714,10 @@ export default function ClientsPage() {
|
|||||||
locale={{
|
locale={{
|
||||||
emptyText: (
|
emptyText: (
|
||||||
<div className="clients-empty">
|
<div className="clients-empty">
|
||||||
<TeamOutlined style={{ fontSize: 32, marginBottom: 8 }} />
|
<TeamOutlined
|
||||||
|
aria-hidden
|
||||||
|
style={{ fontSize: 32, marginBottom: 8 }}
|
||||||
|
/>
|
||||||
<div>{t('noData')}</div>
|
<div>{t('noData')}</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -1734,7 +1742,7 @@ export default function ClientsPage() {
|
|||||||
)}
|
)}
|
||||||
{filteredClients.length === 0 && (
|
{filteredClients.length === 0 && (
|
||||||
<div className="card-empty">
|
<div className="card-empty">
|
||||||
<TeamOutlined style={{ fontSize: 28, opacity: 0.5 }} />
|
<TeamOutlined aria-hidden style={{ fontSize: 28, opacity: 0.5 }} />
|
||||||
<div>{t('noData')}</div>
|
<div>{t('noData')}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1799,6 +1807,7 @@ export default function ClientsPage() {
|
|||||||
checked={!!row.enable}
|
checked={!!row.enable}
|
||||||
size="small"
|
size="small"
|
||||||
loading={togglingEmail === row.email}
|
loading={togglingEmail === row.email}
|
||||||
|
aria-label={t('pages.clients.enabled')}
|
||||||
onChange={(next) => onToggleEnable(row, next)}
|
onChange={(next) => onToggleEnable(row, next)}
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -1810,7 +1819,8 @@ export default function ClientsPage() {
|
|||||||
key: 'qr',
|
key: 'qr',
|
||||||
label: (
|
label: (
|
||||||
<>
|
<>
|
||||||
<QrcodeOutlined /> {t('pages.clients.qrCode')}
|
<QrcodeOutlined aria-hidden />{' '}
|
||||||
|
{t('pages.clients.qrCode')}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
onClick: () => onShowQr(row.email),
|
onClick: () => onShowQr(row.email),
|
||||||
@@ -1819,7 +1829,7 @@ export default function ClientsPage() {
|
|||||||
key: 'reset',
|
key: 'reset',
|
||||||
label: (
|
label: (
|
||||||
<>
|
<>
|
||||||
<RetweetOutlined />{' '}
|
<RetweetOutlined aria-hidden />{' '}
|
||||||
{t('pages.inbounds.resetTraffic')}
|
{t('pages.inbounds.resetTraffic')}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -1829,7 +1839,7 @@ export default function ClientsPage() {
|
|||||||
key: 'edit',
|
key: 'edit',
|
||||||
label: (
|
label: (
|
||||||
<>
|
<>
|
||||||
<EditOutlined /> {t('edit')}
|
<EditOutlined aria-hidden /> {t('edit')}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
onClick: () => onEdit(row.email),
|
onClick: () => onEdit(row.email),
|
||||||
@@ -1839,7 +1849,7 @@ export default function ClientsPage() {
|
|||||||
danger: true,
|
danger: true,
|
||||||
label: (
|
label: (
|
||||||
<>
|
<>
|
||||||
<DeleteOutlined /> {t('delete')}
|
<DeleteOutlined aria-hidden /> {t('delete')}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
onClick: () => onDelete(row.email),
|
onClick: () => onDelete(row.email),
|
||||||
@@ -1851,7 +1861,7 @@ export default function ClientsPage() {
|
|||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
className="row-action-trigger"
|
className="row-action-trigger"
|
||||||
icon={<MoreOutlined />}
|
icon={<MoreOutlined aria-hidden />}
|
||||||
aria-label={t('more')}
|
aria-label={t('more')}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo } from 'react';
|
import { useId, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -46,6 +46,7 @@ export default function FilterDrawer({
|
|||||||
nodes,
|
nodes,
|
||||||
}: FilterDrawerProps) {
|
}: FilterDrawerProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
|
|
||||||
function patch<K extends keyof ClientFilters>(key: K, value: ClientFilters[K]) {
|
function patch<K extends keyof ClientFilters>(key: K, value: ClientFilters[K]) {
|
||||||
onChange({ ...filters, [key]: value });
|
onChange({ ...filters, [key]: value });
|
||||||
@@ -113,8 +114,9 @@ export default function FilterDrawer({
|
|||||||
</Checkbox.Group>
|
</Checkbox.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t('pages.inbounds.protocol')}>
|
<Form.Item label={t('pages.inbounds.protocol')} htmlFor={`${uid}-protocols`}>
|
||||||
<Select
|
<Select
|
||||||
|
id={`${uid}-protocols`}
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
value={filters.protocols}
|
value={filters.protocols}
|
||||||
onChange={(v) => patch('protocols', v as string[])}
|
onChange={(v) => patch('protocols', v as string[])}
|
||||||
@@ -125,8 +127,9 @@ export default function FilterDrawer({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t('inbounds')}>
|
<Form.Item label={t('inbounds')} htmlFor={`${uid}-inbounds`}>
|
||||||
<Select
|
<Select
|
||||||
|
id={`${uid}-inbounds`}
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
value={filters.inboundIds}
|
value={filters.inboundIds}
|
||||||
onChange={(v) => patch('inboundIds', v as number[])}
|
onChange={(v) => patch('inboundIds', v as number[])}
|
||||||
@@ -140,8 +143,9 @@ export default function FilterDrawer({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{nodes.length > 0 && (
|
{nodes.length > 0 && (
|
||||||
<Form.Item label={t('pages.clients.filters.nodes')}>
|
<Form.Item label={t('pages.clients.filters.nodes')} htmlFor={`${uid}-nodes`}>
|
||||||
<Select
|
<Select
|
||||||
|
id={`${uid}-nodes`}
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
value={filters.nodeIds}
|
value={filters.nodeIds}
|
||||||
onChange={(v) => patch('nodeIds', v as number[])}
|
onChange={(v) => patch('nodeIds', v as number[])}
|
||||||
@@ -155,8 +159,9 @@ export default function FilterDrawer({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form.Item label={t('pages.clients.group')}>
|
<Form.Item label={t('pages.clients.group')} htmlFor={`${uid}-groups`}>
|
||||||
<Select
|
<Select
|
||||||
|
id={`${uid}-groups`}
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
value={filters.groups}
|
value={filters.groups}
|
||||||
onChange={(v) => patch('groups', v as string[])}
|
onChange={(v) => patch('groups', v as string[])}
|
||||||
@@ -169,8 +174,9 @@ export default function FilterDrawer({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t('pages.clients.expiryTime')}>
|
<Form.Item label={t('pages.clients.expiryTime')} htmlFor={`${uid}-expiry`}>
|
||||||
<DatePicker.RangePicker
|
<DatePicker.RangePicker
|
||||||
|
id={`${uid}-expiry`}
|
||||||
value={dateRange}
|
value={dateRange}
|
||||||
onChange={(range) => {
|
onChange={(range) => {
|
||||||
const from = range?.[0]?.startOf('day').valueOf();
|
const from = range?.[0]?.startOf('day').valueOf();
|
||||||
@@ -182,10 +188,11 @@ export default function FilterDrawer({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={`${t('pages.clients.traffic')} (GB)`}>
|
<Form.Item label={`${t('pages.clients.traffic')} (GB)`} htmlFor={`${uid}-usage-from`}>
|
||||||
<Row gutter={8}>
|
<Row gutter={8}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
id={`${uid}-usage-from`}
|
||||||
value={filters.usageFromGB}
|
value={filters.usageFromGB}
|
||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
@@ -196,6 +203,7 @@ export default function FilterDrawer({
|
|||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
aria-label={`${t('pages.clients.traffic')} (GB) ${t('to')}`}
|
||||||
value={filters.usageToGB}
|
value={filters.usageToGB}
|
||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const ClientRowActions = memo(function ClientRowActions({
|
|||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
style={ICON_BUTTON_STYLE}
|
style={ICON_BUTTON_STYLE}
|
||||||
icon={<QrcodeOutlined />}
|
icon={<QrcodeOutlined aria-hidden />}
|
||||||
aria-label={t('pages.clients.qrCode')}
|
aria-label={t('pages.clients.qrCode')}
|
||||||
onClick={() => onShowQr(email)}
|
onClick={() => onShowQr(email)}
|
||||||
/>
|
/>
|
||||||
@@ -54,7 +54,7 @@ export const ClientRowActions = memo(function ClientRowActions({
|
|||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
style={ICON_BUTTON_STYLE}
|
style={ICON_BUTTON_STYLE}
|
||||||
icon={<InfoCircleOutlined />}
|
icon={<InfoCircleOutlined aria-hidden />}
|
||||||
aria-label={t('pages.clients.clientInfo')}
|
aria-label={t('pages.clients.clientInfo')}
|
||||||
onClick={() => onShowInfo(email)}
|
onClick={() => onShowInfo(email)}
|
||||||
/>
|
/>
|
||||||
@@ -64,7 +64,7 @@ export const ClientRowActions = memo(function ClientRowActions({
|
|||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
style={ICON_BUTTON_STYLE}
|
style={ICON_BUTTON_STYLE}
|
||||||
icon={<RetweetOutlined />}
|
icon={<RetweetOutlined aria-hidden />}
|
||||||
aria-label={t('pages.inbounds.resetTraffic')}
|
aria-label={t('pages.inbounds.resetTraffic')}
|
||||||
onClick={() => onResetTraffic(email)}
|
onClick={() => onResetTraffic(email)}
|
||||||
/>
|
/>
|
||||||
@@ -74,7 +74,7 @@ export const ClientRowActions = memo(function ClientRowActions({
|
|||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
style={ICON_BUTTON_STYLE}
|
style={ICON_BUTTON_STYLE}
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined aria-hidden />}
|
||||||
aria-label={t('edit')}
|
aria-label={t('edit')}
|
||||||
onClick={() => onEdit(email)}
|
onClick={() => onEdit(email)}
|
||||||
/>
|
/>
|
||||||
@@ -85,7 +85,7 @@ export const ClientRowActions = memo(function ClientRowActions({
|
|||||||
type="text"
|
type="text"
|
||||||
danger
|
danger
|
||||||
style={ICON_BUTTON_STYLE}
|
style={ICON_BUTTON_STYLE}
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined aria-hidden />}
|
||||||
aria-label={t('delete')}
|
aria-label={t('delete')}
|
||||||
onClick={() => onDelete(email)}
|
onClick={() => onDelete(email)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export default function SubLinksModal({
|
|||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
onClick={() => copy(row.link, t('copied'))}
|
onClick={() => copy(row.link, t('copied'))}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -157,7 +157,7 @@ export default function SubLinksModal({
|
|||||||
<Button onClick={() => onOpenChange(false)}>{t('close')}</Button>
|
<Button onClick={() => onOpenChange(false)}>{t('close')}</Button>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
<Button
|
<Button
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
disabled={rows.length === 0}
|
disabled={rows.length === 0}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
copy(allText, t('pages.clients.subLinksCopiedAll', { count: rows.length }))
|
copy(allText, t('pages.clients.subLinksCopiedAll', { count: rows.length }))
|
||||||
@@ -167,7 +167,7 @@ export default function SubLinksModal({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined aria-hidden />}
|
||||||
disabled={rows.length === 0}
|
disabled={rows.length === 0}
|
||||||
onClick={download}
|
onClick={download}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export default function GroupAddClientsModal({
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
placeholder={t('pages.inbounds.attachClientsSearchPlaceholder')}
|
placeholder={t('pages.inbounds.attachClientsSearchPlaceholder')}
|
||||||
|
aria-label={t('pages.inbounds.attachClientsSearchPlaceholder')}
|
||||||
style={{ maxWidth: 320 }}
|
style={{ maxWidth: 320 }}
|
||||||
/>
|
/>
|
||||||
<Typography.Text type="secondary">
|
<Typography.Text type="secondary">
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export default function GroupRemoveClientsModal({
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
placeholder={t('pages.inbounds.attachClientsSearchPlaceholder')}
|
placeholder={t('pages.inbounds.attachClientsSearchPlaceholder')}
|
||||||
|
aria-label={t('pages.inbounds.attachClientsSearchPlaceholder')}
|
||||||
style={{ maxWidth: 320 }}
|
style={{ maxWidth: 320 }}
|
||||||
/>
|
/>
|
||||||
<Typography.Text type="secondary">
|
<Typography.Text type="secondary">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
|
import { lazy, useCallback, useEffect, useId, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -89,6 +89,7 @@ async function fetchEmailsForGroup(name: string): Promise<string[]> {
|
|||||||
export default function GroupsPage() {
|
export default function GroupsPage() {
|
||||||
usePageTitle();
|
usePageTitle();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const { isDark, isUltra, antdThemeConfig } = useTheme();
|
const { isDark, isUltra, antdThemeConfig } = useTheme();
|
||||||
const { isMobile } = useMediaQuery();
|
const { isMobile } = useMediaQuery();
|
||||||
const [modal, modalContextHolder] = Modal.useModal();
|
const [modal, modalContextHolder] = Modal.useModal();
|
||||||
@@ -353,41 +354,41 @@ export default function GroupsPage() {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: 'subLinks',
|
key: 'subLinks',
|
||||||
icon: <LinkOutlined />,
|
icon: <LinkOutlined aria-hidden />,
|
||||||
label: t('pages.clients.subLinksSelected', { count: row.clientCount || 0 }),
|
label: t('pages.clients.subLinksSelected', { count: row.clientCount || 0 }),
|
||||||
disabled: !row.clientCount,
|
disabled: !row.clientCount,
|
||||||
onClick: () => openSubLinksFor(row),
|
onClick: () => openSubLinksFor(row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'adjust',
|
key: 'adjust',
|
||||||
icon: <ClockCircleOutlined />,
|
icon: <ClockCircleOutlined aria-hidden />,
|
||||||
label: t('pages.clients.adjustSelected', { count: row.clientCount || 0 }),
|
label: t('pages.clients.adjustSelected', { count: row.clientCount || 0 }),
|
||||||
disabled: !row.clientCount,
|
disabled: !row.clientCount,
|
||||||
onClick: () => openAdjustFor(row),
|
onClick: () => openAdjustFor(row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'reset',
|
key: 'reset',
|
||||||
icon: <RetweetOutlined />,
|
icon: <RetweetOutlined aria-hidden />,
|
||||||
label: t('pages.groups.resetTraffic'),
|
label: t('pages.groups.resetTraffic'),
|
||||||
disabled: !row.clientCount,
|
disabled: !row.clientCount,
|
||||||
onClick: () => onResetTraffic(row),
|
onClick: () => onResetTraffic(row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'addClients',
|
key: 'addClients',
|
||||||
icon: <UsergroupAddOutlined />,
|
icon: <UsergroupAddOutlined aria-hidden />,
|
||||||
label: t('pages.groups.addToGroup'),
|
label: t('pages.groups.addToGroup'),
|
||||||
onClick: () => openAddClientsFor(row),
|
onClick: () => openAddClientsFor(row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'rename',
|
key: 'rename',
|
||||||
icon: <EditOutlined />,
|
icon: <EditOutlined aria-hidden />,
|
||||||
label: t('pages.groups.rename'),
|
label: t('pages.groups.rename'),
|
||||||
onClick: () => openRename(row),
|
onClick: () => openRename(row),
|
||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
key: 'removeClients',
|
key: 'removeClients',
|
||||||
icon: <UsergroupDeleteOutlined />,
|
icon: <UsergroupDeleteOutlined aria-hidden />,
|
||||||
label: t('pages.groups.removeFromGroup'),
|
label: t('pages.groups.removeFromGroup'),
|
||||||
danger: true,
|
danger: true,
|
||||||
disabled: !row.clientCount,
|
disabled: !row.clientCount,
|
||||||
@@ -395,7 +396,7 @@ export default function GroupsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'deleteClients',
|
key: 'deleteClients',
|
||||||
icon: <DeleteOutlined />,
|
icon: <DeleteOutlined aria-hidden />,
|
||||||
label: t('pages.groups.deleteClients'),
|
label: t('pages.groups.deleteClients'),
|
||||||
danger: true,
|
danger: true,
|
||||||
disabled: !row.clientCount,
|
disabled: !row.clientCount,
|
||||||
@@ -403,7 +404,7 @@ export default function GroupsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
icon: <DeleteOutlined />,
|
icon: <DeleteOutlined aria-hidden />,
|
||||||
label: t('pages.groups.deleteGroupOnly'),
|
label: t('pages.groups.deleteGroupOnly'),
|
||||||
danger: true,
|
danger: true,
|
||||||
onClick: () => onDelete(row),
|
onClick: () => onDelete(row),
|
||||||
@@ -424,7 +425,7 @@ export default function GroupsPage() {
|
|||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
style={{ fontSize: 16 }}
|
style={{ fontSize: 16 }}
|
||||||
icon={<MoreOutlined />}
|
icon={<MoreOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Tooltip title={t('pages.groups.rename')}>
|
<Tooltip title={t('pages.groups.rename')}>
|
||||||
@@ -433,7 +434,7 @@ export default function GroupsPage() {
|
|||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
style={{ fontSize: 16 }}
|
style={{ fontSize: 16 }}
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined aria-hidden />}
|
||||||
onClick={() => openRename(row)}
|
onClick={() => openRename(row)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -518,14 +519,14 @@ export default function GroupsPage() {
|
|||||||
<Statistic
|
<Statistic
|
||||||
title={t('pages.groups.totalGroups')}
|
title={t('pages.groups.totalGroups')}
|
||||||
value={String(totalGroups)}
|
value={String(totalGroups)}
|
||||||
prefix={<TagsOutlined />}
|
prefix={<TagsOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={12} md={6}>
|
<Col xs={12} sm={12} md={6}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t('pages.groups.totalGroupedClients')}
|
title={t('pages.groups.totalGroupedClients')}
|
||||||
value={String(totalClients)}
|
value={String(totalClients)}
|
||||||
prefix={<TeamOutlined />}
|
prefix={<TeamOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={12} md={6}>
|
<Col xs={12} sm={12} md={6}>
|
||||||
@@ -534,9 +535,11 @@ export default function GroupsPage() {
|
|||||||
value={0}
|
value={0}
|
||||||
formatter={() => (
|
formatter={() => (
|
||||||
<span>
|
<span>
|
||||||
<ArrowUpOutlined /> {SizeFormatter.sizeFormat(totalUpload)}
|
<ArrowUpOutlined aria-hidden />{' '}
|
||||||
|
{SizeFormatter.sizeFormat(totalUpload)}
|
||||||
{' / '}
|
{' / '}
|
||||||
<ArrowDownOutlined /> {SizeFormatter.sizeFormat(totalDownload)}
|
<ArrowDownOutlined aria-hidden />{' '}
|
||||||
|
{SizeFormatter.sizeFormat(totalDownload)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -545,7 +548,7 @@ export default function GroupsPage() {
|
|||||||
<Statistic
|
<Statistic
|
||||||
title={t('pages.groups.totalTraffic')}
|
title={t('pages.groups.totalTraffic')}
|
||||||
value={SizeFormatter.sizeFormat(totalTraffic)}
|
value={SizeFormatter.sizeFormat(totalTraffic)}
|
||||||
prefix={<PieChartOutlined />}
|
prefix={<PieChartOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -561,7 +564,7 @@ export default function GroupsPage() {
|
|||||||
<Button
|
<Button
|
||||||
aria-label={t('pages.groups.addGroup')}
|
aria-label={t('pages.groups.addGroup')}
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
onClick={openCreate}
|
onClick={openCreate}
|
||||||
>
|
>
|
||||||
{!isMobile && t('pages.groups.addGroup')}
|
{!isMobile && t('pages.groups.addGroup')}
|
||||||
@@ -579,7 +582,7 @@ export default function GroupsPage() {
|
|||||||
locale={{
|
locale={{
|
||||||
emptyText: (
|
emptyText: (
|
||||||
<div className="card-empty">
|
<div className="card-empty">
|
||||||
<TagsOutlined style={{ fontSize: 32, marginBottom: 8 }} />
|
<TagsOutlined aria-hidden style={{ fontSize: 32, marginBottom: 8 }} />
|
||||||
<div>{t('noData')}</div>
|
<div>{t('noData')}</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -604,8 +607,9 @@ export default function GroupsPage() {
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form layout="vertical">
|
<Form layout="vertical">
|
||||||
<Form.Item label={t('pages.groups.name')}>
|
<Form.Item label={t('pages.groups.name')} htmlFor={`${uid}-create-name`}>
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-create-name`}
|
||||||
value={createName}
|
value={createName}
|
||||||
onChange={(e) => setCreateName(e.target.value)}
|
onChange={(e) => setCreateName(e.target.value)}
|
||||||
onPressEnter={confirmCreate}
|
onPressEnter={confirmCreate}
|
||||||
@@ -627,8 +631,9 @@ export default function GroupsPage() {
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form layout="vertical">
|
<Form layout="vertical">
|
||||||
<Form.Item label={t('pages.groups.name')}>
|
<Form.Item label={t('pages.groups.name')} htmlFor={`${uid}-rename-name`}>
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-rename-name`}
|
||||||
value={renameValue}
|
value={renameValue}
|
||||||
onChange={(e) => setRenameValue(e.target.value)}
|
onChange={(e) => setRenameValue(e.target.value)}
|
||||||
onPressEnter={confirmRename}
|
onPressEnter={confirmRename}
|
||||||
|
|||||||
@@ -208,7 +208,11 @@ export default function HostFormModal({
|
|||||||
{
|
{
|
||||||
key: 'basic',
|
key: 'basic',
|
||||||
forceRender: true,
|
forceRender: true,
|
||||||
label: catTabLabel(<ProfileOutlined />, t('pages.hosts.sections.basic'), isMobile),
|
label: catTabLabel(
|
||||||
|
<ProfileOutlined aria-hidden />,
|
||||||
|
t('pages.hosts.sections.basic'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -291,7 +295,7 @@ export default function HostFormModal({
|
|||||||
key: 'security',
|
key: 'security',
|
||||||
forceRender: true,
|
forceRender: true,
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<SafetyCertificateOutlined />,
|
<SafetyCertificateOutlined aria-hidden />,
|
||||||
t('pages.hosts.sections.security'),
|
t('pages.hosts.sections.security'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -373,7 +377,7 @@ export default function HostFormModal({
|
|||||||
key: 'advanced',
|
key: 'advanced',
|
||||||
forceRender: true,
|
forceRender: true,
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<ControlOutlined />,
|
<ControlOutlined aria-hidden />,
|
||||||
t('pages.hosts.sections.advanced'),
|
t('pages.hosts.sections.advanced'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -386,7 +390,7 @@ export default function HostFormModal({
|
|||||||
key: 'adv-general',
|
key: 'adv-general',
|
||||||
forceRender: true,
|
forceRender: true,
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<SettingOutlined />,
|
<SettingOutlined aria-hidden />,
|
||||||
t('pages.hosts.sections.general'),
|
t('pages.hosts.sections.general'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -425,7 +429,7 @@ export default function HostFormModal({
|
|||||||
key: 'adv-mux',
|
key: 'adv-mux',
|
||||||
forceRender: true,
|
forceRender: true,
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<PartitionOutlined />,
|
<PartitionOutlined aria-hidden />,
|
||||||
t('pages.hosts.fields.muxParams'),
|
t('pages.hosts.fields.muxParams'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -445,7 +449,7 @@ export default function HostFormModal({
|
|||||||
key: 'adv-sockopt',
|
key: 'adv-sockopt',
|
||||||
forceRender: true,
|
forceRender: true,
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<DeploymentUnitOutlined />,
|
<DeploymentUnitOutlined aria-hidden />,
|
||||||
t('pages.hosts.fields.sockoptParams'),
|
t('pages.hosts.fields.sockoptParams'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -465,7 +469,7 @@ export default function HostFormModal({
|
|||||||
key: 'adv-finalmask',
|
key: 'adv-finalmask',
|
||||||
forceRender: true,
|
forceRender: true,
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<RocketOutlined />,
|
<RocketOutlined aria-hidden />,
|
||||||
t('pages.hosts.fields.finalMask'),
|
t('pages.hosts.fields.finalMask'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -489,7 +493,7 @@ export default function HostFormModal({
|
|||||||
key: 'clash',
|
key: 'clash',
|
||||||
forceRender: true,
|
forceRender: true,
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<NodeIndexOutlined />,
|
<NodeIndexOutlined aria-hidden />,
|
||||||
t('pages.hosts.sections.clash'),
|
t('pages.hosts.sections.clash'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export default function HostList(props: HostListProps) {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
icon={<ArrowUpOutlined />}
|
icon={<ArrowUpOutlined aria-hidden />}
|
||||||
aria-label={t('pages.hosts.moveUp')}
|
aria-label={t('pages.hosts.moveUp')}
|
||||||
disabled={idx === 0}
|
disabled={idx === 0}
|
||||||
onClick={() => onMove(h, 'up')}
|
onClick={() => onMove(h, 'up')}
|
||||||
@@ -105,7 +105,7 @@ export default function HostList(props: HostListProps) {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
icon={<ArrowDownOutlined />}
|
icon={<ArrowDownOutlined aria-hidden />}
|
||||||
aria-label={t('pages.hosts.moveDown')}
|
aria-label={t('pages.hosts.moveDown')}
|
||||||
disabled={idx >= count - 1}
|
disabled={idx >= count - 1}
|
||||||
onClick={() => onMove(h, 'down')}
|
onClick={() => onMove(h, 'down')}
|
||||||
@@ -115,7 +115,7 @@ export default function HostList(props: HostListProps) {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined aria-hidden />}
|
||||||
aria-label={t('edit')}
|
aria-label={t('edit')}
|
||||||
onClick={() => onEdit(h)}
|
onClick={() => onEdit(h)}
|
||||||
/>
|
/>
|
||||||
@@ -125,7 +125,7 @@ export default function HostList(props: HostListProps) {
|
|||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined aria-hidden />}
|
||||||
aria-label={t('delete')}
|
aria-label={t('delete')}
|
||||||
onClick={() => onDelete(h)}
|
onClick={() => onDelete(h)}
|
||||||
/>
|
/>
|
||||||
@@ -139,7 +139,12 @@ export default function HostList(props: HostListProps) {
|
|||||||
key: 'enable',
|
key: 'enable',
|
||||||
width: 90,
|
width: 90,
|
||||||
render: (_, h) => (
|
render: (_, h) => (
|
||||||
<Switch size="small" checked={!h.isDisabled} onChange={(next) => onToggleEnable(h, next)} />
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={!h.isDisabled}
|
||||||
|
aria-label={t('pages.hosts.fields.enable')}
|
||||||
|
onChange={(next) => onToggleEnable(h, next)}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -276,7 +281,7 @@ export default function HostList(props: HostListProps) {
|
|||||||
const toolbar = (
|
const toolbar = (
|
||||||
<div className="card-toolbar">
|
<div className="card-toolbar">
|
||||||
{selectedGroupIds.length === 0 ? (
|
{selectedGroupIds.length === 0 ? (
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
|
<Button type="primary" icon={<PlusOutlined aria-hidden />} onClick={onAdd}>
|
||||||
{!isMobile && t('pages.hosts.addHost')}
|
{!isMobile && t('pages.hosts.addHost')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
@@ -291,7 +296,7 @@ export default function HostList(props: HostListProps) {
|
|||||||
</Tag>
|
</Tag>
|
||||||
<Button onClick={() => onBulkEnable(true)}>{t('pages.hosts.bulkEnable')}</Button>
|
<Button onClick={() => onBulkEnable(true)}>{t('pages.hosts.bulkEnable')}</Button>
|
||||||
<Button onClick={() => onBulkEnable(false)}>{t('pages.hosts.bulkDisable')}</Button>
|
<Button onClick={() => onBulkEnable(false)}>{t('pages.hosts.bulkDisable')}</Button>
|
||||||
<Button danger icon={<DeleteOutlined />} onClick={onBulkDelete}>
|
<Button danger icon={<DeleteOutlined aria-hidden />} onClick={onBulkDelete}>
|
||||||
{t('pages.hosts.bulkDelete')}
|
{t('pages.hosts.bulkDelete')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@@ -316,7 +321,7 @@ export default function HostList(props: HostListProps) {
|
|||||||
locale={{
|
locale={{
|
||||||
emptyText: (
|
emptyText: (
|
||||||
<div className="card-empty">
|
<div className="card-empty">
|
||||||
<GlobalOutlined style={{ fontSize: 32, marginBottom: 8 }} />
|
<GlobalOutlined aria-hidden style={{ fontSize: 32, marginBottom: 8 }} />
|
||||||
<div>{t('noData')}</div>
|
<div>{t('noData')}</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export default function HostsPage() {
|
|||||||
<Statistic
|
<Statistic
|
||||||
title={t('pages.hosts.summary.total')}
|
title={t('pages.hosts.summary.total')}
|
||||||
value={String(summary.total)}
|
value={String(summary.total)}
|
||||||
prefix={<GlobalOutlined />}
|
prefix={<GlobalOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={8} sm={8} md={8}>
|
<Col xs={8} sm={8} md={8}>
|
||||||
@@ -182,7 +182,10 @@ export default function HostsPage() {
|
|||||||
title={t('pages.hosts.summary.enabled')}
|
title={t('pages.hosts.summary.enabled')}
|
||||||
value={String(summary.enabled)}
|
value={String(summary.enabled)}
|
||||||
prefix={
|
prefix={
|
||||||
<CheckCircleOutlined style={{ color: 'var(--ant-color-success)' }} />
|
<CheckCircleOutlined
|
||||||
|
aria-hidden
|
||||||
|
style={{ color: 'var(--ant-color-success)' }}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -191,7 +194,10 @@ export default function HostsPage() {
|
|||||||
title={t('pages.hosts.summary.disabled')}
|
title={t('pages.hosts.summary.disabled')}
|
||||||
value={String(summary.disabled)}
|
value={String(summary.disabled)}
|
||||||
prefix={
|
prefix={
|
||||||
<StopOutlined style={{ color: 'var(--ant-color-text-quaternary)' }} />
|
<StopOutlined
|
||||||
|
aria-hidden
|
||||||
|
style={{ color: 'var(--ant-color-text-quaternary)' }}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -754,9 +754,11 @@ export default function InboundsPage() {
|
|||||||
value={0}
|
value={0}
|
||||||
formatter={() => (
|
formatter={() => (
|
||||||
<span>
|
<span>
|
||||||
<ArrowUpOutlined /> {SizeFormatter.sizeFormat(totals.up)}
|
<ArrowUpOutlined aria-hidden />{' '}
|
||||||
|
{SizeFormatter.sizeFormat(totals.up)}
|
||||||
{' / '}
|
{' / '}
|
||||||
<ArrowDownOutlined /> {SizeFormatter.sizeFormat(totals.down)}
|
<ArrowDownOutlined aria-hidden />{' '}
|
||||||
|
{SizeFormatter.sizeFormat(totals.down)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -765,14 +767,14 @@ export default function InboundsPage() {
|
|||||||
<Statistic
|
<Statistic
|
||||||
title={t('pages.inbounds.totalUsage')}
|
title={t('pages.inbounds.totalUsage')}
|
||||||
value={SizeFormatter.sizeFormat(totals.up + totals.down)}
|
value={SizeFormatter.sizeFormat(totals.up + totals.down)}
|
||||||
prefix={<PieChartOutlined />}
|
prefix={<PieChartOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={24} md={8}>
|
<Col xs={24} sm={24} md={8}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t('pages.inbounds.inboundCount')}
|
title={t('pages.inbounds.inboundCount')}
|
||||||
value={String(dbInbounds.length)}
|
value={String(dbInbounds.length)}
|
||||||
prefix={<BarsOutlined />}
|
prefix={<BarsOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -32,7 +32,13 @@ export default function FallbacksCard({
|
|||||||
|
|
||||||
const addButtons = (
|
const addButtons = (
|
||||||
<Space size={8} wrap>
|
<Space size={8} wrap>
|
||||||
<Button type="primary" ghost size="small" icon={<PlusOutlined />} onClick={addFallback}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
size="small"
|
||||||
|
icon={<PlusOutlined aria-hidden />}
|
||||||
|
onClick={addFallback}
|
||||||
|
>
|
||||||
{t('pages.inbounds.fallbacks.add') || 'Add fallback'}
|
{t('pages.inbounds.fallbacks.add') || 'Add fallback'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -90,25 +96,26 @@ export default function FallbacksCard({
|
|||||||
disabled={idx === 0}
|
disabled={idx === 0}
|
||||||
onClick={() => moveFallback(idx, -1)}
|
onClick={() => moveFallback(idx, -1)}
|
||||||
title={t('pages.inbounds.form.moveUp')}
|
title={t('pages.inbounds.form.moveUp')}
|
||||||
icon={<ArrowUpOutlined />}
|
icon={<ArrowUpOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('pages.inbounds.form.moveDown')}
|
aria-label={t('pages.inbounds.form.moveDown')}
|
||||||
disabled={idx === fallbacks.length - 1}
|
disabled={idx === fallbacks.length - 1}
|
||||||
onClick={() => moveFallback(idx, 1)}
|
onClick={() => moveFallback(idx, 1)}
|
||||||
title={t('pages.inbounds.form.moveDown')}
|
title={t('pages.inbounds.form.moveDown')}
|
||||||
icon={<ArrowDownOutlined />}
|
icon={<ArrowDownOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('delete')}
|
aria-label={t('delete')}
|
||||||
danger
|
danger
|
||||||
onClick={() => removeFallback(idx)}
|
onClick={() => removeFallback(idx)}
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
<Row gutter={[8, 8]}>
|
<Row gutter={[8, 8]}>
|
||||||
<Col xs={24} sm={12}>
|
<Col xs={24} sm={12}>
|
||||||
<Input
|
<Input
|
||||||
|
aria-label="SNI"
|
||||||
prefix="SNI"
|
prefix="SNI"
|
||||||
placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
|
placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
|
||||||
value={record.name}
|
value={record.name}
|
||||||
@@ -117,6 +124,7 @@ export default function FallbacksCard({
|
|||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12}>
|
<Col xs={24} sm={12}>
|
||||||
<Input
|
<Input
|
||||||
|
aria-label="ALPN"
|
||||||
prefix="ALPN"
|
prefix="ALPN"
|
||||||
placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
|
placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
|
||||||
value={record.alpn}
|
value={record.alpn}
|
||||||
@@ -125,6 +133,7 @@ export default function FallbacksCard({
|
|||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12}>
|
<Col xs={24} sm={12}>
|
||||||
<Input
|
<Input
|
||||||
|
aria-label="Path"
|
||||||
prefix="Path"
|
prefix="Path"
|
||||||
placeholder="/"
|
placeholder="/"
|
||||||
value={record.path}
|
value={record.path}
|
||||||
@@ -133,6 +142,7 @@ export default function FallbacksCard({
|
|||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12}>
|
<Col xs={24} sm={12}>
|
||||||
<Input
|
<Input
|
||||||
|
aria-label="Dest"
|
||||||
prefix="Dest"
|
prefix="Dest"
|
||||||
placeholder={t('pages.inbounds.fallbacks.destPlaceholder') || 'auto'}
|
placeholder={t('pages.inbounds.fallbacks.destPlaceholder') || 'auto'}
|
||||||
value={record.dest}
|
value={record.dest}
|
||||||
@@ -141,6 +151,7 @@ export default function FallbacksCard({
|
|||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12}>
|
<Col xs={24} sm={12}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
aria-label="xver"
|
||||||
prefix="xver"
|
prefix="xver"
|
||||||
min={0}
|
min={0}
|
||||||
max={2}
|
max={2}
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState, useId } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import {
|
import { Alert, Form, Input, InputNumber, Modal, Radio, Select, Switch, Tabs, message } from 'antd';
|
||||||
Alert,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
InputNumber,
|
|
||||||
Modal,
|
|
||||||
Radio,
|
|
||||||
Select,
|
|
||||||
Switch,
|
|
||||||
Tabs,
|
|
||||||
Tooltip,
|
|
||||||
message,
|
|
||||||
} from 'antd';
|
|
||||||
import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form';
|
import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils';
|
import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils';
|
||||||
@@ -99,16 +87,6 @@ import SniffingTab from './SniffingTab';
|
|||||||
import type { DBInbound } from '@/models/dbinbound';
|
import type { DBInbound } from '@/models/dbinbound';
|
||||||
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||||
|
|
||||||
/* Render a field label with a hover tooltip icon instead of an `extra` help line below. */
|
|
||||||
const labelWithHint = (label: string, hint: string) => (
|
|
||||||
<span>
|
|
||||||
{label}
|
|
||||||
<Tooltip title={hint}>
|
|
||||||
<QuestionCircleOutlined style={{ marginInlineStart: 4, color: 'rgba(128,128,128,0.65)' }} />
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
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 SHARE_ADDR_STRATEGIES = ['node', 'listen', 'custom'] as const;
|
const SHARE_ADDR_STRATEGIES = ['node', 'listen', 'custom'] as const;
|
||||||
const SHARE_ADDR_HOSTNAME_RE =
|
const SHARE_ADDR_HOSTNAME_RE =
|
||||||
@@ -240,6 +218,7 @@ export default function InboundFormModal({
|
|||||||
availableNodesFetched = true,
|
availableNodesFetched = true,
|
||||||
}: InboundFormModalProps) {
|
}: InboundFormModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const [messageApi, messageContextHolder] = message.useMessage();
|
const [messageApi, messageContextHolder] = message.useMessage();
|
||||||
const [modal, modalContextHolder] = Modal.useModal();
|
const [modal, modalContextHolder] = Modal.useModal();
|
||||||
const methods = useForm<InboundFormValues>({ defaultValues: buildAddModeValues() });
|
const methods = useForm<InboundFormValues>({ defaultValues: buildAddModeValues() });
|
||||||
@@ -668,7 +647,8 @@ export default function InboundFormModal({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name="listen"
|
name="listen"
|
||||||
label={labelWithHint(t('pages.inbounds.address'), t('pages.inbounds.form.listenHelp'))}
|
label={t('pages.inbounds.address')}
|
||||||
|
tooltip={t('pages.inbounds.form.listenHelp')}
|
||||||
>
|
>
|
||||||
<Input placeholder={t('pages.inbounds.monitorDesc')} />
|
<Input placeholder={t('pages.inbounds.monitorDesc')} />
|
||||||
</FormField>
|
</FormField>
|
||||||
@@ -677,10 +657,8 @@ export default function InboundFormModal({
|
|||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
name="shareAddrStrategy"
|
name="shareAddrStrategy"
|
||||||
label={labelWithHint(
|
label={t('pages.inbounds.form.shareAddrStrategy')}
|
||||||
t('pages.inbounds.form.shareAddrStrategy'),
|
tooltip={t('pages.inbounds.form.shareAddrStrategyHelp')}
|
||||||
t('pages.inbounds.form.shareAddrStrategyHelp'),
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
options={SHARE_ADDR_STRATEGIES.filter(
|
options={SHARE_ADDR_STRATEGIES.filter(
|
||||||
@@ -695,10 +673,8 @@ export default function InboundFormModal({
|
|||||||
{shareAddrStrategy === 'custom' && (
|
{shareAddrStrategy === 'custom' && (
|
||||||
<FormField
|
<FormField
|
||||||
name="shareAddr"
|
name="shareAddr"
|
||||||
label={labelWithHint(
|
label={t('pages.inbounds.form.shareAddr')}
|
||||||
t('pages.inbounds.form.shareAddr'),
|
tooltip={t('pages.inbounds.form.shareAddrHelp')}
|
||||||
t('pages.inbounds.form.shareAddrHelp'),
|
|
||||||
)}
|
|
||||||
rules={{
|
rules={{
|
||||||
validate: (value) =>
|
validate: (value) =>
|
||||||
isValidShareAddrInput(String(value ?? '')) ||
|
isValidShareAddrInput(String(value ?? '')) ||
|
||||||
@@ -713,10 +689,8 @@ export default function InboundFormModal({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name="subSortIndex"
|
name="subSortIndex"
|
||||||
label={labelWithHint(
|
label={t('pages.inbounds.form.subSortIndex')}
|
||||||
t('pages.inbounds.form.subSortIndex'),
|
tooltip={t('pages.inbounds.form.subSortIndexHelp')}
|
||||||
t('pages.inbounds.form.subSortIndexHelp'),
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<InputNumber />
|
<InputNumber />
|
||||||
</FormField>
|
</FormField>
|
||||||
@@ -724,10 +698,8 @@ export default function InboundFormModal({
|
|||||||
<FormField
|
<FormField
|
||||||
name="excludeFromSub"
|
name="excludeFromSub"
|
||||||
valueProp="checked"
|
valueProp="checked"
|
||||||
label={labelWithHint(
|
label={t('pages.inbounds.form.excludeFromSub')}
|
||||||
t('pages.inbounds.form.excludeFromSub'),
|
tooltip={t('pages.inbounds.form.excludeFromSubHelp')}
|
||||||
t('pages.inbounds.form.excludeFromSubHelp'),
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</FormField>
|
</FormField>
|
||||||
@@ -736,10 +708,8 @@ export default function InboundFormModal({
|
|||||||
<FormField
|
<FormField
|
||||||
name="disableFlow"
|
name="disableFlow"
|
||||||
valueProp="checked"
|
valueProp="checked"
|
||||||
label={labelWithHint(
|
label={t('pages.inbounds.form.disableFlow')}
|
||||||
t('pages.inbounds.form.disableFlow'),
|
tooltip={t('pages.inbounds.form.disableFlowHelp')}
|
||||||
t('pages.inbounds.form.disableFlowHelp'),
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</FormField>
|
</FormField>
|
||||||
@@ -754,13 +724,13 @@ export default function InboundFormModal({
|
|||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={
|
htmlFor={`${uid}-totalFlow`}
|
||||||
<Tooltip title={t('pages.inbounds.meansNoLimit')}>
|
label={t('pages.inbounds.totalFlow')}
|
||||||
{t('pages.inbounds.totalFlow')}
|
tooltip={t('pages.inbounds.meansNoLimit')}
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
id={`${uid}-totalFlow`}
|
||||||
|
aria-describedby={`${uid}-totalFlow-hint`}
|
||||||
value={wTotal ? Math.round((wTotal / SizeFormatter.ONE_GB) * 100) / 100 : 0}
|
value={wTotal ? Math.round((wTotal / SizeFormatter.ONE_GB) * 100) / 100 : 0}
|
||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
@@ -769,6 +739,9 @@ export default function InboundFormModal({
|
|||||||
setV('total', bytes);
|
setV('total', bytes);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<span id={`${uid}-totalFlow-hint`} hidden>
|
||||||
|
{t('pages.inbounds.meansNoLimit')}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<FormField name="trafficReset" label={t('pages.inbounds.periodicTrafficResetTitle')}>
|
<FormField name="trafficReset" label={t('pages.inbounds.periodicTrafficResetTitle')}>
|
||||||
@@ -791,16 +764,19 @@ export default function InboundFormModal({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={
|
htmlFor={`${uid}-expiryTime`}
|
||||||
<Tooltip title={t('pages.inbounds.leaveBlankToNeverExpire')}>
|
label={t('pages.inbounds.expireDate')}
|
||||||
{t('pages.inbounds.expireDate')}
|
tooltip={t('pages.inbounds.leaveBlankToNeverExpire')}
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
|
id={`${uid}-expiryTime`}
|
||||||
|
aria-describedby={`${uid}-expiryTime-hint`}
|
||||||
value={wExpiry > 0 ? dayjs(wExpiry) : null}
|
value={wExpiry > 0 ? dayjs(wExpiry) : null}
|
||||||
onChange={(d) => setV('expiryTime', d ? d.valueOf() : 0)}
|
onChange={(d) => setV('expiryTime', d ? d.valueOf() : 0)}
|
||||||
/>
|
/>
|
||||||
|
<span id={`${uid}-expiryTime-hint`} hidden>
|
||||||
|
{t('pages.inbounds.leaveBlankToNeverExpire')}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -936,8 +912,9 @@ export default function InboundFormModal({
|
|||||||
const streamTab = (
|
const streamTab = (
|
||||||
<>
|
<>
|
||||||
{hasSelectableTransport && (
|
{hasSelectableTransport && (
|
||||||
<Form.Item label={t('transmission')}>
|
<Form.Item htmlFor={`${uid}-network`} label={t('transmission')}>
|
||||||
<Select
|
<Select
|
||||||
|
id={`${uid}-network`}
|
||||||
style={{ width: '75%' }}
|
style={{ width: '75%' }}
|
||||||
value={network}
|
value={network}
|
||||||
onChange={onNetworkChange}
|
onChange={onNetworkChange}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function AccountsList() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<PlusOutlined /> {t('add')}
|
<PlusOutlined aria-hidden /> {t('add')}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{fields.length > 0 && (
|
{fields.length > 0 && (
|
||||||
@@ -32,13 +32,13 @@ export default function AccountsList() {
|
|||||||
<Space.Compact key={field.id} className="mb-8" block>
|
<Space.Compact key={field.id} className="mb-8" block>
|
||||||
<InputAddon>{String(idx + 1)}</InputAddon>
|
<InputAddon>{String(idx + 1)}</InputAddon>
|
||||||
<FormField name={['settings', 'accounts', idx, 'user']} noStyle>
|
<FormField name={['settings', 'accounts', idx, 'user']} noStyle>
|
||||||
<Input placeholder={t('username')} />
|
<Input aria-label={`${t('username')} ${idx + 1}`} placeholder={t('username')} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField name={['settings', 'accounts', idx, 'pass']} noStyle>
|
<FormField name={['settings', 'accounts', idx, 'pass']} noStyle>
|
||||||
<Input placeholder={t('password')} />
|
<Input aria-label={`${t('password')} ${idx + 1}`} placeholder={t('password')} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<Button aria-label={t('remove')} onClick={() => remove(idx)}>
|
<Button aria-label={t('remove')} onClick={() => remove(idx)}>
|
||||||
<MinusOutlined />
|
<MinusOutlined aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useId } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Form, Input, InputNumber, Space, Switch } from 'antd';
|
import { Button, Form, Input, InputNumber, Space, Switch } from 'antd';
|
||||||
import { ReloadOutlined } from '@ant-design/icons';
|
import { ReloadOutlined } from '@ant-design/icons';
|
||||||
@@ -16,22 +17,23 @@ export default function AmneziawgFields({
|
|||||||
regenInboundAwgObfuscation,
|
regenInboundAwgObfuscation,
|
||||||
}: AmneziawgFieldsProps) {
|
}: AmneziawgFieldsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Item label={t('pages.xray.amneziawg.privateKey')}>
|
<Form.Item htmlFor={`${uid}-privateKey`} label={t('pages.xray.amneziawg.privateKey')}>
|
||||||
<Space.Compact block>
|
<Space.Compact block>
|
||||||
<FormField name={['settings', 'server', 'privateKey']} noStyle>
|
<FormField name={['settings', 'server', 'privateKey']} noStyle>
|
||||||
<Input style={{ width: 'calc(100% - 32px)' }} />
|
<Input id={`${uid}-privateKey`} style={{ width: 'calc(100% - 32px)' }} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
onClick={regenInboundAwg}
|
onClick={regenInboundAwg}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t('pages.xray.amneziawg.publicKey')}>
|
<Form.Item htmlFor={`${uid}-publicKey`} label={t('pages.xray.amneziawg.publicKey')}>
|
||||||
<Input value={awgPubKey} disabled />
|
<Input id={`${uid}-publicKey`} value={awgPubKey} disabled />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<FormField
|
<FormField
|
||||||
name={['settings', 'server', 'subnetIp']}
|
name={['settings', 'server', 'subnetIp']}
|
||||||
@@ -89,7 +91,7 @@ export default function AmneziawgFields({
|
|||||||
<Input placeholder="eth0" />
|
<Input placeholder="eth0" />
|
||||||
</FormField>
|
</FormField>
|
||||||
<Form.Item label={t('pages.xray.amneziawg.obfuscation')}>
|
<Form.Item label={t('pages.xray.amneziawg.obfuscation')}>
|
||||||
<Button icon={<ReloadOutlined />} onClick={regenInboundAwgObfuscation}>
|
<Button icon={<ReloadOutlined aria-hidden />} onClick={regenInboundAwgObfuscation}>
|
||||||
{t('pages.xray.amneziawg.regenerateObfuscation')}
|
{t('pages.xray.amneziawg.regenerateObfuscation')}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useId } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Form, Input, InputNumber, Select, Switch } from 'antd';
|
import { Form, Input, InputNumber, Select, Switch } from 'antd';
|
||||||
import { useFormContext, useWatch } from 'react-hook-form';
|
import { useFormContext, useWatch } from 'react-hook-form';
|
||||||
@@ -9,6 +10,7 @@ const MASQ_PATH = ['streamSettings', 'hysteriaSettings', 'masquerade'];
|
|||||||
|
|
||||||
export default function HysteriaFields() {
|
export default function HysteriaFields() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const { control, setValue } = useFormContext();
|
const { control, setValue } = useFormContext();
|
||||||
const masq = useWatch({ control, name: 'streamSettings.hysteriaSettings.masquerade' }) as
|
const masq = useWatch({ control, name: 'streamSettings.hysteriaSettings.masquerade' }) as
|
||||||
| { type?: string }
|
| { type?: string }
|
||||||
@@ -32,8 +34,9 @@ export default function HysteriaFields() {
|
|||||||
<InputNumber min={2} max={600} style={{ width: '100%' }} />
|
<InputNumber min={2} max={600} style={{ width: '100%' }} />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<Form.Item label={t('pages.inbounds.form.masquerade')}>
|
<Form.Item htmlFor={`${uid}-masquerade`} label={t('pages.inbounds.form.masquerade')}>
|
||||||
<Switch
|
<Switch
|
||||||
|
id={`${uid}-masquerade`}
|
||||||
checked={!!masq}
|
checked={!!masq}
|
||||||
onChange={(checked) =>
|
onChange={(checked) =>
|
||||||
setValue(
|
setValue(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useId } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Form, Input, Select, Space, Switch } from 'antd';
|
import { Button, Form, Input, Select, Space, Switch } from 'antd';
|
||||||
import { ReloadOutlined } from '@ant-design/icons';
|
import { ReloadOutlined } from '@ant-design/icons';
|
||||||
@@ -13,6 +14,7 @@ interface ShadowsocksFieldsProps {
|
|||||||
|
|
||||||
export default function ShadowsocksFields({ isSSWith2022 }: ShadowsocksFieldsProps) {
|
export default function ShadowsocksFields({ isSSWith2022 }: ShadowsocksFieldsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const { getValues, setValue } = useFormContext();
|
const { getValues, setValue } = useFormContext();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -26,14 +28,14 @@ export default function ShadowsocksFields({ isSSWith2022 }: ShadowsocksFieldsPro
|
|||||||
<Select options={SSInboundMethodSchema.options.map((m) => ({ value: m, label: m }))} />
|
<Select options={SSInboundMethodSchema.options.map((m) => ({ value: m, label: m }))} />
|
||||||
</FormField>
|
</FormField>
|
||||||
{isSSWith2022 && (
|
{isSSWith2022 && (
|
||||||
<Form.Item label={t('password')}>
|
<Form.Item htmlFor={`${uid}-password`} label={t('password')}>
|
||||||
<Space.Compact block>
|
<Space.Compact block>
|
||||||
<FormField name={['settings', 'password']} noStyle>
|
<FormField name={['settings', 'password']} noStyle>
|
||||||
<Input style={{ width: 'calc(100% - 32px)' }} />
|
<Input id={`${uid}-password`} style={{ width: 'calc(100% - 32px)' }} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const method = getValues('settings.method');
|
const method = getValues('settings.method');
|
||||||
setValue(
|
setValue(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useId } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
AutoComplete,
|
AutoComplete,
|
||||||
@@ -21,6 +21,7 @@ import { HttpUtil } from '@/utils';
|
|||||||
|
|
||||||
export default function TuicFields() {
|
export default function TuicFields() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const [messageApi, messageContextHolder] = message.useMessage();
|
const [messageApi, messageContextHolder] = message.useMessage();
|
||||||
const { control, setValue } = useFormContext();
|
const { control, setValue } = useFormContext();
|
||||||
const [loadingPanelCert, setLoadingPanelCert] = useState(false);
|
const [loadingPanelCert, setLoadingPanelCert] = useState(false);
|
||||||
@@ -147,8 +148,9 @@ export default function TuicFields() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{messageContextHolder}
|
{messageContextHolder}
|
||||||
<Form.Item label={t('pages.inbounds.publicKey')}>
|
<Form.Item htmlFor={`${uid}-certificate`} label={t('pages.inbounds.publicKey')}>
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
|
id={`${uid}-certificate`}
|
||||||
value={certificate}
|
value={certificate}
|
||||||
options={certOptions}
|
options={certOptions}
|
||||||
onChange={(v) => setValue('settings.server.certificate', v)}
|
onChange={(v) => setValue('settings.server.certificate', v)}
|
||||||
@@ -156,8 +158,9 @@ export default function TuicFields() {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t('pages.inbounds.privatekey')}>
|
<Form.Item htmlFor={`${uid}-privateKey`} label={t('pages.inbounds.privatekey')}>
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
|
id={`${uid}-privateKey`}
|
||||||
value={privateKey}
|
value={privateKey}
|
||||||
options={keyOptions}
|
options={keyOptions}
|
||||||
onChange={(v) => setValue('settings.server.private_key', v)}
|
onChange={(v) => setValue('settings.server.private_key', v)}
|
||||||
@@ -169,13 +172,13 @@ export default function TuicFields() {
|
|||||||
<Space wrap style={{ display: 'flex', flexWrap: 'wrap', width: '100%' }}>
|
<Space wrap style={{ display: 'flex', flexWrap: 'wrap', width: '100%' }}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<CloudDownloadOutlined />}
|
icon={<CloudDownloadOutlined aria-hidden />}
|
||||||
loading={loadingPanelCert}
|
loading={loadingPanelCert}
|
||||||
onClick={setCertFromPanel}
|
onClick={setCertFromPanel}
|
||||||
>
|
>
|
||||||
{t('pages.inbounds.setDefaultCert')}
|
{t('pages.inbounds.setDefaultCert')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button icon={<SyncOutlined />} onClick={autofillFromSni}>
|
<Button icon={<SyncOutlined aria-hidden />} onClick={autofillFromSni}>
|
||||||
{t('pages.inbounds.form.autoFill')}
|
{t('pages.inbounds.form.autoFill')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -217,12 +220,21 @@ export default function TuicFields() {
|
|||||||
|
|
||||||
<Divider titlePlacement="start">{t('pages.xray.tuic.profileOptions')}</Divider>
|
<Divider titlePlacement="start">{t('pages.xray.tuic.profileOptions')}</Divider>
|
||||||
|
|
||||||
<Form.Item label={t('pages.xray.tuic.sni')} tooltip={t('pages.xray.tuic.sniHint')}>
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-sni`}
|
||||||
|
label={t('pages.xray.tuic.sni')}
|
||||||
|
tooltip={t('pages.xray.tuic.sniHint')}
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-sni`}
|
||||||
|
aria-describedby={`${uid}-sni-hint`}
|
||||||
value={sni}
|
value={sni}
|
||||||
placeholder="example.com"
|
placeholder="example.com"
|
||||||
onChange={(e) => handleSniChange(e.target.value)}
|
onChange={(e) => handleSniChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
<span id={`${uid}-sni-hint`} hidden>
|
||||||
|
{t('pages.xray.tuic.sniHint')}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ReactNode } from 'react';
|
import { useId, type ReactNode } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Form, Input, InputNumber, Select, Space, Switch, Tooltip } from 'antd';
|
import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
|
||||||
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
|
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
@@ -9,28 +9,39 @@ import { FormField } from '@/components/form/rhf';
|
|||||||
interface StringListProps {
|
interface StringListProps {
|
||||||
name: string[];
|
name: string[];
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
|
tooltip?: string;
|
||||||
placeholder: (index: number) => string;
|
placeholder: (index: number) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function StringList({ name, label, placeholder }: StringListProps) {
|
function StringList({ name, label, tooltip, placeholder }: StringListProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const labelId = useId();
|
||||||
const { control } = useFormContext();
|
const { control } = useFormContext();
|
||||||
const { fields, append, remove } = useFieldArray({ control, name: name.join('.') });
|
const { fields, append, remove } = useFieldArray({ control, name: name.join('.') });
|
||||||
return (
|
return (
|
||||||
<Form.Item label={label}>
|
<Form.Item label={<span id={labelId}>{label}</span>} tooltip={tooltip}>
|
||||||
<Button aria-label={t('add')} size="small" onClick={() => append('')}>
|
<Button aria-label={t('add')} size="small" onClick={() => append('')}>
|
||||||
<PlusOutlined />
|
<PlusOutlined aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
{fields.map((field, j) => (
|
{fields.map((field, j) => (
|
||||||
<Space.Compact key={field.id} block className="mt-4">
|
<Space.Compact key={field.id} block className="mt-4">
|
||||||
<FormField name={[...name, j]} noStyle>
|
<FormField name={[...name, j]} noStyle>
|
||||||
<Input placeholder={placeholder(j)} />
|
<Input
|
||||||
|
aria-labelledby={labelId}
|
||||||
|
aria-describedby={tooltip ? `${labelId}-hint` : undefined}
|
||||||
|
placeholder={placeholder(j)}
|
||||||
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<Button aria-label={t('remove')} size="small" onClick={() => remove(j)}>
|
<Button aria-label={t('remove')} size="small" onClick={() => remove(j)}>
|
||||||
<MinusOutlined />
|
<MinusOutlined aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
))}
|
))}
|
||||||
|
{tooltip && (
|
||||||
|
<span id={`${labelId}-hint`} hidden>
|
||||||
|
{tooltip}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -73,20 +84,14 @@ export default function TunFields() {
|
|||||||
</FormField>
|
</FormField>
|
||||||
<StringList
|
<StringList
|
||||||
name={['settings', 'autoSystemRoutingTable']}
|
name={['settings', 'autoSystemRoutingTable']}
|
||||||
label={
|
label={t('pages.inbounds.info.autoSystemRoutes')}
|
||||||
<Tooltip title={t('pages.inbounds.form.autoSystemRoutesTooltip')}>
|
tooltip={t('pages.inbounds.form.autoSystemRoutesTooltip')}
|
||||||
{t('pages.inbounds.info.autoSystemRoutes')}
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
placeholder={(j) => (j === 0 ? '0.0.0.0/0' : '::/0')}
|
placeholder={(j) => (j === 0 ? '0.0.0.0/0' : '::/0')}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
name={['settings', 'autoOutboundsInterface']}
|
name={['settings', 'autoOutboundsInterface']}
|
||||||
label={
|
label={t('pages.inbounds.form.autoOutboundsInterface')}
|
||||||
<Tooltip title={t('pages.inbounds.form.autoOutboundsInterfaceTooltip')}>
|
tooltip={t('pages.inbounds.form.autoOutboundsInterfaceTooltip')}
|
||||||
{t('pages.inbounds.form.autoOutboundsInterface')}
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Input placeholder="auto" />
|
<Input placeholder="auto" />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useId } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Form, Input, InputNumber, Select, Space, Typography } from 'antd';
|
import { Button, Form, Input, InputNumber, Select, Space, Typography } from 'antd';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
@@ -26,6 +26,7 @@ export default function VlessFields({
|
|||||||
clearVlessEnc,
|
clearVlessEnc,
|
||||||
}: VlessFieldsProps) {
|
}: VlessFieldsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const { control } = useFormContext();
|
const { control } = useFormContext();
|
||||||
const [authKind, setAuthKind] = useState<VlessAuthKind>(vlessAuthKind ?? 'x25519');
|
const [authKind, setAuthKind] = useState<VlessAuthKind>(vlessAuthKind ?? 'x25519');
|
||||||
|
|
||||||
@@ -47,9 +48,10 @@ export default function VlessFields({
|
|||||||
<FormField name={['settings', 'encryption']} label={t('pages.inbounds.encryption')}>
|
<FormField name={['settings', 'encryption']} label={t('pages.inbounds.encryption')}>
|
||||||
<Input />
|
<Input />
|
||||||
</FormField>
|
</FormField>
|
||||||
<Form.Item label={t('pages.inbounds.vlessAuthGenerate')}>
|
<Form.Item htmlFor={`${uid}-authKind`} label={t('pages.inbounds.vlessAuthGenerate')}>
|
||||||
<Space size={8} wrap>
|
<Space size={8} wrap>
|
||||||
<Select
|
<Select
|
||||||
|
id={`${uid}-authKind`}
|
||||||
value={authKind}
|
value={authKind}
|
||||||
onChange={(v) => setAuthKind(v)}
|
onChange={(v) => setAuthKind(v)}
|
||||||
options={authOptions}
|
options={authOptions}
|
||||||
@@ -80,6 +82,7 @@ export default function VlessFields({
|
|||||||
defaultValue={def}
|
defaultValue={def}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
aria-label={`${t('pages.inbounds.form.visionTestseed')} ${i + 1}`}
|
||||||
min={1}
|
min={1}
|
||||||
style={{ width: '25%' }}
|
style={{ width: '25%' }}
|
||||||
value={field.value as number}
|
value={field.value as number}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useId } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Form, Input, InputNumber, Space, Switch } from 'antd';
|
import { Button, Form, Input, InputNumber, Space, Switch } from 'antd';
|
||||||
import { ReloadOutlined } from '@ant-design/icons';
|
import { ReloadOutlined } from '@ant-design/icons';
|
||||||
@@ -11,18 +12,23 @@ interface WireguardFieldsProps {
|
|||||||
|
|
||||||
export default function WireguardFields({ wgPubKey, regenInboundWg }: WireguardFieldsProps) {
|
export default function WireguardFields({ wgPubKey, regenInboundWg }: WireguardFieldsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Item label={t('pages.xray.wireguard.secretKey')}>
|
<Form.Item htmlFor={`${uid}-secretKey`} label={t('pages.xray.wireguard.secretKey')}>
|
||||||
<Space.Compact block>
|
<Space.Compact block>
|
||||||
<FormField name={['settings', 'secretKey']} noStyle>
|
<FormField name={['settings', 'secretKey']} noStyle>
|
||||||
<Input style={{ width: 'calc(100% - 32px)' }} />
|
<Input id={`${uid}-secretKey`} style={{ width: 'calc(100% - 32px)' }} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<Button aria-label={t('regenerate')} icon={<ReloadOutlined />} onClick={regenInboundWg} />
|
<Button
|
||||||
|
aria-label={t('regenerate')}
|
||||||
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
|
onClick={regenInboundWg}
|
||||||
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t('pages.xray.wireguard.publicKey')}>
|
<Form.Item htmlFor={`${uid}-publicKey`} label={t('pages.xray.wireguard.publicKey')}>
|
||||||
<Input value={wgPubKey} disabled />
|
<Input id={`${uid}-publicKey`} value={wgPubKey} disabled />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<FormField name={['settings', 'subnetIp']} label={t('pages.xray.wireguard.subnetIp')}>
|
<FormField name={['settings', 'subnetIp']} label={t('pages.xray.wireguard.subnetIp')}>
|
||||||
<Input placeholder="10.0.0.0" />
|
<Input placeholder="10.0.0.0" />
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ export default function RealityTargetScannerModal({
|
|||||||
{t('pages.inbounds.form.scanModalDesc')}
|
{t('pages.inbounds.form.scanModalDesc')}
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
|
aria-label={t('pages.inbounds.form.scanDiscoverPlaceholder')}
|
||||||
allowClear
|
allowClear
|
||||||
enterButton={t('pages.inbounds.form.scan')}
|
enterButton={t('pages.inbounds.form.scan')}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useId } from 'react';
|
||||||
import { useFormContext, useWatch } from 'react-hook-form';
|
import { useFormContext, useWatch } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
@@ -59,6 +59,7 @@ export default function RealityForm({
|
|||||||
clearMldsa65,
|
clearMldsa65,
|
||||||
}: RealityFormProps) {
|
}: RealityFormProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const { getFieldState, trigger } = useFormContext();
|
const { getFieldState, trigger } = useFormContext();
|
||||||
const [scannerOpen, setScannerOpen] = useState(false);
|
const [scannerOpen, setScannerOpen] = useState(false);
|
||||||
const mldsa65Seed = useWatch({ name: 'streamSettings.realitySettings.mldsa65Seed' });
|
const mldsa65Seed = useWatch({ name: 'streamSettings.realitySettings.mldsa65Seed' });
|
||||||
@@ -113,6 +114,7 @@ export default function RealityForm({
|
|||||||
<Select options={Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp }))} />
|
<Select options={Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp }))} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-target`}
|
||||||
label={t('pages.inbounds.form.target')}
|
label={t('pages.inbounds.form.target')}
|
||||||
tooltip={t('pages.inbounds.form.realityTargetHint')}
|
tooltip={t('pages.inbounds.form.realityTargetHint')}
|
||||||
>
|
>
|
||||||
@@ -127,19 +129,27 @@ export default function RealityForm({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input style={{ flex: 1 }} placeholder="example.com:443" />
|
<Input
|
||||||
|
id={`${uid}-target`}
|
||||||
|
aria-describedby={`${uid}-target-hint`}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
placeholder="example.com:443"
|
||||||
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<Button
|
<Button
|
||||||
icon={<RadarChartOutlined />}
|
icon={<RadarChartOutlined aria-hidden />}
|
||||||
loading={scanning}
|
loading={scanning}
|
||||||
onClick={() => scanRealityTarget()}
|
onClick={() => scanRealityTarget()}
|
||||||
>
|
>
|
||||||
{t('pages.inbounds.form.scan')}
|
{t('pages.inbounds.form.scan')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button icon={<SearchOutlined />} onClick={() => setScannerOpen(true)}>
|
<Button icon={<SearchOutlined aria-hidden />} onClick={() => setScannerOpen(true)}>
|
||||||
{t('pages.inbounds.form.findTargets')}
|
{t('pages.inbounds.form.findTargets')}
|
||||||
</Button>
|
</Button>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
|
<span id={`${uid}-target-hint`} hidden>
|
||||||
|
{t('pages.inbounds.form.realityTargetHint')}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{scanResult && (
|
{scanResult && (
|
||||||
<Form.Item label=" " colon={false}>
|
<Form.Item label=" " colon={false}>
|
||||||
@@ -235,32 +245,45 @@ export default function RealityForm({
|
|||||||
>
|
>
|
||||||
<Input placeholder="x.y.z" />
|
<Input placeholder="x.y.z" />
|
||||||
</FormField>
|
</FormField>
|
||||||
<Form.Item label={t('pages.inbounds.form.shortIds')}>
|
<Form.Item htmlFor={`${uid}-shortIds`} label={t('pages.inbounds.form.shortIds')}>
|
||||||
<Space.Compact block style={{ display: 'flex' }}>
|
<Space.Compact block style={{ display: 'flex' }}>
|
||||||
<FormField name={['streamSettings', 'realitySettings', 'shortIds']} noStyle>
|
<FormField name={['streamSettings', 'realitySettings', 'shortIds']} noStyle>
|
||||||
<Select mode="tags" tokenSeparators={[',']} style={{ flex: 1 }} />
|
<Select
|
||||||
|
id={`${uid}-shortIds`}
|
||||||
|
mode="tags"
|
||||||
|
tokenSeparators={[',']}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
onClick={randomizeShortIds}
|
onClick={randomizeShortIds}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-spiderX`}
|
||||||
label={t('pages.inbounds.form.spiderX')}
|
label={t('pages.inbounds.form.spiderX')}
|
||||||
tooltip={t('pages.inbounds.form.spiderXHint')}
|
tooltip={t('pages.inbounds.form.spiderXHint')}
|
||||||
>
|
>
|
||||||
<Space.Compact block style={{ display: 'flex' }}>
|
<Space.Compact block style={{ display: 'flex' }}>
|
||||||
<FormField name={['streamSettings', 'realitySettings', 'settings', 'spiderX']} noStyle>
|
<FormField name={['streamSettings', 'realitySettings', 'settings', 'spiderX']} noStyle>
|
||||||
<Input style={{ flex: 1 }} />
|
<Input
|
||||||
|
id={`${uid}-spiderX`}
|
||||||
|
aria-describedby={`${uid}-spiderX-hint`}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('regenerate')}
|
aria-label={t('regenerate')}
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
onClick={randomizeSpiderX}
|
onClick={randomizeSpiderX}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
|
<span id={`${uid}-spiderX-hint`} hidden>
|
||||||
|
{t('pages.inbounds.form.spiderXHint')}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<FormField
|
<FormField
|
||||||
name={['streamSettings', 'realitySettings', 'settings', 'publicKey']}
|
name={['streamSettings', 'realitySettings', 'settings', 'publicKey']}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useId } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Form, Input, InputNumber, Radio, Select, Space, Switch } from 'antd';
|
import { Button, Form, Input, InputNumber, Radio, Select, Space, Switch } from 'antd';
|
||||||
import {
|
import {
|
||||||
@@ -77,7 +78,7 @@ function CertRow({
|
|||||||
{total > 1 && (
|
{total > 1 && (
|
||||||
<Form.Item label=" ">
|
<Form.Item label=" ">
|
||||||
<Button size="small" danger onClick={onRemove}>
|
<Button size="small" danger onClick={onRemove}>
|
||||||
<MinusOutlined /> {t('remove')}
|
<MinusOutlined aria-hidden /> {t('remove')}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
@@ -161,16 +162,20 @@ function CertRow({
|
|||||||
|
|
||||||
function EchSockoptSection() {
|
function EchSockoptSection() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const { control, setValue } = useFormContext();
|
const { control, setValue } = useFormContext();
|
||||||
const echSockopt = useWatch({ control, name: 'streamSettings.tlsSettings.echSockopt' });
|
const echSockopt = useWatch({ control, name: 'streamSettings.tlsSettings.echSockopt' });
|
||||||
const on = !!echSockopt;
|
const on = !!echSockopt;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-echSockopt`}
|
||||||
label={t('pages.inbounds.form.echSockopt')}
|
label={t('pages.inbounds.form.echSockopt')}
|
||||||
tooltip={t('pages.inbounds.form.echSockoptTip')}
|
tooltip={t('pages.inbounds.form.echSockoptTip')}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
|
id={`${uid}-echSockopt`}
|
||||||
|
aria-describedby={`${uid}-echSockopt-hint`}
|
||||||
checked={on}
|
checked={on}
|
||||||
onChange={(v) =>
|
onChange={(v) =>
|
||||||
setValue(
|
setValue(
|
||||||
@@ -179,6 +184,9 @@ function EchSockoptSection() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<span id={`${uid}-echSockopt-hint`} hidden>
|
||||||
|
{t('pages.inbounds.form.echSockoptTip')}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{on && (
|
{on && (
|
||||||
<>
|
<>
|
||||||
@@ -226,6 +234,7 @@ export default function TlsForm({
|
|||||||
clearEchCert,
|
clearEchCert,
|
||||||
}: TlsFormProps) {
|
}: TlsFormProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const { control } = useFormContext();
|
const { control } = useFormContext();
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
@@ -242,16 +251,18 @@ export default function TlsForm({
|
|||||||
>
|
>
|
||||||
<CipherSuitesSelect placeholder={t('pages.inbounds.form.autoOption')} />
|
<CipherSuitesSelect placeholder={t('pages.inbounds.form.autoOption')} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<Form.Item label={t('pages.inbounds.form.minMaxVersion')}>
|
<Form.Item htmlFor={`${uid}-minVersion`} label={t('pages.inbounds.form.minMaxVersion')}>
|
||||||
<Space.Compact block>
|
<Space.Compact block>
|
||||||
<FormField name={['streamSettings', 'tlsSettings', 'minVersion']} noStyle>
|
<FormField name={['streamSettings', 'tlsSettings', 'minVersion']} noStyle>
|
||||||
<Select
|
<Select
|
||||||
|
id={`${uid}-minVersion`}
|
||||||
style={{ width: '50%' }}
|
style={{ width: '50%' }}
|
||||||
options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
|
options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField name={['streamSettings', 'tlsSettings', 'maxVersion']} noStyle>
|
<FormField name={['streamSettings', 'tlsSettings', 'maxVersion']} noStyle>
|
||||||
<Select
|
<Select
|
||||||
|
aria-label={t('pages.inbounds.form.maxVersion')}
|
||||||
style={{ width: '50%' }}
|
style={{ width: '50%' }}
|
||||||
options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
|
options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
|
||||||
/>
|
/>
|
||||||
@@ -330,7 +341,7 @@ export default function TlsForm({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<PlusOutlined />
|
<PlusOutlined aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{fields.map((field, idx) => (
|
{fields.map((field, idx) => (
|
||||||
@@ -375,6 +386,7 @@ export default function TlsForm({
|
|||||||
</Space>
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-pinnedPeerCertSha256`}
|
||||||
label={t('pages.inbounds.form.pinnedPeerCertSha256')}
|
label={t('pages.inbounds.form.pinnedPeerCertSha256')}
|
||||||
tooltip={t('pages.inbounds.form.pinnedPeerCertSha256Tip')}
|
tooltip={t('pages.inbounds.form.pinnedPeerCertSha256Tip')}
|
||||||
>
|
>
|
||||||
@@ -384,6 +396,8 @@ export default function TlsForm({
|
|||||||
noStyle
|
noStyle
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
id={`${uid}-pinnedPeerCertSha256`}
|
||||||
|
aria-describedby={`${uid}-pinnedPeerCertSha256-hint`}
|
||||||
mode="tags"
|
mode="tags"
|
||||||
tokenSeparators={[',', ' ']}
|
tokenSeparators={[',', ' ']}
|
||||||
placeholder={t('pages.inbounds.form.pinnedPeerCertSha256Placeholder')}
|
placeholder={t('pages.inbounds.form.pinnedPeerCertSha256Placeholder')}
|
||||||
@@ -391,18 +405,23 @@ export default function TlsForm({
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<Button
|
<Button
|
||||||
icon={<FileProtectOutlined />}
|
icon={<FileProtectOutlined aria-hidden />}
|
||||||
onClick={pinFromCert}
|
onClick={pinFromCert}
|
||||||
loading={saving}
|
loading={saving}
|
||||||
|
aria-label={t('pages.inbounds.form.pinFromCert')}
|
||||||
title={t('pages.inbounds.form.pinFromCert')}
|
title={t('pages.inbounds.form.pinFromCert')}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<CloudDownloadOutlined />}
|
icon={<CloudDownloadOutlined aria-hidden />}
|
||||||
onClick={() => pinFromRemote()}
|
onClick={() => pinFromRemote()}
|
||||||
loading={saving}
|
loading={saving}
|
||||||
|
aria-label={t('pages.inbounds.form.pinFromRemote')}
|
||||||
title={t('pages.inbounds.form.pinFromRemote')}
|
title={t('pages.inbounds.form.pinFromRemote')}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
|
<span id={`${uid}-pinnedPeerCertSha256-hint`} hidden>
|
||||||
|
{t('pages.inbounds.form.pinnedPeerCertSha256Tip')}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<FormField
|
<FormField
|
||||||
name={['streamSettings', 'tlsSettings', 'settings', 'verifyPeerCertByName']}
|
name={['streamSettings', 'tlsSettings', 'settings', 'verifyPeerCertByName']}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useId } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Form, Input, Switch } from 'antd';
|
import { Form, Input, Switch } from 'antd';
|
||||||
import { useFormContext, useWatch } from 'react-hook-form';
|
import { useFormContext, useWatch } from 'react-hook-form';
|
||||||
@@ -7,6 +8,7 @@ import { FormField } from '@/components/form/rhf';
|
|||||||
|
|
||||||
export default function RawForm() {
|
export default function RawForm() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const { control, setValue } = useFormContext();
|
const { control, setValue } = useFormContext();
|
||||||
const headerType = (useWatch({
|
const headerType = (useWatch({
|
||||||
control,
|
control,
|
||||||
@@ -21,8 +23,9 @@ export default function RawForm() {
|
|||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</FormField>
|
</FormField>
|
||||||
<Form.Item label={`HTTP ${t('camouflage')}`}>
|
<Form.Item htmlFor={`${uid}-httpCamouflage`} label={`HTTP ${t('camouflage')}`}>
|
||||||
<Switch
|
<Switch
|
||||||
|
id={`${uid}-httpCamouflage`}
|
||||||
checked={headerType === 'http'}
|
checked={headerType === 'http'}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
setValue(
|
setValue(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useId } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Alert, Form, InputNumber, Segmented, Select, Switch } from 'antd';
|
import { Alert, Form, InputNumber, Segmented, Select, Switch } from 'antd';
|
||||||
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
||||||
@@ -28,6 +29,7 @@ export default function SockoptForm({
|
|||||||
network: string;
|
network: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const { control, getValues, setValue } = useFormContext();
|
const { control, getValues, setValue } = useFormContext();
|
||||||
const sock = useWatch({ control, name: 'streamSettings.sockopt' });
|
const sock = useWatch({ control, name: 'streamSettings.sockopt' });
|
||||||
const on = !!sock && typeof sock === 'object' && Object.keys(sock).length > 0;
|
const on = !!sock && typeof sock === 'object' && Object.keys(sock).length > 0;
|
||||||
@@ -101,6 +103,8 @@ export default function SockoptForm({
|
|||||||
tooltip={t('pages.inbounds.form.realClientIpHint')}
|
tooltip={t('pages.inbounds.form.realClientIpHint')}
|
||||||
>
|
>
|
||||||
<Segmented
|
<Segmented
|
||||||
|
aria-label={t('pages.inbounds.form.realClientIp')}
|
||||||
|
aria-describedby={`${uid}-real-client-ip-hint`}
|
||||||
value={presetValue}
|
value={presetValue}
|
||||||
onChange={(v) => applyRealClientIpPreset(v as RealClientIpPreset)}
|
onChange={(v) => applyRealClientIpPreset(v as RealClientIpPreset)}
|
||||||
options={[
|
options={[
|
||||||
@@ -112,6 +116,9 @@ export default function SockoptForm({
|
|||||||
{ value: 'proxy', label: t('pages.inbounds.form.realClientIpPresetProxyProtocol') },
|
{ value: 'proxy', label: t('pages.inbounds.form.realClientIpPresetProxyProtocol') },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
<span id={`${uid}-real-client-ip-hint`} hidden>
|
||||||
|
{t('pages.inbounds.form.realClientIpHint')}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{trustedMismatch && (
|
{trustedMismatch && (
|
||||||
<Alert
|
<Alert
|
||||||
|
|||||||
@@ -533,7 +533,7 @@ export default function InboundInfoModal({
|
|||||||
<Tooltip title={t('copy')}>
|
<Tooltip title={t('copy')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyText(clientSettings.tgId, t)}
|
onClick={() => copyText(clientSettings.tgId, t)}
|
||||||
/>
|
/>
|
||||||
@@ -552,7 +552,7 @@ export default function InboundInfoModal({
|
|||||||
<Tooltip title={t('copy')}>
|
<Tooltip title={t('copy')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyText(link.link, t)}
|
onClick={() => copyText(link.link, t)}
|
||||||
/>
|
/>
|
||||||
@@ -573,7 +573,7 @@ export default function InboundInfoModal({
|
|||||||
<Tooltip title={t('copy')}>
|
<Tooltip title={t('copy')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyText(subLink, t)}
|
onClick={() => copyText(subLink, t)}
|
||||||
/>
|
/>
|
||||||
@@ -595,7 +595,7 @@ export default function InboundInfoModal({
|
|||||||
<Tooltip title={t('copy')}>
|
<Tooltip title={t('copy')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyText(subJsonLink, t)}
|
onClick={() => copyText(subJsonLink, t)}
|
||||||
/>
|
/>
|
||||||
@@ -714,7 +714,7 @@ export default function InboundInfoModal({
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
className="value-copy"
|
className="value-copy"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyText(encryptionLabel, t)}
|
onClick={() => copyText(encryptionLabel, t)}
|
||||||
/>
|
/>
|
||||||
@@ -984,7 +984,7 @@ export default function InboundInfoModal({
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyText(`${account.user}:${account.pass}`, t)}
|
onClick={() => copyText(`${account.user}:${account.pass}`, t)}
|
||||||
/>
|
/>
|
||||||
@@ -1100,7 +1100,7 @@ export default function InboundInfoModal({
|
|||||||
<Tooltip title={t('copy')}>
|
<Tooltip title={t('copy')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyText(`${account.user}:${account.pass}`, t)}
|
onClick={() => copyText(`${account.user}:${account.pass}`, t)}
|
||||||
/>
|
/>
|
||||||
@@ -1201,7 +1201,7 @@ export default function InboundInfoModal({
|
|||||||
<Tooltip title={t('copy')}>
|
<Tooltip title={t('copy')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyText(cfg, t)}
|
onClick={() => copyText(cfg, t)}
|
||||||
/>
|
/>
|
||||||
@@ -1209,7 +1209,7 @@ export default function InboundInfoModal({
|
|||||||
<Tooltip title={t('download')}>
|
<Tooltip title={t('download')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined aria-hidden />}
|
||||||
aria-label={t('download')}
|
aria-label={t('download')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
downloadText(cfg, peerConfFileName(idx, j, all.length))
|
downloadText(cfg, peerConfFileName(idx, j, all.length))
|
||||||
@@ -1230,7 +1230,7 @@ export default function InboundInfoModal({
|
|||||||
<Tooltip title={t('copy')}>
|
<Tooltip title={t('copy')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyText(link, t)}
|
onClick={() => copyText(link, t)}
|
||||||
/>
|
/>
|
||||||
@@ -1261,7 +1261,7 @@ export default function InboundInfoModal({
|
|||||||
<Tooltip title={t('copy')}>
|
<Tooltip title={t('copy')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyText(cfg, t)}
|
onClick={() => copyText(cfg, t)}
|
||||||
/>
|
/>
|
||||||
@@ -1269,7 +1269,7 @@ export default function InboundInfoModal({
|
|||||||
<Tooltip title={t('download')}>
|
<Tooltip title={t('download')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined aria-hidden />}
|
||||||
aria-label={t('download')}
|
aria-label={t('download')}
|
||||||
onClick={() => downloadText(cfg, peerConfFileName(idx, j, all.length))}
|
onClick={() => downloadText(cfg, peerConfFileName(idx, j, all.length))}
|
||||||
/>
|
/>
|
||||||
@@ -1288,7 +1288,7 @@ export default function InboundInfoModal({
|
|||||||
<Tooltip title={t('copy')}>
|
<Tooltip title={t('copy')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
onClick={() => copyText(link, t)}
|
onClick={() => copyText(link, t)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -193,20 +193,24 @@ export default function InboundList({
|
|||||||
|
|
||||||
const generalActionsMenu: MenuProps = {
|
const generalActionsMenu: MenuProps = {
|
||||||
items: [
|
items: [
|
||||||
{ key: 'import', icon: <ImportOutlined />, label: t('pages.inbounds.importInbound') },
|
{
|
||||||
{ key: 'export', icon: <ExportOutlined />, label: t('pages.inbounds.export') },
|
key: 'import',
|
||||||
|
icon: <ImportOutlined aria-hidden />,
|
||||||
|
label: t('pages.inbounds.importInbound'),
|
||||||
|
},
|
||||||
|
{ key: 'export', icon: <ExportOutlined aria-hidden />, label: t('pages.inbounds.export') },
|
||||||
...(subEnable
|
...(subEnable
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: 'subs',
|
key: 'subs',
|
||||||
icon: <ExportOutlined />,
|
icon: <ExportOutlined aria-hidden />,
|
||||||
label: `${t('pages.inbounds.export')} — ${t('pages.settings.subSettings')}`,
|
label: `${t('pages.inbounds.export')} — ${t('pages.settings.subSettings')}`,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
{
|
{
|
||||||
key: 'resetInbounds',
|
key: 'resetInbounds',
|
||||||
icon: <ReloadOutlined />,
|
icon: <ReloadOutlined aria-hidden />,
|
||||||
label: t('pages.inbounds.resetAllTraffic'),
|
label: t('pages.inbounds.resetAllTraffic'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -221,7 +225,7 @@ export default function InboundList({
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={onAddInbound}
|
onClick={onAddInbound}
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('pages.inbounds.addInbound')}
|
aria-label={t('pages.inbounds.addInbound')}
|
||||||
>
|
>
|
||||||
{!isMobile && t('pages.inbounds.addInbound')}
|
{!isMobile && t('pages.inbounds.addInbound')}
|
||||||
@@ -229,7 +233,7 @@ export default function InboundList({
|
|||||||
<Dropdown trigger={['click']} menu={generalActionsMenu}>
|
<Dropdown trigger={['click']} menu={generalActionsMenu}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<MenuOutlined />}
|
icon={<MenuOutlined aria-hidden />}
|
||||||
aria-label={t('pages.inbounds.generalActions')}
|
aria-label={t('pages.inbounds.generalActions')}
|
||||||
>
|
>
|
||||||
{!isMobile && t('pages.inbounds.generalActions')}
|
{!isMobile && t('pages.inbounds.generalActions')}
|
||||||
@@ -251,7 +255,7 @@ export default function InboundList({
|
|||||||
onChange={(e) => setSearchKey(e.target.value)}
|
onChange={(e) => setSearchKey(e.target.value)}
|
||||||
placeholder={t('search')}
|
placeholder={t('search')}
|
||||||
allowClear
|
allowClear
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined aria-hidden />}
|
||||||
style={{ maxWidth: isMobile ? 110 : 200 }}
|
style={{ maxWidth: isMobile ? 110 : 200 }}
|
||||||
aria-label={t('search')}
|
aria-label={t('search')}
|
||||||
/>
|
/>
|
||||||
@@ -267,7 +271,7 @@ export default function InboundList({
|
|||||||
</Tag>
|
</Tag>
|
||||||
<Button
|
<Button
|
||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined aria-hidden />}
|
||||||
onClick={handleBulkDelete}
|
onClick={handleBulkDelete}
|
||||||
aria-label={t('delete')}
|
aria-label={t('delete')}
|
||||||
>
|
>
|
||||||
@@ -283,7 +287,7 @@ export default function InboundList({
|
|||||||
<div className="inbound-cards">
|
<div className="inbound-cards">
|
||||||
{visibleInbounds.length === 0 ? (
|
{visibleInbounds.length === 0 ? (
|
||||||
<div className="card-empty">
|
<div className="card-empty">
|
||||||
<ImportOutlined style={{ fontSize: 28, opacity: 0.5 }} />
|
<ImportOutlined aria-hidden style={{ fontSize: 28, opacity: 0.5 }} />
|
||||||
<div>{t('noData')}</div>
|
<div>{t('noData')}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -327,6 +331,7 @@ export default function InboundList({
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Switch
|
<Switch
|
||||||
|
aria-label={t('pages.inbounds.enable')}
|
||||||
checked={record.enable}
|
checked={record.enable}
|
||||||
size="small"
|
size="small"
|
||||||
onChange={(next) => onSwitchEnable(record, next)}
|
onChange={(next) => onSwitchEnable(record, next)}
|
||||||
@@ -350,7 +355,7 @@ export default function InboundList({
|
|||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
className="row-action-trigger"
|
className="row-action-trigger"
|
||||||
icon={<MoreOutlined />}
|
icon={<MoreOutlined aria-hidden />}
|
||||||
aria-label={t('more')}
|
aria-label={t('more')}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@@ -377,7 +382,7 @@ export default function InboundList({
|
|||||||
locale={{
|
locale={{
|
||||||
emptyText: (
|
emptyText: (
|
||||||
<div className="card-empty">
|
<div className="card-empty">
|
||||||
<ImportOutlined style={{ fontSize: 32, marginBottom: 8 }} />
|
<ImportOutlined aria-hidden style={{ fontSize: 32, marginBottom: 8 }} />
|
||||||
<div>{t('noData')}</div>
|
<div>{t('noData')}</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -40,69 +40,82 @@ export function buildRowActionsMenu({
|
|||||||
}): MenuProps['items'] {
|
}): MenuProps['items'] {
|
||||||
const items: MenuProps['items'] = [];
|
const items: MenuProps['items'] = [];
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
items.push({ key: 'edit', icon: <EditOutlined />, label: t('edit') });
|
items.push({ key: 'edit', icon: <EditOutlined aria-hidden />, label: t('edit') });
|
||||||
}
|
}
|
||||||
if (isInboundMultiUser(record)) {
|
if (isInboundMultiUser(record)) {
|
||||||
items.push({ key: 'export', icon: <ExportOutlined />, label: t('pages.inbounds.export') });
|
items.push({
|
||||||
|
key: 'export',
|
||||||
|
icon: <ExportOutlined aria-hidden />,
|
||||||
|
label: t('pages.inbounds.export'),
|
||||||
|
});
|
||||||
if (subEnable) {
|
if (subEnable) {
|
||||||
items.push({
|
items.push({
|
||||||
key: 'subs',
|
key: 'subs',
|
||||||
icon: <ExportOutlined />,
|
icon: <ExportOutlined aria-hidden />,
|
||||||
label: `${t('pages.inbounds.export')} — ${t('pages.settings.subSettings')}`,
|
label: `${t('pages.inbounds.export')} — ${t('pages.settings.subSettings')}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
items.push({
|
items.push({
|
||||||
key: 'showInfo',
|
key: 'showInfo',
|
||||||
icon: <InfoCircleOutlined />,
|
icon: <InfoCircleOutlined aria-hidden />,
|
||||||
label: t('pages.inbounds.inboundInfo'),
|
label: t('pages.inbounds.inboundInfo'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
items.push({
|
items.push({
|
||||||
key: 'clipboard',
|
key: 'clipboard',
|
||||||
icon: <CopyOutlined />,
|
icon: <CopyOutlined aria-hidden />,
|
||||||
label: t('pages.inbounds.exportInbound'),
|
label: t('pages.inbounds.exportInbound'),
|
||||||
});
|
});
|
||||||
items.push({
|
items.push({
|
||||||
key: 'resetTraffic',
|
key: 'resetTraffic',
|
||||||
icon: <RetweetOutlined />,
|
icon: <RetweetOutlined aria-hidden />,
|
||||||
label: t('pages.inbounds.resetTraffic'),
|
label: t('pages.inbounds.resetTraffic'),
|
||||||
});
|
});
|
||||||
items.push({ key: 'clone', icon: <BlockOutlined />, label: t('pages.inbounds.clone') });
|
items.push({
|
||||||
|
key: 'clone',
|
||||||
|
icon: <BlockOutlined aria-hidden />,
|
||||||
|
label: t('pages.inbounds.clone'),
|
||||||
|
});
|
||||||
if (isInboundMultiUser(record)) {
|
if (isInboundMultiUser(record)) {
|
||||||
items.push({
|
items.push({
|
||||||
key: 'attachExisting',
|
key: 'attachExisting',
|
||||||
icon: <UsergroupAddOutlined />,
|
icon: <UsergroupAddOutlined aria-hidden />,
|
||||||
label: t('pages.inbounds.attachExistingClients'),
|
label: t('pages.inbounds.attachExistingClients'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isInboundMultiUser(record) && hasClients) {
|
if (isInboundMultiUser(record) && hasClients) {
|
||||||
items.push({
|
items.push({
|
||||||
key: 'attachClients',
|
key: 'attachClients',
|
||||||
icon: <UsergroupAddOutlined />,
|
icon: <UsergroupAddOutlined aria-hidden />,
|
||||||
label: t('pages.inbounds.attachClients'),
|
label: t('pages.inbounds.attachClients'),
|
||||||
});
|
});
|
||||||
items.push({
|
items.push({
|
||||||
key: 'detachClients',
|
key: 'detachClients',
|
||||||
icon: <UsergroupDeleteOutlined />,
|
icon: <UsergroupDeleteOutlined aria-hidden />,
|
||||||
label: t('pages.inbounds.detachClients'),
|
label: t('pages.inbounds.detachClients'),
|
||||||
});
|
});
|
||||||
items.push({
|
items.push({
|
||||||
key: 'addToGroup',
|
key: 'addToGroup',
|
||||||
icon: <TagsOutlined />,
|
icon: <TagsOutlined aria-hidden />,
|
||||||
label: t('pages.inbounds.addClientsToGroup'),
|
label: t('pages.inbounds.addClientsToGroup'),
|
||||||
});
|
});
|
||||||
items.push({ type: 'divider' });
|
items.push({ type: 'divider' });
|
||||||
items.push({
|
items.push({
|
||||||
key: 'delAllClients',
|
key: 'delAllClients',
|
||||||
icon: <UsergroupDeleteOutlined />,
|
icon: <UsergroupDeleteOutlined aria-hidden />,
|
||||||
danger: true,
|
danger: true,
|
||||||
label: t('pages.inbounds.delAllClients'),
|
label: t('pages.inbounds.delAllClients'),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
items.push({ type: 'divider' });
|
items.push({ type: 'divider' });
|
||||||
}
|
}
|
||||||
items.push({ key: 'delete', icon: <DeleteOutlined />, danger: true, label: t('delete') });
|
items.push({
|
||||||
|
key: 'delete',
|
||||||
|
icon: <DeleteOutlined aria-hidden />,
|
||||||
|
danger: true,
|
||||||
|
label: t('delete'),
|
||||||
|
});
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +127,7 @@ export function RowActionsCell({ record, subEnable, hasClients, onClick }: RowAc
|
|||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
style={{ fontSize: 16 }}
|
style={{ fontSize: 16 }}
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined aria-hidden />}
|
||||||
aria-label={t('edit')}
|
aria-label={t('edit')}
|
||||||
onClick={() => onClick('edit')}
|
onClick={() => onClick('edit')}
|
||||||
/>
|
/>
|
||||||
@@ -129,7 +142,7 @@ export function RowActionsCell({ record, subEnable, hasClients, onClick }: RowAc
|
|||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
style={{ fontSize: 16 }}
|
style={{ fontSize: 16 }}
|
||||||
icon={<MoreOutlined />}
|
icon={<MoreOutlined aria-hidden />}
|
||||||
aria-label={t('more')}
|
aria-label={t('more')}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@@ -130,7 +130,11 @@ export function useInboundColumns({
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
width: 80,
|
width: 80,
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Switch checked={record.enable} onChange={(next) => onSwitchEnable(record, next)} />
|
<Switch
|
||||||
|
aria-label={t('pages.inbounds.enable')}
|
||||||
|
checked={record.enable}
|
||||||
|
onChange={(next) => onSwitchEnable(record, next)}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -293,7 +297,7 @@ export function useInboundColumns({
|
|||||||
className="client-count-tag"
|
className="client-count-tag"
|
||||||
style={{ margin: 0, marginRight: 4, padding: '0 2px' }}
|
style={{ margin: 0, marginRight: 4, padding: '0 2px' }}
|
||||||
>
|
>
|
||||||
<TeamOutlined /> {cc.clients}
|
<TeamOutlined aria-hidden /> {cc.clients}
|
||||||
</Tag>
|
</Tag>
|
||||||
{cc.active.length > 0 ? (
|
{cc.active.length > 0 ? (
|
||||||
<Popover
|
<Popover
|
||||||
|
|||||||
@@ -102,13 +102,18 @@ export default function QrPanel({
|
|||||||
{remark}
|
{remark}
|
||||||
</Tag>
|
</Tag>
|
||||||
<Tooltip title={t('copy')}>
|
<Tooltip title={t('copy')}>
|
||||||
<Button size="small" icon={<CopyOutlined />} aria-label={t('copy')} onClick={copy} />
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<CopyOutlined aria-hidden />}
|
||||||
|
aria-label={t('copy')}
|
||||||
|
onClick={copy}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{showQr && (
|
{showQr && (
|
||||||
<Tooltip title={t('downloadImage')}>
|
<Tooltip title={t('downloadImage')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<PictureOutlined />}
|
icon={<PictureOutlined aria-hidden />}
|
||||||
aria-label={t('downloadImage')}
|
aria-label={t('downloadImage')}
|
||||||
onClick={downloadImage}
|
onClick={downloadImage}
|
||||||
/>
|
/>
|
||||||
@@ -118,7 +123,7 @@ export default function QrPanel({
|
|||||||
<Tooltip title={t('download')}>
|
<Tooltip title={t('download')}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined aria-hidden />}
|
||||||
aria-label={t('download')}
|
aria-label={t('download')}
|
||||||
onClick={download}
|
onClick={download}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState, useId } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Checkbox, Empty, Form, Input, Modal, Select, Tag } from 'antd';
|
import { Button, Checkbox, Empty, Form, Input, Modal, Select, Tag } from 'antd';
|
||||||
import { DownloadOutlined, SyncOutlined } from '@ant-design/icons';
|
import { DownloadOutlined, SyncOutlined } from '@ant-design/icons';
|
||||||
@@ -30,6 +30,7 @@ function shortTime(value?: number): string {
|
|||||||
|
|
||||||
export default function AmneziaWGLogModal({ open, onClose }: AmneziaWGLogModalProps) {
|
export default function AmneziaWGLogModal({ open, onClose }: AmneziaWGLogModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const { datepicker } = useDatepicker();
|
const { datepicker } = useDatepicker();
|
||||||
const { isMobile } = useMediaQuery();
|
const { isMobile } = useMediaQuery();
|
||||||
const [rows, setRows] = useState('50');
|
const [rows, setRows] = useState('50');
|
||||||
@@ -120,6 +121,7 @@ export default function AmneziaWGLogModal({ open, onClose }: AmneziaWGLogModalPr
|
|||||||
<Form layout="inline" className="log-toolbar">
|
<Form layout="inline" className="log-toolbar">
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Select
|
<Select
|
||||||
|
aria-label={t('pages.index.logLines')}
|
||||||
value={rows}
|
value={rows}
|
||||||
size="small"
|
size="small"
|
||||||
style={{ width: 100 }}
|
style={{ width: 100 }}
|
||||||
@@ -132,8 +134,9 @@ export default function AmneziaWGLogModal({ open, onClose }: AmneziaWGLogModalPr
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t('filter')} className="filter-item">
|
<Form.Item htmlFor={`${uid}-filter`} label={t('filter')} className="filter-item">
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-filter`}
|
||||||
value={filter}
|
value={filter}
|
||||||
size="small"
|
size="small"
|
||||||
onChange={(e) => setFilter(e.target.value)}
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
@@ -151,7 +154,7 @@ export default function AmneziaWGLogModal({ open, onClose }: AmneziaWGLogModalPr
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={download}
|
onClick={download}
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined aria-hidden />}
|
||||||
aria-label={t('download')}
|
aria-label={t('download')}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export default function BackupModal({
|
|||||||
type="primary"
|
type="primary"
|
||||||
aria-label={t('pages.index.exportDatabase')}
|
aria-label={t('pages.index.exportDatabase')}
|
||||||
onClick={exportDb}
|
onClick={exportDb}
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ export default function BackupModal({
|
|||||||
type="primary"
|
type="primary"
|
||||||
aria-label={t('pages.index.migrationDownload')}
|
aria-label={t('pages.index.migrationDownload')}
|
||||||
onClick={exportMigration}
|
onClick={exportMigration}
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -120,7 +120,7 @@ export default function BackupModal({
|
|||||||
type="primary"
|
type="primary"
|
||||||
aria-label={t('pages.index.importDatabase')}
|
aria-label={t('pages.index.importDatabase')}
|
||||||
onClick={importDb}
|
onClick={importDb}
|
||||||
icon={<UploadOutlined />}
|
icon={<UploadOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState, useId } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Alert, Button, Form, Input, Modal, Select, Space, Spin, Typography, message } from 'antd';
|
import { Alert, Button, Form, Input, Modal, Select, Space, Spin, Typography, message } from 'antd';
|
||||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
@@ -33,6 +33,7 @@ function fileNameFromUrl(url: string): string {
|
|||||||
|
|
||||||
export default function GeodataSection({ active, onBusy, onClose }: GeodataSectionProps) {
|
export default function GeodataSection({ active, onBusy, onClose }: GeodataSectionProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const [modal, modalContextHolder] = Modal.useModal();
|
const [modal, modalContextHolder] = Modal.useModal();
|
||||||
const [messageApi, messageContextHolder] = message.useMessage();
|
const [messageApi, messageContextHolder] = message.useMessage();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -180,15 +181,25 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti
|
|||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<Alert type="info" className="mb-12" title={t('pages.index.geodataHint')} showIcon />
|
<Alert type="info" className="mb-12" title={t('pages.index.geodataHint')} showIcon />
|
||||||
<Form layout="vertical">
|
<Form layout="vertical">
|
||||||
<Form.Item label={t('pages.index.geodataCron')} style={{ marginBottom: 8 }}>
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-cron`}
|
||||||
|
label={t('pages.index.geodataCron')}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-cron`}
|
||||||
value={cron}
|
value={cron}
|
||||||
placeholder={DEFAULT_CRON}
|
placeholder={DEFAULT_CRON}
|
||||||
onChange={(e) => setCron(e.target.value)}
|
onChange={(e) => setCron(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t('pages.index.geodataOutbound')} style={{ marginBottom: 8 }}>
|
<Form.Item
|
||||||
|
htmlFor={`${uid}-outbound`}
|
||||||
|
label={t('pages.index.geodataOutbound')}
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
|
id={`${uid}-outbound`}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
allowClear
|
allowClear
|
||||||
value={outbound}
|
value={outbound}
|
||||||
@@ -204,6 +215,7 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti
|
|||||||
{rows.map((row, i) => (
|
{rows.map((row, i) => (
|
||||||
<Space.Compact key={i} style={{ width: '100%' }}>
|
<Space.Compact key={i} style={{ width: '100%' }}>
|
||||||
<Input
|
<Input
|
||||||
|
aria-label={`URL ${i + 1}`}
|
||||||
style={{ width: '60%' }}
|
style={{ width: '60%' }}
|
||||||
placeholder="https://example.com/geosite_custom.dat"
|
placeholder="https://example.com/geosite_custom.dat"
|
||||||
value={row.url}
|
value={row.url}
|
||||||
@@ -211,6 +223,7 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti
|
|||||||
onBlur={() => onUrlBlur(i)}
|
onBlur={() => onUrlBlur(i)}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
aria-label={`${t('pages.index.geodataFile')} ${i + 1}`}
|
||||||
style={{ width: '40%' }}
|
style={{ width: '40%' }}
|
||||||
placeholder={t('pages.index.geodataFile')}
|
placeholder={t('pages.index.geodataFile')}
|
||||||
value={row.file}
|
value={row.file}
|
||||||
@@ -218,14 +231,14 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('delete')}
|
aria-label={t('delete')}
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined aria-hidden />}
|
||||||
onClick={() => setRows((p) => p.filter((_, j) => j !== i))}
|
onClick={() => setRows((p) => p.filter((_, j) => j !== i))}
|
||||||
/>
|
/>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
))}
|
))}
|
||||||
<div className="actions-row">
|
<div className="actions-row">
|
||||||
<Button
|
<Button
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
onClick={() => setRows((p) => [...p, { url: '', file: '' }])}
|
onClick={() => setRows((p) => [...p, { url: '', file: '' }])}
|
||||||
>
|
>
|
||||||
{t('pages.index.geodataAddFile')}
|
{t('pages.index.geodataAddFile')}
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export default function IndexPage() {
|
|||||||
|
|
||||||
<div className="ov-vitals">
|
<div className="ov-vitals">
|
||||||
<VitalTile
|
<VitalTile
|
||||||
icon={<DashboardOutlined />}
|
icon={<DashboardOutlined aria-hidden />}
|
||||||
label={t('pages.index.cpu')}
|
label={t('pages.index.cpu')}
|
||||||
percent={status.cpu.percent}
|
percent={status.cpu.percent}
|
||||||
statusColor={status.cpu.color}
|
statusColor={status.cpu.color}
|
||||||
@@ -238,7 +238,7 @@ export default function IndexPage() {
|
|||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
<VitalTile
|
<VitalTile
|
||||||
icon={<DatabaseOutlined />}
|
icon={<DatabaseOutlined aria-hidden />}
|
||||||
label={t('pages.index.memory')}
|
label={t('pages.index.memory')}
|
||||||
percent={status.mem.percent}
|
percent={status.mem.percent}
|
||||||
statusColor={status.mem.color}
|
statusColor={status.mem.color}
|
||||||
@@ -249,7 +249,7 @@ export default function IndexPage() {
|
|||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
<VitalTile
|
<VitalTile
|
||||||
icon={<SwapOutlined />}
|
icon={<SwapOutlined aria-hidden />}
|
||||||
label={t('pages.index.swap')}
|
label={t('pages.index.swap')}
|
||||||
percent={status.swap.percent}
|
percent={status.swap.percent}
|
||||||
statusColor={status.swap.color}
|
statusColor={status.swap.color}
|
||||||
@@ -260,7 +260,7 @@ export default function IndexPage() {
|
|||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
<VitalTile
|
<VitalTile
|
||||||
icon={<HddOutlined />}
|
icon={<HddOutlined aria-hidden />}
|
||||||
label={t('pages.index.storage')}
|
label={t('pages.index.storage')}
|
||||||
percent={status.disk.percent}
|
percent={status.disk.percent}
|
||||||
statusColor={status.disk.color}
|
statusColor={status.disk.color}
|
||||||
@@ -358,7 +358,7 @@ export default function IndexPage() {
|
|||||||
key="download"
|
key="download"
|
||||||
onClick={downloadConfig}
|
onClick={downloadConfig}
|
||||||
size={isMobile ? 'small' : 'middle'}
|
size={isMobile ? 'small' : 'middle'}
|
||||||
icon={<CloudDownloadOutlined />}
|
icon={<CloudDownloadOutlined aria-hidden />}
|
||||||
>
|
>
|
||||||
{isMobile ? 'Download' : 'config.json'}
|
{isMobile ? 'Download' : 'config.json'}
|
||||||
</Button>,
|
</Button>,
|
||||||
@@ -367,7 +367,7 @@ export default function IndexPage() {
|
|||||||
type="primary"
|
type="primary"
|
||||||
onClick={copyConfig}
|
onClick={copyConfig}
|
||||||
size={isMobile ? 'small' : 'middle'}
|
size={isMobile ? 'small' : 'middle'}
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
>
|
>
|
||||||
Copy
|
Copy
|
||||||
</Button>,
|
</Button>,
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export default function LogModal({ open, onClose }: LogModalProps) {
|
|||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Space.Compact>
|
<Space.Compact>
|
||||||
<Select
|
<Select
|
||||||
|
aria-label={t('pages.index.logLines')}
|
||||||
value={rows}
|
value={rows}
|
||||||
size="small"
|
size="small"
|
||||||
style={{ width: 100 }}
|
style={{ width: 100 }}
|
||||||
@@ -118,6 +119,7 @@ export default function LogModal({ open, onClose }: LogModalProps) {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
|
aria-label={t('pages.xray.logLevel')}
|
||||||
value={level}
|
value={level}
|
||||||
size="small"
|
size="small"
|
||||||
style={{ minWidth: 95 }}
|
style={{ minWidth: 95 }}
|
||||||
@@ -144,7 +146,7 @@ export default function LogModal({ open, onClose }: LogModalProps) {
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={download}
|
onClick={download}
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined aria-hidden />}
|
||||||
aria-label={t('download')}
|
aria-label={t('download')}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
import { formatPanelVersion } from '@/lib/panel-version';
|
import { formatPanelVersion } from '@/lib/panel-version';
|
||||||
|
import { activateOnKey } from '@/utils/a11y';
|
||||||
import type { Status } from '@/models/status';
|
import type { Status } from '@/models/status';
|
||||||
|
|
||||||
interface OverviewActionBarProps {
|
interface OverviewActionBarProps {
|
||||||
@@ -80,25 +81,30 @@ export default function OverviewActionBar({
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
key: 'restart',
|
key: 'restart',
|
||||||
icon: <ReloadOutlined />,
|
icon: <ReloadOutlined aria-hidden />,
|
||||||
text: t('pages.index.restartXray'),
|
text: t('pages.index.restartXray'),
|
||||||
onClick: onRestartXray,
|
onClick: onRestartXray,
|
||||||
primary: true,
|
primary: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'stop',
|
key: 'stop',
|
||||||
icon: <PoweroffOutlined />,
|
icon: <PoweroffOutlined aria-hidden />,
|
||||||
text: t('pages.index.stopXray'),
|
text: t('pages.index.stopXray'),
|
||||||
onClick: onStopXray,
|
onClick: onStopXray,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ key: 'logs', icon: <BarsOutlined />, text: t('pages.index.logs'), onClick: onOpenLogs },
|
{
|
||||||
|
key: 'logs',
|
||||||
|
icon: <BarsOutlined aria-hidden />,
|
||||||
|
text: t('pages.index.logs'),
|
||||||
|
onClick: onOpenLogs,
|
||||||
|
},
|
||||||
...(accessLogEnable
|
...(accessLogEnable
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: 'accessLogs',
|
key: 'accessLogs',
|
||||||
icon: <FileTextOutlined />,
|
icon: <FileTextOutlined aria-hidden />,
|
||||||
text: t('pages.index.accessLogs'),
|
text: t('pages.index.accessLogs'),
|
||||||
onClick: onOpenXrayLogs,
|
onClick: onOpenXrayLogs,
|
||||||
},
|
},
|
||||||
@@ -108,7 +114,7 @@ export default function OverviewActionBar({
|
|||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: 'amneziawgLogs',
|
key: 'amneziawgLogs',
|
||||||
icon: <ApiOutlined />,
|
icon: <ApiOutlined aria-hidden />,
|
||||||
text: t('pages.index.amneziawgLogs'),
|
text: t('pages.index.amneziawgLogs'),
|
||||||
onClick: onOpenAmneziaWGLogs,
|
onClick: onOpenAmneziaWGLogs,
|
||||||
},
|
},
|
||||||
@@ -116,13 +122,13 @@ export default function OverviewActionBar({
|
|||||||
: []),
|
: []),
|
||||||
{
|
{
|
||||||
key: 'config',
|
key: 'config',
|
||||||
icon: <ControlOutlined />,
|
icon: <ControlOutlined aria-hidden />,
|
||||||
text: t('pages.index.config'),
|
text: t('pages.index.config'),
|
||||||
onClick: onOpenConfig,
|
onClick: onOpenConfig,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'backup',
|
key: 'backup',
|
||||||
icon: <CloudServerOutlined />,
|
icon: <CloudServerOutlined aria-hidden />,
|
||||||
text: t('pages.index.backupTitle'),
|
text: t('pages.index.backupTitle'),
|
||||||
onClick: onOpenBackup,
|
onClick: onOpenBackup,
|
||||||
},
|
},
|
||||||
@@ -130,13 +136,13 @@ export default function OverviewActionBar({
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
key: 'history',
|
key: 'history',
|
||||||
icon: <AreaChartOutlined />,
|
icon: <AreaChartOutlined aria-hidden />,
|
||||||
text: t('pages.index.systemHistoryTitle'),
|
text: t('pages.index.systemHistoryTitle'),
|
||||||
onClick: onOpenSystemHistory,
|
onClick: onOpenSystemHistory,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'metrics',
|
key: 'metrics',
|
||||||
icon: <ArrowUpOutlined />,
|
icon: <ArrowUpOutlined aria-hidden />,
|
||||||
text: t('pages.index.xrayMetricsTitle'),
|
text: t('pages.index.xrayMetricsTitle'),
|
||||||
onClick: onOpenXrayMetrics,
|
onClick: onOpenXrayMetrics,
|
||||||
},
|
},
|
||||||
@@ -177,8 +183,11 @@ export default function OverviewActionBar({
|
|||||||
<Tag
|
<Tag
|
||||||
className="ov-update-tag"
|
className="ov-update-tag"
|
||||||
color="warning"
|
color="warning"
|
||||||
icon={<CloudDownloadOutlined />}
|
icon={<CloudDownloadOutlined aria-hidden />}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
onClick={onOpenPanelUpdate}
|
onClick={onOpenPanelUpdate}
|
||||||
|
onKeyDown={activateOnKey(onOpenPanelUpdate)}
|
||||||
>
|
>
|
||||||
{`${t('update')} ${formatPanelVersion(latestVersion)}`}
|
{`${t('update')} ${formatPanelVersion(latestVersion)}`}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
|||||||
@@ -144,7 +144,12 @@ export default function PanelUpdateModal({
|
|||||||
<div className="version-list">
|
<div className="version-list">
|
||||||
<div className="version-list-item">
|
<div className="version-list-item">
|
||||||
<span>{t('pages.index.devChannel')}</span>
|
<span>{t('pages.index.devChannel')}</span>
|
||||||
<Switch checked={!!devChannelEnable} loading={channelBusy} onChange={handleChannel} />
|
<Switch
|
||||||
|
aria-label={t('pages.index.devChannel')}
|
||||||
|
checked={!!devChannelEnable}
|
||||||
|
loading={channelBusy}
|
||||||
|
onChange={handleChannel}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -190,7 +195,7 @@ export default function PanelUpdateModal({
|
|||||||
type="primary"
|
type="primary"
|
||||||
disabled={!info.updateAvailable}
|
disabled={!info.updateAvailable}
|
||||||
onClick={updatePanel}
|
onClick={updatePanel}
|
||||||
icon={<CloudDownloadOutlined />}
|
icon={<CloudDownloadOutlined aria-hidden />}
|
||||||
>
|
>
|
||||||
{t('pages.index.updatePanel')}
|
{t('pages.index.updatePanel')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const METRICS: MetricDef[] = [
|
|||||||
tab: 'CPU',
|
tab: 'CPU',
|
||||||
tabKey: 'pages.index.cpu',
|
tabKey: 'pages.index.cpu',
|
||||||
title: 'pages.index.historyTitleCpu',
|
title: 'pages.index.historyTitleCpu',
|
||||||
icon: <DashboardOutlined />,
|
icon: <DashboardOutlined aria-hidden />,
|
||||||
valueMax: 100,
|
valueMax: 100,
|
||||||
unit: '%',
|
unit: '%',
|
||||||
stroke: '',
|
stroke: '',
|
||||||
@@ -60,7 +60,7 @@ const METRICS: MetricDef[] = [
|
|||||||
tab: 'RAM',
|
tab: 'RAM',
|
||||||
tabKey: 'pages.index.memory',
|
tabKey: 'pages.index.memory',
|
||||||
title: 'pages.index.historyTitleMem',
|
title: 'pages.index.historyTitleMem',
|
||||||
icon: <DatabaseOutlined />,
|
icon: <DatabaseOutlined aria-hidden />,
|
||||||
valueMax: 100,
|
valueMax: 100,
|
||||||
unit: '%',
|
unit: '%',
|
||||||
stroke: '#7c4dff',
|
stroke: '#7c4dff',
|
||||||
@@ -74,7 +74,7 @@ const METRICS: MetricDef[] = [
|
|||||||
tab: 'Bandwidth',
|
tab: 'Bandwidth',
|
||||||
tabKey: 'pages.index.historyTabBandwidth',
|
tabKey: 'pages.index.historyTabBandwidth',
|
||||||
title: 'pages.index.historyTitleNetwork',
|
title: 'pages.index.historyTitleNetwork',
|
||||||
icon: <GlobalOutlined />,
|
icon: <GlobalOutlined aria-hidden />,
|
||||||
valueMax: null,
|
valueMax: null,
|
||||||
unit: 'B/s',
|
unit: 'B/s',
|
||||||
stroke: '#1890ff',
|
stroke: '#1890ff',
|
||||||
@@ -88,7 +88,7 @@ const METRICS: MetricDef[] = [
|
|||||||
tab: 'Packets',
|
tab: 'Packets',
|
||||||
tabKey: 'pages.index.historyTabPackets',
|
tabKey: 'pages.index.historyTabPackets',
|
||||||
title: 'pages.index.historyTitlePackets',
|
title: 'pages.index.historyTitlePackets',
|
||||||
icon: <DeploymentUnitOutlined />,
|
icon: <DeploymentUnitOutlined aria-hidden />,
|
||||||
valueMax: null,
|
valueMax: null,
|
||||||
unit: 'pkt/s',
|
unit: 'pkt/s',
|
||||||
stroke: '#2f54eb',
|
stroke: '#2f54eb',
|
||||||
@@ -102,7 +102,7 @@ const METRICS: MetricDef[] = [
|
|||||||
tab: 'Connections',
|
tab: 'Connections',
|
||||||
tabKey: 'pages.index.historyTabConnections',
|
tabKey: 'pages.index.historyTabConnections',
|
||||||
title: 'pages.index.historyTitleConnections',
|
title: 'pages.index.historyTitleConnections',
|
||||||
icon: <ApiOutlined />,
|
icon: <ApiOutlined aria-hidden />,
|
||||||
valueMax: null,
|
valueMax: null,
|
||||||
unit: '',
|
unit: '',
|
||||||
stroke: '#597ef7',
|
stroke: '#597ef7',
|
||||||
@@ -116,7 +116,7 @@ const METRICS: MetricDef[] = [
|
|||||||
tab: 'Disk I/O',
|
tab: 'Disk I/O',
|
||||||
tabKey: 'pages.index.historyTabDisk',
|
tabKey: 'pages.index.historyTabDisk',
|
||||||
title: 'pages.index.historyTitleDisk',
|
title: 'pages.index.historyTitleDisk',
|
||||||
icon: <HddOutlined />,
|
icon: <HddOutlined aria-hidden />,
|
||||||
valueMax: null,
|
valueMax: null,
|
||||||
unit: 'B/s',
|
unit: 'B/s',
|
||||||
stroke: '#eb2f96',
|
stroke: '#eb2f96',
|
||||||
@@ -130,7 +130,7 @@ const METRICS: MetricDef[] = [
|
|||||||
tab: 'Disk Usage',
|
tab: 'Disk Usage',
|
||||||
tabKey: 'pages.index.historyTabDiskUsage',
|
tabKey: 'pages.index.historyTabDiskUsage',
|
||||||
title: 'pages.index.historyTitleDiskUsage',
|
title: 'pages.index.historyTitleDiskUsage',
|
||||||
icon: <PieChartOutlined />,
|
icon: <PieChartOutlined aria-hidden />,
|
||||||
valueMax: 100,
|
valueMax: 100,
|
||||||
unit: '%',
|
unit: '%',
|
||||||
stroke: '#13c2c2',
|
stroke: '#13c2c2',
|
||||||
@@ -140,7 +140,7 @@ const METRICS: MetricDef[] = [
|
|||||||
tab: 'Online',
|
tab: 'Online',
|
||||||
tabKey: 'pages.index.historyTabOnline',
|
tabKey: 'pages.index.historyTabOnline',
|
||||||
title: 'pages.index.historyTitleOnline',
|
title: 'pages.index.historyTitleOnline',
|
||||||
icon: <TeamOutlined />,
|
icon: <TeamOutlined aria-hidden />,
|
||||||
valueMax: null,
|
valueMax: null,
|
||||||
unit: '',
|
unit: '',
|
||||||
stroke: '#52c41a',
|
stroke: '#52c41a',
|
||||||
@@ -150,7 +150,7 @@ const METRICS: MetricDef[] = [
|
|||||||
tab: 'Load',
|
tab: 'Load',
|
||||||
tabKey: 'pages.index.historyTabLoad',
|
tabKey: 'pages.index.historyTabLoad',
|
||||||
title: 'pages.index.historyTitleLoad',
|
title: 'pages.index.historyTitleLoad',
|
||||||
icon: <LineChartOutlined />,
|
icon: <LineChartOutlined aria-hidden />,
|
||||||
valueMax: null,
|
valueMax: null,
|
||||||
unit: '',
|
unit: '',
|
||||||
stroke: '#fa8c16',
|
stroke: '#fa8c16',
|
||||||
@@ -334,6 +334,7 @@ export default function SystemHistoryModal({ open, status, onClose }: SystemHist
|
|||||||
<div className="metric-modal-title">
|
<div className="metric-modal-title">
|
||||||
<span>{t('pages.index.systemHistoryTitle')}</span>
|
<span>{t('pages.index.systemHistoryTitle')}</span>
|
||||||
<Select
|
<Select
|
||||||
|
aria-label={t('pages.index.timeRange')}
|
||||||
value={bucket}
|
value={bucket}
|
||||||
size="small"
|
size="small"
|
||||||
className="bucket-select"
|
className="bucket-select"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function SystemStrip({ status, showIp, onToggleIp }: SystemStripP
|
|||||||
<div className="ov-strip-grid">
|
<div className="ov-strip-grid">
|
||||||
<div className="ov-strip-cell">
|
<div className="ov-strip-cell">
|
||||||
<div className="ov-kicker ov-kicker-icon">
|
<div className="ov-kicker ov-kicker-icon">
|
||||||
<ClockCircleOutlined />
|
<ClockCircleOutlined aria-hidden />
|
||||||
{t('pages.index.uptime')}
|
{t('pages.index.uptime')}
|
||||||
</div>
|
</div>
|
||||||
<div className="ov-strip-split">
|
<div className="ov-strip-split">
|
||||||
@@ -46,7 +46,7 @@ export default function SystemStrip({ status, showIp, onToggleIp }: SystemStripP
|
|||||||
|
|
||||||
<div className="ov-strip-cell">
|
<div className="ov-strip-cell">
|
||||||
<div className="ov-kicker ov-kicker-icon">
|
<div className="ov-kicker ov-kicker-icon">
|
||||||
<DatabaseOutlined />
|
<DatabaseOutlined aria-hidden />
|
||||||
{t('pages.index.panel')}
|
{t('pages.index.panel')}
|
||||||
</div>
|
</div>
|
||||||
<div className="ov-strip-split">
|
<div className="ov-strip-split">
|
||||||
@@ -64,7 +64,7 @@ export default function SystemStrip({ status, showIp, onToggleIp }: SystemStripP
|
|||||||
|
|
||||||
<div className="ov-strip-cell">
|
<div className="ov-strip-cell">
|
||||||
<div className="ov-kicker ov-kicker-icon">
|
<div className="ov-kicker ov-kicker-icon">
|
||||||
<GlobalOutlined />
|
<GlobalOutlined aria-hidden />
|
||||||
{t('pages.index.ipAddresses')}
|
{t('pages.index.ipAddresses')}
|
||||||
<Tooltip title={t('pages.index.toggleIpVisibility')}>
|
<Tooltip title={t('pages.index.toggleIpVisibility')}>
|
||||||
{showIp ? (
|
{showIp ? (
|
||||||
|
|||||||
@@ -47,12 +47,12 @@ export default function ThroughputCard({
|
|||||||
</div>
|
</div>
|
||||||
<div className="ov-wide-legend">
|
<div className="ov-wide-legend">
|
||||||
<div className="ov-legend-label">
|
<div className="ov-legend-label">
|
||||||
<ArrowUpOutlined style={{ color: accent }} />
|
<ArrowUpOutlined aria-hidden style={{ color: accent }} />
|
||||||
{t('pages.index.upload')}
|
{t('pages.index.upload')}
|
||||||
<span className="ov-legend-num">{SizeFormatter.speedFormat(status.netIO.up)}</span>
|
<span className="ov-legend-num">{SizeFormatter.speedFormat(status.netIO.up)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ov-legend-label">
|
<div className="ov-legend-label">
|
||||||
<ArrowDownOutlined style={{ color: downColor }} />
|
<ArrowDownOutlined aria-hidden style={{ color: downColor }} />
|
||||||
{t('pages.index.download')}
|
{t('pages.index.download')}
|
||||||
<span className="ov-legend-num">{SizeFormatter.speedFormat(status.netIO.down)}</span>
|
<span className="ov-legend-num">{SizeFormatter.speedFormat(status.netIO.down)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState, useId } from 'react';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Checkbox, Form, Input, Modal, Select, Tag } from 'antd';
|
import { Button, Checkbox, Form, Input, Modal, Select, Tag } from 'antd';
|
||||||
@@ -63,6 +63,7 @@ const AUTO_UPDATE_INTERVAL = 5000;
|
|||||||
|
|
||||||
export default function XrayLogModal({ open, onClose }: XrayLogModalProps) {
|
export default function XrayLogModal({ open, onClose }: XrayLogModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const { datepicker } = useDatepicker();
|
const { datepicker } = useDatepicker();
|
||||||
const { isMobile } = useMediaQuery();
|
const { isMobile } = useMediaQuery();
|
||||||
const [rows, setRows] = useState('20');
|
const [rows, setRows] = useState('20');
|
||||||
@@ -173,6 +174,7 @@ export default function XrayLogModal({ open, onClose }: XrayLogModalProps) {
|
|||||||
<Form layout="inline" className="log-toolbar">
|
<Form layout="inline" className="log-toolbar">
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Select
|
<Select
|
||||||
|
aria-label={t('pages.index.logLines')}
|
||||||
value={rows}
|
value={rows}
|
||||||
size="small"
|
size="small"
|
||||||
style={{ width: 100 }}
|
style={{ width: 100 }}
|
||||||
@@ -186,8 +188,9 @@ export default function XrayLogModal({ open, onClose }: XrayLogModalProps) {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t('filter')} className="filter-item">
|
<Form.Item htmlFor={`${uid}-filter`} label={t('filter')} className="filter-item">
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-filter`}
|
||||||
value={filter}
|
value={filter}
|
||||||
size="small"
|
size="small"
|
||||||
onChange={(e) => setFilter(e.target.value)}
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
@@ -214,7 +217,7 @@ export default function XrayLogModal({ open, onClose }: XrayLogModalProps) {
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={download}
|
onClick={download}
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined aria-hidden />}
|
||||||
aria-label={t('download')}
|
aria-label={t('download')}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const METRICS: MetricDef[] = [
|
|||||||
tab: 'Heap',
|
tab: 'Heap',
|
||||||
tabKey: 'pages.index.xrayTabHeap',
|
tabKey: 'pages.index.xrayTabHeap',
|
||||||
title: 'pages.index.xrayTitleHeap',
|
title: 'pages.index.xrayTitleHeap',
|
||||||
icon: <DatabaseOutlined />,
|
icon: <DatabaseOutlined aria-hidden />,
|
||||||
unit: 'B',
|
unit: 'B',
|
||||||
stroke: '#7c4dff',
|
stroke: '#7c4dff',
|
||||||
},
|
},
|
||||||
@@ -62,7 +62,7 @@ const METRICS: MetricDef[] = [
|
|||||||
tab: 'Sys',
|
tab: 'Sys',
|
||||||
tabKey: 'pages.index.xrayTabSys',
|
tabKey: 'pages.index.xrayTabSys',
|
||||||
title: 'pages.index.xrayTitleSys',
|
title: 'pages.index.xrayTitleSys',
|
||||||
icon: <CloudServerOutlined />,
|
icon: <CloudServerOutlined aria-hidden />,
|
||||||
unit: 'B',
|
unit: 'B',
|
||||||
stroke: '#1890ff',
|
stroke: '#1890ff',
|
||||||
},
|
},
|
||||||
@@ -71,7 +71,7 @@ const METRICS: MetricDef[] = [
|
|||||||
tab: 'Objects',
|
tab: 'Objects',
|
||||||
tabKey: 'pages.index.xrayTabObjects',
|
tabKey: 'pages.index.xrayTabObjects',
|
||||||
title: 'pages.index.xrayTitleObjects',
|
title: 'pages.index.xrayTitleObjects',
|
||||||
icon: <BlockOutlined />,
|
icon: <BlockOutlined aria-hidden />,
|
||||||
unit: '',
|
unit: '',
|
||||||
stroke: '#13c2c2',
|
stroke: '#13c2c2',
|
||||||
},
|
},
|
||||||
@@ -80,7 +80,7 @@ const METRICS: MetricDef[] = [
|
|||||||
tab: 'GC Count',
|
tab: 'GC Count',
|
||||||
tabKey: 'pages.index.xrayTabGcCount',
|
tabKey: 'pages.index.xrayTabGcCount',
|
||||||
title: 'pages.index.xrayTitleGcCount',
|
title: 'pages.index.xrayTitleGcCount',
|
||||||
icon: <DeleteOutlined />,
|
icon: <DeleteOutlined aria-hidden />,
|
||||||
unit: '',
|
unit: '',
|
||||||
stroke: '#fa8c16',
|
stroke: '#fa8c16',
|
||||||
},
|
},
|
||||||
@@ -89,7 +89,7 @@ const METRICS: MetricDef[] = [
|
|||||||
tab: 'GC Pause',
|
tab: 'GC Pause',
|
||||||
tabKey: 'pages.index.xrayTabGcPause',
|
tabKey: 'pages.index.xrayTabGcPause',
|
||||||
title: 'pages.index.xrayTitleGcPause',
|
title: 'pages.index.xrayTitleGcPause',
|
||||||
icon: <PauseCircleOutlined />,
|
icon: <PauseCircleOutlined aria-hidden />,
|
||||||
unit: 'ns',
|
unit: 'ns',
|
||||||
stroke: '#f5222d',
|
stroke: '#f5222d',
|
||||||
},
|
},
|
||||||
@@ -98,7 +98,7 @@ const METRICS: MetricDef[] = [
|
|||||||
tab: 'Observatory',
|
tab: 'Observatory',
|
||||||
tabKey: 'pages.index.xrayTabObservatory',
|
tabKey: 'pages.index.xrayTabObservatory',
|
||||||
title: 'pages.index.xrayTitleObservatory',
|
title: 'pages.index.xrayTitleObservatory',
|
||||||
icon: <EyeOutlined />,
|
icon: <EyeOutlined aria-hidden />,
|
||||||
unit: 'ms',
|
unit: 'ms',
|
||||||
stroke: '#52c41a',
|
stroke: '#52c41a',
|
||||||
},
|
},
|
||||||
@@ -304,6 +304,7 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
|
|||||||
<div className="metric-modal-title">
|
<div className="metric-modal-title">
|
||||||
<span>{t('pages.index.xrayMetricsTitle')}</span>
|
<span>{t('pages.index.xrayMetricsTitle')}</span>
|
||||||
<Select
|
<Select
|
||||||
|
aria-label={t('pages.index.timeRange')}
|
||||||
value={bucket}
|
value={bucket}
|
||||||
size="small"
|
size="small"
|
||||||
className="bucket-select"
|
className="bucket-select"
|
||||||
@@ -362,6 +363,7 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
|
|||||||
) : (
|
) : (
|
||||||
<div className="obs-controls">
|
<div className="obs-controls">
|
||||||
<Select
|
<Select
|
||||||
|
aria-label={t('pages.index.xrayObservatoryTagPlaceholder')}
|
||||||
value={obsActiveTag}
|
value={obsActiveTag}
|
||||||
size="small"
|
size="small"
|
||||||
className="obs-select"
|
className="obs-select"
|
||||||
|
|||||||
@@ -128,7 +128,13 @@ export default function LoginPage() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const themeIcon = !isDark ? <SunOutlined /> : !isUltra ? <MoonOutlined /> : <MoonFilled />;
|
const themeIcon = !isDark ? (
|
||||||
|
<SunOutlined aria-hidden />
|
||||||
|
) : !isUltra ? (
|
||||||
|
<MoonOutlined aria-hidden />
|
||||||
|
) : (
|
||||||
|
<MoonFilled aria-hidden />
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider theme={antdThemeConfig}>
|
<ConfigProvider theme={antdThemeConfig}>
|
||||||
@@ -167,7 +173,7 @@ export default function LoginPage() {
|
|||||||
size="large"
|
size="large"
|
||||||
className="toolbar-btn"
|
className="toolbar-btn"
|
||||||
aria-label={t('pages.settings.language')}
|
aria-label={t('pages.settings.language')}
|
||||||
icon={<TranslationOutlined />}
|
icon={<TranslationOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,7 +205,7 @@ export default function LoginPage() {
|
|||||||
rules={{ validate: rhfZodValidate(LoginFormSchema.shape.username) }}
|
rules={{ validate: rhfZodValidate(LoginFormSchema.shape.username) }}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
prefix={<UserOutlined />}
|
prefix={<UserOutlined aria-hidden />}
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
size="large"
|
size="large"
|
||||||
placeholder={t('username')}
|
placeholder={t('username')}
|
||||||
@@ -213,7 +219,7 @@ export default function LoginPage() {
|
|||||||
rules={{ validate: rhfZodValidate(LoginFormSchema.shape.password) }}
|
rules={{ validate: rhfZodValidate(LoginFormSchema.shape.password) }}
|
||||||
>
|
>
|
||||||
<Input.Password
|
<Input.Password
|
||||||
prefix={<LockOutlined />}
|
prefix={<LockOutlined aria-hidden />}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
size="large"
|
size="large"
|
||||||
placeholder={t('password')}
|
placeholder={t('password')}
|
||||||
@@ -227,7 +233,7 @@ export default function LoginPage() {
|
|||||||
rules={{ validate: rhfZodValidate(TwoFactorCodeSchema) }}
|
rules={{ validate: rhfZodValidate(TwoFactorCodeSchema) }}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
prefix={<KeyOutlined />}
|
prefix={<KeyOutlined aria-hidden />}
|
||||||
autoComplete="one-time-code"
|
autoComplete="one-time-code"
|
||||||
size="large"
|
size="large"
|
||||||
placeholder={t('twoFactorCode')}
|
placeholder={t('twoFactorCode')}
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ export default function NodeList({
|
|||||||
parent: record.parentGuid ? nameByGuid.get(record.parentGuid) || '-' : '-',
|
parent: record.parentGuid ? nameByGuid.get(record.parentGuid) || '-' : '-',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Tag icon={<ApartmentOutlined />} style={{ margin: 0 }}>
|
<Tag icon={<ApartmentOutlined aria-hidden />} style={{ margin: 0 }}>
|
||||||
{t('pages.nodes.subNode')}
|
{t('pages.nodes.subNode')}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -263,7 +263,7 @@ export default function NodeList({
|
|||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
style={{ fontSize: 16 }}
|
style={{ fontSize: 16 }}
|
||||||
icon={<ThunderboltOutlined />}
|
icon={<ThunderboltOutlined aria-hidden />}
|
||||||
aria-label={t('pages.nodes.probe')}
|
aria-label={t('pages.nodes.probe')}
|
||||||
onClick={() => onProbe(record)}
|
onClick={() => onProbe(record)}
|
||||||
/>
|
/>
|
||||||
@@ -274,7 +274,7 @@ export default function NodeList({
|
|||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
style={{ fontSize: 16 }}
|
style={{ fontSize: 16 }}
|
||||||
icon={<CloudDownloadOutlined />}
|
icon={<CloudDownloadOutlined aria-hidden />}
|
||||||
aria-label={t('pages.nodes.updatePanel')}
|
aria-label={t('pages.nodes.updatePanel')}
|
||||||
onClick={() => onUpdateNode(record)}
|
onClick={() => onUpdateNode(record)}
|
||||||
/>
|
/>
|
||||||
@@ -285,7 +285,7 @@ export default function NodeList({
|
|||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
style={{ fontSize: 16 }}
|
style={{ fontSize: 16 }}
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined aria-hidden />}
|
||||||
aria-label={t('edit')}
|
aria-label={t('edit')}
|
||||||
onClick={() => onEdit(record)}
|
onClick={() => onEdit(record)}
|
||||||
/>
|
/>
|
||||||
@@ -296,7 +296,7 @@ export default function NodeList({
|
|||||||
size="small"
|
size="small"
|
||||||
danger
|
danger
|
||||||
style={{ fontSize: 16 }}
|
style={{ fontSize: 16 }}
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined aria-hidden />}
|
||||||
aria-label={t('delete')}
|
aria-label={t('delete')}
|
||||||
onClick={() => onDelete(record)}
|
onClick={() => onDelete(record)}
|
||||||
/>
|
/>
|
||||||
@@ -316,6 +316,7 @@ export default function NodeList({
|
|||||||
<Switch
|
<Switch
|
||||||
checked={!!record.enable}
|
checked={!!record.enable}
|
||||||
size="small"
|
size="small"
|
||||||
|
aria-label={t('pages.nodes.enable')}
|
||||||
onChange={(v) => onToggleEnable(record, v)}
|
onChange={(v) => onToggleEnable(record, v)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -331,7 +332,7 @@ export default function NodeList({
|
|||||||
>
|
>
|
||||||
<span className="name">
|
<span className="name">
|
||||||
{record.transitive && (
|
{record.transitive && (
|
||||||
<ApartmentOutlined style={{ marginInlineEnd: 6, opacity: 0.6 }} />
|
<ApartmentOutlined aria-hidden style={{ marginInlineEnd: 6, opacity: 0.6 }} />
|
||||||
)}
|
)}
|
||||||
{record.name}
|
{record.name}
|
||||||
</span>
|
</span>
|
||||||
@@ -391,7 +392,7 @@ export default function NodeList({
|
|||||||
<StatusLabel status={record.status} xrayState={record.xrayState} />
|
<StatusLabel status={record.status} xrayState={record.xrayState} />
|
||||||
{tip && (
|
{tip && (
|
||||||
<Tooltip title={tip}>
|
<Tooltip title={tip}>
|
||||||
<ExclamationCircleOutlined style={{ color: iconColor }} />
|
<ExclamationCircleOutlined style={{ color: iconColor }} aria-label={tip} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -460,7 +461,7 @@ export default function NodeList({
|
|||||||
render: (_value, record) => (
|
render: (_value, record) => (
|
||||||
<Space size={2}>
|
<Space size={2}>
|
||||||
<Tag className="client-count-tag" style={{ margin: 0, padding: '0 2px' }}>
|
<Tag className="client-count-tag" style={{ margin: 0, padding: '0 2px' }}>
|
||||||
<TeamOutlined /> {record.clientCount || 0}
|
<TeamOutlined aria-hidden /> {record.clientCount || 0}
|
||||||
</Tag>
|
</Tag>
|
||||||
{record.activeCount ? (
|
{record.activeCount ? (
|
||||||
<Tooltip title={t('subscription.active')}>
|
<Tooltip title={t('subscription.active')}>
|
||||||
@@ -562,7 +563,7 @@ export default function NodeList({
|
|||||||
locale={{
|
locale={{
|
||||||
emptyText: (
|
emptyText: (
|
||||||
<div className="card-empty">
|
<div className="card-empty">
|
||||||
<ClusterOutlined style={{ fontSize: 32, marginBottom: 8 }} />
|
<ClusterOutlined aria-hidden style={{ fontSize: 32, marginBottom: 8 }} />
|
||||||
<div>{t('noData')}</div>
|
<div>{t('noData')}</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -579,14 +580,14 @@ export default function NodeList({
|
|||||||
return (
|
return (
|
||||||
<Card size="small" hoverable>
|
<Card size="small" hoverable>
|
||||||
<div className="toolbar">
|
<div className="toolbar">
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={onAdd}>
|
<Button type="primary" icon={<PlusOutlined aria-hidden />} onClick={onAdd}>
|
||||||
{t('pages.nodes.addNode')}
|
{t('pages.nodes.addNode')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button icon={<SafetyCertificateOutlined />} onClick={onMtls}>
|
<Button icon={<SafetyCertificateOutlined aria-hidden />} onClick={onMtls}>
|
||||||
{t('pages.nodes.mtls.title')}
|
{t('pages.nodes.mtls.title')}
|
||||||
</Button>
|
</Button>
|
||||||
{selectedIds.length > 0 && (
|
{selectedIds.length > 0 && (
|
||||||
<Button icon={<CloudDownloadOutlined />} onClick={onUpdateSelected}>
|
<Button icon={<CloudDownloadOutlined aria-hidden />} onClick={onUpdateSelected}>
|
||||||
{t('pages.nodes.updateSelected', { count: selectedIds.length })}
|
{t('pages.nodes.updateSelected', { count: selectedIds.length })}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -597,7 +598,7 @@ export default function NodeList({
|
|||||||
<div className="node-cards">
|
<div className="node-cards">
|
||||||
{dataSource.length === 0 ? (
|
{dataSource.length === 0 ? (
|
||||||
<div className="card-empty">
|
<div className="card-empty">
|
||||||
<ClusterOutlined style={{ fontSize: 28, opacity: 0.5 }} />
|
<ClusterOutlined aria-hidden style={{ fontSize: 28, opacity: 0.5 }} />
|
||||||
<div>{t('noData')}</div>
|
<div>{t('noData')}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -609,11 +610,11 @@ export default function NodeList({
|
|||||||
style={{ paddingInlineStart: 16, opacity: 0.85 }}
|
style={{ paddingInlineStart: 16, opacity: 0.85 }}
|
||||||
>
|
>
|
||||||
<div className="card-head">
|
<div className="card-head">
|
||||||
<ApartmentOutlined style={{ opacity: 0.6 }} />
|
<ApartmentOutlined aria-hidden style={{ opacity: 0.6 }} />
|
||||||
<StatusDot status={record.status} xrayState={record.xrayState} />
|
<StatusDot status={record.status} xrayState={record.xrayState} />
|
||||||
<span className="node-name">{record.name}</span>
|
<span className="node-name">{record.name}</span>
|
||||||
<div className="card-actions">
|
<div className="card-actions">
|
||||||
<Tag icon={<ApartmentOutlined />} style={{ margin: 0 }}>
|
<Tag icon={<ApartmentOutlined aria-hidden />} style={{ margin: 0 }}>
|
||||||
{t('pages.nodes.subNode')}
|
{t('pages.nodes.subNode')}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
@@ -653,6 +654,7 @@ export default function NodeList({
|
|||||||
<Switch
|
<Switch
|
||||||
checked={!!record.enable}
|
checked={!!record.enable}
|
||||||
size="small"
|
size="small"
|
||||||
|
aria-label={t('pages.nodes.enable')}
|
||||||
onChange={(v) => onToggleEnable(record, v)}
|
onChange={(v) => onToggleEnable(record, v)}
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -664,7 +666,7 @@ export default function NodeList({
|
|||||||
key: 'probe',
|
key: 'probe',
|
||||||
label: (
|
label: (
|
||||||
<>
|
<>
|
||||||
<ThunderboltOutlined /> {t('pages.nodes.probe')}
|
<ThunderboltOutlined aria-hidden /> {t('pages.nodes.probe')}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
onClick: () => onProbe(record),
|
onClick: () => onProbe(record),
|
||||||
@@ -675,7 +677,8 @@ export default function NodeList({
|
|||||||
key: 'update',
|
key: 'update',
|
||||||
label: (
|
label: (
|
||||||
<>
|
<>
|
||||||
<CloudDownloadOutlined /> {t('pages.nodes.updatePanel')}
|
<CloudDownloadOutlined aria-hidden />{' '}
|
||||||
|
{t('pages.nodes.updatePanel')}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
onClick: () => onUpdateNode(record),
|
onClick: () => onUpdateNode(record),
|
||||||
@@ -686,7 +689,7 @@ export default function NodeList({
|
|||||||
key: 'edit',
|
key: 'edit',
|
||||||
label: (
|
label: (
|
||||||
<>
|
<>
|
||||||
<EditOutlined /> {t('edit')}
|
<EditOutlined aria-hidden /> {t('edit')}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
onClick: () => onEdit(record),
|
onClick: () => onEdit(record),
|
||||||
@@ -696,7 +699,7 @@ export default function NodeList({
|
|||||||
danger: true,
|
danger: true,
|
||||||
label: (
|
label: (
|
||||||
<>
|
<>
|
||||||
<DeleteOutlined /> {t('delete')}
|
<DeleteOutlined aria-hidden /> {t('delete')}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
onClick: () => onDelete(record),
|
onClick: () => onDelete(record),
|
||||||
@@ -708,7 +711,7 @@ export default function NodeList({
|
|||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
className="row-action-trigger"
|
className="row-action-trigger"
|
||||||
icon={<MoreOutlined />}
|
icon={<MoreOutlined aria-hidden />}
|
||||||
aria-label={t('more')}
|
aria-label={t('more')}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@@ -782,7 +785,7 @@ export default function NodeList({
|
|||||||
const { tip, iconColor } = statusIssue(statsNode);
|
const { tip, iconColor } = statusIssue(statsNode);
|
||||||
return tip ? (
|
return tip ? (
|
||||||
<Tooltip title={tip}>
|
<Tooltip title={tip}>
|
||||||
<ExclamationCircleOutlined style={{ color: iconColor }} />
|
<ExclamationCircleOutlined style={{ color: iconColor }} aria-label={tip} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null;
|
) : null;
|
||||||
})()}
|
})()}
|
||||||
@@ -820,7 +823,7 @@ export default function NodeList({
|
|||||||
<div className="stat-row">
|
<div className="stat-row">
|
||||||
<span className="stat-label">{t('clients')}</span>
|
<span className="stat-label">{t('clients')}</span>
|
||||||
<Tag>
|
<Tag>
|
||||||
<TeamOutlined /> {statsNode.clientCount || 0}
|
<TeamOutlined aria-hidden /> {statsNode.clientCount || 0}
|
||||||
</Tag>
|
</Tag>
|
||||||
{statsNode.activeCount ? (
|
{statsNode.activeCount ? (
|
||||||
<Tag color="green">
|
<Tag color="green">
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ export default function NodesPage() {
|
|||||||
<Statistic
|
<Statistic
|
||||||
title={t('pages.nodes.totalNodes')}
|
title={t('pages.nodes.totalNodes')}
|
||||||
value={String(totals.total)}
|
value={String(totals.total)}
|
||||||
prefix={<CloudServerOutlined />}
|
prefix={<CloudServerOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={12} md={6}>
|
<Col xs={12} sm={12} md={6}>
|
||||||
@@ -321,7 +321,10 @@ export default function NodesPage() {
|
|||||||
title={t('pages.nodes.onlineNodes')}
|
title={t('pages.nodes.onlineNodes')}
|
||||||
value={String(totals.online)}
|
value={String(totals.online)}
|
||||||
prefix={
|
prefix={
|
||||||
<CheckCircleOutlined style={{ color: 'var(--ant-color-success)' }} />
|
<CheckCircleOutlined
|
||||||
|
aria-hidden
|
||||||
|
style={{ color: 'var(--ant-color-success)' }}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -330,7 +333,10 @@ export default function NodesPage() {
|
|||||||
title={t('pages.nodes.offlineNodes')}
|
title={t('pages.nodes.offlineNodes')}
|
||||||
value={String(totals.offline)}
|
value={String(totals.offline)}
|
||||||
prefix={
|
prefix={
|
||||||
<CloseCircleOutlined style={{ color: 'var(--ant-color-error)' }} />
|
<CloseCircleOutlined
|
||||||
|
aria-hidden
|
||||||
|
style={{ color: 'var(--ant-color-error)' }}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -338,7 +344,7 @@ export default function NodesPage() {
|
|||||||
<Statistic
|
<Statistic
|
||||||
title={t('pages.nodes.avgLatency')}
|
title={t('pages.nodes.avgLatency')}
|
||||||
value={totals.avgLatency > 0 ? `${totals.avgLatency} ms` : '-'}
|
value={totals.avgLatency > 0 ? `${totals.avgLatency} ms` : '-'}
|
||||||
prefix={<ThunderboltOutlined />}
|
prefix={<ThunderboltOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -398,6 +404,7 @@ export default function NodesPage() {
|
|||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
<Typography.Text strong>{t('pages.nodes.mtls.trustLabel')}</Typography.Text>
|
<Typography.Text strong>{t('pages.nodes.mtls.trustLabel')}</Typography.Text>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
|
aria-label={t('pages.nodes.mtls.trustLabel')}
|
||||||
rows={5}
|
rows={5}
|
||||||
value={trustCa}
|
value={trustCa}
|
||||||
onChange={(e) => setTrustCa(e.target.value)}
|
onChange={(e) => setTrustCa(e.target.value)}
|
||||||
|
|||||||
@@ -67,7 +67,11 @@ export default function DiscordTab({ allSetting, updateSetting }: DiscordTabProp
|
|||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: '1',
|
key: '1',
|
||||||
label: catTabLabel(<SettingOutlined />, t('pages.settings.panelSettings'), isMobile),
|
label: catTabLabel(
|
||||||
|
<SettingOutlined aria-hidden />,
|
||||||
|
t('pages.settings.panelSettings'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -138,7 +142,7 @@ export default function DiscordTab({ allSetting, updateSetting }: DiscordTabProp
|
|||||||
<Space orientation="vertical" size={8} style={{ width: '100%', marginTop: 16 }}>
|
<Space orientation="vertical" size={8} style={{ width: '100%', marginTop: 16 }}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<SendOutlined />}
|
icon={<SendOutlined aria-hidden />}
|
||||||
loading={testLoading}
|
loading={testLoading}
|
||||||
disabled={!allSetting.discordBotEnable}
|
disabled={!allSetting.discordBotEnable}
|
||||||
onClick={handleTestDiscord}
|
onClick={handleTestDiscord}
|
||||||
@@ -159,7 +163,11 @@ export default function DiscordTab({ allSetting, updateSetting }: DiscordTabProp
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '2',
|
key: '2',
|
||||||
label: catTabLabel(<BellOutlined />, t('pages.settings.notifications'), isMobile),
|
label: catTabLabel(
|
||||||
|
<BellOutlined aria-hidden />,
|
||||||
|
t('pages.settings.notifications'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
|
|||||||
@@ -56,7 +56,11 @@ export default function EmailTab({ allSetting, updateSetting }: EmailTabProps) {
|
|||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: '1',
|
key: '1',
|
||||||
label: catTabLabel(<SettingOutlined />, t('pages.settings.smtpSettings'), isMobile),
|
label: catTabLabel(
|
||||||
|
<SettingOutlined aria-hidden />,
|
||||||
|
t('pages.settings.smtpSettings'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -184,7 +188,7 @@ export default function EmailTab({ allSetting, updateSetting }: EmailTabProps) {
|
|||||||
<Space orientation="vertical" size={8} style={{ width: '100%', marginTop: 16 }}>
|
<Space orientation="vertical" size={8} style={{ width: '100%', marginTop: 16 }}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<SendOutlined />}
|
icon={<SendOutlined aria-hidden />}
|
||||||
loading={testLoading}
|
loading={testLoading}
|
||||||
onClick={handleTestSmtp}
|
onClick={handleTestSmtp}
|
||||||
>
|
>
|
||||||
@@ -213,7 +217,11 @@ export default function EmailTab({ allSetting, updateSetting }: EmailTabProps) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '2',
|
key: '2',
|
||||||
label: catTabLabel(<MailOutlined />, t('pages.settings.emailNotifications'), isMobile),
|
label: catTabLabel(
|
||||||
|
<MailOutlined aria-hidden />,
|
||||||
|
t('pages.settings.emailNotifications'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
|
|||||||
@@ -180,7 +180,11 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
|
|||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: '1',
|
key: '1',
|
||||||
label: catTabLabel(<SettingOutlined />, t('pages.settings.panelSettings'), isMobile),
|
label: catTabLabel(
|
||||||
|
<SettingOutlined aria-hidden />,
|
||||||
|
t('pages.settings.panelSettings'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -349,7 +353,11 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '2',
|
key: '2',
|
||||||
label: catTabLabel(<BellOutlined />, t('pages.settings.notifications'), isMobile),
|
label: catTabLabel(
|
||||||
|
<BellOutlined aria-hidden />,
|
||||||
|
t('pages.settings.notifications'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -386,7 +394,11 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '3',
|
key: '3',
|
||||||
label: catTabLabel(<SafetyCertificateOutlined />, t('pages.settings.certs'), isMobile),
|
label: catTabLabel(
|
||||||
|
<SafetyCertificateOutlined aria-hidden />,
|
||||||
|
t('pages.settings.certs'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -414,7 +426,11 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '4',
|
key: '4',
|
||||||
label: catTabLabel(<GlobalOutlined />, t('pages.settings.externalTraffic'), isMobile),
|
label: catTabLabel(
|
||||||
|
<GlobalOutlined aria-hidden />,
|
||||||
|
t('pages.settings.externalTraffic'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -443,7 +459,11 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '5',
|
key: '5',
|
||||||
label: catTabLabel(<ClockCircleOutlined />, t('pages.settings.dateAndTime'), isMobile),
|
label: catTabLabel(
|
||||||
|
<ClockCircleOutlined aria-hidden />,
|
||||||
|
t('pages.settings.dateAndTime'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -476,7 +496,7 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '6',
|
key: '6',
|
||||||
label: catTabLabel(<ApartmentOutlined />, 'LDAP', isMobile),
|
label: catTabLabel(<ApartmentOutlined aria-hidden />, 'LDAP', isMobile),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.enable')}>
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.enable')}>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export default function HappSettingsContent({
|
|||||||
{
|
{
|
||||||
key: 'routing',
|
key: 'routing',
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<BranchesOutlined />,
|
<BranchesOutlined aria-hidden />,
|
||||||
t('pages.settings.subHappGroupRouting'),
|
t('pages.settings.subHappGroupRouting'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -136,7 +136,7 @@ export default function HappSettingsContent({
|
|||||||
title={t('pages.settings.subHappVisualBuilder')}
|
title={t('pages.settings.subHappVisualBuilder')}
|
||||||
description={t('pages.settings.subHappVisualBuilderDesc')}
|
description={t('pages.settings.subHappVisualBuilderDesc')}
|
||||||
>
|
>
|
||||||
<Button icon={<BuildOutlined />} onClick={() => setIsModalOpen(true)}>
|
<Button icon={<BuildOutlined aria-hidden />} onClick={() => setIsModalOpen(true)}>
|
||||||
{t('pages.settings.subHappVisualBuilder')}
|
{t('pages.settings.subHappVisualBuilder')}
|
||||||
</Button>
|
</Button>
|
||||||
</SettingListItem>
|
</SettingListItem>
|
||||||
@@ -182,7 +182,7 @@ export default function HappSettingsContent({
|
|||||||
{
|
{
|
||||||
key: 'appearance',
|
key: 'appearance',
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<DesktopOutlined />,
|
<DesktopOutlined aria-hidden />,
|
||||||
t('pages.settings.subHappGroupThemes'),
|
t('pages.settings.subHappGroupThemes'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -284,6 +284,7 @@ export default function HappSettingsContent({
|
|||||||
>
|
>
|
||||||
<Space orientation="vertical" style={{ width: '100%' }}>
|
<Space orientation="vertical" style={{ width: '100%' }}>
|
||||||
<Input
|
<Input
|
||||||
|
aria-label={t('pages.settings.subHappColorProfile')}
|
||||||
value={allSetting.subHappColorProfile}
|
value={allSetting.subHappColorProfile}
|
||||||
placeholder='{"serverRowBackgroundColor":"#21003D67"} or resetcolors'
|
placeholder='{"serverRowBackgroundColor":"#21003D67"} or resetcolors'
|
||||||
onChange={(e) => updateSetting({ subHappColorProfile: e.target.value })}
|
onChange={(e) => updateSetting({ subHappColorProfile: e.target.value })}
|
||||||
@@ -326,7 +327,7 @@ export default function HappSettingsContent({
|
|||||||
{
|
{
|
||||||
key: 'network',
|
key: 'network',
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<ThunderboltOutlined />,
|
<ThunderboltOutlined aria-hidden />,
|
||||||
t('pages.settings.subHappGroupNetwork'),
|
t('pages.settings.subHappGroupNetwork'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -493,7 +494,7 @@ export default function HappSettingsContent({
|
|||||||
{
|
{
|
||||||
key: 'failover',
|
key: 'failover',
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<CloudSyncOutlined />,
|
<CloudSyncOutlined aria-hidden />,
|
||||||
t('pages.settings.subHappGroupFailover'),
|
t('pages.settings.subHappGroupFailover'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -53,7 +53,11 @@ export default function IncySettingsContent({
|
|||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: 'app',
|
key: 'app',
|
||||||
label: catTabLabel(<AppstoreOutlined />, t('pages.settings.subIncyGroupApp'), isMobile),
|
label: catTabLabel(
|
||||||
|
<AppstoreOutlined aria-hidden />,
|
||||||
|
t('pages.settings.subIncyGroupApp'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -127,7 +131,7 @@ export default function IncySettingsContent({
|
|||||||
{
|
{
|
||||||
key: 'banners',
|
key: 'banners',
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<NotificationOutlined />,
|
<NotificationOutlined aria-hidden />,
|
||||||
t('pages.settings.subIncyGroupBanners'),
|
t('pages.settings.subIncyGroupBanners'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -196,7 +200,7 @@ export default function IncySettingsContent({
|
|||||||
{
|
{
|
||||||
key: 'privacy',
|
key: 'privacy',
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<SafetyOutlined />,
|
<SafetyOutlined aria-hidden />,
|
||||||
t('pages.settings.subIncyGroupPrivacy'),
|
t('pages.settings.subIncyGroupPrivacy'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -291,7 +295,7 @@ export default function IncySettingsContent({
|
|||||||
{
|
{
|
||||||
key: 'network',
|
key: 'network',
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<ThunderboltOutlined />,
|
<ThunderboltOutlined aria-hidden />,
|
||||||
t('pages.settings.subIncyGroupNetwork'),
|
t('pages.settings.subIncyGroupNetwork'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -450,7 +454,7 @@ export default function IncySettingsContent({
|
|||||||
{
|
{
|
||||||
key: 'routing',
|
key: 'routing',
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<BranchesOutlined />,
|
<BranchesOutlined aria-hidden />,
|
||||||
t('pages.settings.subIncyGroupRouting'),
|
t('pages.settings.subIncyGroupRouting'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface SecretInputProps {
|
|||||||
placeholder: string;
|
placeholder: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
onClearArmedChange: (armed: boolean) => void;
|
onClearArmedChange: (armed: boolean) => void;
|
||||||
|
'aria-labelledby'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SecretInput({
|
export default function SecretInput({
|
||||||
@@ -17,11 +18,13 @@ export default function SecretInput({
|
|||||||
placeholder,
|
placeholder,
|
||||||
onChange,
|
onChange,
|
||||||
onClearArmedChange,
|
onClearArmedChange,
|
||||||
|
'aria-labelledby': ariaLabelledBy,
|
||||||
}: SecretInputProps) {
|
}: SecretInputProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Space.Compact style={{ width: '100%' }}>
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
<Input.Password
|
<Input.Password
|
||||||
|
aria-labelledby={ariaLabelledBy}
|
||||||
value={value}
|
value={value}
|
||||||
placeholder={configured && !clearArmed ? placeholder : ''}
|
placeholder={configured && !clearArmed ? placeholder : ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useId, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -74,6 +74,7 @@ const TFA_INITIAL: TfaState = {
|
|||||||
|
|
||||||
export default function SecurityTab({ allSetting, updateSetting, saveSetting }: SecurityTabProps) {
|
export default function SecurityTab({ allSetting, updateSetting, saveSetting }: SecurityTabProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const { isMobile } = useMediaQuery();
|
const { isMobile } = useMediaQuery();
|
||||||
const [modal, modalContextHolder] = Modal.useModal();
|
const [modal, modalContextHolder] = Modal.useModal();
|
||||||
const [messageApi, messageContextHolder] = message.useMessage();
|
const [messageApi, messageContextHolder] = message.useMessage();
|
||||||
@@ -308,7 +309,11 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }:
|
|||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: '1',
|
key: '1',
|
||||||
label: catTabLabel(<UserOutlined />, t('pages.settings.security.admin'), isMobile),
|
label: catTabLabel(
|
||||||
|
<UserOutlined aria-hidden />,
|
||||||
|
t('pages.settings.security.admin'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem paddings="small" title={t('pages.settings.oldUsername')}>
|
<SettingListItem paddings="small" title={t('pages.settings.oldUsername')}>
|
||||||
@@ -351,7 +356,7 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }:
|
|||||||
{
|
{
|
||||||
key: '2',
|
key: '2',
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<SafetyOutlined />,
|
<SafetyOutlined aria-hidden />,
|
||||||
t('pages.settings.security.twoFactor'),
|
t('pages.settings.security.twoFactor'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -367,7 +372,7 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '3',
|
key: '3',
|
||||||
label: catTabLabel(<ApiOutlined />, t('pages.nodes.apiToken'), isMobile),
|
label: catTabLabel(<ApiOutlined aria-hidden />, t('pages.nodes.apiToken'), isMobile),
|
||||||
children: (
|
children: (
|
||||||
<div className="api-token-section">
|
<div className="api-token-section">
|
||||||
<div className="api-token-header">
|
<div className="api-token-header">
|
||||||
@@ -387,7 +392,9 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }:
|
|||||||
<div className="api-token-row-head">
|
<div className="api-token-row-head">
|
||||||
<div className="api-token-name-wrap">
|
<div className="api-token-name-wrap">
|
||||||
<div className="api-token-name-line">
|
<div className="api-token-name-line">
|
||||||
<span className="api-token-name">{row.name}</span>
|
<span id={`${uid}-token-${row.id}`} className="api-token-name">
|
||||||
|
{row.name}
|
||||||
|
</span>
|
||||||
<Tag color={scopeMeta[row.scope].color}>
|
<Tag color={scopeMeta[row.scope].color}>
|
||||||
{scopeMeta[row.scope].label}
|
{scopeMeta[row.scope].label}
|
||||||
</Tag>
|
</Tag>
|
||||||
@@ -399,6 +406,7 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }:
|
|||||||
<div className="api-token-actions">
|
<div className="api-token-actions">
|
||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
|
aria-labelledby={`${uid}-token-${row.id}`}
|
||||||
checked={row.enabled}
|
checked={row.enabled}
|
||||||
onChange={() => toggleTokenEnabled(row)}
|
onChange={() => toggleTokenEnabled(row)}
|
||||||
/>
|
/>
|
||||||
@@ -431,8 +439,13 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }:
|
|||||||
onCancel={() => setCreateOpen(false)}
|
onCancel={() => setCreateOpen(false)}
|
||||||
>
|
>
|
||||||
<Form layout="vertical">
|
<Form layout="vertical">
|
||||||
<Form.Item label={t('pages.settings.security.apiTokenName') || 'Name'} required>
|
<Form.Item
|
||||||
|
label={t('pages.settings.security.apiTokenName') || 'Name'}
|
||||||
|
htmlFor={`${uid}-tokenName`}
|
||||||
|
required
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-tokenName`}
|
||||||
value={createName}
|
value={createName}
|
||||||
maxLength={64}
|
maxLength={64}
|
||||||
placeholder={
|
placeholder={
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useId, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Form, Input, InputNumber, Modal, Select, Switch, message } from 'antd';
|
import { Form, Input, InputNumber, Modal, Select, Switch, message } from 'antd';
|
||||||
import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form';
|
import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form';
|
||||||
@@ -59,6 +59,7 @@ export default function SubBalancerFormModal({
|
|||||||
onConfirm,
|
onConfirm,
|
||||||
}: SubBalancerFormModalProps) {
|
}: SubBalancerFormModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const [messageApi, messageContextHolder] = message.useMessage();
|
const [messageApi, messageContextHolder] = message.useMessage();
|
||||||
const methods = useForm<SubBalancerFormValues>({ defaultValues: initialState(balancer) });
|
const methods = useForm<SubBalancerFormValues>({ defaultValues: initialState(balancer) });
|
||||||
const isEdit = balancer != null;
|
const isEdit = balancer != null;
|
||||||
@@ -199,6 +200,7 @@ export default function SubBalancerFormModal({
|
|||||||
return (
|
return (
|
||||||
<div key={id} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div key={id} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<span
|
<span
|
||||||
|
id={`${uid}-weight-${id}`}
|
||||||
title={option?.title}
|
title={option?.title}
|
||||||
style={{
|
style={{
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
@@ -214,6 +216,8 @@ export default function SubBalancerFormModal({
|
|||||||
name={`memberWeights.${id}`}
|
name={`memberWeights.${id}`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<InputNumber
|
<InputNumber
|
||||||
|
aria-labelledby={`${uid}-weight-${id}`}
|
||||||
|
aria-describedby={`${uid}-weights-hint`}
|
||||||
min={0.1}
|
min={0.1}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
precision={1}
|
precision={1}
|
||||||
@@ -227,6 +231,9 @@ export default function SubBalancerFormModal({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
<span id={`${uid}-weights-hint`} hidden>
|
||||||
|
{t('pages.settings.subBalancers.weightsHelp')}
|
||||||
|
</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -156,7 +156,12 @@ export default function SubscriptionBalancersTab({
|
|||||||
width: 80,
|
width: 80,
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
render: (_: unknown, r: SubBalancer) => (
|
render: (_: unknown, r: SubBalancer) => (
|
||||||
<Switch size="small" checked={r.enabled} onChange={() => toggleEnabled(r)} />
|
<Switch
|
||||||
|
size="small"
|
||||||
|
aria-label={t('pages.settings.subBalancers.enabled')}
|
||||||
|
checked={r.enabled}
|
||||||
|
onChange={() => toggleEnabled(r)}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -168,7 +173,7 @@ export default function SubscriptionBalancersTab({
|
|||||||
<Button
|
<Button
|
||||||
aria-label={t('edit')}
|
aria-label={t('edit')}
|
||||||
size="small"
|
size="small"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined aria-hidden />}
|
||||||
title={t('edit')}
|
title={t('edit')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditing(r);
|
setEditing(r);
|
||||||
@@ -181,7 +186,12 @@ export default function SubscriptionBalancersTab({
|
|||||||
cancelText={t('cancel')}
|
cancelText={t('cancel')}
|
||||||
onConfirm={() => remove(r.id)}
|
onConfirm={() => remove(r.id)}
|
||||||
>
|
>
|
||||||
<Button aria-label={t('delete')} size="small" danger icon={<DeleteOutlined />} />
|
<Button
|
||||||
|
aria-label={t('delete')}
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined aria-hidden />}
|
||||||
|
/>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
@@ -212,7 +222,7 @@ export default function SubscriptionBalancersTab({
|
|||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
@@ -332,7 +342,7 @@ export default function SubscriptionBalancersTab({
|
|||||||
{
|
{
|
||||||
key: 'balancers',
|
key: 'balancers',
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<DeploymentUnitOutlined />,
|
<DeploymentUnitOutlined aria-hidden />,
|
||||||
t('pages.settings.subBalancers.tabBalancers'),
|
t('pages.settings.subBalancers.tabBalancers'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -341,7 +351,7 @@ export default function SubscriptionBalancersTab({
|
|||||||
{
|
{
|
||||||
key: 'observatory',
|
key: 'observatory',
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<RadarChartOutlined />,
|
<RadarChartOutlined aria-hidden />,
|
||||||
t('pages.settings.subBalancers.tabObservatory'),
|
t('pages.settings.subBalancers.tabObservatory'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -158,7 +158,11 @@ export default function SubscriptionFormatsTab({
|
|||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: '1',
|
key: '1',
|
||||||
label: catTabLabel(<SettingOutlined />, t('pages.settings.panelSettings'), isMobile),
|
label: catTabLabel(
|
||||||
|
<SettingOutlined aria-hidden />,
|
||||||
|
t('pages.settings.panelSettings'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<div className="subscription-format-sections">
|
<div className="subscription-format-sections">
|
||||||
{allSetting.subJsonEnable && (
|
{allSetting.subJsonEnable && (
|
||||||
@@ -167,7 +171,7 @@ export default function SubscriptionFormatsTab({
|
|||||||
className="subscription-format-card"
|
className="subscription-format-card"
|
||||||
title={
|
title={
|
||||||
<span className="subscription-format-card-title">
|
<span className="subscription-format-card-title">
|
||||||
<FileTextOutlined />
|
<FileTextOutlined aria-hidden />
|
||||||
{t('pages.settings.subJsonEnableTitle')}
|
{t('pages.settings.subJsonEnableTitle')}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -261,7 +265,7 @@ export default function SubscriptionFormatsTab({
|
|||||||
className="subscription-format-card"
|
className="subscription-format-card"
|
||||||
title={
|
title={
|
||||||
<span className="subscription-format-card-title">
|
<span className="subscription-format-card-title">
|
||||||
<NodeIndexOutlined />
|
<NodeIndexOutlined aria-hidden />
|
||||||
{t('pages.settings.subClashEnableTitle')}
|
{t('pages.settings.subClashEnableTitle')}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -322,7 +326,7 @@ export default function SubscriptionFormatsTab({
|
|||||||
{
|
{
|
||||||
key: '2',
|
key: '2',
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<RocketOutlined />,
|
<RocketOutlined aria-hidden />,
|
||||||
t('pages.settings.subFormats.finalMask'),
|
t('pages.settings.subFormats.finalMask'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -342,7 +346,7 @@ export default function SubscriptionFormatsTab({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '3',
|
key: '3',
|
||||||
label: catTabLabel(<PartitionOutlined />, t('pages.settings.mux'), isMobile),
|
label: catTabLabel(<PartitionOutlined aria-hidden />, t('pages.settings.mux'), isMobile),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -396,7 +400,7 @@ export default function SubscriptionFormatsTab({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '4',
|
key: '4',
|
||||||
label: catTabLabel(<SendOutlined />, t('pages.settings.direct'), isMobile),
|
label: catTabLabel(<SendOutlined aria-hidden />, t('pages.settings.direct'), isMobile),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -445,7 +449,7 @@ export default function SubscriptionFormatsTab({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '5',
|
key: '5',
|
||||||
label: catTabLabel(<StopOutlined />, t('pages.settings.block'), isMobile),
|
label: catTabLabel(<StopOutlined aria-hidden />, t('pages.settings.block'), isMobile),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
|
|||||||
@@ -48,7 +48,11 @@ export default function SubscriptionGeneralTab({
|
|||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: '1',
|
key: '1',
|
||||||
label: catTabLabel(<SettingOutlined />, t('pages.settings.panelSettings'), isMobile),
|
label: catTabLabel(
|
||||||
|
<SettingOutlined aria-hidden />,
|
||||||
|
t('pages.settings.panelSettings'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -153,7 +157,11 @@ export default function SubscriptionGeneralTab({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '2',
|
key: '2',
|
||||||
label: catTabLabel(<InfoCircleOutlined />, t('pages.settings.information'), isMobile),
|
label: catTabLabel(
|
||||||
|
<InfoCircleOutlined aria-hidden />,
|
||||||
|
t('pages.settings.information'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -273,7 +281,7 @@ export default function SubscriptionGeneralTab({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '3',
|
key: '3',
|
||||||
label: catTabLabel(<IdcardOutlined />, t('pages.settings.profile'), isMobile),
|
label: catTabLabel(<IdcardOutlined aria-hidden />, t('pages.settings.profile'), isMobile),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -379,7 +387,11 @@ export default function SubscriptionGeneralTab({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '4',
|
key: '4',
|
||||||
label: catTabLabel(<SafetyCertificateOutlined />, t('pages.settings.certs'), isMobile),
|
label: catTabLabel(
|
||||||
|
<SafetyCertificateOutlined aria-hidden />,
|
||||||
|
t('pages.settings.certs'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -407,7 +419,7 @@ export default function SubscriptionGeneralTab({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '5',
|
key: '5',
|
||||||
label: catTabLabel(<BranchesOutlined />, 'Happ', isMobile),
|
label: catTabLabel(<BranchesOutlined aria-hidden />, 'Happ', isMobile),
|
||||||
children: (
|
children: (
|
||||||
<HappSettingsContent
|
<HappSettingsContent
|
||||||
allSetting={allSetting}
|
allSetting={allSetting}
|
||||||
@@ -419,7 +431,7 @@ export default function SubscriptionGeneralTab({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '6',
|
key: '6',
|
||||||
label: catTabLabel(<NodeIndexOutlined />, 'Clash / Mihomo', isMobile),
|
label: catTabLabel(<NodeIndexOutlined aria-hidden />, 'Clash / Mihomo', isMobile),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -452,7 +464,7 @@ export default function SubscriptionGeneralTab({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '7',
|
key: '7',
|
||||||
label: catTabLabel(<CompassOutlined />, 'Incy', isMobile),
|
label: catTabLabel(<CompassOutlined aria-hidden />, 'Incy', isMobile),
|
||||||
children: (
|
children: (
|
||||||
<IncySettingsContent
|
<IncySettingsContent
|
||||||
allSetting={allSetting}
|
allSetting={allSetting}
|
||||||
|
|||||||
@@ -65,7 +65,11 @@ export default function TelegramTab({ allSetting, updateSetting }: TelegramTabPr
|
|||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: '1',
|
key: '1',
|
||||||
label: catTabLabel(<SettingOutlined />, t('pages.settings.panelSettings'), isMobile),
|
label: catTabLabel(
|
||||||
|
<SettingOutlined aria-hidden />,
|
||||||
|
t('pages.settings.panelSettings'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -133,7 +137,7 @@ export default function TelegramTab({ allSetting, updateSetting }: TelegramTabPr
|
|||||||
<Space orientation="vertical" size={8} style={{ width: '100%', marginTop: 16 }}>
|
<Space orientation="vertical" size={8} style={{ width: '100%', marginTop: 16 }}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<SendOutlined />}
|
icon={<SendOutlined aria-hidden />}
|
||||||
loading={testLoading}
|
loading={testLoading}
|
||||||
onClick={handleTestTgBot}
|
onClick={handleTestTgBot}
|
||||||
>
|
>
|
||||||
@@ -153,7 +157,11 @@ export default function TelegramTab({ allSetting, updateSetting }: TelegramTabPr
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '2',
|
key: '2',
|
||||||
label: catTabLabel(<BellOutlined />, t('pages.settings.notifications'), isMobile),
|
label: catTabLabel(
|
||||||
|
<BellOutlined aria-hidden />,
|
||||||
|
t('pages.settings.notifications'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ import { Tooltip } from 'antd';
|
|||||||
old top tab bar's icons-only behaviour. */
|
old top tab bar's icons-only behaviour. */
|
||||||
export function catTabLabel(icon: ReactNode, text: ReactNode, iconsOnly: boolean): ReactNode {
|
export function catTabLabel(icon: ReactNode, text: ReactNode, iconsOnly: boolean): ReactNode {
|
||||||
if (iconsOnly) {
|
if (iconsOnly) {
|
||||||
|
// Callers hide the icon for the desktop label; here it is the tab's only name.
|
||||||
const labelledIcon =
|
const labelledIcon =
|
||||||
typeof text === 'string' && isValidElement(icon)
|
typeof text === 'string' && isValidElement(icon)
|
||||||
? cloneElement(icon as ReactElement<{ 'aria-label'?: string }>, { 'aria-label': text })
|
? cloneElement(icon as ReactElement<{ 'aria-label'?: string; 'aria-hidden'?: boolean }>, {
|
||||||
|
'aria-label': text,
|
||||||
|
'aria-hidden': undefined,
|
||||||
|
})
|
||||||
: icon;
|
: icon;
|
||||||
return <Tooltip title={text}>{labelledIcon}</Tooltip>;
|
return <Tooltip title={text}>{labelledIcon}</Tooltip>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function BecomeButton({ contact, block }: { contact?: string; block?: boolean })
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<CrownOutlined />}
|
icon={<CrownOutlined aria-hidden />}
|
||||||
href={contact}
|
href={contact}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -32,7 +32,7 @@ function YourBrandCard({ contact }: { contact?: string }) {
|
|||||||
return (
|
return (
|
||||||
<div className="sponsors-yourbrand">
|
<div className="sponsors-yourbrand">
|
||||||
<span className="sponsors-yourbrand-icon">
|
<span className="sponsors-yourbrand-icon">
|
||||||
<PlusOutlined />
|
<PlusOutlined aria-hidden />
|
||||||
</span>
|
</span>
|
||||||
<div className="sponsors-yourbrand-title">{t('pages.sponsors.yourBrand')}</div>
|
<div className="sponsors-yourbrand-title">{t('pages.sponsors.yourBrand')}</div>
|
||||||
<div className="sponsors-yourbrand-text">{t('pages.sponsors.yourBrandText')}</div>
|
<div className="sponsors-yourbrand-text">{t('pages.sponsors.yourBrandText')}</div>
|
||||||
@@ -64,7 +64,8 @@ export default function SponsorsPage() {
|
|||||||
<div className="sponsors-header">
|
<div className="sponsors-header">
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title level={3} className="sponsors-title">
|
<Typography.Title level={3} className="sponsors-title">
|
||||||
<CrownOutlined className="sponsors-title-icon" /> {t('pages.sponsors.title')}
|
<CrownOutlined aria-hidden className="sponsors-title-icon" />{' '}
|
||||||
|
{t('pages.sponsors.title')}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Typography.Text type="secondary">{t('pages.sponsors.intro')}</Typography.Text>
|
<Typography.Text type="secondary">{t('pages.sponsors.intro')}</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ interface SubAppsTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PLATFORM_OPTIONS = [
|
const PLATFORM_OPTIONS = [
|
||||||
{ value: 'android' as const, label: 'Android', icon: <AndroidOutlined /> },
|
{ value: 'android' as const, label: 'Android', icon: <AndroidOutlined aria-hidden /> },
|
||||||
{ value: 'ios' as const, label: 'iOS', icon: <AppleOutlined /> },
|
{ value: 'ios' as const, label: 'iOS', icon: <AppleOutlined aria-hidden /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
function AppIcon({ name }: { name: string }) {
|
function AppIcon({ name }: { name: string }) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function SubConfigsTab({ links, onCopy }: SubConfigsTabProps) {
|
|||||||
<div className="sub-rows">
|
<div className="sub-rows">
|
||||||
<div className="sub-configs-bar">
|
<div className="sub-configs-bar">
|
||||||
<Button
|
<Button
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
onClick={() => onCopy(links.join('\n'), t('subscription.copyAllConfigsCopied'))}
|
onClick={() => onCopy(links.join('\n'), t('subscription.copyAllConfigsCopied'))}
|
||||||
>
|
>
|
||||||
{t('subscription.copyAllConfigs')}
|
{t('subscription.copyAllConfigs')}
|
||||||
@@ -44,7 +44,7 @@ export default function SubConfigsTab({ links, onCopy }: SubConfigsTabProps) {
|
|||||||
</span>
|
</span>
|
||||||
<div className="sub-row-actions">
|
<div className="sub-row-actions">
|
||||||
<Button
|
<Button
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
onClick={() => onCopy(link)}
|
onClick={() => onCopy(link)}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
title={t('copy')}
|
title={t('copy')}
|
||||||
|
|||||||
@@ -53,14 +53,20 @@ export default function SubHeader({ title, sId, email, lang, onLangChange }: Sub
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const themeIcon = !isDark ? <SunOutlined /> : !isUltra ? <MoonOutlined /> : <MoonFilled />;
|
const themeIcon = !isDark ? (
|
||||||
|
<SunOutlined aria-hidden />
|
||||||
|
) : !isUltra ? (
|
||||||
|
<MoonOutlined aria-hidden />
|
||||||
|
) : (
|
||||||
|
<MoonFilled aria-hidden />
|
||||||
|
);
|
||||||
const initial = Array.from(title)[0]?.toUpperCase();
|
const initial = Array.from(title)[0]?.toUpperCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sub-header">
|
<header className="sub-header">
|
||||||
<div className="sub-brand">
|
<div className="sub-brand">
|
||||||
<span className="sub-brand-mark" aria-hidden="true">
|
<span className="sub-brand-mark" aria-hidden="true">
|
||||||
{initial ?? <WifiOutlined />}
|
{initial ?? <WifiOutlined aria-hidden />}
|
||||||
</span>
|
</span>
|
||||||
<div className="sub-brand-text">
|
<div className="sub-brand-text">
|
||||||
<div className="sub-brand-title" dir="auto">
|
<div className="sub-brand-title" dir="auto">
|
||||||
@@ -103,7 +109,7 @@ export default function SubHeader({ title, sId, email, lang, onLangChange }: Sub
|
|||||||
size="large"
|
size="large"
|
||||||
className="toolbar-btn"
|
className="toolbar-btn"
|
||||||
aria-label={t('pages.settings.language')}
|
aria-label={t('pages.settings.language')}
|
||||||
icon={<TranslationOutlined />}
|
icon={<TranslationOutlined aria-hidden />}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ export default function SubLinksTab({ subUrl, subJsonUrl, subClashUrl, onCopy }:
|
|||||||
href={appendRawView(row.url)}
|
href={appendRawView(row.url)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined aria-hidden />}
|
||||||
aria-label={t('download')}
|
aria-label={t('download')}
|
||||||
title={t('download')}
|
title={t('download')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined aria-hidden />}
|
||||||
onClick={() => onCopy(row.url)}
|
onClick={() => onCopy(row.url)}
|
||||||
aria-label={t('copy')}
|
aria-label={t('copy')}
|
||||||
title={t('copy')}
|
title={t('copy')}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default function SubPage() {
|
|||||||
if (subUrl || subJsonUrl || subClashUrl) {
|
if (subUrl || subJsonUrl || subClashUrl) {
|
||||||
items.push({
|
items.push({
|
||||||
key: 'subscription',
|
key: 'subscription',
|
||||||
icon: <LinkOutlined />,
|
icon: <LinkOutlined aria-hidden />,
|
||||||
label: t('subscription.tabLinks'),
|
label: t('subscription.tabLinks'),
|
||||||
children: (
|
children: (
|
||||||
<SubLinksTab
|
<SubLinksTab
|
||||||
@@ -107,7 +107,7 @@ export default function SubPage() {
|
|||||||
if (subUrl) {
|
if (subUrl) {
|
||||||
items.push({
|
items.push({
|
||||||
key: 'apps',
|
key: 'apps',
|
||||||
icon: <AppstoreOutlined />,
|
icon: <AppstoreOutlined aria-hidden />,
|
||||||
label: t('subscription.tabApps'),
|
label: t('subscription.tabApps'),
|
||||||
children: <SubAppsTab apps={apps} initialPlatform={initialPlatform} onOpen={open} />,
|
children: <SubAppsTab apps={apps} initialPlatform={initialPlatform} onOpen={open} />,
|
||||||
});
|
});
|
||||||
@@ -115,7 +115,7 @@ export default function SubPage() {
|
|||||||
if (links.length > 0) {
|
if (links.length > 0) {
|
||||||
items.push({
|
items.push({
|
||||||
key: 'configs',
|
key: 'configs',
|
||||||
icon: <UnorderedListOutlined />,
|
icon: <UnorderedListOutlined aria-hidden />,
|
||||||
label: (
|
label: (
|
||||||
<>
|
<>
|
||||||
{t('subscription.tabConfigs')}
|
{t('subscription.tabConfigs')}
|
||||||
@@ -153,13 +153,13 @@ export default function SubPage() {
|
|||||||
<footer className="sub-footer">
|
<footer className="sub-footer">
|
||||||
{updateHours > 0 && (
|
{updateHours > 0 && (
|
||||||
<span>
|
<span>
|
||||||
<ClockCircleOutlined />
|
<ClockCircleOutlined aria-hidden />
|
||||||
{t('subscription.updateInterval', { hours: updateHours })}
|
{t('subscription.updateInterval', { hours: updateHours })}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{subSupportUrl && (
|
{subSupportUrl && (
|
||||||
<a href={subSupportUrl} target="_blank" rel="noopener noreferrer">
|
<a href={subSupportUrl} target="_blank" rel="noopener noreferrer">
|
||||||
<CustomerServiceOutlined />
|
<CustomerServiceOutlined aria-hidden />
|
||||||
{t('subscription.support')}
|
{t('subscription.support')}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ export default function SubQrButton({ value, label, onCopy }: SubQrButtonProps)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button icon={<QrcodeOutlined />} aria-label="QR" title="QR" onClick={() => setOpen(true)} />
|
<Button
|
||||||
|
icon={<QrcodeOutlined aria-hidden />}
|
||||||
|
aria-label="QR"
|
||||||
|
title="QR"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
/>
|
||||||
<Modal
|
<Modal
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
@@ -49,6 +54,7 @@ export default function SubQrButton({ value, label, onCopy }: SubQrButtonProps)
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
|
aria-label={t('subscription.tabLinks')}
|
||||||
className="sub-qr-modal-link"
|
className="sub-qr-modal-link"
|
||||||
value={value}
|
value={value}
|
||||||
readOnly
|
readOnly
|
||||||
@@ -56,10 +62,15 @@ export default function SubQrButton({ value, label, onCopy }: SubQrButtonProps)
|
|||||||
autoSize={{ minRows: 2, maxRows: 5 }}
|
autoSize={{ minRows: 2, maxRows: 5 }}
|
||||||
/>
|
/>
|
||||||
<div className="sub-qr-modal-actions">
|
<div className="sub-qr-modal-actions">
|
||||||
<Button type="primary" size="large" icon={<CopyOutlined />} onClick={() => onCopy(value)}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
icon={<CopyOutlined aria-hidden />}
|
||||||
|
onClick={() => onCopy(value)}
|
||||||
|
>
|
||||||
{t('copy')}
|
{t('copy')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="large" icon={<DownloadOutlined />} onClick={saveQr}>
|
<Button size="large" icon={<DownloadOutlined aria-hidden />} onClick={saveQr}>
|
||||||
{t('subscription.saveQr')}
|
{t('subscription.saveQr')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useId, useMemo, useState } from 'react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Alert, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tag } from 'antd';
|
import { Alert, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tag } from 'antd';
|
||||||
@@ -68,6 +68,7 @@ export default function BalancerFormModal({
|
|||||||
onConfirm,
|
onConfirm,
|
||||||
}: BalancerFormModalProps) {
|
}: BalancerFormModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const uid = useId();
|
||||||
const methods = useForm<BalancerFormValues>({ defaultValues: initialState(balancer) });
|
const methods = useForm<BalancerFormValues>({ defaultValues: initialState(balancer) });
|
||||||
const [submitAttempted, setSubmitAttempted] = useState(false);
|
const [submitAttempted, setSubmitAttempted] = useState(false);
|
||||||
const isEdit = balancer != null;
|
const isEdit = balancer != null;
|
||||||
@@ -226,6 +227,7 @@ export default function BalancerFormModal({
|
|||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('pages.xray.balancer.tag')}
|
label={t('pages.xray.balancer.tag')}
|
||||||
|
htmlFor={`${uid}-tag`}
|
||||||
required
|
required
|
||||||
validateStatus={errorMessage ? 'error' : showDuplicate ? 'warning' : ''}
|
validateStatus={errorMessage ? 'error' : showDuplicate ? 'warning' : ''}
|
||||||
help={
|
help={
|
||||||
@@ -234,6 +236,7 @@ export default function BalancerFormModal({
|
|||||||
hasFeedback
|
hasFeedback
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
id={`${uid}-tag`}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={(e) => field.onChange(e.target.value)}
|
onChange={(e) => field.onChange(e.target.value)}
|
||||||
onBlur={field.onBlur}
|
onBlur={field.onBlur}
|
||||||
@@ -319,7 +322,7 @@ export default function BalancerFormModal({
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('add')}
|
aria-label={t('add')}
|
||||||
onClick={() => methods.setValue('settings.baselines', [...baselines, ''])}
|
onClick={() => methods.setValue('settings.baselines', [...baselines, ''])}
|
||||||
/>
|
/>
|
||||||
@@ -345,7 +348,7 @@ export default function BalancerFormModal({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MinusOutlined />
|
<MinusOutlined aria-hidden />
|
||||||
</InputAddon>
|
</InputAddon>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
))}
|
))}
|
||||||
@@ -354,7 +357,7 @@ export default function BalancerFormModal({
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('add')}
|
aria-label={t('add')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
methods.setValue('settings.costs', [
|
methods.setValue('settings.costs', [
|
||||||
@@ -411,7 +414,7 @@ export default function BalancerFormModal({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MinusOutlined />
|
<MinusOutlined aria-hidden />
|
||||||
</InputAddon>
|
</InputAddon>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ export default function BalancersTab({
|
|||||||
aria-label={t('edit')}
|
aria-label={t('edit')}
|
||||||
shape="circle"
|
shape="circle"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined aria-hidden />}
|
||||||
onClick={() => openEdit(index)}
|
onClick={() => openEdit(index)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -338,7 +338,7 @@ export default function BalancersTab({
|
|||||||
key: 'edit',
|
key: 'edit',
|
||||||
label: (
|
label: (
|
||||||
<>
|
<>
|
||||||
<EditOutlined /> {t('edit')}
|
<EditOutlined aria-hidden /> {t('edit')}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
onClick: () => openEdit(index),
|
onClick: () => openEdit(index),
|
||||||
@@ -350,7 +350,7 @@ export default function BalancersTab({
|
|||||||
danger: true,
|
danger: true,
|
||||||
label: (
|
label: (
|
||||||
<>
|
<>
|
||||||
<DeleteOutlined /> {t('delete')}
|
<DeleteOutlined aria-hidden /> {t('delete')}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
onClick: () => confirmDelete(index),
|
onClick: () => confirmDelete(index),
|
||||||
@@ -358,7 +358,12 @@ export default function BalancersTab({
|
|||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button aria-label={t('more')} shape="circle" size="small" icon={<MoreOutlined />} />
|
<Button
|
||||||
|
aria-label={t('more')}
|
||||||
|
shape="circle"
|
||||||
|
size="small"
|
||||||
|
icon={<MoreOutlined aria-hidden />}
|
||||||
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -458,6 +463,7 @@ export default function BalancersTab({
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
size="small"
|
size="small"
|
||||||
|
aria-label={t('pages.xray.balancerOverride')}
|
||||||
style={{ width: 170 }}
|
style={{ width: 170 }}
|
||||||
placeholder={t('pages.xray.balancerOverridePh')}
|
placeholder={t('pages.xray.balancerOverridePh')}
|
||||||
allowClear
|
allowClear
|
||||||
@@ -475,20 +481,20 @@ export default function BalancersTab({
|
|||||||
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
|
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
{rows.length === 0 ? (
|
{rows.length === 0 ? (
|
||||||
<Empty description={t('emptyBalancersDesc')}>
|
<Empty description={t('emptyBalancersDesc')}>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openAdd}>
|
<Button type="primary" icon={<PlusOutlined aria-hidden />} onClick={openAdd}>
|
||||||
{t('pages.xray.Balancers')}
|
{t('pages.xray.Balancers')}
|
||||||
</Button>
|
</Button>
|
||||||
</Empty>
|
</Empty>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openAdd}>
|
<Button type="primary" icon={<PlusOutlined aria-hidden />} onClick={openAdd}>
|
||||||
{t('pages.xray.Balancers')}
|
{t('pages.xray.Balancers')}
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip title={t('pages.xray.balancerLiveRefresh')}>
|
<Tooltip title={t('pages.xray.balancerLiveRefresh')}>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('pages.xray.balancerLiveRefresh')}
|
aria-label={t('pages.xray.balancerLiveRefresh')}
|
||||||
icon={<SyncOutlined spin={liveLoading} />}
|
icon={<SyncOutlined aria-hidden spin={liveLoading} />}
|
||||||
onClick={refreshLive}
|
onClick={refreshLive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -516,7 +522,7 @@ export default function BalancersTab({
|
|||||||
{
|
{
|
||||||
key: 'balancers',
|
key: 'balancers',
|
||||||
label: catTabLabel(
|
label: catTabLabel(
|
||||||
<DeploymentUnitOutlined />,
|
<DeploymentUnitOutlined aria-hidden />,
|
||||||
t('pages.xray.tabBalancerSettings'),
|
t('pages.xray.tabBalancerSettings'),
|
||||||
isMobile,
|
isMobile,
|
||||||
),
|
),
|
||||||
@@ -524,7 +530,11 @@ export default function BalancersTab({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'observatory',
|
key: 'observatory',
|
||||||
label: catTabLabel(<RadarChartOutlined />, t('pages.xray.tabObservatory'), isMobile),
|
label: catTabLabel(
|
||||||
|
<RadarChartOutlined aria-hidden />,
|
||||||
|
t('pages.xray.tabObservatory'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<ObservatorySettingsTab templateSettings={templateSettings} mutate={mutate} />
|
<ObservatorySettingsTab templateSettings={templateSettings} mutate={mutate} />
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export default function BasicsTab({
|
|||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
key: '1',
|
key: '1',
|
||||||
label: catTabLabel(<SettingOutlined />, t('pages.xray.generalConfigs'), isMobile),
|
label: catTabLabel(<SettingOutlined aria-hidden />, t('pages.xray.generalConfigs'), isMobile),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<Alert
|
<Alert
|
||||||
@@ -296,7 +296,7 @@ export default function BasicsTab({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '2',
|
key: '2',
|
||||||
label: catTabLabel(<BarChartOutlined />, t('pages.xray.statistics'), isMobile),
|
label: catTabLabel(<BarChartOutlined aria-hidden />, t('pages.xray.statistics'), isMobile),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
{[
|
{[
|
||||||
@@ -351,7 +351,11 @@ export default function BasicsTab({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'connection',
|
key: 'connection',
|
||||||
label: catTabLabel(<ClockCircleOutlined />, t('pages.xray.connectionLimits'), isMobile),
|
label: catTabLabel(
|
||||||
|
<ClockCircleOutlined aria-hidden />,
|
||||||
|
t('pages.xray.connectionLimits'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<Alert
|
<Alert
|
||||||
@@ -395,7 +399,7 @@ export default function BasicsTab({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '3',
|
key: '3',
|
||||||
label: catTabLabel(<FileTextOutlined />, t('pages.xray.logConfigs'), isMobile),
|
label: catTabLabel(<FileTextOutlined aria-hidden />, t('pages.xray.logConfigs'), isMobile),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<Alert
|
<Alert
|
||||||
@@ -498,10 +502,19 @@ export default function BasicsTab({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'reset',
|
key: 'reset',
|
||||||
label: catTabLabel(<ReloadOutlined />, t('pages.settings.resetDefaultConfig'), isMobile),
|
label: catTabLabel(
|
||||||
|
<ReloadOutlined aria-hidden />,
|
||||||
|
t('pages.settings.resetDefaultConfig'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<Space style={{ padding: '0 20px' }}>
|
<Space style={{ padding: '0 20px' }}>
|
||||||
<Button type="primary" danger icon={<ReloadOutlined />} onClick={confirmResetDefault}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
danger
|
||||||
|
icon={<ReloadOutlined aria-hidden />}
|
||||||
|
onClick={confirmResetDefault}
|
||||||
|
>
|
||||||
{t('pages.settings.resetDefaultConfig')}
|
{t('pages.settings.resetDefaultConfig')}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -221,14 +221,14 @@ export default function DnsServerModal({
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('add')}
|
aria-label={t('add')}
|
||||||
onClick={() => methods.setValue('domains', [...domains, ''])}
|
onClick={() => methods.setValue('domains', [...domains, ''])}
|
||||||
/>
|
/>
|
||||||
{domains.map((_, i) => (
|
{domains.map((_, i) => (
|
||||||
<Space.Compact key={i} block style={{ marginTop: 4 }}>
|
<Space.Compact key={i} block style={{ marginTop: 4 }}>
|
||||||
<FormField name={`domains.${i}`} noStyle>
|
<FormField name={`domains.${i}`} noStyle>
|
||||||
<Input />
|
<Input aria-label={t('pages.xray.dns.domains')} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<InputAddon
|
<InputAddon
|
||||||
ariaLabel={t('remove')}
|
ariaLabel={t('remove')}
|
||||||
@@ -239,7 +239,7 @@ export default function DnsServerModal({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MinusOutlined />
|
<MinusOutlined aria-hidden />
|
||||||
</InputAddon>
|
</InputAddon>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
))}
|
))}
|
||||||
@@ -249,14 +249,14 @@ export default function DnsServerModal({
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('add')}
|
aria-label={t('add')}
|
||||||
onClick={() => methods.setValue('expectedIPs', [...expectedIPs, ''])}
|
onClick={() => methods.setValue('expectedIPs', [...expectedIPs, ''])}
|
||||||
/>
|
/>
|
||||||
{expectedIPs.map((_, i) => (
|
{expectedIPs.map((_, i) => (
|
||||||
<Space.Compact key={i} block style={{ marginTop: 4 }}>
|
<Space.Compact key={i} block style={{ marginTop: 4 }}>
|
||||||
<FormField name={`expectedIPs.${i}`} noStyle>
|
<FormField name={`expectedIPs.${i}`} noStyle>
|
||||||
<Input />
|
<Input aria-label={t('pages.xray.dns.expectIPs')} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<InputAddon
|
<InputAddon
|
||||||
ariaLabel={t('remove')}
|
ariaLabel={t('remove')}
|
||||||
@@ -267,7 +267,7 @@ export default function DnsServerModal({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MinusOutlined />
|
<MinusOutlined aria-hidden />
|
||||||
</InputAddon>
|
</InputAddon>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
))}
|
))}
|
||||||
@@ -277,14 +277,14 @@ export default function DnsServerModal({
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
aria-label={t('add')}
|
aria-label={t('add')}
|
||||||
onClick={() => methods.setValue('unexpectedIPs', [...unexpectedIPs, ''])}
|
onClick={() => methods.setValue('unexpectedIPs', [...unexpectedIPs, ''])}
|
||||||
/>
|
/>
|
||||||
{unexpectedIPs.map((_, i) => (
|
{unexpectedIPs.map((_, i) => (
|
||||||
<Space.Compact key={i} block style={{ marginTop: 4 }}>
|
<Space.Compact key={i} block style={{ marginTop: 4 }}>
|
||||||
<FormField name={`unexpectedIPs.${i}`} noStyle>
|
<FormField name={`unexpectedIPs.${i}`} noStyle>
|
||||||
<Input />
|
<Input aria-label={t('pages.xray.dns.unexpectIPs')} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<InputAddon
|
<InputAddon
|
||||||
ariaLabel={t('remove')}
|
ariaLabel={t('remove')}
|
||||||
@@ -295,7 +295,7 @@ export default function DnsServerModal({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MinusOutlined />
|
<MinusOutlined aria-hidden />
|
||||||
</InputAddon>
|
</InputAddon>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -245,7 +245,11 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab
|
|||||||
const out = [
|
const out = [
|
||||||
{
|
{
|
||||||
key: '1',
|
key: '1',
|
||||||
label: catTabLabel(<SettingOutlined />, t('pages.xray.generalConfigs'), isMobile),
|
label: catTabLabel(
|
||||||
|
<SettingOutlined aria-hidden />,
|
||||||
|
t('pages.xray.generalConfigs'),
|
||||||
|
isMobile,
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<SettingListItem
|
<SettingListItem
|
||||||
@@ -376,13 +380,13 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab
|
|||||||
if (dnsEnabled) {
|
if (dnsEnabled) {
|
||||||
out.push({
|
out.push({
|
||||||
key: 'hosts',
|
key: 'hosts',
|
||||||
label: catTabLabel(<ProfileOutlined />, t('pages.xray.dns.hosts'), isMobile),
|
label: catTabLabel(<ProfileOutlined aria-hidden />, t('pages.xray.dns.hosts'), isMobile),
|
||||||
children:
|
children:
|
||||||
hostsList.length === 0 ? (
|
hostsList.length === 0 ? (
|
||||||
<Empty description={t('pages.xray.dns.hostsEmpty')}>
|
<Empty description={t('pages.xray.dns.hostsEmpty')}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
onClick={() => syncHosts([...hostsList, { domain: '', values: [] }])}
|
onClick={() => syncHosts([...hostsList, { domain: '', values: [] }])}
|
||||||
>
|
>
|
||||||
{t('pages.xray.dns.hostsAdd')}
|
{t('pages.xray.dns.hostsAdd')}
|
||||||
@@ -392,7 +396,7 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab
|
|||||||
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
|
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined aria-hidden />}
|
||||||
onClick={() => syncHosts([...hostsList, { domain: '', values: [] }])}
|
onClick={() => syncHosts([...hostsList, { domain: '', values: [] }])}
|
||||||
>
|
>
|
||||||
{t('pages.xray.dns.hostsAdd')}
|
{t('pages.xray.dns.hostsAdd')}
|
||||||
@@ -426,7 +430,7 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab
|
|||||||
<Button
|
<Button
|
||||||
danger
|
danger
|
||||||
aria-label={t('delete')}
|
aria-label={t('delete')}
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined aria-hidden />}
|
||||||
onClick={() => syncHosts(hostsList.filter((_, i) => i !== idx))}
|
onClick={() => syncHosts(hostsList.filter((_, i) => i !== idx))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -437,15 +441,18 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab
|
|||||||
|
|
||||||
out.push({
|
out.push({
|
||||||
key: '2',
|
key: '2',
|
||||||
label: catTabLabel(<DatabaseOutlined />, 'DNS', isMobile),
|
label: catTabLabel(<DatabaseOutlined aria-hidden />, 'DNS', isMobile),
|
||||||
children:
|
children:
|
||||||
dnsServers.length === 0 ? (
|
dnsServers.length === 0 ? (
|
||||||
<Empty description={t('emptyDnsDesc')}>
|
<Empty description={t('emptyDnsDesc')}>
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openAddServer}>
|
<Button type="primary" icon={<PlusOutlined aria-hidden />} onClick={openAddServer}>
|
||||||
{t('pages.xray.dns.add')}
|
{t('pages.xray.dns.add')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button icon={<MenuOutlined />} onClick={() => setPresetsModalOpen(true)}>
|
<Button
|
||||||
|
icon={<MenuOutlined aria-hidden />}
|
||||||
|
onClick={() => setPresetsModalOpen(true)}
|
||||||
|
>
|
||||||
{t('pages.xray.dns.usePreset')}
|
{t('pages.xray.dns.usePreset')}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -453,13 +460,16 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab
|
|||||||
) : (
|
) : (
|
||||||
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
|
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openAddServer}>
|
<Button type="primary" icon={<PlusOutlined aria-hidden />} onClick={openAddServer}>
|
||||||
{t('pages.xray.dns.add')}
|
{t('pages.xray.dns.add')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button icon={<MenuOutlined />} onClick={() => setPresetsModalOpen(true)}>
|
<Button
|
||||||
|
icon={<MenuOutlined aria-hidden />}
|
||||||
|
onClick={() => setPresetsModalOpen(true)}
|
||||||
|
>
|
||||||
{t('pages.xray.dns.usePreset')}
|
{t('pages.xray.dns.usePreset')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button danger icon={<DeleteOutlined />} onClick={clearAllServers}>
|
<Button danger icon={<DeleteOutlined aria-hidden />} onClick={clearAllServers}>
|
||||||
{t('pages.xray.dns.clearAll')}
|
{t('pages.xray.dns.clearAll')}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -477,17 +487,17 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab
|
|||||||
|
|
||||||
out.push({
|
out.push({
|
||||||
key: '3',
|
key: '3',
|
||||||
label: catTabLabel(<ExperimentOutlined />, 'Fake DNS', isMobile),
|
label: catTabLabel(<ExperimentOutlined aria-hidden />, 'Fake DNS', isMobile),
|
||||||
children:
|
children:
|
||||||
fakeDnsList.length === 0 ? (
|
fakeDnsList.length === 0 ? (
|
||||||
<Empty description={t('emptyFakeDnsDesc')}>
|
<Empty description={t('emptyFakeDnsDesc')}>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={addFakedns}>
|
<Button type="primary" icon={<PlusOutlined aria-hidden />} onClick={addFakedns}>
|
||||||
{t('pages.xray.fakedns.add')}
|
{t('pages.xray.fakedns.add')}
|
||||||
</Button>
|
</Button>
|
||||||
</Empty>
|
</Empty>
|
||||||
) : (
|
) : (
|
||||||
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
|
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={addFakedns}>
|
<Button type="primary" icon={<PlusOutlined aria-hidden />} onClick={addFakedns}>
|
||||||
{t('pages.xray.fakedns.add')}
|
{t('pages.xray.fakedns.add')}
|
||||||
</Button>
|
</Button>
|
||||||
<Table
|
<Table
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function useDnsServerColumns({
|
|||||||
key: 'edit',
|
key: 'edit',
|
||||||
label: (
|
label: (
|
||||||
<>
|
<>
|
||||||
<EditOutlined /> {t('edit')}
|
<EditOutlined aria-hidden /> {t('edit')}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
onClick: () => openEditServer(index),
|
onClick: () => openEditServer(index),
|
||||||
@@ -48,7 +48,7 @@ export function useDnsServerColumns({
|
|||||||
danger: true,
|
danger: true,
|
||||||
label: (
|
label: (
|
||||||
<>
|
<>
|
||||||
<DeleteOutlined /> {t('delete')}
|
<DeleteOutlined aria-hidden /> {t('delete')}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
onClick: () => deleteServer(index),
|
onClick: () => deleteServer(index),
|
||||||
@@ -56,7 +56,12 @@ export function useDnsServerColumns({
|
|||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button aria-label={t('more')} shape="circle" size="small" icon={<MoreOutlined />} />
|
<Button
|
||||||
|
aria-label={t('more')}
|
||||||
|
shape="circle"
|
||||||
|
size="small"
|
||||||
|
icon={<MoreOutlined aria-hidden />}
|
||||||
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
@@ -107,7 +112,7 @@ export function useFakednsColumns({
|
|||||||
shape="circle"
|
shape="circle"
|
||||||
size="small"
|
size="small"
|
||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined aria-hidden />}
|
||||||
onClick={() => deleteFakedns(index)}
|
onClick={() => deleteFakedns(index)}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user