This is an automated email from the ASF dual-hosted git repository.

zqr10159 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git

commit 72ddbb80203eacec94cee63662773b5694ee23c4
Author: Logic <[email protected]>
AuthorDate: Fri May 29 22:08:23 2026 +0800

    fix(web-next): scope topology legend to visible graph types
---
 web-next/app/topology/page.test.tsx               |  2 ++
 web-next/app/topology/topology-page.tsx           | 24 ++++++++++++++++++-----
 web-next/app/ui-lab/page.test.tsx                 |  5 +++++
 web-next/app/ui-lab/page.tsx                      |  8 +++++++-
 web-next/packages/hertzbeat-ui/src/index.test.tsx | 23 ++++++++++++++++++++++
 web-next/packages/hertzbeat-ui/src/index.tsx      |  5 +++--
 6 files changed, 59 insertions(+), 8 deletions(-)

diff --git a/web-next/app/topology/page.test.tsx 
b/web-next/app/topology/page.test.tsx
index 5f5f377db7..33ffd36fbd 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -956,6 +956,8 @@ describe('topology page', () => {
            
expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
            
expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
            
expect(html).toContain('data-hz-topology-legend-item="node-type-database"');
+           
expect(html).not.toContain('data-hz-topology-legend-item="node-type-alert"');
+           
expect(html).not.toContain('data-hz-topology-legend-item="node-type-resource"');
            
expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
            
expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
            
expect(html).toContain('data-hz-topology-legend-icon-name="blocks"');
diff --git a/web-next/app/topology/topology-page.tsx 
b/web-next/app/topology/topology-page.tsx
index f40c4c4d33..ea04862bef 100644
--- a/web-next/app/topology/topology-page.tsx
+++ b/web-next/app/topology/topology-page.tsx
@@ -45,6 +45,7 @@ import {
   HZ_TOPOLOGY_G6_NODE_ICON_CATALOG,
   buildHzTopologyG6LargeGraphStrategy,
   buildHzTopologyG6RenderWindow,
+  getHzTopologyG6NodeIcon,
   type HzTopologyG6GraphInput,
   type HzTopologyG6HoverAnchor
 } from '@hertzbeat/ui/topology-g6';
@@ -72,6 +73,13 @@ function findEdge(edges: TopologyServiceEdge[], id: string) {
   return edges.find(edge => edge.id === id);
 }
 
+function resolveTopologyG6EntityType(node: Pick<TopologyServiceNode, 
'entityType' | 'id' | 'label' | 'source'>) {
+  const directIcon = getHzTopologyG6NodeIcon(node.entityType);
+  if (directIcon.kind !== 'unknown') return directIcon.kind;
+  const inferredIcon = getHzTopologyG6NodeIcon(`${node.label} ${node.source} 
${node.id}`);
+  return inferredIcon.kind === 'unknown' ? node.entityType : inferredIcon.kind;
+}
+
 function formatTimeRange(timeRange: string, t: (key: string) => string) {
   if (timeRange === 'last-1h') return t('topology.time.last-1h');
   return timeRange;
@@ -738,7 +746,7 @@ export default function TopologyPage({
     nodes: topologyCanvasNodes.map(node => ({
       id: node.id,
       label: node.label,
-      entityType: node.entityType,
+      entityType: resolveTopologyG6EntityType(node),
       health: node.health,
       tone: node.tone,
       focus: node.focus,
@@ -1124,8 +1132,13 @@ export default function TopologyPage({
       ? t('topology.degraded.api.source')
       : 'API';
   const topologyLegendNodeTypeItems = React.useMemo(
-    () =>
-      HZ_TOPOLOGY_G6_NODE_ICON_CATALOG.filter(icon => icon.kind !== 
'unknown').map(icon => ({
+    () => {
+      const visibleNodeKinds = new Set(
+        topologyG6Graph.nodes
+          .map(node => getHzTopologyG6NodeIcon(node.entityType).kind)
+          .filter(kind => kind !== 'unknown')
+      );
+      return HZ_TOPOLOGY_G6_NODE_ICON_CATALOG.filter(icon => 
visibleNodeKinds.has(icon.kind)).map(icon => ({
         id: `node-type-${icon.kind}`,
         label: t(`topology.legend.node-type.${icon.kind}`),
         value: icon.iconName,
@@ -1135,8 +1148,9 @@ export default function TopologyPage({
         iconName: icon.iconName,
         iconSource: icon.iconSource,
         visualSource: 'lucide-react' as const
-      })),
-    [t]
+      }));
+    },
+    [topologyG6Graph.nodes, t]
   );
   const topologyLegendSections = React.useMemo(
     () => [
diff --git a/web-next/app/ui-lab/page.test.tsx 
b/web-next/app/ui-lab/page.test.tsx
index 6b600075e3..73be41f88f 100644
--- a/web-next/app/ui-lab/page.test.tsx
+++ b/web-next/app/ui-lab/page.test.tsx
@@ -2697,6 +2697,11 @@ describe('HertzBeat UI lab page', () => {
            
expect(html).not.toContain('data-hz-topology-legend-section="confidence"');
            
expect(html).toContain('data-hz-topology-legend-item="node-type-service"');
            
expect(html).toContain('data-hz-topology-legend-item="node-type-database"');
+           
expect(html).toContain('data-hz-topology-legend-item="node-type-middleware"');
+           
expect(html).toContain('data-hz-topology-legend-item="node-type-monitor"');
+           
expect(html).not.toContain('data-hz-topology-legend-item="node-type-application"');
+           
expect(html).not.toContain('data-hz-topology-legend-item="node-type-alert"');
+           
expect(html).not.toContain('data-hz-topology-legend-item="node-type-resource"');
            
expect(html).toContain('data-hz-topology-legend-icon-library="lucide-react"');
            
expect(html).toContain('data-hz-topology-legend-icon-source="entity-type-catalog"');
            
expect(html).toContain('data-hz-topology-legend-icon-no-handdrawn="true"');
diff --git a/web-next/app/ui-lab/page.tsx b/web-next/app/ui-lab/page.tsx
index 6d4aa16b02..0693307083 100644
--- a/web-next/app/ui-lab/page.tsx
+++ b/web-next/app/ui-lab/page.tsx
@@ -209,6 +209,7 @@ import {
   buildHzTopologyG6RenderWindow,
   buildHzTopologyG6ScaleFixture,
   buildHzTopologyG6ScaleProfile,
+  getHzTopologyG6NodeIcon,
   type HzTopologyG6GraphInput
 } from '@hertzbeat/ui/topology-g6';
 import { AlertNoticeRuleSwitch } from 
'../../components/pages/alert-notice-rule-fields';
@@ -495,7 +496,12 @@ const topologyG6LargeGraphStrategy500 = 
buildHzTopologyG6LargeGraphStrategy(topo
 const topologyG6ScaleLabRenderWindow500 = 
buildHzTopologyG6RenderWindow(topologyG6ScaleLabGraph500, 
topologyG6LargeGraphStrategy500, {
   priorityNodeIds: ['scale-svc-420']
 });
-const topologyG6NodeTypeLegendItems = 
HZ_TOPOLOGY_G6_NODE_ICON_CATALOG.filter(icon => icon.kind !== 
'unknown').map(icon => ({
+const topologyG6LabVisibleNodeKinds = new Set(
+  topologyG6LabGraph.nodes.map(node => 
getHzTopologyG6NodeIcon(node.entityType).kind).filter(kind => kind !== 
'unknown')
+);
+const topologyG6NodeTypeLegendItems = 
HZ_TOPOLOGY_G6_NODE_ICON_CATALOG.filter(icon =>
+  topologyG6LabVisibleNodeKinds.has(icon.kind)
+).map(icon => ({
   id: `node-type-${icon.kind}`,
   label: icon.label,
   value: icon.iconName,
diff --git a/web-next/packages/hertzbeat-ui/src/index.test.tsx 
b/web-next/packages/hertzbeat-ui/src/index.test.tsx
index e045fa1fc7..fa75a423af 100644
--- a/web-next/packages/hertzbeat-ui/src/index.test.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.test.tsx
@@ -5262,6 +5262,29 @@ describe('@hertzbeat/ui', () => {
     expect(html).not.toContain('2 groups');
   });
 
+  it('omits empty topology legend sections so graph-scoped legends do not 
advertise absent node types', () => {
+    const html = renderToStaticMarkup(
+      <HzTopologyLegend
+        title="Legend"
+        boundary="flush"
+        density="canvas-dock"
+        sections={[
+          { id: 'node-type', label: 'Node type', items: [] },
+          {
+            id: 'status',
+            label: 'Status',
+            items: [{ id: 'healthy-node', label: 'Healthy', value: 'healthy', 
visualSource: 'hertzbeat-status-token' }]
+          }
+        ]}
+      />
+    );
+
+    expect(html).not.toContain('data-hz-topology-legend-section="node-type"');
+    expect(html).not.toContain('Node type');
+    expect(html).toContain('data-hz-topology-legend-section="status"');
+    expect(html).toContain('data-hz-topology-legend-item="healthy-node"');
+  });
+
   it('renders a topology detail drawer for edge evidence and cross-signal 
handoffs', () => {
     const html = renderToStaticMarkup(
       <HzTopologyDetailDrawer
diff --git a/web-next/packages/hertzbeat-ui/src/index.tsx 
b/web-next/packages/hertzbeat-ui/src/index.tsx
index f9cfcc8d04..9d2cf77344 100644
--- a/web-next/packages/hertzbeat-ui/src/index.tsx
+++ b/web-next/packages/hertzbeat-ui/src/index.tsx
@@ -11204,6 +11204,7 @@ export function HzTopologyLegend({
   ...props
 }: HzTopologyLegendProps) {
   const isCanvasDock = density === 'canvas-dock';
+  const visibleSections = sections.filter(section => section.items.length > 0);
 
   return (
     <section
@@ -11247,7 +11248,7 @@ export function HzTopologyLegend({
             className="font-mono text-[10px] uppercase tracking-[0.08em] 
text-[#727b8c]"
             data-hz-topology-legend-summary-owner="hertzbeat-ui-legend-summary"
           >
-            {summaryLabel ?? `${sections.length} groups`}
+            {summaryLabel ?? `${visibleSections.length} groups`}
           </span>
         ) : null}
       </header>
@@ -11257,7 +11258,7 @@ export function HzTopologyLegend({
           isCanvasDock ? 'flex flex-wrap gap-x-3 gap-y-1 px-2 py-1.5' : 'grid 
divide-y divide-[var(--hz-ui-line-faint)]'
         )}
       >
-        {sections.map(section => (
+        {visibleSections.map(section => (
           <div
             key={section.id}
             className={cn(


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to