mirror of https://github.com/apache/nifi.git
NIFI-13059: (#8661)
- Adding support for copying and pasting on the canvas. This closes #8661
This commit is contained in:
parent
8d8aebc5be
commit
41e4779bc5
|
@ -54,11 +54,14 @@ import {
|
||||||
startCurrentProcessGroup,
|
startCurrentProcessGroup,
|
||||||
stopComponents,
|
stopComponents,
|
||||||
stopCurrentProcessGroup,
|
stopCurrentProcessGroup,
|
||||||
stopVersionControlRequest
|
stopVersionControlRequest,
|
||||||
|
copy,
|
||||||
|
paste
|
||||||
} from '../state/flow/flow.actions';
|
} from '../state/flow/flow.actions';
|
||||||
import { ComponentType } from '../../../state/shared';
|
import { ComponentType } from '../../../state/shared';
|
||||||
import {
|
import {
|
||||||
ConfirmStopVersionControlRequest,
|
ConfirmStopVersionControlRequest,
|
||||||
|
CopyComponentRequest,
|
||||||
DeleteComponentRequest,
|
DeleteComponentRequest,
|
||||||
MoveComponentRequest,
|
MoveComponentRequest,
|
||||||
OpenChangeVersionDialogRequest,
|
OpenChangeVersionDialogRequest,
|
||||||
|
@ -76,6 +79,7 @@ import { getComponentStateAndOpenDialog } from '../../../state/component-state/c
|
||||||
import { navigateToComponentDocumentation } from '../../../state/documentation/documentation.actions';
|
import { navigateToComponentDocumentation } from '../../../state/documentation/documentation.actions';
|
||||||
import * as d3 from 'd3';
|
import * as d3 from 'd3';
|
||||||
import { Client } from '../../../service/client.service';
|
import { Client } from '../../../service/client.service';
|
||||||
|
import { CanvasView } from './canvas-view.service';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
||||||
|
@ -1173,12 +1177,12 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
condition: (selection: any) => {
|
condition: (selection: d3.Selection<any, any, any, any>) => {
|
||||||
return this.canvasUtils.isDisconnected(selection);
|
return this.canvasUtils.isDisconnected(selection);
|
||||||
},
|
},
|
||||||
clazz: 'fa icon-group',
|
clazz: 'fa icon-group',
|
||||||
text: 'Group',
|
text: 'Group',
|
||||||
action: (selection: any) => {
|
action: (selection: d3.Selection<any, any, any, any>) => {
|
||||||
const moveComponents: MoveComponentRequest[] = [];
|
const moveComponents: MoveComponentRequest[] = [];
|
||||||
selection.each(function (d: any) {
|
selection.each(function (d: any) {
|
||||||
moveComponents.push({
|
moveComponents.push({
|
||||||
|
@ -1212,25 +1216,55 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
||||||
isSeparator: true
|
isSeparator: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
condition: (selection: any) => {
|
condition: (selection: d3.Selection<any, any, any, any>) => {
|
||||||
// TODO - isCopyable
|
return this.canvasUtils.isCopyable(selection);
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
clazz: 'fa fa-copy',
|
clazz: 'fa fa-copy',
|
||||||
text: 'Copy',
|
text: 'Copy',
|
||||||
action: () => {
|
action: (selection: d3.Selection<any, any, any, any>) => {
|
||||||
// TODO - copy
|
const origin = this.canvasUtils.getOrigin(selection);
|
||||||
|
const dimensions = this.canvasView.getSelectionBoundingClientRect(selection);
|
||||||
|
|
||||||
|
const components: CopyComponentRequest[] = [];
|
||||||
|
selection.each((d) => {
|
||||||
|
components.push({
|
||||||
|
id: d.id,
|
||||||
|
type: d.type,
|
||||||
|
uri: d.uri,
|
||||||
|
entity: d
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.store.dispatch(
|
||||||
|
copy({
|
||||||
|
request: {
|
||||||
|
components,
|
||||||
|
origin,
|
||||||
|
dimensions
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
condition: (selection: any) => {
|
condition: () => {
|
||||||
// TODO - isPastable
|
return this.canvasUtils.isPastable();
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
clazz: 'fa fa-paste',
|
clazz: 'fa fa-paste',
|
||||||
text: 'Paste',
|
text: 'Paste',
|
||||||
action: () => {
|
action: (selection: d3.Selection<any, any, any, any>, event) => {
|
||||||
// TODO - paste
|
if (event) {
|
||||||
|
const pasteLocation = this.canvasView.getCanvasPosition({ x: event.pageX, y: event.pageY });
|
||||||
|
if (pasteLocation) {
|
||||||
|
this.store.dispatch(
|
||||||
|
paste({
|
||||||
|
request: {
|
||||||
|
pasteLocation
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1325,7 +1359,8 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
||||||
constructor(
|
constructor(
|
||||||
private store: Store<CanvasState>,
|
private store: Store<CanvasState>,
|
||||||
private canvasUtils: CanvasUtils,
|
private canvasUtils: CanvasUtils,
|
||||||
private client: Client
|
private client: Client,
|
||||||
|
private canvasView: CanvasView
|
||||||
) {
|
) {
|
||||||
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);
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
selectBreadcrumbs,
|
selectBreadcrumbs,
|
||||||
selectCanvasPermissions,
|
selectCanvasPermissions,
|
||||||
selectConnections,
|
selectConnections,
|
||||||
|
selectCopiedSnippet,
|
||||||
selectCurrentProcessGroupId,
|
selectCurrentProcessGroupId,
|
||||||
selectParentProcessGroupId
|
selectParentProcessGroupId
|
||||||
} from '../state/flow/flow.selectors';
|
} from '../state/flow/flow.selectors';
|
||||||
|
@ -39,7 +40,7 @@ import { selectCurrentUser } from '../../../state/current-user/current-user.sele
|
||||||
import { FlowConfiguration } from '../../../state/flow-configuration';
|
import { FlowConfiguration } from '../../../state/flow-configuration';
|
||||||
import { initialState as initialFlowConfigurationState } from '../../../state/flow-configuration/flow-configuration.reducer';
|
import { initialState as initialFlowConfigurationState } from '../../../state/flow-configuration/flow-configuration.reducer';
|
||||||
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
|
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
|
||||||
import { VersionControlInformation } from '../state/flow';
|
import { CopiedSnippet, VersionControlInformation } from '../state/flow';
|
||||||
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
|
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
|
||||||
import { ComponentPortal } from '@angular/cdk/portal';
|
import { ComponentPortal } from '@angular/cdk/portal';
|
||||||
|
|
||||||
|
@ -59,6 +60,7 @@ export class CanvasUtils {
|
||||||
private flowConfiguration: FlowConfiguration | null = initialFlowConfigurationState.flowConfiguration;
|
private flowConfiguration: FlowConfiguration | null = initialFlowConfigurationState.flowConfiguration;
|
||||||
private connections: any[] = [];
|
private connections: any[] = [];
|
||||||
private breadcrumbs: BreadcrumbEntity | null = null;
|
private breadcrumbs: BreadcrumbEntity | null = null;
|
||||||
|
private copiedSnippet: CopiedSnippet | null = null;
|
||||||
|
|
||||||
private readonly humanizeDuration: Humanizer;
|
private readonly humanizeDuration: Humanizer;
|
||||||
|
|
||||||
|
@ -118,6 +120,13 @@ export class CanvasUtils {
|
||||||
.subscribe((breadcrumbs) => {
|
.subscribe((breadcrumbs) => {
|
||||||
this.breadcrumbs = breadcrumbs;
|
this.breadcrumbs = breadcrumbs;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.store
|
||||||
|
.select(selectCopiedSnippet)
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe((copiedSnippet) => {
|
||||||
|
this.copiedSnippet = copiedSnippet;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasDownstream(selection: any): boolean {
|
public hasDownstream(selection: any): boolean {
|
||||||
|
@ -807,14 +816,13 @@ export class CanvasUtils {
|
||||||
*
|
*
|
||||||
* @argument {selection} selection The selection
|
* @argument {selection} selection The selection
|
||||||
*/
|
*/
|
||||||
getOrigin(selection: any): Position {
|
public getOrigin(selection: d3.Selection<any, any, any, any>): Position {
|
||||||
const self: CanvasUtils = this;
|
|
||||||
let x: number | undefined;
|
let x: number | undefined;
|
||||||
let y: number | undefined;
|
let y: number | undefined;
|
||||||
|
|
||||||
selection.each(function (this: any, d: any) {
|
selection.each((d, i, nodes) => {
|
||||||
const selected: any = d3.select(this);
|
const selected: any = d3.select(nodes[i]);
|
||||||
if (!self.isConnection(selected)) {
|
if (!this.isConnection(selected)) {
|
||||||
if (x == null || d.position.x < x) {
|
if (x == null || d.position.x < x) {
|
||||||
x = d.position.x;
|
x = d.position.x;
|
||||||
}
|
}
|
||||||
|
@ -831,6 +839,54 @@ export class CanvasUtils {
|
||||||
return { x, y };
|
return { x, y };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isCopyable(selection: d3.Selection<any, any, any, any>): boolean {
|
||||||
|
// if nothing is selected return
|
||||||
|
if (selection.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.canRead(selection)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine how many copyable components are selected
|
||||||
|
const copyable = selection.filter((d, i, nodes) => {
|
||||||
|
const selected = d3.select(nodes[i]);
|
||||||
|
if (this.isConnection(selected)) {
|
||||||
|
const sourceIncluded = !selection
|
||||||
|
.filter((source) => {
|
||||||
|
const sourceComponentId = this.getConnectionSourceComponentId(d);
|
||||||
|
return sourceComponentId === source.id;
|
||||||
|
})
|
||||||
|
.empty();
|
||||||
|
const destinationIncluded = !selection
|
||||||
|
.filter((destination) => {
|
||||||
|
const destinationComponentId = this.getConnectionDestinationComponentId(d);
|
||||||
|
return destinationComponentId === destination.id;
|
||||||
|
})
|
||||||
|
.empty();
|
||||||
|
return sourceIncluded && destinationIncluded;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
this.isProcessor(selected) ||
|
||||||
|
this.isFunnel(selected) ||
|
||||||
|
this.isLabel(selected) ||
|
||||||
|
this.isProcessGroup(selected) ||
|
||||||
|
this.isRemoteProcessGroup(selected) ||
|
||||||
|
this.isInputPort(selected) ||
|
||||||
|
this.isOutputPort(selected)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ensure everything selected is copyable
|
||||||
|
return selection.size() === copyable.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isPastable(): boolean {
|
||||||
|
return this.canvasPermissions.canWrite && this.copiedSnippet != null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the name for this connection.
|
* Gets the name for this connection.
|
||||||
*
|
*
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { RemoteProcessGroupManager } from './manager/remote-process-group-manage
|
||||||
import { ConnectionManager } from './manager/connection-manager.service';
|
import { ConnectionManager } from './manager/connection-manager.service';
|
||||||
import { deselectAllComponents } from '../state/flow/flow.actions';
|
import { deselectAllComponents } from '../state/flow/flow.actions';
|
||||||
import { CanvasUtils } from './canvas-utils.service';
|
import { CanvasUtils } from './canvas-utils.service';
|
||||||
|
import { Position } from '../state/shared';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
@ -185,7 +186,6 @@ export class CanvasView {
|
||||||
|
|
||||||
public isSelectedComponentOnScreen(): boolean {
|
public isSelectedComponentOnScreen(): boolean {
|
||||||
const canvasContainer: any = document.getElementById('canvas-container');
|
const canvasContainer: any = document.getElementById('canvas-container');
|
||||||
|
|
||||||
if (canvasContainer == null) {
|
if (canvasContainer == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -245,6 +245,55 @@ export class CanvasView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a bounding box is fully in the current viewable canvas area.
|
||||||
|
*
|
||||||
|
* @param {type} boundingBox Bounding box to check.
|
||||||
|
* @param {boolean} strict If true, the entire bounding box must be in the viewport.
|
||||||
|
* If false, only part of the bounding box must be in the viewport.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public isBoundingBoxInViewport(boundingBox: any, strict: boolean): boolean {
|
||||||
|
const selection: any = this.canvasUtils.getSelection();
|
||||||
|
if (selection.size() !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvasContainer: any = document.getElementById('canvas-container');
|
||||||
|
if (!canvasContainer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const yOffset = canvasContainer.getBoundingClientRect().top;
|
||||||
|
|
||||||
|
// scale the translation
|
||||||
|
const translate = [this.x / this.k, this.y / this.k];
|
||||||
|
|
||||||
|
// get the normalized screen width and height
|
||||||
|
const screenWidth = canvasContainer.offsetWidth / this.k;
|
||||||
|
const screenHeight = canvasContainer.offsetHeight / this.k;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
const left = Math.ceil(boundingBox.x);
|
||||||
|
const right = Math.floor(boundingBox.x + boundingBox.width);
|
||||||
|
const top = Math.ceil(boundingBox.y - yOffset / this.k);
|
||||||
|
const bottom = Math.floor(boundingBox.y - yOffset / this.k + boundingBox.height);
|
||||||
|
|
||||||
|
if (strict) {
|
||||||
|
return !(left < screenLeft || right > screenRight || top < screenTop || bottom > screenBottom);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
((left > screenLeft && left < screenRight) || (right < screenRight && right > screenLeft)) &&
|
||||||
|
((top > screenTop && top < screenBottom) || (bottom < screenBottom && bottom > screenTop))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public updateCanvasVisibility(): void {
|
public updateCanvasVisibility(): void {
|
||||||
const self: CanvasView = this;
|
const self: CanvasView = this;
|
||||||
const canvasContainer: any = document.getElementById('canvas-container');
|
const canvasContainer: any = document.getElementById('canvas-container');
|
||||||
|
@ -354,11 +403,6 @@ export class CanvasView {
|
||||||
}
|
}
|
||||||
|
|
||||||
public centerSelectedComponents(allowTransition: boolean): void {
|
public centerSelectedComponents(allowTransition: boolean): void {
|
||||||
const canvasContainer: any = document.getElementById('canvas-container');
|
|
||||||
if (canvasContainer == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selection: any = this.canvasUtils.getSelection();
|
const selection: any = this.canvasUtils.getSelection();
|
||||||
if (selection.empty()) {
|
if (selection.empty()) {
|
||||||
return;
|
return;
|
||||||
|
@ -368,7 +412,7 @@ export class CanvasView {
|
||||||
if (selection.size() === 1) {
|
if (selection.size() === 1) {
|
||||||
bbox = this.getSingleSelectionBoundingClientRect(selection);
|
bbox = this.getSingleSelectionBoundingClientRect(selection);
|
||||||
} else {
|
} else {
|
||||||
bbox = this.getBulkSelectionBoundingClientRect(selection, canvasContainer);
|
bbox = this.getSelectionBoundingClientRect(selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.allowTransition = allowTransition;
|
this.allowTransition = allowTransition;
|
||||||
|
@ -408,8 +452,13 @@ export class CanvasView {
|
||||||
/**
|
/**
|
||||||
* Get a BoundingClientRect, normalized to the canvas, that encompasses all nodes in a given selection.
|
* Get a BoundingClientRect, normalized to the canvas, that encompasses all nodes in a given selection.
|
||||||
*/
|
*/
|
||||||
private getBulkSelectionBoundingClientRect(selection: any, canvasContainer: any): any {
|
public getSelectionBoundingClientRect(selection: any): any {
|
||||||
const canvasBoundingBox: any = canvasContainer.getBoundingClientRect();
|
let yOffset = 0;
|
||||||
|
|
||||||
|
const canvasContainer: any = document.getElementById('canvas-container');
|
||||||
|
if (canvasContainer) {
|
||||||
|
yOffset = canvasContainer.getBoundingClientRect().top;
|
||||||
|
}
|
||||||
|
|
||||||
const initialBBox: any = {
|
const initialBBox: any = {
|
||||||
x: Number.MAX_VALUE,
|
x: Number.MAX_VALUE,
|
||||||
|
@ -430,9 +479,9 @@ export class CanvasView {
|
||||||
|
|
||||||
// normalize the bounding box with scale and translate
|
// normalize the bounding box with scale and translate
|
||||||
bbox.x = (bbox.x - this.x) / this.k;
|
bbox.x = (bbox.x - this.x) / this.k;
|
||||||
bbox.y = (bbox.y - canvasBoundingBox.top - this.y) / this.k;
|
bbox.y = (bbox.y - yOffset - this.y) / this.k;
|
||||||
bbox.right = (bbox.right - this.x) / this.k;
|
bbox.right = (bbox.right - this.x) / this.k;
|
||||||
bbox.bottom = (bbox.bottom - canvasBoundingBox.top - this.y) / this.k;
|
bbox.bottom = (bbox.bottom - yOffset - this.y) / this.k;
|
||||||
|
|
||||||
bbox.width = bbox.right - bbox.x;
|
bbox.width = bbox.right - bbox.x;
|
||||||
bbox.height = bbox.bottom - bbox.y;
|
bbox.height = bbox.bottom - bbox.y;
|
||||||
|
@ -442,6 +491,37 @@ export class CanvasView {
|
||||||
return bbox;
|
return bbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getCanvasPosition(position: Position): Position | null {
|
||||||
|
const canvasContainer: any = document.getElementById('canvas-container');
|
||||||
|
if (!canvasContainer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = canvasContainer.getBoundingClientRect();
|
||||||
|
|
||||||
|
// translate the point onto the canvas
|
||||||
|
const canvasDropPoint = {
|
||||||
|
x: position.x - rect.left,
|
||||||
|
y: position.y - rect.top
|
||||||
|
};
|
||||||
|
|
||||||
|
// if the position is over the canvas fire an event to add the new item
|
||||||
|
if (
|
||||||
|
canvasDropPoint.x >= 0 &&
|
||||||
|
canvasDropPoint.x < rect.width &&
|
||||||
|
canvasDropPoint.y >= 0 &&
|
||||||
|
canvasDropPoint.y < rect.height
|
||||||
|
) {
|
||||||
|
// adjust the x and y coordinates accordingly
|
||||||
|
const x = canvasDropPoint.x / this.k - this.x / this.k;
|
||||||
|
const y = canvasDropPoint.y / this.k - this.y / this.k;
|
||||||
|
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private centerBoundingBox(boundingBox: any): void {
|
private centerBoundingBox(boundingBox: any): void {
|
||||||
let scale: number = this.k;
|
let scale: number = this.k;
|
||||||
if (boundingBox.scale != null) {
|
if (boundingBox.scale != null) {
|
||||||
|
@ -460,7 +540,7 @@ export class CanvasView {
|
||||||
* @param {type} boundingBox
|
* @param {type} boundingBox
|
||||||
* @returns {number[]}
|
* @returns {number[]}
|
||||||
*/
|
*/
|
||||||
private getCenterForBoundingBox(boundingBox: any): number[] {
|
public getCenterForBoundingBox(boundingBox: any): number[] {
|
||||||
let scale: number = this.k;
|
let scale: number = this.k;
|
||||||
if (boundingBox.scale != null) {
|
if (boundingBox.scale != null) {
|
||||||
scale = boundingBox.scale;
|
scale = boundingBox.scale;
|
||||||
|
|
|
@ -35,7 +35,6 @@ import {
|
||||||
ReplayLastProvenanceEventRequest,
|
ReplayLastProvenanceEventRequest,
|
||||||
RunOnceRequest,
|
RunOnceRequest,
|
||||||
SaveToVersionControlRequest,
|
SaveToVersionControlRequest,
|
||||||
Snippet,
|
|
||||||
StartComponentRequest,
|
StartComponentRequest,
|
||||||
StartProcessGroupRequest,
|
StartProcessGroupRequest,
|
||||||
StopComponentRequest,
|
StopComponentRequest,
|
||||||
|
@ -257,33 +256,6 @@ export class FlowService implements PropertyDescriptorRetriever {
|
||||||
return this.httpClient.delete(this.nifiCommon.stripProtocol(deleteComponent.uri), { params });
|
return this.httpClient.delete(this.nifiCommon.stripProtocol(deleteComponent.uri), { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
createSnippet(snippet: Snippet): Observable<any> {
|
|
||||||
return this.httpClient.post(`${FlowService.API}/snippets`, {
|
|
||||||
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
|
|
||||||
snippet
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
moveSnippet(snippetId: string, groupId: string): Observable<any> {
|
|
||||||
const payload: any = {
|
|
||||||
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
|
|
||||||
snippet: {
|
|
||||||
id: snippetId,
|
|
||||||
parentGroupId: groupId
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return this.httpClient.put(`${FlowService.API}/snippets/${snippetId}`, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteSnippet(snippetId: string): Observable<any> {
|
|
||||||
const params = new HttpParams({
|
|
||||||
fromObject: {
|
|
||||||
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return this.httpClient.delete(`${FlowService.API}/snippets/${snippetId}`, { params });
|
|
||||||
}
|
|
||||||
|
|
||||||
replayLastProvenanceEvent(request: ReplayLastProvenanceEventRequest): Observable<any> {
|
replayLastProvenanceEvent(request: ReplayLastProvenanceEventRequest): Observable<any> {
|
||||||
return this.httpClient.post(`${FlowService.API}/provenance-events/latest/replays`, request);
|
return this.httpClient.post(`${FlowService.API}/provenance-events/latest/replays`, request);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Snippet, SnippetComponentRequest } from '../state/flow';
|
||||||
|
import { ClusterConnectionService } from '../../../service/cluster-connection.service';
|
||||||
|
import { ComponentType } from '../../../state/shared';
|
||||||
|
import { Client } from '../../../service/client.service';
|
||||||
|
import { Position } from '../state/shared';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class SnippetService {
|
||||||
|
private static readonly API: string = '../nifi-api';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private httpClient: HttpClient,
|
||||||
|
private client: Client,
|
||||||
|
private clusterConnectionService: ClusterConnectionService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
marshalSnippet(components: SnippetComponentRequest[], processGroupId: string): Snippet {
|
||||||
|
return components.reduce(
|
||||||
|
(snippet, component) => {
|
||||||
|
switch (component.type) {
|
||||||
|
case ComponentType.Processor:
|
||||||
|
snippet.processors[component.id] = this.client.getRevision(component.entity);
|
||||||
|
break;
|
||||||
|
case ComponentType.InputPort:
|
||||||
|
snippet.inputPorts[component.id] = this.client.getRevision(component.entity);
|
||||||
|
break;
|
||||||
|
case ComponentType.OutputPort:
|
||||||
|
snippet.outputPorts[component.id] = this.client.getRevision(component.entity);
|
||||||
|
break;
|
||||||
|
case ComponentType.ProcessGroup:
|
||||||
|
snippet.processGroups[component.id] = this.client.getRevision(component.entity);
|
||||||
|
break;
|
||||||
|
case ComponentType.RemoteProcessGroup:
|
||||||
|
snippet.remoteProcessGroups[component.id] = this.client.getRevision(component.entity);
|
||||||
|
break;
|
||||||
|
case ComponentType.Funnel:
|
||||||
|
snippet.funnels[component.id] = this.client.getRevision(component.entity);
|
||||||
|
break;
|
||||||
|
case ComponentType.Label:
|
||||||
|
snippet.labels[component.id] = this.client.getRevision(component.entity);
|
||||||
|
break;
|
||||||
|
case ComponentType.Connection:
|
||||||
|
snippet.connections[component.id] = this.client.getRevision(component.entity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return snippet;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentGroupId: processGroupId,
|
||||||
|
processors: {},
|
||||||
|
funnels: {},
|
||||||
|
inputPorts: {},
|
||||||
|
outputPorts: {},
|
||||||
|
remoteProcessGroups: {},
|
||||||
|
processGroups: {},
|
||||||
|
connections: {},
|
||||||
|
labels: {}
|
||||||
|
} as Snippet
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createSnippet(snippet: Snippet): Observable<any> {
|
||||||
|
return this.httpClient.post(`${SnippetService.API}/snippets`, {
|
||||||
|
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
|
||||||
|
snippet
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
moveSnippet(snippetId: string, groupId: string): Observable<any> {
|
||||||
|
const payload: any = {
|
||||||
|
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
|
||||||
|
snippet: {
|
||||||
|
id: snippetId,
|
||||||
|
parentGroupId: groupId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return this.httpClient.put(`${SnippetService.API}/snippets/${snippetId}`, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
copySnippet(snippetId: string, pasteLocation: Position, groupId: string): Observable<any> {
|
||||||
|
const payload: any = {
|
||||||
|
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
|
||||||
|
originX: pasteLocation.x,
|
||||||
|
originY: pasteLocation.y,
|
||||||
|
snippetId
|
||||||
|
};
|
||||||
|
return this.httpClient.post(`${SnippetService.API}/process-groups/${groupId}/snippet-instance`, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSnippet(snippetId: string): Observable<any> {
|
||||||
|
const params = new HttpParams({
|
||||||
|
fromObject: {
|
||||||
|
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return this.httpClient.delete(`${SnippetService.API}/snippets/${snippetId}`, { params });
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,8 @@ import {
|
||||||
ChangeVersionDialogRequest,
|
ChangeVersionDialogRequest,
|
||||||
ComponentEntity,
|
ComponentEntity,
|
||||||
ConfirmStopVersionControlRequest,
|
ConfirmStopVersionControlRequest,
|
||||||
|
CopiedSnippet,
|
||||||
|
CopyRequest,
|
||||||
CreateComponentRequest,
|
CreateComponentRequest,
|
||||||
CreateComponentResponse,
|
CreateComponentResponse,
|
||||||
CreateConnection,
|
CreateConnection,
|
||||||
|
@ -64,6 +66,8 @@ import {
|
||||||
OpenGroupComponentsDialogRequest,
|
OpenGroupComponentsDialogRequest,
|
||||||
OpenLocalChangesDialogRequest,
|
OpenLocalChangesDialogRequest,
|
||||||
OpenSaveVersionDialogRequest,
|
OpenSaveVersionDialogRequest,
|
||||||
|
PasteRequest,
|
||||||
|
PasteResponse,
|
||||||
RefreshRemoteProcessGroupRequest,
|
RefreshRemoteProcessGroupRequest,
|
||||||
ReplayLastProvenanceEventRequest,
|
ReplayLastProvenanceEventRequest,
|
||||||
RpgManageRemotePortsRequest,
|
RpgManageRemotePortsRequest,
|
||||||
|
@ -476,6 +480,14 @@ export const moveComponents = createAction(
|
||||||
props<{ request: MoveComponentsRequest }>()
|
props<{ request: MoveComponentsRequest }>()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const copy = createAction(`${CANVAS_PREFIX} Copy`, props<{ request: CopyRequest }>());
|
||||||
|
|
||||||
|
export const copySuccess = createAction(`${CANVAS_PREFIX} Copy Success`, props<{ copiedSnippet: CopiedSnippet }>());
|
||||||
|
|
||||||
|
export const paste = createAction(`${CANVAS_PREFIX} Paste`, props<{ request: PasteRequest }>());
|
||||||
|
|
||||||
|
export const pasteSuccess = createAction(`${CANVAS_PREFIX} Paste Success`, props<{ response: PasteResponse }>());
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Delete Component Actions
|
Delete Component Actions
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -39,14 +39,17 @@ import {
|
||||||
tap
|
tap
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
|
CopyComponentRequest,
|
||||||
CreateProcessGroupDialogRequest,
|
CreateProcessGroupDialogRequest,
|
||||||
DeleteComponentResponse,
|
DeleteComponentResponse,
|
||||||
GroupComponentsDialogRequest,
|
GroupComponentsDialogRequest,
|
||||||
ImportFromRegistryDialogRequest,
|
ImportFromRegistryDialogRequest,
|
||||||
LoadProcessGroupRequest,
|
LoadProcessGroupRequest,
|
||||||
LoadProcessGroupResponse,
|
LoadProcessGroupResponse,
|
||||||
|
MoveComponentRequest,
|
||||||
SaveVersionDialogRequest,
|
SaveVersionDialogRequest,
|
||||||
SaveVersionRequest,
|
SaveVersionRequest,
|
||||||
|
SelectedComponent,
|
||||||
Snippet,
|
Snippet,
|
||||||
StopVersionControlRequest,
|
StopVersionControlRequest,
|
||||||
StopVersionControlResponse,
|
StopVersionControlResponse,
|
||||||
|
@ -61,6 +64,7 @@ import { Action, Store } from '@ngrx/store';
|
||||||
import {
|
import {
|
||||||
selectAnySelectedComponentIds,
|
selectAnySelectedComponentIds,
|
||||||
selectChangeVersionRequest,
|
selectChangeVersionRequest,
|
||||||
|
selectCopiedSnippet,
|
||||||
selectCurrentParameterContext,
|
selectCurrentParameterContext,
|
||||||
selectCurrentProcessGroupId,
|
selectCurrentProcessGroupId,
|
||||||
selectMaxZIndex,
|
selectMaxZIndex,
|
||||||
|
@ -119,6 +123,8 @@ import { LocalChangesDialog } from '../../ui/canvas/items/flow/local-changes-dia
|
||||||
import { ClusterConnectionService } from '../../../../service/cluster-connection.service';
|
import { ClusterConnectionService } from '../../../../service/cluster-connection.service';
|
||||||
import { ExtensionTypesService } from '../../../../service/extension-types.service';
|
import { ExtensionTypesService } from '../../../../service/extension-types.service';
|
||||||
import { ChangeComponentVersionDialog } from '../../../../ui/common/change-component-version-dialog/change-component-version-dialog';
|
import { ChangeComponentVersionDialog } from '../../../../ui/common/change-component-version-dialog/change-component-version-dialog';
|
||||||
|
import { SnippetService } from '../../service/snippet.service';
|
||||||
|
import { selectTransform } from '../transform/transform.selectors';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FlowEffects {
|
export class FlowEffects {
|
||||||
|
@ -134,6 +140,7 @@ export class FlowEffects {
|
||||||
private birdseyeView: BirdseyeView,
|
private birdseyeView: BirdseyeView,
|
||||||
private connectionManager: ConnectionManager,
|
private connectionManager: ConnectionManager,
|
||||||
private clusterConnectionService: ClusterConnectionService,
|
private clusterConnectionService: ClusterConnectionService,
|
||||||
|
private snippetService: SnippetService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private dialog: MatDialog,
|
private dialog: MatDialog,
|
||||||
private propertyTableHelperService: PropertyTableHelperService,
|
private propertyTableHelperService: PropertyTableHelperService,
|
||||||
|
@ -1816,53 +1823,11 @@ export class FlowEffects {
|
||||||
map((action) => action.request),
|
map((action) => action.request),
|
||||||
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
|
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
|
||||||
mergeMap(([request, processGroupId]) => {
|
mergeMap(([request, processGroupId]) => {
|
||||||
const components: any[] = request.components;
|
const components: MoveComponentRequest[] = request.components;
|
||||||
|
const snippet = this.snippetService.marshalSnippet(components, processGroupId);
|
||||||
|
|
||||||
const snippet: Snippet = components.reduce(
|
return from(this.snippetService.createSnippet(snippet)).pipe(
|
||||||
(snippet, component) => {
|
switchMap((response) => this.snippetService.moveSnippet(response.snippet.id, request.groupId)),
|
||||||
switch (component.type) {
|
|
||||||
case ComponentType.Processor:
|
|
||||||
snippet.processors[component.id] = this.client.getRevision(component.entity);
|
|
||||||
break;
|
|
||||||
case ComponentType.InputPort:
|
|
||||||
snippet.inputPorts[component.id] = this.client.getRevision(component.entity);
|
|
||||||
break;
|
|
||||||
case ComponentType.OutputPort:
|
|
||||||
snippet.outputPorts[component.id] = this.client.getRevision(component.entity);
|
|
||||||
break;
|
|
||||||
case ComponentType.ProcessGroup:
|
|
||||||
snippet.processGroups[component.id] = this.client.getRevision(component.entity);
|
|
||||||
break;
|
|
||||||
case ComponentType.RemoteProcessGroup:
|
|
||||||
snippet.remoteProcessGroups[component.id] = this.client.getRevision(component.entity);
|
|
||||||
break;
|
|
||||||
case ComponentType.Funnel:
|
|
||||||
snippet.funnels[component.id] = this.client.getRevision(component.entity);
|
|
||||||
break;
|
|
||||||
case ComponentType.Label:
|
|
||||||
snippet.labels[component.id] = this.client.getRevision(component.entity);
|
|
||||||
break;
|
|
||||||
case ComponentType.Connection:
|
|
||||||
snippet.connections[component.id] = this.client.getRevision(component.entity);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return snippet;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
parentGroupId: processGroupId,
|
|
||||||
processors: {},
|
|
||||||
funnels: {},
|
|
||||||
inputPorts: {},
|
|
||||||
outputPorts: {},
|
|
||||||
remoteProcessGroups: {},
|
|
||||||
processGroups: {},
|
|
||||||
connections: {},
|
|
||||||
labels: {}
|
|
||||||
} as Snippet
|
|
||||||
);
|
|
||||||
|
|
||||||
return from(this.flowService.createSnippet(snippet)).pipe(
|
|
||||||
switchMap((response) => this.flowService.moveSnippet(response.snippet.id, request.groupId)),
|
|
||||||
map(() => {
|
map(() => {
|
||||||
const deleteResponses: DeleteComponentResponse[] = [];
|
const deleteResponses: DeleteComponentResponse[] = [];
|
||||||
|
|
||||||
|
@ -1884,6 +1849,157 @@ export class FlowEffects {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
copy$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(FlowActions.copy),
|
||||||
|
map((action) => action.request),
|
||||||
|
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
|
||||||
|
switchMap(([request, processGroupId]) => {
|
||||||
|
const components: CopyComponentRequest[] = request.components;
|
||||||
|
const snippet = this.snippetService.marshalSnippet(components, processGroupId);
|
||||||
|
return of(
|
||||||
|
FlowActions.copySuccess({
|
||||||
|
copiedSnippet: {
|
||||||
|
snippet,
|
||||||
|
dimensions: request.dimensions,
|
||||||
|
origin: request.origin
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
paste$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(FlowActions.paste),
|
||||||
|
map((action) => action.request),
|
||||||
|
concatLatestFrom(() => [
|
||||||
|
this.store.select(selectCopiedSnippet).pipe(isDefinedAndNotNull()),
|
||||||
|
this.store.select(selectCurrentProcessGroupId),
|
||||||
|
this.store.select(selectTransform)
|
||||||
|
]),
|
||||||
|
switchMap(([request, copiedSnippet, processGroupId, transform]) =>
|
||||||
|
from(this.snippetService.createSnippet(copiedSnippet.snippet)).pipe(
|
||||||
|
switchMap((response) => {
|
||||||
|
let pasteLocation = request.pasteLocation;
|
||||||
|
const snippetOrigin = copiedSnippet.origin;
|
||||||
|
const dimensions = copiedSnippet.dimensions;
|
||||||
|
|
||||||
|
if (!pasteLocation) {
|
||||||
|
// if the copied snippet is from a different group or the original items are not in the viewport, center the pasted snippet
|
||||||
|
if (
|
||||||
|
copiedSnippet.snippet.parentGroupId != processGroupId ||
|
||||||
|
!this.canvasView.isBoundingBoxInViewport(dimensions, false)
|
||||||
|
) {
|
||||||
|
const center = this.canvasView.getCenterForBoundingBox(dimensions);
|
||||||
|
pasteLocation = {
|
||||||
|
x: center[0] - transform.translate.x / transform.scale,
|
||||||
|
y: center[1] - transform.translate.y / transform.scale
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
pasteLocation = {
|
||||||
|
x: snippetOrigin.x + 25,
|
||||||
|
y: snippetOrigin.y + 25
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return from(
|
||||||
|
this.snippetService.copySnippet(response.snippet.id, pasteLocation, processGroupId)
|
||||||
|
).pipe(map((response) => FlowActions.pasteSuccess({ response })));
|
||||||
|
}),
|
||||||
|
catchError((error) => of(FlowActions.flowSnackbarError({ error: error.error })))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
pasteSuccess$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(FlowActions.pasteSuccess),
|
||||||
|
map((action) => action.response),
|
||||||
|
switchMap((response) => {
|
||||||
|
this.canvasView.updateCanvasVisibility();
|
||||||
|
this.birdseyeView.refresh();
|
||||||
|
|
||||||
|
const components: SelectedComponent[] = [];
|
||||||
|
components.push(
|
||||||
|
...response.flow.labels.map((label) => {
|
||||||
|
return {
|
||||||
|
id: label.id,
|
||||||
|
componentType: ComponentType.Label
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
components.push(
|
||||||
|
...response.flow.funnels.map((funnel) => {
|
||||||
|
return {
|
||||||
|
id: funnel.id,
|
||||||
|
componentType: ComponentType.Funnel
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
components.push(
|
||||||
|
...response.flow.remoteProcessGroups.map((remoteProcessGroups) => {
|
||||||
|
return {
|
||||||
|
id: remoteProcessGroups.id,
|
||||||
|
componentType: ComponentType.RemoteProcessGroup
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
components.push(
|
||||||
|
...response.flow.inputPorts.map((inputPorts) => {
|
||||||
|
return {
|
||||||
|
id: inputPorts.id,
|
||||||
|
componentType: ComponentType.InputPort
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
components.push(
|
||||||
|
...response.flow.outputPorts.map((outputPorts) => {
|
||||||
|
return {
|
||||||
|
id: outputPorts.id,
|
||||||
|
componentType: ComponentType.OutputPort
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
components.push(
|
||||||
|
...response.flow.processGroups.map((processGroup) => {
|
||||||
|
return {
|
||||||
|
id: processGroup.id,
|
||||||
|
componentType: ComponentType.ProcessGroup
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
components.push(
|
||||||
|
...response.flow.processors.map((processor) => {
|
||||||
|
return {
|
||||||
|
id: processor.id,
|
||||||
|
componentType: ComponentType.Processor
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
components.push(
|
||||||
|
...response.flow.connections.map((connection) => {
|
||||||
|
return {
|
||||||
|
id: connection.id,
|
||||||
|
componentType: ComponentType.Connection
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return of(
|
||||||
|
FlowActions.selectComponents({
|
||||||
|
request: {
|
||||||
|
components
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
deleteComponent$ = createEffect(() =>
|
deleteComponent$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(FlowActions.deleteComponents),
|
ofType(FlowActions.deleteComponents),
|
||||||
|
@ -1962,8 +2078,8 @@ export class FlowEffects {
|
||||||
} as Snippet
|
} as Snippet
|
||||||
);
|
);
|
||||||
|
|
||||||
return from(this.flowService.createSnippet(snippet)).pipe(
|
return from(this.snippetService.createSnippet(snippet)).pipe(
|
||||||
switchMap((response) => this.flowService.deleteSnippet(response.snippet.id)),
|
switchMap((response) => this.snippetService.deleteSnippet(response.snippet.id)),
|
||||||
map(() => {
|
map(() => {
|
||||||
const deleteResponses: DeleteComponentResponse[] = [];
|
const deleteResponses: DeleteComponentResponse[] = [];
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
changeVersionComplete,
|
changeVersionComplete,
|
||||||
changeVersionSuccess,
|
changeVersionSuccess,
|
||||||
clearFlowApiError,
|
clearFlowApiError,
|
||||||
|
copySuccess,
|
||||||
createComponentComplete,
|
createComponentComplete,
|
||||||
createComponentSuccess,
|
createComponentSuccess,
|
||||||
createConnection,
|
createConnection,
|
||||||
|
@ -41,6 +42,7 @@ import {
|
||||||
loadProcessorSuccess,
|
loadProcessorSuccess,
|
||||||
loadRemoteProcessGroupSuccess,
|
loadRemoteProcessGroupSuccess,
|
||||||
navigateWithoutTransform,
|
navigateWithoutTransform,
|
||||||
|
pasteSuccess,
|
||||||
pollChangeVersionSuccess,
|
pollChangeVersionSuccess,
|
||||||
pollRevertChangesSuccess,
|
pollRevertChangesSuccess,
|
||||||
requestRefreshRemoteProcessGroup,
|
requestRefreshRemoteProcessGroup,
|
||||||
|
@ -143,6 +145,7 @@ export const initialState: FlowState = {
|
||||||
parameterProviderBulletins: [],
|
parameterProviderBulletins: [],
|
||||||
reportingTaskBulletins: []
|
reportingTaskBulletins: []
|
||||||
},
|
},
|
||||||
|
copiedSnippet: null,
|
||||||
dragging: false,
|
dragging: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
versionSaving: false,
|
versionSaving: false,
|
||||||
|
@ -324,6 +327,51 @@ export const flowReducer = createReducer(
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
on(copySuccess, (state, { copiedSnippet }) => ({
|
||||||
|
...state,
|
||||||
|
copiedSnippet
|
||||||
|
})),
|
||||||
|
on(pasteSuccess, (state, { response }) => {
|
||||||
|
return produce(state, (draftState) => {
|
||||||
|
const labels: any[] | null = getComponentCollection(draftState, ComponentType.Label);
|
||||||
|
if (labels) {
|
||||||
|
labels.push(...response.flow.labels);
|
||||||
|
}
|
||||||
|
const funnels: any[] | null = getComponentCollection(draftState, ComponentType.Funnel);
|
||||||
|
if (funnels) {
|
||||||
|
funnels.push(...response.flow.funnels);
|
||||||
|
}
|
||||||
|
const remoteProcessGroups: any[] | null = getComponentCollection(
|
||||||
|
draftState,
|
||||||
|
ComponentType.RemoteProcessGroup
|
||||||
|
);
|
||||||
|
if (remoteProcessGroups) {
|
||||||
|
remoteProcessGroups.push(...response.flow.remoteProcessGroups);
|
||||||
|
}
|
||||||
|
const inputPorts: any[] | null = getComponentCollection(draftState, ComponentType.InputPort);
|
||||||
|
if (inputPorts) {
|
||||||
|
inputPorts.push(...response.flow.inputPorts);
|
||||||
|
}
|
||||||
|
const outputPorts: any[] | null = getComponentCollection(draftState, ComponentType.OutputPort);
|
||||||
|
if (outputPorts) {
|
||||||
|
outputPorts.push(...response.flow.outputPorts);
|
||||||
|
}
|
||||||
|
const processGroups: any[] | null = getComponentCollection(draftState, ComponentType.ProcessGroup);
|
||||||
|
if (processGroups) {
|
||||||
|
processGroups.push(...response.flow.processGroups);
|
||||||
|
}
|
||||||
|
const processors: any[] | null = getComponentCollection(draftState, ComponentType.Processor);
|
||||||
|
if (processors) {
|
||||||
|
processors.push(...response.flow.processors);
|
||||||
|
}
|
||||||
|
const connections: any[] | null = getComponentCollection(draftState, ComponentType.Connection);
|
||||||
|
if (connections) {
|
||||||
|
connections.push(...response.flow.connections);
|
||||||
|
}
|
||||||
|
|
||||||
|
draftState.copiedSnippet = null;
|
||||||
|
});
|
||||||
|
}),
|
||||||
on(setDragging, (state, { dragging }) => ({
|
on(setDragging, (state, { dragging }) => ({
|
||||||
...state,
|
...state,
|
||||||
dragging
|
dragging
|
||||||
|
|
|
@ -42,6 +42,8 @@ export const selectCurrentProcessGroupId = createSelector(selectFlowState, (stat
|
||||||
|
|
||||||
export const selectRefreshRpgDetails = createSelector(selectFlowState, (state: FlowState) => state.refreshRpgDetails);
|
export const selectRefreshRpgDetails = createSelector(selectFlowState, (state: FlowState) => state.refreshRpgDetails);
|
||||||
|
|
||||||
|
export const selectCopiedSnippet = createSelector(selectFlowState, (state: FlowState) => state.copiedSnippet);
|
||||||
|
|
||||||
export const selectCurrentParameterContext = createSelector(
|
export const selectCurrentParameterContext = createSelector(
|
||||||
selectFlowState,
|
selectFlowState,
|
||||||
(state: FlowState) => state.flow.processGroupFlow.parameterContext
|
(state: FlowState) => state.flow.processGroupFlow.parameterContext
|
||||||
|
|
|
@ -423,18 +423,36 @@ export interface UpdatePositionsRequest {
|
||||||
connectionUpdates: UpdateComponentRequest[];
|
connectionUpdates: UpdateComponentRequest[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MoveComponentRequest {
|
export interface SnippetComponentRequest {
|
||||||
id: string;
|
id: string;
|
||||||
uri: string;
|
uri: string;
|
||||||
type: ComponentType;
|
type: ComponentType;
|
||||||
entity: any;
|
entity: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MoveComponentRequest extends SnippetComponentRequest {}
|
||||||
|
|
||||||
export interface MoveComponentsRequest {
|
export interface MoveComponentsRequest {
|
||||||
components: MoveComponentRequest[];
|
components: MoveComponentRequest[];
|
||||||
groupId: string;
|
groupId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CopyComponentRequest extends SnippetComponentRequest {}
|
||||||
|
|
||||||
|
export interface CopyRequest {
|
||||||
|
components: CopyComponentRequest[];
|
||||||
|
origin: Position;
|
||||||
|
dimensions: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasteRequest {
|
||||||
|
pasteLocation?: Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasteResponse {
|
||||||
|
flow: Flow;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DeleteComponentRequest {
|
export interface DeleteComponentRequest {
|
||||||
id: string;
|
id: string;
|
||||||
uri: string;
|
uri: string;
|
||||||
|
@ -490,6 +508,12 @@ export interface Snippet {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CopiedSnippet {
|
||||||
|
snippet: Snippet;
|
||||||
|
origin: Position;
|
||||||
|
dimensions: any;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Tooltips
|
Tooltips
|
||||||
*/
|
*/
|
||||||
|
@ -613,6 +637,7 @@ export interface FlowState {
|
||||||
error: string | null;
|
error: string | null;
|
||||||
versionSaving: boolean;
|
versionSaving: boolean;
|
||||||
changeVersionRequest: FlowUpdateRequestEntity | null;
|
changeVersionRequest: FlowUpdateRequestEntity | null;
|
||||||
|
copiedSnippet: CopiedSnippet | null;
|
||||||
status: 'pending' | 'loading' | 'error' | 'success';
|
status: 'pending' | 'loading' | 'error' | 'success';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -116,8 +116,8 @@
|
||||||
color="primary"
|
color="primary"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
type="button"
|
type="button"
|
||||||
[disabled]="!canPaste(selection)"
|
[disabled]="!canPaste()"
|
||||||
(click)="paste(selection)">
|
(click)="paste()">
|
||||||
<i class="fa fa-paste"></i>
|
<i class="fa fa-paste"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -17,11 +17,13 @@
|
||||||
|
|
||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
|
copy,
|
||||||
deleteComponents,
|
deleteComponents,
|
||||||
getParameterContextsAndOpenGroupComponentsDialog,
|
getParameterContextsAndOpenGroupComponentsDialog,
|
||||||
navigateToEditComponent,
|
navigateToEditComponent,
|
||||||
navigateToEditCurrentProcessGroup,
|
navigateToEditCurrentProcessGroup,
|
||||||
navigateToManageComponentPolicies,
|
navigateToManageComponentPolicies,
|
||||||
|
paste,
|
||||||
setOperationCollapsed,
|
setOperationCollapsed,
|
||||||
startComponents,
|
startComponents,
|
||||||
startCurrentProcessGroup,
|
startCurrentProcessGroup,
|
||||||
|
@ -34,6 +36,7 @@ import { CanvasUtils } from '../../../../service/canvas-utils.service';
|
||||||
import { initialState } from '../../../../state/flow/flow.reducer';
|
import { initialState } from '../../../../state/flow/flow.reducer';
|
||||||
import { Storage } from '../../../../../../service/storage.service';
|
import { Storage } from '../../../../../../service/storage.service';
|
||||||
import {
|
import {
|
||||||
|
CopyComponentRequest,
|
||||||
DeleteComponentRequest,
|
DeleteComponentRequest,
|
||||||
MoveComponentRequest,
|
MoveComponentRequest,
|
||||||
StartComponentRequest,
|
StartComponentRequest,
|
||||||
|
@ -43,6 +46,8 @@ import {
|
||||||
import { BreadcrumbEntity } from '../../../../state/shared';
|
import { BreadcrumbEntity } from '../../../../state/shared';
|
||||||
import { ComponentType } from '../../../../../../state/shared';
|
import { ComponentType } from '../../../../../../state/shared';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import * as d3 from 'd3';
|
||||||
|
import { CanvasView } from '../../../../service/canvas-view.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'operation-control',
|
selector: 'operation-control',
|
||||||
|
@ -63,6 +68,7 @@ export class OperationControl {
|
||||||
constructor(
|
constructor(
|
||||||
private store: Store<CanvasState>,
|
private store: Store<CanvasState>,
|
||||||
public canvasUtils: CanvasUtils,
|
public canvasUtils: CanvasUtils,
|
||||||
|
private canvasView: CanvasView,
|
||||||
private storage: Storage
|
private storage: Storage
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
@ -335,22 +341,45 @@ export class OperationControl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canCopy(selection: any): boolean {
|
canCopy(selection: d3.Selection<any, any, any, any>): boolean {
|
||||||
// TODO - isCopyable
|
return this.canvasUtils.isCopyable(selection);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
copy(selection: any): void {
|
copy(selection: d3.Selection<any, any, any, any>): void {
|
||||||
// TODO - copy
|
const components: CopyComponentRequest[] = [];
|
||||||
|
selection.each((d) => {
|
||||||
|
components.push({
|
||||||
|
id: d.id,
|
||||||
|
type: d.type,
|
||||||
|
uri: d.uri,
|
||||||
|
entity: d
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const origin = this.canvasUtils.getOrigin(selection);
|
||||||
|
const dimensions = this.canvasView.getSelectionBoundingClientRect(selection);
|
||||||
|
|
||||||
|
this.store.dispatch(
|
||||||
|
copy({
|
||||||
|
request: {
|
||||||
|
components,
|
||||||
|
origin,
|
||||||
|
dimensions
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
canPaste(selection: any): boolean {
|
canPaste(): boolean {
|
||||||
// TODO - isPastable
|
return this.canvasUtils.isPastable();
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
paste(selection: any): void {
|
paste(): void {
|
||||||
// TODO - paste
|
this.store.dispatch(
|
||||||
|
paste({
|
||||||
|
request: {}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
canGroup(selection: any): boolean {
|
canGroup(selection: any): boolean {
|
||||||
|
|
|
@ -19,14 +19,12 @@ import { Component, Input } from '@angular/core';
|
||||||
import { CdkDrag, CdkDragEnd } from '@angular/cdk/drag-drop';
|
import { CdkDrag, CdkDragEnd } from '@angular/cdk/drag-drop';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { CanvasState } from '../../../../state';
|
import { CanvasState } from '../../../../state';
|
||||||
import { INITIAL_SCALE, INITIAL_TRANSLATE } from '../../../../state/transform/transform.reducer';
|
|
||||||
import { selectTransform } from '../../../../state/transform/transform.selectors';
|
|
||||||
import { createComponentRequest, setDragging } from '../../../../state/flow/flow.actions';
|
import { createComponentRequest, setDragging } from '../../../../state/flow/flow.actions';
|
||||||
import { Client } from '../../../../../../service/client.service';
|
import { Client } from '../../../../../../service/client.service';
|
||||||
import { selectDragging } from '../../../../state/flow/flow.selectors';
|
import { selectDragging } from '../../../../state/flow/flow.selectors';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { Position } from '../../../../state/shared';
|
|
||||||
import { ComponentType } from '../../../../../../state/shared';
|
import { ComponentType } from '../../../../../../state/shared';
|
||||||
|
import { CanvasView } from '../../../../service/canvas-view.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'new-canvas-item',
|
selector: 'new-canvas-item',
|
||||||
|
@ -45,21 +43,11 @@ export class NewCanvasItem {
|
||||||
|
|
||||||
private hovering = false;
|
private hovering = false;
|
||||||
|
|
||||||
private scale: number = INITIAL_SCALE;
|
|
||||||
private translate: Position = INITIAL_TRANSLATE;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private client: Client,
|
private client: Client,
|
||||||
|
private canvasView: CanvasView,
|
||||||
private store: Store<CanvasState>
|
private store: Store<CanvasState>
|
||||||
) {
|
) {
|
||||||
this.store
|
|
||||||
.select(selectTransform)
|
|
||||||
.pipe(takeUntilDestroyed())
|
|
||||||
.subscribe((transform) => {
|
|
||||||
this.scale = transform.scale;
|
|
||||||
this.translate = transform.translate;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.store
|
this.store
|
||||||
.select(selectDragging)
|
.select(selectDragging)
|
||||||
.pipe(takeUntilDestroyed())
|
.pipe(takeUntilDestroyed())
|
||||||
|
@ -93,27 +81,10 @@ export class NewCanvasItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
onDragEnded(event: CdkDragEnd): void {
|
onDragEnded(event: CdkDragEnd): void {
|
||||||
const canvasContainer: any = document.getElementById('canvas-container');
|
|
||||||
const rect = canvasContainer.getBoundingClientRect();
|
|
||||||
const dropPoint = event.dropPoint;
|
const dropPoint = event.dropPoint;
|
||||||
|
|
||||||
// translate the drop point onto the canvas
|
const position = this.canvasView.getCanvasPosition(dropPoint);
|
||||||
const canvasDropPoint = {
|
if (position) {
|
||||||
x: dropPoint.x - rect.left,
|
|
||||||
y: dropPoint.y - rect.top
|
|
||||||
};
|
|
||||||
|
|
||||||
// if the position is over the canvas fire an event to add the new item
|
|
||||||
if (
|
|
||||||
canvasDropPoint.x >= 0 &&
|
|
||||||
canvasDropPoint.x < rect.width &&
|
|
||||||
canvasDropPoint.y >= 0 &&
|
|
||||||
canvasDropPoint.y < rect.height
|
|
||||||
) {
|
|
||||||
// adjust the x and y coordinates accordingly
|
|
||||||
const x = canvasDropPoint.x / this.scale - this.translate.x / this.scale;
|
|
||||||
const y = canvasDropPoint.y / this.scale - this.translate.y / this.scale;
|
|
||||||
|
|
||||||
this.store.dispatch(
|
this.store.dispatch(
|
||||||
createComponentRequest({
|
createComponentRequest({
|
||||||
request: {
|
request: {
|
||||||
|
@ -122,7 +93,7 @@ export class NewCanvasItem {
|
||||||
version: 0
|
version: 0
|
||||||
},
|
},
|
||||||
type: this.type,
|
type: this.type,
|
||||||
position: { x, y }
|
position
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -44,7 +44,11 @@
|
||||||
<b>History</b>
|
<b>History</b>
|
||||||
<ul class="px-2">
|
<ul class="px-2">
|
||||||
@for (previousValue of propertyHistory.previousValues; track previousValue) {
|
@for (previousValue of propertyHistory.previousValues; track previousValue) {
|
||||||
<li>{{ previousValue.previousValue }} - {{ previousValue.timestamp }} ({{ previousValue.userIdentity }})</li>
|
<li>
|
||||||
|
{{ previousValue.previousValue }} - {{ previousValue.timestamp }} ({{
|
||||||
|
previousValue.userIdentity
|
||||||
|
}})
|
||||||
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue