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 ecb87149fe NIFI-12611: (#8296)
ecb87149fe is described below

commit ecb87149fe6baea4a6707d98d491482461bcb8eb
Author: Matt Gilman <matt.c.gil...@gmail.com>
AuthorDate: Thu Jan 25 14:53:17 2024 -0500

    NIFI-12611: (#8296)
    
    - Component State.
    
    This closes #8296
---
 .../src/main/nifi/src/app/app.module.ts            |   4 +-
 .../service/canvas-context-menu.service.ts         |  17 ++-
 .../flow-designer/service/canvas-utils.service.ts  |  22 +++
 .../controller-services.component.html             |   1 +
 .../controller-services.component.spec.ts          |  10 +-
 .../controller-services.component.ts               |  13 ++
 .../flow-analysis-rule-table.component.html        |   6 +-
 .../flow-analysis-rule-table.component.ts          |   6 +
 .../flow-analysis-rules.component.html             |   1 +
 .../flow-analysis-rules.component.ts               |  14 ++
 .../management-controller-services.component.html  |   1 +
 .../management-controller-services.component.ts    |  13 ++
 .../reporting-task-table.component.html            |   6 +-
 .../reporting-task-table.component.ts              |   5 +
 .../reporting-tasks/reporting-tasks.component.html |   1 +
 .../reporting-tasks/reporting-tasks.component.ts   |  14 ++
 .../src/app/service/component-state.service.ts     |  51 +++++++
 .../component-state/component-state.actions.ts     |  49 ++++++
 .../component-state/component-state.effects.ts     | 139 +++++++++++++++++
 .../component-state.reducer.ts}                    |  53 +++----
 .../component-state/component-state.selectors.ts   |  38 +++++
 .../nifi/src/app/state/component-state/index.ts    |  65 ++++++++
 .../src/main/nifi/src/app/state/index.ts           |   6 +-
 .../state/status-history/status-history.reducer.ts |   1 -
 .../component-state/component-state.component.html |  86 +++++++++++
 .../component-state/component-state.component.scss |  32 ++++
 .../component-state.component.spec.ts}             |  23 ++-
 .../component-state/component-state.component.ts   | 169 +++++++++++++++++++++
 .../controller-service-table.component.html        |   6 +-
 .../controller-service-table.component.ts          |   6 +
 .../extension-creation.component.html              |   1 -
 31 files changed, 803 insertions(+), 56 deletions(-)

diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts
index 4fde32890c..749a8dda7a 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts
@@ -41,6 +41,7 @@ import { MatDialogModule } from '@angular/material/dialog';
 import { ControllerServiceStateEffects } from 
'./state/contoller-service-state/controller-service-state.effects';
 import { SystemDiagnosticsEffects } from 
'./state/system-diagnostics/system-diagnostics.effects';
 import { FlowConfigurationEffects } from 
