diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts index 8e6ec08eee..938152634a 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts @@ -58,13 +58,19 @@ import { copy, paste, terminateThreads, - navigateToParameterContext + navigateToParameterContext, + enableCurrentProcessGroup, + enableComponents, + disableCurrentProcessGroup, + disableComponents } from '../state/flow/flow.actions'; import { ComponentType } from '../../../state/shared'; import { ConfirmStopVersionControlRequest, CopyComponentRequest, DeleteComponentRequest, + DisableComponentRequest, + EnableComponentRequest, MoveComponentRequest, OpenChangeVersionDialogRequest, OpenLocalChangesDialogRequest, @@ -573,8 +579,9 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider { // are runnable or can start transmitting. However, if all the startable components are RGPs, we will defer // to the Enable Transmission menu option and not show the start option. const allRpgs = + !startable.empty() && startable.filter((d: any) => d.type === ComponentType.RemoteProcessGroup).size() === - startable.size(); + startable.size(); return this.canvasUtils.areAnyRunnable(selection) && !allRpgs; }, @@ -613,8 +620,9 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider { // are runnable or can stop transmitting. However, if all the stoppable components are RGPs, we will defer // to the Disable Transmission menu option and not show the start option. const allRpgs = + !stoppable.empty() && stoppable.filter((d: any) => d.type === ComponentType.RemoteProcessGroup).size() === - stoppable.size(); + stoppable.size(); return this.canvasUtils.areAnyStoppable(selection) && !allRpgs; }, @@ -685,25 +693,65 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider { } }, { - condition: (selection: any) => { - // TODO - canEnable - return false; + condition: (selection: d3.Selection) => { + return this.canvasUtils.canEnable(selection); }, clazz: 'fa fa-flash', text: 'Enable', - action: () => { - // TODO - enable + action: (selection: d3.Selection) => { + if (selection.empty()) { + // attempting to enable the current process group + this.store.dispatch(enableCurrentProcessGroup()); + } else { + const components: EnableComponentRequest[] = []; + const enableable = this.canvasUtils.filterEnable(selection); + enableable.each((d: any) => { + components.push({ + id: d.id, + uri: d.uri, + type: d.type, + revision: this.client.getRevision(d) + }); + }); + this.store.dispatch( + enableComponents({ + request: { + components + } + }) + ); + } } }, { - condition: (selection: any) => { - // TODO - canDisable - return false; + condition: (selection: d3.Selection) => { + return this.canvasUtils.canDisable(selection); }, clazz: 'icon icon-enable-false', text: 'Disable', - action: () => { - // TODO - disable + action: (selection: d3.Selection) => { + if (selection.empty()) { + // attempting to disable the current process group + this.store.dispatch(disableCurrentProcessGroup()); + } else { + const components: DisableComponentRequest[] = []; + const disableable = this.canvasUtils.filterDisable(selection); + disableable.each((d: any) => { + components.push({ + id: d.id, + uri: d.uri, + type: d.type, + revision: this.client.getRevision(d) + }); + }); + this.store.dispatch( + disableComponents({ + request: { + components + } + }) + ); + } } }, { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts index d8d2320f51..10c56ff3fd 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts @@ -1440,6 +1440,89 @@ export class CanvasUtils { return '#ffffff'; } + /** + * Filters the specified selection for any components that supports enable. + * + * @argument {selection} selection The selection + */ + public filterEnable(selection: d3.Selection): d3.Selection { + return selection.filter((d, i, nodes) => { + const selected = d3.select(nodes[i]); + + // enable always allowed for PGs since they will invoke the /flow endpoint for enabling all applicable components (based on permissions) + if (this.isProcessGroup(selected)) { + return true; + } + + // not a PG, verify permissions to modify + if (!this.canOperate(selected)) { + return false; + } + + // ensure it's a processor, input port, or output port and supports modification and is disabled (can enable) + return ( + (this.isProcessor(selected) || this.isInputPort(selected) || this.isOutputPort(selected)) && + this.supportsModification(selected) && + d.status.aggregateSnapshot.runStatus === 'Disabled' + ); + }); + } + + /** + * Determines if the specified selection contains any components that supports enable. + * + * @argument {selection} selection The selection + */ + public canEnable(selection: d3.Selection): boolean { + if (selection.empty()) { + return true; + } + + return this.filterEnable(selection).size() === selection.size(); + } + + /** + * Filters the specified selection for any components that supports disable. + * + * @argument {selection} selection The selection + */ + public filterDisable(selection: d3.Selection): d3.Selection { + return selection.filter((d, i, nodes) => { + const selected = d3.select(nodes[i]); + + // disable always allowed for PGs since they will invoke the /flow endpoint for disabling all applicable components (based on permissions) + if (this.isProcessGroup(selected)) { + return true; + } + + // not a PG, verify permissions to modify + if (!this.canOperate(selected)) { + return false; + } + + // ensure it's a processor, input port, or output port and supports modification and is stopped (can disable) + return ( + (this.isProcessor(selected) || this.isInputPort(selected) || this.isOutputPort(selected)) && + this.supportsModification(selected) && + (d.status.aggregateSnapshot.runStatus === 'Stopped' || + d.status.aggregateSnapshot.runStatus === 'Invalid') + ); + }); + } + + /** + * Determines if the specified selection contains any components that supports disable. + * + * @argument {selection} selection The selection + */ + public canDisable(selection: d3.Selection): boolean { + if (selection.empty()) { + return true; + } + + return this.filterDisable(selection).size() === selection.size(); + } + /** * Determines if the components in the specified selection are runnable. * diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts index aee0657396..9207f59507 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts @@ -27,7 +27,11 @@ import { CreateProcessorRequest, CreateRemoteProcessGroupRequest, DeleteComponentRequest, + DisableComponentRequest, + DisableProcessGroupRequest, DownloadFlowRequest, + EnableComponentRequest, + EnableProcessGroupRequest, FlowComparisonEntity, FlowUpdateRequestEntity, GoToRemoteProcessGroupRequest, @@ -270,6 +274,24 @@ export class FlowService implements PropertyDescriptorRetriever { return this.httpClient.put(`${this.nifiCommon.stripProtocol(request.uri)}/run-status`, startRequest); } + enableComponent(request: EnableComponentRequest): Observable { + const enableRequest: ComponentRunStatusRequest = { + revision: request.revision, + disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(), + state: 'STOPPED' + }; + return this.httpClient.put(`${this.nifiCommon.stripProtocol(request.uri)}/run-status`, enableRequest); + } + + disableComponent(request: DisableComponentRequest): Observable { + const disableRequest: ComponentRunStatusRequest = { + revision: request.revision, + disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(), + state: 'DISABLED' + }; + return this.httpClient.put(`${this.nifiCommon.stripProtocol(request.uri)}/run-status`, disableRequest); + } + startComponent(request: StartComponentRequest): Observable { const startRequest: ComponentRunStatusRequest = { revision: request.revision, @@ -292,6 +314,24 @@ export class FlowService implements PropertyDescriptorRetriever { return this.httpClient.delete(`${this.nifiCommon.stripProtocol(request.uri)}/threads`); } + enableProcessGroup(request: EnableProcessGroupRequest): Observable { + const enableRequest: ProcessGroupRunStatusRequest = { + id: request.id, + disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(), + state: 'ENABLED' + }; + return this.httpClient.put(`${FlowService.API}/flow/process-groups/${request.id}`, enableRequest); + } + + disableProcessGroup(request: DisableProcessGroupRequest): Observable { + const disableComponent: ProcessGroupRunStatusRequest = { + id: request.id, + disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(), + state: 'DISABLED' + }; + return this.httpClient.put(`${FlowService.API}/flow/process-groups/${request.id}`, disableComponent); + } + startProcessGroup(request: StartProcessGroupRequest): Observable { const startRequest: ProcessGroupRunStatusRequest = { id: request.id, diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts index c00c48bc3f..0a0f8f4af8 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts @@ -34,10 +34,20 @@ import { CreateRemoteProcessGroupRequest, DeleteComponentRequest, DeleteComponentResponse, + DisableComponentRequest, + DisableComponentResponse, + DisableComponentsRequest, + DisableProcessGroupRequest, + DisableProcessGroupResponse, DownloadFlowRequest, EditComponentDialogRequest, EditConnectionDialogRequest, EditCurrentProcessGroupRequest, + EnableComponentRequest, + EnableComponentResponse, + EnableComponentsRequest, + EnableProcessGroupRequest, + EnableProcessGroupResponse, EnterProcessGroupRequest, FlowUpdateRequestEntity, GoToRemoteProcessGroupRequest, @@ -588,6 +598,50 @@ export const replayLastProvenanceEvent = createAction( props<{ request: ReplayLastProvenanceEventRequest }>() ); +export const enableComponent = createAction( + `${CANVAS_PREFIX} Enable Component`, + props<{ request: EnableComponentRequest | EnableProcessGroupRequest }>() +); + +export const enableComponents = createAction( + `${CANVAS_PREFIX} Enable Components`, + props<{ request: EnableComponentsRequest }>() +); + +export const enableComponentSuccess = createAction( + `${CANVAS_PREFIX} Enable Component Success`, + props<{ response: EnableComponentResponse }>() +); + +export const enableProcessGroupSuccess = createAction( + `${CANVAS_PREFIX} Enable Process Group Success`, + props<{ response: EnableProcessGroupResponse }>() +); + +export const enableCurrentProcessGroup = createAction(`${CANVAS_PREFIX} Enable Current Process Group`); + +export const disableComponent = createAction( + `${CANVAS_PREFIX} Disable Component`, + props<{ request: DisableComponentRequest | DisableProcessGroupRequest }>() +); + +export const disableComponents = createAction( + `${CANVAS_PREFIX} Disable Components`, + props<{ request: DisableComponentsRequest }>() +); + +export const disableComponentSuccess = createAction( + `${CANVAS_PREFIX} Disable Component Success`, + props<{ response: DisableComponentResponse }>() +); + +export const disableProcessGroupSuccess = createAction( + `${CANVAS_PREFIX} Disable Process Group Success`, + props<{ response: DisableProcessGroupResponse }>() +); + +export const disableCurrentProcessGroup = createAction(`${CANVAS_PREFIX} Disable Current Process Group`); + export const runOnce = createAction(`${CANVAS_PREFIX} Run Once`, props<{ request: RunOnceRequest }>()); export const runOnceSuccess = createAction(`${CANVAS_PREFIX} Run Once Success`, props<{ response: RunOnceResponse }>()); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts index d5f9d96b55..bbfe801474 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts @@ -2436,6 +2436,240 @@ export class FlowEffects { { dispatch: false } ); + enableCurrentProcessGroup$ = createEffect(() => + this.actions$.pipe( + ofType(FlowActions.enableCurrentProcessGroup), + concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)), + switchMap(([, pgId]) => { + return of( + FlowActions.enableComponent({ + request: { + id: pgId, + type: ComponentType.ProcessGroup + } + }) + ); + }) + ) + ); + + enableComponents$ = createEffect(() => + this.actions$.pipe( + ofType(FlowActions.enableComponents), + map((action) => action.request), + mergeMap((request) => [ + ...request.components.map((component) => { + return FlowActions.enableComponent({ + request: component + }); + }) + ]) + ) + ); + + enableComponent$ = createEffect(() => + this.actions$.pipe( + ofType(FlowActions.enableComponent), + map((action) => action.request), + mergeMap((request) => { + switch (request.type) { + case ComponentType.InputPort: + case ComponentType.OutputPort: + case ComponentType.Processor: + if ('uri' in request && 'revision' in request) { + return from(this.flowService.enableComponent(request)).pipe( + map((response) => { + return FlowActions.enableComponentSuccess({ + response: { + type: request.type, + component: response + } + }); + }), + catchError((errorResponse: HttpErrorResponse) => + of(FlowActions.flowSnackbarError({ error: errorResponse.error })) + ) + ); + } + return of( + FlowActions.flowSnackbarError({ + error: `Enabling ${request.type} requires both uri and revision properties` + }) + ); + case ComponentType.ProcessGroup: + return from(this.flowService.enableProcessGroup(request)).pipe( + map((enablePgResponse) => { + return FlowActions.enableProcessGroupSuccess({ + response: { + type: request.type, + component: enablePgResponse + } + }); + }), + catchError((errorResponse: HttpErrorResponse) => + of(FlowActions.flowSnackbarError({ error: errorResponse.error })) + ) + ); + default: + return of( + FlowActions.flowSnackbarError({ error: `${request.type} does not support enabling` }) + ); + } + }) + ) + ); + + /** + * If the component enabled was the current process group, reload the flow + */ + enableCurrentProcessGroupSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(FlowActions.enableProcessGroupSuccess), + map((action) => action.response), + concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)), + filter(([response, currentPg]) => response.component.id === currentPg), + switchMap(() => of(FlowActions.reloadFlow())) + ) + ); + + /** + * If a ProcessGroup was enabled, it should be reloaded as the response from the start operation doesn't contain all the displayed info + */ + enableProcessGroupSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(FlowActions.enableProcessGroupSuccess), + map((action) => action.response), + concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)), + filter(([response, currentPg]) => response.component.id !== currentPg), + switchMap(([response]) => + of( + FlowActions.loadChildProcessGroup({ + request: { + id: response.component.id + } + }) + ) + ) + ) + ); + + disableCurrentProcessGroup$ = createEffect(() => + this.actions$.pipe( + ofType(FlowActions.disableCurrentProcessGroup), + concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)), + switchMap(([, pgId]) => { + return of( + FlowActions.disableComponent({ + request: { + id: pgId, + type: ComponentType.ProcessGroup + } + }) + ); + }) + ) + ); + + disableComponents$ = createEffect(() => + this.actions$.pipe( + ofType(FlowActions.disableComponents), + map((action) => action.request), + mergeMap((request) => [ + ...request.components.map((component) => { + return FlowActions.disableComponent({ + request: component + }); + }) + ]) + ) + ); + + disableComponent$ = createEffect(() => + this.actions$.pipe( + ofType(FlowActions.disableComponent), + map((action) => action.request), + mergeMap((request) => { + switch (request.type) { + case ComponentType.InputPort: + case ComponentType.OutputPort: + case ComponentType.Processor: + if ('uri' in request && 'revision' in request) { + return from(this.flowService.disableComponent(request)).pipe( + map((response) => { + return FlowActions.disableComponentSuccess({ + response: { + type: request.type, + component: response + } + }); + }), + catchError((errorResponse: HttpErrorResponse) => + of(FlowActions.flowSnackbarError({ error: errorResponse.error })) + ) + ); + } + return of( + FlowActions.flowSnackbarError({ + error: `Disabling ${request.type} requires both uri and revision properties` + }) + ); + case ComponentType.ProcessGroup: + return from(this.flowService.disableProcessGroup(request)).pipe( + map((enablePgResponse) => { + return FlowActions.disableProcessGroupSuccess({ + response: { + type: request.type, + component: enablePgResponse + } + }); + }), + catchError((errorResponse: HttpErrorResponse) => + of(FlowActions.flowSnackbarError({ error: errorResponse.error })) + ) + ); + default: + return of( + FlowActions.flowSnackbarError({ error: `${request.type} does not support disabling` }) + ); + } + }) + ) + ); + + /** + * If the component disabled was the current process group, reload the flow + */ + disableCurrentProcessGroupSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(FlowActions.disableProcessGroupSuccess), + map((action) => action.response), + concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)), + filter(([response, currentPg]) => response.component.id === currentPg), + switchMap(() => of(FlowActions.reloadFlow())) + ) + ); + + /** + * If a ProcessGroup was disabled, it should be reloaded as the response from the start operation doesn't contain all the displayed info + */ + disableProcessGroupSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(FlowActions.disableProcessGroupSuccess), + map((action) => action.response), + concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)), + filter(([response, currentPg]) => response.component.id !== currentPg), + switchMap(([response]) => + of( + FlowActions.loadChildProcessGroup({ + request: { + id: response.component.id + } + }) + ) + ) + ) + ); + startCurrentProcessGroup$ = createEffect(() => this.actions$.pipe( ofType(FlowActions.startCurrentProcessGroup), @@ -2449,8 +2683,7 @@ export class FlowEffects { } }) ); - }), - catchError((error) => of(FlowActions.flowApiError({ error: error.error }))) + }) ) ); @@ -2488,11 +2721,13 @@ export class FlowEffects { } }); }), - catchError((error) => of(FlowActions.flowApiError({ error: error.error }))) + catchError((errorResponse: HttpErrorResponse) => + of(FlowActions.flowSnackbarError({ error: errorResponse.error })) + ) ); } return of( - FlowActions.flowApiError({ + FlowActions.flowSnackbarError({ error: `Starting ${request.type} requires both uri and revision properties` }) ); @@ -2509,13 +2744,16 @@ export class FlowEffects { } }); }), - catchError((error) => of(FlowActions.flowApiError({ error: error.error }))) + catchError((errorResponse: HttpErrorResponse) => + of(FlowActions.flowSnackbarError({ error: errorResponse.error })) + ) ); default: - return of(FlowActions.flowApiError({ error: `${request.type} does not support starting` })); + return of( + FlowActions.flowSnackbarError({ error: `${request.type} does not support starting` }) + ); } - }), - catchError((error) => of(FlowActions.flowApiError({ error: error.error }))) + }) ) ); @@ -2604,11 +2842,13 @@ export class FlowEffects { } }); }), - catchError((error) => of(FlowActions.flowApiError({ error: error.error }))) + catchError((errorResponse: HttpErrorResponse) => + of(FlowActions.flowSnackbarError({ error: errorResponse.error })) + ) ); } return of( - FlowActions.flowApiError({ + FlowActions.flowSnackbarError({ error: `Stopping ${request.type} requires both uri and revision properties` }) ); @@ -2625,13 +2865,16 @@ export class FlowEffects { } }); }), - catchError((error) => of(FlowActions.flowApiError({ error: error.error }))) + catchError((errorResponse: HttpErrorResponse) => + of(FlowActions.flowSnackbarError({ error: errorResponse.error })) + ) ); default: - return of(FlowActions.flowApiError({ error: `${request.type} does not support stopping` })); + return of( + FlowActions.flowSnackbarError({ error: `${request.type} does not support stopping` }) + ); } - }), - catchError((error) => of(FlowActions.flowApiError({ error: error.error }))) + }) ) ); @@ -2739,6 +2982,7 @@ export class FlowEffects { ////////////////////////////////// // Start version control effects ////////////////////////////////// + openSaveVersionDialogRequest$ = createEffect(() => this.actions$.pipe( ofType(FlowActions.openSaveVersionDialogRequest), diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts index 7eb089b8ba..3ce6560e2f 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts @@ -30,6 +30,12 @@ import { createProcessGroup, createProcessor, deleteComponentsSuccess, + disableComponent, + disableComponentSuccess, + disableProcessGroupSuccess, + enableComponent, + enableComponentSuccess, + enableProcessGroupSuccess, flowApiError, flowVersionBannerError, groupComponents, @@ -61,8 +67,11 @@ import { setTransitionRequired, startComponent, startComponentSuccess, + startProcessGroupSuccess, startRemoteProcessGroupPolling, + stopComponent, stopComponentSuccess, + stopProcessGroupSuccess, stopRemoteProcessGroupPolling, stopVersionControl, stopVersionControlSuccess, @@ -275,10 +284,30 @@ export const flowReducer = createReducer( dragging: false, saving: false })), - on(updateComponent, updateProcessor, updateConnection, startComponent, runOnce, (state) => ({ - ...state, - saving: true - })), + on( + updateComponent, + updateProcessor, + updateConnection, + enableComponent, + disableComponent, + startComponent, + stopComponent, + runOnce, + (state) => ({ + ...state, + saving: true + }) + ), + on( + enableProcessGroupSuccess, + disableProcessGroupSuccess, + startProcessGroupSuccess, + stopProcessGroupSuccess, + (state) => ({ + ...state, + saving: false + }) + ), on(updateComponentSuccess, updateProcessorSuccess, updateConnectionSuccess, (state, { response }) => { return produce(state, (draftState) => { const collection: any[] | null = getComponentCollection(draftState, response.type); @@ -400,20 +429,26 @@ export const flowReducer = createReducer( ...state, operationCollapsed })), - on(startComponentSuccess, stopComponentSuccess, (state, { response }) => { - return produce(state, (draftState) => { - const collection: any[] | null = getComponentCollection(draftState, response.type); + on( + startComponentSuccess, + stopComponentSuccess, + enableComponentSuccess, + disableComponentSuccess, + (state, { response }) => { + return produce(state, (draftState) => { + const collection: any[] | null = getComponentCollection(draftState, response.type); - if (collection) { - const componentIndex: number = collection.findIndex((f: any) => response.component.id === f.id); - if (componentIndex > -1) { - collection[componentIndex] = response.component; + if (collection) { + const componentIndex: number = collection.findIndex((f: any) => response.component.id === f.id); + if (componentIndex > -1) { + collection[componentIndex] = response.component; + } } - } - draftState.saving = false; - }); - }), + draftState.saving = false; + }); + } + ), on(runOnceSuccess, (state, { response }) => { return produce(state, (draftState) => { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts index 09083969bb..53d89209a8 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts @@ -655,6 +655,64 @@ export interface RunOnceResponse { component: ComponentEntity; } +export interface EnableProcessGroupRequest { + id: string; + type: ComponentType; +} + +export interface EnableComponentRequest { + id: string; + uri: string; + type: ComponentType; + revision: Revision; +} + +export interface EnableComponentsRequest { + components: EnableComponentRequest[]; +} + +export interface EnableComponentResponse { + type: ComponentType; + component: ComponentEntity; +} + +export interface EnableProcessGroupResponse { + type: ComponentType; + component: { + id: string; + state: string; + }; +} + +export interface DisableProcessGroupRequest { + id: string; + type: ComponentType; +} + +export interface DisableComponentRequest { + id: string; + uri: string; + type: ComponentType; + revision: Revision; +} + +export interface DisableComponentsRequest { + components: DisableComponentRequest[]; +} + +export interface DisableComponentResponse { + type: ComponentType; + component: ComponentEntity; +} + +export interface DisableProcessGroupResponse { + type: ComponentType; + component: { + id: string; + state: string; + }; +} + export interface StartProcessGroupRequest { id: string; type: ComponentType; @@ -692,10 +750,6 @@ export interface StopProcessGroupResponse { }; } -export interface StartComponentsResponse { - components: StartComponentsResponse[]; -} - export interface ComponentRunStatusRequest { revision: Revision; state: string; diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.ts index 6eac3f66f6..42bdbdcd56 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component.ts @@ -19,6 +19,10 @@ import { Component, Input } from '@angular/core'; import { copy, deleteComponents, + disableComponents, + disableCurrentProcessGroup, + enableComponents, + enableCurrentProcessGroup, getParameterContextsAndOpenGroupComponentsDialog, navigateToEditComponent, navigateToEditCurrentProcessGroup, @@ -38,6 +42,8 @@ import { Storage } from '../../../../../../service/storage.service'; import { CopyComponentRequest, DeleteComponentRequest, + DisableComponentRequest, + EnableComponentRequest, MoveComponentRequest, StartComponentRequest, StopComponentRequest @@ -48,6 +54,7 @@ import { ComponentType } from '../../../../../../state/shared'; import { MatButtonModule } from '@angular/material/button'; import * as d3 from 'd3'; import { CanvasView } from '../../../../service/canvas-view.service'; +import { Client } from '../../../../../../service/client.service'; @Component({ selector: 'operation-control', @@ -69,6 +76,7 @@ export class OperationControl { private store: Store, public canvasUtils: CanvasUtils, private canvasView: CanvasView, + private client: Client, private storage: Storage ) { try { @@ -76,7 +84,7 @@ export class OperationControl { OperationControl.CONTROL_VISIBILITY_KEY ); if (item) { - this.operationCollapsed = item[OperationControl.OPERATION_KEY] === false; + this.operationCollapsed = !item[OperationControl.OPERATION_KEY]; this.store.dispatch(setOperationCollapsed({ operationCollapsed: this.operationCollapsed })); } } catch (e) { @@ -98,7 +106,7 @@ export class OperationControl { this.storage.setItem(OperationControl.CONTROL_VISIBILITY_KEY, item); } - getContextIcon(selection: any): string { + getContextIcon(selection: d3.Selection): string { if (selection.size() === 0) { if (this.breadcrumbEntity.parentBreadcrumb == null) { return 'icon-drop'; @@ -128,7 +136,7 @@ export class OperationControl { } } - getContextName(selection: any): string { + getContextName(selection: d3.Selection): string { if (selection.size() === 0) { if (this.breadcrumbEntity.permissions.canRead) { return this.breadcrumbEntity.breadcrumb.name; @@ -153,7 +161,7 @@ export class OperationControl { } } - getContextType(selection: any): string { + getContextType(selection: d3.Selection): string { if (selection.size() === 0) { return 'Process Group'; } else if (selection.size() > 1) { @@ -179,7 +187,7 @@ export class OperationControl { } } - getContextId(selection: any): string { + getContextId(selection: d3.Selection): string { if (selection.size() === 0) { return this.breadcrumbEntity.id; } else if (selection.size() > 1) { @@ -190,11 +198,11 @@ export class OperationControl { return selectionData.id; } - canConfigure(selection: any): boolean { + canConfigure(selection: d3.Selection): boolean { return this.canvasUtils.isConfigurable(selection); } - configure(selection: any): void { + configure(selection: d3.Selection): void { if (selection.empty()) { this.store.dispatch(navigateToEditCurrentProcessGroup()); } else { @@ -214,11 +222,11 @@ export class OperationControl { return this.canvasUtils.supportsManagedAuthorizer(); } - canManageAccess(selection: any): boolean { + canManageAccess(selection: d3.Selection): boolean { return this.canvasUtils.canManagePolicies(selection); } - manageAccess(selection: any): void { + manageAccess(selection: d3.Selection): void { if (selection.empty()) { this.store.dispatch( navigateToManageComponentPolicies({ @@ -265,29 +273,69 @@ export class OperationControl { } } - canEnable(selection: any): boolean { - // TODO - canEnable - return false; + canEnable(selection: d3.Selection): boolean { + return this.canvasUtils.canEnable(selection); } - enable(selection: any): void { - // TODO - enable + enable(selection: d3.Selection): void { + if (selection.empty()) { + // attempting to enable the current process group + this.store.dispatch(enableCurrentProcessGroup()); + } else { + const components: EnableComponentRequest[] = []; + const enableable = this.canvasUtils.filterEnable(selection); + enableable.each((d: any) => { + components.push({ + id: d.id, + uri: d.uri, + type: d.type, + revision: this.client.getRevision(d) + }); + }); + this.store.dispatch( + enableComponents({ + request: { + components + } + }) + ); + } } - canDisable(selection: any): boolean { - // TODO - canDisable - return false; + canDisable(selection: d3.Selection): boolean { + return this.canvasUtils.canDisable(selection); } - disable(selection: any): void { - // TODO - disable + disable(selection: d3.Selection): void { + if (selection.empty()) { + // attempting to disable the current process group + this.store.dispatch(disableCurrentProcessGroup()); + } else { + const components: DisableComponentRequest[] = []; + const disableable = this.canvasUtils.filterDisable(selection); + disableable.each((d: any) => { + components.push({ + id: d.id, + uri: d.uri, + type: d.type, + revision: this.client.getRevision(d) + }); + }); + this.store.dispatch( + disableComponents({ + request: { + components + } + }) + ); + } } - canStart(selection: any): boolean { + canStart(selection: d3.Selection): boolean { return this.canvasUtils.areAnyRunnable(selection); } - start(selection: any): void { + start(selection: d3.Selection): void { if (selection.empty()) { // attempting to start the current process group this.store.dispatch(startCurrentProcessGroup()); @@ -299,7 +347,7 @@ export class OperationControl { id: d.id, uri: d.uri, type: d.type, - revision: d.revision + revision: this.client.getRevision(d) }); }); this.store.dispatch( @@ -312,11 +360,11 @@ export class OperationControl { } } - canStop(selection: any): boolean { + canStop(selection: d3.Selection): boolean { return this.canvasUtils.areAnyStoppable(selection); } - stop(selection: any): void { + stop(selection: d3.Selection): void { if (selection.empty()) { // attempting to start the current process group this.store.dispatch(stopCurrentProcessGroup()); @@ -328,7 +376,7 @@ export class OperationControl { id: d.id, uri: d.uri, type: d.type, - revision: d.revision + revision: this.client.getRevision(d) }); }); this.store.dispatch( @@ -382,11 +430,11 @@ export class OperationControl { ); } - canGroup(selection: any): boolean { + canGroup(selection: d3.Selection): boolean { return this.canvasUtils.isDisconnected(selection); } - group(selection: any): void { + group(selection: d3.Selection): void { const moveComponents: MoveComponentRequest[] = []; selection.each(function (d: any) { moveComponents.push({ @@ -408,20 +456,20 @@ export class OperationControl { ); } - canColor(selection: any): boolean { + canColor(selection: d3.Selection): boolean { // TODO return false; } - color(selection: any): void { + color(selection: d3.Selection): void { // TODO } - canDelete(selection: any): boolean { + canDelete(selection: d3.Selection): boolean { return this.canvasUtils.areDeletable(selection); } - delete(selection: any): void { + delete(selection: d3.Selection): void { if (selection.size() === 1) { const selectionData = selection.datum(); this.store.dispatch(