Files
l-ui/frontend/src/pages/xray/routing/RouteTester.tsx
T
Sanaei f727d04f65
Release 3X-UI for Docker / build (push) Canceled after 0s
Release 3X-UI / build (386) (push) Canceled after 0s
Release 3X-UI / build (amd64) (push) Canceled after 0s
Release 3X-UI / build (arm64) (push) Canceled after 0s
Release 3X-UI / build (armv5) (push) Canceled after 0s
Release 3X-UI / build (armv6) (push) Canceled after 0s
Release 3X-UI / build (armv7) (push) Canceled after 0s
Release 3X-UI / build (s390x) (push) Canceled after 0s
Release 3X-UI / Build for Windows (amd64) (push) Canceled after 0s
Release 3X-UI / Publish rolling dev release (push) Canceled after 0s
v3.7.0
2026-08-24 15:07:15 +02:00

166 lines
5.4 KiB
TypeScript

import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Button, Col, Input, InputNumber, Row, Select, Space, Tag } from 'antd';
import { AimOutlined } from '@ant-design/icons';
import { HttpUtil } from '@/utils';
import { useInboundOptions } from '@/api/queries/useInboundOptions';
import { buildRemarkByTag, formatInboundTag } from './helpers';
interface RouteTesterProps {
inboundTags: string[];
isMobile: boolean;
}
// Mirror of the /xray/routeTest response (RoutingService.TestRoute).
interface RouteTestResult {
matched: boolean;
outboundTag: string;
groupTags?: string[];
}
const PROTOCOL_OPTIONS = ['http', 'tls', 'quic', 'bittorrent'].map((p) => ({ label: p, value: p }));
export default function RouteTester({ inboundTags, isMobile }: RouteTesterProps) {
const { t } = useTranslation();
const { data: inboundOptions } = useInboundOptions();
const remarkByTag = useMemo(() => buildRemarkByTag(inboundOptions || []), [inboundOptions]);
const [dest, setDest] = useState('');
const [port, setPort] = useState<number | null>(443);
const [network, setNetwork] = useState('tcp');
const [inboundTag, setInboundTag] = useState<string | undefined>(undefined);
const [protocol, setProtocol] = useState<string | undefined>(undefined);
const [testing, setTesting] = useState(false);
const [result, setResult] = useState<RouteTestResult | null>(null);
async function run() {
const value = dest.trim();
if (!value) return;
// Domains never contain ':' and a pure dotted-quad is an IPv4 address;
// everything else is treated as a domain.
const isIp = /^(\d{1,3}\.){3}\d{1,3}$/.test(value) || value.includes(':');
setTesting(true);
setResult(null);
try {
const msg = await HttpUtil.post('/panel/api/xray/routeTest', {
domain: isIp ? '' : value,
ip: isIp ? value : '',
port: port ?? 0,
network,
inboundTag: inboundTag || '',
protocol: protocol || '',
});
if (msg?.success && msg.obj && typeof msg.obj === 'object') {
setResult(msg.obj as RouteTestResult);
}
} finally {
setTesting(false);
}
}
const fieldSpan = isMobile ? 24 : undefined;
return (
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
<Alert type="info" showIcon title={t('pages.xray.routeTesterDesc')} />
<Row gutter={[8, 8]} align="bottom">
<Col xs={fieldSpan} sm={7}>
<Input
aria-label={t('pages.xray.routeTesterDest')}
placeholder={t('pages.xray.routeTesterDest')}
value={dest}
onChange={(e) => setDest(e.target.value)}
onPressEnter={run}
allowClear
/>
</Col>
<Col xs={12} sm={3}>
<InputNumber
aria-label={t('pages.xray.routeTesterPort')}
style={{ width: '100%' }}
min={0}
max={65535}
placeholder={t('pages.xray.routeTesterPort')}
value={port}
onChange={(v) => setPort(v)}
/>
</Col>
<Col xs={12} sm={3}>
<Select
aria-label={t('pages.inbounds.network')}
style={{ width: '100%' }}
value={network}
onChange={setNetwork}
options={[
{ label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' },
]}
/>
</Col>
<Col xs={12} sm={4}>
<Select
aria-label={t('pages.xray.routeTesterInbound')}
style={{ width: '100%' }}
placeholder={t('pages.xray.routeTesterInbound')}
allowClear
value={inboundTag}
onChange={setInboundTag}
options={inboundTags
.filter(Boolean)
.map((tag) => ({ label: formatInboundTag(tag, remarkByTag), value: tag }))}
/>
</Col>
<Col xs={12} sm={4}>
<Select
aria-label={t('pages.xray.routeTesterProtocol')}
style={{ width: '100%' }}
placeholder={t('pages.xray.routeTesterProtocol')}
allowClear
value={protocol}
onChange={setProtocol}
options={PROTOCOL_OPTIONS}
/>
</Col>
<Col xs={fieldSpan} sm={3}>
<Button
type="primary"
icon={<AimOutlined />}
loading={testing}
disabled={!dest.trim()}
onClick={run}
block
>
{t('pages.xray.routeTesterTest')}
</Button>
</Col>
</Row>
{result &&
(result.matched ? (
<Alert
type="success"
showIcon
title={
<Space wrap>
<span>{t('pages.xray.routeTesterMatchedOutbound')}:</span>
<Tag color="blue">{result.outboundTag || '—'}</Tag>
{(result.groupTags || []).length > 0 && (
<>
<span>{t('pages.xray.routeTesterViaBalancer')}:</span>
{(result.groupTags || []).map((tag) => (
<Tag key={tag} color="orange">
{tag}
</Tag>
))}
</>
)}
</Space>
}
/>
) : (
<Alert type="warning" showIcon title={t('pages.xray.routeTesterDefaultOutbound')} />
))}
</Space>
);
}