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);