Skip to content

Commit 6b87303

Browse files
committed
feat: 增加连接池图
1 parent 7a3b565 commit 6b87303

4 files changed

Lines changed: 363 additions & 89 deletions

File tree

app/tunnels/details/page.tsx

Lines changed: 221 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ interface PingTrendData {
125125
ping: number;
126126
}
127127

128+
// 添加连接池趋势数据类型 - 后端返回的是绝对值数据
129+
interface PoolTrendData {
130+
eventTime: string;
131+
pool: number;
132+
}
133+
128134
// 添加流量单位转换函数
129135
const formatTrafficValue = (bytes: number) => {
130136
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
@@ -211,6 +217,10 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
211217
const [pingTrend, setPingTrend] = React.useState<PingTrendData[]>([]);
212218
const [pingRefreshLoading, setPingRefreshLoading] = React.useState(false);
213219
const [pingTimeRange, setPingTimeRange] = React.useState<"1h" | "6h" | "12h" | "24h">("24h");
220+
// 连接池趋势相关状态
221+
const [poolTrend, setPoolTrend] = React.useState<PoolTrendData[]>([]);
222+
const [poolRefreshLoading, setPoolRefreshLoading] = React.useState(false);
223+
const [poolTimeRange, setPoolTimeRange] = React.useState<"1h" | "6h" | "12h" | "24h">("24h");
214224
const [isPasswordVisible, setIsPasswordVisible] = React.useState(false);
215225
// 编辑实例模态控制
216226
const [editModalOpen, setEditModalOpen] = React.useState(false);
@@ -309,6 +319,41 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
309319
return filteredData;
310320
}, []);
311321

322+
// 根据时间范围过滤连接池数据
323+
const filterPoolDataByTimeRange = React.useCallback((data: PoolTrendData[], timeRange: "1h" | "6h" | "12h" | "24h") => {
324+
if (data.length === 0) return data;
325+
326+
// 获取当前时间
327+
const now = new Date();
328+
const hoursAgo = timeRange === "1h" ? 1 : timeRange === "6h" ? 6 : timeRange === "12h" ? 12 : 24;
329+
const cutoffTime = new Date(now.getTime() - hoursAgo * 60 * 60 * 1000);
330+
331+
// 过滤数据
332+
const filteredData = data.filter((item) => {
333+
const timeStr = item.eventTime;
334+
if (!timeStr) return false;
335+
336+
try {
337+
const [datePart, timePart] = timeStr.split(' ');
338+
if (datePart && timePart) {
339+
const [year, month, day] = datePart.split('-').map(Number);
340+
const [hour, minute] = timePart.split(':').map(Number);
341+
const itemTime = new Date(year, month - 1, day, hour, minute);
342+
const isValid = !isNaN(itemTime.getTime());
343+
const isInRange = isValid && itemTime >= cutoffTime;
344+
345+
return isInRange;
346+
}
347+
return false;
348+
} catch (error) {
349+
console.error(`连接池数据时间解析错误: ${timeStr}`, error);
350+
return false;
351+
}
352+
});
353+
354+
return filteredData;
355+
}, []);
356+
312357
// 文件日志控制函数
313358
const handleLogRefresh = React.useCallback(() => {
314359
setLogRefreshTrigger(prev => prev + 1);
@@ -407,6 +452,15 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
407452
setPingTrend(pingData.pingTrend);
408453
}
409454
}
455+
456+
// 获取连接池趋势数据
457+
const poolResponse = await fetch(`/api/tunnels/${resolvedId}/pool-trend`);
458+
if (poolResponse.ok) {
459+
const poolData = await poolResponse.json();
460+
if (poolData.poolTrend && Array.isArray(poolData.poolTrend)) {
461+
setPoolTrend(poolData.poolTrend);
462+
}
463+
}
410464

411465
// 刷新文件日志 - 直接更新trigger而不依赖handleLogRefresh
412466
setLogRefreshTrigger(prev => prev + 1);
@@ -542,12 +596,48 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
542596
}
543597
}, [resolvedId]);
544598

