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

rfellows pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 13c70c0f30 NIFI-12737: Removing all transitions when updating the 
Canvas transform (#8355)
13c70c0f30 is described below

commit 13c70c0f30a87b87d58ba85843df364a61e22385
Author: Matt Gilman <matt.c.gil...@gmail.com>
AuthorDate: Tue Feb 6 15:04:35 2024 -0500

    NIFI-12737: Removing all transitions when updating the Canvas transform 
(#8355)
    
    * NIFI-12737:
    - Recording last canvas URL in local storage.
    - Using last canvas URL in global menu Canvas item.
    - Conditionally applying transitions when centering components.
    - Always applying transitions during zoom events (1:1, fit, zoom in/out).
    - Adding support to center more than one component.
    
    * NIFI-12737:
    - Fixing bug when attempting to click on the search result of the currently 
selected component.
    - Handling centering of a single selection different from a bulk selection 
as it performs betters with Connections.
    
    This closes #8355
---
 .../service/canvas-context-menu.service.ts         |   6 +-
 .../flow-designer/service/canvas-utils.service.ts  |  23 +++
 .../flow-designer/service/canvas-view.service.ts   | 214 +++++++++++++++------
 .../service/manager/connection-manager.service.ts  |  10 +-
 .../pages/flow-designer/state/flow/flow.actions.ts |  24 ++-
 .../pages/flow-designer/state/flow/flow.effects.ts |  25 ++-
 .../pages/flow-designer/state/flow/flow.reducer.ts |  16 +-
 .../flow-designer/state/flow/flow.selectors.ts     |   4 +-
 .../app/pages/flow-designer/state/flow/index.ts    |   5 +
 .../flow-designer/ui/canvas/canvas.component.ts    |  33 +++-
 .../flow-status/flow-status.component.spec.ts      |  18 +-
 .../ui/canvas/header/search/search.component.html  |  15 +-
 .../canvas/header/search/search.component.spec.ts  |   7 +
 .../ui/canvas/header/search/search.component.ts    |  29 ++-
 .../ui/common/navigation/navigation.component.html |   2 +-
 .../ui/common/navigation/navigation.component.ts   |   7 +
 16 files changed, 340 insertions(+), 98 deletions(-)

diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
index 5d083166e1..c819095656 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
@@ -20,7 +20,7 @@ import { CanvasUtils } from './canvas-utils.service';
 import { Store } from '@ngrx/store';
 import { CanvasState } from '../state';
 import {
-    centerSelectedComponent,
+    centerSelectedComponents,
     deleteComponents,
     enterProcessGroup,
     getParameterContextsAndOpenGroupComponentsDialog,
@@ -928,12 +928,12 @@ export class CanvasContextMenu implements 
ContextMenuDefinitionProvider {
             },
             {
                 condition: (selection: any) => {
-                    return selection.size() === 1 && 
!this.canvasUtils.isConnection(selection);
+                    return !selection.empty();
                 },
                 clazz: 'fa fa-crosshairs',
                 text: 'Center in view',
                 action: () => {
-                    this.store.dispatch(centerSelectedComponent());
+                    this.store.dispatch(centerSelectedComponents({ request: { 
allowTransition: true } }));
                 }
             },
             {
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
index fa43903e36..4f3c284e82 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
@@ -590,6 +590,28 @@ export class CanvasUtils {
         });
     }
 
+    /**
+     * Returns the position for centering a connection based on the presence 
of bends.
+     *
+     * @param d connection data
+     */
+    public getPositionForCenteringConnection(d: any): Position {
+        let x, y;
+        if (d.bends.length > 0) {
+            const i: number = Math.min(Math.max(0, d.labelIndex), 
d.bends.length - 1);
+            x = d.bends[i].x;
+            y = d.bends[i].y;
+        } else {
+            x = (d.start.x + d.end.x) / 2;
+            y = (d.start.y + d.end.y) / 2;
+        }
+
+        return {
+            x,
+            y
+        };
+    }
+
     /**
      * Returns the component id of the source of this processor. If the 
connection is attached
      * to a port in a [sub|remote] group, the component id will be that of the 
group. Otherwise
@@ -1484,6 +1506,7 @@ export class CanvasUtils {
         });
         return canStopTransmitting;
     }
+
     /**
      * Determines whether the components in the specified selection can be 
operated.
      *
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.ts
index ed70a7191c..699656dd86 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.ts
@@ -51,6 +51,7 @@ export class CanvasView {
     private behavior: any;
 
     private birdseyeTranslateInProgress = false;
+    private allowTransition = false;
 
     constructor(
         private store: Store<CanvasState>,
@@ -85,6 +86,10 @@ export class CanvasView {
         this.svg = svg;
         this.canvas = canvas;
 
+        this.k = INITIAL_SCALE;
+        this.x = INITIAL_TRANSLATE.x;
+        this.y = INITIAL_TRANSLATE.y;
+
         this.labelManager.init();
         this.funnelManager.init();
         this.portManager.init(viewContainerRef);
@@ -118,7 +123,7 @@ export class CanvasView {
 
                 // refresh the canvas
                 refreshed = self.refresh({
-                    transition: self.shouldTransition(event.sourceEvent),
+                    transition: self.shouldTransition(),
                     refreshComponents: false,
                     refreshBirdseye: false
                 });
@@ -170,18 +175,73 @@ export class CanvasView {
         return this.birdseyeTranslateInProgress;
     }
 
-    // see if the scale has changed during this zoom event,
-    // we want to only transition when zooming in/out as running
-    // the transitions during pan events is undesirable
-    private shouldTransition(sourceEvent: any): boolean {
+    private shouldTransition(): boolean {
         if (this.birdseyeTranslateInProgress) {
             return false;
         }
 
-        if (sourceEvent) {
-            return sourceEvent.type === 'wheel' || sourceEvent.type === 
'mousewheel';
+        return this.allowTransition;
+    }
+
+    public isSelectedComponentOnScreen(): boolean {
+        const canvasContainer: any = 
document.getElementById('canvas-container');
+
+        if (canvasContainer == null) {
+            return false;
+        }
+
+        const selection: any = this.canvasUtils.getSelection();
+        if (selection.size() !== 1) {
+            return false;
+        }
+        const d = selection.datum();
+
+        let translate = [this.x, this.y];
+        const scale = this.k;
+
+        // scale the translation
+        translate = [translate[0] / scale, translate[1] / scale];
+
+        // get the normalized screen width and height
+        const screenWidth = canvasContainer.offsetWidth / scale;
+        const screenHeight = canvasContainer.offsetHeight / scale;
+
+        // calculate the screen bounds one screens worth in each direction
+        const screenLeft = -translate[0];
+        const screenTop = -translate[1];
+        const screenRight = screenLeft + screenWidth;
+        const screenBottom = screenTop + screenHeight;
+
+        if (this.canvasUtils.isConnection(selection)) {
+            let connectionX, connectionY;
+            if (d.bends.length > 0) {
+                const i: number = Math.min(Math.max(0, d.labelIndex), 
d.bends.length - 1);
+                connectionX = d.bends[i].x;
+                connectionY = d.bends[i].y;
+            } else {
+                connectionX = (d.start.x + d.end.x) / 2;
+                connectionY = (d.start.y + d.end.y) / 2;
+            }
+
+            return (
+                screenLeft < connectionX &&
+                screenRight > connectionX &&
+                screenTop < connectionY &&
+                screenBottom > connectionY
+            );
         } else {
-            return true;
+            const componentLeft: number = d.position.x;
+            const componentTop: number = d.position.y;
+            const componentRight: number = componentLeft + d.dimensions.width;
+            const componentBottom: number = componentTop + d.dimensions.height;
+
+            // determine if the component is now visible
+            return (
+                screenLeft < componentRight &&
+                screenRight > componentLeft &&
+                screenTop < componentBottom &&
+                screenBottom > componentTop
+            );
         }
     }
 
@@ -230,15 +290,7 @@ export class CanvasView {
                 return false;
             }
 
-            let x, y;
-            if (d.bends.length > 0) {
-                const i: number = Math.min(Math.max(0, d.labelIndex), 
d.bends.length - 1);
-                x = d.bends[i].x;
-                y = d.bends[i].y;
-            } else {
-                x = (d.start.x + d.end.x) / 2;
-                y = (d.start.y + d.end.y) / 2;
-            }
+            const { x, y } = 
self.canvasUtils.getPositionForCenteringConnection(d);
 
             return screenLeft < x && screenRight > x && screenTop < y && 
screenBottom > y;
         };
@@ -293,7 +345,7 @@ export class CanvasView {
     }
 
     /**
-     * Whether or not a component should be rendered based solely on the 
current scale.
+     * Whether a component should be rendered based solely on the current 
scale.
      *
      * @returns {Boolean}
      */
@@ -301,45 +353,93 @@ export class CanvasView {
         return this.k >= CanvasView.MIN_SCALE_TO_RENDER;
     }
 
-    public centerSelectedComponent(): void {
+    public centerSelectedComponents(allowTransition: boolean): void {
+        const canvasContainer: any = 
document.getElementById('canvas-container');
+        if (canvasContainer == null) {
+            return;
+        }
+
         const selection: any = this.canvasUtils.getSelection();
+        if (selection.empty()) {
+            return;
+        }
+
+        let bbox;
         if (selection.size() === 1) {
-            let box;
-            if (this.canvasUtils.isConnection(selection)) {
-                let x, y;
-                const d = selection.datum();
-
-                // get the position of the connection label
-                if (d.bends.length > 0) {
-                    const i: number = Math.min(Math.max(0, d.labelIndex), 
d.bends.length - 1);
-                    x = d.bends[i].x;
-                    y = d.bends[i].y;
-                } else {
-                    x = (d.start.x + d.end.x) / 2;
-                    y = (d.start.y + d.end.y) / 2;
-                }
+            bbox = this.getSingleSelectionBoundingClientRect(selection);
+        } else {
+            bbox = this.getBulkSelectionBoundingClientRect(selection, 
canvasContainer);
+        }
 
-                box = {
-                    x: x,
-                    y: y,
-                    width: 1,
-                    height: 1
-                };
-            } else {
-                const selectionData = selection.datum();
-                const selectionPosition = selectionData.position;
-
-                box = {
-                    x: selectionPosition.x,
-                    y: selectionPosition.y,
-                    width: selectionData.dimensions.width,
-                    height: selectionData.dimensions.height
-                };
-            }
+        this.allowTransition = allowTransition;
+        this.centerBoundingBox(bbox);
+        this.allowTransition = false;
+    }
+
+    private getSingleSelectionBoundingClientRect(selection: any): any {
+        let bbox;
+        if (this.canvasUtils.isConnection(selection)) {
+            const d = selection.datum();
+
+            // get the position of the connection label
+            const { x, y } = 
this.canvasUtils.getPositionForCenteringConnection(d);
 
-            // center on the component
-            this.centerBoundingBox(box);
+            bbox = {
+                x: x,
+                y: y,
+                width: 1,
+                height: 1
+            };
+        } else {
+            const selectionData = selection.datum();
+            const selectionPosition = selectionData.position;
+
+            bbox = {
+                x: selectionPosition.x,
+                y: selectionPosition.y,
+                width: selectionData.dimensions.width,
+                height: selectionData.dimensions.height
+            };
         }
+
+        return bbox;
+    }
+
+    /**
+     * Get a BoundingClientRect, normalized to the canvas, that encompasses 
all nodes in a given selection.
+     */
+    private getBulkSelectionBoundingClientRect(selection: any, 
canvasContainer: any): any {
+        const canvasBoundingBox: any = canvasContainer.getBoundingClientRect();
+
+        const initialBBox: any = {
+            x: Number.MAX_VALUE,
+            y: Number.MAX_VALUE,
+            right: Number.MIN_VALUE,
+            bottom: Number.MIN_VALUE
+        };
+
+        const bbox = selection.nodes().reduce((aggregateBBox: any, node: any) 
=> {
+            const rect = node.getBoundingClientRect();
+            aggregateBBox.x = Math.min(rect.x, aggregateBBox.x);
+            aggregateBBox.y = Math.min(rect.y, aggregateBBox.y);
+            aggregateBBox.right = Math.max(rect.right, aggregateBBox.right);
+            aggregateBBox.bottom = Math.max(rect.bottom, aggregateBBox.bottom);
+
+            return aggregateBBox;
+        }, initialBBox);
+
+        // normalize the bounding box with scale and translate
+        bbox.x = (bbox.x - this.x) / this.k;
+        bbox.y = (bbox.y - canvasBoundingBox.top - this.y) / this.k;
+        bbox.right = (bbox.right - this.x) / this.k;
+        bbox.bottom = (bbox.bottom - canvasBoundingBox.top - this.y) / this.k;
+
+        bbox.width = bbox.right - bbox.x;
+        bbox.height = bbox.bottom - bbox.y;
+        bbox.top = bbox.y;
+        bbox.left = bbox.x;
+
+        return bbox;
     }
 
     private centerBoundingBox(boundingBox: any): void {
@@ -430,14 +530,18 @@ export class CanvasView {
      * Zooms in a single zoom increment.
      */
     public zoomIn(): void {
+        this.allowTransition = true;
         this.scale(CanvasView.INCREMENT);
+        this.allowTransition = false;
     }
 
     /**
      * Zooms out a single zoom increment.
      */
     public zoomOut(): void {
+        this.allowTransition = true;
         this.scale(1 / CanvasView.INCREMENT);
+        this.allowTransition = false;
     }
 
     /**
@@ -476,7 +580,7 @@ export class CanvasView {
             graphTop -= 50;
         }
 
-        // center as appropriate
+        this.allowTransition = true;
         this.centerBoundingBox({
             x: graphLeft - translate[0] / scale,
             y: graphTop - translate[1] / scale,
@@ -484,6 +588,7 @@ export class CanvasView {
             height: canvasHeight / newScale,
             scale: newScale
         });
+        this.allowTransition = false;
     }
 
     /**
@@ -530,8 +635,9 @@ export class CanvasView {
             };
         }
 
-        // center as appropriate
+        this.allowTransition = true;
         this.centerBoundingBox(box);
+        this.allowTransition = false;
     }
 
     /**
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.ts
index b2e23f111b..1fbc0d7f03 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.ts
@@ -109,15 +109,7 @@ export class ConnectionManager {
     private getLabelPosition(connectionLabel: any): Position {
         const d = connectionLabel.datum();
 
-        let x, y;
-        if (d.bends.length > 0) {
-            const i: number = Math.min(Math.max(0, d.labelIndex), 
d.bends.length - 1);
-            x = d.bends[i].x;
-            y = d.bends[i].y;
-        } else {
-            x = (d.start.x + d.end.x) / 2;
-            y = (d.start.y + d.end.y) / 2;
-        }
+        let { x, y } = this.canvasUtils.getPositionForCenteringConnection(d);
 
         // offset to account for the label dimensions
         x -= ConnectionManager.DIMENSIONS.width / 2;
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
index 2135726913..53f7f10f2d 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
@@ -70,7 +70,8 @@ import {
     UploadProcessGroupRequest,
     NavigateToQueueListing,
     StartProcessGroupResponse,
-    StopProcessGroupResponse
+    StopProcessGroupResponse,
+    CenterComponentRequest
 } from './index';
 import { StatusHistoryRequest } from '../../../../state/status-history';
 
@@ -186,7 +187,10 @@ export const removeSelectedComponents = createAction(
     props<{ request: SelectComponentsRequest }>()
 );
 
-export const centerSelectedComponent = createAction(`${CANVAS_PREFIX} Center 
Selected Component`);
+export const centerSelectedComponents = createAction(
+    `${CANVAS_PREFIX} Center Selected Components`,
+    props<{ request: CenterComponentRequest }>()
+);
 
 /*
     Create Component Actions
@@ -425,11 +429,27 @@ export const setTransitionRequired = createAction(
     props<{ transitionRequired: boolean }>()
 );
 
+/**
+ * skipTransform is used when handling URL events for loading the current PG 
and component [bulk] selection. since the
+ * URL is the source of truth we need to indicate skipTransform when the URL 
changes based on the user selection on
+ * the canvas. However, we do not want the transform skipped when using link 
to open or a particular part of the flow.
+ * In these cases, we want the transform to be applied so the viewport is 
restored or the component(s) is centered.
+ */
 export const setSkipTransform = createAction(
     `${CANVAS_PREFIX} Set Skip Transform`,
     props<{ skipTransform: boolean }>()
 );
 
+/**
+ * allowTransition is a flag that can be set that indicates if a transition 
should be used when applying a transform.
+ * By default, restoring the viewport or selecting/centering components will 
not use a transition unless explicitly
+ * specified. Zoom based transforms (like fit or 1:1) will always use a 
transition.
+ */
+export const setAllowTransition = createAction(
+    `${CANVAS_PREFIX} Set Allow Transition`,
+    props<{ allowTransition: boolean }>()
+);
+
 export const navigateToComponent = createAction(
     `${CANVAS_PREFIX} Navigate To Component`,
     props<{ request: NavigateToComponentRequest }>()
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
index 0fbd0a6289..05ca83d1f3 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
@@ -636,7 +636,12 @@ export class FlowEffects {
                 map((action) => action.request),
                 concatLatestFrom(() => 
this.store.select(selectCurrentProcessGroupId)),
                 tap(([request, processGroupId]) => {
-                    this.router.navigate(['/process-groups', processGroupId, 
request.type, request.id, 'edit']);
+                    const url = ['/process-groups', processGroupId, 
request.type, request.id, 'edit'];
+                    if (this.canvasView.isSelectedComponentOnScreen()) {
+                        
this.store.dispatch(FlowActions.navigateWithoutTransform({ url }));
+                    } else {
+                        this.router.navigate(url);
+                    }
                 })
             ),
         { dispatch: false }
@@ -1771,15 +1776,15 @@ export class FlowEffects {
         { dispatch: false }
     );
 
-    centerSelectedComponent$ = createEffect(
-        () =>
-            this.actions$.pipe(
-                ofType(FlowActions.centerSelectedComponent),
-                tap(() => {
-                    this.canvasView.centerSelectedComponent();
-                })
-            ),
-        { dispatch: false }
+    centerSelectedComponents$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(FlowActions.centerSelectedComponents),
+            map((action) => action.request),
+            tap((request) => {
+                
this.canvasView.centerSelectedComponents(request.allowTransition);
+            }),
+            switchMap(() => of(FlowActions.setAllowTransition({ 
allowTransition: false })))
+        )
     );
 
     navigateToProvenanceForComponent$ = createEffect(
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
index 1b5a54d707..6daf2432d9 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
@@ -41,6 +41,7 @@ import {
     resetFlowState,
     runOnce,
     runOnceSuccess,
+    setAllowTransition,
     setDragging,
     setNavigationCollapsed,
     setOperationCollapsed,
@@ -137,6 +138,7 @@ export const initialState: FlowState = {
     saving: false,
     transitionRequired: false,
     skipTransform: false,
+    allowTransition: false,
     navigationCollapsed: false,
     operationCollapsed: false,
     error: null,
@@ -297,15 +299,19 @@ export const flowReducer = createReducer(
     }),
     on(setDragging, (state, { dragging }) => ({
         ...state,
-        dragging: dragging
+        dragging
     })),
     on(setTransitionRequired, (state, { transitionRequired }) => ({
         ...state,
-        transitionRequired: transitionRequired
+        transitionRequired
     })),
     on(setSkipTransform, (state, { skipTransform }) => ({
         ...state,
-        skipTransform: skipTransform
+        skipTransform
+    })),
+    on(setAllowTransition, (state, { allowTransition }) => ({
+        ...state,
+        allowTransition
     })),
     on(navigateWithoutTransform, (state) => ({
         ...state,
@@ -313,11 +319,11 @@ export const flowReducer = createReducer(
     })),
     on(setNavigationCollapsed, (state, { navigationCollapsed }) => ({
         ...state,
-        navigationCollapsed: navigationCollapsed
+        navigationCollapsed
     })),
     on(setOperationCollapsed, (state, { operationCollapsed }) => ({
         ...state,
-        operationCollapsed: operationCollapsed
+        operationCollapsed
     })),
     on(startComponentSuccess, stopComponentSuccess, (state, { response }) => {
         return produce(state, (draftState) => {
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
index 5c2c3d1b25..e940b480ff 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
@@ -80,7 +80,7 @@ export const selectAnySelectedComponentIds = 
createSelector(selectCurrentRoute,
 
 export const selectBulkSelectedComponentIds = 
createSelector(selectCurrentRoute, (route) => {
     const ids: string[] = [];
-    // only handle either bulk component route
+    // only handle bulk component route
     if (route?.params.ids) {
         ids.push(...route.params.ids.split(','));
     }
@@ -140,6 +140,8 @@ export const selectDragging = 
createSelector(selectFlowState, (state: FlowState)
 
 export const selectSkipTransform = createSelector(selectFlowState, (state: 
FlowState) => state.skipTransform);
 
+export const selectAllowTransition = createSelector(selectFlowState, (state: 
FlowState) => state.allowTransition);
+
 export const selectFunnels = createSelector(
     selectFlowState,
     (state: FlowState) => state.flow.processGroupFlow?.flow.funnels
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
index c19437aa9c..1352c36e2b 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
@@ -40,6 +40,10 @@ export interface SelectComponentsRequest {
     components: SelectedComponent[];
 }
 
+export interface CenterComponentRequest {
+    allowTransition: boolean;
+}
+
 /*
   Load Process Group
  */
@@ -473,6 +477,7 @@ export interface FlowState {
     dragging: boolean;
     transitionRequired: boolean;
     skipTransform: boolean;
+    allowTransition: boolean;
     saving: boolean;
     navigationCollapsed: boolean;
     operationCollapsed: boolean;
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts
index 62a1779df8..cb10ccb727 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts
@@ -20,7 +20,7 @@ import { CanvasState } from '../../state';
 import { Position } from '../../state/shared';
 import { Store } from '@ngrx/store';
 import {
-    centerSelectedComponent,
+    centerSelectedComponents,
     deselectAllComponents,
     editComponent,
     editCurrentProcessGroup,
@@ -38,6 +38,7 @@ import { selectTransform } from 
'../../state/transform/transform.selectors';
 import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
 import { SelectedComponent } from '../../state/flow';
 import {
+    selectAllowTransition,
     selectBulkSelectedComponentIds,
     selectConnection,
     selectCurrentProcessGroupId,
@@ -57,13 +58,15 @@ import {
     selectViewStatusHistoryComponent
 } from '../../state/flow/flow.selectors';
 import { filter, map, switchMap, take } from 'rxjs';
-import { restoreViewport, zoomFit } from 
'../../state/transform/transform.actions';
+import { restoreViewport } from '../../state/transform/transform.actions';
 import { ComponentType, isDefinedAndNotNull } from '../../../../state/shared';
 import { initialState } from '../../state/flow/flow.reducer';
 import { CanvasContextMenu } from '../../service/canvas-context-menu.service';
 import { getStatusHistoryAndOpenDialog } from 
'../../../../state/status-history/status-history.actions';
 import { loadFlowConfiguration } from 
'../../../../state/flow-configuration/flow-configuration.actions';
 import { concatLatestFrom } from '@ngrx/effects';
+import { selectUrl } from '../../../../state/router/router.selectors';
+import { Storage } from '../../../../service/storage.service';
 
 @Component({
     selector: 'fd-canvas',
@@ -81,6 +84,7 @@ export class Canvas implements OnInit, OnDestroy {
         private viewContainerRef: ViewContainerRef,
         private store: Store<CanvasState>,
         private canvasView: CanvasView,
+        private storage: Storage,
         public canvasContextMenu: CanvasContextMenu
     ) {
         this.store
@@ -90,6 +94,13 @@ export class Canvas implements OnInit, OnDestroy {
                 this.scale = transform.scale;
             });
 
+        this.store
+            .select(selectUrl)
+            .pipe(takeUntilDestroyed())
+            .subscribe((route) => {
+                this.storage.setItem('current-canvas-route', route);
+            });
+
         // load the process group from the route
         this.store
             .select(selectProcessGroupIdFromRoute)
@@ -133,14 +144,17 @@ export class Canvas implements OnInit, OnDestroy {
                 filter((processGroupId) => processGroupId != initialState.id),
                 switchMap(() => 
this.store.select(selectSingleSelectedComponent)),
                 filter((selectedComponent) => selectedComponent != null),
-                concatLatestFrom(() => this.store.select(selectSkipTransform)),
+                concatLatestFrom(() => [
+                    this.store.select(selectSkipTransform),
+                    this.store.select(selectAllowTransition)
+                ]),
                 takeUntilDestroyed()
             )
-            .subscribe(([, skipTransform]) => {
+            .subscribe(([, skipTransform, allowTransition]) => {
                 if (skipTransform) {
                     this.store.dispatch(setSkipTransform({ skipTransform: 
false }));
                 } else {
-                    this.store.dispatch(centerSelectedComponent());
+                    this.store.dispatch(centerSelectedComponents({ request: { 
allowTransition } }));
                 }
             });
 
@@ -151,14 +165,17 @@ export class Canvas implements OnInit, OnDestroy {
                 filter((processGroupId) => processGroupId != initialState.id),
                 switchMap(() => 
this.store.select(selectBulkSelectedComponentIds)),
                 filter((ids) => ids.length > 0),
-                concatLatestFrom(() => this.store.select(selectSkipTransform)),
+                concatLatestFrom(() => [
+                    this.store.select(selectSkipTransform),
+                    this.store.select(selectAllowTransition)
+                ]),
                 takeUntilDestroyed()
             )
-            .subscribe(([, skipTransform]) => {
+            .subscribe(([, skipTransform, allowTransition]) => {
                 if (skipTransform) {
                     this.store.dispatch(setSkipTransform({ skipTransform: 
false }));
                 } else {
-                    this.store.dispatch(zoomFit());
+                    this.store.dispatch(centerSelectedComponents({ request: { 
allowTransition } }));
                 }
             });
 
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.spec.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.spec.ts
index 06ad1f4a40..30b37b5898 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.spec.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.spec.ts
@@ -18,27 +18,41 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 
 import { FlowStatus } from './flow-status.component';
-import { Search } from '../search/search.component';
 import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
 import { MatAutocompleteModule } from '@angular/material/autocomplete';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { Component } from '@angular/core';
+import { provideMockStore } from '@ngrx/store/testing';
+import { initialState } from '../../../../state/flow/flow.reducer';
 
 describe('FlowStatus', () => {
     let component: FlowStatus;
     let fixture: ComponentFixture<FlowStatus>;
 
+    @Component({
+        selector: 'search',
+        standalone: true,
+        template: ''
+    })
+    class MockSearch {}
+
     beforeEach(() => {
         TestBed.configureTestingModule({
             imports: [
                 FlowStatus,
-                Search,
+                MockSearch,
                 HttpClientTestingModule,
                 CdkOverlayOrigin,
                 CdkConnectedOverlay,
                 MatAutocompleteModule,
                 FormsModule,
                 ReactiveFormsModule
+            ],
+            providers: [
+                provideMockStore({
+                    initialState
+                })
             ]
         });
         fixture = TestBed.createComponent(FlowStatus);
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/search/search.component.html
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/search/search.component.html
index fc124d06a5..62257addd4 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/search/search.component.html
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/search/search.component.html
@@ -186,10 +186,21 @@
             </li>
             <!-- TODO - Consider showing more context of match like existing 
UI -->
             <li *ngFor="let result of results" class="ml-2 py-1">
-                <a *ngIf="!result.parentGroup; else resultLink" 
[routerLink]="['/process-groups', result.id]">
+                <a *ngIf="!result.parentGroup; else componentLink" 
[routerLink]="['/process-groups', result.id]">
                     {{ result.name }}
                 </a>
-                <ng-template #resultLink>
+                <ng-template #componentLink>
+                    <a
+                        *ngIf="
+                            result.parentGroup.id == currentProcessGroupId;
+                            else componentInDifferentProcessGroupLink
+                        "
+                        (click)="componentLinkClicked(path, result.id)"
+                        [routerLink]="['/process-groups', 
result.parentGroup.id, path, result.id]">
+                        {{ result.name ? result.name : result.id }}
+                    </a>
+                </ng-template>
+                <ng-template #componentInDifferentProcessGroupLink>
                     <a [routerLink]="['/process-groups', 
result.parentGroup.id, path, result.id]">
                         {{ result.name ? result.name : result.id }}
                     </a>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/search/search.component.spec.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/search/search.component.spec.ts
index 6cabc8a946..e53daf63b7 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/search/search.component.spec.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/search/search.component.spec.ts
@@ -22,6 +22,8 @@ import { HttpClientTestingModule } from 
'@angular/common/http/testing';
 import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { MatAutocompleteModule } from '@angular/material/autocomplete';
+import { provideMockStore } from '@ngrx/store/testing';
+import { initialState } from '../../../../state/flow/flow.reducer';
 
 describe('Search', () => {
     let component: Search;
@@ -37,6 +39,11 @@ describe('Search', () => {
                 ReactiveFormsModule,
                 CdkConnectedOverlay,
                 MatAutocompleteModule
+            ],
+            providers: [
+                provideMockStore({
+                    initialState
+                })
             ]
         });
         fixture = TestBed.createComponent(Search);
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/search/search.component.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/search/search.component.ts
index 4e7b2eef5f..402ed7460d 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/search/search.component.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/search/search.component.ts
@@ -32,6 +32,11 @@ import { NgForOf, NgIf, NgTemplateOutlet } from 
'@angular/common';
 import { RouterLink } from '@angular/router';
 import { MatFormFieldModule } from '@angular/material/form-field';
 import { MatInputModule } from '@angular/material/input';
+import { CanvasState } from '../../../../state';
+import { Store } from '@ngrx/store';
+import { centerSelectedComponents, setAllowTransition } from 
'../../../../state/flow/flow.actions';
+import { selectCurrentRoute } from 
'../../../../../../state/router/router.selectors';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
 
 @Component({
     selector: 'search',
@@ -86,11 +91,25 @@ export class Search implements OnInit {
     parameterProviderNodeResults: ComponentSearchResult[] = [];
     parameterResults: ComponentSearchResult[] = [];
 
+    selectedComponentType: ComponentType | null = null;
+    selectedComponentId: string | null = null;
+
     constructor(
         private formBuilder: FormBuilder,
-        private searchService: SearchService
+        private searchService: SearchService,
+        private store: Store<CanvasState>
     ) {
         this.searchForm = this.formBuilder.group({ searchBar: '' });
+
+        this.store
+            .select(selectCurrentRoute)
+            .pipe(takeUntilDestroyed())
+            .subscribe((route) => {
+                if (route?.params) {
+                    this.selectedComponentId = route.params.id;
+                    this.selectedComponentType = route.params.type;
+                }
+            });
     }
 
     ngOnInit(): void {
@@ -169,4 +188,12 @@ export class Search implements OnInit {
         this.parameterProviderNodeResults = [];
         this.parameterResults = [];
     }
+
+    componentLinkClicked(componentType: ComponentType, id: string): void {
+        if (componentType == this.selectedComponentType && id == 
this.selectedComponentId) {
+            this.store.dispatch(centerSelectedComponents({ request: { 
allowTransition: true } }));
+        } else {
+            this.store.dispatch(setAllowTransition({ allowTransition: true }));
+        }
+    }
 }
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.html
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.html
index 16450e9bf7..0684c760a9 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.html
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.html
@@ -43,7 +43,7 @@
                 <i class="fa fa-navicon"></i>
             </button>
             <mat-menu #globalMenu="matMenu" xPosition="before">
-                <button mat-menu-item class="global-menu-item" 
[routerLink]="['/']">
+                <button mat-menu-item class="global-menu-item" 
[routerLink]="getCanvasLink()">
                     <i class="icon icon-drop mr-2"></i>
                     Canvas
                 </button>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.ts
index 0f6bbd0a54..617139733b 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.ts
@@ -32,6 +32,7 @@ import { MatButtonModule } from '@angular/material/button';
 import { NiFiState } from '../../../state';
 import { selectFlowConfiguration } from 
'../../../state/flow-configuration/flow-configuration.selectors';
 import { MatSlideToggleModule } from '@angular/material/slide-toggle';
+import { Storage } from '../../../service/storage.service';
 
 @Component({
     selector: 'navigation',
@@ -59,6 +60,7 @@ export class Navigation {
         private store: Store<NiFiState>,
         private authStorage: AuthStorage,
         private authService: AuthService,
+        private storage: Storage,
         @Inject(DOCUMENT) private _document: Document
     ) {}
 
@@ -94,6 +96,11 @@ export class Navigation {
         );
     }
 
+    getCanvasLink(): string {
+        const canvasRoute = this.storage.getItem('current-canvas-route');
+        return canvasRoute || '/';
+    }
+
     toggleTheme(value = !this.isDarkMode) {
         this.isDarkMode = value;
         this._document.body.classList.toggle('dark-theme', value);


Reply via email to