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 { 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<CanvasState>,
|
||||
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:
|
||||
*
|
||||
|
|
|
@ -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<string, UpdateComponentRequest> = new Map();
|
||||
const connectionUpdates: Map<string, UpdateComponentRequest> = 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<any, any, any, any> = d3.selectAll('g.connection.selected');
|
||||
const selectedComponents: d3.Selection<any, any, any, any> = 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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -101,6 +101,7 @@ export interface CreateComponentRequest {
|
|||
export interface CreateConnectionRequest {
|
||||
source: SelectedComponent;
|
||||
destination: SelectedComponent;
|
||||
bends?: Position[];
|
||||
}
|
||||
|
||||
export const loadBalanceStrategies: SelectOption[] = [
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in New Issue