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

scottyaslan 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 8e0c4aeb33 NIFI-13248: Add Flow Analysis report menu to new ui (#8974)
8e0c4aeb33 is described below

commit 8e0c4aeb333168fec568917778ac9ab2dc3098fe
Author: Shane Ardell <[email protected]>
AuthorDate: Mon Aug 19 09:04:52 2024 -0400

    NIFI-13248: Add Flow Analysis report menu to new ui (#8974)
    
    * NIFI-13248: Add Flow Analysis report menu to new ui
    
    * NIFI-13248: use ngrx to manage state for flow analysis component
    
    * NIFI-13248: update types used in flow analysis drawer
    
    * NIFI-13248: add violation details dialog
    
    * NIFI-13248: add go to component functionality
    
    * NIFI-13248: use Tailwind classes
    
    * NIFI-13248: update analysis status icon based on response
    
    * NIFI-13248: fix broken unit tests
    
    * NIFI-13248: add missing license headers
    
    * NIFI-13248: refactor styling
    
    patch provided by @scottyaslan
    
    * NIFI-13248: fix broken styling
    
    * NIFI-13248: further style refactoring
    
    patch provided by @scottyaslan
    
    * NIFI-13248: binding and spacing fixes
    
    * NIFI-13248: wire up pg name and id
    
    * NIFI-13248: use breadcrumb selector to obtain process group name
    
    * NIFI-13248: remove border color classes
    
    * NIFI-13248: update drawer button icon
---
 .../flow-designer/feature/flow-designer.module.ts  |   4 +-
 .../behavior/connectable-behavior.service.spec.ts  |   5 +-
 .../behavior/draggable-behavior.service.spec.ts    |   5 +-
 .../behavior/editable-behavior.service.spec.ts     |   5 +-
 .../behavior/quick-select-behavior.service.spec.ts |   5 +-
 .../behavior/selectable-behavior.service.spec.ts   |   5 +-
 .../service/birdseye-view.service.spec.ts          |   5 +-
 .../service/canvas-utils.service.spec.ts           |   5 +-
 .../service/canvas-view.service.spec.ts            |   5 +-
 .../flow-analysis.service.spec.ts}                 |  34 ++-
 .../flow-analysis.service.ts}                      |  20 +-
 .../manager/connection-manager.service.spec.ts     |   5 +-
 .../service/manager/funnel-manager.service.spec.ts |   5 +-
 .../service/manager/label-manager.service.spec.ts  |   5 +-
 .../service/manager/port-manager.service.spec.ts   |   5 +-
 .../manager/process-group-manager.service.spec.ts  |   5 +-
 .../manager/processor-manager.service.spec.ts      |   5 +-
 .../remote-process-group-manager.service.spec.ts   |   5 +-
 .../state/flow-analysis/flow-analysis.actions.ts   |  44 ++++
 .../state/flow-analysis/flow-analysis.effects.ts   | 120 ++++++++++
 .../flow-analysis/flow-analysis.reducer.ts}        |  33 ++-
 .../flow-analysis/flow-analysis.selectors.ts}      |  18 +-
 .../flow-designer/state/flow-analysis/index.ts     |  68 ++++++
 .../pages/flow-designer/state/flow/flow.actions.ts |   5 +
 .../pages/flow-designer/state/flow/flow.effects.ts |   2 +
 .../pages/flow-designer/state/flow/flow.reducer.ts |   6 +
 .../flow-designer/state/flow/flow.selectors.ts     |   2 +
 .../app/pages/flow-designer/state/flow/index.ts    |   1 +
 .../src/app/pages/flow-designer/state/index.ts     |   6 +-
 .../ui/canvas/_canvas.component-theme.scss         |   8 +
 .../flow-designer/ui/canvas/canvas.component.html  |  19 +-
 .../ui/canvas/canvas.component.spec.ts             |  12 +-
 .../flow-designer/ui/canvas/canvas.component.ts    |   3 +
 .../pages/flow-designer/ui/canvas/canvas.module.ts |   6 +-
 .../flow-analysis-drawer.component.html            | 258 +++++++++++++++++++++
 .../flow-analysis-drawer.component.scss}           |  13 --
 .../flow-analysis-drawer.component.spec.ts         |  41 ++++
 .../flow-analysis-drawer.component.ts              | 167 +++++++++++++
 ..._violation-details-dialog.component-theme.scss} |  51 ++--
 .../violation-details-dialog.component.html}       |  35 ++-
 .../violation-details-dialog.component.scss}       |  13 --
 .../violation-details-dialog.component.spec.ts     | 104 +++++++++
 .../violation-details-dialog.component.ts          |  74 ++++++
 .../flow-status/_flow-status.component-theme.scss  |  71 ++++--
 .../header/flow-status/flow-status.component.html  |   6 +
 .../header/flow-status/flow-status.component.scss  |   5 -
 .../header/flow-status/flow-status.component.ts    |  56 ++++-
 .../ui/canvas/header/header.component.html         |   4 +-
 .../ui/canvas/header/header.component.ts           |   4 +
 .../src/main/frontend/apps/nifi/src/styles.scss    |   4 +
 50 files changed, 1234 insertions(+), 158 deletions(-)

diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/feature/flow-designer.module.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/feature/flow-designer.module.ts
index 0aa5219148..b065fe30a7 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/feature/flow-designer.module.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/feature/flow-designer.module.ts
@@ -30,6 +30,7 @@ import { ControllerServicesEffects } from 
'../state/controller-services/controll
 import { ParameterEffects } from '../state/parameter/parameter.effects';
 import { QueueEffects } from '../state/queue/queue.effects';
 import { BannerText } from 