599+
// 获取连接池趋势数据
600+
const fetchPoolTrend = React.useCallback(async () => {
601+
try {
602+
setPoolRefreshLoading(true);
603+
604+
const response = await fetch(`/api/tunnels/${resolvedId}/pool-trend`);
605+
if (!response.ok) {
606+
throw new Error('获取连接池趋势失败');
607+
}
608+
609+
const data = await response.json();
610+
611+
// 设置连接池趋势数据
612+
if (data.poolTrend && Array.isArray(data.poolTrend)) {
613+
setPoolTrend(data.poolTrend);
614+
console.log('[连接池趋势] 数据获取成功', {
615+
数据点数: data.poolTrend.length,
616+
最新数据: data.poolTrend[data.poolTrend.length - 1] || null
617+
});
618+
} else {
619+
console.log('[连接池趋势] 数据为空或格式错误', { poolTrend: data.poolTrend });
620+
setPoolTrend([]);
621+
}
622+
} catch (error) {
623+
console.error('获取连接池趋势失败:', error);
624+
addToast({
625+
title: "获取连接池趋势失败",
626+
description: error instanceof Error ? error.message : "未知错误",
627+
color: "danger",
628+
});
629+
} finally {
630+
setPoolRefreshLoading(false);
631+
}
632+
}, [resolvedId]);
633+
545634
// 初始加载数据
546635
React.useEffect(() => {
547636
fetchTunnelDetails();
548637
fetchTrafficTrend();
549638
fetchPingTrend();
550-
}, [fetchTunnelDetails, fetchTrafficTrend, fetchPingTrend]);
639+
fetchPoolTrend();
640+
}, [fetchTunnelDetails, fetchTrafficTrend, fetchPingTrend, fetchPoolTrend]);
551641

