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 6afe1a7f0e151404b083c1ba03c4127970534b50 Author: Logic <[email protected]> AuthorDate: Sun May 31 10:26:42 2026 +0800 fix(web-next): keep compact topology graphs at 1x --- .../packages/hertzbeat-ui/src/topology-g6.test.tsx | 20 ++++++++- web-next/packages/hertzbeat-ui/src/topology-g6.tsx | 48 +++++++++++++++++++--- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx index 47bd846a98..b4f71a5d5f 100644 --- a/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx +++ b/web-next/packages/hertzbeat-ui/src/topology-g6.test.tsx @@ -6,6 +6,7 @@ import { buildHzTopologyG6Graph, buildHzTopologyG6FilterScope, buildHzTopologyG6GroupSummary, + buildHzTopologyG6InitialFitStrategy, buildHzTopologyG6LargeGraphStrategy, buildHzTopologyG6NeighborFocus, buildHzTopologyG6RenderWindow, @@ -363,6 +364,9 @@ describe('@hertzbeat/ui topology G6 canvas', () => { const source = topologyG6Source; const html = renderToStaticMarkup(<HzTopologyG6Canvas graph={graph} />); + expect(source).toContain('buildHzTopologyG6InitialFitStrategy'); + expect(source).toContain('initialFitStrategy === "center-only"'); + expect(source).toContain('centerOnlyG6Viewport'); expect(source).toContain('scheduleInitialFitView'); expect(source).toContain('fitAndCenterG6Viewport'); expect(source).toContain("fitAndCenterG6Viewport(runtimeGraph, { when: 'overflow' }, false)"); @@ -374,6 +378,8 @@ describe('@hertzbeat/ui topology G6 canvas', () => { expect(html).toContain('data-hz-topology-g6-auto-fit-max-zoom="1"'); expect(html).toContain('data-hz-topology-g6-auto-fit-growth="no-magnify-small-graphs"'); expect(html).toContain('data-hz-topology-g6-auto-fit-zoom-range-owner="hertzbeat-ui-g6-auto-fit-zoom-range"'); + expect(html).toContain('data-hz-topology-g6-initial-fit-strategy="center-only"'); + expect(html).toContain('data-hz-topology-g6-initial-fit-strategy-owner="hertzbeat-ui-g6-initial-fit-strategy"'); expect(html).toContain('data-hz-topology-g6-operator-zoom-bounds="0.18-2.2"'); expect(html).toContain('data-hz-topology-g6-operator-zoom-growth="bounded-readable-nodes"'); expect(html).toContain('data-hz-topology-g6-fit-mode="overflow-only-center"'); @@ -384,6 +390,18 @@ describe('@hertzbeat/ui topology G6 canvas', () => { expect(source).toContain('runtimeGraph.setZoomRange?.([HZ_TOPOLOGY_G6_MIN_ZOOM, HZ_TOPOLOGY_G6_MAX_ZOOM])'); }); + it('keeps compact service graphs at readable one-to-one scale instead of fitting them to fill wide canvases', () => { + const compactGraph = buildHzTopologyG6ScaleFixture(7); + const denseGraph = buildHzTopologyG6ScaleFixture(50); + const compactHtml = renderToStaticMarkup(<HzTopologyG6Canvas graph={compactGraph} />); + const denseHtml = renderToStaticMarkup(<HzTopologyG6Canvas graph={denseGraph} />); + + expect(buildHzTopologyG6InitialFitStrategy(compactGraph)).toBe('center-only'); + expect(buildHzTopologyG6InitialFitStrategy(denseGraph)).toBe('overflow-fit'); + expect(compactHtml).toContain('data-hz-topology-g6-initial-fit-strategy="center-only"'); + expect(denseHtml).toContain('data-hz-topology-g6-initial-fit-strategy="overflow-fit"'); + }); + it('centers the shared G6 canvas after fit and reset view actions', () => { const source = topologyG6Source; const html = renderToStaticMarkup(<HzTopologyG6Canvas graph={buildHzTopologyG6ScaleFixture(8)} />); @@ -1121,7 +1139,7 @@ describe('@hertzbeat/ui topology G6 canvas', () => { expect(source).toContain('initialFitTimerRef.current'); expect(source).toContain('cancelPendingInitialFitView();'); expect(source).toContain('if (source === "initial-fit" && hasUserViewportInteractedRef.current) return;'); - expect(source).toContain('scheduleInitialFitView(runtimeGraph, () => !hasUserViewportInteractedRef.current)'); + expect(source).toContain('scheduleInitialFitView(runtimeGraph, initialFitStrategy, () => !hasUserViewportInteractedRef.current)'); expect(source).toContain('publishViewportTelemetryAfterViewportAction("wheel"'); expect(source).toContain('publishViewportTelemetry("pointer-pan")'); expect(source).toContain('publishViewportTelemetry("redraw-restore")'); diff --git a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx index 4374eb251d..86e954234a 100644 --- a/web-next/packages/hertzbeat-ui/src/topology-g6.tsx +++ b/web-next/packages/hertzbeat-ui/src/topology-g6.tsx @@ -142,6 +142,8 @@ export type HzTopologyG6LargeGraphStrategy = { visibleNodeBudget: number; }; +export type HzTopologyG6InitialFitStrategy = 'center-only' | 'overflow-fit'; + export type HzTopologyG6RenderWindow = { graph: HzTopologyG6GraphInput; mode: 'direct' | 'windowed'; @@ -722,6 +724,12 @@ export function buildHzTopologyG6LargeGraphStrategy(input: HzTopologyG6GraphInpu }; } +export function buildHzTopologyG6InitialFitStrategy(input: HzTopologyG6GraphInput): HzTopologyG6InitialFitStrategy { + const nodeCount = input.nodes.length; + const edgeCount = input.edges.length; + return nodeCount > 0 && nodeCount <= 12 && edgeCount <= 18 ? 'center-only' : 'overflow-fit'; +} + export function buildHzTopologyG6RenderWindow( input: HzTopologyG6GraphInput, strategy: HzTopologyG6LargeGraphStrategy = buildHzTopologyG6LargeGraphStrategy(input), @@ -1150,6 +1158,15 @@ async function fitAndCenterG6Viewport( }); } +async function centerOnlyG6Viewport( + runtimeGraph: G6GraphRuntime | null | undefined, + animation: Record<string, unknown> | boolean +) { + if (!runtimeGraph) return; + await runtimeGraph.zoomTo?.(HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM, animation); + await runtimeGraph.fitCenter?.(animation); +} + async function withG6AutoFitZoomRange(runtimeGraph: G6GraphRuntime, action: () => Promise<void>) { runtimeGraph.setZoomRange?.([HZ_TOPOLOGY_G6_MIN_ZOOM, HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM]); try { @@ -1159,9 +1176,17 @@ async function withG6AutoFitZoomRange(runtimeGraph: G6GraphRuntime, action: () = } } -function scheduleInitialFitView(runtimeGraph: G6GraphRuntime, shouldRun: () => boolean) { +function scheduleInitialFitView( + runtimeGraph: G6GraphRuntime, + initialFitStrategy: HzTopologyG6InitialFitStrategy, + shouldRun: () => boolean +) { return window.setTimeout(() => { if (!shouldRun()) return; + if (initialFitStrategy === "center-only") { + void centerOnlyG6Viewport(runtimeGraph, { duration: 120 }); + return; + } void fitAndCenterG6Viewport(runtimeGraph, { when: 'overflow' }, { duration: 120 }); }, 180); } @@ -1398,6 +1423,7 @@ export function HzTopologyG6Canvas({ () => buildHzTopologyG6RenderWindow(canvasGraph, largeGraphStrategy, { priorityNodeIds: renderWindowPriorityNodeIds }), [canvasGraph, largeGraphStrategy, renderWindowPriorityNodeIds] ); + const initialFitStrategy = React.useMemo(() => buildHzTopologyG6InitialFitStrategy(renderWindow.graph), [renderWindow.graph]); const g6Graph = React.useMemo(() => buildHzTopologyG6Graph(renderWindow.graph), [renderWindow.graph]); const g6RenderKey = React.useMemo(() => buildHzTopologyG6RenderKey(g6Graph), [g6Graph]); latestG6GraphRef.current = g6Graph; @@ -1614,10 +1640,14 @@ export function HzTopologyG6Canvas({ clearSharedHover(); }); await runtimeGraph.render(); - await fitAndCenterG6Viewport(runtimeGraph, { when: 'overflow' }, false); + if (initialFitStrategy === "center-only") { + await centerOnlyG6Viewport(runtimeGraph, false); + } else { + await fitAndCenterG6Viewport(runtimeGraph, { when: 'overflow' }, false); + } lastFitStructureKeyRef.current = graphStructureKey; lastDrawGraphKeyRef.current = latestG6RenderKeyRef.current; - initialFitTimerRef.current = scheduleInitialFitView(runtimeGraph, () => !hasUserViewportInteractedRef.current); + initialFitTimerRef.current = scheduleInitialFitView(runtimeGraph, initialFitStrategy, () => !hasUserViewportInteractedRef.current); publishViewportTelemetry("initial-fit"); if (!disposed) setRenderState('ready'); handleG6WheelViewportControl = event => { @@ -1667,7 +1697,7 @@ export function HzTopologyG6Canvas({ // The mount lifecycle intentionally excludes g6Graph so hover/selection style redraws do not destroy and refit the operator viewport. // Graph data updates flow through the setData/draw effect below. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [cancelPendingInitialFitView, clearSharedHover, graphStructureKey, handleG6EdgeSelect, handleG6NodeFocus, handleG6NodeSelect, height, layout, markUserViewportInteraction, publishViewportTelemetry]); + }, [cancelPendingInitialFitView, clearSharedHover, graphStructureKey, handleG6EdgeSelect, handleG6NodeFocus, handleG6NodeSelect, height, initialFitStrategy, layout, markUserViewportInteraction, publishViewportTelemetry]); React.useEffect(() => { const runtimeGraph = graphRef.current; @@ -1681,7 +1711,11 @@ export function HzTopologyG6Canvas({ if (shouldFitAfterDataChange) { lastFitStructureKeyRef.current = graphStructureKey; lastDrawGraphKeyRef.current = g6RenderKey; - void fitAndCenterG6Viewport(runtimeGraph, { when: "overflow" }, false); + if (initialFitStrategy === "center-only") { + void centerOnlyG6Viewport(runtimeGraph, false); + } else { + void fitAndCenterG6Viewport(runtimeGraph, { when: "overflow" }, false); + } } else { await restoreG6ViewportSnapshot(runtimeGraph, snapshot); publishViewportTelemetry("redraw-restore"); @@ -1691,7 +1725,7 @@ export function HzTopologyG6Canvas({ }).catch(error => { console.warn('HzTopologyG6Canvas failed to update AntV G6 data.', error); }); - }, [g6Graph, g6RenderKey, graphStructureKey, publishViewportTelemetry, renderState]); + }, [g6Graph, g6RenderKey, graphStructureKey, initialFitStrategy, publishViewportTelemetry, renderState]); React.useEffect(() => { const runtimeGraph = graphRef.current; @@ -1854,6 +1888,8 @@ export function HzTopologyG6Canvas({ data-hz-topology-g6-auto-fit-max-zoom={HZ_TOPOLOGY_G6_AUTO_FIT_MAX_ZOOM} data-hz-topology-g6-auto-fit-growth="no-magnify-small-graphs" data-hz-topology-g6-auto-fit-zoom-range-owner="hertzbeat-ui-g6-auto-fit-zoom-range" + data-hz-topology-g6-initial-fit-strategy={initialFitStrategy} + data-hz-topology-g6-initial-fit-strategy-owner="hertzbeat-ui-g6-initial-fit-strategy" data-hz-topology-g6-operator-zoom-bounds={`${HZ_TOPOLOGY_G6_MIN_ZOOM}-${HZ_TOPOLOGY_G6_MAX_ZOOM}`} data-hz-topology-g6-operator-zoom-growth="bounded-readable-nodes" data-hz-topology-g6-fit-mode="overflow-only-center" --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
