mirror of
https://github.com/apache/nifi.git
synced 2025-03-04 00:19:44 +00:00
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:
parent
f48d60abf5
commit
ade260266e
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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 {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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()))
|
||||
)
|
||||
);
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}>()
|
||||
);
|
@ -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
|
||||
}))
|
||||
);
|
@ -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);
|
@ -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;
|
||||
}
|
@ -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
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user