mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-03 09:06:36 +08:00
feat: monitor tunnel top level
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
import type {
|
||||
MonitorTunnelApiItem,
|
||||
NodeMetricApiItem,
|
||||
TunnelMetricApiItem,
|
||||
ServiceMonitorApiItem,
|
||||
ServiceMonitorResultApiItem,
|
||||
ServiceMonitorMutationPayload,
|
||||
@@ -34,9 +32,7 @@ import {
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import {
|
||||
getMonitorTunnels,
|
||||
getNodeMetrics,
|
||||
getTunnelMetrics,
|
||||
getServiceMonitorList,
|
||||
getServiceMonitorLimits,
|
||||
getServiceMonitorResults,
|
||||
@@ -287,17 +283,7 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
|
||||
const [metricsRangeMs, setMetricsRangeMs] = useState(60 * 60 * 1000);
|
||||
|
||||
const [tunnels, setTunnels] = useState<MonitorTunnelApiItem[]>([]);
|
||||
const [tunnelsLoading, setTunnelsLoading] = useState(false);
|
||||
const [, setTunnelsError] = useState<string | null>(null);
|
||||
const [selectedTunnelId, setSelectedTunnelId] = useState<number | null>(null);
|
||||
const [tunnelMetrics, setTunnelMetrics] = useState<TunnelMetricApiItem[]>([]);
|
||||
const [tunnelMetricsLoading, setTunnelMetricsLoading] = useState(false);
|
||||
const [, setTunnelMetricsTruncated] = useState(false);
|
||||
const [tunnelMetricsError, setTunnelMetricsError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [tunnelRangeMs, setTunnelRangeMs] = useState(60 * 60 * 1000);
|
||||
|
||||
|
||||
const [serviceMonitors, setServiceMonitors] = useState<
|
||||
ServiceMonitorApiItem[]
|
||||
@@ -327,6 +313,8 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
enabled: true,
|
||||
});
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
const [activeServiceMonitorId, setActiveServiceMonitorId] = useState<number | null>(null);
|
||||
const [serviceMonitorRangeLimit, setServiceMonitorRangeLimit] = useState(50);
|
||||
|
||||
const [accessDenied, setAccessDenied] = useState<string | null>(null);
|
||||
const [resultsModalOpen, setResultsModalOpen] = useState(false);
|
||||
@@ -446,111 +434,7 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
}
|
||||
}, [preferredNodeId, selectedNodeId]);
|
||||
|
||||
const loadTunnels = useCallback(async (options?: { silent?: boolean }) => {
|
||||
const silent = options?.silent ?? false;
|
||||
if (!silent) setTunnelsLoading(true);
|
||||
try {
|
||||
const response = await getMonitorTunnels();
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
setAccessDenied(null);
|
||||
setTunnelsError(null);
|
||||
setTunnels(response.data);
|
||||
|
||||
return;
|
||||
}
|
||||
if (response.code === 403) {
|
||||
setAccessDenied(response.msg || "暂无监控权限,请联系管理员授权");
|
||||
setTunnelsError(null);
|
||||
setTunnels([]);
|
||||
|
||||
return;
|
||||
}
|
||||
setTunnelsError(response.msg || "加载隧道列表失败");
|
||||
if (!silent) toast.error(response.msg || "加载隧道列表失败");
|
||||
} catch {
|
||||
if (!silent) {
|
||||
setTunnelsError("加载隧道列表失败");
|
||||
toast.error("加载隧道列表失败");
|
||||
}
|
||||
} finally {
|
||||
if (!silent) setTunnelsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadTunnels();
|
||||
}, [loadTunnels]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
void loadTunnels({ silent: true });
|
||||
}, 60_000);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, [loadTunnels]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTunnelId && tunnels.length > 0) {
|
||||
setSelectedTunnelId(tunnels[0].id);
|
||||
}
|
||||
}, [selectedTunnelId, tunnels]);
|
||||
|
||||
const loadTunnelMetrics = useCallback(
|
||||
async (tunnelId: number, options?: { silent?: boolean }) => {
|
||||
const silent = options?.silent ?? false;
|
||||
if (!silent) setTunnelMetricsLoading(true);
|
||||
try {
|
||||
const end = Date.now();
|
||||
const start = end - tunnelRangeMs;
|
||||
const response = await getTunnelMetrics(tunnelId, start, end);
|
||||
|
||||
if (response.code === 0 && Array.isArray(response.data)) {
|
||||
setAccessDenied(null);
|
||||
setTunnelMetricsError(null);
|
||||
setTunnelMetricsTruncated(response.data.length >= METRICS_MAX_ROWS);
|
||||
const ordered = [...response.data].sort(
|
||||
(a, b) => a.timestamp - b.timestamp,
|
||||
);
|
||||
|
||||
setTunnelMetrics(ordered);
|
||||
|
||||
return;
|
||||
}
|
||||
if (response.code === 403) {
|
||||
setAccessDenied(response.msg || "暂无监控权限,请联系管理员授权");
|
||||
setTunnelMetricsTruncated(false);
|
||||
setTunnelMetricsError(null);
|
||||
|
||||
return;
|
||||
}
|
||||
setTunnelMetricsTruncated(false);
|
||||
setTunnelMetricsError(response.msg || "加载隧道指标失败");
|
||||
if (!silent) toast.error(response.msg || "加载隧道指标失败");
|
||||
} catch {
|
||||
setTunnelMetricsTruncated(false);
|
||||
if (!silent) setTunnelMetricsError("加载隧道指标失败");
|
||||
} finally {
|
||||
if (!silent) setTunnelMetricsLoading(false);
|
||||
}
|
||||
},
|
||||
[tunnelRangeMs],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTunnelId) {
|
||||
void loadTunnelMetrics(selectedTunnelId);
|
||||
}
|
||||
}, [selectedTunnelId, loadTunnelMetrics]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTunnelId) return;
|
||||
const timer = window.setInterval(() => {
|
||||
void loadTunnelMetrics(selectedTunnelId, { silent: true });
|
||||
}, 30_000);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, [selectedTunnelId, loadTunnelMetrics]);
|
||||
|
||||
const loadMetrics = useCallback(
|
||||
async (nodeId: number, options?: { silent?: boolean }) => {
|
||||
@@ -757,6 +641,12 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
void loadResultsForModal();
|
||||
}, [resultsModalOpen, resultsMonitorId, resultsLimit, loadResultsForModal]);
|
||||
|
||||
// Reload results for the active service monitor chart when range limit changes
|
||||
useEffect(() => {
|
||||
if (!activeServiceMonitorId) return;
|
||||
void loadMonitorResults(activeServiceMonitorId, serviceMonitorRangeLimit);
|
||||
}, [activeServiceMonitorId, serviceMonitorRangeLimit, loadMonitorResults]);
|
||||
|
||||
const chartData = metrics.map((m) => ({
|
||||
time: formatTimestamp(m.timestamp, metricsRangeMs),
|
||||
cpu: m.cpuUsage,
|
||||
@@ -816,21 +706,7 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const tunnelChartData = tunnelMetrics.map((m) => ({
|
||||
time: formatTimestamp(m.timestamp, tunnelRangeMs),
|
||||
bytesIn: m.bytesIn,
|
||||
bytesOut: m.bytesOut,
|
||||
}));
|
||||
|
||||
const getTunnelChartConfig = () => {
|
||||
return {
|
||||
lines: [
|
||||
{ dataKey: "bytesIn", color: "#10b981", name: "入站流量" },
|
||||
{ dataKey: "bytesOut", color: "#ef4444", name: "出站流量" },
|
||||
],
|
||||
yAxisLabel: "流量",
|
||||
};
|
||||
};
|
||||
|
||||
const handleOpenEditModal = (monitor?: ServiceMonitorApiItem) => {
|
||||
if (monitor) {
|
||||
@@ -1091,7 +967,6 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
}, [getLatestResult, isResultStale, serviceMonitors]);
|
||||
|
||||
const chartConfig = getChartConfig();
|
||||
const tunnelChartConfig = getTunnelChartConfig();
|
||||
|
||||
const nodeYAxisTickFormatter = (value: unknown) => {
|
||||
const n = Number(value);
|
||||
@@ -1131,21 +1006,7 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const tunnelYAxisTickFormatter = (value: unknown) => {
|
||||
const n = Number(value);
|
||||
|
||||
if (!Number.isFinite(n)) return "";
|
||||
|
||||
return formatBytes(n);
|
||||
};
|
||||
|
||||
const tunnelTooltipFormatter = (value: unknown) => {
|
||||
const n = Number(value);
|
||||
|
||||
if (!Number.isFinite(n)) return "-";
|
||||
|
||||
return formatBytes(n);
|
||||
};
|
||||
|
||||
const detailNode = detailNodeId != null ? nodes.find((n) => n.id === detailNodeId) : null;
|
||||
const detailRealtimeMetric = detailNodeId != null ? realtimeNodeMetrics[detailNodeId] || null : null;
|
||||
@@ -1440,158 +1301,158 @@ export function MonitorView({ nodeMap, viewMode = "grid" }: MonitorViewProps) {
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* Tunnel traffic chart */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">隧道流量趋势</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
className="w-56"
|
||||
placeholder={tunnelsLoading ? "加载中..." : "选择隧道"}
|
||||
selectedKeys={selectedTunnelId ? [String(selectedTunnelId)] : []}
|
||||
onSelectionChange={(keys) => {
|
||||
const tunnelId = Number(Array.from(keys)[0]);
|
||||
setSelectedTunnelId(tunnelId || null);
|
||||
}}
|
||||
>
|
||||
{tunnels.map((t) => (
|
||||
<SelectItem key={String(t.id)}>{t.name}{t.status === 1 ? "" : " (禁用)"}</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
className="w-36"
|
||||
selectedKeys={[String(tunnelRangeMs)]}
|
||||
onSelectionChange={(keys) => {
|
||||
const v = Number(Array.from(keys)[0]);
|
||||
if (v > 0) setTunnelRangeMs(v);
|
||||
}}
|
||||
>
|
||||
<SelectItem key={String(15 * 60 * 1000)}>15分钟</SelectItem>
|
||||
<SelectItem key={String(60 * 60 * 1000)}>1小时</SelectItem>
|
||||
<SelectItem key={String(6 * 60 * 60 * 1000)}>6小时</SelectItem>
|
||||
<SelectItem key={String(24 * 60 * 60 * 1000)}>24小时</SelectItem>
|
||||
</Select>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{selectedTunnelId ? (
|
||||
tunnelMetricsLoading ? (
|
||||
<div className="flex justify-center py-8"><RefreshCw className="w-6 h-6 animate-spin" /></div>
|
||||
) : tunnelMetricsError ? (
|
||||
<div className="text-center py-8 text-danger text-sm">{tunnelMetricsError}</div>
|
||||
) : tunnelMetrics.length > 0 ? (
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer height="100%" width="100%">
|
||||
<LineChart data={tunnelChartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="time" fontSize={12} />
|
||||
<YAxis fontSize={12} tickFormatter={tunnelYAxisTickFormatter} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: "rgba(0,0,0,0.8)", border: "none", borderRadius: "8px" }}
|
||||
labelStyle={{ color: "#fff" }}
|
||||
formatter={tunnelTooltipFormatter}
|
||||
/>
|
||||
{tunnelChartConfig.lines.map((line) => (
|
||||
<Line key={line.dataKey} dataKey={line.dataKey} dot={false} name={line.name} stroke={line.color} strokeWidth={2} type="monotone" />
|
||||
))}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
{/* Service monitors chart – same style as node metrics */}
|
||||
{(() => {
|
||||
// Resolve which monitor is active
|
||||
const resolvedActiveMonitor =
|
||||
detailServiceMonitors.find((m) => m.id === activeServiceMonitorId) ||
|
||||
detailServiceMonitors[0] ||
|
||||
null;
|
||||
const resolvedActiveMonitorId = resolvedActiveMonitor?.id ?? null;
|
||||
const activeLatestResult = resolvedActiveMonitorId != null ? getLatestResult(resolvedActiveMonitorId) : null;
|
||||
const activeStale = resolvedActiveMonitor ? isResultStale(resolvedActiveMonitor, activeLatestResult) : false;
|
||||
const activeResults = resolvedActiveMonitorId != null ? (monitorResults[resolvedActiveMonitorId] || []) : [];
|
||||
const activeLatencyData = [...activeResults]
|
||||
.slice(0, serviceMonitorRangeLimit)
|
||||
.reverse()
|
||||
.map((r) => ({
|
||||
time: formatTimestamp(r.timestamp),
|
||||
latency: r.success === 1 ? r.latencyMs : null,
|
||||
success: r.success,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">服务监控图表</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
className="w-28"
|
||||
selectedKeys={[String(serviceMonitorRangeLimit)]}
|
||||
onSelectionChange={(keys) => {
|
||||
const v = Number(Array.from(keys)[0]);
|
||||
if (v > 0) setServiceMonitorRangeLimit(v);
|
||||
}}
|
||||
>
|
||||
<SelectItem key="20">20条</SelectItem>
|
||||
<SelectItem key="50">50条</SelectItem>
|
||||
<SelectItem key="100">100条</SelectItem>
|
||||
<SelectItem key="200">200条</SelectItem>
|
||||
</Select>
|
||||
{resolvedActiveMonitorId != null && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={() => {
|
||||
void loadMonitorResults(resolvedActiveMonitorId, serviceMonitorRangeLimit);
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-1" />
|
||||
刷新
|
||||
</Button>
|
||||
)}
|
||||
<Button color="primary" size="sm" variant="flat" onPress={() => handleOpenEditModal()}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
添加监控
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-default-500">暂无指标数据</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-center py-8 text-default-500">请选择一个隧道查看指标</div>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</CardHeader>
|
||||
<CardBody className="space-y-4">
|
||||
{monitorsLoading ? (
|
||||
<div className="flex justify-center py-8"><RefreshCw className="w-6 h-6 animate-spin" /></div>
|
||||
) : detailServiceMonitors.length > 0 ? (
|
||||
<>
|
||||
{/* Monitor switcher buttons – like metric type tabs */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{detailServiceMonitors.map((monitor) => {
|
||||
const lr = getLatestResult(monitor.id);
|
||||
const statusColor =
|
||||
monitor.enabled !== 1 ? "default" :
|
||||
!lr ? "default" :
|
||||
lr.success === 1 ? "success" : "danger";
|
||||
const isActive = monitor.id === resolvedActiveMonitorId;
|
||||
|
||||
{/* Service monitors with latency charts */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">服务监控</h3>
|
||||
<Button color="primary" size="sm" variant="flat" onPress={() => handleOpenEditModal()}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
添加监控
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardBody className="space-y-4">
|
||||
{monitorsLoading ? (
|
||||
<div className="flex justify-center py-8"><RefreshCw className="w-6 h-6 animate-spin" /></div>
|
||||
) : detailServiceMonitors.length > 0 ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{detailServiceMonitors.map((monitor) => {
|
||||
const latestResult = getLatestResult(monitor.id);
|
||||
const stale = isResultStale(monitor, latestResult);
|
||||
const results = monitorResults[monitor.id] || [];
|
||||
const latencyData = [...results].reverse().map((r) => ({
|
||||
time: formatTimestamp(r.timestamp),
|
||||
latency: r.success === 1 ? r.latencyMs : null,
|
||||
success: r.success,
|
||||
}));
|
||||
return (
|
||||
<Button
|
||||
key={monitor.id}
|
||||
color={isActive ? "primary" : statusColor as any}
|
||||
size="sm"
|
||||
variant={isActive ? "solid" : "flat"}
|
||||
onPress={() => {
|
||||
setActiveServiceMonitorId(monitor.id);
|
||||
// Load results for this monitor if not loaded
|
||||
if (!monitorResults[monitor.id] || monitorResults[monitor.id].length <= 1) {
|
||||
void loadMonitorResults(monitor.id, serviceMonitorRangeLimit);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={`w-2 h-2 rounded-full shrink-0 mr-1 ${
|
||||
monitor.enabled !== 1 ? "bg-default-300" :
|
||||
!lr ? "bg-default-400" :
|
||||
lr.success === 1 ? (isActive ? "bg-white" : "bg-success") : (isActive ? "bg-white" : "bg-danger")
|
||||
}`} />
|
||||
{monitor.name}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
return (
|
||||
<Card key={monitor.id} className="border border-default-200/60">
|
||||
<CardHeader className="pb-1 flex flex-row items-center justify-between">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className={`w-2.5 h-2.5 rounded-full shrink-0 ${
|
||||
monitor.enabled !== 1 ? "bg-default-300" :
|
||||
!latestResult ? "bg-default-400" :
|
||||
latestResult.success === 1 ? "bg-success" : "bg-danger"
|
||||
}`} />
|
||||
<span className="font-semibold text-sm truncate">{monitor.name}</span>
|
||||
<Chip size="sm" color="primary" variant="flat">{monitor.type.toUpperCase()}</Chip>
|
||||
{stale ? <Chip size="sm" color="warning" variant="flat">陈旧</Chip> : null}
|
||||
{/* Active monitor info bar */}
|
||||
{resolvedActiveMonitor && (
|
||||
<div className="flex items-center justify-between gap-3 p-3 rounded-lg bg-default-50 dark:bg-default-100/30 border border-divider">
|
||||
<div className="flex items-center gap-3 min-w-0 flex-wrap">
|
||||
<Chip size="sm" color="primary" variant="flat">{resolvedActiveMonitor.type.toUpperCase()}</Chip>
|
||||
<span className="font-mono text-xs text-default-500">{resolvedActiveMonitor.target}</span>
|
||||
<span className="text-xs text-default-500">间隔 {resolvedActiveMonitor.intervalSec}s</span>
|
||||
{activeLatestResult && Number.isFinite(activeLatestResult.latencyMs) ? (
|
||||
<span className="font-mono text-xs font-semibold text-success">{activeLatestResult.latencyMs.toFixed(0)}ms</span>
|
||||
) : null}
|
||||
{activeStale ? <Chip size="sm" color="warning" variant="flat">陈旧</Chip> : null}
|
||||
{resolvedActiveMonitor.enabled !== 1 ? <Chip size="sm" color="default" variant="flat">已禁用</Chip> : null}
|
||||
</div>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly size="sm" variant="light"><MoreVertical className="w-4 h-4" /></Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownItem startContent={<Play className="w-4 h-4" />} onPress={() => handleRunMonitor(monitor.id)}>立即检查</DropdownItem>
|
||||
<DropdownItem startContent={<Activity className="w-4 h-4" />} onPress={() => openResultsModal(monitor.id)}>查看记录</DropdownItem>
|
||||
<DropdownItem startContent={<Edit className="w-4 h-4" />} onPress={() => handleOpenEditModal(monitor)}>编辑</DropdownItem>
|
||||
<DropdownItem className="text-danger" color="danger" startContent={<Trash2 className="w-4 h-4" />} onPress={() => handleDeleteMonitor(monitor.id)}>删除</DropdownItem>
|
||||
<DropdownItem startContent={<Play className="w-4 h-4" />} onPress={() => resolvedActiveMonitorId != null && handleRunMonitor(resolvedActiveMonitorId)}>立即检查</DropdownItem>
|
||||
<DropdownItem startContent={<Activity className="w-4 h-4" />} onPress={() => resolvedActiveMonitorId != null && openResultsModal(resolvedActiveMonitorId)}>查看记录</DropdownItem>
|
||||
<DropdownItem startContent={<Edit className="w-4 h-4" />} onPress={() => resolvedActiveMonitor && handleOpenEditModal(resolvedActiveMonitor)}>编辑</DropdownItem>
|
||||
<DropdownItem className="text-danger" color="danger" startContent={<Trash2 className="w-4 h-4" />} onPress={() => resolvedActiveMonitorId != null && handleDeleteMonitor(resolvedActiveMonitorId)}>删除</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</CardHeader>
|
||||
<CardBody className="pt-0 space-y-2">
|
||||
<div className="flex items-center gap-3 text-xs text-default-500">
|
||||
<span className="font-mono">{monitor.target}</span>
|
||||
<span>间隔 {monitor.intervalSec}s</span>
|
||||
{latestResult && Number.isFinite(latestResult.latencyMs) ? (
|
||||
<span className="font-mono text-success">{latestResult.latencyMs.toFixed(0)}ms</span>
|
||||
) : null}
|
||||
</div>
|
||||
{latencyData.length > 0 ? (
|
||||
<div className="h-24">
|
||||
<ResponsiveContainer height="100%" width="100%">
|
||||
<LineChart data={latencyData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(128,128,128,0.15)" />
|
||||
<XAxis dataKey="time" fontSize={10} tick={false} />
|
||||
<YAxis fontSize={10} tickFormatter={(v: number) => `${Math.round(v)}ms`} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: "rgba(0,0,0,0.85)", border: "none", borderRadius: "8px", fontSize: "12px" }}
|
||||
labelStyle={{ color: "#fff" }}
|
||||
formatter={(value: unknown) => [`${Number(value).toFixed(0)}ms`, "延迟"]}
|
||||
/>
|
||||
<Line dataKey="latency" dot={false} stroke="#10b981" strokeWidth={1.5} type="monotone" connectNulls={false} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-default-400 py-2">暂无检查记录</div>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-default-500">暂无服务监控,点击"添加监控"创建</div>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Chart area – same h-64 as node metrics */}
|
||||
{activeLatencyData.length > 0 ? (
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer height="100%" width="100%">
|
||||
<LineChart data={activeLatencyData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="time" fontSize={12} />
|
||||
<YAxis fontSize={12} tickFormatter={(v: number) => `${Math.round(v)}ms`} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: "rgba(0,0,0,0.8)", border: "none", borderRadius: "8px" }}
|
||||
labelStyle={{ color: "#fff" }}
|
||||
formatter={(value: unknown) => [`${Number(value).toFixed(0)}ms`, "延迟"]}
|
||||
/>
|
||||
<Line dataKey="latency" dot={false} stroke="#10b981" strokeWidth={2} type="monotone" connectNulls={false} name="延迟" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-default-500">暂无检查记录</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-8 text-default-500">暂无服务监控,点击"添加监控"创建</div>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user