'../../../ui/common/banner-text/banner-text.component';
+import { FlowAnalysisEffects } from 
'../state/flow-analysis/flow-analysis.effects';
 
 @NgModule({
     declarations: [FlowDesigner, VersionControlTip],
@@ -43,7 +44,8 @@ import { BannerText } from 
'../../../ui/common/banner-text/banner-text.component
             TransformEffects,
             ControllerServicesEffects,
             ParameterEffects,
-            QueueEffects
+            QueueEffects,
+            FlowAnalysisEffects
         ),
         NgOptimizedImage,
         MatDialogModule,
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts
index e7273d5db2..91b1988fce 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts
@@ -35,6 +35,8 @@ import { selectFlowConfiguration } from 
'../../../../state/flow-configuration/fl
 import * as fromFlowConfiguration from 
'../../../../state/flow-configuration/flow-configuration.reducer';
 import { queueFeatureKey } from '../../../queue/state';
 import * as fromQueue from '../../state/queue/queue.reducer';
+import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
+import * as fromFlowAnalysis from 
'../../state/flow-analysis/flow-analysis.reducer';
 
 describe('ConnectableBehavior', () => {
     let service: ConnectableBehavior;
@@ -45,7 +47,8 @@ describe('ConnectableBehavior', () => {
             [transformFeatureKey]: fromTransform.initialState,
             [controllerServicesFeatureKey]: 
fromControllerServices.initialState,
             [parameterFeatureKey]: fromParameter.initialState,
-            [queueFeatureKey]: fromQueue.initialState
+            [queueFeatureKey]: fromQueue.initialState,
+            [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
         };
 
         TestBed.configureTestingModule({
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts
index d28a4c4168..601625d608 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts
@@ -36,6 +36,8 @@ import { selectFlowConfiguration } from 
'../../../../state/flow-configuration/fl
 import * as fromFlowConfiguration from 
'../../../../state/flow-configuration/flow-configuration.reducer';
 import { queueFeatureKey } from '../../../queue/state';
 import * as fromQueue from '../../state/queue/queue.reducer';
+import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
+import * as fromFlowAnalysis from 
'../../state/flow-analysis/flow-analysis.reducer';
 
 describe('DraggableBehavior', () => {
     let service: DraggableBehavior;
@@ -46,7 +48,8 @@ describe('DraggableBehavior', () => {
             [transformFeatureKey]: fromTransform.initialState,
             [controllerServicesFeatureKey]: 
fromControllerServices.initialState,
             [parameterFeatureKey]: fromParameter.initialState,
-            [queueFeatureKey]: fromQueue.initialState
+            [queueFeatureKey]: fromQueue.initialState,
+            [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
         };
 
         TestBed.configureTestingModule({
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/editable-behavior.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/editable-behavior.service.spec.ts
index 8a8074a875..153387fdfd 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/editable-behavior.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/editable-behavior.service.spec.ts
@@ -36,6 +36,8 @@ import { selectFlowConfiguration } from 
'../../../../state/flow-configuration/fl
 import * as fromFlowConfiguration from 
'../../../../state/flow-configuration/flow-configuration.reducer';
 import { queueFeatureKey } from '../../../queue/state';
 import * as fromQueue from '../../state/queue/queue.reducer';
+import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
+import * as fromFlowAnalysis from 
'../../state/flow-analysis/flow-analysis.reducer';
 
 describe('EditableBehaviorService', () => {
     let service: EditableBehavior;
@@ -45,7 +47,8 @@ describe('EditableBehaviorService', () => {
         [transformFeatureKey]: fromTransform.initialState,
         [controllerServicesFeatureKey]: fromControllerServices.initialState,
         [parameterFeatureKey]: fromParameter.initialState,
-        [queueFeatureKey]: fromQueue.initialState
+        [queueFeatureKey]: fromQueue.initialState,
+        [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
     };
 
     beforeEach(() => {
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts
index e157963476..9faed60018 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts
@@ -35,6 +35,8 @@ import { selectFlowConfiguration } from 
'../../../../state/flow-configuration/fl
 import * as fromFlowConfiguration from 
'../../../../state/flow-configuration/flow-configuration.reducer';
 import { queueFeatureKey } from '../../../queue/state';
 import * as fromQueue from '../../state/queue/queue.reducer';
+import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
+import * as fromFlowAnalysis from 
'../../state/flow-analysis/flow-analysis.reducer';
 
 describe('QuickSelectBehavior', () => {
     let service: QuickSelectBehavior;
@@ -45,7 +47,8 @@ describe('QuickSelectBehavior', () => {
             [transformFeatureKey]: fromTransform.initialState,
             [controllerServicesFeatureKey]: 
fromControllerServices.initialState,
             [parameterFeatureKey]: fromParameter.initialState,
-            [queueFeatureKey]: fromQueue.initialState
+            [queueFeatureKey]: fromQueue.initialState,
+            [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
         };
 
         TestBed.configureTestingModule({
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/selectable-behavior.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/selectable-behavior.service.spec.ts
index 52d05081a3..ebad15236a 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/selectable-behavior.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/behavior/selectable-behavior.service.spec.ts
@@ -34,6 +34,8 @@ import { selectFlowConfiguration } from 
'../../../../state/flow-configuration/fl
 import * as fromFlowConfiguration from 
'../../../../state/flow-configuration/flow-configuration.reducer';
 import { queueFeatureKey } from '../../../queue/state';
 import * as fromQueue from '../../state/queue/queue.reducer';
+import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
+import * as fromFlowAnalysis from 
'../../state/flow-analysis/flow-analysis.reducer';
 
 describe('SelectableBehavior', () => {
     let service: SelectableBehavior;
@@ -44,7 +46,8 @@ describe('SelectableBehavior', () => {
             [transformFeatureKey]: fromTransform.initialState,
             [controllerServicesFeatureKey]: 
fromControllerServices.initialState,
             [parameterFeatureKey]: fromParameter.initialState,
-            [queueFeatureKey]: fromQueue.initialState
+            [queueFeatureKey]: fromQueue.initialState,
+            [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
         };
 
         TestBed.configureTestingModule({
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts
index 3545e43e46..66c331e309 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts
@@ -36,6 +36,8 @@ import { selectFlowConfiguration } from 
'../../../state/flow-configuration/flow-
 import * as fromFlowConfiguration from 
'../../../state/flow-configuration/flow-configuration.reducer';
 import { queueFeatureKey } from '../../queue/state';
 import * as fromQueue from '../state/queue/queue.reducer';
+import { flowAnalysisFeatureKey } from '../state/flow-analysis';
+import * as fromFlowAnalysis from 
'../state/flow-analysis/flow-analysis.reducer';
 
 describe('BirdseyeView', () => {
     let service: BirdseyeView;
@@ -46,7 +48,8 @@ describe('BirdseyeView', () => {
             [transformFeatureKey]: fromTransform.initialState,
             [controllerServicesFeatureKey]: 
fromControllerServices.initialState,
             [parameterFeatureKey]: fromParameter.initialState,
-            [queueFeatureKey]: fromQueue.initialState
+            [queueFeatureKey]: fromQueue.initialState,
+            [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
         };
 
         TestBed.configureTestingModule({
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.spec.ts
index 45013d9b60..3c82d29c98 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-utils.service.spec.ts
@@ -35,6 +35,8 @@ import { selectFlowConfiguration } from 
'../../../state/flow-configuration/flow-
 import * as fromFlowConfiguration from 
'../../../state/flow-configuration/flow-configuration.reducer';
 import { queueFeatureKey } from '../../queue/state';
 import * as fromQueue from '../state/queue/queue.reducer';
+import { flowAnalysisFeatureKey } from '../state/flow-analysis';
+import * as fromFlowAnalysis from 
'../state/flow-analysis/flow-analysis.reducer';
 
 describe('CanvasUtils', () => {
     let service: CanvasUtils;
@@ -45,7 +47,8 @@ describe('CanvasUtils', () => {
             [transformFeatureKey]: fromTransform.initialState,
             [controllerServicesFeatureKey]: 
fromControllerServices.initialState,
             [parameterFeatureKey]: fromParameter.initialState,
-            [queueFeatureKey]: fromQueue.initialState
+            [queueFeatureKey]: fromQueue.initialState,
+            [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
         };
 
         TestBed.configureTestingModule({
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts
index dffdec8902..de8f8d9370 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts
@@ -36,6 +36,8 @@ import { selectFlowConfiguration } from 
'../../../state/flow-configuration/flow-
 import * as fromFlowConfiguration from 
'../../../state/flow-configuration/flow-configuration.reducer';
 import { queueFeatureKey } from '../../queue/state';
 import * as fromQueue from '../state/queue/queue.reducer';
+import { flowAnalysisFeatureKey } from '../state/flow-analysis';
+import * as fromFlowAnalysis from 
'../state/flow-analysis/flow-analysis.reducer';
 
 describe('CanvasView', () => {
     let service: CanvasView;
@@ -46,7 +48,8 @@ describe('CanvasView', () => {
             [transformFeatureKey]: fromTransform.initialState,
             [controllerServicesFeatureKey]: 
fromControllerServices.initialState,
             [parameterFeatureKey]: fromParameter.initialState,
-            [queueFeatureKey]: fromQueue.initialState
+            [queueFeatureKey]: fromQueue.initialState,
+            [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
         };
 
         TestBed.configureTestingModule({
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/flow-analysis.service.spec.ts
similarity index 53%
copy from 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
copy to 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/flow-analysis.service.spec.ts
index 5414141b19..d85a11dcf0 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/flow-analysis.service.spec.ts
@@ -15,15 +15,29 @@
  * limitations under the License.
  */
 
-.flow-status {
-    box-sizing: content-box;
+import { TestBed } from '@angular/core/testing';
 
-    .fa,
-    .icon {
-        font-style: normal;
-    }
+import { FlowAnalysisService } from './flow-analysis.service';
+import { provideMockStore } from '@ngrx/store/testing';
+import { HttpClient } from '@angular/common/http';
 
-    .controller-bulletins {
-        cursor: default;
-    }
-}
+describe('FlowAnalysisService', () => {
+    let service: FlowAnalysisService;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            providers: [
+                provideMockStore({}),
+                {
+                    provide: HttpClient,
+                    useValue: {}
+                }
+            ]
+        });
+        service = TestBed.inject(FlowAnalysisService);
+    });
+
+    it('should be created', () => {
+        expect(service).toBeTruthy();
+    });
+});
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/flow-analysis.service.ts
similarity index 62%
copy from 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
copy to 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/flow-analysis.service.ts
index 5414141b19..571d91534f 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/flow-analysis.service.ts
@@ -15,15 +15,19 @@
  * limitations under the License.
  */
 
-.flow-status {
-    box-sizing: content-box;
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
 
-    .fa,
-    .icon {
-        font-style: normal;
-    }
+@Injectable({
+    providedIn: 'root'
+})
+export class FlowAnalysisService {
+    private static readonly API: string = '../nifi-api';
+
+    constructor(private httpClient: HttpClient) {}
 
-    .controller-bulletins {
-        cursor: default;
+    getResults(processGroupId: string): Observable<any> {
+        return 
this.httpClient.get(`${FlowAnalysisService.API}/flow/flow-analysis/results/${processGroupId}`);
     }
 }
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
index c334a35f86..f4b0970bbc 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
@@ -37,6 +37,8 @@ import * as fromFlowConfiguration from 
'../../../../state/flow-configuration/flo
 import { queueFeatureKey } from '../../../queue/state';
 import * as fromQueue from '../../state/queue/queue.reducer';
 import { ClusterConnectionService } from 
'../../../../service/cluster-connection.service';
+import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
+import * as fromFlowAnalysis from 
'../../state/flow-analysis/flow-analysis.reducer';
 
 describe('ConnectionManager', () => {
     let service: ConnectionManager;
@@ -47,7 +49,8 @@ describe('ConnectionManager', () => {
             [transformFeatureKey]: fromTransform.initialState,
             [controllerServicesFeatureKey]: 
fromControllerServices.initialState,
             [parameterFeatureKey]: fromParameter.initialState,
-            [queueFeatureKey]: fromQueue.initialState
+            [queueFeatureKey]: fromQueue.initialState,
+            [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
         };
 
         TestBed.configureTestingModule({
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts
index a48d1ac593..1dd171f002 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts
@@ -36,6 +36,8 @@ import { selectFlowConfiguration } from 
'../../../../state/flow-configuration/fl
 import * as fromFlowConfiguration from 
'../../../../state/flow-configuration/flow-configuration.reducer';
 import { queueFeatureKey } from '../../../queue/state';
 import * as fromQueue from '../../state/queue/queue.reducer';
+import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
+import * as fromFlowAnalysis from 
'../../state/flow-analysis/flow-analysis.reducer';
 
 describe('FunnelManager', () => {
     let service: FunnelManager;
@@ -46,7 +48,8 @@ describe('FunnelManager', () => {
             [transformFeatureKey]: fromTransform.initialState,
             [controllerServicesFeatureKey]: 
fromControllerServices.initialState,
             [parameterFeatureKey]: fromParameter.initialState,
-            [queueFeatureKey]: fromQueue.initialState
+            [queueFeatureKey]: fromQueue.initialState,
+            [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
         };
 
         TestBed.configureTestingModule({
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/label-manager.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/label-manager.service.spec.ts
index 517ae0dc3f..f7dfbc41b4 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/label-manager.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/label-manager.service.spec.ts
@@ -37,6 +37,8 @@ import * as fromFlowConfiguration from 
'../../../../state/flow-configuration/flo
 import { queueFeatureKey } from '../../../queue/state';
 import * as fromQueue from '../../state/queue/queue.reducer';
 import { ClusterConnectionService } from 
'../../../../service/cluster-connection.service';
+import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
+import * as fromFlowAnalysis from 
'../../state/flow-analysis/flow-analysis.reducer';
 
 describe('LabelManager', () => {
     let service: LabelManager;
@@ -47,7 +49,8 @@ describe('LabelManager', () => {
             [transformFeatureKey]: fromTransform.initialState,
             [controllerServicesFeatureKey]: 
fromControllerServices.initialState,
             [parameterFeatureKey]: fromParameter.initialState,
-            [queueFeatureKey]: fromQueue.initialState
+            [queueFeatureKey]: fromQueue.initialState,
+            [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
         };
 
         TestBed.configureTestingModule({
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/port-manager.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/port-manager.service.spec.ts
index 009f256fd7..3ca730eec6 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/port-manager.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/port-manager.service.spec.ts
@@ -36,6 +36,8 @@ import { selectFlowConfiguration } from 
'../../../../state/flow-configuration/fl
 import * as fromFlowConfiguration from 
'../../../../state/flow-configuration/flow-configuration.reducer';
 import { queueFeatureKey } from '../../../queue/state';
 import * as fromQueue from '../../state/queue/queue.reducer';
+import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
+import * as fromFlowAnalysis from 
'../../state/flow-analysis/flow-analysis.reducer';
 
 describe('PortManager', () => {
     let service: PortManager;
@@ -46,7 +48,8 @@ describe('PortManager', () => {
             [transformFeatureKey]: fromTransform.initialState,
             [controllerServicesFeatureKey]: 
fromControllerServices.initialState,
             [parameterFeatureKey]: fromParameter.initialState,
-            [queueFeatureKey]: fromQueue.initialState
+            [queueFeatureKey]: fromQueue.initialState,
+            [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
         };
 
         TestBed.configureTestingModule({
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/process-group-manager.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/process-group-manager.service.spec.ts
index cc01af6a99..53a5451268 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/process-group-manager.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/process-group-manager.service.spec.ts
@@ -36,6 +36,8 @@ import { selectFlowConfiguration } from 
'../../../../state/flow-configuration/fl
 import * as fromFlowConfiguration from 
'../../../../state/flow-configuration/flow-configuration.reducer';
 import { queueFeatureKey } from '../../../queue/state';
 import * as fromQueue from '../../state/queue/queue.reducer';
+import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
+import * as fromFlowAnalysis from 
'../../state/flow-analysis/flow-analysis.reducer';
 
 describe('ProcessGroupManager', () => {
     let service: ProcessGroupManager;
@@ -46,7 +48,8 @@ describe('ProcessGroupManager', () => {
             [transformFeatureKey]: fromTransform.initialState,
             [controllerServicesFeatureKey]: 
fromControllerServices.initialState,
             [parameterFeatureKey]: fromParameter.initialState,
-            [queueFeatureKey]: fromQueue.initialState
+            [queueFeatureKey]: fromQueue.initialState,
+            [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
         };
 
         TestBed.configureTestingModule({
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/processor-manager.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/processor-manager.service.spec.ts
index 677b4d2bad..1e6a3876ab 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/processor-manager.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/processor-manager.service.spec.ts
@@ -36,6 +36,8 @@ import { selectFlowConfiguration } from 
'../../../../state/flow-configuration/fl
 import * as fromFlowConfiguration from 
'../../../../state/flow-configuration/flow-configuration.reducer';
 import { queueFeatureKey } from '../../../queue/state';
 import * as fromQueue from '../../state/queue/queue.reducer';
+import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
+import * as fromFlowAnalysis from 
'../../state/flow-analysis/flow-analysis.reducer';
 
 describe('ProcessorManager', () => {
     let service: ProcessorManager;
@@ -46,7 +48,8 @@ describe('ProcessorManager', () => {
             [transformFeatureKey]: fromTransform.initialState,
             [controllerServicesFeatureKey]: 
fromControllerServices.initialState,
             [parameterFeatureKey]: fromParameter.initialState,
-            [queueFeatureKey]: fromQueue.initialState
+            [queueFeatureKey]: fromQueue.initialState,
+            [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
         };
 
         TestBed.configureTestingModule({
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts
index 0ac01b7d2b..6e0e2f12d7 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts
@@ -36,6 +36,8 @@ import { selectFlowConfiguration } from 
'../../../../state/flow-configuration/fl
 import * as fromFlowConfiguration from 
'../../../../state/flow-configuration/flow-configuration.reducer';
 import { queueFeatureKey } from '../../../queue/state';
 import * as fromQueue from '../../state/queue/queue.reducer';
+import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
+import * as fromFlowAnalysis from 
'../../state/flow-analysis/flow-analysis.reducer';
 
 describe('RemoteProcessGroupManager', () => {
     let service: RemoteProcessGroupManager;
@@ -46,7 +48,8 @@ describe('RemoteProcessGroupManager', () => {
             [transformFeatureKey]: fromTransform.initialState,
             [controllerServicesFeatureKey]: 
fromControllerServices.initialState,
             [parameterFeatureKey]: fromParameter.initialState,
-            [queueFeatureKey]: fromQueue.initialState
+            [queueFeatureKey]: fromQueue.initialState,
+            [flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
         };
 
         TestBed.configureTestingModule({
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow-analysis/flow-analysis.actions.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow-analysis/flow-analysis.actions.ts
new file mode 100644
index 0000000000..e5adce0adc
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow-analysis/flow-analysis.actions.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 { FlowAnalysisRequestResponse, FlowAnalysisRule, 
FlowAnalysisRuleViolation } from '.';
+
+export const startPollingFlowAnalysis = createAction('[Flow Analysis] Start 
Polling Flow Analysis');
+
+export const stopPollingFlowAnalysis = createAction('[Flow Analysis] Stop 
Polling Flow Analysis');
+
+export const pollFlowAnalysis = createAction(`[Flow Analysis] Poll Flow 
Analysis`);
+
+export const pollFlowAnalysisSuccess = createAction(
+    `[Flow Analysis] Poll Flow Analysis Success`,
+    props<{ response: FlowAnalysisRequestResponse }>()
+);
+
+export const flowAnalysisApiError = createAction('[Flow Analysis] API Error', 
props<{ error: string }>());
+
+export const resetPollingFlowAnalysis = createAction(`[Flow Analysis] Reset 
Polling Flow Analysis`);
+
+export const navigateToEditFlowAnalysisRule = createAction(
+    '[Flow Analysis Rules] Navigate To Edit Flow Analysis Rule',
+    props<{ id: string }>()
+);
+
+export const openRuleDetailsDialog = createAction(
+    '[Flow Analysis Rules] Open Flow Analysis Rule Details Dialog',
+    props<{ violation: FlowAnalysisRuleViolation; rule: FlowAnalysisRule }>()
+);
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow-analysis/flow-analysis.effects.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow-analysis/flow-analysis.effects.ts
new file mode 100644
index 0000000000..c8b808d17e
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow-analysis/flow-analysis.effects.ts
@@ -0,0 +1,120 @@
+/*
+ * 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, createEffect, ofType } from '@ngrx/effects';
+import { concatLatestFrom } from '@ngrx/operators';
+import { NiFiState } from '../../../../state';
+import { Store } from '@ngrx/store';
+import { asyncScheduler, catchError, from, interval, map, of, startWith, 
switchMap, takeUntil, tap } from 'rxjs';
+import * as FlowAnalysisActions from './flow-analysis.actions';
+import { HttpErrorResponse } from '@angular/common/http';
+import { FlowAnalysisService } from '../../service/flow-analysis.service';
+import { ErrorHelper } from 'apps/nifi/src/app/service/error-helper.service';
+import { selectCurrentProcessGroupId } from '../flow/flow.selectors';
+import { Router } from '@angular/router';
+import { MatDialog } from '@angular/material/dialog';
+import { LARGE_DIALOG } from '@nifi/shared';
+import { ViolationDetailsDialogComponent } from 
'../../ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component';
+
+@Injectable()
+export class FlowAnalysisEffects {
+    constructor(
+        private actions$: Actions,
+        private store: Store<NiFiState>,
+        private flowAnalysisService: FlowAnalysisService,
+        private errorHelper: ErrorHelper,
+        private router: Router,
+        private dialog: MatDialog
+    ) {}
+
+    startPollingFlowAnalysis$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(FlowAnalysisActions.startPollingFlowAnalysis),
+            switchMap(() =>
+                interval(30000, asyncScheduler).pipe(
+                    startWith(0),
+                    
takeUntil(this.actions$.pipe(ofType(FlowAnalysisActions.stopPollingFlowAnalysis)))
+                )
+            ),
+            switchMap(() => of(FlowAnalysisActions.pollFlowAnalysis()))
+        )
+    );
+
+    resetPollingFlowAnalysis$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(FlowAnalysisActions.resetPollingFlowAnalysis),
+            switchMap(() => {
+                
this.store.dispatch(FlowAnalysisActions.stopPollingFlowAnalysis());
+                return of(FlowAnalysisActions.pollFlowAnalysis());
+            })
+        )
+    );
+
+    pollFlowAnalysis$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(FlowAnalysisActions.pollFlowAnalysis),
+            concatLatestFrom(() => 
this.store.select(selectCurrentProcessGroupId)),
+            switchMap(([, pgId]) => {
+                return from(this.flowAnalysisService.getResults(pgId)).pipe(
+                    map((response) =>
+                        FlowAnalysisActions.pollFlowAnalysisSuccess({
+                            response: response
+                        })
+                    ),
+                    catchError((errorResponse: HttpErrorResponse) => {
+                        
this.store.dispatch(FlowAnalysisActions.stopPollingFlowAnalysis());
+                        return of(
+                            FlowAnalysisActions.flowAnalysisApiError({
+                                error: 
this.errorHelper.getErrorString(errorResponse)
+                            })
+                        );
+                    })
+                );
+            })
+        )
+    );
+
+    navigateToEditFlowAnalysisRule$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(FlowAnalysisActions.navigateToEditFlowAnalysisRule),
+                map((action) => action.id),
+                tap((id) => {
+                    this.router.navigate(['/settings', 'flow-analysis-rules', 
id, 'edit']);
+                })
+            ),
+        { dispatch: false }
+    );
+
+    openRuleDetailsDialog$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(FlowAnalysisActions.openRuleDetailsDialog),
+                tap(({ violation, rule }) => {
+                    this.dialog.open(ViolationDetailsDialogComponent, {
+                        ...LARGE_DIALOG,
+                        data: {
+                            violation,
+                            rule
+                        }
+                    });
+                })
+            ),
+        { dispatch: false }
+    );
+}
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow-analysis/flow-analysis.reducer.ts
similarity index 51%
copy from 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
copy to 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow-analysis/flow-analysis.reducer.ts
index 5414141b19..486fb32757 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow-analysis/flow-analysis.reducer.ts
@@ -15,15 +15,28 @@
  * limitations under the License.
  */
 
-.flow-status {
-    box-sizing: content-box;
+import { createReducer, on } from '@ngrx/store';
+import { FlowAnalysisState } from './index';
+import { pollFlowAnalysis, pollFlowAnalysisSuccess } from 
'./flow-analysis.actions';
 
-    .fa,
-    .icon {
-        font-style: normal;
-    }
+export const initialState: FlowAnalysisState = {
+    rules: [],
+    ruleViolations: [],
+    flowAnalysisPending: false,
+    status: 'pending'
+};
 
-    .controller-bulletins {
-        cursor: default;
-    }
-}
+export const flowAnalysisReducer = createReducer(
+    initialState,
+    on(pollFlowAnalysis, (state) => ({
+        ...state,
+        status: 'loading' as const
+    })),
+    on(pollFlowAnalysisSuccess, (state, { response }) => ({
+        ...state,
+        rules: response.rules,
+        ruleViolations: response.ruleViolations,
+        flowAnalysisPending: response.flowAnalysisPending,
+        status: 'success' as const
+    }))
+);
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow-analysis/flow-analysis.selectors.ts
similarity index 73%
copy from 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
copy to 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow-analysis/flow-analysis.selectors.ts
index 5414141b19..7fd8035793 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow-analysis/flow-analysis.selectors.ts
@@ -15,15 +15,11 @@
  * limitations under the License.
  */
 
-.flow-status {
-    box-sizing: content-box;
+import { createSelector } from '@ngrx/store';
+import { flowAnalysisFeatureKey } from './index';
+import { CanvasState, selectCanvasState } from '../index';
 
-    .fa,
-    .icon {
-        font-style: normal;
-    }
-
-    .controller-bulletins {
-        cursor: default;
-    }
-}
+export const selectFlowAnalysisState = createSelector(
+    selectCanvasState,
+    (state: CanvasState) => state[flowAnalysisFeatureKey]
+);
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow-analysis/index.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow-analysis/index.ts
new file mode 100644
index 0000000000..49deb49658
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow-analysis/index.ts
@@ -0,0 +1,68 @@
+/*
+ * 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 { PropertyDescriptor, Bundle, Permissions } from 
'../../../../state/shared';
+
+export const flowAnalysisFeatureKey = 'flowAnalysis';
+
+export interface FlowAnalysisRule {
+    id: string;
+    name: string;
+    type: string;
+    bundle: Bundle;
+    state: string;
+    comments?: string;
+    persistsState: boolean;
+    restricted: boolean;
+    deprecated: boolean;
+    extensionMissing: boolean;
+    multipleVersionsAvailable: boolean;
+    supportsSensitiveDynamicProperties: boolean;
+    enforcementPolicy: string;
+    properties: { [key: string]: string };
+    descriptors: { [key: string]: PropertyDescriptor };
+    sensitiveDynamicPropertyNames: string[];
+    validationErrors: string[];
+    validationStatus: 'VALID' | 'INVALID' | 'VALIDATING';
+}
+
+export interface FlowAnalysisRuleViolation {
+    enforcementPolicy: string;
+    scope: string;
+    subjectId: string;
+    subjectDisplayName: string;
+    groupId: string;
+    ruleId: string;
+    issueId: string;
+    violationMessage: string;
+    subjectComponentType: string;
+    subjectPermissionDto: Permissions;
+    enabled: boolean;
+}
+
+export interface FlowAnalysisRequestResponse {
+    rules: FlowAnalysisRule[];
+    ruleViolations: FlowAnalysisRuleViolation[];
+    flowAnalysisPending: boolean;
+}
+
+export interface FlowAnalysisState {
+    rules: FlowAnalysisRule[];
+    ruleViolations: FlowAnalysisRuleViolation[];
+    flowAnalysisPending: boolean;
+    status: 'pending' | 'loading' | 'success';
+}
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 9a41e2cb9d..96d8386d08 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
@@ -573,6 +573,11 @@ export const setOperationCollapsed = createAction(
     props<{ operationCollapsed: boolean }>()
 );
 
+export const setFlowAnalysisOpen = createAction(
+    `${CANVAS_PREFIX} Set Flow Analysis Open`,
+    props<{ flowAnalysisOpen: boolean }>()
+);
+
 /*
     General
  */
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 fba867dc01..da3d62e3c8 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
@@ -153,6 +153,7 @@ import {
 import { VerifyPropertiesRequestContext } from 
'../../../../state/property-verification';
 import { BackNavigation } from '../../../../state/navigation';
 import { Storage, NiFiCommon } from '@nifi/shared';
+import { resetPollingFlowAnalysis } from 
'../flow-analysis/flow-analysis.actions';
 
 @Injectable()
 export class FlowEffects {
@@ -212,6 +213,7 @@ export class FlowEffects {
                     this.flowService.getControllerBulletins()
                 ]).pipe(
                     map(([flow, flowStatus, controllerBulletins]) => {
+                        this.store.dispatch(resetPollingFlowAnalysis());
                         return FlowActions.loadProcessGroupSuccess({
                             response: {
                                 id: request.id,
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 1744866759..b05d79baab 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
@@ -60,6 +60,7 @@ import {
     saveToFlowRegistrySuccess,
     setAllowTransition,
     setDragging,
+    setFlowAnalysisOpen,
     setNavigationCollapsed,
     setOperationCollapsed,
     setSkipTransform,
@@ -164,6 +165,7 @@ export const initialState: FlowState = {
     allowTransition: false,
     navigationCollapsed: false,
     operationCollapsed: false,
+    flowAnalysisOpen: false,
     status: 'pending'
 };
 
@@ -541,6 +543,10 @@ export const flowReducer = createReducer(
         ...state,
         operationCollapsed
     })),
+    on(setFlowAnalysisOpen, (state, { flowAnalysisOpen }) => ({
+        ...state,
+        flowAnalysisOpen
+    })),
     on(
         startComponentSuccess,
         stopComponentSuccess,
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 d8164c3e94..3f282eafdf 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
@@ -257,3 +257,5 @@ export const selectMaxZIndex = (componentType: 
ComponentType.Connection | Compon
         );
     }
 };
+
+export const selectFlowAnalysisOpen = createSelector(selectFlowState, (state: 
FlowState) => state.flowAnalysisOpen);
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 2f586351d4..d01516b90b 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
@@ -638,6 +638,7 @@ export interface FlowState {
     saving: boolean;
     navigationCollapsed: boolean;
     operationCollapsed: boolean;
+    flowAnalysisOpen: boolean;
     versionSaving: boolean;
     changeVersionRequest: FlowUpdateRequestEntity | null;
     copiedSnippet: CopiedSnippet | null;
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/index.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/index.ts
index 077b7c92da..7c69e18b74 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/index.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/index.ts
@@ -31,6 +31,8 @@ import { parameterReducer } from 
'./parameter/parameter.reducer';
 import { queueFeatureKey } from '../../queue/state';
 import { QueueState } from './queue';
 import { queueReducer } from './queue/queue.reducer';
+import { FlowAnalysisState, flowAnalysisFeatureKey } from './flow-analysis';
+import { flowAnalysisReducer } from './flow-analysis/flow-analysis.reducer';
 
 export const canvasFeatureKey = 'canvas';
 
@@ -40,6 +42,7 @@ export interface CanvasState {
     [controllerServicesFeatureKey]: ControllerServicesState;
     [parameterFeatureKey]: ParameterState;
     [queueFeatureKey]: QueueState;
+    [flowAnalysisFeatureKey]: FlowAnalysisState;
 }
 
 export function reducers(state: CanvasState | undefined, action: Action) {
@@ -48,7 +51,8 @@ export function reducers(state: CanvasState | undefined, 
action: Action) {
         [transformFeatureKey]: transformReducer,
         [controllerServicesFeatureKey]: controllerServicesReducer,
         [parameterFeatureKey]: parameterReducer,
-        [queueFeatureKey]: queueReducer
+        [queueFeatureKey]: queueReducer,
+        [flowAnalysisFeatureKey]: flowAnalysisReducer
     })(state, action);
 }
 
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/_canvas.component-theme.scss
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/_canvas.component-theme.scss
index b7933733f3..364043c91c 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/_canvas.component-theme.scss
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/_canvas.component-theme.scss
@@ -104,6 +104,14 @@
             );
     }
 
+    mat-sidenav {
+        background-color: if(
+                $is-dark,
+                $supplemental-theme-surface-palette-darker,
+                $supplemental-theme-surface-palette-lighter
+        );
+    }
+
     /* svg styles */
 
     svg.canvas-svg {
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.html
index 802e15c2fa..2f5b308b42 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.html
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.html
@@ -18,12 +18,19 @@
 <div class="flex flex-col h-full">
     <fd-header></fd-header>
     <div class="flex-1">
-        <graph-controls></graph-controls>
-        <div
-            id="canvas-container"
-            class="canvas-background select-none h-full"
-            [cdkContextMenuTriggerFor]="contextMenu.menu"></div>
-        <fd-context-menu #contextMenu [menuProvider]="canvasContextMenu" 
menuId="root"></fd-context-menu>
+        <mat-sidenav-container class="h-full">
+            <mat-sidenav mode="side" [opened]="flowAnalysisOpen()" 
position="end">
+                <flow-analysis-drawer></flow-analysis-drawer>
+            </mat-sidenav>
+            <mat-sidenav-content>
+                <graph-controls></graph-controls>
+                <div
+                    id="canvas-container"
+                    class="canvas-background select-none h-full w-full"
+                    [cdkContextMenuTriggerFor]="contextMenu.menu"></div>
+                <fd-context-menu #contextMenu 
[menuProvider]="canvasContextMenu" menuId="root"></fd-context-menu>
+            </mat-sidenav-content>
+        </mat-sidenav-container>
     </div>
     <fd-footer></fd-footer>
 </div>
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.spec.ts
index 26c8de4e9e..7ae42d3448 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.spec.ts
@@ -30,6 +30,10 @@ import { HeaderComponent } from './header/header.component';
 import { FooterComponent } from './footer/footer.component';
 import { canvasFeatureKey } from '../../state';
 import { flowFeatureKey } from '../../state/flow';
+import { MatSidenavModule } from '@angular/material/sidenav';
+import { FlowAnalysisDrawerComponent } from 
'./header/flow-analysis-drawer/flow-analysis-drawer.component';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { CanvasActionsService } from '../../service/canvas-actions.service';
 
 describe('Canvas', () => {
     let component: Canvas;
@@ -54,9 +58,12 @@ describe('Canvas', () => {
             imports: [
                 CdkContextMenuTrigger,
                 ContextMenu,
+                MatSidenavModule,
+                NoopAnimationsModule,
                 MockComponent(GraphControls),
                 MockComponent(HeaderComponent),
-                MockComponent(FooterComponent)
+                MockComponent(FooterComponent),
+                FlowAnalysisDrawerComponent
             ],
             providers: [
                 provideMockStore({
@@ -71,7 +78,8 @@ describe('Canvas', () => {
                             value: breadcrumbEntity
                         }
                     ]
-                })
+                }),
+                CanvasActionsService
             ]
         });
         fixture = TestBed.createComponent(Canvas);
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts
index 0eeef0f8d5..ee923e4d69 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.ts
@@ -43,6 +43,7 @@ import {
     selectConnection,
     selectCurrentProcessGroupId,
     selectEditedCurrentProcessGroup,
+    selectFlowAnalysisOpen,
     selectFunnel,
     selectInputPort,
     selectLabel,
@@ -80,6 +81,8 @@ export class Canvas implements OnInit, OnDestroy {
     private scale: number = INITIAL_SCALE;
     private canvasClicked = false;
 
+    flowAnalysisOpen = this.store.selectSignal(selectFlowAnalysisOpen);
+
     constructor(
         private store: Store<CanvasState>,
         private canvasView: CanvasView,
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.module.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.module.ts
index 4f09f38986..b459f1e665 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.module.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.module.ts
@@ -17,6 +17,7 @@
 
 import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
+import { MatSidenavModule } from '@angular/material/sidenav';
 import { Canvas } from './canvas.component';
 import { ContextMenu } from 
'../../../../ui/common/context-menu/context-menu.component';
 import { CdkContextMenuTrigger, CdkMenu, CdkMenuItem, CdkMenuTrigger } from 
'@angular/cdk/menu';
@@ -24,6 +25,7 @@ import { GraphControls } from 
'./graph-controls/graph-controls.component';
 import { CanvasRoutingModule } from './canvas-routing.module';
 import { HeaderComponent } from './header/header.component';
 import { FooterComponent } from './footer/footer.component';
+import { FlowAnalysisDrawerComponent } from 
'./header/flow-analysis-drawer/flow-analysis-drawer.component';
 
 @NgModule({
     declarations: [Canvas],
@@ -38,7 +40,9 @@ import { FooterComponent } from './footer/footer.component';
         GraphControls,
         ContextMenu,
         HeaderComponent,
-        FooterComponent
+        FooterComponent,
+        MatSidenavModule,
+        FlowAnalysisDrawerComponent
     ]
 })
 export class CanvasModule {}
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/flow-analysis-drawer.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/flow-analysis-drawer.component.html
new file mode 100644
index 0000000000..3493603e37
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/flow-analysis-drawer.component.html
@@ -0,0 +1,258 @@
+<!--
+  ~ 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="flow-analysis-drawer h-full w-96 p-5">
+    <component-context type="ProcessGroup" [name]="processGroupName" 
[id]="currentProcessGroupId"></component-context>
+    <div class="flex items-center w-full">
+        <ng-container *ngIf="isAnalysisPending">
+            <span *nifiSpinner="isAnalysisPending"></span>
+            <div class="ml-1">Rules analysis pending...</div>
+        </ng-container>
+    </div>
+    <div class="mt-5">
+        <div class="flex items-center justify-between">
+            <div>
+                <div>
+                    <mat-checkbox class="text-sm" color="primary" 
[(ngModel)]="showEnforcedViolations">
+                        Show enforced violations
+                    </mat-checkbox>
+                </div>
+                <div>
+                    <mat-checkbox color="primary" 
[(ngModel)]="showWarningViolations">
+                        Show warning violations
+                    </mat-checkbox>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="mt-5 mb-2" [hidden]="showEnforcedViolations() || 
showWarningViolations()">
+        <div class="flex flex-col gap-y-2">
+            <mat-expansion-panel hideToggle>
+                <mat-expansion-panel-header>
+                    <mat-panel-title>
+                        <div class="flex flex-1 justify-start">
+                            <div>Enforced Rules ({{ enforcedRules.length 
}})</div>
+                        </div>
+                    </mat-panel-title>
+                </mat-expansion-panel-header>
+
+                @if (rules) {
+                    @for (rule of enforcedRules; track rule.id) {
+                        <div class="mb-2">
+                            <div class="flex justify-between">
+                                <div class="flex items-center">{{ rule.name 
}}</div>
+                                <button
+                                    mat-icon-button
+                                    type="button"
+                                    [matMenuTriggerFor]="menu"
+                                    class="h-16 w-16 flex items-center 
justify-center">
+                                    <i class="fa fa-ellipsis-v"></i>
+                                </button>
+                                <mat-menu #menu="matMenu">
+                                    <button mat-menu-item 
(click)="openDocumentation(rule)">
+                                        <i class="fa fa-book mr-2 
primary-color"></i>
+                                        View Documentation
+                                    </button>
+                                    <button mat-menu-item 
(click)="openRule(rule)">
+                                        <i class="fa fa-cog mr-2 
primary-color"></i>
+                                        Edit Rule
+                                    </button>
+                                </mat-menu>
+                            </div>
+                            @if (violationsMap.size > 0 && 
violationsMap.get(rule.id)) {
+                                <div class="warn-color-darker text-sm">
+                                    <ng-container 
[ngPlural]="violationsMap.get(rule.id).length">
+                                        <ng-template ngPluralCase="=1"
+                                            >{{ 
violationsMap.get(rule.id).length }} violation</ng-template
+                                        >
+                                        <ng-template ngPluralCase="other"
+                                            >{{ 
violationsMap.get(rule.id).length }} violations</ng-template
+                                        >
+                                    </ng-container>
+                                </div>
+                                @for (violation of violationsMap.get(rule.id); 
track violation.scope) {
+                                    <div class="flex align-center 
justify-between mt-2">
+                                        <div class="flex flex-col items-start 
ml-2">
+                                            <div 
*ngIf="violation?.subjectPermissionDto?.canRead; else unauthorized">
+                                                {{ 
violation.subjectDisplayName }}
+                                            </div>
+                                            <span class="text-sm">
+                                                {{ violation.subjectId }}
+                                            </span>
+                                        </div>
+
+                                        <ng-template
+                                            
[ngTemplateOutlet]="violationMenuTemplate"
+                                            [ngTemplateOutletContext]="{ 
violation: violation }"></ng-template>
+                                    </div>
+                                }
+                            }
+                        </div>
+                    }
+                }
+            </mat-expansion-panel>
+            <mat-expansion-panel hideToggle>
+                <mat-expansion-panel-header>
+                    <mat-panel-title>
+                        <div class="flex flex-1 justify-start">
+                            <div>Warning Rules ({{ warningRules.length 
}})</div>
+                        </div>
+                    </mat-panel-title>
+                </mat-expansion-panel-header>
+
+                @if (rules) {
+                    @for (rule of warningRules; track rule.id) {
+                        <div>
+                            <div class="flex justify-between">
+                                <div class="flex items-center">{{ rule.name 
}}</div>
+                                <button
+                                    mat-icon-button
+                                    type="button"
+                                    [matMenuTriggerFor]="menu"
+                                    class="h-16 w-16 flex items-center 
justify-center">
+                                    <i class="fa fa-ellipsis-v"></i>
+                                </button>
+                                <mat-menu #menu="matMenu" class="rule-menu 
w-52 shadow-lg">
+                                    <button mat-menu-item 
(click)="openDocumentation(rule)">
+                                        <i class="fa fa-book mr-2 
primary-color"></i>
+                                        View Documentation
+                                    </button>
+                                    <button mat-menu-item 
(click)="openRule(rule)">
+                                        <i class="fa fa-cog mr-2 
primary-color"></i>
+                                        Edit Rule
+                                    </button>
+                                </mat-menu>
+                            </div>
+                            @if (violationsMap.size > 0 && 
violationsMap.get(rule.id)) {
+                                <div class="primary-color text-sm">
+                                    <ng-container 
[ngPlural]="violationsMap.get(rule.id).length">
+                                        <ng-template ngPluralCase="=1"
+                                            >{{ 
violationsMap.get(rule.id).length }} violation</ng-template
+                                        >
+                                        <ng-template ngPluralCase="other"
+                                            >{{ 
violationsMap.get(rule.id).length }} violations</ng-template
+                                        >
+                                    </ng-container>
+                                </div>
+                                <ul>
+                                    @for (violation of 
violationsMap.get(rule.id); track violation.scope) {
+                                        <li class="flex align-center 
justify-between mt-2">
+                                            <div class="flex flex-col 
items-start ml-2">
+                                                <div 
*ngIf="violation?.subjectPermissionDto?.canRead; else unauthorized">
+                                                    {{ 
violation.subjectDisplayName }}
+                                                </div>
+                                                <span class="text-sm">
+                                                    {{ violation.subjectId }}
+                                                </span>
+                                            </div>
+
+                                            <ng-template
+                                                
[ngTemplateOutlet]="violationMenuTemplate"
+                                                [ngTemplateOutletContext]="{ 
violation: violation }"></ng-template>
+                                        </li>
+                                    }
+                                </ul>
+                            }
+                        </div>
+                    }
+                }
+            </mat-expansion-panel>
+        </div>
+    </div>
+
+    <div class="mt-5 mb-2" [hidden]="!showEnforcedViolations()" 
[class.mb-5]="!showWarningViolations()">
+        <div class="border-b pb-2">
+            <div>
+                Enforced Violations
+                <span>({{ enforcedViolations.length }})</span>
+            </div>
+        </div>
+
+        <ul>
+            @for (violation of enforcedViolations; track violation.scope) {
+                <li class="mt-2 pb-2 border-b last-of-type:border-0">
+                    <div class="warn-color-darker">{{ 
getRuleName(violation.ruleId) }}</div>
+                    <div class="flex align-center justify-between ml-2">
+                        <div class="flex flex-col items-start">
+                            <div 
*ngIf="violation?.subjectPermissionDto?.canRead; else unauthorized">
+                                {{ violation.subjectDisplayName }}
+                            </div>
+                            <span class="text-sm">{{ violation.subjectId 
}}</span>
+                        </div>
+
+                        <ng-template [ngTemplateOutlet]="violationMenuTemplate"
+                                     [ngTemplateOutletContext]="{ violation: 
violation }"></ng-template>
+                    </div>
+                </li>
+            }
+        </ul>
+    </div>
+
+    <div class="mt-5 mb-2" [hidden]="!showWarningViolations()">
+        <div class="border-b pb-2">
+            <div>
+                Warning Violations
+                <span>({{ warningViolations.length }})</span>
+            </div>
+        </div>
+
+        <ul>
+            @for (violation of warningViolations; track violation.scope) {
+                <li class="mt-2 pb-2 border-b last-of-type:border-0">
+                    <div class="warn-color-darker">{{ 
getRuleName(violation.ruleId) }}</div>
+                    <div class="flex align-center justify-between ml-2">
+                        <div class="flex flex-col items-start">
+                            <div 
*ngIf="violation?.subjectPermissionDto?.canRead; else unauthorized">
+                                {{ violation.subjectDisplayName }}
+                            </div>
+                            <span class="text-sm">{{ violation.subjectId 
}}</span>
+                        </div>
+
+                        <ng-template
+                            [ngTemplateOutlet]="violationMenuTemplate"
+                            [ngTemplateOutletContext]="{ violation: violation 
}"></ng-template>
+                    </div>
+                </li>
+            }
+        </ul>
+    </div>
+</div>
+
+<ng-template #violationMenuTemplate let-violation="violation">
+    <button
+        mat-icon-button
+        type="button"
+        [matMenuTriggerFor]="violationMenu"
+        class="h-16 w-16 flex items-center justify-center">
+        <i class="fa fa-ellipsis-v"></i>
+    </button>
+    <mat-menu #violationMenu="matMenu">
+        <button
+            mat-menu-item
+            (click)="viewViolationDetails(violation)"
+            [disabled]="!violation?.subjectPermissionDto?.canRead">
+            <i class="fa fa-info-circle mr-2 primary-color"></i>Violation 
details
+        </button>
+        <button
+            mat-menu-item
+            [routerLink]="getProcessorLink(violation)"
+            *ngIf="violation?.subjectComponentType === 'PROCESSOR' && 
violation?.subjectPermissionDto?.canRead">
+            <i class="fa mr-2 fa-long-arrow-right primary-color"></i>Go to 
component
+        </button>
+    </mat-menu>
+</ng-template>
+
+<ng-template #unauthorized> Unauthorized </ng-template>
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/flow-analysis-drawer.component.scss
similarity index 83%
copy from 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
copy to 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/flow-analysis-drawer.component.scss
index 5414141b19..2944f98194 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/flow-analysis-drawer.component.scss
@@ -14,16 +14,3 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-.flow-status {
-    box-sizing: content-box;
-
-    .fa,
-    .icon {
-        font-style: normal;
-    }
-
-    .controller-bulletins {
-        cursor: default;
-    }
-}
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/flow-analysis-drawer.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/flow-analysis-drawer.component.spec.ts
new file mode 100644
index 0000000000..dcd357b816
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/flow-analysis-drawer.component.spec.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FlowAnalysisDrawerComponent } from './flow-analysis-drawer.component';
+import { provideMockStore } from '@ngrx/store/testing';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+
+describe('FlowAnalysisDrawerComponent', () => {
+    let component: FlowAnalysisDrawerComponent;
+    let fixture: ComponentFixture<FlowAnalysisDrawerComponent>;
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [FlowAnalysisDrawerComponent, NoopAnimationsModule],
+            providers: [provideMockStore({})]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(FlowAnalysisDrawerComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/flow-analysis-drawer.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/flow-analysis-drawer.component.ts
new file mode 100644
index 0000000000..ed5283eb2c
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/flow-analysis-drawer.component.ts
@@ -0,0 +1,167 @@
+/*
+ * 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 { Component, model } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { Store } from '@ngrx/store';
+import { MatMenuModule } from '@angular/material/menu';
+import { MatIconModule } from '@angular/material/icon';
+import { MatExpansionModule } from '@angular/material/expansion';
+import { navigateToComponentDocumentation } from 
'../../../../../../state/documentation/documentation.actions';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { FormsModule } from '@angular/forms';
+import { selectFlowAnalysisState } from 
'../../../../state/flow-analysis/flow-analysis.selectors';
+import {
+    navigateToEditFlowAnalysisRule,
+    startPollingFlowAnalysis,
+    openRuleDetailsDialog
+} from '../../../../state/flow-analysis/flow-analysis.actions';
+import { FlowAnalysisRule, FlowAnalysisRuleViolation } from 
'../../../../state/flow-analysis';
+import {
+    selectBreadcrumbs,
+    selectCurrentProcessGroupId
+} from '../../../../state/flow/flow.selectors';
+import { RouterLink } from '@angular/router';
+import { NifiSpinnerDirective } from 
'../../../../../../ui/common/spinner/nifi-spinner.directive';
+import { MatIconButton } from '@angular/material/button';
+import { ComponentContext } from '@nifi/shared';
+import { BreadcrumbEntity } from '../../../../state/shared';
+
+@Component({
+    selector: 'flow-analysis-drawer',
+    standalone: true,
+    imports: [
+        CommonModule,
+        MatMenuModule,
+        MatIconModule,
+        MatExpansionModule,
+        MatCheckboxModule,
+        FormsModule,
+        RouterLink,
+        NifiSpinnerDirective,
+        MatIconButton,
+        ComponentContext
+    ],
+    templateUrl: './flow-analysis-drawer.component.html',
+    styleUrl: './flow-analysis-drawer.component.scss'
+})
+export class FlowAnalysisDrawerComponent {
+    violationsMap = new Map();
+    warningRules: FlowAnalysisRule[] = [];
+    enforcedRules: FlowAnalysisRule[] = [];
+    warningViolations: FlowAnalysisRuleViolation[] = [];
+    enforcedViolations: FlowAnalysisRuleViolation[] = [];
+    rules: FlowAnalysisRule[] = [];
+    currentProcessGroupId = '';
+    isAnalysisPending = false;
+    showEnforcedViolations = model(false);
+    showWarningViolations = model(false);
+    flowAnalysisState$ = this.store.select(selectFlowAnalysisState);
+    currentProcessGroupId$ = this.store.select(selectCurrentProcessGroupId);
+    processGroupName = '';
+
+    constructor(private store: Store) {
+        this.store.dispatch(startPollingFlowAnalysis());
+        this.flowAnalysisState$.pipe(takeUntilDestroyed()).subscribe((res) => {
+            this.clearRulesTracking();
+            this.isAnalysisPending = res.flowAnalysisPending;
+            this.rules = res.rules;
+
+            res.rules.forEach((rule: FlowAnalysisRule) => {
+                if (rule.enforcementPolicy === 'WARN') {
+                    this.warningRules.push(rule);
+                } else {
+                    this.enforcedRules.push(rule);
+                }
+            });
+            res.ruleViolations.forEach((violation: FlowAnalysisRuleViolation) 
=> {
+                if (this.violationsMap.has(violation.ruleId)) {
+                    this.violationsMap.get(violation.ruleId).push(violation);
+                } else {
+                    this.violationsMap.set(violation.ruleId, [violation]);
+                }
+            });
+            this.enforcedViolations = res.ruleViolations.filter(function 
(violation: FlowAnalysisRuleViolation) {
+                return violation.enforcementPolicy === 'ENFORCE';
+            });
+            this.warningViolations = res.ruleViolations.filter(function 
(violation: FlowAnalysisRuleViolation) {
+                return violation.enforcementPolicy === 'WARN';
+            });
+        });
+        this.currentProcessGroupId$.subscribe((pgId) => {
+            this.currentProcessGroupId = pgId;
+        });
+        this.store
+            .select(selectBreadcrumbs)
+            .pipe(takeUntilDestroyed())
+            .subscribe((breadcrumbs: BreadcrumbEntity) => {
+                this.processGroupName = breadcrumbs.breadcrumb.name;
+            });
+    }
+
+    openRule(rule: FlowAnalysisRule) {
+        this.store.dispatch(
+            navigateToEditFlowAnalysisRule({
+                id: rule.id
+            })
+        );
+    }
+
+    clearRulesTracking() {
+        this.enforcedRules = [];
+        this.warningRules = [];
+        this.violationsMap.clear();
+    }
+
+    openDocumentation(rule: FlowAnalysisRule) {
+        this.store.dispatch(
+            navigateToComponentDocumentation({
+                request: {
+                    backNavigation: {
+                        route: ['/process-groups', this.currentProcessGroupId],
+                        routeBoundary: ['/documentation'],
+                        context: 'Canvas'
+                    },
+                    parameters: {
+                        select: rule.type,
+                        group: rule.bundle.group,
+                        artifact: rule.bundle.artifact,
+                        version: rule.bundle.version
+                    }
+                }
+            })
+        );
+    }
+
+    viewViolationDetails(violation: FlowAnalysisRuleViolation) {
+        const ruleTest: FlowAnalysisRule = this.rules.find((rule) => rule.id 
=== violation.ruleId)!;
+        this.store.dispatch(openRuleDetailsDialog({ violation, rule: ruleTest 
}));
+    }
+
+    getProcessorLink(violation: FlowAnalysisRuleViolation): string[] {
+        return ['/process-groups', violation.groupId, 
violation.subjectComponentType, violation.subjectId];
+    }
+
+    getRuleName(id: string) {
+        const rule = this.rules.find(function (rule: FlowAnalysisRule) {
+            return rule.id === id;
+        });
+
+        return rule ? rule.name : '';
+    }
+}
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-analysis-drawer/violation-details-dialog/_violation-details-dialog.component-theme.scss
similarity index 63%
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/header/flow-analysis-drawer/violation-details-dialog/_violation-details-dialog.component-theme.scss
index 4c4e73ca53..c47dd4dee9 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-analysis-drawer/violation-details-dialog/_violation-details-dialog.component-theme.scss
@@ -29,18 +29,10 @@
     $supplemental-theme-surface-palette: 
map.get($supplemental-theme-color-config, 'primary');
 
     // Get hues from palette
-    $is-dark: map-get($supplemental-theme-color-config, is-dark);
+    $is-dark: map-get($material-theme-color-config, is-dark);
     $material-theme-primary-palette-default: 
mat.m2-get-color-from-palette($material-theme-primary-palette);
     $material-theme-primary-palette-lighter: 
mat.m2-get-color-from-palette($material-theme-primary-palette, lighter);
     $material-theme-warn-palette-darker: 
mat.m2-get-color-from-palette($material-theme-warn-palette, darker);
-    $supplemental-theme-surface-palette-lighter: mat.m2-get-color-from-palette(
-        $supplemental-theme-surface-palette,
-        lighter
-    );
-    $supplemental-theme-surface-palette-darker: mat.m2-get-color-from-palette(
-        $supplemental-theme-surface-palette,
-        darker
-    );
     $supplemental-theme-surface-palette-darker-contrast: 
mat.m2-get-color-from-palette(
         $supplemental-theme-surface-palette,
         darker-contrast
@@ -50,32 +42,25 @@
         lighter-contrast
     );
 
-    .flow-status {
-        background: if(
+    .pill.enforce {
+        background-color: $material-theme-warn-palette-darker;
+        color: if(
             $is-dark,
-            $supplemental-theme-surface-palette-darker,
-            $supplemental-theme-surface-palette-lighter
+            $supplemental-theme-surface-palette-lighter-contrast,
+            $supplemental-theme-surface-palette-darker-contrast
         );
+    }
 
-        .controller-bulletins {
-            background-color: if(
-                $is-dark,
-                $material-theme-primary-palette-lighter,
-                $material-theme-primary-palette-default
-            );
-
-            .fa {
-                // invert the contrast colors since the surface is dark in 
light mode and light in dark mode
-                color: if(
-                    $is-dark,
-                    $supplemental-theme-surface-palette-lighter-contrast,
-                    $supplemental-theme-surface-palette-darker-contrast
-                );
-            }
-        }
-
-        .controller-bulletins.has-bulletins {
-            background-color: $material-theme-warn-palette-darker;
-        }
+    .pill.warn {
+        background-color: if(
+            $is-dark,
+            $material-theme-primary-palette-lighter,
+            $material-theme-primary-palette-default
+        );
+        color: if(
+            $is-dark,
+            $supplemental-theme-surface-palette-lighter-contrast,
+            $supplemental-theme-surface-palette-darker-contrast
+        );
     }
 }
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component.html
similarity index 50%
copy from 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.html
copy to 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component.html
index 802e15c2fa..96bd40d4df 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/canvas.component.html
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component.html
@@ -15,15 +15,28 @@
   ~ limitations under the License.
   -->
 
-<div class="flex flex-col h-full">
-    <fd-header></fd-header>
-    <div class="flex-1">
-        <graph-controls></graph-controls>
-        <div
-            id="canvas-container"
-            class="canvas-background select-none h-full"
-            [cdkContextMenuTriggerFor]="contextMenu.menu"></div>
-        <fd-context-menu #contextMenu [menuProvider]="canvasContextMenu" 
menuId="root"></fd-context-menu>
+<h2 mat-dialog-title>
+    <div class="flex justify-between items-baseline">
+        <div>Violation Information</div>
     </div>
-    <fd-footer></fd-footer>
-</div>
+</h2>
+<mat-dialog-content>
+    <div>
+        <div class="flex justify-between mt-4 mb-4">
+            <div>Violation</div>
+            <div class="py-1 px-2 rounded-xl pill" 
[ngClass]="violation.enforcementPolicy | lowercase">
+                {{ violation.enforcementPolicy }}
+            </div>
+        </div>
+
+        <p>{{ violation.violationMessage }}</p>
+
+        <button mat-menu-item (click)="viewDocumentation()">
+            <i class="fa fa-book primary-color mr-2"></i>
+            View Documentation
+        </button>
+    </div>
+    <mat-dialog-actions align="end">
+        <button mat-button mat-dialog-close color="primary">Close</button>
+    </mat-dialog-actions>
+</mat-dialog-content>
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component.scss
similarity index 83%
copy from 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
copy to 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component.scss
index 5414141b19..2944f98194 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component.scss
@@ -14,16 +14,3 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-.flow-status {
-    box-sizing: content-box;
-
-    .fa,
-    .icon {
-        font-style: normal;
-    }
-
-    .controller-bulletins {
-        cursor: default;
-    }
-}
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component.spec.ts
new file mode 100644
index 0000000000..e25954b911
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component.spec.ts
@@ -0,0 +1,104 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ViolationDetailsDialogComponent } from 
'./violation-details-dialog.component';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { provideMockStore } from '@ngrx/store/testing';
+
+describe('ViolationDetailsDialogComponent', () => {
+    let component: ViolationDetailsDialogComponent;
+    let fixture: ComponentFixture<ViolationDetailsDialogComponent>;
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [ViolationDetailsDialogComponent],
+            providers: [
+                {
+                    provide: MAT_DIALOG_DATA,
+                    useValue: {
+                        violation: {
+                            enforcementPolicy: 'ENFORCE',
+                            scope: '4b4cfdf2-0190-1000-500d-b170055a0a34',
+                            subjectId: '4b4cfdf2-0190-1000-500d-b170055a0a34',
+                            subjectDisplayName: 'AttributeRollingWindow',
+                            groupId: '36890551-0190-1000-cfaa-27b854604d18',
+                            ruleId: '369c8d4e-0190-1000-99b5-8c4e0144f1a9',
+                            issueId: 'default',
+                            violationMessage: "'AttributeRollingWindow' is not 
allowed",
+                            subjectComponentType: 'PROCESSOR',
+                            subjectPermissionDto: {
+                                canRead: true,
+                                canWrite: true
+                            },
+                            enabled: false
+                        },
+                        rule: {
+                            id: '369c8d4e-0190-1000-99b5-8c4e0144f1a9',
+                            name: 'DisallowComponentType',
+                            type: 
'org.apache.nifi.flowanalysis.rules.DisallowComponentType',
+                            bundle: {
+                                group: 'org.apache.nifi',
+                                artifact: 'nifi-standard-nar',
+                                version: '2.0.0-SNAPSHOT'
+                            },
+                            state: 'ENABLED',
+                            persistsState: false,
+                            restricted: false,
+                            deprecated: false,
+                            multipleVersionsAvailable: false,
+                            supportsSensitiveDynamicProperties: false,
+                            enforcementPolicy: 'ENFORCE',
+                            properties: {
+                                'component-type': 'AttributeRollingWindow'
+                            },
+                            descriptors: {
+                                'component-type': {
+                                    name: 'component-type',
+                                    displayName: 'Component Type',
+                                    description:
+                                        "Components of the given type will 
produce a rule violation (i.e. they shouldn't exist). Either the simple or the 
fully qualified name of the type should be provided.",
+                                    required: true,
+                                    sensitive: false,
+                                    dynamic: false,
+                                    supportsEl: false,
+                                    expressionLanguageScope: 'Not Supported',
+                                    dependencies: []
+                                }
+                            },
+                            validationStatus: 'VALID',
+                            extensionMissing: false
+                        }
+                    }
+                },
+                {
+                    provide: MatDialogRef,
+                    useValue: {}
+                },
+                provideMockStore({})
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ViolationDetailsDialogComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component.ts
new file mode 100644
index 0000000000..f76ae28325
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component.ts
@@ -0,0 +1,74 @@
+/*
+ * 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 { Component, Inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { FlowAnalysisRule, FlowAnalysisRuleViolation } from 
'../../../../../state/flow-analysis';
+import { Store } from '@ngrx/store';
+import { navigateToComponentDocumentation } from 
'apps/nifi/src/app/state/documentation/documentation.actions';
+import { selectCurrentProcessGroupId } from 
'../../../../../state/flow/flow.selectors';
+
+interface Data {
+    violation: FlowAnalysisRuleViolation;
+    rule: FlowAnalysisRule;
+}
+
+@Component({
+    selector: 'app-violation-details-dialog',
+    standalone: true,
+    imports: [CommonModule, MatDialogModule],
+    templateUrl: './violation-details-dialog.component.html',
+    styleUrl: './violation-details-dialog.component.scss'
+})
+export class ViolationDetailsDialogComponent {
+    violation: FlowAnalysisRuleViolation;
+    rule: FlowAnalysisRule;
+    currentProcessGroupId$ = this.store.select(selectCurrentProcessGroupId);
+    currentProcessGroupId = '';
+
+    constructor(
+        @Inject(MAT_DIALOG_DATA) public data: Data,
+        private store: Store
+    ) {
+        this.violation = data.violation;
+        this.rule = data.rule;
+        this.currentProcessGroupId$.subscribe((pgId) => {
+            this.currentProcessGroupId = pgId;
+        });
+    }
+
+    viewDocumentation() {
+        this.store.dispatch(
+            navigateToComponentDocumentation({
+                request: {
+                    backNavigation: {
+                        route: ['/process-groups', this.currentProcessGroupId],
+                        routeBoundary: ['/documentation'],
+                        context: 'Canvas'
+                    },
+                    parameters: {
+                        select: this.rule.type,
+                        group: this.rule.bundle.group,
+                        artifact: this.rule.bundle.artifact,
+                        version: this.rule.bundle.version
+                    }
+                }
+            })
+        );
+    }
+}
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 4c4e73ca53..0e11c021d2 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
@@ -34,48 +34,89 @@
     $material-theme-primary-palette-lighter: 
mat.m2-get-color-from-palette($material-theme-primary-palette, lighter);
     $material-theme-warn-palette-darker: 
mat.m2-get-color-from-palette($material-theme-warn-palette, darker);
     $supplemental-theme-surface-palette-lighter: mat.m2-get-color-from-palette(
-        $supplemental-theme-surface-palette,
-        lighter
+            $supplemental-theme-surface-palette,
+            lighter
     );
     $supplemental-theme-surface-palette-darker: mat.m2-get-color-from-palette(
-        $supplemental-theme-surface-palette,
-        darker
+            $supplemental-theme-surface-palette,
+            darker
     );
     $supplemental-theme-surface-palette-darker-contrast: 
mat.m2-get-color-from-palette(
-        $supplemental-theme-surface-palette,
-        darker-contrast
+            $supplemental-theme-surface-palette,
+            darker-contrast
     );
     $supplemental-theme-surface-palette-lighter-contrast: 
mat.m2-get-color-from-palette(
-        $supplemental-theme-surface-palette,
-        lighter-contrast
+            $supplemental-theme-surface-palette,
+            lighter-contrast
     );
 
     .flow-status {
         background: if(
-            $is-dark,
-            $supplemental-theme-surface-palette-darker,
-            $supplemental-theme-surface-palette-lighter
+                $is-dark,
+                $supplemental-theme-surface-palette-darker,
+                $supplemental-theme-surface-palette-lighter
         );
 
         .controller-bulletins {
             background-color: if(
-                $is-dark,
-                $material-theme-primary-palette-lighter,
-                $material-theme-primary-palette-default
+                    $is-dark,
+                    $material-theme-primary-palette-lighter,
+                    $material-theme-primary-palette-default
             );
 
             .fa {
                 // invert the contrast colors since the surface is dark in 
light mode and light in dark mode
                 color: if(
+                        $is-dark,
+                        $supplemental-theme-surface-palette-lighter-contrast,
+                        $supplemental-theme-surface-palette-darker-contrast
+                );
+            }
+        }
+
+        .controller-bulletins.has-bulletins {
+            background-color: $material-theme-warn-palette-darker;
+        }
+
+        .flow-analysis-notifications.warn {
+            background-color: if(
+                    $is-dark,
+                    $material-theme-primary-palette-lighter,
+                    $material-theme-primary-palette-default
+            );
+            border-right-color: if(
                     $is-dark,
                     $supplemental-theme-surface-palette-lighter-contrast,
                     $supplemental-theme-surface-palette-darker-contrast
+            );
+
+
+            .fa {
+                // invert the contrast colors since the surface is dark in 
light mode and light in dark mode
+                color: if(
+                        $is-dark,
+                        $supplemental-theme-surface-palette-lighter-contrast,
+                        $supplemental-theme-surface-palette-darker-contrast
                 );
             }
         }
 
-        .controller-bulletins.has-bulletins {
+        .flow-analysis-notifications.enforce {
             background-color: $material-theme-warn-palette-darker;
+            border-right-color: if(
+                    $is-dark,
+                    $supplemental-theme-surface-palette-lighter-contrast,
+                    $supplemental-theme-surface-palette-darker-contrast
+            );
+
+            .fa {
+                // invert the contrast colors since the surface is dark in 
light mode and light in dark mode
+                color: if(
+                        $is-dark,
+                        $supplemental-theme-surface-palette-lighter-contrast,
+                        $supplemental-theme-surface-palette-darker-contrast
+                );
+            }
         }
     }
 }
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.html
index 1903fa5d2c..d2b1261dbc 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.html
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.html
@@ -127,6 +127,12 @@
         </div>
         <div class="flex">
             <search [currentProcessGroupId]="currentProcessGroupId"></search>
+            <button
+                class="flow-analysis-notifications w-8 border-l border-r flex 
justify-center items-center pointer"
+                (click)="toggleFlowAnalysis()"
+                [ngClass]="flowAnalysisNotificationClass">
+                <i class="fa fa-medkit"></i>
+            </button>
             @if (hasBulletins()) {
                 <button
                     nifiTooltip
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
index 5414141b19..af36ba0a8f 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.scss
@@ -18,11 +18,6 @@
 .flow-status {
     box-sizing: content-box;
 
-    .fa,
-    .icon {
-        font-style: normal;
-    }
-
     .controller-bulletins {
         cursor: default;
     }
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.ts
index 42b9424eb8..95f71cc4c7 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component.ts
@@ -22,23 +22,44 @@ import { BulletinsTip } from 
'../../../../../../ui/common/tooltips/bulletins-tip
 import { BulletinEntity, BulletinsTipInput } from 
'../../../../../../state/shared';
 
 import { Search } from '../search/search.component';
-import { NifiTooltipDirective } from '@nifi/shared';
+import { NifiTooltipDirective, Storage } from '@nifi/shared';
 import { ClusterSummary } from '../../../../../../state/cluster-summary';
 import { ConnectedPosition } from '@angular/cdk/overlay';
+import { FlowAnalysisState } from '../../../../state/flow-analysis';
+import { CommonModule } from '@angular/common';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../../../../state';
+import { setFlowAnalysisOpen } from '../../../../state/flow/flow.actions';
 
 @Component({
     selector: 'flow-status',
     standalone: true,
     templateUrl: './flow-status.component.html',
-    imports: [Search, NifiTooltipDirective],
+    imports: [Search, NifiTooltipDirective, CommonModule],
     styleUrls: ['./flow-status.component.scss']
 })
 export class FlowStatus {
+    private static readonly FLOW_ANALYSIS_VISIBILITY_KEY: string = 
'flow-analysis-visibility';
+    private static readonly FLOW_ANALYSIS_KEY: string = 'flow-analysis';
+    public flowAnalysisNotificationClass: string = '';
     @Input() controllerStatus: ControllerStatus = 
initialState.flowStatus.controllerStatus;
     @Input() lastRefreshed: string = 
initialState.flow.processGroupFlow.lastRefreshed;
     @Input() clusterSummary: ClusterSummary | null = null;
     @Input() currentProcessGroupId: string = initialState.id;
     @Input() loadingStatus = false;
+    @Input() flowAnalysisOpen = initialState.flowAnalysisOpen;
+    @Input() set flowAnalysisState(state: FlowAnalysisState) {
+        if (!state.ruleViolations.length) {
+            this.flowAnalysisNotificationClass = 'primary-color';
+        } else {
+            const isEnforcedRuleViolated = state.ruleViolations.find((v) => {
+                return v.enforcementPolicy === 'ENFORCE';
+            });
+            isEnforcedRuleViolated
+                ? (this.flowAnalysisNotificationClass = 'enforce')
+                : (this.flowAnalysisNotificationClass = 'warn');
+        }
+    }
 
     @Input() set bulletins(bulletins: BulletinEntity[]) {
         if (bulletins) {
@@ -52,6 +73,23 @@ export class FlowStatus {
 
     protected readonly BulletinsTip = BulletinsTip;
 
+    constructor(
+        private store: Store<NiFiState>,
+        private storage: Storage
+    ) {
+        try {
+            const item: { [key: string]: boolean } | null = 
this.storage.getItem(
+                FlowStatus.FLOW_ANALYSIS_VISIBILITY_KEY
+            );
+            if (item) {
+                const flowAnalysisOpen = item[FlowStatus.FLOW_ANALYSIS_KEY] 
=== true;
+                this.store.dispatch(setFlowAnalysisOpen({ flowAnalysisOpen }));
+            }
+        } catch (e) {
+            // likely could not parse item... ignoring
+        }
+    }
+
     hasTerminatedThreads(): boolean {
         return this.controllerStatus.terminatedThreadCount > 0;
     }
@@ -141,4 +179,18 @@ export class FlowStatus {
             offsetY: 8
         };
     }
+
+    toggleFlowAnalysis(): void {
+        const flowAnalysisOpen = !this.flowAnalysisOpen;
+        this.store.dispatch(setFlowAnalysisOpen({ flowAnalysisOpen }));
+
+        // update the current value in storage
+        let item: { [key: string]: boolean } | null = 
this.storage.getItem(FlowStatus.FLOW_ANALYSIS_VISIBILITY_KEY);
+        if (item == null) {
+            item = {};
+        }
+
+        item[FlowStatus.FLOW_ANALYSIS_KEY] = flowAnalysisOpen;
+        this.storage.setItem(FlowStatus.FLOW_ANALYSIS_VISIBILITY_KEY, item);
+    }
 }
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.html
index d77493993a..7c320341e3 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.html
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.html
@@ -69,11 +69,13 @@
         }
     </navigation>
     <flow-status
+        [flowAnalysisOpen]="(flowAnalysisOpen$ | async)!"
         [controllerStatus]="(controllerStatus$ | async)!"
         [lastRefreshed]="(lastRefreshed$ | async)!"
         [clusterSummary]="(clusterSummary$ | async)!"
         [bulletins]="(controllerBulletins$ | async)!"
         [currentProcessGroupId]="(currentProcessGroupId$ | async)!"
-        [loadingStatus]="(loadingService.status$ | async)!">
+        [loadingStatus]="(loadingService.status$ | async)!"
+        [flowAnalysisState]="(flowAnalysisState$ | async)!">
     </flow-status>
 </header>
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.ts
index a438299367..4ec1e91f72 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.ts
@@ -24,6 +24,7 @@ import {
     selectControllerBulletins,
     selectControllerStatus,
     selectCurrentProcessGroupId,
+    selectFlowAnalysisOpen,
     selectLastRefreshed
 } from '../../../state/flow/flow.selectors';
 import { LoadingService } from '../../../../../service/loading.service';
@@ -36,6 +37,7 @@ import { RouterLink } from '@angular/router';
 import { FlowStatus } from './flow-status/flow-status.component';
 import { Navigation } from 
'../../../../../ui/common/navigation/navigation.component';
 import { selectClusterSummary } from 
'../../../../../state/cluster-summary/cluster-summary.selectors';
+import { selectFlowAnalysisState } from 
'../../../state/flow-analysis/flow-analysis.selectors';
 
 @Component({
     selector: 'fd-header',
@@ -63,6 +65,8 @@ export class HeaderComponent {
     controllerBulletins$ = this.store.select(selectControllerBulletins);
     currentProcessGroupId$ = this.store.select(selectCurrentProcessGroupId);
     canvasPermissions$ = this.store.select(selectCanvasPermissions);
+    flowAnalysisState$ = this.store.select(selectFlowAnalysisState);
+    flowAnalysisOpen$ = this.store.select(selectFlowAnalysisOpen);
 
     constructor(
         private store: Store<CanvasState>,
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 7db0432493..8bdcf1fe58 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/styles.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/styles.scss
@@ -34,6 +34,8 @@
 @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;
+@use 
'app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component-theme'
+    as violation-details-dialog;
 @use 'app/pages/login/feature/login.component-theme' as login;
 @use 'app/pages/logout/feature/logout.component-theme' as logout;
 @use 'app/pages/provenance/feature/provenance.component-theme' as provenance;
@@ -107,6 +109,7 @@
 @include processor-status-table.generate-theme($supplemental-theme-light);
 @include change-color-dialog.generate-theme($supplemental-theme-light);
 @include text-editor.generate-theme($material-theme-light, 
$supplemental-theme-light);
+@include violation-details-dialog.generate-theme($material-theme-light, 
$supplemental-theme-light);
 
 .dark-theme {
     // Include the dark theme color styles.
@@ -140,4 +143,5 @@
     @include processor-status-table.generate-theme($supplemental-theme-dark);
     @include change-color-dialog.generate-theme($supplemental-theme-dark);
     @include text-editor.generate-theme($material-theme-dark, 
$supplemental-theme-dark);
+    @include violation-details-dialog.generate-theme($material-theme-dark, 
$supplemental-theme-dark);
 }

Reply via email to