refactor(frontend): extract InboundInfoModal helpers, types & buildInboundInfo

InboundInfoModal.tsx 1081 -> 836 lines. Moved the pure data helpers (network host/path readers, link-protocol check, copy/download/statsColor/IP formatting) plus all shared types and the buildInboundInfo data builder into info/helpers.ts and info/types.ts. The state-coupled render body is left intact (no React render tests to guard a deeper split). Code moved verbatim; no behavior change. All gates green, 337 tests pass.
This commit is contained in:
MHSanaei 2026-05-30 15:35:43 +02:00
parent a32fe94872
commit f116b09f7c
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
3 changed files with 268 additions and 261 deletions

View file

@ -1,280 +1,30 @@
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Divider, Modal, Space, Tabs, Tag, Tooltip } from 'antd'; import { Button, Divider, Modal, Space, Tabs, Tag, Tooltip } from 'antd';
import { getMessage } from '@/utils/messageBus';
import { CopyOutlined, SyncOutlined, DeleteOutlined, DownloadOutlined } from '@ant-design/icons'; import { CopyOutlined, SyncOutlined, DeleteOutlined, DownloadOutlined } from '@ant-design/icons';
import { import { HttpUtil, IntlUtil, SizeFormatter, ColorUtils } from '@/utils';
HttpUtil,
IntlUtil,
SizeFormatter,
ColorUtils,
ClipboardManager,
FileManager,
} from '@/utils';
import { Protocols } from '@/schemas/primitives'; import { Protocols } from '@/schemas/primitives';
import { InfinityIcon } from '@/components/ui'; import { InfinityIcon } from '@/components/ui';
import { useDatepicker } from '@/hooks/useDatepicker'; import { useDatepicker } from '@/hooks/useDatepicker';
import { coerceInboundJsonField } from '@/models/dbinbound';
import {
canEnableTlsFlow,
isSS2022 as isSS2022Helper,
isSSMultiUser as isSSMultiUserHelper,
} from '@/lib/xray/protocol-capabilities';
import { import {
genAllLinks, genAllLinks,
genWireguardConfigs, genWireguardConfigs,
genWireguardLinks, genWireguardLinks,
} from '@/lib/xray/inbound-link'; } from '@/lib/xray/inbound-link';
import { inboundFromDb } from '@/lib/xray/inbound-from-db'; import { inboundFromDb } from '@/lib/xray/inbound-from-db';
import type { SubSettings } from '../useInbounds';
import {
buildInboundInfo,
copyText,
downloadText,
formatIpInfo,
hasShareLink,
statsColor,
} from './helpers';
import type { ClientSetting, ClientStats, InboundInfo, InboundInfoModalProps } from './types';
import './InboundInfoModal.css'; import './InboundInfoModal.css';
const LINK_PROTOCOLS: ReadonlySet<string> = new Set([
Protocols.VMESS,
Protocols.VLESS,
Protocols.TROJAN,
Protocols.SHADOWSOCKS,
Protocols.HYSTERIA,
]);
function hasShareLink(protocol: string): boolean {
return LINK_PROTOCOLS.has(protocol);
}
function readHeader(headers: unknown, name: string): string {
const needle = name.toLowerCase();
if (Array.isArray(headers)) {
for (const h of headers) {
if (h && typeof h === 'object' && String((h as { name?: string }).name ?? '').toLowerCase() === needle) {
return String((h as { value?: unknown }).value ?? '');
}
}
return '';
}
if (headers && typeof headers === 'object') {
for (const [k, v] of Object.entries(headers as Record<string, unknown>)) {
if (k.toLowerCase() === needle) {
return Array.isArray(v) ? String(v[0] ?? '') : String(v ?? '');
}
}
}
return '';
}
function readNetworkHost(stream: Record<string, unknown>, network: string): string | null {
switch (network) {
case 'tcp': {
const tcp = stream.tcpSettings as { header?: { request?: { headers?: unknown } } } | undefined;
return readHeader(tcp?.header?.request?.headers, 'host');
}
case 'ws': {
const ws = stream.wsSettings as { host?: string; headers?: unknown } | undefined;
return (ws?.host && ws.host.length > 0) ? ws.host : readHeader(ws?.headers, 'host');
}
case 'httpupgrade': {
const hu = stream.httpupgradeSettings as { host?: string; headers?: unknown } | undefined;
return (hu?.host && hu.host.length > 0) ? hu.host : readHeader(hu?.headers, 'host');
}
case 'xhttp': {
const xh = stream.xhttpSettings as { host?: string; headers?: unknown } | undefined;
return (xh?.host && xh.host.length > 0) ? xh.host : readHeader(xh?.headers, 'host');
}
default:
return null;
}
}
function readNetworkPath(stream: Record<string, unknown>, network: string): string | null {
switch (network) {
case 'tcp': {
const tcp = stream.tcpSettings as { header?: { request?: { path?: string[] } } } | undefined;
return tcp?.header?.request?.path?.[0] ?? null;
}
case 'ws':
return (stream.wsSettings as { path?: string } | undefined)?.path ?? null;
case 'httpupgrade':
return (stream.httpupgradeSettings as { path?: string } | undefined)?.path ?? null;
case 'xhttp':
return (stream.xhttpSettings as { path?: string } | undefined)?.path ?? null;
default:
return null;
}
}
interface ClientStats {
email: string;
up: number;
down: number;
total: number;
expiryTime: number;
enable?: boolean;
}
interface ClientSetting {
email?: string;
id?: string;
security?: string;
password?: string;
flow?: string;
subId?: string;
totalGB?: number;
expiryTime?: number;
comment?: string;
tgId?: string;
enable?: boolean;
limitIp?: number;
created_at?: number;
updated_at?: number;
}
interface InboundInfo {
protocol: string;
clients: ClientSetting[];
settings: Record<string, unknown>;
isTcp: boolean;
isWs: boolean;
isHttpupgrade: boolean;
isXHTTP: boolean;
isGrpc: boolean;
isSSMultiUser: boolean;
isSS2022: boolean;
isVlessTlsFlow: boolean;
host: string | null;
path: string | null;
serviceName: string;
serverName: string;
stream: {
network: string;
security: string;
xhttp?: { mode?: string };
grpc?: { multiMode?: boolean };
};
}
interface DBInboundLike {
id: number;
address: string;
port: number;
listen: string;
protocol: string;
remark: string;
enable?: boolean;
isVMess?: boolean;
isVLess?: boolean;
isTrojan?: boolean;
isSS?: boolean;
isMixed?: boolean;
isHTTP?: boolean;
isWireguard?: boolean;
settings: unknown;
streamSettings: unknown;
sniffing: unknown;
clientStats?: ClientStats[];
}
function buildInboundInfo(dbInbound: DBInboundLike): InboundInfo {
const settings = coerceInboundJsonField(dbInbound.settings) as Record<string, unknown>;
const stream = coerceInboundJsonField(dbInbound.streamSettings) as Record<string, unknown>;
const network = (stream.network as string | undefined) ?? '';
const security = (stream.security as string | undefined) ?? 'none';
const clients = Array.isArray(settings.clients) ? (settings.clients as ClientSetting[]) : [];
const xhttpSettings = stream.xhttpSettings as { mode?: string } | undefined;
const grpcSettings = stream.grpcSettings as { multiMode?: boolean; serviceName?: string } | undefined;
let serverName = '';
if (security === 'tls') {
const tls = stream.tlsSettings as { sni?: string; serverName?: string } | undefined;
serverName = tls?.sni ?? tls?.serverName ?? '';
} else if (security === 'reality') {
const reality = stream.realitySettings as { serverNames?: string[]; serverName?: string } | undefined;
if (Array.isArray(reality?.serverNames)) {
serverName = reality.serverNames.join(', ');
} else if (reality?.serverName) {
serverName = reality.serverName;
}
}
return {
protocol: dbInbound.protocol,
clients,
settings,
isTcp: network === 'tcp',
isWs: network === 'ws',
isHttpupgrade: network === 'httpupgrade',
isXHTTP: network === 'xhttp',
isGrpc: network === 'grpc',
isSSMultiUser: isSSMultiUserHelper({
protocol: dbInbound.protocol,
settings: settings as { method?: string },
}),
isSS2022: isSS2022Helper({
protocol: dbInbound.protocol,
settings: settings as { method?: string },
}),
isVlessTlsFlow: canEnableTlsFlow({
protocol: dbInbound.protocol,
streamSettings: { network, security },
}),
host: readNetworkHost(stream, network),
path: readNetworkPath(stream, network),
serviceName: grpcSettings?.serviceName ?? '',
serverName,
stream: {
network,
security,
xhttp: xhttpSettings ? { mode: xhttpSettings.mode } : undefined,
grpc: grpcSettings ? { multiMode: grpcSettings.multiMode } : undefined,
},
};
}
interface InboundInfoModalProps {
open: boolean;
onClose: () => void;
dbInbound: DBInboundLike | null;
clientIndex?: number;
remarkModel?: string;
expireDiff?: number;
trafficDiff?: number;
ipLimitEnable?: boolean;
tgBotEnable?: boolean;
nodeAddress?: string;
subSettings?: SubSettings;
lastOnlineMap?: Record<string, number>;
}
function copyText(value: unknown, t: (k: string) => string) {
ClipboardManager.copyText(String(value ?? '')).then((ok) => {
if (ok) getMessage().success(t('copied'));
});
}
function downloadText(content: string, filename: string) {
FileManager.downloadTextFile(content, filename);
}
function statsColor(stats: ClientStats, trafficDiff: number) {
return ColorUtils.usageColor(stats.up + stats.down, trafficDiff, stats.total);
}
function formatIpInfo(record: unknown) {
if (record == null) return '';
if (typeof record === 'string' || typeof record === 'number') return String(record);
const r = record as { ip?: string; IP?: string; timestamp?: number | string; Timestamp?: number | string };
const ip = r.ip || r.IP || '';
const ts = r.timestamp || r.Timestamp || 0;
if (!ip) return String(record);
if (!ts) return String(ip);
const date = new Date(Number(ts) * 1000);
const timeStr = date
.toLocaleString('en-GB', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false,
})
.replace(',', '');
return `${ip} (${timeStr})`;
}
export default function InboundInfoModal({ export default function InboundInfoModal({
open, open,
onClose, onClose,

View file

@ -0,0 +1,170 @@
import { getMessage } from '@/utils/messageBus';
import { ColorUtils, ClipboardManager, FileManager } from '@/utils';
import { Protocols } from '@/schemas/primitives';
import { coerceInboundJsonField } from '@/models/dbinbound';
import {
canEnableTlsFlow,
isSS2022 as isSS2022Helper,
isSSMultiUser as isSSMultiUserHelper,
} from '@/lib/xray/protocol-capabilities';
import type { ClientSetting, ClientStats, DBInboundLike, InboundInfo } from './types';
const LINK_PROTOCOLS: ReadonlySet<string> = new Set([
Protocols.VMESS,
Protocols.VLESS,
Protocols.TROJAN,
Protocols.SHADOWSOCKS,
Protocols.HYSTERIA,
]);
export function hasShareLink(protocol: string): boolean {
return LINK_PROTOCOLS.has(protocol);
}
function readHeader(headers: unknown, name: string): string {
const needle = name.toLowerCase();
if (Array.isArray(headers)) {
for (const h of headers) {
if (h && typeof h === 'object' && String((h as { name?: string }).name ?? '').toLowerCase() === needle) {
return String((h as { value?: unknown }).value ?? '');
}
}
return '';
}
if (headers && typeof headers === 'object') {
for (const [k, v] of Object.entries(headers as Record<string, unknown>)) {
if (k.toLowerCase() === needle) {
return Array.isArray(v) ? String(v[0] ?? '') : String(v ?? '');
}
}
}
return '';
}
function readNetworkHost(stream: Record<string, unknown>, network: string): string | null {
switch (network) {
case 'tcp': {
const tcp = stream.tcpSettings as { header?: { request?: { headers?: unknown } } } | undefined;
return readHeader(tcp?.header?.request?.headers, 'host');
}
case 'ws': {
const ws = stream.wsSettings as { host?: string; headers?: unknown } | undefined;
return (ws?.host && ws.host.length > 0) ? ws.host : readHeader(ws?.headers, 'host');
}
case 'httpupgrade': {
const hu = stream.httpupgradeSettings as { host?: string; headers?: unknown } | undefined;
return (hu?.host && hu.host.length > 0) ? hu.host : readHeader(hu?.headers, 'host');
}
case 'xhttp': {
const xh = stream.xhttpSettings as { host?: string; headers?: unknown } | undefined;
return (xh?.host && xh.host.length > 0) ? xh.host : readHeader(xh?.headers, 'host');
}
default:
return null;
}
}
function readNetworkPath(stream: Record<string, unknown>, network: string): string | null {
switch (network) {
case 'tcp': {
const tcp = stream.tcpSettings as { header?: { request?: { path?: string[] } } } | undefined;
return tcp?.header?.request?.path?.[0] ?? null;
}
case 'ws':
return (stream.wsSettings as { path?: string } | undefined)?.path ?? null;
case 'httpupgrade':
return (stream.httpupgradeSettings as { path?: string } | undefined)?.path ?? null;
case 'xhttp':
return (stream.xhttpSettings as { path?: string } | undefined)?.path ?? null;
default:
return null;
}
}
export function buildInboundInfo(dbInbound: DBInboundLike): InboundInfo {
const settings = coerceInboundJsonField(dbInbound.settings) as Record<string, unknown>;
const stream = coerceInboundJsonField(dbInbound.streamSettings) as Record<string, unknown>;
const network = (stream.network as string | undefined) ?? '';
const security = (stream.security as string | undefined) ?? 'none';
const clients = Array.isArray(settings.clients) ? (settings.clients as ClientSetting[]) : [];
const xhttpSettings = stream.xhttpSettings as { mode?: string } | undefined;
const grpcSettings = stream.grpcSettings as { multiMode?: boolean; serviceName?: string } | undefined;
let serverName = '';
if (security === 'tls') {
const tls = stream.tlsSettings as { sni?: string; serverName?: string } | undefined;
serverName = tls?.sni ?? tls?.serverName ?? '';
} else if (security === 'reality') {
const reality = stream.realitySettings as { serverNames?: string[]; serverName?: string } | undefined;
if (Array.isArray(reality?.serverNames)) {
serverName = reality.serverNames.join(', ');
} else if (reality?.serverName) {
serverName = reality.serverName;
}
}
return {
protocol: dbInbound.protocol,
clients,
settings,
isTcp: network === 'tcp',
isWs: network === 'ws',
isHttpupgrade: network === 'httpupgrade',
isXHTTP: network === 'xhttp',
isGrpc: network === 'grpc',
isSSMultiUser: isSSMultiUserHelper({
protocol: dbInbound.protocol,
settings: settings as { method?: string },
}),
isSS2022: isSS2022Helper({
protocol: dbInbound.protocol,
settings: settings as { method?: string },
}),
isVlessTlsFlow: canEnableTlsFlow({
protocol: dbInbound.protocol,
streamSettings: { network, security },
}),
host: readNetworkHost(stream, network),
path: readNetworkPath(stream, network),
serviceName: grpcSettings?.serviceName ?? '',
serverName,
stream: {
network,
security,
xhttp: xhttpSettings ? { mode: xhttpSettings.mode } : undefined,
grpc: grpcSettings ? { multiMode: grpcSettings.multiMode } : undefined,
},
};
}
export function copyText(value: unknown, t: (k: string) => string) {
ClipboardManager.copyText(String(value ?? '')).then((ok) => {
if (ok) getMessage().success(t('copied'));
});
}
export function downloadText(content: string, filename: string) {
FileManager.downloadTextFile(content, filename);
}
export function statsColor(stats: ClientStats, trafficDiff: number) {
return ColorUtils.usageColor(stats.up + stats.down, trafficDiff, stats.total);
}
export function formatIpInfo(record: unknown) {
if (record == null) return '';
if (typeof record === 'string' || typeof record === 'number') return String(record);
const r = record as { ip?: string; IP?: string; timestamp?: number | string; Timestamp?: number | string };
const ip = r.ip || r.IP || '';
const ts = r.timestamp || r.Timestamp || 0;
if (!ip) return String(record);
if (!ts) return String(ip);
const date = new Date(Number(ts) * 1000);
const timeStr = date
.toLocaleString('en-GB', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false,
})
.replace(',', '');
return `${ip} (${timeStr})`;
}

View file

@ -0,0 +1,87 @@
import type { SubSettings } from '../useInbounds';
export interface ClientStats {
email: string;
up: number;
down: number;
total: number;
expiryTime: number;
enable?: boolean;
}
export interface ClientSetting {
email?: string;
id?: string;
security?: string;
password?: string;
flow?: string;
subId?: string;
totalGB?: number;
expiryTime?: number;
comment?: string;
tgId?: string;
enable?: boolean;
limitIp?: number;
created_at?: number;
updated_at?: number;
}
export interface InboundInfo {
protocol: string;
clients: ClientSetting[];
settings: Record<string, unknown>;
isTcp: boolean;
isWs: boolean;
isHttpupgrade: boolean;
isXHTTP: boolean;
isGrpc: boolean;
isSSMultiUser: boolean;
isSS2022: boolean;
isVlessTlsFlow: boolean;
host: string | null;
path: string | null;
serviceName: string;
serverName: string;
stream: {
network: string;
security: string;
xhttp?: { mode?: string };
grpc?: { multiMode?: boolean };
};
}
export interface DBInboundLike {
id: number;
address: string;
port: number;
listen: string;
protocol: string;
remark: string;
enable?: boolean;
isVMess?: boolean;
isVLess?: boolean;
isTrojan?: boolean;
isSS?: boolean;
isMixed?: boolean;
isHTTP?: boolean;
isWireguard?: boolean;
settings: unknown;
streamSettings: unknown;
sniffing: unknown;
clientStats?: ClientStats[];
}
export interface InboundInfoModalProps {
open: boolean;
onClose: () => void;
dbInbound: DBInboundLike | null;
clientIndex?: number;
remarkModel?: string;
expireDiff?: number;
trafficDiff?: number;
ipLimitEnable?: boolean;
tgBotEnable?: boolean;
nodeAddress?: string;
subSettings?: SubSettings;
lastOnlineMap?: Record<string, number>;
}