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 f47ac7311fc233ca5bce740c94cbff2e8ebfeb80
Author: Logic <[email protected]>
AuthorDate: Sun May 31 09:50:20 2026 +0800

    fix(web-next): surface topology windowed metric table
---
 web-next/app/topology/page.test.tsx     | 82 +++++++++++++++++++++++++++++++++
 web-next/app/topology/topology-page.tsx | 75 +++++++++++++++++++++---------
 2 files changed, 134 insertions(+), 23 deletions(-)

diff --git a/web-next/app/topology/page.test.tsx 
b/web-next/app/topology/page.test.tsx
index f7db52bbe8..5b2c37689a 100644
--- a/web-next/app/topology/page.test.tsx
+++ b/web-next/app/topology/page.test.tsx
@@ -181,6 +181,54 @@ describe('topology page', () => {
     };
   }
 
+  function buildLargeApiTopologyFixture() {
+    const nodes = Array.from({ length: 230 }, (_, index) => {
+      const padded = String(index).padStart(3, '0');
+      return {
+        id: `scale-svc-${padded}`,
+        entityId: `service:scale/${padded}`,
+        entityName: `Scale API ${padded}`,
+        entityType: index % 20 === 0 && index > 0 ? 'database' : 'service',
+        namespace: 'scale',
+        environment: 'prod',
+        health: index % 17 === 0 ? 'critical' : index % 7 === 0 ? 'warning' : 
'healthy',
+        evidenceBadges: ['entity-relation', 'otlp-trace-call'],
+        redMetrics: {
+          requestRatePerSecond: 4 + (index % 23),
+          errorRate: Number((((index % 11) + 1) / 1000).toFixed(3)),
+          latencyP95Ms: 40 + (index % 13) * 8
+        }
+      };
+    });
+    const edges = Array.from({ length: 229 }, (_, index) => {
+      const from = String(index).padStart(3, '0');
+      const to = String(index + 1).padStart(3, '0');
+      return {
+        sourceNodeId: `scale-svc-${from}`,
+        targetNodeId: `scale-svc-${to}`,
+        relationType: 'HTTP call',
+        relationSource: 'otlp-trace-call',
+        status: index % 17 === 0 ? 'critical' : index % 7 === 0 ? 'warning' : 
'active',
+        score: 80 - (index % 12),
+        evidenceBadges: ['entity-relation', 'otlp-trace-call'],
+        redMetrics: {
+          requestRatePerSecond: 5 + (index % 29),
+          errorRate: Number((((index % 13) + 1) / 1000).toFixed(3)),
+          latencyP95Ms: 45 + (index % 19) * 7
+        }
+      };
+    });
+
+    return {
+      apiBacked: true,
+      focusEntityId: 'service:scale/000',
+      depth: 2,
+      sourceKinds: ['entity-relation', 'otlp-trace-call'],
+      nodes,
+      edges
+    };
+  }
+
   it('keeps topology on the HertzBeat entity relationship surface instead of 
copied service-map chrome', () => {
     const source = readFileSync(resolve(process.cwd(), 
'app/topology/topology-page.tsx'), 'utf8');
 
@@ -1551,6 +1599,40 @@ describe('topology page', () => {
     expect(html).toContain('边指标排行');
   }, 60000);
 
+  it('surfaces the render-window RED table below the graph when a real API 
topology is windowed', async () => {
+    const routeContext = {
+      environment: 'prod',
+      timeRange: 'last-1h',
+      sourceKind: 'otlp-trace-call',
+      viewMode: 'service-call',
+      groupBy: 'source-kind',
+      depth: '2'
+    };
+    const { default: TopologyPage } = await import('./topology-page');
+    const source = readFileSync(resolve(process.cwd(), 
'app/topology/topology-page.tsx'), 'utf8');
+    const html = renderToStaticMarkup(<TopologyPage 
routeContext={routeContext} apiGraph={buildLargeApiTopologyFixture()} />);
+
+    expect(source).toContain('const topologyShouldShowRenderWindowMetricTable 
=');
+    expect(source).toContain("topologyRenderWindowCompanion.mode === 
'windowed'");
+    expect(source).toContain("topologyRenderWindowCompanion.tableCompanion === 
'required'");
+    
expect(html).toContain('data-hz-topology-g6-render-window-mode="windowed"');
+    
expect(html).toContain('data-hz-topology-g6-render-window-total-node-count="230"');
+    
expect(html).toContain('data-hz-topology-g6-render-window-hidden-node-count="30"');
+    
expect(html).toContain('data-topology-metric-table-placement="graph-bottom"');
+    
expect(html).toContain('data-topology-metric-table-visibility="render-window-companion"');
+    
expect(html).toContain('data-topology-metric-table-scope="edge-red-render-window"');
+    
expect(html).toContain('data-hz-topology-metric-table-render-window-mode="windowed"');
+    
expect(html).toContain('data-hz-topology-metric-table-render-window-total-node-count="230"');
+    
expect(html).toContain('data-hz-topology-metric-table-render-window-hidden-node-count="30"');
+    
expect(html).toContain('data-hz-topology-metric-table-render-window-table-companion="required"');
+    
expect(html).toContain('data-hz-topology-metric-table-hidden-node-companion="required"');
+    
expect(html).toContain('data-hz-topology-metric-table-filter-control="hidden"');
+    
expect(html).toContain('data-hz-topology-edge-row-render-window-visibility="hidden"');
+    
expect(html.indexOf('data-topology-g6-canvas-owner="hertzbeat-ui-g6-canvas"')).toBeLessThan(
+      html.indexOf('data-topology-metric-table-placement="graph-bottom"')
+    );
+  }, 60000);
+
   it('renders API-backed impact timeline evidence when topology returns change 
events', async () => {
     i18nState.locale = 'en-US';
     const { default: TopologyPage } = await import('./topology-page');
diff --git a/web-next/app/topology/topology-page.tsx 
b/web-next/app/topology/topology-page.tsx
index 76feb0035c..20ec9f0449 100644
--- a/web-next/app/topology/topology-page.tsx
+++ b/web-next/app/topology/topology-page.tsx
@@ -1017,6 +1017,31 @@ export default function TopologyPage({
     const strategy = buildHzTopologyG6LargeGraphStrategy(topologyG6Graph);
     return buildHzTopologyG6RenderWindow(topologyG6Graph, strategy, { 
priorityNodeIds: topologyRenderWindowPriorityNodeIds });
   }, [topologyG6Graph, topologyRenderWindowPriorityNodeIds]);
+  const topologyMetricRenderWindowCompanion = React.useMemo(() => ({
+    mode: topologyRenderWindowCompanion.mode,
+    totalNodeCount: topologyRenderWindowCompanion.totalNodeCount,
+    renderedNodeCount: topologyRenderWindowCompanion.renderedNodeCount,
+    hiddenNodeCount: topologyRenderWindowCompanion.hiddenNodeCount,
+    visibleNodeBudget: topologyRenderWindowCompanion.visibleNodeBudget,
+    tableCompanion: topologyRenderWindowCompanion.tableCompanion,
+    priorityNodeIds: topologyRenderWindowCompanion.priorityNodeIds,
+    renderedNodeIds: topologyRenderWindowCompanion.graph.nodes.map(node => 
node.id)
+  }), [topologyRenderWindowCompanion]);
+  const topologyShouldShowRenderWindowMetricTable =
+    topologyRenderWindowCompanion.mode === 'windowed' || 
topologyRenderWindowCompanion.tableCompanion === 'required';
+  const topologyMetricTableLabels = React.useMemo(() => ({
+    edgeCount: t('topology.metric-table.edge-count', { count: 
topologyMetricRows.length }),
+    requestRate: t('topology.metric-table.request-rate-unit'),
+    errorRate: t('topology.metric-table.error-rate-unit'),
+    latencyP95: t('topology.metric-table.latency-p95-unit'),
+    rowAction: t('topology.metric-table.row-action'),
+    renderWindowFilterAll: t('topology.metric-table.filter.all'),
+    renderWindowFilterVisible: t('topology.metric-table.filter.visible'),
+    renderWindowFilterPartial: t('topology.metric-table.filter.partial'),
+    renderWindowFilterHidden: t('topology.metric-table.filter.hidden'),
+    renderWindowFilterUnknown: t('topology.metric-table.filter.unknown'),
+    rowAriaLabel: (row: HzTopologyMetricRow) => 
t('topology.metric-table.open-edge-aria', { edge: String(row.id) })
+  }), [t, topologyMetricRows.length]);
   const topologyHoveredDetailEdge = topologyG6HoveredEdgeId ? 
findEdge(map.edges, topologyG6HoveredEdgeId) : undefined;
   const topologyLiveHoverEdge = topologyHoveredDetailEdge;
   const topologyInvestigationEdge = topologyHoveredDetailEdge ?? 
topologyDetailEdge;
@@ -1738,30 +1763,9 @@ export default function TopologyPage({
               selectedRowId={topologyMetricSelectedRowId}
               renderWindowFilter={topologyMetricWindowFilter}
               onRenderWindowFilterChange={setTopologyMetricWindowFilter}
-              renderWindowCompanion={{
-                mode: topologyRenderWindowCompanion.mode,
-                totalNodeCount: topologyRenderWindowCompanion.totalNodeCount,
-                renderedNodeCount: 
topologyRenderWindowCompanion.renderedNodeCount,
-                hiddenNodeCount: topologyRenderWindowCompanion.hiddenNodeCount,
-                visibleNodeBudget: 
topologyRenderWindowCompanion.visibleNodeBudget,
-                tableCompanion: topologyRenderWindowCompanion.tableCompanion,
-                priorityNodeIds: topologyRenderWindowCompanion.priorityNodeIds,
-                renderedNodeIds: 
topologyRenderWindowCompanion.graph.nodes.map(node => node.id)
-              }}
+              renderWindowCompanion={topologyMetricRenderWindowCompanion}
               emptyLabel={t('topology.metric-table.empty')}
-              labels={{
-                edgeCount: t('topology.metric-table.edge-count', { count: 
topologyMetricRows.length }),
-                requestRate: t('topology.metric-table.request-rate-unit'),
-                errorRate: t('topology.metric-table.error-rate-unit'),
-                latencyP95: t('topology.metric-table.latency-p95-unit'),
-                rowAction: t('topology.metric-table.row-action'),
-                renderWindowFilterAll: t('topology.metric-table.filter.all'),
-                renderWindowFilterVisible: 
t('topology.metric-table.filter.visible'),
-                renderWindowFilterPartial: 
t('topology.metric-table.filter.partial'),
-                renderWindowFilterHidden: 
t('topology.metric-table.filter.hidden'),
-                renderWindowFilterUnknown: 
t('topology.metric-table.filter.unknown'),
-                rowAriaLabel: row => t('topology.metric-table.open-edge-aria', 
{ edge: String(row.id) })
-              }}
+              labels={topologyMetricTableLabels}
               onRowSelect={handleTopologyMetricRowSelect}
               boundary="framed"
             />
@@ -1901,6 +1905,31 @@ export default function TopologyPage({
         </HzTopologyWorkbenchSlot>
       </HzTopologyWorkbenchGrid>
 
+      {topologyShouldShowRenderWindowMetricTable && topologyMetricRows.length 
> 0 ? (
+        <HzTopologyMetricTable
+          id="topology-metric-table"
+          data-topology-metric-table-owner="hertzbeat-ui"
+          data-topology-metric-table-placement="graph-bottom"
+          data-topology-metric-table-visibility="render-window-companion"
+          data-topology-metric-table-scope="edge-red-render-window"
+          
data-topology-metric-table-boundary-owner="hertzbeat-ui-metric-table-boundary"
+          
data-topology-metric-table-interaction-owner="hertzbeat-ui-metric-table-interaction"
+          
data-topology-metric-table-selection-clear-owner="hertzbeat-ui-g6-hover-clear"
+          data-topology-metric-table-filter-behavior="in-page-no-route-change"
+          title={t('topology.metric-table.title')}
+          density="graph-first"
+          rows={topologyMetricRows}
+          selectedRowId={topologyMetricSelectedRowId}
+          renderWindowFilter={topologyMetricWindowFilter}
+          onRenderWindowFilterChange={setTopologyMetricWindowFilter}
+          renderWindowCompanion={topologyMetricRenderWindowCompanion}
+          emptyLabel={t('topology.metric-table.empty')}
+          labels={topologyMetricTableLabels}
+          onRowSelect={handleTopologyMetricRowSelect}
+          boundary="framed"
+        />
+      ) : null}
+
       {map.faultContextRows.length > 0 ? (
         <HzTopologyEvidenceList
           data-topology-fault-context="incoming-evidence"


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

Reply via email to