mirror of https://github.com/apache/nifi.git
[NIFI-13162] horizontal and vertical canvas component alignment (#8762)
* [NIFI-13162] horizontal and vertical canvas component alignment * review feedback * rename to updatePositionRequestId * use enum * use the appropriate generic type and separate components and connections updates * use enum This closes #8762
This commit is contained in:
parent
fc8f072e0a
commit
6950290c24
|
@ -40,7 +40,7 @@ export class DraggableBehavior {
|
||||||
|
|
||||||
private scale: number = INITIAL_SCALE;
|
private scale: number = INITIAL_SCALE;
|
||||||
|
|
||||||
private updateConnectionRequestId = 0;
|
private updatePositionRequestId = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private store: Store<CanvasState>,
|
private store: Store<CanvasState>,
|
||||||
|
@ -240,7 +240,7 @@ export class DraggableBehavior {
|
||||||
this.store.dispatch(
|
this.store.dispatch(
|
||||||
updatePositions({
|
updatePositions({
|
||||||
request: {
|
request: {
|
||||||
requestId: this.updateConnectionRequestId++,
|
requestId: this.updatePositionRequestId++,
|
||||||
componentUpdates: Array.from(componentUpdates.values()),
|
componentUpdates: Array.from(componentUpdates.values()),
|
||||||
connectionUpdates: Array.from(connectionUpdates.values())
|
connectionUpdates: Array.from(connectionUpdates.values())
|
||||||
}
|
}
|
||||||
|
@ -298,7 +298,7 @@ export class DraggableBehavior {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateComponentPosition(d: any, delta: Position): UpdateComponentRequest {
|
updateComponentPosition(d: any, delta: Position): UpdateComponentRequest {
|
||||||
const newPosition = {
|
const newPosition = {
|
||||||
x: this.snapEnabled
|
x: this.snapEnabled
|
||||||
? Math.round((d.position.x + delta.x) / this.snapAlignmentPixels) * this.snapAlignmentPixels
|
? Math.round((d.position.x + delta.x) / this.snapAlignmentPixels) * this.snapAlignmentPixels
|
||||||
|
@ -334,7 +334,7 @@ export class DraggableBehavior {
|
||||||
* @param delta The change in position
|
* @param delta The change in position
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
private updateConnectionPosition(connection: any, delta: any): UpdateComponentRequest | null {
|
updateConnectionPosition(connection: any, delta: any): UpdateComponentRequest | null {
|
||||||
// only update if necessary
|
// only update if necessary
|
||||||
if (connection.bends.length === 0) {
|
if (connection.bends.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -47,14 +47,16 @@ import {
|
||||||
requestRefreshRemoteProcessGroup,
|
requestRefreshRemoteProcessGroup,
|
||||||
runOnce,
|
runOnce,
|
||||||
stopVersionControlRequest,
|
stopVersionControlRequest,
|
||||||
terminateThreads
|
terminateThreads,
|
||||||
|
updatePositions
|
||||||
} from '../state/flow/flow.actions';
|
} from '../state/flow/flow.actions';
|
||||||
import { ComponentType } from '../../../state/shared';
|
import { ComponentType } from '../../../state/shared';
|
||||||
import {
|
import {
|
||||||
ConfirmStopVersionControlRequest,
|
ConfirmStopVersionControlRequest,
|
||||||
MoveComponentRequest,
|
MoveComponentRequest,
|
||||||
OpenChangeVersionDialogRequest,
|
OpenChangeVersionDialogRequest,
|
||||||
OpenLocalChangesDialogRequest
|
OpenLocalChangesDialogRequest,
|
||||||
|
UpdateComponentRequest
|
||||||
} from '../state/flow';
|
} from '../state/flow';
|
||||||
import {
|
import {
|
||||||
ContextMenuDefinition,
|
ContextMenuDefinition,
|
||||||
|
@ -68,9 +70,13 @@ import * as d3 from 'd3';
|
||||||
import { Client } from '../../../service/client.service';
|
import { Client } from '../../../service/client.service';
|
||||||
import { CanvasView } from './canvas-view.service';
|
import { CanvasView } from './canvas-view.service';
|
||||||
import { CanvasActionsService } from './canvas-actions.service';
|
import { CanvasActionsService } from './canvas-actions.service';
|
||||||
|
import * as FlowActions from '../state/flow/flow.actions';
|
||||||
|
import { DraggableBehavior } from './behavior/draggable-behavior.service';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
||||||
|
private updatePositionRequestId = 0;
|
||||||
|
|
||||||
readonly VERSION_MENU = {
|
readonly VERSION_MENU = {
|
||||||
id: 'version',
|
id: 'version',
|
||||||
menuItems: [
|
menuItems: [
|
||||||
|
@ -301,24 +307,195 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
||||||
menuItems: [
|
menuItems: [
|
||||||
{
|
{
|
||||||
condition: (selection: any) => {
|
condition: (selection: any) => {
|
||||||
// TODO - canAlign
|
return this.canvasUtils.canAlign(selection);
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
clazz: 'fa fa-align-center fa-rotate-90',
|
clazz: 'fa fa-align-center fa-rotate-90',
|
||||||
text: 'Horizontally',
|
text: 'Horizontally',
|
||||||
action: () => {
|
action: (selection: any) => {
|
||||||
// TODO - alignHorizontal
|
const componentUpdates: Map<string, UpdateComponentRequest> = new Map();
|
||||||
|
const connectionUpdates: Map<string, UpdateComponentRequest> = new Map();
|
||||||
|
|
||||||
|
// determine the extent
|
||||||
|
let minY: number = 0,
|
||||||
|
maxY: number = 0;
|
||||||
|
selection.each((d: any) => {
|
||||||
|
if (d.type !== ComponentType.Connection) {
|
||||||
|
if (minY === 0 || d.position.y < minY) {
|
||||||
|
minY = d.position.y;
|
||||||
|
}
|
||||||
|
const componentMaxY = d.position.y + d.dimensions.height;
|
||||||
|
if (maxY === 0 || componentMaxY > maxY) {
|
||||||
|
maxY = componentMaxY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const center = (minY + maxY) / 2;
|
||||||
|
|
||||||
|
// align all components with top most component
|
||||||
|
selection.each((d: any) => {
|
||||||
|
if (d.type !== ComponentType.Connection) {
|
||||||
|
const delta = {
|
||||||
|
x: 0,
|
||||||
|
y: center - (d.position.y + d.dimensions.height / 2)
|
||||||
|
};
|
||||||
|
|
||||||
|
// if this component is already centered, no need to updated it
|
||||||
|
if (delta.y !== 0) {
|
||||||
|
// consider any connections
|
||||||
|
const connections = this.canvasUtils.getComponentConnections(d.id);
|
||||||
|
|
||||||
|
connections.forEach((connection: any) => {
|
||||||
|
const connectionSelection = d3.select('#id-' + connection.id);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!connectionUpdates.has(connection.id) &&
|
||||||
|
this.canvasUtils.getConnectionSourceComponentId(connection) ===
|
||||||
|
this.canvasUtils.getConnectionDestinationComponentId(connection)
|
||||||
|
) {
|
||||||
|
// this connection is self looping and hasn't been updated by the delta yet
|
||||||
|
const connectionUpdate = this.draggableBehavior.updateConnectionPosition(
|
||||||
|
connectionSelection.datum(),
|
||||||
|
delta
|
||||||
|
);
|
||||||
|
if (connectionUpdate !== null) {
|
||||||
|
connectionUpdates.set(connection.id, connectionUpdate);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
!connectionUpdates.has(connection.id) &&
|
||||||
|
connectionSelection.classed('selected') &&
|
||||||
|
this.canvasUtils.canModify(connectionSelection)
|
||||||
|
) {
|
||||||
|
// this is a selected connection that hasn't been updated by the delta yet
|
||||||
|
if (
|
||||||
|
this.canvasUtils.getConnectionSourceComponentId(connection) === d.id ||
|
||||||
|
!this.canvasUtils.isSourceSelected(connection, selection)
|
||||||
|
) {
|
||||||
|
// the connection is either outgoing or incoming when the source of the connection is not part of the selection
|
||||||
|
const connectionUpdate = this.draggableBehavior.updateConnectionPosition(
|
||||||
|
connectionSelection.datum(),
|
||||||
|
delta
|
||||||
|
);
|
||||||
|
if (connectionUpdate !== null) {
|
||||||
|
connectionUpdates.set(connection.id, connectionUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
componentUpdates.set(d.id, this.draggableBehavior.updateComponentPosition(d, delta));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (connectionUpdates.size > 0 || componentUpdates.size > 0) {
|
||||||
|
// dispatch the position updates
|
||||||
|
this.store.dispatch(
|
||||||
|
updatePositions({
|
||||||
|
request: {
|
||||||
|
requestId: this.updatePositionRequestId++,
|
||||||
|
componentUpdates: Array.from(componentUpdates.values()),
|
||||||
|
connectionUpdates: Array.from(connectionUpdates.values())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
condition: (selection: any) => {
|
condition: (selection: any) => {
|
||||||
// TODO - canAlign
|
return this.canvasUtils.canAlign(selection);
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
clazz: 'fa fa-align-center',
|
clazz: 'fa fa-align-center',
|
||||||
text: 'Vertically',
|
text: 'Vertically',
|
||||||
action: () => {
|
action: (selection: any) => {
|
||||||
// TODO - alignVertical
|
const componentUpdates: Map<string, UpdateComponentRequest> = new Map();
|
||||||
|
const connectionUpdates: Map<string, UpdateComponentRequest> = new Map();
|
||||||
|
|
||||||
|
// determine the extent
|
||||||
|
let minX = 0;
|
||||||
|
let maxX = 0;
|
||||||
|
selection.each((d: any) => {
|
||||||
|
if (d.type !== ComponentType.Connection) {
|
||||||
|
if (minX === 0 || d.position.x < minX) {
|
||||||
|
minX = d.position.x;
|
||||||
|
}
|
||||||
|
const componentMaxX = d.position.x + d.dimensions.width;
|
||||||
|
if (maxX === 0 || componentMaxX > maxX) {
|
||||||
|
maxX = componentMaxX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const center = (minX + maxX) / 2;
|
||||||
|
|
||||||
|
// align all components with top most component
|
||||||
|
selection.each((d: any) => {
|
||||||
|
if (d.type !== ComponentType.Connection) {
|
||||||
|
const delta = {
|
||||||
|
x: center - (d.position.x + d.dimensions.width / 2),
|
||||||
|
y: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// if this component is already centered, no need to updated it
|
||||||
|
if (delta.x !== 0) {
|
||||||
|
// consider any connections
|
||||||
|
const connections = this.canvasUtils.getComponentConnections(d.id);
|
||||||
|
connections.forEach((connection: any) => {
|
||||||
|
const connectionSelection = d3.select('#id-' + connection.id);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!connectionUpdates.has(connection.id) &&
|
||||||
|
this.canvasUtils.getConnectionSourceComponentId(connection) ===
|
||||||
|
this.canvasUtils.getConnectionDestinationComponentId(connection)
|
||||||
|
) {
|
||||||
|
// this connection is self looping and hasn't been updated by the delta yet
|
||||||
|
const connectionUpdate = this.draggableBehavior.updateConnectionPosition(
|
||||||
|
connectionSelection.datum(),
|
||||||
|
delta
|
||||||
|
);
|
||||||
|
if (connectionUpdate !== null) {
|
||||||
|
connectionUpdates.set(connection.id, connectionUpdate);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
!connectionUpdates.has(connection.id) &&
|
||||||
|
connectionSelection.classed('selected') &&
|
||||||
|
this.canvasUtils.canModify(connectionSelection)
|
||||||
|
) {
|
||||||
|
// this is a selected connection that hasn't been updated by the delta yet
|
||||||
|
if (
|
||||||
|
this.canvasUtils.getConnectionSourceComponentId(connection) === d.id ||
|
||||||
|
!this.canvasUtils.isSourceSelected(connection, selection)
|
||||||
|
) {
|
||||||
|
// the connection is either outgoing or incoming when the source of the connection is not part of the selection
|
||||||
|
const connectionUpdate = this.draggableBehavior.updateConnectionPosition(
|
||||||
|
connectionSelection.datum(),
|
||||||
|
delta
|
||||||
|
);
|
||||||
|
if (connectionUpdate !== null) {
|
||||||
|
connectionUpdates.set(connection.id, connectionUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
componentUpdates.set(d.id, this.draggableBehavior.updateComponentPosition(d, delta));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (connectionUpdates.size > 0 || componentUpdates.size > 0) {
|
||||||
|
// dispatch the position updates
|
||||||
|
this.store.dispatch(
|
||||||
|
updatePositions({
|
||||||
|
request: {
|
||||||
|
requestId: this.updatePositionRequestId++,
|
||||||
|
componentUpdates: Array.from(componentUpdates.values()),
|
||||||
|
connectionUpdates: Array.from(connectionUpdates.values())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1145,7 +1322,8 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
||||||
private canvasUtils: CanvasUtils,
|
private canvasUtils: CanvasUtils,
|
||||||
private client: Client,
|
private client: Client,
|
||||||
private canvasView: CanvasView,
|
private canvasView: CanvasView,
|
||||||
private canvasActionsService: CanvasActionsService
|
private canvasActionsService: CanvasActionsService,
|
||||||
|
private draggableBehavior: DraggableBehavior
|
||||||
) {
|
) {
|
||||||
this.allMenus = new Map<string, ContextMenuDefinition>();
|
this.allMenus = new Map<string, ContextMenuDefinition>();
|
||||||
this.allMenus.set(this.ROOT_MENU.id, this.ROOT_MENU);
|
this.allMenus.set(this.ROOT_MENU.id, this.ROOT_MENU);
|
||||||
|
|
|
@ -231,6 +231,33 @@ export class CanvasUtils {
|
||||||
return this.isNotRootGroup() && selection.empty();
|
return this.isNotRootGroup() && selection.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the specified selection is alignable (in a single action).
|
||||||
|
*
|
||||||
|
* @param {selection} selection The selection
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public canAlign(selection: any) {
|
||||||
|
let canAlign = true;
|
||||||
|
|
||||||
|
// determine if the current selection is entirely connections
|
||||||
|
const selectedConnections = selection.filter((d: any) => {
|
||||||
|
return d.type == ComponentType.Connection;
|
||||||
|
});
|
||||||
|
|
||||||
|
// require multiple selections besides connections
|
||||||
|
if (selection.size() - selectedConnections.size() < 2) {
|
||||||
|
canAlign = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// require write permissions
|
||||||
|
if (!this.canModify(selection)) {
|
||||||
|
canAlign = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return canAlign;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether the components in the specified selection are writable.
|
* Determines whether the components in the specified selection are writable.
|
||||||
*
|
*
|
||||||
|
@ -526,6 +553,17 @@ export class CanvasUtils {
|
||||||
return selection.size() === 1 && selection.classed('funnel');
|
return selection.size() === 1 && selection.classed('funnel');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// determine if the source of this connection is part of the selection
|
||||||
|
public isSourceSelected(connection: any, selection: any): boolean {
|
||||||
|
return (
|
||||||
|
selection
|
||||||
|
.filter((d: any) => {
|
||||||
|
return this.getConnectionSourceComponentId(connection) === d.id;
|
||||||
|
})
|
||||||
|
.size() > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether the current selection is a stateful processor.
|
* Determines whether the current selection is a stateful processor.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1927,6 +1927,9 @@ export class FlowEffects {
|
||||||
updatePositionComplete$ = createEffect(() =>
|
updatePositionComplete$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(FlowActions.updatePositionComplete),
|
ofType(FlowActions.updatePositionComplete),
|
||||||
|
tap(() => {
|
||||||
|
this.birdseyeView.refresh();
|
||||||
|
}),
|
||||||
map((action) => action.response),
|
map((action) => action.response),
|
||||||
switchMap((response) =>
|
switchMap((response) =>
|
||||||
of(FlowActions.renderConnectionsForComponent({ id: response.id, updatePath: true, updateLabel: false }))
|
of(FlowActions.renderConnectionsForComponent({ id: response.id, updatePath: true, updateLabel: false }))
|
||||||
|
|
Loading…
Reference in New Issue