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/behavior/connectable-behavior.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/behavior/connectable-behavior.service.ts index b553a1ad7b..31a6f1d76a 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.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/behavior/connectable-behavior.service.ts @@ -20,9 +20,11 @@ import * as d3 from 'd3'; import { CanvasUtils } from '../canvas-utils.service'; import { Store } from '@ngrx/store'; import { CanvasState } from '../../state'; -import { getDefaultsAndOpenNewConnectionDialog, selectComponents } from '../../state/flow/flow.actions'; +import { openNewConnectionDialog, selectComponents } from '../../state/flow/flow.actions'; import { ConnectionManager } from '../manager/connection-manager.service'; import { Position } from '../../state/shared'; +import { CreateConnectionRequest } from '../../state/flow'; +import { NiFiCommon } from '../../../../service/nifi-common.service'; @Injectable({ providedIn: 'root' @@ -33,7 +35,8 @@ export class ConnectableBehavior { constructor( private store: Store, - private canvasUtils: CanvasUtils + private canvasUtils: CanvasUtils, + private nifiCommon: NiFiCommon ) { const self: ConnectableBehavior = this; @@ -222,26 +225,174 @@ export class ConnectableBehavior { // create the connection const destinationData = destination.datum(); + // prepare the connection request + const request: CreateConnectionRequest = { + source: { + id: sourceData.id, + componentType: sourceData.type, + entity: sourceData + }, + destination: { + id: destinationData.id, + componentType: destinationData.type, + entity: destinationData + } + }; + + // add initial bend points if necessary + const bends: Position[] = self.calculateInitialBendPoints(sourceData, destinationData); + if (bends) { + request.bends = bends; + } + self.store.dispatch( - getDefaultsAndOpenNewConnectionDialog({ - request: { - source: { - id: sourceData.id, - componentType: sourceData.type, - entity: sourceData - }, - destination: { - id: destinationData.id, - componentType: destinationData.type, - entity: destinationData - } - } + openNewConnectionDialog({ + request }) ); } }); } + /** + * Calculate bend points for a new Connection if necessary. + * + * @param sourceData + * @param destinationData + */ + private calculateInitialBendPoints(sourceData: any, destinationData: any): Position[] { + const bends: Position[] = []; + + if (sourceData.id == destinationData.id) { + const rightCenter: Position = { + x: sourceData.position.x + sourceData.dimensions.width, + y: sourceData.position.y + sourceData.dimensions.height / 2 + }; + + const xOffset = ConnectionManager.SELF_LOOP_X_OFFSET; + const yOffset = ConnectionManager.SELF_LOOP_Y_OFFSET; + bends.push({ + x: rightCenter.x + xOffset, + y: rightCenter.y - yOffset + }); + bends.push({ + x: rightCenter.x + xOffset, + y: rightCenter.y + yOffset + }); + } else { + const existingConnections: any[] = []; + + // get all connections for the source component + const connectionsForSourceComponent: any[] = this.canvasUtils.getComponentConnections(sourceData.id); + connectionsForSourceComponent.forEach((connectionForSourceComponent) => { + // get the id for the source/destination component + const connectionSourceComponentId = + this.canvasUtils.getConnectionSourceComponentId(connectionForSourceComponent); + const connectionDestinationComponentId = + this.canvasUtils.getConnectionDestinationComponentId(connectionForSourceComponent); + + // if the connection is between these same components, consider it for collisions + if ( + (connectionSourceComponentId === sourceData.id && + connectionDestinationComponentId === destinationData.id) || + (connectionDestinationComponentId === sourceData.id && + connectionSourceComponentId === destinationData.id) + ) { + // record all connections between these two components in question + existingConnections.push(connectionForSourceComponent); + } + }); + + // if there are existing connections between these components, ensure the new connection won't collide + if (existingConnections) { + const avoidCollision = existingConnections.some((existingConnection) => { + // only consider multiple connections with no bend points a collision, the existence of + // bend points suggests that the user has placed the connection into a desired location + return this.nifiCommon.isEmpty(existingConnection.bends); + }); + + // if we need to avoid a collision + if (avoidCollision) { + // determine the middle of the source/destination components + const sourceMiddle: Position = { + x: sourceData.position.x + sourceData.dimensions.width / 2, + y: sourceData.position.y + sourceData.dimensions.height / 2 + }; + const destinationMiddle: Position = { + x: destinationData.position.x + destinationData.dimensions.width / 2, + y: destinationData.position.y + destinationData.dimensions.height / 2 + }; + + // detect if the line is more horizontal or vertical + const slope = (sourceMiddle.y - destinationMiddle.y) / (sourceMiddle.x - destinationMiddle.x); + const isMoreHorizontal = slope <= 1 && slope >= -1; + + // find the midpoint on the connection + const xCandidate = (sourceMiddle.x + destinationMiddle.x) / 2; + const yCandidate = (sourceMiddle.y + destinationMiddle.y) / 2; + + // attempt to position this connection so it doesn't collide + let xStep = isMoreHorizontal ? 0 : ConnectionManager.CONNECTION_OFFSET_X_INCREMENT; + let yStep = isMoreHorizontal ? ConnectionManager.CONNECTION_OFFSET_Y_INCREMENT : 0; + + let positioned = false; + while (!positioned) { + // consider above and below, then increment and try again (if necessary) + if (!this.collides(existingConnections, xCandidate - xStep, yCandidate - yStep)) { + bends.push({ + x: xCandidate - xStep, + y: yCandidate - yStep + }); + positioned = true; + } else if (!this.collides(existingConnections, xCandidate + xStep, yCandidate + yStep)) { + bends.push({ + x: xCandidate + xStep, + y: yCandidate + yStep + }); + positioned = true; + } + + if (isMoreHorizontal) { + yStep += ConnectionManager.CONNECTION_OFFSET_Y_INCREMENT; + } else { + xStep += ConnectionManager.CONNECTION_OFFSET_X_INCREMENT; + } + } + } + } + } + + return bends; + } + + /** + * Determines if the specified coordinate collides with another connection. + * + * @param existingConnections + * @param x + * @param y + */ + private collides(existingConnections: any[], x: number, y: number): boolean { + return existingConnections.some((existingConnection) => { + if (!this.nifiCommon.isEmpty(existingConnection.bends)) { + let labelIndex = existingConnection.labelIndex; + if (labelIndex >= existingConnection.bends.length) { + labelIndex = 0; + } + + // determine collision based on y space or x space depending on whether the connection is more horizontal + return ( + existingConnection.bends[labelIndex].y - 25 < y && + existingConnection.bends[labelIndex].y + 25 > y && + existingConnection.bends[labelIndex].x - 100 < x && + existingConnection.bends[labelIndex].x + 100 > x + ); + } + + return false; + }); + } + /** * Determines if we want to allow adding connections in the current state: * 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/behavior/draggable-behavior.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/behavior/draggable-behavior.service.ts index 8dc3082b45..c68f692c87 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.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/behavior/draggable-behavior.service.ts @@ -176,7 +176,6 @@ export class DraggableBehavior { * @param {selection} dragSelection The current drag selection */ private updateComponentsPosition(dragSelection: any): void { - const self: DraggableBehavior = this; const componentUpdates: Map = new Map(); const connectionUpdates: Map = new Map(); @@ -192,8 +191,8 @@ export class DraggableBehavior { return; } - const selectedConnections: any = d3.selectAll('g.connection.selected'); - const selectedComponents: any = d3.selectAll('g.component.selected'); + const selectedConnections: d3.Selection = d3.selectAll('g.connection.selected'); + const selectedComponents: d3.Selection = d3.selectAll('g.component.selected'); // ensure every component is writable if (!this.canvasUtils.canModify(selectedConnections) || !this.canvasUtils.canModify(selectedComponents)) { @@ -207,29 +206,34 @@ export class DraggableBehavior { } // go through each selected connection - selectedConnections.each(function (d: any) { - const connectionUpdate = self.updateConnectionPosition(d, delta); - if (connectionUpdate !== null) { + selectedConnections.each((d) => { + const connectionUpdate = this.updateConnectionPosition(d, delta); + if (connectionUpdate) { connectionUpdates.set(d.id, connectionUpdate); } }); // go through each selected component - selectedComponents.each(function (d: any) { + selectedComponents.each((d) => { // consider any self looping connections - const componentConnections = self.canvasUtils.getComponentConnections(d.id); + const componentConnections = this.canvasUtils.getComponentConnections(d.id); - componentConnections.forEach((componentConnection) => { - if (!connectionUpdates.has(componentConnection.id)) { - const connectionUpdate = self.updateConnectionPosition(componentConnection, delta); - if (connectionUpdate !== null) { - connectionUpdates.set(componentConnection.id, connectionUpdate); + componentConnections.forEach((connection) => { + if (!connectionUpdates.has(connection.id)) { + const sourceId = this.canvasUtils.getConnectionSourceComponentId(connection); + const destinationId = this.canvasUtils.getConnectionDestinationComponentId(connection); + + if (sourceId === destinationId) { + const connectionUpdate = this.updateConnectionPosition(connection, delta); + if (connectionUpdate) { + connectionUpdates.set(connection.id, connectionUpdate); + } } } }); // consider the component itself - componentUpdates.set(d.id, self.updateComponentPosition(d, delta)); + componentUpdates.set(d.id, this.updateComponentPosition(d, delta)); }); // dispatch the position updates @@ -247,7 +251,7 @@ export class DraggableBehavior { /** * Updates the parent group of all selected components. * - * @param {selection} the destination group + * @param group */ private updateComponentsGroup(group: any): void { // get the selection and deselect the components being moved 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 50e5f976e2..3cf2c574b5 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 @@ -72,6 +72,9 @@ export class ConnectionManager { public static readonly SELF_LOOP_X_OFFSET: number = ConnectionManager.DIMENSIONS.width / 2 + 5; public static readonly SELF_LOOP_Y_OFFSET: number = 25; + public static readonly CONNECTION_OFFSET_Y_INCREMENT: number = 75; + public static readonly CONNECTION_OFFSET_X_INCREMENT: number = 250; + private static readonly HEIGHT_FOR_BACKPRESSURE: number = 3; private static readonly SNAP_ALIGNMENT_PIXELS: number = 8; 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 6d11c676b4..67deb5e17b 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 @@ -26,7 +26,6 @@ import { CreateComponentRequest, CreateComponentResponse, CreateConnection, - CreateConnectionDialogRequest, CreateConnectionRequest, CreatePortRequest, CreateProcessGroupDialogRequest, @@ -305,14 +304,9 @@ export const createProcessor = createAction( props<{ request: CreateProcessorRequest }>() ); -export const getDefaultsAndOpenNewConnectionDialog = createAction( - `${CANVAS_PREFIX} Get Defaults And Open New Connection Dialog`, - props<{ request: CreateConnectionRequest }>() -); - export const openNewConnectionDialog = createAction( `${CANVAS_PREFIX} Open New Connection Dialog`, - props<{ request: CreateConnectionDialogRequest }>() + props<{ request: CreateConnectionRequest }>() ); export const createConnection = createAction( 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 98b2cbf110..0ca8744b6b 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 @@ -41,6 +41,7 @@ import { } from 'rxjs'; import { CopyComponentRequest, + CreateConnectionDialogRequest, CreateProcessGroupDialogRequest, DeleteComponentResponse, GroupComponentsDialogRequest, @@ -610,36 +611,32 @@ export class FlowEffects { ) ); - getDefaultsAndOpenNewConnectionDialog$ = createEffect(() => - this.actions$.pipe( - ofType(FlowActions.getDefaultsAndOpenNewConnectionDialog), - map((action) => action.request), - concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)), - switchMap(([request, currentProcessGroupId]) => - from(this.flowService.getProcessGroup(currentProcessGroupId)).pipe( - map((response) => - FlowActions.openNewConnectionDialog({ - request: { + openNewConnectionDialog$ = createEffect( + () => + this.actions$.pipe( + ofType(FlowActions.openNewConnectionDialog), + map((action) => action.request), + concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)), + switchMap(([request, currentProcessGroupId]) => + from(this.flowService.getProcessGroup(currentProcessGroupId)).pipe( + map((response) => { + return { request, defaults: { flowfileExpiration: response.component.defaultFlowFileExpiration, objectThreshold: response.component.defaultBackPressureObjectThreshold, dataSizeThreshold: response.component.defaultBackPressureDataSizeThreshold } + } as CreateConnectionDialogRequest; + }), + tap({ + error: (errorResponse: HttpErrorResponse) => { + this.canvasUtils.removeTempEdge(); + this.store.dispatch(FlowActions.flowSnackbarError({ error: errorResponse.error })); } }) - ), - catchError((error) => of(FlowActions.flowApiError({ error: error.error }))) - ) - ) - ) - ); - - openNewConnectionDialog$ = createEffect( - () => - this.actions$.pipe( - ofType(FlowActions.openNewConnectionDialog), - map((action) => action.request), + ) + ), tap((request) => { const dialogReference = this.dialog.open(CreateConnection, { ...LARGE_DIALOG, 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 440975350c..c0ce2dd56d 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 @@ -101,6 +101,7 @@ export interface CreateComponentRequest { export interface CreateConnectionRequest { source: SelectedComponent; destination: SelectedComponent; + bends?: Position[]; } export const loadBalanceStrategies: SelectOption[] = [ 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/items/connection/create-connection/create-connection.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/items/connection/create-connection/create-connection.component.ts index 669d8c5be8..034e77c091 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/items/connection/create-connection/create-connection.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/items/connection/create-connection/create-connection.component.ts @@ -241,6 +241,7 @@ export class CreateConnection { flowFileExpiration: this.createConnectionForm.get('flowFileExpiration')?.value, loadBalanceStrategy: this.createConnectionForm.get('loadBalanceStrategy')?.value, name: this.createConnectionForm.get('name')?.value, + labelIndex: 0, prioritizers: this.createConnectionForm.get('prioritizers')?.value } }; @@ -296,6 +297,10 @@ export class CreateConnection { payload.component.loadBalanceCompression = 'DO_NOT_COMPRESS'; } + if (this.dialogRequest.request.bends) { + payload.component.bends = this.dialogRequest.request.bends; + } + this.store.dispatch( createConnection({ request: {