mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
feat: 更新节点详情页,添加健康事件过滤和流量分析功能
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user