This is an automated email from the ASF dual-hosted git repository.

rfellows pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 562eece6e3 NIFI-13078: Adding support to Enable and Disable through 
the context menu and operation control (#8680)
562eece6e3 is described below

commit 562eece6e3c1eec8d3eec15fd15d9fb986cbc433
Author: Matt Gilman <matt.c.gil...@gmail.com>
AuthorDate: Tue Apr 23 16:47:25 2024 -0400

    NIFI-13078: Adding support to Enable and Disable through the context menu 
and operation control (#8680)
    
    * NIFI-13078:
    - Adding support to Enable and Disable through the context menu and 
operation control.
    
    * NIFI-13078:
    - Addressing review feedback.
    
    This closes #8680
---
 .../service/canvas-context-menu.service.ts         |  74 +++++-
 .../flow-designer/service/canvas-utils.service.ts  |  83 +++++++
 .../pages/flow-designer/service/flow.service.ts    |  40 +++
 .../pages/flow-designer/state/flow/flow.actions.ts |  54 ++++
 .../pages/flow-designer/state/flow/flow.effects.ts | 272 +++++++++++++++++++--
 .../pages/flow-designer/state/flow/flow.reducer.ts |  65 +++--
 .../app/pages/flow-designer/state/flow/index.ts    |  62 ++++-
 .../operation-control.component.ts                 | 110 ++++++---
 8 files changed, 683 insertions(+), 77 deletions(-)

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<any, any, any, any>) => {
+                    return this.canvasUtils.canEnable(selection);
                 },
                 clazz: 'fa fa-flash',
                 text: 'Enable',
-                action: () => {
-                    // TODO - enable
+                action: (selection: d3.Selection<any, any, any, any>) => {
+                    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<any, any, any, any>) => {
+                    return this.canvasUtils.canDisable(selection);
                 },
                 clazz: 'icon icon-enable-false',
                 text: 'Disable',
-                action: () => {
-                    // TODO - disable
+                action: (selection: d3.Selection<any, any, any, any>) => {
+                    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<any, any, any, any>): 
d3.Selection<any, any, any, any> {
+        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<any, any, any, any>): 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<any, any, any, any>): 
d3.Selection<any, any, any, any> {
+        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<any, any, any, any>): 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<any> {
+        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<any> {
+        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<any> {
         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<any> {
+        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<any> {
+        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<any> {
         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<CanvasState>,
         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<any, any, any, any>): 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<any, any, any, any>): 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<any, any, any, any>): 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<any, any, any, any>): 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<any, any, any, any>): boolean {
         return this.canvasUtils.isConfigurable(selection);
     }
 
-    configure(selection: any): void {
+    configure(selection: d3.Selection<any, any, any, any>): 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<any, any, any, any>): boolean {
         return this.canvasUtils.canManagePolicies(selection);
     }
 
-    manageAccess(selection: any): void {
+    manageAccess(selection: d3.Selection<any, any, any, any>): 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<any, any, any, any>): boolean {
+        return this.canvasUtils.canEnable(selection);
     }
 
-    enable(selection: any): void {
-        // TODO - enable
+    enable(selection: d3.Selection<any, any, any, any>): 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<any, any, any, any>): boolean {
+        return this.canvasUtils.canDisable(selection);
     }
 
-    disable(selection: any): void {
-        // TODO - disable
+    disable(selection: d3.Selection<any, any, any, any>): 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<any, any, any, any>): boolean {
         return this.canvasUtils.areAnyRunnable(selection);
     }
 
-    start(selection: any): void {
+    start(selection: d3.Selection<any, any, any, any>): 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<any, any, any, any>): boolean {
         return this.canvasUtils.areAnyStoppable(selection);
     }
 
-    stop(selection: any): void {
+    stop(selection: d3.Selection<any, any, any, any>): 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<any, any, any, any>): boolean {
         return this.canvasUtils.isDisconnected(selection);
     }
 
-    group(selection: any): void {
+    group(selection: d3.Selection<any, any, any, any>): 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<any, any, any, any>): boolean {
         // TODO
         return false;
     }
 
-    color(selection: any): void {
+    color(selection: d3.Selection<any, any, any, any>): void {
         // TODO
     }
 
-    canDelete(selection: any): boolean {
+    canDelete(selection: d3.Selection<any, any, any, any>): boolean {
         return this.canvasUtils.areDeletable(selection);
     }
 
-    delete(selection: any): void {
+    delete(selection: d3.Selection<any, any, any, any>): void {
         if (selection.size() === 1) {
             const selectionData = selection.datum();
             this.store.dispatch(

Reply via email to