This is an automated email from the ASF dual-hosted git repository.
scottyaslan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 41e4779bc5 NIFI-13059: (#8661)
41e4779bc5 is described below
commit 41e4779bc560bf80e9e0fde253ae89525f115cf7
Author: Matt Gilman <[email protected]>
AuthorDate: Thu Apr 18 18:32:24 2024 -0400
NIFI-13059: (#8661)
- Adding support for copying and pasting on the canvas.
This closes #8661
---
.../service/canvas-context-menu.service.ts | 63 ++++--
.../flow-designer/service/canvas-utils.service.ts | 68 ++++++-
.../flow-designer/service/canvas-view.service.ts | 104 ++++++++--
.../pages/flow-designer/service/flow.service.ts | 28 ---
.../pages/flow-designer/service/snippet.service.ts | 118 ++++++++++++
.../pages/flow-designer/state/flow/flow.actions.ts | 12 ++
.../pages/flow-designer/state/flow/flow.effects.ts | 212 ++++++++++++++++-----
.../pages/flow-designer/state/flow/flow.reducer.ts | 48 +++++
.../flow-designer/state/flow/flow.selectors.ts | 2 +
.../app/pages/flow-designer/state/flow/index.ts | 27 ++-
.../operation-control.component.html | 4 +-
.../operation-control.component.ts | 49 ++++-
.../new-canvas-item/new-canvas-item.component.ts | 39 +---
.../property-tip/property-tip.component.html | 6 +-
14 files changed, 624 insertions(+), 156 deletions(-)
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
index ec00ba526d..ba80a35559 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
@@ -54,11 +54,14 @@ import {
startCurrentProcessGroup,
stopComponents,
stopCurrentProcessGroup,
- stopVersionControlRequest
+ stopVersionControlRequest,
+ copy,
+ paste
} from '../state/flow/flow.actions';
import { ComponentType } from '../../../state/shared';
import {
ConfirmStopVersionControlRequest,
+ CopyComponentRequest,
DeleteComponentRequest,
MoveComponentRequest,
OpenChangeVersionDialogRequest,
@@ -76,6 +79,7 @@ import { getComponentStateAndOpenDialog } from
'../../../state/component-state/c
import { navigateToComponentDocumentation } from
'../../../state/documentation/documentation.actions';
import * as d3 from 'd3';
import { Client } from '../../../service/client.service';
+import { CanvasView } from './canvas-view.service';
@Injectable({ providedIn: 'root' })
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);
},
clazz: 'fa icon-group',
text: 'Group',
- action: (selection: any) => {
+ action: (selection: d3.Selection<any, any, any, any>) => {
const moveComponents: MoveComponentRequest[] = [];
selection.each(function (d: any) {
moveComponents.push({
@@ -1212,25 +1216,55 @@ export class CanvasContextMenu implements
ContextMenuDefinitionProvider {
isSeparator: true
},
{
- condition: (selection: any) => {
- // TODO - isCopyable
- return false;
+ condition: (selection: d3.Selection<any, any, any, any>) => {
+ return this.canvasUtils.isCopyable(selection);
},
clazz: 'fa fa-copy',
text: 'Copy',
- action: () => {
- // TODO - copy
+ action: (selection: d3.Selection<any, any, any, any>) => {
+ 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) => {
- // TODO - isPastable
- return false;
+ condition: () => {
+ return this.canvasUtils.isPastable();
},
clazz: 'fa fa-paste',
text: 'Paste',
- action: () => {
- // TODO - paste
+ action: (selection: d3.Selection<any, any, any, any>, event)
=> {
+ 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(
private store: Store<CanvasState>,
private canvasUtils: CanvasUtils,
- private client: Client
+ private client: Client,
+ private canvasView: CanvasView
) {
this.allMenus = new Map<string, ContextMenuDefinition>();
this.allMenus.set(this.ROOT_MENU.id, this.ROOT_MENU);
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
index 37b4e7adb8..24ede50236 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
@@ -24,6 +24,7 @@ import {
selectBreadcrumbs,
selectCanvasPermissions,
selectConnections,
+ selectCopiedSnippet,
selectCurrentProcessGroupId,
selectParentProcessGroupId
} 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 { initialState as initialFlowConfigurationState } from
'../../../state/flow-configuration/flow-configuration.reducer';
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 { ComponentPortal } from '@angular/cdk/portal';
@@ -59,6 +60,7 @@ export class CanvasUtils {
private flowConfiguration: FlowConfiguration | null =
initialFlowConfigurationState.flowConfiguration;
private connections: any[] = [];
private breadcrumbs: BreadcrumbEntity | null = null;
+ private copiedSnippet: CopiedSnippet | null = null;
private readonly humanizeDuration: Humanizer;
@@ -118,6 +120,13 @@ export class CanvasUtils {
.subscribe((breadcrumbs) => {
this.breadcrumbs = breadcrumbs;
});
+
+ this.store
+ .select(selectCopiedSnippet)
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((copiedSnippet) => {
+ this.copiedSnippet = copiedSnippet;
+ });
}
public hasDownstream(selection: any): boolean {
@@ -807,14 +816,13 @@ export class CanvasUtils {
*
* @argument {selection} selection The selection
*/
- getOrigin(selection: any): Position {
- const self: CanvasUtils = this;
+ public getOrigin(selection: d3.Selection<any, any, any, any>): Position {
let x: number | undefined;
let y: number | undefined;
- selection.each(function (this: any, d: any) {
- const selected: any = d3.select(this);
- if (!self.isConnection(selected)) {
+ selection.each((d, i, nodes) => {
+ const selected: any = d3.select(nodes[i]);
+ if (!this.isConnection(selected)) {
if (x == null || d.position.x < x) {
x = d.position.x;
}
@@ -831,6 +839,54 @@ export class CanvasUtils {
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.
*
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.ts
index bb8a844836..8614e150ed 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.ts
@@ -31,6 +31,7 @@ import { RemoteProcessGroupManager } from
'./manager/remote-process-group-manage
import { ConnectionManager } from './manager/connection-manager.service';
import { deselectAllComponents } from '../state/flow/flow.actions';
import { CanvasUtils } from './canvas-utils.service';
+import { Position } from '../state/shared';
@Injectable({
providedIn: 'root'
@@ -185,7 +186,6 @@ export class CanvasView {
public isSelectedComponentOnScreen(): boolean {
const canvasContainer: any =
document.getElementById('canvas-container');
-
if (canvasContainer == null) {
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 {
const self: CanvasView = this;
const canvasContainer: any =
document.getElementById('canvas-container');
@@ -354,11 +403,6 @@ export class CanvasView {
}
public centerSelectedComponents(allowTransition: boolean): void {
- const canvasContainer: any =
document.getElementById('canvas-container');
- if (canvasContainer == null) {
- return;
- }
-
const selection: any = this.canvasUtils.getSelection();
if (selection.empty()) {
return;
@@ -368,7 +412,7 @@ export class CanvasView {
if (selection.size() === 1) {
bbox = this.getSingleSelectionBoundingClientRect(selection);
} else {
- bbox = this.getBulkSelectionBoundingClientRect(selection,
canvasContainer);
+ bbox = this.getSelectionBoundingClientRect(selection);
}
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.
*/
- private getBulkSelectionBoundingClientRect(selection: any,
canvasContainer: any): any {
- const canvasBoundingBox: any = canvasContainer.getBoundingClientRect();
+ public getSelectionBoundingClientRect(selection: any): any {
+ let yOffset = 0;
+
+ const canvasContainer: any =
document.getElementById('canvas-container');
+ if (canvasContainer) {
+ yOffset = canvasContainer.getBoundingClientRect().top;
+ }
const initialBBox: any = {
x: Number.MAX_VALUE,
@@ -430,9 +479,9 @@ export class CanvasView {
// normalize the bounding box with scale and translate
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.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.height = bbox.bottom - bbox.y;
@@ -442,6 +491,37 @@ export class CanvasView {
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 {
let scale: number = this.k;
if (boundingBox.scale != null) {
@@ -460,7 +540,7 @@ export class CanvasView {
* @param {type} boundingBox
* @returns {number[]}
*/
- private getCenterForBoundingBox(boundingBox: any): number[] {
+ public getCenterForBoundingBox(boundingBox: any): number[] {
let scale: number = this.k;
if (boundingBox.scale != null) {
scale = boundingBox.scale;
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts
index 79be4a518c..08ee7b6c05 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts
@@ -35,7 +35,6 @@ import {
ReplayLastProvenanceEventRequest,
RunOnceRequest,
SaveToVersionControlRequest,
- Snippet,
StartComponentRequest,
StartProcessGroupRequest,
StopComponentRequest,
@@ -257,33 +256,6 @@ export class FlowService implements
PropertyDescriptorRetriever {
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> {
return
this.httpClient.post(`${FlowService.API}/provenance-events/latest/replays`,
request);
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/snippet.service.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/snippet.service.ts
new file mode 100644
index 0000000000..411ea5e975
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/snippet.service.ts
@@ -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
});
+ }
+}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
index 030d9a01fc..7e38808152 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
@@ -21,6 +21,8 @@ import {
ChangeVersionDialogRequest,
ComponentEntity,
ConfirmStopVersionControlRequest,
+ CopiedSnippet,
+ CopyRequest,
CreateComponentRequest,
CreateComponentResponse,
CreateConnection,
@@ -64,6 +66,8 @@ import {
OpenGroupComponentsDialogRequest,
OpenLocalChangesDialogRequest,
OpenSaveVersionDialogRequest,
+ PasteRequest,
+ PasteResponse,
RefreshRemoteProcessGroupRequest,
ReplayLastProvenanceEventRequest,
RpgManageRemotePortsRequest,
@@ -476,6 +480,14 @@ export const moveComponents = createAction(
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
*/
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
index 4e6f79db87..cac1991f97 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
@@ -39,14 +39,17 @@ import {
tap
} from 'rxjs';
import {
+ CopyComponentRequest,
CreateProcessGroupDialogRequest,
DeleteComponentResponse,
GroupComponentsDialogRequest,
ImportFromRegistryDialogRequest,
LoadProcessGroupRequest,
LoadProcessGroupResponse,
+ MoveComponentRequest,
SaveVersionDialogRequest,
SaveVersionRequest,
+ SelectedComponent,
Snippet,
StopVersionControlRequest,
StopVersionControlResponse,
@@ -61,6 +64,7 @@ import { Action, Store } from '@ngrx/store';
import {
selectAnySelectedComponentIds,
selectChangeVersionRequest,
+ selectCopiedSnippet,
selectCurrentParameterContext,
selectCurrentProcessGroupId,
selectMaxZIndex,
@@ -119,6 +123,8 @@ import { LocalChangesDialog } from
'../../ui/canvas/items/flow/local-changes-dia
import { ClusterConnectionService } from
'../../../../service/cluster-connection.service';
import { ExtensionTypesService } from
'../../../../service/extension-types.service';
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()
export class FlowEffects {
@@ -134,6 +140,7 @@ export class FlowEffects {
private birdseyeView: BirdseyeView,
private connectionManager: ConnectionManager,
private clusterConnectionService: ClusterConnectionService,
+ private snippetService: SnippetService,
private router: Router,
private dialog: MatDialog,
private propertyTableHelperService: PropertyTableHelperService,
@@ -1816,53 +1823,11 @@ export class FlowEffects {
map((action) => action.request),
concatLatestFrom(() =>
this.store.select(selectCurrentProcessGroupId)),
mergeMap(([request, processGroupId]) => {
- const components: any[] = request.components;
-
- const snippet: Snippet = 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
- );
+ const components: MoveComponentRequest[] = request.components;
+ const snippet = this.snippetService.marshalSnippet(components,
processGroupId);
- return from(this.flowService.createSnippet(snippet)).pipe(
- switchMap((response) =>
this.flowService.moveSnippet(response.snippet.id, request.groupId)),
+ return from(this.snippetService.createSnippet(snippet)).pipe(
+ switchMap((response) =>
this.snippetService.moveSnippet(response.snippet.id, request.groupId)),
map(() => {
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(() =>
this.actions$.pipe(
ofType(FlowActions.deleteComponents),
@@ -1962,8 +2078,8 @@ export class FlowEffects {
} as Snippet
);
- return from(this.flowService.createSnippet(snippet)).pipe(
- switchMap((response) =>
this.flowService.deleteSnippet(response.snippet.id)),
+ return
from(this.snippetService.createSnippet(snippet)).pipe(
+ switchMap((response) =>
this.snippetService.deleteSnippet(response.snippet.id)),
map(() => {
const deleteResponses: DeleteComponentResponse[] =
[];
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
index 67a8aa0bc8..7eb089b8ba 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
@@ -20,6 +20,7 @@ import {
changeVersionComplete,
changeVersionSuccess,
clearFlowApiError,
+ copySuccess,
createComponentComplete,
createComponentSuccess,
createConnection,
@@ -41,6 +42,7 @@ import {
loadProcessorSuccess,
loadRemoteProcessGroupSuccess,
navigateWithoutTransform,
+ pasteSuccess,
pollChangeVersionSuccess,
pollRevertChangesSuccess,
requestRefreshRemoteProcessGroup,
@@ -143,6 +145,7 @@ export const initialState: FlowState = {
parameterProviderBulletins: [],
reportingTaskBulletins: []
},
+ copiedSnippet: null,
dragging: false,
saving: 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 }) => ({
...state,
dragging
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
index 01a75fc101..536b5378f8 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
@@ -42,6 +42,8 @@ export const selectCurrentProcessGroupId =
createSelector(selectFlowState, (stat
export const selectRefreshRpgDetails = createSelector(selectFlowState, (state:
FlowState) => state.refreshRpgDetails);
+export const selectCopiedSnippet = createSelector(selectFlowState, (state:
FlowState) => state.copiedSnippet);
+
export const selectCurrentParameterContext = createSelector(
selectFlowState,
(state: FlowState) => state.flow.processGroupFlow.parameterContext
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
index 092cab1e54..392c267b55 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
@@ -423,18 +423,36 @@ export interface UpdatePositionsRequest {
connectionUpdates: UpdateComponentRequest[];
}
-export interface MoveComponentRequest {
+export interface SnippetComponentRequest {
id: string;
uri: string;
type: ComponentType;
entity: any;
}
+export interface MoveComponentRequest extends SnippetComponentRequest {}
+
export interface MoveComponentsRequest {
components: MoveComponentRequest[];
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 {
id: string;
uri: string;
@@ -490,6 +508,12 @@ export interface Snippet {
};
}
+export interface CopiedSnippet {
+ snippet: Snippet;
+ origin: Position;
+ dimensions: any;
+}
+
/*
Tooltips
*/
@@ -613,6 +637,7 @@ export interface FlowState {
error: string | null;
versionSaving: boolean;
changeVersionRequest: FlowUpdateRequestEntity | null;
+ copiedSnippet: CopiedSnippet | null;
status: 'pending' | 'loading' | 'error' | 'success';
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.html
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.html
index 7dc0e816a1..03f21089f9 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.html
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.html
@@ -116,8 +116,8 @@
color="primary"
class="mr-2"
type="button"
- [disabled]="!canPaste(selection)"
- (click)="paste(selection)">
+ [disabled]="!canPaste()"
+ (click)="paste()">
<i class="fa fa-paste"></i>
</button>
<button
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.ts
index 853c0bd1d9..6eac3f66f6 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.ts
@@ -17,11 +17,13 @@
import { Component, Input } from '@angular/core';
import {
+ copy,
deleteComponents,
getParameterContextsAndOpenGroupComponentsDialog,
navigateToEditComponent,
navigateToEditCurrentProcessGroup,
navigateToManageComponentPolicies,
+ paste,
setOperationCollapsed,
startComponents,
startCurrentProcessGroup,
@@ -34,6 +36,7 @@ import { CanvasUtils } from
'../../../../service/canvas-utils.service';
import { initialState } from '../../../../state/flow/flow.reducer';
import { Storage } from '../../../../../../service/storage.service';
import {
+ CopyComponentRequest,
DeleteComponentRequest,
MoveComponentRequest,
StartComponentRequest,
@@ -43,6 +46,8 @@ import {
import { BreadcrumbEntity } from '../../../../state/shared';
import { ComponentType } from '../../../../../../state/shared';
import { MatButtonModule } from '@angular/material/button';
+import * as d3 from 'd3';
+import { CanvasView } from '../../../../service/canvas-view.service';
@Component({
selector: 'operation-control',
@@ -63,6 +68,7 @@ export class OperationControl {
constructor(
private store: Store<CanvasState>,
public canvasUtils: CanvasUtils,
+ private canvasView: CanvasView,
private storage: Storage
) {
try {
@@ -335,22 +341,45 @@ export class OperationControl {
}
}
- canCopy(selection: any): boolean {
- // TODO - isCopyable
- return false;
+ canCopy(selection: d3.Selection<any, any, any, any>): boolean {
+ return this.canvasUtils.isCopyable(selection);
}
- copy(selection: any): void {
- // TODO - copy
+ copy(selection: d3.Selection<any, any, any, any>): void {
+ 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 {
- // TODO - isPastable
- return false;
+ canPaste(): boolean {
+ return this.canvasUtils.isPastable();
}
- paste(selection: any): void {
- // TODO - paste
+ paste(): void {
+ this.store.dispatch(
+ paste({
+ request: {}
+ })
+ );
}
canGroup(selection: any): boolean {
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/new-canvas-item/new-canvas-item.component.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/new-canvas-item/new-canvas-item.component.ts
index 45fa0d4594..6a42dc247a 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/new-canvas-item/new-canvas-item.component.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/new-canvas-item/new-canvas-item.component.ts
@@ -19,14 +19,12 @@ import { Component, Input } from '@angular/core';
import { CdkDrag, CdkDragEnd } from '@angular/cdk/drag-drop';
import { Store } from '@ngrx/store';
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 { Client } from '../../../../../../service/client.service';
import { selectDragging } from '../../../../state/flow/flow.selectors';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { Position } from '../../../../state/shared';
import { ComponentType } from '../../../../../../state/shared';
+import { CanvasView } from '../../../../service/canvas-view.service';
@Component({
selector: 'new-canvas-item',
@@ -45,21 +43,11 @@ export class NewCanvasItem {
private hovering = false;
- private scale: number = INITIAL_SCALE;
- private translate: Position = INITIAL_TRANSLATE;
-
constructor(
private client: Client,
+ private canvasView: CanvasView,
private store: Store<CanvasState>
) {
- this.store
- .select(selectTransform)
- .pipe(takeUntilDestroyed())
- .subscribe((transform) => {
- this.scale = transform.scale;
- this.translate = transform.translate;
- });
-
this.store
.select(selectDragging)
.pipe(takeUntilDestroyed())
@@ -93,27 +81,10 @@ export class NewCanvasItem {
}
onDragEnded(event: CdkDragEnd): void {
- const canvasContainer: any =
document.getElementById('canvas-container');
- const rect = canvasContainer.getBoundingClientRect();
const dropPoint = event.dropPoint;
- // translate the drop point onto the canvas
- const canvasDropPoint = {
- 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;
-
+ const position = this.canvasView.getCanvasPosition(dropPoint);
+ if (position) {
this.store.dispatch(
createComponentRequest({
request: {
@@ -122,7 +93,7 @@ export class NewCanvasItem {
version: 0
},
type: this.type,
- position: { x, y }
+ position
}
})
);
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/tooltips/property-tip/property-tip.component.html
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/tooltips/property-tip/property-tip.component.html
index f094092019..b2a14e92bb 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/tooltips/property-tip/property-tip.component.html
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/tooltips/property-tip/property-tip.component.html
@@ -44,7 +44,11 @@
<b>History</b>
<ul class="px-2">
@for (previousValue of propertyHistory.previousValues;
track previousValue) {
- <li>{{ previousValue.previousValue }} - {{
previousValue.timestamp }} ({{ previousValue.userIdentity }})</li>
+ <li>
+ {{ previousValue.previousValue }} - {{
previousValue.timestamp }} ({{
+ previousValue.userIdentity
+ }})
+ </li>
}
</ul>
</div>