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

mcgilman 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 0271e926eb [NIFI-13318] processor stop and configure UX (#9548)
0271e926eb is described below

commit 0271e926eba51563e52ceceff393c382b37c4582
Author: Scott Aslan <[email protected]>
AuthorDate: Tue Dec 10 16:54:30 2024 -0500

    [NIFI-13318] processor stop and configure UX (#9548)
    
    * [NIFI-13318] processor stop and configure UX
    
    * handle invalid run status
    
    * display validation errors
    
    * invalid icon and tooltip alternative placement
    
    * remove validation errors tooltip, move invalid icon into status button, 
update edit processor entity and readonly updates
    
    * restore MAT_DIALOG_DATA and only enable/disable form controls via api
    
    * clean up
    
    * only allow updates by current client and poll until stopped and no active 
threads
    
    * update menu options to display when available
    
    * display processor bulletins
    
    * align dialog header text and run status button
    
    * update filter for incoming updated entities and submit appropriate 
revision on run status changes
    
    * disable button when stopping
    
    * code clean up
    
    * update method name
    
    * update error message
    
    * update types
    
    * add types and cleanup
    
    * move run status action button
    
    * review feedback
    
    * update run status action button to consider when user cannot operate 
processor
    
    * update to also handle disabled run status when user does not have operate
    
    * clean up pollingProcessor
    
    * disable button when stopping
    
    * prettier
    
    * prettier
    
    * readd thread count
    
    * poll when necessary
    
    This closes #9548
---
 .../apache/nifi/web/StandardNiFiServiceFacade.java |   2 +-
 nifi-frontend/src/main/frontend/.gitignore         |   1 +
 .../pages/flow-designer/state/flow/flow.actions.ts |  15 ++
 .../pages/flow-designer/state/flow/flow.effects.ts | 174 +++++++++++++++++
 .../pages/flow-designer/state/flow/flow.reducer.ts |  14 +-
 .../flow-designer/state/flow/flow.selectors.ts     |   2 +
 .../app/pages/flow-designer/state/flow/index.ts    |   5 +
 .../flow-status/_flow-status.component-theme.scss  |   5 -
 .../_edit-processor.component-theme.scss}          |  43 +----
 .../edit-processor/edit-processor.component.html   | 149 +++++++++++---
 .../edit-processor/edit-processor.component.ts     | 215 +++++++++++++++++++--
 .../apps/nifi/src/app/service/client.service.ts    |  14 +-
 .../src/main/frontend/apps/nifi/src/styles.scss    |   3 +
 .../libs/shared/src/assets/styles/_app.scss        |   1 +
 .../frontend/libs/shared/src/services/index.ts     |   1 +
 .../{index.ts => session-storage.service.spec.ts}  |  20 +-
 .../shared/src/services/session-storage.service.ts | 110 +++++++++++
 17 files changed, 685 insertions(+), 89 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 824c28d2e3..ad5f200f5d 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -524,7 +524,7 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
             return;
         }
 
