mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 08:36:37 +08:00
feat: 更新节点详情页,添加健康事件过滤和流量分析功能
This commit is contained in:
@@ -84,7 +84,7 @@ func AgentHeartbeat(c *gin.Context) {
|
|||||||
func AgentGetActiveConfig(c *gin.Context) {
|
func AgentGetActiveConfig(c *gin.Context) {
|
||||||
config, err := service.GetActiveConfigForAgent()
|
config, err := service.GetActiveConfigForAgent()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondFailure(c, "褰撳墠娌℃湁婵€娲荤増鏈?")
|
respondFailure(c, "当前没有激活版本")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
respondSuccess(c, config)
|
respondSuccess(c, config)
|
||||||
|
|||||||
@@ -18,20 +18,24 @@ type RankChartProps = {
|
|||||||
|
|
||||||
const defaultFormatter = (value: number) => value.toLocaleString('zh-CN');
|
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({
|
export function RankChart({
|
||||||
items,
|
items,
|
||||||
color,
|
color,
|
||||||
valueFormatter = defaultFormatter,
|
valueFormatter = defaultFormatter,
|
||||||
emptyMessage = '暂无分布数据',
|
emptyMessage = '暂无分布数据',
|
||||||
}: RankChartProps) {
|
}: 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>(
|
const option = useMemo<EChartsOption>(
|
||||||
() => ({
|
() => ({
|
||||||
animationDuration: 400,
|
animationDuration: 400,
|
||||||
@@ -100,14 +104,7 @@ export function RankChart({
|
|||||||
show: true,
|
show: true,
|
||||||
position: 'right',
|
position: 'right',
|
||||||
color: '#e2e8f0',
|
color: '#e2e8f0',
|
||||||
formatter: (params: any) => {
|
formatter: (params: unknown) => valueFormatter(getChartValue(params)),
|
||||||
const rawValue = Array.isArray(params.value)
|
|
||||||
? params.value[0]
|
|
||||||
: params.value;
|
|
||||||
return valueFormatter(
|
|
||||||
typeof rawValue === 'number' ? rawValue : 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -115,6 +112,14 @@ export function RankChart({
|
|||||||
[color, items, valueFormatter],
|
[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 (
|
return (
|
||||||
<ReactECharts
|
<ReactECharts
|
||||||
option={option}
|
option={option}
|
||||||
|
|||||||
@@ -22,14 +22,6 @@ type TrendChartProps = {
|
|||||||
const defaultFormatter = (value: number) => value.toLocaleString('zh-CN');
|
const defaultFormatter = (value: number) => value.toLocaleString('zh-CN');
|
||||||
|
|
||||||
export function TrendChart({ labels, series, height = 220 }: TrendChartProps) {
|
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 option = useMemo<EChartsOption>(() => {
|
||||||
const maxValue =
|
const maxValue =
|
||||||
Math.max(
|
Math.max(
|
||||||
@@ -120,6 +112,14 @@ export function TrendChart({ labels, series, height = 220 }: TrendChartProps) {
|
|||||||
};
|
};
|
||||||
}, [labels, series]);
|
}, [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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
|
|||||||
@@ -499,6 +499,53 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
|||||||
);
|
);
|
||||||
}, [configVersionsQuery.data]);
|
}, [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) {
|
if (nodesQuery.isLoading) {
|
||||||
return <LoadingState />;
|
return <LoadingState />;
|
||||||
}
|
}
|
||||||
@@ -540,28 +587,6 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
|||||||
? previewAgentReleaseQuery.isFetching
|
? previewAgentReleaseQuery.isFetching
|
||||||
: stableAgentReleaseQuery.isFetching;
|
: stableAgentReleaseQuery.isFetching;
|
||||||
const applyLogs = applyLogsQuery.data ?? [];
|
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 dominantStatusCode = statusCodeDistribution[0] ?? null;
|
||||||
const dominantDomain = topDomains[0] ?? null;
|
const dominantDomain = topDomains[0] ?? null;
|
||||||
const topSourceCountry =
|
const topSourceCountry =
|
||||||
@@ -569,28 +594,6 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
|||||||
const trafficSummary = observability?.analytics.traffic ?? null;
|
const trafficSummary = observability?.analytics.traffic ?? null;
|
||||||
const healthSummary = observability?.analytics.health ?? null;
|
const healthSummary = observability?.analytics.health ?? null;
|
||||||
const latestHealthEvent = activeHealthEvents[0] ?? 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(
|
const memoryUsageRatio = formatUsageRatio(
|
||||||
latestMetricSnapshot?.memory_used_bytes,
|
latestMetricSnapshot?.memory_used_bytes,
|
||||||
latestMetricSnapshot?.memory_total_bytes,
|
latestMetricSnapshot?.memory_total_bytes,
|
||||||
|
|||||||
Reference in New Issue
Block a user