'./state/flow-configuration/flow-configuration.effects';
+import { ComponentStateEffects } from 
'./state/component-state/component-state.effects';
 
 @NgModule({
     declarations: [AppComponent],
@@ -65,7 +66,8 @@ import { FlowConfigurationEffects } from 
'./state/flow-configuration/flow-config
             FlowConfigurationEffects,
             StatusHistoryEffects,
             ControllerServiceStateEffects,
-            SystemDiagnosticsEffects
+            SystemDiagnosticsEffects,
+            ComponentStateEffects
         ),
         StoreDevtoolsModule.instrument({
             maxAge: 25,
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 1210caef12..5e27e1cab8 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
@@ -55,6 +55,7 @@ import {
     ContextMenuItemDefinition
 } from '../../../ui/common/context-menu/context-menu.component';
 import { promptEmptyQueueRequest, promptEmptyQueuesRequest } from 
'../state/queue/queue.actions';
+import { getComponentStateAndOpenDialog } from 
'../../../state/component-state/component-state.actions';
 
 @Injectable({ providedIn: 'root' })
 export class CanvasContextMenu implements ContextMenuDefinitionProvider {
@@ -679,13 +680,21 @@ export class CanvasContextMenu implements 
ContextMenuDefinitionProvider {
             },
             {
                 condition: (selection: any) => {
-                    // TODO - isStatefulProcessor
-                    return false;
+                    return this.canvasUtils.isStatefulProcessor(selection);
                 },
                 clazz: 'fa fa-tasks',
                 text: 'View state',
-                action: () => {
-                    // TODO - viewState
+                action: (selection: any) => {
+                    const selectionData = selection.datum();
+                    this.store.dispatch(
+                        getComponentStateAndOpenDialog({
+                            request: {
+                                componentName: selectionData.component.name,
+                                componentUri: selectionData.uri,
+                                canClear: 
this.canvasUtils.isConfigurable(selection)
+                            }
+                        })
+                    );
                 }
             },
             {
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 4e51520609..1f13e28667 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
@@ -464,6 +464,28 @@ export class CanvasUtils {
         return selection.size() === 1 && selection.classed('funnel');
     }
 
+    /**
+     * Determines whether the current selection is a stateful processor.
+     *
+     * @param {selection} selection
+     */
+    public isStatefulProcessor(selection: any): boolean {
+        // ensure the correct number of components are selected
+        if (selection.size() !== 1) {
+            return false;
+        }
+        if (this.canRead(selection) === false || this.canModify(selection) === 
false) {
+            return false;
+        }
+
+        if (this.isProcessor(selection)) {
+            const processorData: any = selection.datum();
+            return processorData.component.persistsState === true;
+        } else {
+            return false;
+        }
+    }
+
     /**
      * Determines whether the user can configure or open the policy management 
page.
      */
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/controller-service/controller-services.component.html
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.html
index b950e7567c..0b3b547be5 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.html
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.html
@@ -48,6 +48,7 @@
                             
(configureControllerService)="configureControllerService($event)"
                             
(enableControllerService)="enableControllerService($event)"
                             
(disableControllerService)="disableControllerService($event)"
+                            
(viewStateControllerService)="viewStateControllerService($event)"
                             
(deleteControllerService)="deleteControllerService($event)"></controller-service-table>
                     </div>
                     <div class="flex justify-between">
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/controller-service/controller-services.component.spec.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts
index 435846b74a..34cf6502be 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts
@@ -21,15 +21,23 @@ import { ControllerServices } from 
'./controller-services.component';
 import { provideMockStore } from '@ngrx/store/testing';
 import { initialState } from 
'../../state/controller-services/controller-services.reducer';
 import { RouterTestingModule } from '@angular/router/testing';
+import { Component } from '@angular/core';
 
 describe('ControllerServices', () => {
     let component: ControllerServices;
     let fixture: ComponentFixture<ControllerServices>;
 
+    @Component({
+        selector: 'navigation',
+        standalone: true,
+        template: ''
+    })
+    class MockNavigation {}
+
     beforeEach(() => {
         TestBed.configureTestingModule({
             declarations: [ControllerServices],
-            imports: [RouterTestingModule],
+            imports: [RouterTestingModule, MockNavigation],
             providers: [
                 provideMockStore({
                     initialState
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/controller-service/controller-services.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/controller-service/controller-services.component.ts
index 0f2b78db14..6117b2894c 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.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/controller-service/controller-services.component.ts
@@ -45,6 +45,7 @@ import { selectCurrentUser } from 
'../../../../state/current-user/current-user.s
 import { selectFlowConfiguration } from 
'../../../../state/flow-configuration/flow-configuration.selectors';
 import { NiFiState } from '../../../../state';
 import { loadFlowConfiguration } from 
'../../../../state/flow-configuration/flow-configuration.actions';
+import { getComponentStateAndOpenDialog } from 
'../../../../state/component-state/component-state.actions';
 
 @Component({
     selector: 'controller-services',
@@ -189,6 +190,18 @@ export class ControllerServices implements OnInit, 
OnDestroy {
         );
     }
 
+    viewStateControllerService(entity: ControllerServiceEntity): void {
+        this.store.dispatch(
+            getComponentStateAndOpenDialog({
+                request: {
+                    componentUri: entity.uri,
+                    componentName: entity.component.name,
+                    canClear: entity.component.state === 'DISABLED'
+                }
+            })
+        );
+    }
+
     deleteControllerService(entity: ControllerServiceEntity): void {
         this.store.dispatch(
             promptControllerServiceDeletion({
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rule-table/flow-analysis-rule-table.component.html
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rule-table/flow-analysis-rule-table.component.html
index 32802aa136..1b0d10065a 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rule-table/flow-analysis-rule-table.component.html
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rule-table/flow-analysis-rule-table.component.html
@@ -134,7 +134,11 @@
                             *ngIf="canDelete(item)"
                             (click)="deleteClicked(item)"
                             title="Delete"></div>
-                        <div class="pointer fa fa-tasks" 
*ngIf="canViewState(item)" title="View State"></div>
+                        <div
+                            class="pointer fa fa-tasks"
+                            *ngIf="canViewState(item)"
+                            (click)="viewStateClicked(item)"
+                            title="View State"></div>
                     </div>
                 </td>
             </ng-container>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rule-table/flow-analysis-rule-table.component.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rule-table/flow-analysis-rule-table.component.ts
index 5108c5d352..16869ec6ef 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rule-table/flow-analysis-rule-table.component.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rule-table/flow-analysis-rule-table.component.ts
@@ -60,6 +60,8 @@ export class FlowAnalysisRuleTable {
     @Output() configureFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> =
         new EventEmitter<FlowAnalysisRuleEntity>();
     @Output() enableFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = 
new EventEmitter<FlowAnalysisRuleEntity>();
+    @Output() viewStateFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> =
+        new EventEmitter<FlowAnalysisRuleEntity>();
     @Output() disableFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> =
         new EventEmitter<FlowAnalysisRuleEntity>();
 
@@ -255,6 +257,10 @@ export class FlowAnalysisRuleTable {
         return this.canRead(entity) && this.canWrite(entity) && 
entity.component.persistsState === true;
     }
 
+    viewStateClicked(entity: FlowAnalysisRuleEntity): void {
+        this.viewStateFlowAnalysisRule.next(entity);
+    }
+
     select(entity: FlowAnalysisRuleEntity): void {
         this.selectFlowAnalysisRule.next(entity);
     }
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rules.component.html
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rules.component.html
index 219b7dd22e..0c1b2e96bf 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rules.component.html
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rules.component.html
@@ -35,6 +35,7 @@
                     (selectFlowAnalysisRule)="selectFlowAnalysisRule($event)"
                     (enableFlowAnalysisRule)="enableFlowAnalysisRule($event)"
                     (disableFlowAnalysisRule)="disableFlowAnalysisRule($event)"
+                    
(viewStateFlowAnalysisRule)="viewStateFlowAnalysisRule($event)"
                     
(deleteFlowAnalysisRule)="deleteFlowAnalysisRule($event)"></flow-analysis-rule-table>
             </div>
             <div class="flex justify-between">
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rules.component.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rules.component.ts
index 42f882ceba..c1596d7295 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rules.component.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rules.component.ts
@@ -41,6 +41,7 @@ import { selectCurrentUser } from 
'../../../../state/current-user/current-user.s
 import { NiFiState } from '../../../../state';
 import { FlowAnalysisRuleEntity, FlowAnalysisRulesState } from 
'../../state/flow-analysis-rules';
 import { CurrentUser } from '../../../../state/current-user';
+import { getComponentStateAndOpenDialog } from 
'../../../../state/component-state/component-state.actions';
 
 @Component({
     selector: 'flow-analysis-rules',
@@ -128,6 +129,19 @@ export class FlowAnalysisRules implements OnInit, 
OnDestroy {
         );
     }
 
+    viewStateFlowAnalysisRule(entity: FlowAnalysisRuleEntity): void {
+        const canClear: boolean = entity.status.runStatus === 'DISABLED';
+        this.store.dispatch(
+            getComponentStateAndOpenDialog({
+                request: {
+                    componentUri: entity.uri,
+                    componentName: entity.component.name,
+                    canClear
+                }
+            })
+        );
+    }
+
     deleteFlowAnalysisRule(entity: FlowAnalysisRuleEntity): void {
         this.store.dispatch(
             promptFlowAnalysisRuleDeletion({
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/management-controller-services/management-controller-services.component.html
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/management-controller-services/management-controller-services.component.html
index 9545cb37ca..42de3d7de0 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/management-controller-services/management-controller-services.component.html
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/management-controller-services/management-controller-services.component.html
@@ -39,6 +39,7 @@
                     
(configureControllerService)="configureControllerService($event)"
                     (enableControllerService)="enableControllerService($event)"
                     
(disableControllerService)="disableControllerService($event)"
+                    
(viewStateControllerService)="viewStateControllerService($event)"
                     
(deleteControllerService)="deleteControllerService($event)"></controller-service-table>
             </div>
             <div class="flex justify-between">
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/management-controller-services/management-controller-services.component.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/management-controller-services/management-controller-services.component.ts
index 403bef96f7..7635b648cb 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/management-controller-services/management-controller-services.component.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/management-controller-services/management-controller-services.component.ts
@@ -44,6 +44,7 @@ import { NiFiState } from '../../../../state';
 import { selectFlowConfiguration } from 
'../../../../state/flow-configuration/flow-configuration.selectors';
 import { loadFlowConfiguration } from 
'../../../../state/flow-configuration/flow-configuration.actions';
 import { CurrentUser } from '../../../../state/current-user';
+import { getComponentStateAndOpenDialog } from 
'../../../../state/component-state/component-state.actions';
 
 @Component({
     selector: 'management-controller-services',
@@ -139,6 +140,18 @@ export class ManagementControllerServices implements 
OnInit, OnDestroy {
         );
     }
 
+    viewStateControllerService(entity: ControllerServiceEntity): void {
+        this.store.dispatch(
+            getComponentStateAndOpenDialog({
+                request: {
+                    componentUri: entity.uri,
+                    componentName: entity.component.name,
+                    canClear: entity.component.state === 'DISABLED'
+                }
+            })
+        );
+    }
+
     deleteControllerService(entity: ControllerServiceEntity): void {
         this.store.dispatch(
             promptControllerServiceDeletion({
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-task-table/reporting-task-table.component.html
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-task-table/reporting-task-table.component.html
index c0265f5d1b..ca78eaeaa7 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-task-table/reporting-task-table.component.html
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-task-table/reporting-task-table.component.html
@@ -124,7 +124,11 @@
                             *ngIf="canDelete(item)"
                             (click)="deleteClicked(item)"
                             title="Delete"></div>
-                        <div class="pointer fa fa-tasks" 
*ngIf="canViewState(item)" title="View State"></div>
+                        <div
+                            class="pointer fa fa-tasks"
+                            *ngIf="canViewState(item)"
+                            (click)="viewStateClicked(item)"
+                            title="View State"></div>
                         <div
                             class="pointer fa fa-key"
                             *ngIf="canManageAccessPolicies()"
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-task-table/reporting-task-table.component.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-task-table/reporting-task-table.component.ts
index ca03470c8c..a09304f2ee 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-task-table/reporting-task-table.component.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-task-table/reporting-task-table.component.ts
@@ -51,6 +51,7 @@ export class ReportingTaskTable {
     @Output() deleteReportingTask: EventEmitter<ReportingTaskEntity> = new 
EventEmitter<ReportingTaskEntity>();
     @Output() startReportingTask: EventEmitter<ReportingTaskEntity> = new 
EventEmitter<ReportingTaskEntity>();
     @Output() configureReportingTask: EventEmitter<ReportingTaskEntity> = new 
EventEmitter<ReportingTaskEntity>();
+    @Output() viewStateReportingTask: EventEmitter<ReportingTaskEntity> = new 
EventEmitter<ReportingTaskEntity>();
     @Output() stopReportingTask: EventEmitter<ReportingTaskEntity> = new 
EventEmitter<ReportingTaskEntity>();
 
     protected readonly TextTip = TextTip;
@@ -233,6 +234,10 @@ export class ReportingTaskTable {
         return this.canRead(entity) && this.canWrite(entity) && 
entity.component.persistsState === true;
     }
 
+    viewStateClicked(entity: ReportingTaskEntity): void {
+        this.viewStateReportingTask.next(entity);
+    }
+
     canManageAccessPolicies(): boolean {
         return this.flowConfiguration.supportsManagedAuthorizer && 
this.currentUser.tenantsPermissions.canRead;
     }
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.html
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.html
index 771e6bc830..3414c21b1d 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.html
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.html
@@ -33,6 +33,7 @@
                     [currentUser]="currentUser"
                     [flowConfiguration]="(flowConfiguration$ | async)!"
                     (configureReportingTask)="configureReportingTask($event)"
+                    (viewStateReportingTask)="viewStateReportingTask($event)"
                     (selectReportingTask)="selectReportingTask($event)"
                     (deleteReportingTask)="deleteReportingTask($event)"
                     (stopReportingTask)="stopReportingTask($event)"
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.ts
index c35e36fee8..fd8c6471cf 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.ts
@@ -42,6 +42,7 @@ import { selectCurrentUser } from 
'../../../../state/current-user/current-user.s
 import { NiFiState } from '../../../../state';
 import { loadFlowConfiguration } from 
'../../../../state/flow-configuration/flow-configuration.actions';
 import { selectFlowConfiguration } from 
'../../../../state/flow-configuration/flow-configuration.selectors';
+import { getComponentStateAndOpenDialog } from 
'../../../../state/component-state/component-state.actions';
 
 @Component({
     selector: 'reporting-tasks',
@@ -127,6 +128,19 @@ export class ReportingTasks implements OnInit, OnDestroy {
         );
     }
 
+    viewStateReportingTask(entity: ReportingTaskEntity): void {
+        const canClear: boolean = entity.status.runStatus === 'STOPPED' && 
entity.status.activeThreadCount === 0;
+        this.store.dispatch(
+            getComponentStateAndOpenDialog({
+                request: {
+                    componentUri: entity.uri,
+                    componentName: entity.component.name,
+                    canClear
+                }
+            })
+        );
+    }
+
     startReportingTask(entity: ReportingTaskEntity): void {
         this.store.dispatch(
             startReportingTask({
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/component-state.service.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/component-state.service.ts
new file mode 100644
index 0000000000..f87d93b7b4
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/component-state.service.ts
@@ -0,0 +1,51 @@
+/*
+ *  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';
+import { HttpClient } from '@angular/common/http';
+import { ClearComponentStateRequest, LoadComponentStateRequest } from 
'../state/component-state';
+import { Observable } from 'rxjs';
+import { NiFiCommon } from './nifi-common.service';
+
+@Injectable({ providedIn: 'root' })
+export class ComponentStateService {
+    constructor(
+        private httpClient: HttpClient,
+        private nifiCommon: NiFiCommon
+    ) {}
+
+    /**
+     * The NiFi model contain the url for each component. That URL is an 
absolute URL. Angular CSRF handling
+     * does not work on absolute URLs, so we need to strip off the proto for 
the request header to be added.
+     *
+     * https://stackoverflow.com/a/59586462
+     *
+     * @param url
+     * @private
+     */
+    private stripProtocol(url: string): string {
+        return this.nifiCommon.substringAfterFirst(url, ':');
+    }
+
+    getComponentState(request: LoadComponentStateRequest): Observable<any> {
+        return 
this.httpClient.get(`${this.stripProtocol(request.componentUri)}/state`);
+    }
+
+    clearComponentState(request: ClearComponentStateRequest): Observable<any> {
+        return 
this.httpClient.post(`${this.stripProtocol(request.componentUri)}/state/clear-requests`,
 {});
+    }
+}
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.actions.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.actions.ts
new file mode 100644
index 0000000000..bb2ee31219
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.actions.ts
@@ -0,0 +1,49 @@
+/*
+ *  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 { createAction, props } from '@ngrx/store';
+import { ComponentStateRequest, ComponentStateResponse } from './index';
+
+const COMPONENT_STATE_PREFIX = '[Component State]';
+
+export const getComponentStateAndOpenDialog = createAction(
+    `${COMPONENT_STATE_PREFIX} Get Component State and Open Dialog`,
+    props<{ request: ComponentStateRequest }>()
+);
+
+export const loadComponentStateSuccess = createAction(
+    `${COMPONENT_STATE_PREFIX} Load Component State Success`,
+    props<{ response: ComponentStateResponse }>()
+);
+
+export const openComponentStateDialog = 
createAction(`${COMPONENT_STATE_PREFIX} Open Component State Dialog`);
+
+export const componentStateApiError = createAction(
+    `${COMPONENT_STATE_PREFIX} Component State API error`,
+    props<{ error: string }>()
+);
+
+export const clearComponentState = createAction(`${COMPONENT_STATE_PREFIX} 
Clear Component State`);
+
+export const reloadComponentState = createAction(`${COMPONENT_STATE_PREFIX} 
Reload Component State`);
+
+export const reloadComponentStateSuccess = createAction(
+    `${COMPONENT_STATE_PREFIX} Reload Component State Success`,
+    props<{ response: ComponentStateResponse }>()
+);
+
+export const resetComponentState = createAction(`${COMPONENT_STATE_PREFIX} 
Reset Component State`);
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.effects.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.effects.ts
new file mode 100644
index 0000000000..5f02df85a2
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.effects.ts
@@ -0,0 +1,139 @@
+/*
+ *  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';
+import { Actions, concatLatestFrom, createEffect, ofType } from 
'@ngrx/effects';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../index';
+import * as ComponentStateActions from './component-state.actions';
+import { catchError, from, map, of, switchMap, tap } from 'rxjs';
+import { MatDialog } from '@angular/material/dialog';
+import { ComponentStateService } from '../../service/component-state.service';
+import { ComponentStateDialog } from 
'../../ui/common/component-state/component-state.component';
+import { resetComponentState } from './component-state.actions';
+import { selectComponentUri } from './component-state.selectors';
+import { isDefinedAndNotNull } from '../shared';
+
+@Injectable()
+export class ComponentStateEffects {
+    constructor(
+        private actions$: Actions,
+        private store: Store<NiFiState>,
+        private componentStateService: ComponentStateService,
+        private dialog: MatDialog
+    ) {}
+
+    getComponentStateAndOpenDialog$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ComponentStateActions.getComponentStateAndOpenDialog),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(
+                    this.componentStateService.getComponentState({ 
componentUri: request.componentUri }).pipe(
+                        map((response: any) =>
+                            ComponentStateActions.loadComponentStateSuccess({
+                                response: {
+                                    componentState: response.componentState
+                                }
+                            })
+                        ),
+                        catchError((error) =>
+                            of(
+                                ComponentStateActions.componentStateApiError({
+                                    error: error.error
+                                })
+                            )
+                        )
+                    )
+                )
+            )
+        )
+    );
+
+    loadComponentStateSuccess$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ComponentStateActions.loadComponentStateSuccess),
+            map((action) => action.response),
+            switchMap((response) => 
of(ComponentStateActions.openComponentStateDialog()))
+        )
+    );
+
+    openComponentStateDialog$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(ComponentStateActions.openComponentStateDialog),
+                tap(() => {
+                    const dialogReference = 
this.dialog.open(ComponentStateDialog, {
+                        panelClass: 'large-dialog'
+                    });
+
+                    dialogReference.afterClosed().subscribe((response) => {
+                        this.store.dispatch(resetComponentState());
+                    });
+                })
+            ),
+        { dispatch: false }
+    );
+
+    clearComponentState$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ComponentStateActions.clearComponentState),
+            concatLatestFrom(() => 
this.store.select(selectComponentUri).pipe(isDefinedAndNotNull())),
+            switchMap(([action, componentUri]) =>
+                from(
+                    this.componentStateService.clearComponentState({ 
componentUri }).pipe(
+                        map((response: any) => 
ComponentStateActions.reloadComponentState()),
+                        catchError((error) =>
+                            of(
+                                ComponentStateActions.componentStateApiError({
+                                    error: error.error
+                                })
+                            )
+                        )
+                    )
+                )
+            )
+        )
+    );
+
+    reloadComponentState$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ComponentStateActions.reloadComponentState),
+            concatLatestFrom(() => 
this.store.select(selectComponentUri).pipe(isDefinedAndNotNull())),
+            switchMap(([action, componentUri]) =>
+                from(
+                    this.componentStateService.getComponentState({ 
componentUri }).pipe(
+                        map((response: any) =>
+                            ComponentStateActions.reloadComponentStateSuccess({
+                                response: {
+                                    componentState: response.componentState
+                                }
+                            })
+                        ),
+                        catchError((error) =>
+                            of(
+                                ComponentStateActions.componentStateApiError({
+                                    error: error.error
+                                })
+                            )
+                        )
+                    )
+                )
+            )
+        )
+    );
+}
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/status-history/status-history.reducer.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.reducer.ts
similarity index 54%
copy from 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/status-history/status-history.reducer.ts
copy to 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.reducer.ts
index 7f41618bf3..d8ee39c266 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/status-history/status-history.reducer.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.reducer.ts
@@ -15,53 +15,46 @@
  *  limitations under the License.
  */
 
-import { StatusHistoryEntity, StatusHistoryState } from './index';
+import { ComponentStateState } from './index';
 import { createReducer, on } from '@ngrx/store';
 import {
-    clearStatusHistory,
-    reloadStatusHistory,
-    loadStatusHistorySuccess,
-    statusHistoryApiError,
-    viewStatusHistoryComplete,
-    reloadStatusHistorySuccess,
-    getStatusHistoryAndOpenDialog
-} from './status-history.actions';
-import { produce } from 'immer';
-
-export const initialState: StatusHistoryState = {
-    statusHistory: {} as StatusHistoryEntity,
+    resetComponentState,
+    loadComponentStateSuccess,
+    componentStateApiError,
+    getComponentStateAndOpenDialog,
+    reloadComponentStateSuccess
+} from './component-state.actions';
+
+export const initialState: ComponentStateState = {
+    componentName: null,
+    componentUri: null,
+    componentState: null,
+    canClear: null,
     status: 'pending',
-    error: null,
-    loadedTimestamp: ''
+    error: null
 };
 
-export const statusHistoryReducer = createReducer(
+export const componentStateReducer = createReducer(
     initialState,
-
-    on(reloadStatusHistory, getStatusHistoryAndOpenDialog, (state) => ({
+    on(getComponentStateAndOpenDialog, (state, { request }) => ({
         ...state,
+        componentName: request.componentName,
+        componentUri: request.componentUri,
+        canClear: request.canClear,
         status: 'loading' as const
     })),
-
-    on(loadStatusHistorySuccess, reloadStatusHistorySuccess, (state, { 
response }) => ({
+    on(loadComponentStateSuccess, reloadComponentStateSuccess, (state, { 
response }) => ({
         ...state,
         error: null,
         status: 'success' as const,
-        loadedTimestamp: response.statusHistory.statusHistory.generated,
-        statusHistory: response.statusHistory
+        componentState: response.componentState
     })),
-
-    on(statusHistoryApiError, (state, { error }) => ({
+    on(componentStateApiError, (state, { error }) => ({
         ...state,
         error,
         status: 'error' as const
     })),
-
-    on(clearStatusHistory, (state) => ({
-        ...initialState
-    })),
-
-    on(viewStatusHistoryComplete, (state) => ({
+    on(resetComponentState, (state) => ({
         ...initialState
     }))
 );
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.selectors.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.selectors.ts
new file mode 100644
index 0000000000..e418e0d8a9
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.selectors.ts
@@ -0,0 +1,38 @@
+/*
+ *  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 { createFeatureSelector, createSelector } from '@ngrx/store';
+import { componentStateFeatureKey, ComponentStateState } from './index';
+
+export const selectComponentStateState = 
createFeatureSelector<ComponentStateState>(componentStateFeatureKey);
+
+export const selectComponentState = createSelector(
+    selectComponentStateState,
+    (state: ComponentStateState) => state.componentState
+);
+
+export const selectComponentName = createSelector(
+    selectComponentStateState,
+    (state: ComponentStateState) => state.componentName
+);
+
+export const selectComponentUri = createSelector(
+    selectComponentStateState,
+    (state: ComponentStateState) => state.componentUri
+);
+
+export const selectCanClear = createSelector(selectComponentStateState, 
(state: ComponentStateState) => state.canClear);
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/index.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/index.ts
new file mode 100644
index 0000000000..7335e99fba
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/index.ts
@@ -0,0 +1,65 @@
+/*
+ *  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.
+ */
+
+export const componentStateFeatureKey = 'componentState';
+
+export interface ComponentStateRequest {
+    componentName: string;
+    componentUri: string;
+    canClear: boolean;
+}
+
+export interface LoadComponentStateRequest {
+    componentUri: string;
+}
+
+export interface ClearComponentStateRequest {
+    componentUri: string;
+}
+
+export interface ComponentStateResponse {
+    componentState: ComponentState;
+}
+
+export interface StateEntry {
+    key: string;
+    value: string;
+    clusterNodeId?: string;
+    clusterNodeAddress?: string;
+}
+
+export interface StateMap {
+    scope: string;
+    state: StateEntry[];
+    totalEntryCount: number;
+}
+
+export interface ComponentState {
+    componentId: string;
+    localState?: StateMap;
+    clusterState?: StateMap;
+    stateDescription: string;
+}
+
+export interface ComponentStateState {
+    componentName: string | null;
+    componentUri: string | null;
+    componentState: ComponentState | null;
+    canClear: boolean | null;
+    error: string | null;
+    status: 'pending' | 'loading' | 'error' | 'success';
+}
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
index e4b7452e89..4ae6619448 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
@@ -31,6 +31,8 @@ import { systemDiagnosticsFeatureKey, SystemDiagnosticsState 
} from './system-di
 import { systemDiagnosticsReducer } from 
'./system-diagnostics/system-diagnostics.reducer';
 import { flowConfigurationFeatureKey, FlowConfigurationState } from 
'./flow-configuration';
 import { flowConfigurationReducer } from 
'./flow-configuration/flow-configuration.reducer';
+import { componentStateFeatureKey, ComponentStateState } from 
'./component-state';
+import { componentStateReducer } from 
'./component-state/component-state.reducer';
 
 export interface NiFiState {
     router: RouterReducerState;
@@ -41,6 +43,7 @@ export interface NiFiState {
     [statusHistoryFeatureKey]: StatusHistoryState;
     [controllerServiceStateFeatureKey]: ControllerServiceState;
     [systemDiagnosticsFeatureKey]: SystemDiagnosticsState;
+    [componentStateFeatureKey]: ComponentStateState;
 }
 
 export const rootReducers: ActionReducerMap<NiFiState> = {
@@ -51,5 +54,6 @@ export const rootReducers: ActionReducerMap<NiFiState> = {
     [flowConfigurationFeatureKey]: flowConfigurationReducer,
     [statusHistoryFeatureKey]: statusHistoryReducer,
     [controllerServiceStateFeatureKey]: controllerServiceStateReducer,
-    [systemDiagnosticsFeatureKey]: systemDiagnosticsReducer
+    [systemDiagnosticsFeatureKey]: systemDiagnosticsReducer,
+    [componentStateFeatureKey]: componentStateReducer
 };
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/status-history/status-history.reducer.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/status-history/status-history.reducer.ts
index 7f41618bf3..3f565207e0 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/status-history/status-history.reducer.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/status-history/status-history.reducer.ts
@@ -26,7 +26,6 @@ import {
     reloadStatusHistorySuccess,
     getStatusHistoryAndOpenDialog
 } from './status-history.actions';
-import { produce } from 'immer';
 
 export const initialState: StatusHistoryState = {
     statusHistory: {} as StatusHistoryEntity,
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.html
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.html
new file mode 100644
index 0000000000..b04dbeea01
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.html
@@ -0,0 +1,86 @@
+<!--
+  ~ 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.
+  -->
+
+<div class="component-state-dialog" tabindex="0">
+    <h2 mat-dialog-title>Component State</h2>
+    <mat-dialog-content>
+        <div class="flex flex-col justify-between gap-y-5">
+            <div class="flex flex-col" *ngIf="componentName$ | async; let 
componentName">
+                <div>Name</div>
+                <div class="value">{{ componentName }}</div>
+            </div>
+            <div class="flex flex-col">
+                <div>Description</div>
+                <div class="value">
+                    {{ stateDescription }}
+                </div>
+            </div>
+            <div class="listing-table">
+                <form [formGroup]="filterForm" class="flex flex-col gap-y-2">
+                    <div class="value">Displaying {{ filteredEntries }} of {{ 
totalEntries }}</div>
+                    <div class="flex justify-between items-center">
+                        <mat-form-field>
+                            <mat-label>Filter</mat-label>
+                            <input matInput type="text" class="small" 
formControlName="filterTerm" />
+                        </mat-form-field>
+                        <ng-container *ngIf="{ value: (canClear$ | async)! } 
as canClear">
+                            <div *ngIf="canClear.value && totalEntries > 0">
+                                <a (click)="clearState()">Clear state</a>
+                            </div>
+                        </ng-container>
+                    </div>
+                </form>
+                <div class="h-72 overflow-y-auto overflow-x-hidden border">
+                    <table
+                        mat-table
+                        [dataSource]="dataSource"
+                        matSort
+                        matSortDisableClear
+                        (matSortChange)="sortData($event)"
+                        [matSortActive]="initialSortColumn"
+                        [matSortDirection]="initialSortDirection">
+                        <!-- Key Column -->
+                        <ng-container matColumnDef="key">
+                            <th mat-header-cell *matHeaderCellDef 
mat-sort-header>Key</th>
+                            <td mat-cell *matCellDef="let item">
+                                {{ item.key }}
+                            </td>
+                        </ng-container>
+
+                        <!-- Value Column -->
+                        <ng-container matColumnDef="value">
+                            <th mat-header-cell *matHeaderCellDef 
mat-sort-header>Value</th>
+                            <td mat-cell *matCellDef="let item" 
[title]="item.value">
+                                {{ item.value }}
+                            </td>
+                        </ng-container>
+
+                        <tr mat-header-row *matHeaderRowDef="displayedColumns; 
sticky: true"></tr>
+                        <tr
+                            mat-row
+                            *matRowDef="let row; let even = even; columns: 
displayedColumns"
+                            [class.even]="even"></tr>
+                    </table>
+                </div>
+            </div>
+            <div *ngIf="partialResults" class="-mt-3">Showing partial 
results</div>
+        </div>
+    </mat-dialog-content>
+    <mat-dialog-actions align="end">
+        <button color="primary" mat-raised-button 
mat-dialog-close>Close</button>
+    </mat-dialog-actions>
+</div>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.scss
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.scss
new file mode 100644
index 0000000000..17606c83f5
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.scss
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+@use '@angular/material' as mat;
+
+.component-state-dialog {
+    @include mat.button-density(-1);
+
+    width: 760px;
+
+    .listing-table {
+        table {
+            .mat-column-key {
+                width: 200px;
+            }
+        }
+    }
+}
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/controller-service/controller-services.component.spec.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.spec.ts
similarity index 64%
copy from 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts
copy to 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.spec.ts
index 435846b74a..033fe2a105 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.spec.ts
@@ -17,26 +17,21 @@
 
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 
-import { ControllerServices } from './controller-services.component';
+import { ComponentStateDialog } from './component-state.component';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { provideMockStore } from '@ngrx/store/testing';
-import { initialState } from 
'../../state/controller-services/controller-services.reducer';
-import { RouterTestingModule } from '@angular/router/testing';
+import { initialState } from 
'../../../state/component-state/component-state.reducer';
 
-describe('ControllerServices', () => {
-    let component: ControllerServices;
-    let fixture: ComponentFixture<ControllerServices>;
+describe('ComponentStateDialog', () => {
+    let component: ComponentStateDialog;
+    let fixture: ComponentFixture<ComponentStateDialog>;
 
     beforeEach(() => {
         TestBed.configureTestingModule({
-            declarations: [ControllerServices],
-            imports: [RouterTestingModule],
-            providers: [
-                provideMockStore({
-                    initialState
-                })
-            ]
+            imports: [ComponentStateDialog, BrowserAnimationsModule],
+            providers: [provideMockStore({ initialState })]
         });
-        fixture = TestBed.createComponent(ControllerServices);
+        fixture = TestBed.createComponent(ComponentStateDialog);
         component = fixture.componentInstance;
         fixture.detectChanges();
     });
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.ts
new file mode 100644
index 0000000000..8f37bb96c7
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.ts
@@ -0,0 +1,169 @@
+/*
+ * 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 { AfterViewInit, Component, Input } from '@angular/core';
+import { MatButtonModule } from '@angular/material/button';
+import { MatDialogModule } from '@angular/material/dialog';
+import { MatTableDataSource, MatTableModule } from '@angular/material/table';
+import { NiFiCommon } from '../../../service/nifi-common.service';
+import { MatSortModule, Sort } from '@angular/material/sort';
+import { AsyncPipe, NgIf } from '@angular/common';
+import { NifiTooltipDirective } from '../tooltips/nifi-tooltip.directive';
+import { NifiSpinnerDirective } from '../spinner/nifi-spinner.directive';
+import { ComponentStateState, StateEntry, StateMap } from 
'../../../state/component-state';
+import { Store } from '@ngrx/store';
+import { clearComponentState } from 
'../../../state/component-state/component-state.actions';
+import {
+    selectCanClear,
+    selectComponentName,
+    selectComponentState
+} from '../../../state/component-state/component-state.selectors';
+import { isDefinedAndNotNull } from '../../../state/shared';
+import { debounceTime, Observable } from 'rxjs';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+
+@Component({
+    selector: 'component-state',
+    standalone: true,
+    templateUrl: './component-state.component.html',
+    imports: [
+        MatButtonModule,
+        MatDialogModule,
+        MatTableModule,
+        MatSortModule,
+        NgIf,
+        NifiTooltipDirective,
+        NifiSpinnerDirective,
+        AsyncPipe,
+        ReactiveFormsModule,
+        MatFormFieldModule,
+        MatInputModule
+    ],
+    styleUrls: ['./component-state.component.scss', 
'../../../../assets/styles/listing-table.scss']
+})
+export class ComponentStateDialog implements AfterViewInit {
+    @Input() initialSortColumn: 'key' | 'value' = 'key';
+    @Input() initialSortDirection: 'asc' | 'desc' = 'asc';
+
+    componentName$: Observable<string> = 
this.store.select(selectComponentName).pipe(isDefinedAndNotNull());
+    canClear$: Observable<boolean> = 
this.store.select(selectCanClear).pipe(isDefinedAndNotNull());
+
+    // TODO - need to include scope column when clustered
+    displayedColumns: string[] = ['key', 'value'];
+    dataSource: MatTableDataSource<StateEntry> = new 
MatTableDataSource<StateEntry>();
+
+    filterForm: FormGroup;
+
+    stateDescription: string = '';
+    totalEntries: number = 0;
+    filteredEntries: number = 0;
+    partialResults: boolean = false;
+
+    constructor(
+        private store: Store<ComponentStateState>,
+        private formBuilder: FormBuilder,
+        private nifiCommon: NiFiCommon
+    ) {
+        this.filterForm = this.formBuilder.group({ filterTerm: '' });
+
+        this.store
+            .select(selectComponentState)
+            .pipe(isDefinedAndNotNull(), takeUntilDestroyed())
+            .subscribe((componentState) => {
+                this.stateDescription = componentState.stateDescription;
+
+                const stateItems: StateEntry[] = [];
+                if (componentState.localState) {
+                    const localStateItems: StateEntry[] = 
this.processStateMap(componentState.localState);
+                    stateItems.push(...localStateItems);
+                }
+                if (componentState.clusterState) {
+                    const clusterStateItems: StateEntry[] = 
this.processStateMap(componentState.clusterState);
+                    stateItems.push(...clusterStateItems);
+                }
+
+                this.dataSource.data = this.sortStateEntries(stateItems, {
+                    active: this.initialSortColumn,
+                    direction: this.initialSortDirection
+                });
+                this.filteredEntries = stateItems.length;
+
+                // apply any filtering to the new data
+                const filterTerm = this.filterForm.get('filterTerm')?.value;
+                if (filterTerm?.length > 0) {
+                    this.applyFilter(filterTerm);
+                }
+            });
+    }
+
+    ngAfterViewInit(): void {
+        this.filterForm
+            .get('filterTerm')
+            ?.valueChanges.pipe(debounceTime(500))
+            .subscribe((filterTerm: string) => {
+                this.applyFilter(filterTerm);
+            });
+    }
+
+    processStateMap(stateMap: StateMap): StateEntry[] {
+        const stateItems: StateEntry[] = stateMap.state ? stateMap.state : [];
+
+        if (stateItems.length !== stateMap.totalEntryCount) {
+            this.partialResults = true;
+        }
+
+        this.totalEntries += stateMap.totalEntryCount;
+
+        return stateItems;
+    }
+
+    applyFilter(filterTerm: string) {
+        this.dataSource.filter = filterTerm.trim().toLowerCase();
+        this.filteredEntries = this.dataSource.filteredData.length;
+    }
+
+    sortData(sort: Sort) {
+        this.dataSource.data = this.sortStateEntries(this.dataSource.data, 
sort);
+    }
+
+    private sortStateEntries(data: StateEntry[], sort: Sort): StateEntry[] {
+        if (!data) {
+            return [];
+        }
+
+        return data.slice().sort((a, b) => {
+            const isAsc = sort.direction === 'asc';
+            let retVal = 0;
+            switch (sort.active) {
+                case 'key':
+                    retVal = this.nifiCommon.compareString(a.key, b.key);
+                    break;
+                case 'value':
+                    retVal = this.nifiCommon.compareString(a.value, b.value);
+                    break;
+            }
+            return retVal * (isAsc ? 1 : -1);
+        });
+    }
+
+    clearState(): void {
+        this.store.dispatch(clearComponentState());
+    }
+}
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/controller-service-table/controller-service-table.component.html
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/controller-service-table/controller-service-table.component.html
index 03076a9a03..0ae491ed2f 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/controller-service-table/controller-service-table.component.html
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/controller-service-table/controller-service-table.component.html
@@ -141,7 +141,11 @@
                                 *ngIf="canDelete(item)"
                                 (click)="deleteClicked(item, $event)"
                                 title="Delete"></div>
-                            <div class="pointer fa fa-tasks" 
*ngIf="canViewState(item)" title="View State"></div>
+                            <div
+                                class="pointer fa fa-tasks"
+                                *ngIf="canViewState(item)"
+                                (click)="viewStateClicked(item)"
+                                title="View State"></div>
                             <div
                                 class="pointer fa fa-key"
                                 *ngIf="canManageAccessPolicies()"
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/controller-service-table/controller-service-table.component.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/controller-service-table/controller-service-table.component.ts
index 81b968cd20..fe33870ba1 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/controller-service-table/controller-service-table.component.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/controller-service-table/controller-service-table.component.ts
@@ -80,6 +80,8 @@ export class ControllerServiceTable {
         new EventEmitter<ControllerServiceEntity>();
     @Output() disableControllerService: EventEmitter<ControllerServiceEntity> =
         new EventEmitter<ControllerServiceEntity>();
+    @Output() viewStateControllerService: 
EventEmitter<ControllerServiceEntity> =
+        new EventEmitter<ControllerServiceEntity>();
 
     protected readonly TextTip = TextTip;
     protected readonly BulletinsTip = BulletinsTip;
@@ -251,6 +253,10 @@ export class ControllerServiceTable {
         return this.canRead(entity) && this.canWrite(entity) && 
entity.component.persistsState === true;
     }
 
+    viewStateClicked(entity: ControllerServiceEntity): void {
+        this.viewStateControllerService.next(entity);
+    }
+
     canManageAccessPolicies(): boolean {
         return this.flowConfiguration.supportsManagedAuthorizer && 
this.currentUser.tenantsPermissions.canRead;
     }
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/extension-creation/extension-creation.component.html
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/extension-creation/extension-creation.component.html
index ba83e572d8..e850ee7ef7 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/extension-creation/extension-creation.component.html
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/extension-creation/extension-creation.component.html
@@ -100,7 +100,6 @@
             <button color="accent" mat-raised-button 
mat-dialog-close>Cancel</button>
             <button
                 [disabled]="selectedType == null || saving"
-                type="submit"
                 color="primary"
                 (click)="createExtension(selectedType)"
                 mat-raised-button>

Reply via email to