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]
