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]

Reply via email to