feat: 更新节点详情页,添加健康事件过滤和流量分析功能

This commit is contained in:
ryan
2026-03-14 14:31:51 +08:00
parent 91cafa99b1
commit 4bfad019d0
4 changed files with 77 additions and 69 deletions
+1 -1
View File
@@ -84,7 +84,7 @@ func AgentHeartbeat(c *gin.Context) {
func AgentGetActiveConfig(c *gin.Context) {
config, err := service.GetActiveConfigForAgent()
if err != nil {
respondFailure(c, "褰撳墠娌℃湁婵€娲荤増鏈?")
respondFailure(c, "当前没有激活版本")
return
}
respondSuccess(c, config)
+21 -16
View File
@@ -18,20 +18,24 @@ type RankChartProps = {
const defaultFormatter = (value: number) => value.toLocaleString('zh-CN');
function getChartValue(params: unknown) {
if (typeof params !== 'object' || params === null || !('value' in params)) {
return 0;
}
const rawValue = (params as { value?: unknown }).value;
if (Array.isArray(rawValue)) {
const candidate = rawValue[0];
return typeof candidate === 'number' ? candidate : 0;
}
return typeof rawValue === 'number' ? rawValue : 0;
}
export function RankChart({
items,
color,
valueFormatter = defaultFormatter,
emptyMessage = '暂无分布数据',
}: RankChartProps) {
if (items.length === 0) {
return (
<div className="flex h-[220px] items-center justify-center rounded-3xl border border-dashed border-[var(--border-default)] bg-[var(--surface-muted)] text-sm text-[var(--foreground-secondary)]">
{emptyMessage}
</div>
);
}
const option = useMemo<EChartsOption>(
() => ({
animationDuration: 400,
@@ -100,14 +104,7 @@ export function RankChart({
show: true,
position: 'right',
color: '#e2e8f0',
formatter: (params: any) => {
const rawValue = Array.isArray(params.value)
? params.value[0]
: params.value;
return valueFormatter(
typeof rawValue === 'number' ? rawValue : 0,
);
},
formatter: (params: unknown) => valueFormatter(getChartValue(params)),
},
},
],
@@ -115,6 +112,14 @@ export function RankChart({
[color, items, valueFormatter],
);
if (items.length === 0) {
return (
<div className="flex h-[220px] items-center justify-center rounded-3xl border border-dashed border-[var(--border-default)] bg-[var(--surface-muted)] text-sm text-[var(--foreground-secondary)]">
{emptyMessage}
</div>
);
}
return (
<ReactECharts
option={option}
@@ -22,14 +22,6 @@ type TrendChartProps = {
const defaultFormatter = (value: number) => value.toLocaleString('zh-CN');
export function TrendChart({ labels, series, height = 220 }: TrendChartProps) {
if (labels.length === 0 || series.length === 0) {
return (
<div className="flex h-[220px] items-center justify-center rounded-3xl border border-dashed border-[var(--border-default)] bg-[var(--surface-muted)] text-sm text-[var(--foreground-secondary)]">
暂无趋势数据
</div>
);
}
const option = useMemo<EChartsOption>(() => {
const maxValue =
Math.max(
@@ -120,6 +112,14 @@ export function TrendChart({ labels, series, height = 220 }: TrendChartProps) {
};
}, [labels, series]);
if (labels.length === 0 || series.length === 0) {
return (
<div className="flex h-[220px] items-center justify-center rounded-3xl border border-dashed border-[var(--border-default)] bg-[var(--surface-muted)] text-sm text-[var(--foreground-secondary)]">
暂无趋势数据
</div>
);
}
return (
<div className="space-y-4">
<div className="flex flex-wrap gap-3">
@@ -499,6 +499,53 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
);
}, [configVersionsQuery.data]);
const observability = observabilityQuery.data ?? null;
const latestMetricSnapshot = observability?.metric_snapshots?.[0] ?? null;
const activeHealthEvents = useMemo(
() =>
observability?.health_events.filter((event) => event.status === 'active') ??
[],
[observability?.health_events],
);
const statusCodeDistribution = useMemo(
() =>
aggregateTrafficBreakdown(
observability?.traffic_reports ?? [],
'status_codes_json',
),
[observability?.traffic_reports],
);
const topDomains = useMemo(
() =>
aggregateTrafficBreakdown(
observability?.traffic_reports ?? [],
'top_domains_json',
),
[observability?.traffic_reports],
);
const resolvedHealthEvents = useMemo(
() =>
observability?.health_events.filter(
(event) => event.status === 'resolved',
) ?? [],
[observability?.health_events],
);
const filteredHealthEvents = useMemo(() => {
switch (healthEventFilter) {
case 'active':
return activeHealthEvents;
case 'resolved':
return resolvedHealthEvents;
default:
return observability?.health_events ?? [];
}
}, [
activeHealthEvents,
healthEventFilter,
observability?.health_events,
resolvedHealthEvents,
]);
if (nodesQuery.isLoading) {
return <LoadingState />;
}
@@ -540,28 +587,6 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
? previewAgentReleaseQuery.isFetching
: stableAgentReleaseQuery.isFetching;
const applyLogs = applyLogsQuery.data ?? [];
const observability = observabilityQuery.data ?? null;
const latestMetricSnapshot = observability?.metric_snapshots?.[0] ?? null;
const latestTrafficReport = observability?.traffic_reports?.[0] ?? null;
const activeHealthEvents =
observability?.health_events.filter((event) => event.status === 'active') ??
[];
const statusCodeDistribution = useMemo(
() =>
aggregateTrafficBreakdown(
observability?.traffic_reports ?? [],
'status_codes_json',
),
[observability?.traffic_reports],
);
const topDomains = useMemo(
() =>
aggregateTrafficBreakdown(
observability?.traffic_reports ?? [],
'top_domains_json',
),
[observability?.traffic_reports],
);
const dominantStatusCode = statusCodeDistribution[0] ?? null;
const dominantDomain = topDomains[0] ?? null;
const topSourceCountry =
@@ -569,28 +594,6 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
const trafficSummary = observability?.analytics.traffic ?? null;
const healthSummary = observability?.analytics.health ?? null;
const latestHealthEvent = activeHealthEvents[0] ?? null;
const resolvedHealthEvents = useMemo(
() =>
observability?.health_events.filter(
(event) => event.status === 'resolved',
) ?? [],
[observability?.health_events],
);
const filteredHealthEvents = useMemo(() => {
switch (healthEventFilter) {
case 'active':
return activeHealthEvents;
case 'resolved':
return resolvedHealthEvents;
default:
return observability?.health_events ?? [];
}
}, [
activeHealthEvents,
healthEventFilter,
observability?.health_events,
resolvedHealthEvents,
]);
const memoryUsageRatio = formatUsageRatio(
latestMetricSnapshot?.memory_used_bytes,
latestMetricSnapshot?.memory_total_bytes,