NIFI-13044: Adding support to move to front (#8646)

* NIFI-13044:
- Adding support to move to front.
- Fixing revisions sent in various payloads.

* NIFI-13047:
- Address review feedback.

This closes #8646
This commit is contained in:
Matt Gilman 2024-04-15 13:14:37 -04:00 committed by GitHub
parent 2d9943e2d3
commit a79b210d4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 128 additions and 24 deletions

View File

@ -52,7 +52,8 @@ import {
stopComponents, stopComponents,
stopCurrentProcessGroup, stopCurrentProcessGroup,
stopVersionControlRequest, stopVersionControlRequest,
downloadFlow downloadFlow,
moveToFront
} from '../state/flow/flow.actions'; } from '../state/flow/flow.actions';
import { ComponentType } from '../../../state/shared'; import { ComponentType } from '../../../state/shared';
import { import {
@ -73,6 +74,7 @@ import { promptEmptyQueueRequest, promptEmptyQueuesRequest } from '../state/queu
import { getComponentStateAndOpenDialog } from '../../../state/component-state/component-state.actions'; import { getComponentStateAndOpenDialog } from '../../../state/component-state/component-state.actions';
import { navigateToComponentDocumentation } from '../../../state/documentation/documentation.actions'; import { navigateToComponentDocumentation } from '../../../state/documentation/documentation.actions';
import * as d3 from 'd3'; import * as d3 from 'd3';
import { Client } from '../../../service/client.service';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class CanvasContextMenu implements ContextMenuDefinitionProvider { export class CanvasContextMenu implements ContextMenuDefinitionProvider {
@ -557,7 +559,7 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
id: d.id, id: d.id,
uri: d.uri, uri: d.uri,
type: d.type, type: d.type,
revision: d.revision revision: this.client.getRevision(d)
}); });
}); });
this.store.dispatch( this.store.dispatch(
@ -597,7 +599,7 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
id: d.id, id: d.id,
uri: d.uri, uri: d.uri,
type: d.type, type: d.type,
revision: d.revision revision: this.client.getRevision(d)
}); });
}); });
this.store.dispatch( this.store.dispatch(
@ -625,7 +627,7 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
runOnce({ runOnce({
request: { request: {
uri: d.uri, uri: d.uri,
revision: d.revision revision: this.client.getRevision(d)
} }
}) })
); );
@ -678,7 +680,7 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
id: d.id, id: d.id,
uri: d.uri, uri: d.uri,
type: d.type, type: d.type,
revision: d.revision revision: this.client.getRevision(d)
}); });
}); });
@ -706,7 +708,7 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
id: d.id, id: d.id,
uri: d.uri, uri: d.uri,
type: d.type, type: d.type,
revision: d.revision revision: this.client.getRevision(d)
}); });
}); });
this.store.dispatch( this.store.dispatch(
@ -982,12 +984,12 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
isSeparator: true isSeparator: true
}, },
{ {
condition: (selection: any) => { condition: (selection: d3.Selection<any, any, any, any>) => {
return this.canvasUtils.isConnection(selection); return this.canvasUtils.isConnection(selection);
}, },
clazz: 'fa fa-long-arrow-left', clazz: 'fa fa-long-arrow-left',
text: 'Go to source', text: 'Go to source',
action: (selection: any) => { action: (selection: d3.Selection<any, any, any, any>) => {
const selectionData = selection.datum(); const selectionData = selection.datum();
const remoteConnectableType: string = this.canvasUtils.getConnectableTypeForSource( const remoteConnectableType: string = this.canvasUtils.getConnectableTypeForSource(
ComponentType.RemoteProcessGroup ComponentType.RemoteProcessGroup
@ -1023,12 +1025,12 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
} }
}, },
{ {
condition: (selection: any) => { condition: (selection: d3.Selection<any, any, any, any>) => {
return this.canvasUtils.isConnection(selection); return this.canvasUtils.isConnection(selection);
}, },
clazz: 'fa fa-long-arrow-right', clazz: 'fa fa-long-arrow-right',
text: 'Go to destination', text: 'Go to destination',
action: (selection: any) => { action: (selection: d3.Selection<any, any, any, any>) => {
const selectionData = selection.datum(); const selectionData = selection.datum();
const remoteConnectableType: string = this.canvasUtils.getConnectableTypeForDestination( const remoteConnectableType: string = this.canvasUtils.getConnectableTypeForDestination(
ComponentType.RemoteProcessGroup ComponentType.RemoteProcessGroup
@ -1072,18 +1074,29 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
subMenuId: this.ALIGN.id subMenuId: this.ALIGN.id
}, },
{ {
condition: (selection: any) => { condition: (selection: d3.Selection<any, any, any, any>) => {
// TODO - canMoveToFront return this.canvasUtils.canMoveToFront(selection);
return false;
}, },
clazz: 'fa fa-clone', clazz: 'fa fa-clone',
text: 'Bring to front', text: 'Bring to front',
action: () => { action: (selection: d3.Selection<any, any, any, any>) => {
// TODO - toFront const selectionData = selection.datum();
this.store.dispatch(
moveToFront({
request: {
componentType: selectionData.type,
id: selectionData.id,
uri: selectionData.uri,
revision: this.client.getRevision(selectionData),
zIndex: selectionData.zIndex
}
})
);
} }
}, },
{ {
condition: (selection: any) => { condition: (selection: d3.Selection<any, any, any, any>) => {
return !selection.empty(); return !selection.empty();
}, },
clazz: 'fa fa-crosshairs', clazz: 'fa fa-crosshairs',
@ -1104,12 +1117,12 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
} }
}, },
{ {
condition: (selection: any) => { condition: (selection: d3.Selection<any, any, any, any>) => {
return this.canvasUtils.canRead(selection) && this.canvasUtils.isRemoteProcessGroup(selection); return this.canvasUtils.canRead(selection) && this.canvasUtils.isRemoteProcessGroup(selection);
}, },
clazz: 'fa fa-external-link', clazz: 'fa fa-external-link',
text: 'Go to', text: 'Go to',
action: (selection: any) => { action: (selection: d3.Selection<any, any, any, any>) => {
const selectionData = selection.datum(); const selectionData = selection.datum();
const uri = selectionData.component.targetUri; const uri = selectionData.component.targetUri;
@ -1120,12 +1133,12 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
isSeparator: true isSeparator: true
}, },
{ {
condition: (selection: any) => { condition: (selection: d3.Selection<any, any, any, any>) => {
return this.canvasUtils.isNotRootGroup(); return this.canvasUtils.isNotRootGroup();
}, },
clazz: 'fa fa-arrows', clazz: 'fa fa-arrows',
text: 'Move to parent group', text: 'Move to parent group',
action: (selection: any) => { action: (selection: d3.Selection<any, any, any, any>) => {
const components: MoveComponentRequest[] = []; const components: MoveComponentRequest[] = [];
selection.each(function (d: any) { selection.each(function (d: any) {
components.push({ components.push({
@ -1300,7 +1313,8 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
constructor( constructor(
private store: Store<CanvasState>, private store: Store<CanvasState>,
private canvasUtils: CanvasUtils private canvasUtils: CanvasUtils,
private client: Client
) { ) {
this.allMenus = new Map<string, ContextMenuDefinition>(); this.allMenus = new Map<string, ContextMenuDefinition>();
this.allMenus.set(this.ROOT_MENU.id, this.ROOT_MENU); this.allMenus.set(this.ROOT_MENU.id, this.ROOT_MENU);

View File

@ -1711,4 +1711,16 @@ export class CanvasUtils {
return false; return false;
} }
public canMoveToFront(selection: d3.Selection<any, any, any, any>): boolean {
// ensure the correct number of components are selected
if (selection.size() !== 1) {
return false;
}
if (this.canModify(selection) === false) {
return false;
}
return this.isConnection(selection) || this.isLabel(selection);
}
} }

View File

@ -270,8 +270,8 @@ export class ConnectionManager {
* @param {type} connections * @param {type} connections
*/ */
private sort(connections: any[]): void { private sort(connections: any[]): void {
connections.sort(function (a, b) { connections.sort((a, b) => {
return a.zIndex === b.zIndex ? 0 : a.zIndex > b.zIndex ? 1 : -1; return this.nifiCommon.compareNumber(a.zIndex, b.zIndex);
}); });
} }

View File

@ -76,6 +76,17 @@ export class LabelManager {
}); });
} }
/**
* Sorts the specified labels according to the z index.
*
* @param {type} labels
*/
private sort(labels: any[]): void {
labels.sort((a, b) => {
return this.nifiCommon.compareNumber(a.zIndex, b.zIndex);
});
}
private renderLabels(entered: any) { private renderLabels(entered: any) {
if (entered.empty()) { if (entered.empty()) {
return entered; return entered;
@ -393,6 +404,7 @@ export class LabelManager {
// update // update
const updated = selection.merge(entered); const updated = selection.merge(entered);
this.updateLabels(updated); this.updateLabels(updated);
this.sort(updated);
// position // position
this.positionBehavior.position(updated, this.transitionRequired); this.positionBehavior.position(updated, this.transitionRequired);

View File

@ -54,6 +54,7 @@ import {
LoadRemoteProcessGroupSuccess, LoadRemoteProcessGroupSuccess,
LocalChangesDialogRequest, LocalChangesDialogRequest,
MoveComponentsRequest, MoveComponentsRequest,
MoveToFrontRequest,
NavigateToComponentRequest, NavigateToComponentRequest,
NavigateToControllerServicesRequest, NavigateToControllerServicesRequest,
NavigateToManageComponentPoliciesRequest, NavigateToManageComponentPoliciesRequest,
@ -766,3 +767,5 @@ export const downloadFlow = createAction(
`${CANVAS_PREFIX} Download Flow Request`, `${CANVAS_PREFIX} Download Flow Request`,
props<{ request: DownloadFlowRequest }>() props<{ request: DownloadFlowRequest }>()
); );
export const moveToFront = createAction(`${CANVAS_PREFIX} Move To Front`, props<{ request: MoveToFrontRequest }>());

View File

@ -51,6 +51,7 @@ import {
StopVersionControlRequest, StopVersionControlRequest,
StopVersionControlResponse, StopVersionControlResponse,
UpdateComponentFailure, UpdateComponentFailure,
UpdateComponentRequest,
UpdateComponentResponse, UpdateComponentResponse,
UpdateConnectionSuccess, UpdateConnectionSuccess,
UpdateProcessorRequest, UpdateProcessorRequest,
@ -62,6 +63,7 @@ import {
selectChangeVersionRequest, selectChangeVersionRequest,
selectCurrentParameterContext, selectCurrentParameterContext,
selectCurrentProcessGroupId, selectCurrentProcessGroupId,
selectMaxZIndex,
selectParentProcessGroupId, selectParentProcessGroupId,
selectProcessGroup, selectProcessGroup,
selectProcessor, selectProcessor,
@ -113,6 +115,7 @@ import { SaveVersionDialog } from '../../ui/canvas/items/flow/save-version-dialo
import { ChangeVersionDialog } from '../../ui/canvas/items/flow/change-version-dialog/change-version-dialog'; import { ChangeVersionDialog } from '../../ui/canvas/items/flow/change-version-dialog/change-version-dialog';
import { ChangeVersionProgressDialog } from '../../ui/canvas/items/flow/change-version-progress-dialog/change-version-progress-dialog'; import { ChangeVersionProgressDialog } from '../../ui/canvas/items/flow/change-version-progress-dialog/change-version-progress-dialog';
import { LocalChangesDialog } from '../../ui/canvas/items/flow/local-changes-dialog/local-changes-dialog'; import { LocalChangesDialog } from '../../ui/canvas/items/flow/local-changes-dialog/local-changes-dialog';
import { ClusterConnectionService } from '../../../../service/cluster-connection.service';
@Injectable() @Injectable()
export class FlowEffects { export class FlowEffects {
@ -127,6 +130,7 @@ export class FlowEffects {
private canvasView: CanvasView, private canvasView: CanvasView,
private birdseyeView: BirdseyeView, private birdseyeView: BirdseyeView,
private connectionManager: ConnectionManager, private connectionManager: ConnectionManager,
private clusterConnectionService: ClusterConnectionService,
private router: Router, private router: Router,
private dialog: MatDialog, private dialog: MatDialog,
private propertyTableHelperService: PropertyTableHelperService, private propertyTableHelperService: PropertyTableHelperService,
@ -2846,7 +2850,7 @@ export class FlowEffects {
version: selectedVersion.version version: selectedVersion.version
}, },
processGroupRevision: request.revision, processGroupRevision: request.revision,
disconnectedNodeAcknowledged: false disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged()
}; };
dialogRef.close(); dialogRef.close();
@ -3153,4 +3157,42 @@ export class FlowEffects {
), ),
{ dispatch: false } { dispatch: false }
); );
moveToFront$ = createEffect(() =>
this.actions$.pipe(
ofType(FlowActions.moveToFront),
map((action) => action.request),
concatLatestFrom((request) => this.store.select(selectMaxZIndex(request.componentType))),
filter(([request, maxZIndex]) => request.zIndex < maxZIndex),
switchMap(([request, maxZIndex]) => {
const updateRequest: UpdateComponentRequest = {
id: request.id,
type: request.componentType,
uri: request.uri,
payload: {
revision: request.revision,
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
component: {
id: request.id,
zIndex: maxZIndex + 1
}
}
};
return from(this.flowService.updateComponent(updateRequest)).pipe(
map((response) => {
const updateResponse: UpdateComponentResponse = {
id: updateRequest.id,
type: updateRequest.type,
response
};
return FlowActions.updateComponentSuccess({ response: updateResponse });
}),
catchError((errorResponse: HttpErrorResponse) =>
of(FlowActions.flowSnackbarError({ error: errorResponse.error }))
)
);
})
)
);
} }

View File

@ -19,6 +19,7 @@ import { flowFeatureKey, FlowState, SelectedComponent } from './index';
import { createSelector } from '@ngrx/store'; import { createSelector } from '@ngrx/store';
import { CanvasState, selectCanvasState } from '../index'; import { CanvasState, selectCanvasState } from '../index';
import { selectCurrentRoute } from '../../../../state/router/router.selectors'; import { selectCurrentRoute } from '../../../../state/router/router.selectors';
import { ComponentType } from '../../../../state/shared';
export const selectFlowState = createSelector(selectCanvasState, (state: CanvasState) => state[flowFeatureKey]); export const selectFlowState = createSelector(selectCanvasState, (state: CanvasState) => state[flowFeatureKey]);
@ -246,3 +247,15 @@ export const selectNavigationCollapsed = createSelector(
); );
export const selectOperationCollapsed = createSelector(selectFlowState, (state: FlowState) => state.operationCollapsed); export const selectOperationCollapsed = createSelector(selectFlowState, (state: FlowState) => state.operationCollapsed);
export const selectMaxZIndex = (componentType: ComponentType.Connection | ComponentType.Label) => {
if (componentType === ComponentType.Connection) {
return createSelector(selectConnections, (connections: any[]) =>
connections.reduce((maxZIndex, connection) => Math.max(maxZIndex, connection.zIndex), -1)
);
} else {
return createSelector(selectLabels, (labels: any[]) =>
labels.reduce((maxZIndex, label) => Math.max(maxZIndex, label.zIndex), -1)
);
}
};

View File

@ -739,3 +739,11 @@ export interface DownloadFlowRequest {
processGroupId: string; processGroupId: string;
includeReferencedServices: boolean; includeReferencedServices: boolean;
} }
export interface MoveToFrontRequest {
componentType: ComponentType.Connection | ComponentType.Label;
id: string;
uri: string;
revision: Revision;
zIndex: number;
}