mirror of https://github.com/apache/nifi.git
NIFI-13065: Adding initial bend points for self looping connections and connections that collide with existing connections (#8671)
* NIFI-13065: - Adding initial bend points for self looping connections and connections that collide with existing connections. - Merging two actions into one for opening the new connection dialog. * NIFI-13065: - Only considering self looping connections when automatically moving bends when the source component moves. * NIFI-13065: - Making collision check for more lenient. - Setting initial label index to 0. This closes #8671
This commit is contained in:
parent
fe5ef39ff2
commit
d9e48f8645
|
@ -20,9 +20,11 @@ import * as d3 from 'd3';
|
||||||
import { CanvasUtils } from '../canvas-utils.service';
|
import { CanvasUtils } from '../canvas-utils.service';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { CanvasState } from '../../state';
|
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 { ConnectionManager } from '../manager/connection-manager.service';
|
||||||
import { Position } from '../../state/shared';
|
import { Position } from '../../state/shared';
|
||||||
|
import { CreateConnectionRequest } from '../../state/flow';
|
||||||
|
import { NiFiCommon } from '../../../../service/nifi-common.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
@ -33,7 +35,8 @@ export class ConnectableBehavior {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private store: Store<CanvasState>,
|
private store: Store<CanvasState>,
|
||||||
private canvasUtils: CanvasUtils
|
private canvasUtils: CanvasUtils,
|
||||||
|
private nifiCommon: NiFiCommon
|
||||||
) {
|
) {
|
||||||
const self: ConnectableBehavior = this;
|
const self: ConnectableBehavior = this;
|
||||||
|
|
||||||
|
@ -222,26 +225,174 @@ export class ConnectableBehavior {
|
||||||
// create the connection
|
// create the connection
|
||||||
const destinationData = destination.datum();
|
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(
|
self.store.dispatch(
|
||||||
getDefaultsAndOpenNewConnectionDialog({
|
openNewConnectionDialog({
|
||||||
request: {
|
request
|
||||||
source: {
|
|
||||||
id: sourceData.id,
|
|
||||||
componentType: sourceData.type,
|
|
||||||
entity: sourceData
|
|
||||||
},
|
|
||||||
destination: {
|
|
||||||
id: destinationData.id,
|
|
||||||
componentType: destinationData.type,
|
|
||||||
entity: destinationData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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:
|
* Determines if we want to allow adding connections in the current state:
|
||||||
*
|
*
|
||||||
|
|
|
@ -176,7 +176,6 @@ export class DraggableBehavior {
|
||||||
* @param {selection} dragSelection The current drag selection
|
* @param {selection} dragSelection The current drag selection
|
||||||
*/
|
*/
|
||||||
private updateComponentsPosition(dragSelection: any): void {
|
private updateComponentsPosition(dragSelection: any): void {
|
||||||
const self: DraggableBehavior = this;
|
|
||||||
const componentUpdates: Map<string, UpdateComponentRequest> = new Map();
|
const componentUpdates: Map<string, UpdateComponentRequest> = new Map();
|
||||||
const connectionUpdates: Map<string, UpdateComponentRequest> = new Map();
|
const connectionUpdates: Map<string, UpdateComponentRequest> = new Map();
|
||||||
|
|
||||||
|
@ -192,8 +191,8 @@ export class DraggableBehavior {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedConnections: any = d3.selectAll('g.connection.selected');
|
const selectedConnections: d3.Selection<any, any, any, any> = d3.selectAll('g.connection.selected');
|
||||||
const selectedComponents: any = d3.selectAll('g.component.selected');
|
const selectedComponents: d3.Selection<any, any, any, any> = d3.selectAll('g.component.selected');
|
||||||
|
|
||||||
// ensure every component is writable
|
// ensure every component is writable
|
||||||
if (!this.canvasUtils.canModify(selectedConnections) || !this.canvasUtils.canModify(selectedComponents)) {
|
if (!this.canvasUtils.canModify(selectedConnections) || !this.canvasUtils.canModify(selectedComponents)) {
|
||||||
|
@ -207,29 +206,34 @@ export class DraggableBehavior {
|
||||||
}
|
}
|
||||||
|
|
||||||
// go through each selected connection
|
// go through each selected connection
|
||||||
selectedConnections.each(function (d: any) {
|
selectedConnections.each((d) => {
|
||||||
const connectionUpdate = self.updateConnectionPosition(d, delta);
|
const connectionUpdate = this.updateConnectionPosition(d, delta);
|
||||||
if (connectionUpdate !== null) {
|
if (connectionUpdate) {
|
||||||
connectionUpdates.set(d.id, connectionUpdate);
|
connectionUpdates.set(d.id, connectionUpdate);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// go through each selected component
|
// go through each selected component
|
||||||
selectedComponents.each(function (d: any) {
|
selectedComponents.each((d) => {
|
||||||
// consider any self looping connections
|
// consider any self looping connections
|
||||||
const componentConnections = self.canvasUtils.getComponentConnections(d.id);
|
const componentConnections = this.canvasUtils.getComponentConnections(d.id);
|
||||||
|
|
||||||
componentConnections.forEach((componentConnection) => {
|
componentConnections.forEach((connection) => {
|
||||||
if (!connectionUpdates.has(componentConnection.id)) {
|
if (!connectionUpdates.has(connection.id)) {
|
||||||
const connectionUpdate = self.updateConnectionPosition(componentConnection, delta);
|
const sourceId = this.canvasUtils.getConnectionSourceComponentId(connection);
|
||||||
if (connectionUpdate !== null) {
|
const destinationId = this.canvasUtils.getConnectionDestinationComponentId(connection);
|
||||||
connectionUpdates.set(componentConnection.id, connectionUpdate);
|
|
||||||
|
if (sourceId === destinationId) {
|
||||||
|
const connectionUpdate = this.updateConnectionPosition(connection, delta);
|
||||||
|
if (connectionUpdate) {
|
||||||
|
connectionUpdates.set(connection.id, connectionUpdate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// consider the component itself
|
// consider the component itself
|
||||||
componentUpdates.set(d.id, self.updateComponentPosition(d, delta));
|
componentUpdates.set(d.id, this.updateComponentPosition(d, delta));
|
||||||
});
|
});
|
||||||
|
|
||||||
// dispatch the position updates
|
// dispatch the position updates
|
||||||
|
@ -247,7 +251,7 @@ export class DraggableBehavior {
|
||||||
/**
|
/**
|
||||||
* Updates the parent group of all selected components.
|
* Updates the parent group of all selected components.
|
||||||
*
|
*
|
||||||
* @param {selection} the destination group
|
* @param group
|
||||||
*/
|
*/
|
||||||
private updateComponentsGroup(group: any): void {
|
private updateComponentsGroup(group: any): void {
|
||||||
// get the selection and deselect the components being moved
|
// get the selection and deselect the components being moved
|
||||||
|
|
|
@ -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_X_OFFSET: number = ConnectionManager.DIMENSIONS.width / 2 + 5;
|
||||||
public static readonly SELF_LOOP_Y_OFFSET: number = 25;
|
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 HEIGHT_FOR_BACKPRESSURE: number = 3;
|
||||||
|
|
||||||
private static readonly SNAP_ALIGNMENT_PIXELS: number = 8;
|
private static readonly SNAP_ALIGNMENT_PIXELS: number = 8;
|
||||||
|
|
|
@ -26,7 +26,6 @@ import {
|
||||||
CreateComponentRequest,
|
CreateComponentRequest,
|
||||||
CreateComponentResponse,
|
CreateComponentResponse,
|
||||||
CreateConnection,
|
CreateConnection,
|
||||||
CreateConnectionDialogRequest,
|
|
||||||
CreateConnectionRequest,
|
CreateConnectionRequest,
|
||||||
CreatePortRequest,
|
CreatePortRequest,
|
||||||
CreateProcessGroupDialogRequest,
|
CreateProcessGroupDialogRequest,
|
||||||
|
@ -305,14 +304,9 @@ export const createProcessor = createAction(
|
||||||
props<{ request: CreateProcessorRequest }>()
|
props<{ request: CreateProcessorRequest }>()
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getDefaultsAndOpenNewConnectionDialog = createAction(
|
|
||||||
`${CANVAS_PREFIX} Get Defaults And Open New Connection Dialog`,
|
|
||||||
props<{ request: CreateConnectionRequest }>()
|
|
||||||
);
|
|
||||||
|
|
||||||
export const openNewConnectionDialog = createAction(
|
export const openNewConnectionDialog = createAction(
|
||||||
`${CANVAS_PREFIX} Open New Connection Dialog`,
|
`${CANVAS_PREFIX} Open New Connection Dialog`,
|
||||||
props<{ request: CreateConnectionDialogRequest }>()
|
props<{ request: CreateConnectionRequest }>()
|
||||||
);
|
);
|
||||||
|
|
||||||
export const createConnection = createAction(
|
export const createConnection = createAction(
|
||||||
|
|
|
@ -41,6 +41,7 @@ import {
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
CopyComponentRequest,
|
CopyComponentRequest,
|
||||||
|
CreateConnectionDialogRequest,
|
||||||
CreateProcessGroupDialogRequest,
|
CreateProcessGroupDialogRequest,
|
||||||
DeleteComponentResponse,
|
DeleteComponentResponse,
|
||||||
GroupComponentsDialogRequest,
|
GroupComponentsDialogRequest,
|
||||||
|
@ -610,36 +611,32 @@ export class FlowEffects {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
getDefaultsAndOpenNewConnectionDialog$ = createEffect(() =>
|
openNewConnectionDialog$ = createEffect(
|
||||||
this.actions$.pipe(
|
() =>
|
||||||
ofType(FlowActions.getDefaultsAndOpenNewConnectionDialog),
|
this.actions$.pipe(
|
||||||
map((action) => action.request),
|
ofType(FlowActions.openNewConnectionDialog),
|
||||||
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
|
map((action) => action.request),
|
||||||
switchMap(([request, currentProcessGroupId]) =>
|
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
|
||||||
from(this.flowService.getProcessGroup(currentProcessGroupId)).pipe(
|
switchMap(([request, currentProcessGroupId]) =>
|
||||||
map((response) =>
|
from(this.flowService.getProcessGroup(currentProcessGroupId)).pipe(
|
||||||
FlowActions.openNewConnectionDialog({
|
map((response) => {
|
||||||
request: {
|
return {
|
||||||
request,
|
request,
|
||||||
defaults: {
|
defaults: {
|
||||||
flowfileExpiration: response.component.defaultFlowFileExpiration,
|
flowfileExpiration: response.component.defaultFlowFileExpiration,
|
||||||
objectThreshold: response.component.defaultBackPressureObjectThreshold,
|
objectThreshold: response.component.defaultBackPressureObjectThreshold,
|
||||||
dataSizeThreshold: response.component.defaultBackPressureDataSizeThreshold
|
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) => {
|
tap((request) => {
|
||||||
const dialogReference = this.dialog.open(CreateConnection, {
|
const dialogReference = this.dialog.open(CreateConnection, {
|
||||||
...LARGE_DIALOG,
|
...LARGE_DIALOG,
|
||||||
|
|
|
@ -101,6 +101,7 @@ export interface CreateComponentRequest {
|
||||||
export interface CreateConnectionRequest {
|
export interface CreateConnectionRequest {
|
||||||
source: SelectedComponent;
|
source: SelectedComponent;
|
||||||
destination: SelectedComponent;
|
destination: SelectedComponent;
|
||||||
|
bends?: Position[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadBalanceStrategies: SelectOption[] = [
|
export const loadBalanceStrategies: SelectOption[] = [
|
||||||
|
|
|
@ -241,6 +241,7 @@ export class CreateConnection {
|
||||||
flowFileExpiration: this.createConnectionForm.get('flowFileExpiration')?.value,
|
flowFileExpiration: this.createConnectionForm.get('flowFileExpiration')?.value,
|
||||||
loadBalanceStrategy: this.createConnectionForm.get('loadBalanceStrategy')?.value,
|
loadBalanceStrategy: this.createConnectionForm.get('loadBalanceStrategy')?.value,
|
||||||
name: this.createConnectionForm.get('name')?.value,
|
name: this.createConnectionForm.get('name')?.value,
|
||||||
|
labelIndex: 0,
|
||||||
prioritizers: this.createConnectionForm.get('prioritizers')?.value
|
prioritizers: this.createConnectionForm.get('prioritizers')?.value
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -296,6 +297,10 @@ export class CreateConnection {
|
||||||
payload.component.loadBalanceCompression = 'DO_NOT_COMPRESS';
|
payload.component.loadBalanceCompression = 'DO_NOT_COMPRESS';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.dialogRequest.request.bends) {
|
||||||
|
payload.component.bends = this.dialogRequest.request.bends;
|
||||||
|
}
|
||||||
|
|
||||||
this.store.dispatch(
|
this.store.dispatch(
|
||||||
createConnection({
|
createConnection({
|
||||||
request: {
|
request: {
|
||||||
|
|
Loading…
Reference in New Issue