NIFI-13534: Only reloading the flow when the browser tab is visible. … (#9344)

* NIFI-13534: Only reloading the flow when the browser tab is visible. This prevents attempts to re-render the SVG when the browser tab is hidden.

* NIFI-13534: Destroying the canvas view (and managers) and birdeyes view when the corresponding components are destroyed.
- When document visibility changes, only reloading the flow when the canvas is still initialized.

* NIFI-13534: Completing subjects and cleaning up tooltip positioning strategies which referenced html elements.

This closes #9344
This commit is contained in:
Matt Gilman 2024-10-10 14:24:26 -04:00 committed by GitHub
parent f48d60abf5
commit ade260266e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 955 additions and 592 deletions

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component } from '@angular/core';
import { Component, OnDestroy } from '@angular/core';
import {
GuardsCheckEnd,
GuardsCheckStart,
@ -34,16 +34,30 @@ import { popBackNavigation, pushBackNavigation } from './state/navigation/naviga
import { filter, map, tap } from 'rxjs';
import { concatLatestFrom } from '@ngrx/operators';
import { selectBackNavigation } from './state/navigation/navigation.selectors';
import { documentVisibilityChanged } from './state/document-visibility/document-visibility.actions';
import { DocumentVisibility } from './state/document-visibility';
@Component({
selector: 'nifi',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
export class AppComponent implements OnDestroy {
title = 'nifi';
guardLoading = true;
documentVisibilityListener: () => void = () => {
this.store.dispatch(
documentVisibilityChanged({
change: {
documentVisibility:
document.visibilityState === 'visible' ? DocumentVisibility.Visible : DocumentVisibility.Hidden,
changedTimestamp: Date.now()
}
})
);
};
constructor(
private router: Router,
private storage: Storage,
@ -102,5 +116,11 @@ export class AppComponent {
this.themingService.toggleTheme(e.matches, theme);
});
}
document.addEventListener('visibilitychange', this.documentVisibilityListener);
}
ngOnDestroy(): void {
document.removeEventListener('visibilitychange', this.documentVisibilityListener);
}
}

View File

@ -23,6 +23,7 @@ import {
EventEmitter,
inject,
Input,
OnDestroy,
Output,
ViewChild
} from '@angular/core';
@ -47,7 +48,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
templateUrl: './bulletin-board-list.component.html',
styleUrls: ['./bulletin-board-list.component.scss']
})
export class BulletinBoardList implements AfterViewInit {
export class BulletinBoardList implements AfterViewInit, OnDestroy {
filterTerm = '';
filterColumn: 'message' | 'name' | 'id' | 'groupId' = 'message';
filterForm: FormGroup;
@ -105,6 +106,10 @@ export class BulletinBoardList implements AfterViewInit {
});
}
ngOnDestroy(): void {
this.bulletinsChanged$.complete();
}
private scrollToBottom() {
if (this.scroll) {
this.scroll.nativeElement.scroll({

View File

@ -41,14 +41,16 @@ import { ComponentEntityWithDimensions } from '../state/flow';
export class BirdseyeView {
private destroyRef = inject(DestroyRef);
private birdseyeGroup: any;
private componentGroup: any;
private birdseyeGroup: any = null;
private componentGroup: any = null;
private navigationCollapsed: boolean = initialState.navigationCollapsed;
private k: number = INITIAL_SCALE;
private x: number = INITIAL_TRANSLATE.x;
private y: number = INITIAL_TRANSLATE.y;
private initialized = false;
constructor(
private store: Store<CanvasState>,
private canvasView: CanvasView,
@ -146,11 +148,13 @@ export class BirdseyeView {
y: 0
})
.call(brush);
this.initialized = true;
}
public refresh(): void {
// if navigation is collapsed there is no need to perform the calculations below
if (this.navigationCollapsed) {
// do not refresh if the component is not initialized or navigation is collapsed
if (!this.initialized || this.navigationCollapsed) {
return;
}
@ -347,4 +351,17 @@ export class BirdseyeView {
context.restore();
}
public destroy(): void {
this.initialized = false;
this.navigationCollapsed = initialState.navigationCollapsed;
this.k = INITIAL_SCALE;
this.x = INITIAL_TRANSLATE.x;
this.y = INITIAL_TRANSLATE.y;
this.birdseyeGroup = null;
this.componentGroup = null;
}
}

View File

@ -43,7 +43,7 @@ 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 { CopiedSnippet, VersionControlInformation } from '../state/flow';
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { Overlay, OverlayRef, PositionStrategy } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
@Injectable({
@ -1190,6 +1190,7 @@ export class CanvasUtils {
let openTimer = -1;
const overlay = this.overlay;
let overlayRef: OverlayRef | null = null;
let positionStrategy: PositionStrategy | null = null;
selection
.on('mouseenter', function (this: any) {
@ -1198,24 +1199,23 @@ export class CanvasUtils {
}
if (!overlayRef) {
const positionStrategy = overlay
.position()
.flexibleConnectedTo(d3.select(this).node())
.withPositions([
{
originX: 'end',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top',
offsetX: 8,
offsetY: 8
}
])
.withPush(true);
openTimer = window.setTimeout(() => {
overlayRef = overlay.create({ positionStrategy });
positionStrategy = overlay
.position()
.flexibleConnectedTo(d3.select(this).node())
.withPositions([
{
originX: 'end',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top',
offsetX: 8,
offsetY: 8
}
])
.withPush(true);
overlayRef = overlay.create({ positionStrategy });
const tooltipReference = overlayRef.attach(new ComponentPortal(type));
tooltipReference.setInput('data', tooltipData);
@ -1230,6 +1230,12 @@ export class CanvasUtils {
overlayRef?.detach();
overlayRef?.dispose();
overlayRef = null;
if (positionStrategy?.detach) {
positionStrategy.detach();
}
positionStrategy?.dispose();
positionStrategy = null;
});
}, NiFiCommon.TOOLTIP_DELAY_OPEN_MILLIS);
}
@ -1243,6 +1249,12 @@ export class CanvasUtils {
overlayRef?.detach();
overlayRef?.dispose();
overlayRef = null;
if (positionStrategy?.detach) {
positionStrategy.detach();
}
positionStrategy?.dispose();
positionStrategy = null;
}, NiFiCommon.TOOLTIP_DELAY_OPEN_MILLIS);
});
}

View File

@ -54,6 +54,8 @@ export class CanvasView {
private birdseyeTranslateInProgress = false;
private allowTransition = false;
private canvasInitialized: boolean = false;
constructor(
private store: Store<CanvasState>,
private canvasUtils: CanvasUtils,
@ -64,41 +66,7 @@ export class CanvasView {
private funnelManager: FunnelManager,
private labelManager: LabelManager,
private connectionManager: ConnectionManager
) {}
public init(svg: any, canvas: any): void {
WebFont.load({
custom: {
families: ['Inter', 'flowfont', 'FontAwesome']
},
active: function () {
// re-render once the fonts have loaded, without the fonts
// positions of elements on the canvas may be incorrect
self.processorManager.render();
self.processGroupManager.render();
self.remoteProcessGroupManager.render();
self.portManager.render();
self.labelManager.render();
self.funnelManager.render();
self.connectionManager.render();
}
});
this.svg = svg;
this.canvas = canvas;
this.k = INITIAL_SCALE;
this.x = INITIAL_TRANSLATE.x;
this.y = INITIAL_TRANSLATE.y;
this.labelManager.init();
this.funnelManager.init();
this.portManager.init();
this.remoteProcessGroupManager.init();
this.processGroupManager.init();
this.processorManager.init();
this.connectionManager.init();
) {
const self: CanvasView = this;
let refreshed: Promise<void> | null;
let panning = false;
@ -166,9 +134,47 @@ export class CanvasView {
// reset the panning flag
panning = false;
});
}
public init(svg: any, canvas: any): void {
const self: CanvasView = this;
WebFont.load({
custom: {
families: ['Inter', 'flowfont', 'FontAwesome']
},
active: function () {
// re-render once the fonts have loaded, without the fonts
// positions of elements on the canvas may be incorrect
self.processorManager.render();
self.processGroupManager.render();
self.remoteProcessGroupManager.render();
self.portManager.render();
self.labelManager.render();
self.funnelManager.render();
self.connectionManager.render();
}
});
this.svg = svg;
this.canvas = canvas;
this.k = INITIAL_SCALE;
this.x = INITIAL_TRANSLATE.x;
this.y = INITIAL_TRANSLATE.y;
this.labelManager.init();
this.funnelManager.init();
this.portManager.init();
this.remoteProcessGroupManager.init();
this.processGroupManager.init();
this.processorManager.init();
this.connectionManager.init();
// add the behavior to the canvas and disable dbl click zoom
this.svg.call(this.behavior).on('dblclick.zoom', null);
this.canvasInitialized = true;
}
// filters zoom events as programmatically modifying the translate or scale now triggers the handlers
@ -776,4 +782,27 @@ export class CanvasView {
}
});
}
public isCanvasInitialized(): boolean {
return this.canvasInitialized;
}
public destroy(): void {
this.canvasInitialized = false;
this.labelManager.destroy();
this.funnelManager.destroy();
this.portManager.destroy();
this.remoteProcessGroupManager.destroy();
this.processGroupManager.destroy();
this.processorManager.destroy();
this.connectionManager.destroy();
this.k = INITIAL_SCALE;
this.x = INITIAL_TRANSLATE.x;
this.y = INITIAL_TRANSLATE.y;
this.svg = null;
this.canvas = null;
}
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { DestroyRef, inject, Injectable } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';
import { CanvasState } from '../../state';
import { CanvasUtils } from '../canvas-utils.service';
@ -39,12 +39,11 @@ import {
updateComponent,
updateConnection
} from '../../state/flow/flow.actions';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { UnorderedListTip } from '../../../../ui/common/tooltips/unordered-list-tip/unordered-list-tip.component';
import { ComponentType, SelectOption } from 'libs/shared/src';
import { Dimension, Position } from '../../state/shared';
import { loadBalanceStrategies, UpdateComponentRequest } from '../../state/flow';
import { filter, switchMap } from 'rxjs';
import { filter, Subject, switchMap, takeUntil } from 'rxjs';
import { NiFiCommon } from '@nifi/shared';
import { QuickSelectBehavior } from '../behavior/quick-select-behavior.service';
import { ClusterConnectionService } from '../../../../service/cluster-connection.service';
@ -57,8 +56,8 @@ export class ConnectionRenderOptions {
@Injectable({
providedIn: 'root'
})
export class ConnectionManager {
private destroyRef = inject(DestroyRef);
export class ConnectionManager implements OnDestroy {
private destroyed$: Subject<boolean> = new Subject();
private static readonly DIMENSIONS: Dimension = {
width: 224,
@ -80,7 +79,7 @@ export class ConnectionManager {
private static readonly SNAP_ALIGNMENT_PIXELS: number = 8;
private connections: [] = [];
private connectionContainer: any;
private connectionContainer: any = null;
private transitionRequired = false;
private currentProcessGroupId: string = initialState.id;
private scale: number = INITIAL_SCALE;
@ -102,7 +101,359 @@ export class ConnectionManager {
private transitionBehavior: TransitionBehavior,
private quickSelectBehavior: QuickSelectBehavior,
private clusterConnectionService: ClusterConnectionService
) {}
) {
const self: ConnectionManager = this;
// define the line generator
this.lineGenerator = d3
.line()
.x(function (d: any) {
return d.x;
})
.y(function (d: any) {
return d.y;
})
.curve(d3.curveLinear);
// handle bend point drag events
this.bendPointDrag = d3
.drag()
.on('start', function (this: any, event) {
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging start
const connection: any = d3.select(this.parentNode);
const connectionData: any = connection.datum();
connectionData.dragging = true;
})
.on('drag', function (this: any, event, d: any) {
const connection: any = d3.select(this.parentNode);
const connectionData: any = connection.datum();
if (connectionData.dragging) {
self.snapEnabled = !event.sourceEvent.shiftKey;
d.x = self.snapEnabled
? Math.round(event.x / ConnectionManager.SNAP_ALIGNMENT_PIXELS) *
ConnectionManager.SNAP_ALIGNMENT_PIXELS
: event.x;
d.y = self.snapEnabled
? Math.round(event.y / ConnectionManager.SNAP_ALIGNMENT_PIXELS) *
ConnectionManager.SNAP_ALIGNMENT_PIXELS
: event.y;
// redraw this connection
self.updateConnections(d3.select(this.parentNode), {
updatePath: true,
updateLabel: false
});
}
})
.on('end', function (this: any, event) {
const connection: any = d3.select(this.parentNode);
const connectionData: any = connection.datum();
if (connectionData.dragging) {
const bends: Position[] = connection.selectAll('rect.midpoint').data();
// ensure the bend lengths are the same
if (bends.length === connectionData.component.bends.length) {
// determine if the bend points have moved
let different = false;
for (let i = 0; i < bends.length && !different; i++) {
if (
bends[i].x !== connectionData.component.bends[i].x ||
bends[i].y !== connectionData.component.bends[i].y
) {
different = true;
}
}
// only save the updated bends if necessary
if (different) {
self.save(
connectionData,
{
id: connectionData.id,
bends: bends
},
{
bends: [...connectionData.component.bends]
}
);
}
}
}
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging complete
connectionData.dragging = false;
});
// handle endpoint drag events
this.endpointDrag = d3
.drag()
.on('start', function (this: any, event, d: any) {
// indicate that end point dragging has begun
d.endPointDragging = true;
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging start
const connection: any = d3.select(this.parentNode);
const connectionData: any = connection.datum();
connectionData.dragging = true;
})
.on('drag', function (this: any, event, d: any) {
const connection: any = d3.select(this.parentNode);
const connectionData: any = connection.datum();
if (connectionData.dragging) {
d.x = event.x - 8;
d.y = event.y - 8;
// ensure the new destination is valid
d3.select('g.hover').classed('connectable-destination', function () {
return self.canvasUtils.isValidConnectionDestination(d3.select(this));
});
// redraw this connection
self.updateConnections(d3.select(this.parentNode), {
updatePath: true,
updateLabel: false
});
}
})
.on('end', function (this: any, event, d: any) {
// indicate that end point dragging as stopped
d.endPointDragging = false;
// get the corresponding connection
const connection: any = d3.select(this.parentNode);
const connectionData: any = connection.datum();
if (connectionData.dragging) {
// attempt to select a new destination
const destination: any = d3.select('g.connectable-destination');
// resets the connection if we're not over a new destination
if (destination.empty()) {
self.updateConnections(connection, {
updatePath: true,
updateLabel: false
});
} else {
const destinationData: any = destination.datum();
// prompt for the new port if appropriate
if (
self.canvasUtils.isProcessGroup(destination) ||
self.canvasUtils.isRemoteProcessGroup(destination)
) {
// when the new destination is a group, show the edit connection dialog
// to allow the user to select the desired port
self.store.dispatch(
openEditConnectionDialog({
request: {
type: ComponentType.Connection,
uri: connectionData.uri,
entity: { ...connectionData },
newDestination: {
type: destinationData.type,
groupId: destinationData.id,
name: destinationData.permissions.canRead
? destinationData.component.name
: destinationData.id
}
}
})
);
} else {
const destinationType: string = self.canvasUtils.getConnectableTypeForDestination(
destinationData.type
);
const payload: any = {
revision: self.client.getRevision(connectionData),
disconnectedNodeAcknowledged:
self.clusterConnectionService.isDisconnectionAcknowledged(),
component: {
id: connectionData.id,
destination: {
id: destinationData.id,
groupId: self.currentProcessGroupId,
type: destinationType
}
}
};
// if this is a self loop and there are less than 2 bends, add them
if (connectionData.bends.length < 2 && connectionData.sourceId === destinationData.id) {
const rightCenter: any = {
x: destinationData.position.x + destinationData.dimensions.width,
y: destinationData.position.y + destinationData.dimensions.height / 2
};
payload.component.bends = [];
payload.component.bends.push({
x: rightCenter.x + ConnectionManager.SELF_LOOP_X_OFFSET,
y: rightCenter.y - ConnectionManager.SELF_LOOP_Y_OFFSET
});
payload.component.bends.push({
x: rightCenter.x + ConnectionManager.SELF_LOOP_X_OFFSET,
y: rightCenter.y + ConnectionManager.SELF_LOOP_Y_OFFSET
});
}
self.store.dispatch(
updateConnection({
request: {
id: connectionData.id,
type: ComponentType.Connection,
uri: connectionData.uri,
previousDestination: connectionData.component.destination,
payload,
errorStrategy: 'snackbar'
}
})
);
}
}
}
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging complete
connectionData.dragging = false;
});
// label drag behavior
this.labelDrag = d3
.drag()
.on('start', function (event, d: any) {
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging start
d.dragging = true;
})
.on('drag', function (this: any, event, d: any) {
if (d.dragging && d.bends.length > 1) {
// get the dragged component
let drag: any = d3.select('rect.label-drag');
// lazily create the drag selection box
if (drag.empty()) {
const connectionLabel: any = d3.select(this).select('rect.body');
const position: Position = self.getLabelPosition(connectionLabel);
const width: number = ConnectionManager.DIMENSIONS.width;
const height: number = connectionLabel.attr('height');
// create a selection box for the move
drag = d3
.select('#canvas')
.append('rect')
.attr('x', position.x)
.attr('y', position.y)
.attr('class', 'label-drag')
.attr('width', width)
.attr('height', height)
.attr('stroke-width', function () {
return 1 / self.scale;
})
.attr('stroke-dasharray', function () {
return 4 / self.scale;
})
.datum({
x: position.x,
y: position.y,
width: width,
height: height
});
} else {
// update the position of the drag selection
drag.attr('x', function (d: any) {
d.x += event.dx;
return d.x;
}).attr('y', function (d: any) {
d.y += event.dy;
return d.y;
});
}
// calculate the current point
const datum: any = drag.datum();
const currentPoint: Position = {
x: datum.x + datum.width / 2,
y: datum.y + datum.height / 2
};
let closestBendIndex = -1;
let minDistance: number;
d.bends.forEach((bend: Position, i: number) => {
const bendPoint: Position = {
x: bend.x,
y: bend.y
};
// get the distance
const distance: number = self.distanceBetweenPoints(currentPoint, bendPoint);
// see if its the minimum
if (closestBendIndex === -1 || distance < minDistance) {
closestBendIndex = i;
minDistance = distance;
}
});
// record the closest bend
d.labelIndex = closestBendIndex;
// refresh the connection
self.updateConnections(d3.select(this.parentNode), {
updatePath: true,
updateLabel: false
});
}
})
.on('end', function (this: any, event, d: any) {
if (d.dragging && d.bends.length > 1) {
// get the drag selection
const drag: any = d3.select('rect.label-drag');
// ensure we found a drag selection
if (!drag.empty()) {
// remove the drag selection
drag.remove();
}
// only save if necessary
if (d.labelIndex !== d.component.labelIndex) {
self.save(
d,
{
id: d.id,
labelIndex: d.labelIndex
},
{
labelIndex: d.component.labelIndex
}
);
}
}
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging complete
d.dragging = false;
});
}
/**
* Gets the position of the label for the specified connection.
@ -1823,367 +2174,18 @@ export class ConnectionManager {
}
public init(): void {
const self: ConnectionManager = this;
this.connectionContainer = d3
.select('#canvas')
.append('g')
.attr('pointer-events', 'stroke')
.attr('class', 'connections');
// define the line generator
this.lineGenerator = d3
.line()
.x(function (d: any) {
return d.x;
})
.y(function (d: any) {
return d.y;
})
.curve(d3.curveLinear);
// handle bend point drag events
this.bendPointDrag = d3
.drag()
.on('start', function (this: any, event) {
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging start
const connection: any = d3.select(this.parentNode);
const connectionData: any = connection.datum();
connectionData.dragging = true;
})
.on('drag', function (this: any, event, d: any) {
const connection: any = d3.select(this.parentNode);
const connectionData: any = connection.datum();
if (connectionData.dragging) {
self.snapEnabled = !event.sourceEvent.shiftKey;
d.x = self.snapEnabled
? Math.round(event.x / ConnectionManager.SNAP_ALIGNMENT_PIXELS) *
ConnectionManager.SNAP_ALIGNMENT_PIXELS
: event.x;
d.y = self.snapEnabled
? Math.round(event.y / ConnectionManager.SNAP_ALIGNMENT_PIXELS) *
ConnectionManager.SNAP_ALIGNMENT_PIXELS
: event.y;
// redraw this connection
self.updateConnections(d3.select(this.parentNode), {
updatePath: true,
updateLabel: false
});
}
})
.on('end', function (this: any, event) {
const connection: any = d3.select(this.parentNode);
const connectionData: any = connection.datum();
if (connectionData.dragging) {
const bends: Position[] = connection.selectAll('rect.midpoint').data();
// ensure the bend lengths are the same
if (bends.length === connectionData.component.bends.length) {
// determine if the bend points have moved
let different = false;
for (let i = 0; i < bends.length && !different; i++) {
if (
bends[i].x !== connectionData.component.bends[i].x ||
bends[i].y !== connectionData.component.bends[i].y
) {
different = true;
}
}
// only save the updated bends if necessary
if (different) {
self.save(
connectionData,
{
id: connectionData.id,
bends: bends
},
{
bends: [...connectionData.component.bends]
}
);
}
}
}
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging complete
connectionData.dragging = false;
});
// handle endpoint drag events
this.endpointDrag = d3
.drag()
.on('start', function (this: any, event, d: any) {
// indicate that end point dragging has begun
d.endPointDragging = true;
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging start
const connection: any = d3.select(this.parentNode);
const connectionData: any = connection.datum();
connectionData.dragging = true;
})
.on('drag', function (this: any, event, d: any) {
const connection: any = d3.select(this.parentNode);
const connectionData: any = connection.datum();
if (connectionData.dragging) {
d.x = event.x - 8;
d.y = event.y - 8;
// ensure the new destination is valid
d3.select('g.hover').classed('connectable-destination', function () {
return self.canvasUtils.isValidConnectionDestination(d3.select(this));
});
// redraw this connection
self.updateConnections(d3.select(this.parentNode), {
updatePath: true,
updateLabel: false
});
}
})
.on('end', function (this: any, event, d: any) {
// indicate that end point dragging as stopped
d.endPointDragging = false;
// get the corresponding connection
const connection: any = d3.select(this.parentNode);
const connectionData: any = connection.datum();
if (connectionData.dragging) {
// attempt to select a new destination
const destination: any = d3.select('g.connectable-destination');
// resets the connection if we're not over a new destination
if (destination.empty()) {
self.updateConnections(connection, {
updatePath: true,
updateLabel: false
});
} else {
const destinationData: any = destination.datum();
// prompt for the new port if appropriate
if (
self.canvasUtils.isProcessGroup(destination) ||
self.canvasUtils.isRemoteProcessGroup(destination)
) {
// when the new destination is a group, show the edit connection dialog
// to allow the user to select the desired port
self.store.dispatch(
openEditConnectionDialog({
request: {
type: ComponentType.Connection,
uri: connectionData.uri,
entity: { ...connectionData },
newDestination: {
type: destinationData.type,
groupId: destinationData.id,
name: destinationData.permissions.canRead
? destinationData.component.name
: destinationData.id
}
}
})
);
} else {
const destinationType: string = self.canvasUtils.getConnectableTypeForDestination(
destinationData.type
);
const payload: any = {
revision: self.client.getRevision(connectionData),
disconnectedNodeAcknowledged:
self.clusterConnectionService.isDisconnectionAcknowledged(),
component: {
id: connectionData.id,
destination: {
id: destinationData.id,
groupId: self.currentProcessGroupId,
type: destinationType
}
}
};
// if this is a self loop and there are less than 2 bends, add them
if (connectionData.bends.length < 2 && connectionData.sourceId === destinationData.id) {
const rightCenter: any = {
x: destinationData.position.x + destinationData.dimensions.width,
y: destinationData.position.y + destinationData.dimensions.height / 2
};
payload.component.bends = [];
payload.component.bends.push({
x: rightCenter.x + ConnectionManager.SELF_LOOP_X_OFFSET,
y: rightCenter.y - ConnectionManager.SELF_LOOP_Y_OFFSET
});
payload.component.bends.push({
x: rightCenter.x + ConnectionManager.SELF_LOOP_X_OFFSET,
y: rightCenter.y + ConnectionManager.SELF_LOOP_Y_OFFSET
});
}
self.store.dispatch(
updateConnection({
request: {
id: connectionData.id,
type: ComponentType.Connection,
uri: connectionData.uri,
previousDestination: connectionData.component.destination,
payload,
errorStrategy: 'snackbar'
}
})
);
}
}
}
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging complete
connectionData.dragging = false;
});
// label drag behavior
this.labelDrag = d3
.drag()
.on('start', function (event, d: any) {
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging start
d.dragging = true;
})
.on('drag', function (this: any, event, d: any) {
if (d.dragging && d.bends.length > 1) {
// get the dragged component
let drag: any = d3.select('rect.label-drag');
// lazily create the drag selection box
if (drag.empty()) {
const connectionLabel: any = d3.select(this).select('rect.body');
const position: Position = self.getLabelPosition(connectionLabel);
const width: number = ConnectionManager.DIMENSIONS.width;
const height: number = connectionLabel.attr('height');
// create a selection box for the move
drag = d3
.select('#canvas')
.append('rect')
.attr('x', position.x)
.attr('y', position.y)
.attr('class', 'label-drag')
.attr('width', width)
.attr('height', height)
.attr('stroke-width', function () {
return 1 / self.scale;
})
.attr('stroke-dasharray', function () {
return 4 / self.scale;
})
.datum({
x: position.x,
y: position.y,
width: width,
height: height
});
} else {
// update the position of the drag selection
drag.attr('x', function (d: any) {
d.x += event.dx;
return d.x;
}).attr('y', function (d: any) {
d.y += event.dy;
return d.y;
});
}
// calculate the current point
const datum: any = drag.datum();
const currentPoint: Position = {
x: datum.x + datum.width / 2,
y: datum.y + datum.height / 2
};
let closestBendIndex = -1;
let minDistance: number;
d.bends.forEach((bend: Position, i: number) => {
const bendPoint: Position = {
x: bend.x,
y: bend.y
};
// get the distance
const distance: number = self.distanceBetweenPoints(currentPoint, bendPoint);
// see if its the minimum
if (closestBendIndex === -1 || distance < minDistance) {
closestBendIndex = i;
minDistance = distance;
}
});
// record the closest bend
d.labelIndex = closestBendIndex;
// refresh the connection
self.updateConnections(d3.select(this.parentNode), {
updatePath: true,
updateLabel: false
});
}
})
.on('end', function (this: any, event, d: any) {
if (d.dragging && d.bends.length > 1) {
// get the drag selection
const drag: any = d3.select('rect.label-drag');
// ensure we found a drag selection
if (!drag.empty()) {
// remove the drag selection
drag.remove();
}
// only save if necessary
if (d.labelIndex !== d.component.labelIndex) {
self.save(
d,
{
id: d.id,
labelIndex: d.labelIndex
},
{
labelIndex: d.component.labelIndex
}
);
}
}
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging complete
d.dragging = false;
});
this.store
.select(selectConnections)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(
filter(() => this.connectionContainer !== null),
takeUntil(this.destroyed$)
)
.subscribe((connections) => {
this.set(connections);
});
@ -2192,8 +2194,9 @@ export class ConnectionManager {
.select(selectFlowLoadingStatus)
.pipe(
filter((status) => status === 'success'),
filter(() => this.connectionContainer !== null),
switchMap(() => this.store.select(selectAnySelectedComponentIds)),
takeUntilDestroyed(this.destroyRef)
takeUntil(this.destroyed$)
)
.subscribe((selected) => {
this.connectionContainer.selectAll('g.connection').classed('selected', function (d: any) {
@ -2203,26 +2206,35 @@ export class ConnectionManager {
this.store
.select(selectTransitionRequired)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(takeUntil(this.destroyed$))
.subscribe((transitionRequired) => {
this.transitionRequired = transitionRequired;
});
this.store
.select(selectCurrentProcessGroupId)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(takeUntil(this.destroyed$))
.subscribe((currentProcessGroupId) => {
this.currentProcessGroupId = currentProcessGroupId;
});
this.store
.select(selectTransform)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(takeUntil(this.destroyed$))
.subscribe((transform) => {
this.scale = transform.scale;
});
}
public destroy(): void {
this.connectionContainer = null;
this.destroyed$.next(true);
}
ngOnDestroy(): void {
this.destroyed$.complete();
}
private set(connections: any): void {
// update the connections
this.connections = connections.map((connection: any) => {

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { DestroyRef, inject, Injectable } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';
import * as d3 from 'd3';
import { PositionBehavior } from '../behavior/position-behavior.service';
import { CanvasState } from '../../state';
@ -28,14 +28,13 @@ import {
selectAnySelectedComponentIds,
selectTransitionRequired
} from '../../state/flow/flow.selectors';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Dimension } from '../../state/shared';
import { ComponentType } from 'libs/shared/src';
import { filter, switchMap } from 'rxjs';
import { filter, Subject, switchMap, takeUntil } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class FunnelManager {
private destroyRef = inject(DestroyRef);
export class FunnelManager implements OnDestroy {
private destroyed$: Subject<boolean> = new Subject();
private dimensions: Dimension = {
width: 48,
@ -43,7 +42,7 @@ export class FunnelManager {
};
private funnels: [] = [];
private funnelContainer: any;
private funnelContainer: any = null;
private transitionRequired = false;
constructor(
@ -138,7 +137,10 @@ export class FunnelManager {
this.store
.select(selectFunnels)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(
filter(() => this.funnelContainer !== null),
takeUntil(this.destroyed$)
)
.subscribe((funnels) => {
this.set(funnels);
});
@ -147,8 +149,9 @@ export class FunnelManager {
.select(selectFlowLoadingStatus)
.pipe(
filter((status) => status === 'success'),
filter(() => this.funnelContainer !== null),
switchMap(() => this.store.select(selectAnySelectedComponentIds)),
takeUntilDestroyed(this.destroyRef)
takeUntil(this.destroyed$)
)
.subscribe((selected) => {
this.funnelContainer.selectAll('g.funnel').classed('selected', function (d: any) {
@ -158,12 +161,21 @@ export class FunnelManager {
this.store
.select(selectTransitionRequired)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(takeUntil(this.destroyed$))
.subscribe((transitionRequired) => {
this.transitionRequired = transitionRequired;
});
}
public destroy(): void {
this.funnelContainer = null;
this.destroyed$.next(true);
}
ngOnDestroy(): void {
this.destroyed$.complete();
}
private set(funnels: any): void {
// update the funnels
this.funnels = funnels.map((funnel: any) => {

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { DestroyRef, inject, Injectable } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';
import { CanvasState } from '../../state';
import { CanvasUtils } from '../canvas-utils.service';
@ -31,19 +31,18 @@ import {
} from '../../state/flow/flow.selectors';
import { Client } from '../../../../service/client.service';
import { updateComponent } from '../../state/flow/flow.actions';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { QuickSelectBehavior } from '../behavior/quick-select-behavior.service';
import { ComponentType } from 'libs/shared/src';
import { UpdateComponentRequest } from '../../state/flow';
import { filter, switchMap } from 'rxjs';
import { filter, Subject, switchMap, takeUntil } from 'rxjs';
import { NiFiCommon } from '@nifi/shared';
import { ClusterConnectionService } from '../../../../service/cluster-connection.service';
@Injectable({
providedIn: 'root'
})
export class LabelManager {
private destroyRef = inject(DestroyRef);
export class LabelManager implements OnDestroy {
private destroyed$: Subject<boolean> = new Subject();
public static readonly INITIAL_WIDTH: number = 148;
public static readonly INITIAL_HEIGHT: number = 148;
@ -52,7 +51,7 @@ export class LabelManager {
private static readonly SNAP_ALIGNMENT_PIXELS: number = 8;
private labels: [] = [];
private labelContainer: any;
private labelContainer: any = null;
private transitionRequired = false;
private labelPointDrag: any;
@ -68,7 +67,106 @@ export class LabelManager {
private selectableBehavior: SelectableBehavior,
private quickSelectBehavior: QuickSelectBehavior,
private editableBehavior: EditableBehavior
) {}
) {
const self: LabelManager = this;
// handle bend point drag events
this.labelPointDrag = d3
.drag()
.on('start', function (this: any, event) {
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging start
const label = d3.select(this.parentNode);
const labelData: any = label.datum();
labelData.dragging = true;
})
.on('drag', function (this: any, event) {
const label = d3.select(this.parentNode);
const labelData: any = label.datum();
if (labelData.dragging) {
// update the dimensions and ensure they are still within bounds
// snap between aligned sizes unless the user is holding shift
self.snapEnabled = !event.sourceEvent.shiftKey;
labelData.dimensions.width = Math.max(
LabelManager.MIN_WIDTH,
self.snapEnabled
? Math.round(event.x / LabelManager.SNAP_ALIGNMENT_PIXELS) *
LabelManager.SNAP_ALIGNMENT_PIXELS
: event.x
);
labelData.dimensions.height = Math.max(
LabelManager.MIN_HEIGHT,
self.snapEnabled
? Math.round(event.y / LabelManager.SNAP_ALIGNMENT_PIXELS) *
LabelManager.SNAP_ALIGNMENT_PIXELS
: event.y
);
// redraw this connection
self.updateLabels(label);
}
})
.on('end', function (this: any, event) {
const label = d3.select(this.parentNode);
const labelData: any = label.datum();
if (labelData.dragging) {
// determine if the width has changed
let different = false;
const widthSet = !!labelData.component.width;
if (widthSet || labelData.dimensions.width !== labelData.component.width) {
different = true;
}
// determine if the height has changed
const heightSet = !!labelData.component.height;
if ((!different && heightSet) || labelData.dimensions.height !== labelData.component.height) {
different = true;
}
// only save the updated dimensions if necessary
if (different) {
const updateLabel: UpdateComponentRequest = {
id: labelData.id,
type: ComponentType.Label,
uri: labelData.uri,
payload: {
revision: self.client.getRevision(labelData),
disconnectedNodeAcknowledged:
self.clusterConnectionService.isDisconnectionAcknowledged(),
component: {
id: labelData.id,
width: labelData.dimensions.width,
height: labelData.dimensions.height
}
},
restoreOnFailure: {
dimensions: {
width: widthSet ? labelData.component.width : LabelManager.INITIAL_WIDTH,
height: heightSet ? labelData.component.height : LabelManager.INITIAL_HEIGHT
}
},
errorStrategy: 'snackbar'
};
self.store.dispatch(
updateComponent({
request: updateLabel
})
);
}
}
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging complete
labelData.dragging = false;
});
}
private select() {
return this.labelContainer.selectAll('g.label').data(this.labels, function (d: any) {
@ -273,7 +371,10 @@ export class LabelManager {
this.store
.select(selectLabels)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(
filter(() => this.labelContainer !== null),
takeUntil(this.destroyed$)
)
.subscribe((labels) => {
this.set(labels);
});
@ -282,8 +383,9 @@ export class LabelManager {
.select(selectFlowLoadingStatus)
.pipe(
filter((status) => status === 'success'),
filter(() => this.labelContainer !== null),
switchMap(() => this.store.select(selectAnySelectedComponentIds)),
takeUntilDestroyed(this.destroyRef)
takeUntil(this.destroyed$)
)
.subscribe((selected) => {
this.labelContainer.selectAll('g.label').classed('selected', function (d: any) {
@ -293,109 +395,19 @@ export class LabelManager {
this.store
.select(selectTransitionRequired)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(takeUntil(this.destroyed$))
.subscribe((transitionRequired) => {
this.transitionRequired = transitionRequired;
});
}
const self: LabelManager = this;
public destroy(): void {
this.labelContainer = null;
this.destroyed$.next(true);
}
// handle bend point drag events
this.labelPointDrag = d3
.drag()
.on('start', function (this: any, event) {
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging start
const label = d3.select(this.parentNode);
const labelData: any = label.datum();
labelData.dragging = true;
})
.on('drag', function (this: any, event) {
const label = d3.select(this.parentNode);
const labelData: any = label.datum();
if (labelData.dragging) {
// update the dimensions and ensure they are still within bounds
// snap between aligned sizes unless the user is holding shift
self.snapEnabled = !event.sourceEvent.shiftKey;
labelData.dimensions.width = Math.max(
LabelManager.MIN_WIDTH,
self.snapEnabled
? Math.round(event.x / LabelManager.SNAP_ALIGNMENT_PIXELS) *
LabelManager.SNAP_ALIGNMENT_PIXELS
: event.x
);
labelData.dimensions.height = Math.max(
LabelManager.MIN_HEIGHT,
self.snapEnabled
? Math.round(event.y / LabelManager.SNAP_ALIGNMENT_PIXELS) *
LabelManager.SNAP_ALIGNMENT_PIXELS
: event.y
);
// redraw this connection
self.updateLabels(label);
}
})
.on('end', function (this: any, event) {
const label = d3.select(this.parentNode);
const labelData: any = label.datum();
if (labelData.dragging) {
// determine if the width has changed
let different = false;
const widthSet = !!labelData.component.width;
if (widthSet || labelData.dimensions.width !== labelData.component.width) {
different = true;
}
// determine if the height has changed
const heightSet = !!labelData.component.height;
if ((!different && heightSet) || labelData.dimensions.height !== labelData.component.height) {
different = true;
}
// only save the updated dimensions if necessary
if (different) {
const updateLabel: UpdateComponentRequest = {
id: labelData.id,
type: ComponentType.Label,
uri: labelData.uri,
payload: {
revision: self.client.getRevision(labelData),
disconnectedNodeAcknowledged:
self.clusterConnectionService.isDisconnectionAcknowledged(),
component: {
id: labelData.id,
width: labelData.dimensions.width,
height: labelData.dimensions.height
}
},
restoreOnFailure: {
dimensions: {
width: widthSet ? labelData.component.width : LabelManager.INITIAL_WIDTH,
height: heightSet ? labelData.component.height : LabelManager.INITIAL_HEIGHT
}
},
errorStrategy: 'snackbar'
};
self.store.dispatch(
updateComponent({
request: updateLabel
})
);
}
}
// stop further propagation
event.sourceEvent.stopPropagation();
// indicate dragging complete
labelData.dragging = false;
});
ngOnDestroy(): void {
this.destroyed$.complete();
}
private set(labels: any): void {

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { DestroyRef, inject, Injectable } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';
import { CanvasState } from '../../state';
import { Store } from '@ngrx/store';
import { CanvasUtils } from '../canvas-utils.service';
@ -29,20 +29,19 @@ import {
selectPorts,
selectTransitionRequired
} from '../../state/flow/flow.selectors';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { QuickSelectBehavior } from '../behavior/quick-select-behavior.service';
import { TextTip, NiFiCommon } from '@nifi/shared';
import { ValidationErrorsTip } from '../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component';
import { Dimension } from '../../state/shared';
import { ComponentType } from 'libs/shared/src';
import { filter, switchMap } from 'rxjs';
import { filter, Subject, switchMap, takeUntil } from 'rxjs';
import { renderConnectionsForComponent } from '../../state/flow/flow.actions';
@Injectable({
providedIn: 'root'
})
export class PortManager {
private destroyRef = inject(DestroyRef);
export class PortManager implements OnDestroy {
private destroyed$: Subject<boolean> = new Subject();
private portDimensions: Dimension = {
width: 240,
@ -57,7 +56,7 @@ export class PortManager {
private static readonly OFFSET_VALUE: number = 25;
private ports: [] = [];
private portContainer: any;
private portContainer: any = null;
private transitionRequired = false;
constructor(
@ -472,7 +471,10 @@ export class PortManager {
this.store
.select(selectPorts)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(
filter(() => this.portContainer !== null),
takeUntil(this.destroyed$)
)
.subscribe((ports) => {
this.set(ports);
});
@ -481,8 +483,9 @@ export class PortManager {
.select(selectFlowLoadingStatus)
.pipe(
filter((status) => status === 'success'),
filter(() => this.portContainer !== null),
switchMap(() => this.store.select(selectAnySelectedComponentIds)),
takeUntilDestroyed(this.destroyRef)
takeUntil(this.destroyed$)
)
.subscribe((selected) => {
this.portContainer.selectAll('g.input-port, g.output-port').classed('selected', function (d: any) {
@ -492,12 +495,21 @@ export class PortManager {
this.store
.select(selectTransitionRequired)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(takeUntil(this.destroyed$))
.subscribe((transitionRequired) => {
this.transitionRequired = transitionRequired;
});
}
public destroy(): void {
this.portContainer = null;
this.destroyed$.next(true);
}
ngOnDestroy(): void {
this.destroyed$.complete();
}
private set(ports: any): void {
// update the ports
this.ports = ports.map((port: any) => {

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { DestroyRef, inject, Injectable } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';
import { CanvasState } from '../../state';
import { Store } from '@ngrx/store';
import { PositionBehavior } from '../behavior/position-behavior.service';
@ -30,18 +30,17 @@ import {
} from '../../state/flow/flow.selectors';
import { CanvasUtils } from '../canvas-utils.service';
import { enterProcessGroup } from '../../state/flow/flow.actions';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { VersionControlTip } from '../../ui/common/tooltips/version-control-tip/version-control-tip.component';
import { Dimension } from '../../state/shared';
import { ComponentType } from 'libs/shared/src';
import { filter, switchMap } from 'rxjs';
import { filter, Subject, switchMap, takeUntil } from 'rxjs';
import { NiFiCommon, TextTip } from '@nifi/shared';
@Injectable({
providedIn: 'root'
})
export class ProcessGroupManager {
private destroyRef = inject(DestroyRef);
export class ProcessGroupManager implements OnDestroy {
private destroyed$: Subject<boolean> = new Subject();
private dimensions: Dimension = {
width: 384,
@ -54,7 +53,7 @@ export class ProcessGroupManager {
private static readonly PREVIEW_NAME_LENGTH: number = 30;
private processGroups: [] = [];
private processGroupContainer: any;
private processGroupContainer: any = null;
private transitionRequired = false;
constructor(
@ -1321,7 +1320,10 @@ export class ProcessGroupManager {
this.store
.select(selectProcessGroups)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(
filter(() => this.processGroupContainer !== null),
takeUntil(this.destroyed$)
)
.subscribe((processGroups) => {
this.set(processGroups);
});
@ -1330,8 +1332,9 @@ export class ProcessGroupManager {
.select(selectFlowLoadingStatus)
.pipe(
filter((status) => status === 'success'),
filter(() => this.processGroupContainer !== null),
switchMap(() => this.store.select(selectAnySelectedComponentIds)),
takeUntilDestroyed(this.destroyRef)
takeUntil(this.destroyed$)
)
.subscribe((selected) => {
this.processGroupContainer.selectAll('g.process-group').classed('selected', function (d: any) {
@ -1341,12 +1344,21 @@ export class ProcessGroupManager {
this.store
.select(selectTransitionRequired)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(takeUntil(this.destroyed$))
.subscribe((transitionRequired) => {
this.transitionRequired = transitionRequired;
});
}
public destroy(): void {
this.processGroupContainer = null;
this.destroyed$.next(true);
}
ngOnDestroy(): void {
this.destroyed$.complete();
}
private set(processGroups: any): void {
// update the process groups
this.processGroups = processGroups.map((processGroup: any) => {

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { DestroyRef, inject, Injectable } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';
import { CanvasState } from '../../state';
import { CanvasUtils } from '../canvas-utils.service';
@ -29,19 +29,18 @@ import {
selectAnySelectedComponentIds,
selectTransitionRequired
} from '../../state/flow/flow.selectors';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { QuickSelectBehavior } from '../behavior/quick-select-behavior.service';
import { ValidationErrorsTip } from '../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component';
import { TextTip, NiFiCommon } from '@nifi/shared';
import { Dimension } from '../../state/shared';
import { ComponentType } from 'libs/shared/src';
import { filter, switchMap } from 'rxjs';
import { filter, Subject, switchMap, takeUntil } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ProcessorManager {
private destroyRef = inject(DestroyRef);
export class ProcessorManager implements OnDestroy {
private destroyed$: Subject<boolean> = new Subject();
private dimensions: Dimension = {
width: 352,
@ -51,7 +50,7 @@ export class ProcessorManager {
private static readonly PREVIEW_NAME_LENGTH: number = 25;
private processors: [] = [];
private processorContainer: any;
private processorContainer: any = null;
private transitionRequired = false;
constructor(
@ -831,7 +830,10 @@ export class ProcessorManager {
this.store
.select(selectProcessors)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(
filter(() => this.processorContainer !== null),
takeUntil(this.destroyed$)
)
.subscribe((processors) => {
this.set(processors);
});
@ -840,8 +842,9 @@ export class ProcessorManager {
.select(selectFlowLoadingStatus)
.pipe(
filter((status) => status === 'success'),
filter(() => this.processorContainer !== null),
switchMap(() => this.store.select(selectAnySelectedComponentIds)),
takeUntilDestroyed(this.destroyRef)
takeUntil(this.destroyed$)
)
.subscribe((selected) => {
this.processorContainer.selectAll('g.processor').classed('selected', function (d: any) {
@ -851,12 +854,21 @@ export class ProcessorManager {
this.store
.select(selectTransitionRequired)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(takeUntil(this.destroyed$))
.subscribe((transitionRequired) => {
this.transitionRequired = transitionRequired;
});
}
public destroy(): void {
this.processorContainer = null;
this.destroyed$.next(true);
}
ngOnDestroy(): void {
this.destroyed$.complete();
}
private set(processors: any): void {
// update the processors
this.processors = processors.map((processor: any) => {

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { DestroyRef, inject, Injectable } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';
import { CanvasState } from '../../state';
import { CanvasUtils } from '../canvas-utils.service';
@ -29,19 +29,18 @@ import {
selectAnySelectedComponentIds,
selectTransitionRequired
} from '../../state/flow/flow.selectors';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { QuickSelectBehavior } from '../behavior/quick-select-behavior.service';
import { ValidationErrorsTip } from '../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component';
import { Dimension } from '../../state/shared';
import { ComponentType } from 'libs/shared/src';
import { filter, switchMap } from 'rxjs';
import { filter, Subject, switchMap, takeUntil } from 'rxjs';
import { NiFiCommon, TextTip } from '@nifi/shared';
@Injectable({
providedIn: 'root'
})
export class RemoteProcessGroupManager {
private destroyRef = inject(DestroyRef);
export class RemoteProcessGroupManager implements OnDestroy {
private destroyed$: Subject<boolean> = new Subject();
private dimensions: Dimension = {
width: 384,
@ -51,7 +50,7 @@ export class RemoteProcessGroupManager {
private static readonly PREVIEW_NAME_LENGTH: number = 30;
private remoteProcessGroups: [] = [];
private remoteProcessGroupContainer: any;
private remoteProcessGroupContainer: any = null;
private transitionRequired = false;
constructor(
@ -673,7 +672,10 @@ export class RemoteProcessGroupManager {
this.store
.select(selectRemoteProcessGroups)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(
filter(() => this.remoteProcessGroupContainer !== null),
takeUntil(this.destroyed$)
)
.subscribe((remoteProcessGroups) => {
this.set(remoteProcessGroups);
});
@ -682,8 +684,9 @@ export class RemoteProcessGroupManager {
.select(selectFlowLoadingStatus)
.pipe(
filter((status) => status === 'success'),
filter(() => this.remoteProcessGroupContainer !== null),
switchMap(() => this.store.select(selectAnySelectedComponentIds)),
takeUntilDestroyed(this.destroyRef)
takeUntil(this.destroyed$)
)
.subscribe((selected) => {
this.remoteProcessGroupContainer
@ -695,12 +698,21 @@ export class RemoteProcessGroupManager {
this.store
.select(selectTransitionRequired)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(takeUntil(this.destroyed$))
.subscribe((transitionRequired) => {
this.transitionRequired = transitionRequired;
});
}
public destroy(): void {
this.remoteProcessGroupContainer = null;
this.destroyed$.next(true);
}
ngOnDestroy(): void {
this.destroyed$.complete();
}
private set(remoteProcessGroups: any): void {
// update the remote process groups
this.remoteProcessGroups = remoteProcessGroups.map((remoteProcessGroup: any) => {

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { DestroyRef, inject, Injectable } from '@angular/core';
import { FlowService } from '../../service/flow.service';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { concatLatestFrom } from '@ngrx/operators';
@ -154,9 +154,15 @@ import { VerifyPropertiesRequestContext } from '../../../../state/property-verif
import { BackNavigation } from '../../../../state/navigation';
import { Storage, NiFiCommon } from '@nifi/shared';
import { resetPollingFlowAnalysis } from '../flow-analysis/flow-analysis.actions';
import { selectDocumentVisibilityState } from '../../../../state/document-visibility/document-visibility.selectors';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DocumentVisibility } from '../../../../state/document-visibility';
@Injectable()
export class FlowEffects {
private destroyRef = inject(DestroyRef);
private lastReload: number = 0;
constructor(
private actions$: Actions,
private store: Store<NiFiState>,
@ -177,7 +183,22 @@ export class FlowEffects {
private parameterHelperService: ParameterHelperService,
private extensionTypesService: ExtensionTypesService,
private errorHelper: ErrorHelper
) {}
) {
this.store
.select(selectDocumentVisibilityState)
.pipe(
takeUntilDestroyed(this.destroyRef),
filter((documentVisibility) => documentVisibility.documentVisibility === DocumentVisibility.Visible),
filter(() => this.canvasView.isCanvasInitialized()),
filter(
(documentVisibility) =>
documentVisibility.changedTimestamp - this.lastReload > 30 * NiFiCommon.MILLIS_PER_SECOND
)
)
.subscribe(() => {
this.store.dispatch(FlowActions.reloadFlow());
});
}
reloadFlow$ = createEffect(() =>
this.actions$.pipe(
@ -185,6 +206,8 @@ export class FlowEffects {
throttleTime(1000),
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
switchMap(([, processGroupId]) => {
this.lastReload = Date.now();
return of(
FlowActions.loadProcessGroup({
request: {
@ -266,6 +289,8 @@ export class FlowEffects {
takeUntil(this.actions$.pipe(ofType(FlowActions.stopProcessGroupPolling)))
)
),
concatLatestFrom(() => this.store.select(selectDocumentVisibilityState)),
filter(([, documentVisibility]) => documentVisibility.documentVisibility === DocumentVisibility.Visible),
switchMap(() => of(FlowActions.reloadFlow()))
)
);

View File

@ -625,6 +625,8 @@ export class Canvas implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.store.dispatch(resetFlowState());
this.store.dispatch(stopProcessGroupPolling());
this.canvasView.destroy();
}
private processKeyboardEvents(event: KeyboardEvent): boolean {

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { BirdseyeView } from '../../../../../service/birdseye-view.service';
@Component({
@ -24,7 +24,7 @@ import { BirdseyeView } from '../../../../../service/birdseye-view.service';
templateUrl: './birdseye.component.html',
styleUrls: ['./birdseye.component.scss']
})
export class Birdseye implements OnInit {
export class Birdseye implements OnInit, OnDestroy {
constructor(private birdseyeView: BirdseyeView) {}
ngOnInit(): void {
@ -32,4 +32,8 @@ export class Birdseye implements OnInit {
this.birdseyeView.init(birdseye);
this.birdseyeView.refresh();
}
ngOnDestroy(): void {
this.birdseyeView.destroy();
}
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { AfterViewInit, Component, DestroyRef, inject, OnDestroy, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import {
selectProcessorIdFromRoute,
@ -50,7 +50,9 @@ import { NodeSearchResult } from '../../../../state/cluster-summary';
templateUrl: './processor-status-listing.component.html',
styleUrls: ['./processor-status-listing.component.scss']
})
export class ProcessorStatusListing implements AfterViewInit {
export class ProcessorStatusListing implements AfterViewInit, OnDestroy {
private destroyRef = inject(DestroyRef);
processorStatusSnapshots$ = this.store.select(selectProcessorStatusSnapshots);
loadedTimestamp$ = this.store.select(selectSummaryListingLoadedTimestamp);
summaryListingStatus$ = this.store.select(selectSummaryListingStatus);
@ -103,19 +105,24 @@ export class ProcessorStatusListing implements AfterViewInit {
combineLatest([this.processorStatusSnapshots$, this.loadedTimestamp$])
.pipe(
filter(([processors, ts]) => !!processors && !this.isInitialLoading(ts)),
delay(0)
delay(0),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => {
this.subject.next();
});
this.subject.subscribe(() => {
this.subject.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
if (this.table) {
this.table.paginator = this.paginator;
}
});
}
ngOnDestroy(): void {
this.subject.complete();
}
isInitialLoading(loadedTimestamp: string): boolean {
return loadedTimestamp == initialState.loadedTimestamp;
}

View File

@ -0,0 +1,26 @@
/*
* 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 { createAction, props } from '@ngrx/store';
import { DocumentVisibilityChange } from './index';
export const documentVisibilityChanged = createAction(
'[Document Visibility] Document Visibility Changed',
props<{
change: DocumentVisibilityChange;
}>()
);

View File

@ -0,0 +1,34 @@
/*
* 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 { createReducer, on } from '@ngrx/store';
import { DocumentVisibility, DocumentVisibilityState } from './index';
import { documentVisibilityChanged } from './document-visibility.actions';
export const initialState: DocumentVisibilityState = {
documentVisibility: DocumentVisibility.Visible,
changedTimestamp: Date.now()
};
export const documentVisibilityReducer = createReducer(
initialState,
on(documentVisibilityChanged, (state, { change }) => ({
...state,
documentVisibility: change.documentVisibility,
changedTimestamp: change.changedTimestamp
}))
);

View File

@ -0,0 +1,22 @@
/*
* 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 { createFeatureSelector } from '@ngrx/store';
import { documentVisibilityFeatureKey, DocumentVisibilityState } from './index';
export const selectDocumentVisibilityState =
createFeatureSelector<DocumentVisibilityState>(documentVisibilityFeatureKey);

View File

@ -0,0 +1,33 @@
/*
* 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.
*/
export const documentVisibilityFeatureKey = 'documentVisibility';
export enum DocumentVisibility {
Visible = 'VISIBLE',
Hidden = 'HIDDEN'
}
export interface DocumentVisibilityChange {
documentVisibility: DocumentVisibility;
changedTimestamp: number;
}
export interface DocumentVisibilityState {
documentVisibility: DocumentVisibility;
changedTimestamp: number;
}

View File

@ -45,6 +45,8 @@ import { navigationFeatureKey, NavigationState } from './navigation';
import { navigationReducer } from './navigation/navigation.reducer';
import { bannerTextFeatureKey, BannerTextState } from './banner-text';
import { bannerTextReducer } from './banner-text/banner-text.reducer';
import { documentVisibilityFeatureKey, DocumentVisibilityState } from './document-visibility';
import { documentVisibilityReducer } from './document-visibility/document-visibility.reducer';
export interface NiFiState {
[DEFAULT_ROUTER_FEATURENAME]: RouterReducerState;
@ -60,6 +62,7 @@ export interface NiFiState {
[controllerServiceStateFeatureKey]: ControllerServiceState;
[systemDiagnosticsFeatureKey]: SystemDiagnosticsState;
[componentStateFeatureKey]: ComponentStateState;
[documentVisibilityFeatureKey]: DocumentVisibilityState;
[clusterSummaryFeatureKey]: ClusterSummaryState;
[propertyVerificationFeatureKey]: PropertyVerificationState;
}
@ -78,6 +81,7 @@ export const rootReducers: ActionReducerMap<NiFiState> = {
[controllerServiceStateFeatureKey]: controllerServiceStateReducer,
[systemDiagnosticsFeatureKey]: systemDiagnosticsReducer,
[componentStateFeatureKey]: componentStateReducer,
[documentVisibilityFeatureKey]: documentVisibilityReducer,
[clusterSummaryFeatureKey]: clusterSummaryReducer,
[propertyVerificationFeatureKey]: propertyVerificationReducer
};

View File

@ -15,10 +15,11 @@
* limitations under the License.
*/
import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Component, DestroyRef, inject, Input, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { AsyncPipe } from '@angular/common';
import { CdkMenu, CdkMenuItem, CdkMenuTrigger } from '@angular/cdk/menu';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export interface ContextMenuDefinitionProvider {
getMenu(menuId: string): ContextMenuDefinition | undefined;
@ -57,13 +58,15 @@ export interface ContextMenuDefinition {
imports: [AsyncPipe, CdkMenu, CdkMenuItem, CdkMenuTrigger],
styleUrls: ['./context-menu.component.scss']
})
export class ContextMenu implements OnInit {
export class ContextMenu implements OnInit, OnDestroy {
private destroyRef = inject(DestroyRef);
@Input() menuProvider!: ContextMenuDefinitionProvider;
@Input() menuId: string | undefined;
@ViewChild('menu', { static: true }) menu!: TemplateRef<any>;
private showFocused: Subject<boolean> = new Subject();
showFocused$: Observable<boolean> = this.showFocused.asObservable();
showFocused$: Observable<boolean> = this.showFocused.asObservable().pipe(takeUntilDestroyed(this.destroyRef));
isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
getMenuItems(menuId: string | undefined): ContextMenuItemDefinition[] {
@ -142,4 +145,8 @@ export class ContextMenu implements OnInit {
menuItemClicked(menuItem: ContextMenuItemDefinition, event: MouseEvent) {
this.menuProvider.menuItemClicked(menuItem, event);
}
ngOnDestroy(): void {
this.showFocused.complete();
}
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
import { FieldDescriptor } from '../../../../state/status-history';
import * as d3 from 'd3';
@ -31,7 +31,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
templateUrl: './status-history-chart.component.html',
styleUrls: ['./status-history-chart.component.scss']
})
export class StatusHistoryChart {
export class StatusHistoryChart implements OnDestroy {
private _instances!: Instance[];
private _selectedDescriptor: FieldDescriptor | null = null;
private _visibleInstances: VisibleInstances = {};
@ -686,4 +686,9 @@ export class StatusHistoryChart {
});
return totalValue / snapshotCount;
}
ngOnDestroy(): void {
this.nodeStats$.complete();
this.clusterStats$.complete();
}
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, ElementRef, EventEmitter, Input, Output } from '@angular/core';
import { Component, ElementRef, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
import { MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { CdkDrag, CdkDragEnd, CdkDragMove } from '@angular/cdk/drag-drop';
@ -29,7 +29,7 @@ import { AsyncPipe } from '@angular/common';
templateUrl: './resizable.component.html',
styleUrls: ['./resizable.component.scss']
})
export class Resizable {
export class Resizable implements OnDestroy {
@Output() resized = new EventEmitter<DOMRect>();
@Input() minHeight = 0;
@Input() minWidth = 0;
@ -72,4 +72,9 @@ export class Resizable {
dragMoved($event: CdkDragMove): void {
this.dragMove$.next($event);
}
ngOnDestroy(): void {
this.startSize$.complete();
this.dragMove$.complete();
}
}

View File

@ -34,6 +34,7 @@ export class NifiTooltipDirective<T> implements OnDestroy {
private closeTimer = -1;
private overlayRef: OverlayRef | null = null;
private positionStrategy: PositionStrategy | null = null;
private overTip = false;
private openTimer = -1;
@ -49,6 +50,7 @@ export class NifiTooltipDirective<T> implements OnDestroy {
if (!this.overlayRef?.hasAttached()) {
this.attach();
}
this.openTimer = -1;
}, NiFiCommon.TOOLTIP_DELAY_OPEN_MILLIS);
} else {
if (!this.overlayRef?.hasAttached()) {
@ -63,12 +65,22 @@ export class NifiTooltipDirective<T> implements OnDestroy {
if (this.delayClose) {
this.closeTimer = window.setTimeout(() => {
this.overlayRef?.detach();
if (this.positionStrategy?.detach) {
this.positionStrategy.detach();
}
this.closeTimer = -1;
}, NiFiCommon.TOOLTIP_DELAY_CLOSE_MILLIS);
} else {
this.overlayRef?.detach();
if (this.positionStrategy?.detach) {
this.positionStrategy.detach();
}
}
}
if (this.openTimer > 0) {
window.clearTimeout(this.openTimer);
this.openTimer = -1;
@ -79,6 +91,10 @@ export class NifiTooltipDirective<T> implements OnDestroy {
mouseMove() {
if (this.overlayRef?.hasAttached() && this.tooltipDisabled) {
this.overlayRef?.detach();
if (this.positionStrategy?.detach) {
this.positionStrategy.detach();
}
}
}
@ -91,6 +107,7 @@ export class NifiTooltipDirective<T> implements OnDestroy {
ngOnDestroy(): void {
this.overlayRef?.dispose();
this.positionStrategy?.dispose();
}
private attach(): void {
@ -99,8 +116,8 @@ export class NifiTooltipDirective<T> implements OnDestroy {
}
if (!this.overlayRef) {
const positionStrategy = this.getPositionStrategy();
this.overlayRef = this.overlay.create({ positionStrategy });
this.positionStrategy = this.getPositionStrategy();
this.overlayRef = this.overlay.create({ positionStrategy: this.positionStrategy });
}
const tooltipReference = this.overlayRef.attach(new ComponentPortal(this.tooltipComponentType));
@ -117,6 +134,11 @@ export class NifiTooltipDirective<T> implements OnDestroy {
});
tooltipReference.location.nativeElement.addEventListener('mouseleave', () => {
this.overlayRef?.detach();
if (this.positionStrategy?.detach) {
this.positionStrategy.detach();
}
this.closeTimer = -1;
this.overTip = false;
});