-        throw new InvalidRevisionException(revision + " is not the most 
up-to-date revision. This component appears to have been modified");
+        throw new InvalidRevisionException(revision + " is not the most 
up-to-date revision. This component appears to have been modified. Retrieve the 
most up-to-date revision and try again.");
     }
 
     @Override
diff --git a/nifi-frontend/src/main/frontend/.gitignore 
b/nifi-frontend/src/main/frontend/.gitignore
index 8224631ef2..581343e403 100644
--- a/nifi-frontend/src/main/frontend/.gitignore
+++ b/nifi-frontend/src/main/frontend/.gitignore
@@ -36,6 +36,7 @@ yarn-error.log
 /libpeerconnection.log
 testem.log
 /typings
+/.tool-versions
 
 # System files
 .DS_Store
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
index a5882f5396..4850df9ab5 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
@@ -87,6 +87,7 @@ import {
     StartComponentRequest,
     StartComponentResponse,
     StartComponentsRequest,
+    StartPollingProcessorUntilStoppedRequest,
     StartProcessGroupRequest,
     StartProcessGroupResponse,
     StopComponentRequest,
@@ -776,6 +777,20 @@ export const pollChangeVersionSuccess = createAction(
 
 export const stopPollingChangeVersion = createAction(`${CANVAS_PREFIX} Stop 
Polling Change Version`);
 
+export const startPollingProcessorUntilStopped = createAction(
+    `${CANVAS_PREFIX} Start Polling Processor Until Stopped`,
+    props<{ request: StartPollingProcessorUntilStoppedRequest }>()
+);
+
+export const pollProcessorUntilStopped = createAction(`${CANVAS_PREFIX} Poll 
Processor Until Stopped`);
+
+export const pollProcessorUntilStoppedSuccess = createAction(
+    `${CANVAS_PREFIX} Poll Processor Until Stopped Success`,
+    props<{ response: LoadProcessorSuccess }>()
+);
+
+export const stopPollingProcessor = createAction(`${CANVAS_PREFIX} Stop 
Polling Processor`);
+
 export const openSaveVersionDialog = createAction(
     `${CANVAS_PREFIX} Open Save Flow Version Dialog`,
     props<{ request: SaveVersionDialogRequest }>()
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
index 576f57114b..7819a895be 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
@@ -45,6 +45,8 @@ import {
     CreateConnectionDialogRequest,
     CreateProcessGroupDialogRequest,
     DeleteComponentResponse,
+    DisableComponentRequest,
+    EnableComponentRequest,
     GroupComponentsDialogRequest,
     ImportFromRegistryDialogRequest,
     LoadProcessGroupResponse,
@@ -56,6 +58,8 @@ import {
     SaveVersionRequest,
     SelectedComponent,
     Snippet,
+    StartComponentRequest,
+    StopComponentRequest,
     StopVersionControlRequest,
     StopVersionControlResponse,
     UpdateComponentFailure,
@@ -80,6 +84,7 @@ import {
     selectParentProcessGroupId,
     selectProcessGroup,
     selectProcessor,
+    selectPollingProcessor,
     selectRefreshRpgDetails,
     selectRemoteProcessGroup,
     selectSaving,
@@ -160,6 +165,13 @@ import { selectDocumentVisibilityState } from 
'../../../../state/document-visibi
 import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
 import { DocumentVisibility } from '../../../../state/document-visibility';
 import { ErrorContextKey } from '../../../../state/error';
+import {
+    disableComponent,
+    enableComponent,
+    startComponent,
+    startPollingProcessorUntilStopped,
+    stopComponent
+} from './flow.actions';
 import { CopyPasteService } from '../../service/copy-paste.service';
 import { selectCopiedContent } from '../../../../state/copy/copy.selectors';
 import { CopyRequestContext, CopyResponseContext } from 
'../../../../state/copy';
@@ -1428,6 +1440,7 @@ export class FlowEffects {
                 }),
                 tap(([request, parameterContext, processGroupId]) => {
                     const processorId: string = request.entity.id;
+                    let runStatusChanged: boolean = false;
 
                     const editDialogReference = 
this.dialog.open(EditProcessor, {
                         ...XL_DIALOG,
@@ -1555,6 +1568,116 @@ export class FlowEffects {
                                 })
                             );
                         });
+                    const startPollingIfNecessary = (processorEntity: any): 
boolean => {
+                        if (
+                            
(processorEntity.status.aggregateSnapshot.runStatus === 'Stopped' &&
+                                
processorEntity.status.aggregateSnapshot.activeThreadCount > 0) ||
+                            processorEntity.status.aggregateSnapshot.runStatus 
=== 'Validating'
+                        ) {
+                            this.store.dispatch(
+                                startPollingProcessorUntilStopped({
+                                    request: {
+                                        id: processorEntity.id
+                                    }
+                                })
+                            );
+                            return true;
+                        }
+
+                        return false;
+                    };
+
+                    const pollingStarted = 
startPollingIfNecessary(request.entity);
+
+                    this.store
+                        .select(selectProcessor(processorId))
+                        .pipe(
+                            takeUntil(editDialogReference.afterClosed()),
+                            isDefinedAndNotNull(),
+                            filter((processorEntity) => {
+                                return (
+                                    (runStatusChanged || pollingStarted) &&
+                                    processorEntity.revision.clientId === 
this.client.getClientId()
+                                );
+                            }),
+                            concatLatestFrom(() => 
this.store.select(selectPollingProcessor))
+                        )
+                        .subscribe(([processorEntity, pollingProcessor]) => {
+                            
editDialogReference.componentInstance.processorUpdates = processorEntity;
+
+                            // if we're already polling we do not want to 
start polling again
+                            if (!pollingProcessor) {
+                                startPollingIfNecessary(processorEntity);
+                            }
+                        });
+
+                    editDialogReference.componentInstance.stopComponentRequest
+                        .pipe(takeUntil(editDialogReference.afterClosed()))
+                        .subscribe((stopComponentRequest: 
StopComponentRequest) => {
+                            runStatusChanged = true;
+                            this.store.dispatch(
+                                stopComponent({
+                                    request: {
+                                        id: stopComponentRequest.id,
+                                        uri: stopComponentRequest.uri,
+                                        type: ComponentType.Processor,
+                                        revision: 
stopComponentRequest.revision,
+                                        errorStrategy: 'snackbar'
+                                    }
+                                })
+                            );
+                        });
+
+                    
editDialogReference.componentInstance.disableComponentRequest
+                        .pipe(takeUntil(editDialogReference.afterClosed()))
+                        .subscribe((disableComponentsRequest: 
DisableComponentRequest) => {
+                            runStatusChanged = true;
+                            this.store.dispatch(
+                                disableComponent({
+                                    request: {
+                                        id: disableComponentsRequest.id,
+                                        uri: disableComponentsRequest.uri,
+                                        type: ComponentType.Processor,
+                                        revision: 
disableComponentsRequest.revision,
+                                        errorStrategy: 'snackbar'
+                                    }
+                                })
+                            );
+                        });
+
+                    
editDialogReference.componentInstance.enableComponentRequest
+                        .pipe(takeUntil(editDialogReference.afterClosed()))
+                        .subscribe((enableComponentsRequest: 
EnableComponentRequest) => {
+                            runStatusChanged = true;
+                            this.store.dispatch(
+                                enableComponent({
+                                    request: {
+                                        id: enableComponentsRequest.id,
+                                        uri: enableComponentsRequest.uri,
+                                        type: ComponentType.Processor,
+                                        revision: 
enableComponentsRequest.revision,
+                                        errorStrategy: 'snackbar'
+                                    }
+                                })
+                            );
+                        });
+
+                    editDialogReference.componentInstance.startComponentRequest
+                        .pipe(takeUntil(editDialogReference.afterClosed()))
+                        .subscribe((startComponentRequest: 
StartComponentRequest) => {
+                            runStatusChanged = true;
+                            this.store.dispatch(
+                                startComponent({
+                                    request: {
+                                        id: startComponentRequest.id,
+                                        uri: startComponentRequest.uri,
+                                        type: ComponentType.Processor,
+                                        revision: 
startComponentRequest.revision,
+                                        errorStrategy: 'snackbar'
+                                    }
+                                })
+                            );
+                        });
 
                     editDialogReference.afterClosed().subscribe((response) => {
                         this.store.dispatch(resetPropertyVerificationState());
@@ -1578,6 +1701,57 @@ export class FlowEffects {
         { dispatch: false }
     );
 
+    startPollingProcessorUntilStopped = createEffect(() =>
+        this.actions$.pipe(
+            ofType(FlowActions.startPollingProcessorUntilStopped),
+            switchMap(() =>
+                interval(2000, asyncScheduler).pipe(
+                    
takeUntil(this.actions$.pipe(ofType(FlowActions.stopPollingProcessor)))
+                )
+            ),
+            switchMap(() => of(FlowActions.pollProcessorUntilStopped()))
+        )
+    );
+
+    pollProcessorUntilStopped$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(FlowActions.pollProcessorUntilStopped),
+            concatLatestFrom(() => 
[this.store.select(selectPollingProcessor).pipe(isDefinedAndNotNull())]),
+            switchMap(([, pollingProcessor]) => {
+                return from(
+                    this.flowService.getProcessor(pollingProcessor.id).pipe(
+                        map((response) =>
+                            FlowActions.pollProcessorUntilStoppedSuccess({
+                                response: {
+                                    id: pollingProcessor.id,
+                                    processor: response
+                                }
+                            })
+                        ),
+                        catchError((errorResponse: HttpErrorResponse) => {
+                            
this.store.dispatch(FlowActions.stopPollingProcessor());
+                            return 
of(this.snackBarOrFullScreenError(errorResponse));
+                        })
+                    )
+                );
+            })
+        )
+    );
+
+    pollProcessorUntilStoppedSuccess$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(FlowActions.pollProcessorUntilStoppedSuccess),
+            map((action) => action.response),
+            filter((response) => {
+                return (
+                    response.processor.status.runStatus === 'Stopped' &&
+                    
response.processor.status.aggregateSnapshot.activeThreadCount === 0
+                );
+            }),
+            switchMap(() => of(FlowActions.stopPollingProcessor()))
+        )
+    );
+
     openEditConnectionDialog$ = createEffect(
         () =>
             this.actions$.pipe(
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
index cd3ec9f31b..4cff6a7b2d 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
@@ -50,6 +50,7 @@ import {
     navigateWithoutTransform,
     pasteSuccess,
     pollChangeVersionSuccess,
+    pollProcessorUntilStoppedSuccess,
     pollRevertChangesSuccess,
     requestRefreshRemoteProcessGroup,
     resetFlowState,
@@ -68,10 +69,12 @@ import {
     setTransitionRequired,
     startComponent,
     startComponentSuccess,
+    startPollingProcessorUntilStopped,
     startProcessGroupSuccess,
     startRemoteProcessGroupPolling,
     stopComponent,
     stopComponentSuccess,
+    stopPollingProcessor,
     stopProcessGroupSuccess,
     stopRemoteProcessGroupPolling,
     stopVersionControl,
@@ -92,6 +95,7 @@ import { produce } from 'immer';
 export const initialState: FlowState = {
     id: 'root',
     changeVersionRequest: null,
+    pollingProcessor: null,
     flow: {
         revision: {
             version: 0
@@ -297,7 +301,7 @@ export const flowReducer = createReducer(
             }
         });
     }),
-    on(loadProcessorSuccess, (state, { response }) => {
+    on(loadProcessorSuccess, pollProcessorUntilStoppedSuccess, (state, { 
response }) => {
         return produce(state, (draftState) => {
             const proposedProcessor = response.processor;
             const componentIndex: number = 
draftState.flow.processGroupFlow.flow.processors.findIndex(
@@ -373,6 +377,14 @@ export const flowReducer = createReducer(
         saving: false,
         versionSaving: false
     })),
+    on(startPollingProcessorUntilStopped, (state, { request }) => ({
+        ...state,
+        pollingProcessor: request
+    })),
+    on(stopPollingProcessor, (state) => ({
+        ...state,
+        pollingProcessor: null
+    })),
     on(
         createProcessor,
         createProcessGroup,
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
index 101dcb8c77..31e04c68e7 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
@@ -30,6 +30,8 @@ export const selectChangeVersionRequest = createSelector(
     (state: FlowState) => state.changeVersionRequest
 );
 
+export const selectPollingProcessor = createSelector(selectFlowState, (state: 
FlowState) => state.pollingProcessor);
+
 export const selectSaving = createSelector(selectFlowState, (state: FlowState) 
=> state.saving);
 
 export const selectVersionSaving = createSelector(selectFlowState, (state: 
FlowState) => state.versionSaving);
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts
index eea1eea233..5160c70c8f 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts
@@ -659,6 +659,7 @@ export interface FlowState {
     flowAnalysisOpen: boolean;
     versionSaving: boolean;
     changeVersionRequest: FlowUpdateRequestEntity | null;
+    pollingProcessor: StartPollingProcessorUntilStoppedRequest | null;
     status: 'pending' | 'loading' | 'success' | 'complete';
 }
 
@@ -792,6 +793,10 @@ export interface StopComponentRequest {
     errorStrategy: 'snackbar' | 'banner';
 }
 
+export interface StartPollingProcessorUntilStoppedRequest {
+    id: string;
+}
+
 export interface StopProcessGroupRequest {
     id: string;
     type: ComponentType;
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/_flow-status.component-theme.scss
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/_flow-status.component-theme.scss
index d753fa35ad..08e581d217 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/_flow-status.component-theme.scss
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/_flow-status.component-theme.scss
@@ -40,11 +40,6 @@
         neutral,
         map.get(map.get($config, neutral), lighter)
     );
-    $material-theme-neutral-palette-default: mat.get-theme-color(
-        $material-theme,
-        neutral,
-        map.get(map.get($config, neutral), default)
-    );
 
     $material-theme-primary-palette-default: mat.get-theme-color(
         $material-theme,
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/_flow-status.component-theme.scss
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/_edit-processor.component-theme.scss
similarity index 68%
copy from 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/_flow-status.component-theme.scss
copy to 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/_edit-processor.component-theme.scss
index d753fa35ad..f7cfc4563f 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/_flow-status.component-theme.scss
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/_edit-processor.component-theme.scss
@@ -30,21 +30,6 @@
         error,
         map.get(map.get($config, error), default)
     );
-    $material-theme-neutral-palette-darker: mat.get-theme-color(
-        $material-theme,
-        neutral,
-        map.get(map.get($config, neutral), darker)
-    );
-    $material-theme-neutral-palette-lighter: mat.get-theme-color(
-        $material-theme,
-        neutral,
-        map.get(map.get($config, neutral), lighter)
-    );
-    $material-theme-neutral-palette-default: mat.get-theme-color(
-        $material-theme,
-        neutral,
-        map.get(map.get($config, neutral), default)
-    );
 
     $material-theme-primary-palette-default: mat.get-theme-color(
         $material-theme,
@@ -58,14 +43,8 @@
     $success: map.get(map.get($config, success), default);
     $caution: map.get(map.get($config, caution), default);
 
-    .flow-status {
-        background-color: if(
-            $is-material-dark,
-            $material-theme-neutral-palette-darker,
-            $material-theme-neutral-palette-lighter
-        );
-
-        .controller-bulletins {
+    #edit-processor-header {
+        .bulletins {
             background-color: unset;
 
             .fa {
@@ -73,7 +52,7 @@
             }
         }
 
-        .controller-bulletins.has-bulletins {
+        .bulletins.has-bulletins {
             .fa {
                 color: $primary-contrast;
             }
@@ -98,21 +77,5 @@
                 background-color: $success;
             }
         }
-
-        .flow-analysis-notifications.warn {
-            background-color: $material-theme-secondary-palette-default;
-
-            .fa {
-                color: $primary-contrast;
-            }
-        }
-
-        .flow-analysis-notifications.enforce {
-            background-color: $material-theme-error-palette-default;
-
-            .fa {
-                color: $primary-contrast;
-            }
-        }
     }
 }
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.html
index 93a9b035fe..c9ccb9e622 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.html
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.html
@@ -15,19 +15,37 @@
   ~ limitations under the License.
   -->
 
-<h2 mat-dialog-title>
-    <div class="flex justify-between items-baseline">
-        <div>
-            {{ readonly ? 'Processor Details' : 'Edit Processor' }}
+<h2 id="edit-processor-header" mat-dialog-title>
+    <div class="flex justify-between items-center">
+        <div class="flex items-baseline">
+            <div class="mr-2">
+                {{ readonly ? 'Processor Details' : 'Edit Processor' }}
+            </div>
+            |
+            <div class="ml-2 text-base">
+                {{ formatType() }}
+            </div>
         </div>
-        <div class="text-base">
-            {{ formatType(request.entity) }}
+        <div class="flex">
+            @if (hasBulletins()) {
+                <div class="w-[48px]">
+                    <div
+                        nifiTooltip
+                        [delayClose]="true"
+                        [tooltipComponentType]="BulletinsTip"
+                        [tooltipInputData]="getBulletinsTipData()"
+                        [position]="getBulletinTooltipPosition()"
+                        [ngClass]="getMostSevereBulletinLevel()"
+                        class="absolute top-0 right-0 text-3xl h-14 w-14 
bulletins has-bulletins flex justify-center items-center">
+                        <i class="fa fa-sticky-note-o"></i>
+                    </div>
+                </div>
+            }
         </div>
     </div>
 </h2>
 <form class="processor-edit-form" [formGroup]="editProcessorForm">
     <context-error-banner 
[context]="ErrorContextKey.PROCESSOR"></context-error-banner>
-    <!-- TODO - Stop & Configure -->
     <mat-tab-group [(selectedIndex)]="selectedIndex" 
(selectedIndexChange)="tabChanged($event)">
         <mat-tab label="Settings">
             <mat-dialog-content>
@@ -48,13 +66,13 @@
                         <div class="flex flex-col mb-5">
                             <div>Type</div>
                             <div class="tertiary-color font-medium">
-                                {{ formatType(request.entity) }}
+                                {{ formatType() }}
                             </div>
                         </div>
                         <div class="flex flex-col mb-6">
                             <div>Bundle</div>
                             <div class="tertiary-color font-medium">
-                                {{ formatBundle(request.entity) }}
+                                {{ formatBundle() }}
                             </div>
                         </div>
                         <div class="flex gap-x-4">
@@ -290,19 +308,106 @@
         </mat-tab>
     </mat-tab-group>
     @if ({ value: (saving$ | async)! }; as saving) {
-        <mat-dialog-actions align="end">
-            @if (readonly) {
-                <button mat-flat-button mat-dialog-close>Close</button>
-            } @else {
-                <button mat-button mat-dialog-close>Cancel</button>
-                <button
-                    [disabled]="!editProcessorForm.dirty || 
editProcessorForm.invalid || saving.value"
-                    type="button"
-                    (click)="submitForm()"
-                    mat-flat-button>
-                    <span *nifiSpinner="saving.value">Apply</span>
-                </button>
-            }
+        <mat-dialog-actions align="start">
+            <div class="flex w-full justify-between items-center">
+                <div>
+                    @if (isStoppable()) {
+                        <button
+                            type="button"
+                            [disabled]="!canOperate()"
+                            mat-stroked-button
+                            [matMenuTriggerFor]="operateMenu">
+                            <div class="flex items-center">
+                                <i class="mr-2 success-color-default fa 
fa-play"></i>Running<i
+                                    class="ml-2 -mt-1 fa fa-sort-desc"></i>
+                            </div>
+                        </button>
+                    } @else if (isRunnable()) {
+                        <button
+                            type="button"
+                            [disabled]="!canOperate()"
+                            mat-stroked-button
+                            [matMenuTriggerFor]="operateMenu">
+                            <div class="flex items-center">
+                                <i class="mr-2 error-color-variant fa 
fa-stop"></i>Stopped<i
+                                    class="ml-2 -mt-1 fa fa-sort-desc"></i>
+                            </div>
+                        </button>
+                    } @else if (isDisabled()) {
+                        <button
+                            type="button"
+                            [disabled]="!canOperate()"
+                            mat-stroked-button
+                            [matMenuTriggerFor]="operateMenu">
+                            <div class="flex items-center">
+                                <i class="mr-2 icon icon-enable-false 
primary-color"></i>Disable<i
+                                    class="ml-2 -mt-1 fa fa-sort-desc"></i>
+                            </div>
+                        </button>
+                    } @else if (isStopping()) {
+                        <button type="button" [disabled]="true" 
mat-stroked-button [matMenuTriggerFor]="operateMenu">
+                            <div class="flex items-center">
+                                <i class="mr-2 fa fa-circle-o-notch fa-spin 
primary-color"></i>Stopping ({{
+                                    status.aggregateSnapshot.activeThreadCount
+                                }})
+                            </div>
+                        </button>
+                    } @else if (isValidating()) {
+                        <button type="button" [disabled]="true" 
mat-stroked-button [matMenuTriggerFor]="operateMenu">
+                            <div class="flex items-center">
+                                <i class="mr-2 fa fa-circle-o-notch fa-spin 
primary-color"></i>Validating
+                            </div>
+                        </button>
+                    } @else if (isInvalid()) {
+                        <button
+                            type="button"
+                            mat-stroked-button
+                            [disabled]="!canOperate()"
+                            [matMenuTriggerFor]="operateMenu">
+                            <div class="flex items-center">
+                                <i class="mr-2 fa fa-warning 
caution-color"></i>Invalid<i
+                                    class="ml-2 -mt-1 fa fa-sort-desc"></i>
+                            </div>
+                        </button>
+                    }
+                    <mat-menu #operateMenu="matMenu" xPosition="before">
+                        @if (isStoppable() && canOperate()) {
+                            <button mat-menu-item (click)="stop()">
+                                <i class="mr-2 fa fa-stop 
primary-color"></i>Stop
+                            </button>
+                        }
+                        @if (isRunnable() && canOperate()) {
+                            <button mat-menu-item 
[disabled]="editProcessorForm.dirty" (click)="start()">
+                                <i class="mr-2 fa fa-play 
primary-color"></i>Start
+                            </button>
+                        }
+                        @if (isDisableable() && canOperate()) {
+                            <button mat-menu-item 
[disabled]="editProcessorForm.dirty" (click)="disable()">
+                                <i class="mr-2 icon icon-enable-false 
primary-color"></i>Disable
+                            </button>
+                        }
+                        @if (isEnableable() && canOperate()) {
+                            <button mat-menu-item 
[disabled]="editProcessorForm.dirty" (click)="enable()">
+                                <i class="mr-2 fa fa-flash 
primary-color"></i>Enable
+                            </button>
+                        }
+                    </mat-menu>
+                </div>
+                <div>
+                    @if (readonly) {
+                        <button mat-flat-button mat-dialog-close>Close</button>
+                    } @else {
+                        <button mat-button mat-dialog-close>Cancel</button>
+                        <button
+                            [disabled]="!editProcessorForm.dirty || 
editProcessorForm.invalid || saving.value"
+                            type="button"
+                            (click)="submitForm()"
+                            mat-flat-button>
+                            <span *nifiSpinner="saving.value">Apply</span>
+                        </button>
+                    }
+                </div>
+            </div>
         </mat-dialog-actions>
     }
 </form>
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts
index b238e9ecc8..b1264b036d 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts
@@ -17,6 +17,7 @@
 
 import { Component, EventEmitter, Inject, Input, Output } from '@angular/core';
 import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { MatMenuModule } from '@angular/material/menu';
 import {
     AbstractControl,
     FormBuilder,
@@ -30,19 +31,29 @@ import {
 import { MatInputModule } from '@angular/material/input';
 import { MatCheckboxModule } from '@angular/material/checkbox';
 import { MatButtonModule } from '@angular/material/button';
-import { AsyncPipe } from '@angular/common';
+import { AsyncPipe, NgClass } from '@angular/common';
 import { MatTabsModule } from '@angular/material/tabs';
 import { MatOptionModule } from '@angular/material/core';
 import { MatSelectModule } from '@angular/material/select';
 import { Observable, of } from 'rxjs';
 import {
+    BulletinEntity,
+    BulletinsTipInput,
     InlineServiceCreationRequest,
     InlineServiceCreationResponse,
     ParameterContextEntity,
-    Property
+    Property,
+    Revision
 } from '../../../../../../../state/shared';
 import { Client } from '../../../../../../../service/client.service';
-import { EditComponentDialogRequest, UpdateProcessorRequest } from 
'../../../../../state/flow';
+import {
+    DisableComponentRequest,
+    EditComponentDialogRequest,
+    EnableComponentRequest,
+    StartComponentRequest,
+    StopComponentRequest,
+    UpdateProcessorRequest
+} from '../../../../../state/flow';
 import { PropertyTable } from 
'../../../../../../../ui/common/property-table/property-table.component';
 import { NifiSpinnerDirective } from 
'../../../../../../../ui/common/spinner/nifi-spinner.directive';
 import { NifiTooltipDirective, NiFiCommon, TextTip, CopyDirective } from 
'@nifi/shared';
@@ -51,7 +62,6 @@ import {
     RelationshipConfiguration,
     RelationshipSettings
 } from './relationship-settings/relationship-settings.component';
-import { ErrorBanner } from 
'../../../../../../../ui/common/error-banner/error-banner.component';
 import { ClusterConnectionService } from 
'../../../../../../../service/cluster-connection.service';
 import { CanvasUtils } from '../../../../../service/canvas-utils.service';
 import { ConvertToParameterResponse } from 
'../../../../../service/parameter-helper.service';
@@ -65,6 +75,8 @@ import { TabbedDialog } from 
'../../../../../../../ui/common/tabbed-dialog/tabbe
 import { ComponentType, SelectOption } from 'libs/shared/src';
 import { ErrorContextKey } from '../../../../../../../state/error';
 import { ContextErrorBanner } from 
'../../../../../../../ui/common/context-error-banner/context-error-banner.component';
+import { BulletinsTip } from 
'../../../../../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
+import { ConnectedPosition } from '@angular/cdk/overlay';
 
 @Component({
     selector: 'edit-processor',
@@ -79,20 +91,24 @@ import { ContextErrorBanner } from 
'../../../../../../../ui/common/context-error
         MatTabsModule,
         MatOptionModule,
         MatSelectModule,
+        MatMenuModule,
         AsyncPipe,
         PropertyTable,
         NifiSpinnerDirective,
         NifiTooltipDirective,
         RunDurationSlider,
         RelationshipSettings,
-        ErrorBanner,
         PropertyVerification,
         ContextErrorBanner,
-        CopyDirective
+        CopyDirective,
+        NgClass
     ],
     styleUrls: ['./edit-processor.component.scss']
 })
 export class EditProcessor extends TabbedDialog {
+    @Input() set processorUpdates(processorUpdates: any | undefined) {
+        this.processRunStateUpdates(processorUpdates);
+    }
     @Input() createNewProperty!: (existingProperties: string[], 
allowsSensitive: boolean) => Observable<Property>;
     @Input() createNewService!: (request: InlineServiceCreationRequest) => 
Observable<InlineServiceCreationResponse>;
     @Input() parameterContext: ParameterContextEntity | undefined;
@@ -110,11 +126,20 @@ export class EditProcessor extends TabbedDialog {
 
     @Output() verify: EventEmitter<VerifyPropertiesRequestContext> = new 
EventEmitter<VerifyPropertiesRequestContext>();
     @Output() editProcessor: EventEmitter<UpdateProcessorRequest> = new 
EventEmitter<UpdateProcessorRequest>();
+    @Output() stopComponentRequest: EventEmitter<StopComponentRequest> = new 
EventEmitter<StopComponentRequest>();
+    @Output() startComponentRequest: EventEmitter<StartComponentRequest> = new 
EventEmitter<StartComponentRequest>();
+    @Output() disableComponentRequest: EventEmitter<DisableComponentRequest> =
+        new EventEmitter<DisableComponentRequest>();
+    @Output() enableComponentRequest: EventEmitter<EnableComponentRequest> = 
new EventEmitter<EnableComponentRequest>();
 
     protected readonly TextTip = TextTip;
+    protected readonly BulletinsTip = BulletinsTip;
 
     editProcessorForm: FormGroup;
-    readonly: boolean;
+    readonly: boolean = true;
+    status: any;
+    revision!: Revision;
+    bulletins!: BulletinEntity[];
 
     bulletinLevels = [
         {
@@ -182,9 +207,6 @@ export class EditProcessor extends TabbedDialog {
     ) {
         super('edit-processor-selected-index');
 
-        this.readonly =
-            !request.entity.permissions.canWrite || 
!this.canvasUtils.runnableSupportsModification(request.entity);
-
         const processorProperties: any = 
request.entity.component.config.properties;
         const properties: Property[] = 
Object.entries(processorProperties).map((entry: any) => {
             const [property, value] = entry;
@@ -253,6 +275,32 @@ export class EditProcessor extends TabbedDialog {
                 new FormControl({ value: this.runDurationMillis, disabled: 
this.readonly }, Validators.required)
             );
         }
+
+        this.processRunStateUpdates(request.entity);
+    }
+
+    private processRunStateUpdates(entity: any) {
+        this.status = entity.status;
+        this.revision = entity.revision;
+        this.bulletins = entity.bulletins;
+
+        this.readonly = !entity.permissions.canWrite || 
!this.canvasUtils.runnableSupportsModification(entity);
+
+        if (this.readonly) {
+            this.editProcessorForm.get('properties')?.disable();
+            this.editProcessorForm.get('relationshipConfiguration')?.disable();
+
+            if (this.supportsBatching()) {
+                this.editProcessorForm.get('runDuration')?.disable();
+            }
+        } else {
+            this.editProcessorForm.get('properties')?.enable();
+            this.editProcessorForm.get('relationshipConfiguration')?.enable();
+
+            if (this.supportsBatching()) {
+                this.editProcessorForm.get('runDuration')?.enable();
+            }
+        }
     }
 
     private relationshipConfigurationValidator(): ValidatorFn {
@@ -288,12 +336,12 @@ export class EditProcessor extends TabbedDialog {
         return this.request.entity.component.supportsBatching == true;
     }
 
-    formatType(entity: any): string {
-        return this.nifiCommon.formatType(entity.component);
+    formatType(): string {
+        return this.nifiCommon.formatType(this.request.entity.component);
     }
 
-    formatBundle(entity: any): string {
-        return this.nifiCommon.formatBundle(entity.component.bundle);
+    formatBundle(): string {
+        return 
this.nifiCommon.formatBundle(this.request.entity.component.bundle);
     }
 
     concurrentTasksChanged(): void {
@@ -351,7 +399,10 @@ export class EditProcessor extends TabbedDialog {
             .map((relationship) => relationship.name);
 
         const payload: any = {
-            revision: this.client.getRevision(this.request.entity),
+            revision: this.client.getRevision({
+                ...this.request.entity,
+                revision: this.revision
+            }),
             disconnectedNodeAcknowledged: 
this.clusterConnectionService.isDisconnectionAcknowledged(),
             component: {
                 id: this.request.entity.id,
@@ -401,6 +452,140 @@ export class EditProcessor extends TabbedDialog {
         });
     }
 
+    hasBulletins(): boolean {
+        return this.request.entity.permissions.canRead && 
!this.nifiCommon.isEmpty(this.bulletins);
+    }
+
+    getBulletinsTipData(): BulletinsTipInput {
+        return {
+            bulletins: this.bulletins
+        };
+    }
+
+    getBulletinTooltipPosition(): ConnectedPosition {
+        return {
+            originX: 'end',
+            originY: 'bottom',
+            overlayX: 'end',
+            overlayY: 'top',
+            offsetX: -8,
+            offsetY: 8
+        };
+    }
+
+    getMostSevereBulletinLevel(): string | null {
+        // determine the most severe of the bulletins
+        const mostSevere = 
this.canvasUtils.getMostSevereBulletin(this.bulletins);
+        return mostSevere ? mostSevere.bulletin.level.toLowerCase() : null;
+    }
+
+    isStoppable(): boolean {
+        return this.status.aggregateSnapshot.runStatus === 'Running';
+    }
+
+    isStopping(): boolean {
+        return (
+            this.status.aggregateSnapshot.runStatus === 'Stopped' && 
this.status.aggregateSnapshot.activeThreadCount > 0
+        );
+    }
+
+    isValidating(): boolean {
+        return this.status.aggregateSnapshot.runStatus === 'Validating';
+    }
+
+    isInvalid(): boolean {
+        return this.status.aggregateSnapshot.runStatus === 'Invalid';
+    }
+
+    isDisabled(): boolean {
+        return this.status.aggregateSnapshot.runStatus === 'Disabled';
+    }
+
+    isRunnable(): boolean {
+        return (
+            !(
+                this.status.aggregateSnapshot.runStatus === 'Running' ||
+                this.status.aggregateSnapshot.activeThreadCount > 0
+            ) && this.status.aggregateSnapshot.runStatus === 'Stopped'
+        );
+    }
+
+    isDisableable(): boolean {
+        return (
+            !(
+                this.status.aggregateSnapshot.runStatus === 'Running' ||
+                this.status.aggregateSnapshot.activeThreadCount > 0
+            ) &&
+            (this.status.aggregateSnapshot.runStatus === 'Stopped' ||
+                this.status.aggregateSnapshot.runStatus === 'Invalid')
+        );
+    }
+
+    isEnableable(): boolean {
+        return (
+            !(
+                this.status.aggregateSnapshot.runStatus === 'Running' ||
+                this.status.aggregateSnapshot.activeThreadCount > 0
+            ) && this.status.aggregateSnapshot.runStatus === 'Disabled'
+        );
+    }
+
+    canOperate(): boolean {
+        return this.request.entity.permissions.canWrite || 
this.request.entity.operatePermissions?.canWrite;
+    }
+
+    stop() {
+        this.stopComponentRequest.next({
+            id: this.request.entity.id,
+            uri: this.request.entity.uri,
+            type: ComponentType.Processor,
+            revision: this.client.getRevision({
+                ...this.request.entity,
+                revision: this.revision
+            }),
+            errorStrategy: 'snackbar'
+        });
+    }
+
+    start() {
+        this.startComponentRequest.next({
+            id: this.request.entity.id,
+            uri: this.request.entity.uri,
+            type: ComponentType.Processor,
+            revision: this.client.getRevision({
+                ...this.request.entity,
+                revision: this.revision
+            }),
+            errorStrategy: 'snackbar'
+        });
+    }
+
+    disable() {
+        this.disableComponentRequest.next({
+            id: this.request.entity.id,
+            uri: this.request.entity.uri,
+            type: ComponentType.Processor,
+            revision: this.client.getRevision({
+                ...this.request.entity,
+                revision: this.revision
+            }),
+            errorStrategy: 'snackbar'
+        });
+    }
+
+    enable() {
+        this.enableComponentRequest.next({
+            id: this.request.entity.id,
+            uri: this.request.entity.uri,
+            type: ComponentType.Processor,
+            revision: this.client.getRevision({
+                ...this.request.entity,
+                revision: this.revision
+            }),
+            errorStrategy: 'snackbar'
+        });
+    }
+
     private getModifiedProperties(): ModifiedProperties {
         const propertyControl: AbstractControl | null = 
this.editProcessorForm.get('properties');
         if (propertyControl && propertyControl.dirty) {
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/service/client.service.ts 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/service/client.service.ts
index 99704c4736..fe2d7167d5 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/service/client.service.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/service/client.service.ts
@@ -17,12 +17,24 @@
 
 import { Injectable } from '@angular/core';
 import { v4 as uuidv4 } from 'uuid';
+import { SessionStorageService } from '@nifi/shared';
 
 @Injectable({
     providedIn: 'root'
 })
 export class Client {
-    private clientId: string = uuidv4();
+    private clientId: string;
+
+    constructor(private sessionStorage: SessionStorageService) {
+        let clientId = this.sessionStorage.getItem<string>('clientId');
+
+        if (clientId === null) {
+            clientId = uuidv4();
+            this.sessionStorage.setItem('clientId', clientId);
+        }
+
+        this.clientId = clientId;
+    }
 
     public getClientId(): string {
         return this.clientId;
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/styles.scss 
b/nifi-frontend/src/main/frontend/apps/nifi/src/styles.scss
index 8ce41675cf..3ae320876c 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/styles.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/styles.scss
@@ -33,6 +33,7 @@
     birdseye-control;
 @use 
'app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component-theme'
 as
     operation-control;
+@use 
'app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component-theme'
 as edit-processor;
 @use 
'app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component-theme'
 as flow-status;
 @use 
'app/pages/flow-designer/ui/canvas/header/new-canvas-item/new-canvas-item.component-theme'
 as new-canvas-item;
 @use 'app/pages/flow-designer/ui/canvas/header/search/search.component-theme' 
as search;
@@ -94,6 +95,7 @@ html {
     @include navigation-control.generate-theme($m3-light-theme, 
$m3-light-theme-config);
     @include operation-control.generate-theme($m3-light-theme, 
$m3-light-theme-config);
     @include flow-status.generate-theme($m3-light-theme, 
$m3-light-theme-config);
+    @include edit-processor.generate-theme($m3-light-theme, 
$m3-light-theme-config);
     @include violation-details-dialog.generate-theme($m3-light-theme, 
$m3-light-theme-config);
     @include new-canvas-item.generate-theme($m3-light-theme, 
$m3-light-theme-config);
     @include search.generate-theme($m3-light-theme, $m3-light-theme-config);
@@ -127,6 +129,7 @@ html {
         @include navigation-control.generate-theme($m3-dark-theme, 
$m3-dark-theme-config);
         @include operation-control.generate-theme($m3-dark-theme, 
$m3-dark-theme-config);
         @include flow-status.generate-theme($m3-dark-theme, 
$m3-dark-theme-config);
+        @include edit-processor.generate-theme($m3-dark-theme, 
$m3-dark-theme-config);
         @include violation-details-dialog.generate-theme($m3-dark-theme, 
$m3-dark-theme-config);
         @include new-canvas-item.generate-theme($m3-dark-theme, 
$m3-dark-theme-config);
         @include search.generate-theme($m3-dark-theme, $m3-dark-theme-config);
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/assets/styles/_app.scss 
b/nifi-frontend/src/main/frontend/libs/shared/src/assets/styles/_app.scss
index 58709319c3..89beaac405 100644
--- a/nifi-frontend/src/main/frontend/libs/shared/src/assets/styles/_app.scss
+++ b/nifi-frontend/src/main/frontend/libs/shared/src/assets/styles/_app.scss
@@ -161,6 +161,7 @@
         --mdc-outlined-button-label-text-tracking: normal;
         --mdc-outlined-button-label-text-weight: 400;
         --mat-outlined-button-horizontal-padding: 15px;
+        --mdc-outlined-button-container-height: 32px;
     }
 
     .mat-mdc-tab-header {
diff --git a/nifi-frontend/src/main/frontend/libs/shared/src/services/index.ts 
b/nifi-frontend/src/main/frontend/libs/shared/src/services/index.ts
index 5e5054972e..8572877ade 100644
--- a/nifi-frontend/src/main/frontend/libs/shared/src/services/index.ts
+++ b/nifi-frontend/src/main/frontend/libs/shared/src/services/index.ts
@@ -17,5 +17,6 @@
 
 export * from './nifi-common.service';
 export * from './storage.service';
+export * from './session-storage.service';
 export * from './theming.service';
 export * from './map-table-helper.service';
diff --git a/nifi-frontend/src/main/frontend/libs/shared/src/services/index.ts 
b/nifi-frontend/src/main/frontend/libs/shared/src/services/session-storage.service.spec.ts
similarity index 65%
copy from nifi-frontend/src/main/frontend/libs/shared/src/services/index.ts
copy to 
nifi-frontend/src/main/frontend/libs/shared/src/services/session-storage.service.spec.ts
index 5e5054972e..231824f15d 100644
--- a/nifi-frontend/src/main/frontend/libs/shared/src/services/index.ts
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/services/session-storage.service.spec.ts
@@ -15,7 +15,19 @@
  * limitations under the License.
  */
 
-export * from './nifi-common.service';
-export * from './storage.service';
-export * from './theming.service';
-export * from './map-table-helper.service';
+import { TestBed } from '@angular/core/testing';
+
+import { SessionStorageService } from './session-storage.service';
+
+describe('SessionStorageService', () => {
+    let service: SessionStorageService;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({});
+        service = TestBed.inject(SessionStorageService);
+    });
+
+    it('should be created', () => {
+        expect(service).toBeTruthy();
+    });
+});
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/services/session-storage.service.ts
 
b/nifi-frontend/src/main/frontend/libs/shared/src/services/session-storage.service.ts
new file mode 100644
index 0000000000..31d36ac443
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/services/session-storage.service.ts
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Injectable } from '@angular/core';
+
+interface SessionStorageEntry<T> {
+    item: T;
+}
+
+@Injectable({
+    providedIn: 'root'
+})
+export class SessionStorageService {
+    /**
+     * Gets an entry for the key. The entry expiration is not checked.
+     *
+     * @param {string} key
+     */
+    private getEntry<T>(key: string): null | SessionStorageEntry<T> {
+        try {
+            // parse the entry
+            const item = sessionStorage.getItem(key);
+            if (!item) {
+                return null;
+            }
+
+            const entry = JSON.parse(item);
+
+            // ensure the entry is present
+            if (entry) {
+                return entry;
+            } else {
+                return null;
+            }
+        } catch (e) {
+            return null;
+        }
+    }
+
+    /**
+     * Stores the specified item.
+     *
+     * @param {string} key
+     * @param {object} item
+     */
+    public setItem<T>(key: string, item: T): void {
+        // create the entry
+        const entry: SessionStorageEntry<T> = {
+            item
+        };
+
+        // store the item
+        sessionStorage.setItem(key, JSON.stringify(entry));
+    }
+
+    /**
+     * Returns whether there is an entry for this key. This will not check the 
expiration. If
+     * the entry is expired, it will return null on a subsequent getItem 
invocation.
+     *
+     * @param {string} key
+     * @returns {boolean}
+     */
+    public hasItem(key: string): boolean {
+        return this.getEntry(key) !== null;
+    }
+
+    /**
+     * Gets the item with the specified key. If an item with this key does
+     * not exist, null is returned. If an item exists but cannot be parsed
+     * or is malformed/unrecognized, null is returned.
+     *
+     * @param {type} key
+     */
+    public getItem<T>(key: string): null | T {
+        const entry: SessionStorageEntry<T> | null = this.getEntry(key);
+        if (entry === null) {
+            return null;
+        }
+
+        // if the entry has the specified field return its value
+        if (entry['item']) {
+            return entry['item'];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Removes the item with the specified key.
+     *
+     * @param {string} key
+     */
+    public removeItem(key: string): void {
+        sessionStorage.removeItem(key);
+    }
+}

Reply via email to