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

View File

@ -1711,4 +1711,16 @@ export class CanvasUtils {
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
*/
private sort(connections: any[]): void {
connections.sort(function (a, b) {
return a.zIndex === b.zIndex ? 0 : a.zIndex > b.zIndex ? 1 : -1;
connections.sort((a, b) => {
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) {
if (entered.empty()) {
return entered;
@ -393,6 +404,7 @@ export class LabelManager {
// update
const updated = selection.merge(entered);
this.updateLabels(updated);
this.sort(updated);
// position
this.positionBehavior.position(updated, this.transitionRequired);

View File

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

View File

@ -51,6 +51,7 @@ import {
StopVersionControlRequest,
StopVersionControlResponse,
UpdateComponentFailure,
UpdateComponentRequest,
UpdateComponentResponse,
UpdateConnectionSuccess,
UpdateProcessorRequest,
@ -62,6 +63,7 @@ import {
selectChangeVersionRequest,
selectCurrentParameterContext,
selectCurrentProcessGroupId,
selectMaxZIndex,
selectParentProcessGroupId,
selectProcessGroup,
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 { 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 { ClusterConnectionService } from '../../../../service/cluster-connection.service';
@Injectable()
export class FlowEffects {
@ -127,6 +130,7 @@ export class FlowEffects {
private canvasView: CanvasView,
private birdseyeView: BirdseyeView,
private connectionManager: ConnectionManager,
private clusterConnectionService: ClusterConnectionService,
private router: Router,
private dialog: MatDialog,
private propertyTableHelperService: PropertyTableHelperService,
@ -2846,7 +2850,7 @@ export class FlowEffects {
version: selectedVersion.version
},
processGroupRevision: request.revision,
disconnectedNodeAcknowledged: false
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged()
};
dialogRef.close();
@ -3153,4 +3157,42 @@ export class FlowEffects {
),
{ 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 { CanvasState, selectCanvasState } from '../index';
import { selectCurrentRoute } from '../../../../state/router/router.selectors';
import { ComponentType } from '../../../../state/shared';
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 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;
includeReferencedServices: boolean;
}
export interface MoveToFrontRequest {
componentType: ComponentType.Connection | ComponentType.Label;
id: string;
uri: string;
revision: Revision;
zIndex: number;
}