552642
// SSE监听逻辑
553643
useTunnelSSE(tunnelInfo?.instanceId || "", {
@@ -574,6 +664,8 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
574664
fetchTrafficTrend();
575665
// 刷新延迟趋势
576666
fetchPingTrend();
667+
// 刷新连接池趋势
668+
fetchPoolTrend();
577669
// 刷新日志(通过触发器)
578670
setLogRefreshTrigger(prev => prev + 1);
579671

@@ -1072,7 +1164,14 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
10721164
<CellValue label="实例ID" value={tunnelInfo.instanceId} />
10731165
<CellValue
10741166
label="主控"
1075-
value={<Chip variant="bordered" color="default" size="sm">{tunnelInfo.endpoint}</Chip>}
1167+
value={
1168+
<div className="flex items-center gap-2">
1169+
<Chip variant="bordered" color="default" size="sm">{tunnelInfo.endpoint}</Chip>
1170+
<Chip variant="flat" color="secondary" size="sm">
1171+
{tunnelInfo.endpointVersion || "< v1.4.0"}
1172+
</Chip>
1173+
</div>
1174+
}
10761175
/>
10771176

10781177
<CellValue
@@ -1214,7 +1313,7 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
12141313
key="command"
12151314
aria-label="命令行"
12161315
title={
1217-
<h3 className="text-lg font-semibold">命令行</h3>
1316+
<h3 className="text-lg font-semibold ps-1">命令行</h3>
12181317
}
12191318
>
12201319
<div className="pb-4">
@@ -1408,93 +1507,127 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
14081507
</div>
14091508
</CardBody>
14101509
</Card>
1411-
1412-
{/* 延迟趋势图表 - 仅服务端隧道显示 */}
1413-
{tunnelInfo.type === '服务端1' && (
1414-
<Card className="p-2">
1415-
<CardHeader className="font-bold text-sm md:text-base justify-between">
1416-
<div className="flex items-center gap-3">
1417-
<div className="flex items-center gap-2">
1418-
延迟趋势
1419-
<Tooltip content="仅服务端隧道显示延迟数据" placement="top">
1420-
<FontAwesomeIcon
1421-
icon={faQuestionCircle}
1422-
className="text-default-400 hover:text-default-600 cursor-help text-xs"
1423-
/>
1424-
</Tooltip>
1425-
</div>
1426-
</div>
1510+
1511+
{/* 连接池变化 - 独立Card - 只有当traffic.pool不为null时才显示 */}
1512+
{tunnelInfo.traffic.pool !== null && (
1513+
<Card className="p-2">
1514+
<CardHeader className="flex items-center justify-between">
1515+
<div className="flex items-center gap-3">
14271516
<div className="flex items-center gap-2">
1428-
{/* 刷新按钮 */}
1429-
<Button
1430-
size="sm"
1431-
variant="flat"
1432-
isIconOnly
1433-
onPress={fetchTrafficTrend}
1434-
isLoading={trafficRefreshLoading}
1435-
className="h-7 w-7 min-w-0"
1436-
>
1437-
<FontAwesomeIcon icon={faRefresh} className="text-xs" />
1438-
</Button>
1439-
{/* 时间范围选择 */}
1440-
<Tabs
1441-
selectedKey={trafficTimeRange}
1442-
onSelectionChange={(key) => setTrafficTimeRange(key as "1h" | "6h" | "12h" | "24h")}
1443-
size="sm"
1444-
variant="light"
1445-
classNames={{
1446-
tabList: "gap-1",
1447-
tab: "text-xs px-2 py-1 min-w-0 h-7",
1448-
tabContent: "text-xs"
1449-
}}
1450-
>
1451-
<Tab key="1h" title="1小时" />
1452-
<Tab key="6h" title="6小时" />
1453-
<Tab key="12h" title="12小时" />
1454-
<Tab key="24h" title="24小时" />
1455-
</Tabs>
1517+
<h3 className="text-lg font-semibold">连接池变化</h3>
1518+
<Tooltip content="显示隧道连接池数量的变化趋势" placement="right">
1519+
<FontAwesomeIcon
1520+
icon={faQuestionCircle}
1521+
className="text-default-400 hover:text-default-600 cursor-help text-xs"
1522+
/>
1523+
</Tooltip>
14561524
</div>
1457-
</CardHeader>
1458-
<CardBody>
1459-
<div className="h-64">
1460-
{(() => {
1461-
// 使用过滤后的数据计算单位
1462-
if (!trafficTrend || !Array.isArray(trafficTrend) || trafficTrend.length === 0) {
1463-
return <div className="flex items-center justify-center h-full text-default-400">暂无延迟数据</div>;
1464-
}
1465-
1466-
const filteredData = filterDataByTimeRange(trafficTrend, trafficTimeRange);
1467-
if (filteredData.length === 0) {
1468-
return <div className="flex items-center justify-center h-full text-default-400">所选时间范围内暂无延迟数据</div>;
1469-
}
1470-
1471-
// 检查是否有延迟数据
1472-
const hasPingData = filteredData.some((item: TrafficTrendData) => item.pingDiff !== null && item.pingDiff !== undefined);
1473-
if (!hasPingData) {
1474-
return <div className="flex items-center justify-center h-full text-default-400">暂无延迟数据</div>;
1475-
}
1476-
1477-
return (
1478-
<FlowTrafficChart
1479-
data={[{
1480-
id: `延迟`,
1481-
data: filteredData.map((item: TrafficTrendData) => ({
1482-
x: item.eventTime || '',
1483-
y: Number(item.pingDiff) || 0,
1484-
unit: 'ms'
1525+
</div>
1526+
<div className="flex items-center gap-2">
1527+
{/* 刷新按钮 */}
1528+
<Button
1529+
size="sm"
1530+
variant="flat"
1531+
isIconOnly
1532+
onPress={fetchPoolTrend}
1533+
isLoading={poolRefreshLoading}
1534+
className="h-7 w-7 min-w-0"
1535+
>
1536+
<FontAwesomeIcon icon={faRefresh} className="text-xs" />
1537+
</Button>
1538+
{/* 时间范围选择 */}
1539+
<Tabs
1540+
selectedKey={poolTimeRange}
1541+
onSelectionChange={(key) => setPoolTimeRange(key as "1h" | "6h" | "12h" | "24h")}
1542+
size="sm"
1543+
variant="light"
1544+
classNames={{
1545+
tabList: "gap-1",
1546+
tab: "text-xs px-2 py-1 min-w-0 h-7",
1547+
tabContent: "text-xs"
1548+
}}
1549+
>
1550+
<Tab key="1h" title="1小时" />
1551+
<Tab key="6h" title="6小时" />
1552+
<Tab key="12h" title="12小时" />
1553+
<Tab key="24h" title="24小时" />
1554+
</Tabs>
1555+
</div>
1556+
</CardHeader>
1557+
<CardBody>
1558+
<div className="h-[250px] md:h-[300px]">
1559+
{loading ? (
1560+
<div className="flex items-center justify-center h-full">
1561+
<div className="space-y-4 text-center">
1562+
<div className="flex justify-center">
1563+
<div className="relative w-8 h-8">
1564+
<div className="absolute inset-0 rounded-full border-4 border-default-200 border-t-primary animate-spin" />
1565+
</div>
1566+
</div>
1567+
<p className="text-default-500 animate-pulse text-sm md:text-base">加载连接池数据中...</p>
1568+
</div>
1569+
</div>
1570+
) : (() => {
1571+
// 检查原始数据是否为空
1572+
if (!poolTrend || !Array.isArray(poolTrend) || poolTrend.length === 0) {
1573+
return true; // 显示占位符
1574+
}
1575+
1576+
// 检查过滤后的数据是否为空
1577+
const filteredData = filterPoolDataByTimeRange(poolTrend, poolTimeRange);
1578+
return filteredData.length === 0;
1579+
})() ? (
1580+
<div className="flex items-center justify-center h-full">
1581+
<div className="text-center">
1582+
<p className="text-default-500 text-base md:text-lg">暂无连接池数据</p>
1583+
<p className="text-default-400 text-xs md:text-sm mt-2">
1584+
{!poolTrend || poolTrend.length === 0
1585+
? "当实例运行时,连接池变化数据将在此显示"
1586+
: `在过去${poolTimeRange === "1h" ? "1小时" : poolTimeRange === "6h" ? "6小时" : poolTimeRange === "12h" ? "12小时" : "24小时"}内暂无连接池数据`
1587+
}
1588+
</p>
1589+
</div>
1590+
</div>
1591+
) : (
1592+
<FlowTrafficChart
1593+
key={`pool-${poolTimeRange}-${poolTrend?.length || 0}`} // 强制重新渲染
1594+
timeRange={poolTimeRange}
1595+
showLegend={false}
1596+
data={(() => {
1597+
// 安全检查
1598+
if (!poolTrend || !Array.isArray(poolTrend) || poolTrend.length === 0) {
1599+
return [];
1600+
}
1601+
1602+
// 首先根据时间范围过滤数据
1603+
const filteredData = filterPoolDataByTimeRange(poolTrend, poolTimeRange);
1604+
1605+
if (filteredData.length === 0) return [];
1606+
1607+
const chartData = [
1608+
{
1609+
id: `连接池`,
1610+
data: filteredData.map((item: PoolTrendData) => ({
1611+
x: item.eventTime || '', // 直接使用后端返回的格式 "2025-06-26 18:40"
1612+
y: Number(item.pool) || 0,
1613+
unit: '个'
14851614
}))
1486-
}]}
1487-
unit="ms"
1488-
/>
1489-
);
1490-
})()}
1491-
</div>
1492-
</CardBody>
1493-
</Card>
1494-
)}
1615+
}
1616+
];
1617+
1618+
return chartData;
1619+
})()}
1620+
unit="个"
1621+
/>
1622+
)}
1623+
</div>
1624+
</CardBody>
1625+
</Card>
1626+
)}
14951627

1496-
{/* 延迟变化 - 独立Card */}
1497-
<Card className="p-2">
1628+
{/* 延迟变化 - 独立Card - 只有当traffic.ping不为null时才显示 */}
1629+
{tunnelInfo.traffic.ping !== null && (
1630+
<Card className="p-2">
14981631
<CardHeader className="flex items-center justify-between">
14991632
<div className="flex items-center gap-3">
15001633
<div className="flex items-center gap-2">
@@ -1607,6 +1740,8 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
16071740
</div>
16081741
</CardBody>
16091742
</Card>
1743+
)}
1744+
16101745

16111746
{/* 日志 - 独立Card */}
16121747
<Card className="p-2">

0 commit comments

Comments
 (0)