From 15fc2f533f54463502bc9a2c98bddaa3007375c4 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 14 Mar 2026 12:29:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(nodes):=20=E6=B7=BB=E5=8A=A0=E5=81=A5?= =?UTF-8?q?=E5=BA=B7=E4=BA=8B=E4=BB=B6=E7=AD=9B=E9=80=89=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=92=8C=E8=8A=82=E7=82=B9=E5=88=97=E8=A1=A8=E8=BF=87=E6=BB=A4?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/dashboard-overview.tsx | 5 +- .../nodes/components/node-detail-page.tsx | 62 +++- .../features/nodes/components/nodes-page.tsx | 324 ++++++++++++------ 3 files changed, 281 insertions(+), 110 deletions(-) diff --git a/atsf_server/web/features/dashboard/components/dashboard-overview.tsx b/atsf_server/web/features/dashboard/components/dashboard-overview.tsx index 2d2add5a..0f6cb701 100644 --- a/atsf_server/web/features/dashboard/components/dashboard-overview.tsx +++ b/atsf_server/web/features/dashboard/components/dashboard-overview.tsx @@ -104,10 +104,7 @@ function RiskSignal({ return content; } return ( - + {content} ); diff --git a/atsf_server/web/features/nodes/components/node-detail-page.tsx b/atsf_server/web/features/nodes/components/node-detail-page.tsx index f86b1865..9974d672 100644 --- a/atsf_server/web/features/nodes/components/node-detail-page.tsx +++ b/atsf_server/web/features/nodes/components/node-detail-page.tsx @@ -78,6 +78,8 @@ type FeedbackState = { message: string; }; +type HealthEventFilter = 'all' | 'active' | 'resolved'; + const defaultValues: NodeFormValues = { name: '', auto_update_enabled: false, @@ -266,6 +268,8 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { const [agentUpdateFeedback, setAgentUpdateFeedback] = useState(null); const [serverUrl, setServerUrl] = useState(''); + const [healthEventFilter, setHealthEventFilter] = + useState('all'); const form = useForm({ resolver: zodResolver(nodeSchema), @@ -535,6 +539,21 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { ) ?? [], [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, @@ -1051,7 +1070,43 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) { > {observability?.health_events.length ? (
- {observability.health_events.slice(0, 8).map((event) => ( +
+ + + +
+ + {filteredHealthEvents.slice(0, 8).map((event) => (
))} + {filteredHealthEvents.length === 0 ? ( +
+ 当前筛选下没有健康事件。 +
+ ) : null}
) : ( (null); const [editingNodeId, setEditingNodeId] = useState(null); @@ -97,6 +108,12 @@ export function NodesPage() { refetchInterval: 5000, }); + const configVersionsQuery = useQuery({ + queryKey: ['config-versions'], + queryFn: getConfigVersions, + refetchInterval: 5000, + }); + const saveMutation = useMutation({ mutationFn: async (values: NodeFormValues) => { const payload = toPayload(values); @@ -133,6 +150,50 @@ export function NodesPage() { }); const nodes = useMemo(() => nodesQuery.data ?? [], [nodesQuery.data]); + const activeVersion = useMemo( + () => + (configVersionsQuery.data ?? []).find((item) => item.is_active) + ?.version ?? '', + [configVersionsQuery.data], + ); + const riskFilter = useMemo(() => { + const current = searchParams.get('risk')?.trim().toLowerCase() ?? 'all'; + return supportedRiskFilters.includes(current as NodeRiskFilter) + ? (current as NodeRiskFilter) + : 'all'; + }, [searchParams]); + const filteredNodes = useMemo(() => { + switch (riskFilter) { + case 'offline': + return nodes.filter((node) => node.status === 'offline'); + case 'unhealthy': + return nodes.filter((node) => node.openresty_status === 'unhealthy'); + case 'lagging': + return nodes.filter((node) => { + if (!activeVersion) { + return false; + } + return node.current_version !== activeVersion; + }); + default: + return nodes; + } + }, [activeVersion, nodes, riskFilter]); + + const filterDescription = useMemo(() => { + switch (riskFilter) { + case 'offline': + return '当前仅展示离线节点。'; + case 'unhealthy': + return '当前仅展示 OpenResty 异常节点。'; + case 'lagging': + return activeVersion + ? `当前仅展示未追平激活版本 ${activeVersion} 的节点。` + : '当前没有激活版本,无法筛选配置落后节点。'; + default: + return '列表每 5 秒自动刷新一次。'; + } + }, [activeVersion, riskFilter]); const handleReset = () => { setFeedback(null); @@ -207,7 +268,7 @@ export function NodesPage() { - ) : nodes.length === 0 ? ( + ) : filteredNodes.length === 0 ? ( ) : ( -
- - - - - - - - - - - - - - - {nodes.map((node) => ( - - - - - - - - - +
+
+ + 全部节点 + + + 离线节点 + + + OpenResty 异常 + + + 配置落后 + +
+ +
+
节点状态Agent / OpenResty运行健康当前版本最近应用最近心跳操作
-
-

- {node.name} -

-

- IP:{node.ip || 'null'} -

-
-
- - - {node.agent_version || 'unknown'} /{' '} - {node.nginx_version || 'unknown'} - -
- -
-
- {node.current_version || '未应用'} - -
- -
-
- {isMeaningfulTime(node.last_seen_at) - ? `${formatRelativeTime( - node.last_seen_at, - )} · ${formatDateTime(node.last_seen_at)}` - : '暂无'} - -
- - 详情 - - - handleEdit( - node.id, - node.name, - node.auto_update_enabled, - ) - } - className="px-3 py-2 text-xs" - > - 编辑 - - handleDelete(node.id, node.name)} - disabled={deleteMutation.isPending} - className="px-3 py-2 text-xs" - > - 删除 - -
-
+ + + + + + + + + + - ))} - -
节点状态 + Agent / OpenResty + 运行健康当前版本最近应用最近心跳操作
+ + + {filteredNodes.map((node) => ( + + +
+

+ {node.name} +

+

+ IP:{node.ip || 'null'} +

+
+ + + + + + {node.agent_version || 'unknown'} /{' '} + {node.nginx_version || 'unknown'} + + +
+ +
+ + + {node.current_version || '未应用'} + + +
+ +
+ + + {isMeaningfulTime(node.last_seen_at) + ? `${formatRelativeTime( + node.last_seen_at, + )} · ${formatDateTime(node.last_seen_at)}` + : '暂无'} + + +
+ + 详情 + + + handleEdit( + node.id, + node.name, + node.auto_update_enabled, + ) + } + className="px-3 py-2 text-xs" + > + 编辑 + + handleDelete(node.id, node.name)} + disabled={deleteMutation.isPending} + className="px-3 py-2 text-xs" + > + 删除 + +
+ + + ))} + + +
)}