You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by rf...@apache.org on 2023/12/11 22:36:48 UTC

(nifi) branch main updated: NIFI-12445: Provenance Event Listing (#8133)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new 4f59f46ce4 NIFI-12445: Provenance Event Listing (#8133)
4f59f46ce4 is described below

commit 4f59f46ce4d95b23805643b8f6026b0cfc4ee59a
Author: Matt Gilman <ma...@gmail.com>
AuthorDate: Mon Dec 11 17:36:42 2023 -0500

    NIFI-12445: Provenance Event Listing (#8133)
    
    - Provenance Event Listing.
    - View Provenance Event dialog.
    - Provenance routing.
    - Provenance search.
    - Replay.
    - Download content.
    - View content.
    - Addressing review feedback.
    - Addressing review feedback.
    
    This closes #8133
---
 .../src/main/nifi/proxy.config.mjs                 |  19 +
 .../src/main/nifi/src/app/app-routing.module.ts    |   5 +
 .../src/main/nifi/src/app/app.module.ts            |   9 +-
 .../behavior/connectable-behavior.service.spec.ts  |   6 +
 .../behavior/draggable-behavior.service.spec.ts    |   6 +
 .../behavior/quick-select-behavior.service.spec.ts |   6 +
 .../service/birdseye-view.service.spec.ts          |   6 +
 .../flow-designer/service/canvas-utils.service.ts  |  60 ++-
 .../service/canvas-view.service.spec.ts            |   6 +
 .../pages/flow-designer/service/flow.service.ts    |   9 +-
 .../manager/connection-manager.service.spec.ts     |   6 +
 .../service/manager/funnel-manager.service.spec.ts |   6 +
 .../remote-process-group-manager.service.spec.ts   |   6 +
 .../pages/flow-designer/state/flow/flow.actions.ts |  13 +-
 .../pages/flow-designer/state/flow/flow.effects.ts |  88 ++++
 .../app/pages/flow-designer/state/flow/index.ts    |   5 +
 .../canvas/context-menu/context-menu.component.ts  |  47 ++-
 .../ui/canvas/header/header.component.html         |   6 +-
 .../ui/canvas/header/header.component.spec.ts      |   6 +
 .../create-process-group.component.ts              |   1 +
 .../service/parameter-contexts.service.ts          |   4 -
 .../feature/provenance-routing.module.ts}          |  42 +-
 .../provenance/feature/provenance.component.html   |  28 ++
 .../provenance/feature/provenance.component.scss}  |  19 +-
 .../feature/provenance.component.spec.ts           |  48 +++
 .../provenance/feature/provenance.component.ts     |  42 ++
 .../pages/provenance/feature/provenance.module.ts  |  39 ++
 .../pages/provenance/service/provenance.service.ts | 130 ++++++
 .../src/app/{ => pages/provenance}/state/index.ts  |  33 +-
 .../state/provenance-event-listing/index.ts        | 100 +++++
 .../provenance-event-listing.actions.ts            |  78 ++++
 .../provenance-event-listing.effects.ts            | 361 +++++++++++++++++
 .../provenance-event-listing.reducer.ts            |  71 ++++
 .../provenance-event-listing.selectors.ts          |  86 ++++
 .../provenance-event-listing-routing.module.ts}    |  30 +-
 .../provenance-event-listing.component.html        |  43 ++
 .../provenance-event-listing.component.scss}       |  19 -
 .../provenance-event-listing.component.spec.ts     |  41 ++
 .../provenance-event-listing.component.ts          | 177 ++++++++
 .../provenance-event-listing.module.ts             |  41 ++
 .../provenance-event-table.component.html          | 156 +++++++
 .../provenance-event-table.component.scss}         |  26 +-
 .../provenance-event-table.component.spec.ts       |  40 ++
 .../provenance-event-table.component.ts            | 232 +++++++++++
 .../provenance-search-dialog.component.html        |  76 ++++
 .../provenance-search-dialog.component.scss}       |  27 +-
 .../provenance-search-dialog.component.spec.ts     |  85 ++++
 .../provenance-search-dialog.component.ts          | 220 ++++++++++
 .../{state/index.ts => service/about.service.ts}   |  28 +-
 .../nifi/src/app/service/nifi-common.service.ts    | 145 +++++++
 .../app/state/{index.ts => about/about.actions.ts} |  24 +-
 .../main/nifi/src/app/state/about/about.effects.ts |  48 +++
 .../main/nifi/src/app/state/about/about.reducer.ts |  50 +++
 .../state/{index.ts => about/about.selectors.ts}   |  20 +-
 .../main/nifi/src/app/state/{ => about}/index.ts   |  35 +-
 .../src/main/nifi/src/app/state/index.ts           |   6 +-
 .../src/main/nifi/src/app/state/shared/index.ts    |  64 +++
 .../cancel-dialog/cancel-dialog.component.html     |  24 ++
 .../cancel-dialog/cancel-dialog.component.scss}    |  19 -
 .../cancel-dialog/cancel-dialog.component.spec.ts  |  48 +++
 .../cancel-dialog/cancel-dialog.component.ts}      |  35 +-
 .../property-table/property-table.component.ts     |   8 +-
 .../provenance-event-dialog.component.html         | 447 +++++++++++++++++++++
 .../provenance-event-dialog.component.scss}        |  46 ++-
 .../provenance-event-dialog.component.spec.ts      |  85 ++++
 .../provenance-event-dialog.component.ts           |  98 +++++
 .../src/main/nifi/src/styles.scss                  |  10 +-
 67 files changed, 3579 insertions(+), 271 deletions(-)

diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/proxy.config.mjs b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/proxy.config.mjs
index ee3441d494..1ac6ebf440 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/proxy.config.mjs
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/proxy.config.mjs
@@ -16,5 +16,24 @@ export default {
         headers: {
             'X-ProxyPort': 4200
         }
+    },
+    '/nifi-content-viewer/*': {
+        target: 'https://localhost:8443',
+        secure: false,
+        logLevel: 'debug',
+        changeOrigin: true,
+        headers: {
+            'X-ProxyPort': 4200
+        }
+    },
+    // the following entry is needed because the content viewer (and other UIs) load resources from existing nifi ui
+    '/nifi/*': {
+        target: 'https://localhost:8443',
+        secure: false,
+        logLevel: 'debug',
+        changeOrigin: true,
+        headers: {
+            'X-ProxyPort': 4200
+        }
     }
 };
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app-routing.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app-routing.module.ts
index 07361220c0..f70d4a9d40 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app-routing.module.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app-routing.module.ts
@@ -29,6 +29,11 @@ const routes: Routes = [
         canMatch: [authGuard],
         loadChildren: () => import('./pages/settings/feature/settings.module').then((m) => m.SettingsModule)
     },
+    {
+        path: 'provenance',
+        canMatch: [authGuard],
+        loadChildren: () => import('./pages/provenance/feature/provenance.module').then((m) => m.ProvenanceModule)
+    },
     {
         path: 'parameter-contexts',
         canMatch: [authGuard],
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts
index 6afaea16c7..505e438dd4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts
@@ -20,7 +20,6 @@ import { BrowserModule } from '@angular/platform-browser';
 import { AppRoutingModule } from './app-routing.module';
 import { AppComponent } from './app.component';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
-import { FlowDesignerModule } from './pages/flow-designer/feature/flow-designer.module';
 import { StoreModule } from '@ngrx/store';
 import { EffectsModule } from '@ngrx/effects';
 import { StoreDevtoolsModule } from '@ngrx/store-devtools';
@@ -29,13 +28,14 @@ import { HTTP_INTERCEPTORS, HttpClientModule, HttpClientXsrfModule } from '@angu
 import { NavigationActionTiming, RouterState, StoreRouterConnectingModule } from '@ngrx/router-store';
 import { rootReducers } from './state';
 import { UserEffects } from './state/user/user.effects';
-import { LoginModule } from './pages/login/feature/login.module';
 import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
 import { LoadingInterceptor } from './service/interceptors/loading.interceptor';
 import { AuthInterceptor } from './service/interceptors/auth.interceptor';
 import { ExtensionTypesEffects } from './state/extension-types/extension-types.effects';
 import { PollingInterceptor } from './service/interceptors/polling.interceptor';
 import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
+import { MatNativeDateModule } from '@angular/material/core';
+import { AboutEffects } from './state/about/about.effects';
 
 // @ts-ignore
 @NgModule({
@@ -54,13 +54,14 @@ import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
             routerState: RouterState.Minimal,
             navigationActionTiming: NavigationActionTiming.PostActivation
         }),
-        EffectsModule.forRoot(UserEffects, ExtensionTypesEffects),
+        EffectsModule.forRoot(UserEffects, ExtensionTypesEffects, AboutEffects),
         StoreDevtoolsModule.instrument({
             maxAge: 25,
             logOnly: environment.production,
             autoPause: true
         }),
-        MatProgressSpinnerModule
+        MatProgressSpinnerModule,
+        MatNativeDateModule
     ],
     providers: [
         {
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts
index 7848b5a4ad..c161f9865c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts
@@ -27,6 +27,8 @@ import { CanvasState } from '../../state';
 import { transformFeatureKey } from '../../state/transform';
 import { controllerServicesFeatureKey } from '../../state/controller-services';
 import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../../state/user/user.selectors';
+import * as fromUser from '../../../../state/user/user.reducer';
 
 describe('ConnectableBehavior', () => {
     let service: ConnectableBehavior;
@@ -46,6 +48,10 @@ describe('ConnectableBehavior', () => {
                         {
                             selector: selectFlowState,
                             value: initialState[flowFeatureKey]
+                        },
+                        {
+                            selector: selectUser,
+                            value: fromUser.initialState.user
                         }
                     ]
                 })
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts
index 0d35ab7053..71042a1b90 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts
@@ -28,6 +28,8 @@ import { transformFeatureKey } from '../../state/transform';
 import { selectFlowState } from '../../state/flow/flow.selectors';
 import { controllerServicesFeatureKey } from '../../state/controller-services';
 import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../../state/user/user.selectors';
+import * as fromUser from '../../../../state/user/user.reducer';
 
 describe('DraggableBehavior', () => {
     let service: DraggableBehavior;
@@ -51,6 +53,10 @@ describe('DraggableBehavior', () => {
                         {
                             selector: selectTransform,
                             value: initialState[transformFeatureKey]
+                        },
+                        {
+                            selector: selectUser,
+                            value: fromUser.initialState.user
                         }
                     ]
                 })
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts
index b1257754ef..fab7e0f9b6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts
@@ -27,6 +27,8 @@ import { transformFeatureKey } from '../../state/transform';
 import * as fromTransform from '../../state/transform/transform.reducer';
 import { controllerServicesFeatureKey } from '../../state/controller-services';
 import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../../state/user/user.selectors';
+import * as fromUser from '../../../../state/user/user.reducer';
 
 describe('QuickSelectBehavior', () => {
     let service: QuickSelectBehavior;
@@ -46,6 +48,10 @@ describe('QuickSelectBehavior', () => {
                         {
                             selector: selectFlowState,
                             value: initialState[flowFeatureKey]
+                        },
+                        {
+                            selector: selectUser,
+                            value: fromUser.initialState.user
                         }
                     ]
                 })
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts
index b1fb156735..93cf794068 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts
@@ -28,6 +28,8 @@ import { selectFlowState } from '../state/flow/flow.selectors';
 import { selectTransform } from '../state/transform/transform.selectors';
 import { controllerServicesFeatureKey } from '../state/controller-services';
 import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../state/user/user.selectors';
+import * as fromUser from '../../../state/user/user.reducer';
 
 describe('BirdseyeView', () => {
     let service: BirdseyeView;
@@ -51,6 +53,10 @@ describe('BirdseyeView', () => {
                         {
                             selector: selectTransform,
                             value: initialState[transformFeatureKey]
+                        },
+                        {
+                            selector: selectUser,
+                            value: fromUser.initialState.user
                         }
                     ]
                 })
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
index a184c5ddf2..5aa09cf287 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
@@ -26,12 +26,15 @@ import {
     selectCurrentProcessGroupId,
     selectParentProcessGroupId
 } from '../state/flow/flow.selectors';
-import { initialState } from '../state/flow/flow.reducer';
+import { initialState as initialFlowState } from '../state/flow/flow.reducer';
 import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
 import { BulletinsTip } from '../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
 import { Position } from '../state/shared';
 import { ComponentType, Permissions } from '../../../state/shared';
 import { NiFiCommon } from '../../../service/nifi-common.service';
+import { User } from '../../../state/user';
+import { initialState as initialUserState } from '../../../state/user/user.reducer';
+import { selectUser } from '../../../state/user/user.selectors';
 
 @Injectable({
     providedIn: 'root'
@@ -42,9 +45,10 @@ export class CanvasUtils {
     private destroyRef = inject(DestroyRef);
 
     private trimLengthCaches: Map<string, Map<string, Map<number, number>>> = new Map();
-    private currentProcessGroupId: string = initialState.id;
-    private parentProcessGroupId: string | null = initialState.flow.processGroupFlow.parentGroupId;
-    private canvasPermissions: Permissions = initialState.flow.permissions;
+    private currentProcessGroupId: string = initialFlowState.id;
+    private parentProcessGroupId: string | null = initialFlowState.flow.processGroupFlow.parentGroupId;
+    private canvasPermissions: Permissions = initialFlowState.flow.permissions;
+    private currentUser: User = initialUserState.user;
     private connections: any[] = [];
 
     private readonly humanizeDuration: Humanizer;
@@ -83,6 +87,13 @@ export class CanvasUtils {
             .subscribe((connections) => {
                 this.connections = connections;
             });
+
+        this.store
+            .select(selectUser)
+            .pipe(takeUntilDestroyed(this.destroyRef))
+            .subscribe((user) => {
+                this.currentUser = user;
+            });
     }
 
     public hasDownstream(selection: any): boolean {
@@ -124,6 +135,13 @@ export class CanvasUtils {
         d3.select('path.connector').remove();
     }
 
+    /**
+     * Determines whether the current user can access provenance.
+     */
+    public canAccessProvenance(): boolean {
+        return this.currentUser.provenancePermissions.canRead;
+    }
+
     /**
      * Determines whether the specified selection is empty.
      *
@@ -435,6 +453,40 @@ export class CanvasUtils {
         return selection.size() === 1 && selection.classed('funnel');
     }
 
+    /**
+     * Determines whether the current user can access provenance for the specified component.
+     *
+     * @argument {selection} selection      The selection
+     */
+    public canAccessComponentProvenance(selection: any): boolean {
+        // ensure the correct number of components are selected
+        if (selection.size() !== 1) {
+            return false;
+        }
+
+        return (
+            !this.isLabel(selection) &&
+            !this.isConnection(selection) &&
+            !this.isProcessGroup(selection) &&
+            !this.isRemoteProcessGroup(selection) &&
+            this.canAccessProvenance()
+        );
+    }
+
+    /**
+     * Determines whether the current selection should provide ability to replay latest provenance event.
+     *
+     * @param {selection} selection
+     */
+    public canReplayComponentProvenance(selection: any): boolean {
+        // ensure the correct number of components are selected
+        if (selection.size() !== 1) {
+            return false;
+        }
+
+        return this.isProcessor(selection) && this.canAccessProvenance();
+    }
+
     /**
      * Gets the currently selected components and connections.
      *
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts
index 21e166cc3a..77aa95ca71 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts
@@ -28,6 +28,8 @@ import { selectFlowState } from '../state/flow/flow.selectors';
 import { selectTransform } from '../state/transform/transform.selectors';
 import { controllerServicesFeatureKey } from '../state/controller-services';
 import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../state/user/user.selectors';
+import * as fromUser from '../../../state/user/user.reducer';
 
 describe('CanvasView', () => {
     let service: CanvasView;
@@ -51,6 +53,10 @@ describe('CanvasView', () => {
                         {
                             selector: selectTransform,
                             value: initialState[transformFeatureKey]
+                        },
+                        {
+                            selector: selectUser,
+                            value: fromUser.initialState.user
                         }
                     ]
                 })
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts
index 53df2eb224..6d56999806 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts
@@ -26,6 +26,7 @@ import {
     CreateProcessGroupRequest,
     CreateProcessorRequest,
     DeleteComponentRequest,
+    ReplayLastProvenanceEventRequest,
     Snippet,
     UpdateComponentRequest,
     UploadProcessGroupRequest
@@ -95,10 +96,6 @@ export class FlowService {
         });
     }
 
-    getCurrentUser(): Observable<any> {
-        return this.httpClient.get(`${FlowService.API}/flow/controller/bulletins`);
-    }
-
     getProcessor(id: string): Observable<any> {
         return this.httpClient.get(`${FlowService.API}/processors/${id}`);
     }
@@ -245,4 +242,8 @@ export class FlowService {
     deleteSnippet(snippetId: string): Observable<any> {
         return this.httpClient.delete(`${FlowService.API}/snippets/${snippetId}`);
     }
+
+    replayLastProvenanceEvent(request: ReplayLastProvenanceEventRequest): Observable<any> {
+        return this.httpClient.post(`${FlowService.API}/provenance-events/latest/replays`, request);
+    }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
index 5ecd9dd8c0..222784270e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
@@ -28,6 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
 import { selectTransform } from '../../state/transform/transform.selectors';
 import { controllerServicesFeatureKey } from '../../state/controller-services';
 import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../../state/user/user.selectors';
+import * as fromUser from '../../../../state/user/user.reducer';
 
 describe('ConnectionManager', () => {
     let service: ConnectionManager;
@@ -51,6 +53,10 @@ describe('ConnectionManager', () => {
                         {
                             selector: selectTransform,
                             value: initialState[transformFeatureKey]
+                        },
+                        {
+                            selector: selectUser,
+                            value: fromUser.initialState.user
                         }
                     ]
                 })
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts
index 155847e2d2..eff69edad4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts
@@ -28,6 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
 import { selectTransform } from '../../state/transform/transform.selectors';
 import { controllerServicesFeatureKey } from '../../state/controller-services';
 import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../../state/user/user.selectors';
+import * as fromUser from '../../../../state/user/user.reducer';
 
 describe('FunnelManager', () => {
     let service: FunnelManager;
@@ -51,6 +53,10 @@ describe('FunnelManager', () => {
                         {
                             selector: selectTransform,
                             value: initialState[transformFeatureKey]
+                        },
+                        {
+                            selector: selectUser,
+                            value: fromUser.initialState.user
                         }
                     ]
                 })
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts
index 422f1a41f9..8bb03c75ef 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts
@@ -28,6 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
 import { selectTransform } from '../../state/transform/transform.selectors';
 import { controllerServicesFeatureKey } from '../../state/controller-services';
 import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../../state/user/user.selectors';
+import * as fromUser from '../../../../state/user/user.reducer';
 
 describe('RemoteProcessGroupManager', () => {
     let service: RemoteProcessGroupManager;
@@ -51,6 +53,10 @@ describe('RemoteProcessGroupManager', () => {
                         {
                             selector: selectTransform,
                             value: initialState[transformFeatureKey]
+                        },
+                        {
+                            selector: selectUser,
+                            value: fromUser.initialState.user
                         }
                     ]
                 })
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
index 49fbb1e428..52ba23be40 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
@@ -53,7 +53,8 @@ import {
     UpdatePositionsRequest,
     UploadProcessGroupRequest,
     EditCurrentProcessGroupRequest,
-    NavigateToControllerServicesRequest
+    NavigateToControllerServicesRequest,
+    ReplayLastProvenanceEventRequest
 } from './index';
 
 /*
@@ -375,3 +376,13 @@ export const renderConnectionsForComponent = createAction(
     '[Canvas] Render Connections For Component',
     props<{ id: string; updatePath: boolean; updateLabel: boolean }>()
 );
+
+export const navigateToProvenanceForComponent = createAction(
+    '[Canvas] Navigate To Provenance For Component',
+    props<{ id: string }>()
+);
+
+export const replayLastProvenanceEvent = createAction(
+    '[Canvas] Replay Last Provenance Event',
+    props<{ request: ReplayLastProvenanceEventRequest }>()
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
index 623bf47a54..c7f255f941 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
@@ -1770,6 +1770,94 @@ export class FlowEffects {
         { dispatch: false }
     );
 
+    navigateToProvenanceForComponent$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(FlowActions.navigateToProvenanceForComponent),
+                map((action) => action.id),
+                tap((componentId) => {
+                    this.router.navigate(['/provenance'], { queryParams: { componentId } });
+                })
+            ),
+        { dispatch: false }
+    );
+
+    replayLastProvenanceEvent = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(FlowActions.replayLastProvenanceEvent),
+                map((action) => action.request),
+                tap((request) => {
+                    this.flowService.replayLastProvenanceEvent(request).subscribe({
+                        next: (response) => {
+                            if (response.aggregateSnapshot.failureExplanation) {
+                                this.store.dispatch(
+                                    FlowActions.showOkDialog({
+                                        title: 'Replay Event Failure',
+                                        message: response.aggregateSnapshot.failureExplanation
+                                    })
+                                );
+                            } else if (response.aggregateSnapshot.eventAvailable !== true) {
+                                this.store.dispatch(
+                                    FlowActions.showOkDialog({
+                                        title: 'No Event Available',
+                                        message: 'There was no recent event available to be replayed.'
+                                    })
+                                );
+                            } else if (response.nodeSnapshots) {
+                                let replayedCount: number = 0;
+                                let unavailableCount: number = 0;
+
+                                response.nodeSnapshots.forEach((nodeResponse: any) => {
+                                    if (nodeResponse.snapshot.eventAvailable) {
+                                        replayedCount++;
+                                    } else {
+                                        unavailableCount++;
+                                    }
+                                });
+
+                                let message: string;
+                                if (unavailableCount === 0) {
+                                    message = 'All nodes successfully replayed the latest event.';
+                                } else {
+                                    message = `${replayedCount} nodes successfully replayed the latest event but ${unavailableCount} had no recent event available to be replayed.`;
+                                }
+
+                                this.store.dispatch(
+                                    FlowActions.showOkDialog({
+                                        title: 'Events Replayed',
+                                        message
+                                    })
+                                );
+                            } else {
+                                this.store.dispatch(
+                                    FlowActions.showOkDialog({
+                                        title: 'Events Replayed',
+                                        message: 'Successfully replayed the latest event.'
+                                    })
+                                );
+                            }
+
+                            this.store.dispatch(
+                                FlowActions.loadConnectionsForComponent({
+                                    id: request.componentId
+                                })
+                            );
+                        },
+                        error: (error) => {
+                            this.store.dispatch(
+                                FlowActions.showOkDialog({
+                                    title: 'Failed to Replay Event',
+                                    message: error.error
+                                })
+                            );
+                        }
+                    });
+                })
+            ),
+        { dispatch: false }
+    );
+
     showOkDialog$ = createEffect(
         () =>
             this.actions$.pipe(
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
index 08c25e4dad..47c8f56eef 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
@@ -304,6 +304,11 @@ export interface NavigateToComponentRequest {
     processGroupId?: string;
 }
 
+export interface ReplayLastProvenanceEventRequest {
+    componentId: string;
+    nodes: string;
+}
+
 /*
     Snippets
  */
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/context-menu/context-menu.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/context-menu/context-menu.component.ts
index 5ac2010bd9..d02892d948 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/context-menu/context-menu.component.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/context-menu/context-menu.component.ts
@@ -30,7 +30,9 @@ import {
     navigateToControllerServicesForProcessGroup,
     navigateToEditComponent,
     navigateToEditCurrentProcessGroup,
-    reloadFlow
+    navigateToProvenanceForComponent,
+    reloadFlow,
+    replayLastProvenanceEvent
 } from '../../../state/flow/flow.actions';
 import { CanvasUtils } from '../../../service/canvas-utils.service';
 import { DeleteComponentRequest, MoveComponentRequest } from '../../../state/flow';
@@ -154,24 +156,38 @@ export class ContextMenu implements OnInit {
         menuItems: [
             {
                 condition: function (canvasUtils: CanvasUtils, selection: any) {
-                    // TODO - canReplayProvenance
-                    return false;
+                    return canvasUtils.canReplayComponentProvenance(selection);
                 },
                 clazz: 'fa',
                 text: 'All nodes',
-                action: function (store: Store<CanvasState>) {
-                    // TODO - replayLastAllNodes
+                action: function (store: Store<CanvasState>, selection: any) {
+                    const selectionData = selection.datum();
+                    store.dispatch(
+                        replayLastProvenanceEvent({
+                            request: {
+                                componentId: selectionData.id,
+                                nodes: 'ALL'
+                            }
+                        })
+                    );
                 }
             },
             {
                 condition: function (canvasUtils: CanvasUtils, selection: any) {
-                    // TODO - canReplayProvenance
-                    return false;
+                    return canvasUtils.canReplayComponentProvenance(selection);
                 },
                 clazz: 'fa',
                 text: 'Primary node',
-                action: function (store: Store<CanvasState>) {
-                    // TODO - replayLastPrimaryNode
+                action: function (store: Store<CanvasState>, selection: any) {
+                    const selectionData = selection.datum();
+                    store.dispatch(
+                        replayLastProvenanceEvent({
+                            request: {
+                                componentId: selectionData.id,
+                                nodes: 'PRIMARY'
+                            }
+                        })
+                    );
                 }
             }
         ]
@@ -525,14 +541,18 @@ export class ContextMenu implements OnInit {
             },
             {
                 condition: function (canvasUtils: CanvasUtils, selection: any) {
-                    // TODO - canAccessProvenance
-                    return false;
+                    return canvasUtils.canAccessComponentProvenance(selection);
                 },
                 clazz: 'icon icon-provenance',
                 // imgStyle: 'context-menu-provenance',
                 text: 'View data provenance',
-                action: function (store: Store<CanvasState>) {
-                    // TODO - openProvenance
+                action: function (store: Store<CanvasState>, selection: any) {
+                    const selectionData = selection.datum();
+                    store.dispatch(
+                        navigateToProvenanceForComponent({
+                            id: selectionData.id
+                        })
+                    );
                 }
             },
             {
@@ -958,6 +978,7 @@ export class ContextMenu implements OnInit {
     ) {
         this.allMenus = new Map<string, ContextMenuDefinition>();
         this.allMenus.set(this.ROOT_MENU.id, this.ROOT_MENU);
+        this.allMenus.set(this.PROVENANCE_REPLAY.id, this.PROVENANCE_REPLAY);
         this.allMenus.set(this.VERSION_MENU.id, this.VERSION_MENU);
         this.allMenus.set(this.UPSTREAM_DOWNSTREAM.id, this.UPSTREAM_DOWNSTREAM);
         this.allMenus.set(this.ALIGN.id, this.ALIGN);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.html
index f0c9bd2d0b..f3d27c6ee0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.html
@@ -81,7 +81,11 @@
                         Bulletin Board
                     </button>
                     <mat-divider></mat-divider>
-                    <button mat-menu-item class="global-menu-item">
+                    <button
+                        mat-menu-item
+                        class="global-menu-item"
+                        [routerLink]="['/provenance']"
+                        [disabled]="!user.provenancePermissions.canRead">
                         <i class="icon fa-fw icon-provenance mr-2"></i>
                         Data Provenance
                     </button>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.spec.ts
index b836f11894..fb47ed315d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.spec.ts
@@ -36,6 +36,8 @@ import { ClusterSummary, ControllerStatus } from '../../../state/flow';
 import { Search } from './search/search.component';
 import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { selectUser } from '../../../../../state/user/user.selectors';
+import * as fromUser from '../../../../../state/user/user.reducer';
 
 describe('HeaderComponent', () => {
     let component: HeaderComponent;
@@ -99,6 +101,10 @@ describe('HeaderComponent', () => {
                         {
                             selector: selectControllerBulletins,
                             value: []
+                        },
+                        {
+                            selector: selectUser,
+                            value: fromUser.initialState.user
                         }
                     ]
                 })
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/process-group/create-process-group/create-process-group.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/process-group/create-process-group/create-process-group.component.ts
index 608f3aaeec..74b8865aed 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/process-group/create-process-group/create-process-group.component.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/process-group/create-process-group/create-process-group.component.ts
@@ -114,6 +114,7 @@ export class CreateProcessGroup {
             this.createProcessGroupForm
                 .get('newProcessGroupName')
                 ?.setValue(this.nifiCommon.substringBeforeLast(file.name, '.'));
+            this.createProcessGroupForm.get('newProcessGroupName')?.markAsDirty();
             this.createProcessGroupForm.get('newProcessGroupParameterContext')?.setValue(null);
             this.flowNameAttached = file.name;
             this.flowDefinition = file;
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/parameter-contexts/service/parameter-contexts.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/parameter-contexts/service/parameter-contexts.service.ts
index deb0e86b0d..01dd297cdc 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/parameter-contexts/service/parameter-contexts.service.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/parameter-contexts/service/parameter-contexts.service.ts
@@ -92,8 +92,4 @@ export class ParameterContextService {
             params: revision
         });
     }
-
-    // updateControllerConfig(controllerEntity: ControllerEntity): Observable<any> {
-    //     return this.httpClient.put(`${ControllerServiceService.API}/controller/config`, controllerEntity);
-    // }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance-routing.module.ts
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance-routing.module.ts
index 3789433cd3..3dc770a7a7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance-routing.module.ts
@@ -15,21 +15,31 @@
  * limitations under the License.
  */
 
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { Provenance } from './provenance.component';
 
-export interface NiFiState {
-    router: RouterReducerState;
-    [userFeatureKey]: UserState;
-    [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+const routes: Routes = [
+    {
+        path: '',
+        component: Provenance,
+        children: [
+            // {
+            //     path: 'lineage'
+            // },
+            {
+                path: '',
+                loadChildren: () =>
+                    import('../ui/provenance-event-listing/provenance-event-listing.module').then(
+                        (m) => m.ProvenanceEventListingModule
+                    )
+            }
+        ]
+    }
+];
 
-export const rootReducers: ActionReducerMap<NiFiState> = {
-    router: routerReducer,
-    [userFeatureKey]: userReducer,
-    [extensionTypesFeatureKey]: extensionTypesReducer
-};
+@NgModule({
+    imports: [RouterModule.forChild(routes)],
+    exports: [RouterModule]
+})
+export class ProvenanceRoutingModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.html
new file mode 100644
index 0000000000..f14af50f77
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.html
@@ -0,0 +1,28 @@
+<!--
+  ~ 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="p-4 flex flex-col h-screen justify-between gap-y-5">
+    <div class="flex justify-between">
+        <h3 class="text-xl bold provenance-header">Provenance</h3>
+        <button class="nifi-button" [routerLink]="['/']">
+            <i class="fa fa-times"></i>
+        </button>
+    </div>
+    <div class="flex-1">
+        <router-outlet></router-outlet>
+    </div>
+</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.scss
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.scss
index 3789433cd3..288912eaa7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.scss
@@ -15,21 +15,6 @@
  * limitations under the License.
  */
 
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
-
-export interface NiFiState {
-    router: RouterReducerState;
-    [userFeatureKey]: UserState;
-    [extensionTypesFeatureKey]: ExtensionTypesState;
+.provenance-header {
+    color: #728e9b;
 }
-
-export const rootReducers: ActionReducerMap<NiFiState> = {
-    router: routerReducer,
-    [userFeatureKey]: userReducer,
-    [extensionTypesFeatureKey]: extensionTypesReducer
-};
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.spec.ts
new file mode 100644
index 0000000000..200affe6d8
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.spec.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { Provenance } from './provenance.component';
+import { provideMockStore } from '@ngrx/store/testing';
+import { RouterModule } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+import { initialState } from '../state/provenance-event-listing/provenance-event-listing.reducer';
+
+describe('Provenance', () => {
+    let component: Provenance;
+    let fixture: ComponentFixture<Provenance>;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            declarations: [Provenance],
+            imports: [RouterModule, RouterTestingModule],
+            providers: [
+                provideMockStore({
+                    initialState
+                })
+            ]
+        });
+        fixture = TestBed.createComponent(Provenance);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.ts
new file mode 100644
index 0000000000..3ae2282b2c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.ts
@@ -0,0 +1,42 @@
+/*
+ * 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, OnDestroy, OnInit } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../state';
+import { startUserPolling, stopUserPolling } from '../../../state/user/user.actions';
+import { loadProvenanceOptions } from '../state/provenance-event-listing/provenance-event-listing.actions';
+import { loadAbout } from '../../../state/about/about.actions';
+
+@Component({
+    selector: 'provenance',
+    templateUrl: './provenance.component.html',
+    styleUrls: ['./provenance.component.scss']
+})
+export class Provenance implements OnInit, OnDestroy {
+    constructor(private store: Store<NiFiState>) {}
+
+    ngOnInit(): void {
+        this.store.dispatch(startUserPolling());
+        this.store.dispatch(loadProvenanceOptions());
+        this.store.dispatch(loadAbout());
+    }
+
+    ngOnDestroy(): void {
+        this.store.dispatch(stopUserPolling());
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.module.ts
new file mode 100644
index 0000000000..be2c442b48
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.module.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { StoreModule } from '@ngrx/store';
+import { EffectsModule } from '@ngrx/effects';
+import { Provenance } from './provenance.component';
+import { ProvenanceRoutingModule } from './provenance-routing.module';
+import { provenanceFeatureKey, reducers } from '../state';
+import { ProvenanceEventListingEffects } from '../state/provenance-event-listing/provenance-event-listing.effects';
+import { MatDialogModule } from '@angular/material/dialog';
+
+@NgModule({
+    declarations: [Provenance],
+    exports: [Provenance],
+    imports: [
+        CommonModule,
+        MatDialogModule,
+        ProvenanceRoutingModule,
+        StoreModule.forFeature(provenanceFeatureKey, reducers),
+        EffectsModule.forFeature(ProvenanceEventListingEffects)
+    ]
+})
+export class ProvenanceModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/service/provenance.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/service/provenance.service.ts
new file mode 100644
index 0000000000..7d831f9444
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/service/provenance.service.ts
@@ -0,0 +1,130 @@
+/*
+ * 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 { Observable, throwError } from 'rxjs';
+import { HttpClient } from '@angular/common/http';
+import { NiFiCommon } from '../../../service/nifi-common.service';
+import { ProvenanceRequest } from '../state/provenance-event-listing';
+
+@Injectable({ providedIn: 'root' })
+export class ProvenanceService {
+    private static readonly API: string = '../nifi-api';
+
+    constructor(
+        private httpClient: HttpClient,
+        private nifiCommon: NiFiCommon
+    ) {}
+
+    /**
+     * The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
+     * does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
+     *
+     * https://stackoverflow.com/a/59586462
+     *
+     * @param url
+     * @private
+     */
+    private stripProtocol(url: string): string {
+        return this.nifiCommon.substringAfterFirst(url, ':');
+    }
+
+    getSearchOptions(): Observable<any> {
+        return this.httpClient.get(`${ProvenanceService.API}/provenance/search-options`);
+    }
+
+    submitProvenanceQuery(request: ProvenanceRequest): Observable<any> {
+        return this.httpClient.post(`${ProvenanceService.API}/provenance`, { provenance: { request } });
+    }
+
+    getProvenanceQuery(id: string, clusterNodeId?: string): Observable<any> {
+        // TODO - cluster node id
+        return this.httpClient.get(`${ProvenanceService.API}/provenance/${encodeURIComponent(id)}`);
+    }
+
+    deleteProvenanceQuery(id: string, clusterNodeId?: string): Observable<any> {
+        // TODO - cluster node id
+        return this.httpClient.delete(`${ProvenanceService.API}/provenance/${encodeURIComponent(id)}`);
+    }
+
+    getProvenanceEvent(id: string): Observable<any> {
+        // TODO - cluster node id
+        return this.httpClient.get(`${ProvenanceService.API}/provenance-events/${encodeURIComponent(id)}`);
+    }
+
+    downloadContent(id: string, direction: string): void {
+        let dataUri: string = `${ProvenanceService.API}/provenance-events/${encodeURIComponent(
+            id
+        )}/content/${encodeURIComponent(direction)}`;
+
+        const queryParameters: any = {};
+
+        // TODO - cluster node id in query parameters
+
+        if (Object.keys(queryParameters).length > 0) {
+            const query: string = new URLSearchParams(queryParameters).toString();
+            dataUri = `${dataUri}?${query}`;
+        }
+
+        window.open(dataUri);
+    }
+
+    viewContent(nifiUrl: string, contentViewerUrl: string, id: string, direction: string): void {
+        // build the uri to the data
+        let dataUri: string = `${nifiUrl}provenance-events/${encodeURIComponent(id)}/content/${encodeURIComponent(
+            direction
+        )}`;
+
+        const dataUriParameters: any = {};
+
+        // TODO - cluster node id in data uri parameters
+
+        // include parameters if necessary
+        if (Object.keys(dataUriParameters).length > 0) {
+            const dataUriQuery: string = new URLSearchParams(dataUriParameters).toString();
+            dataUri = `${dataUri}?${dataUriQuery}`;
+        }
+
+        // if there's already a query string don't add another ?... this assumes valid
+        // input meaning that if the url has already included a ? it also contains at
+        // least one query parameter
+        let contentViewer: string = contentViewerUrl;
+        if (contentViewer.indexOf('?') === -1) {
+            contentViewer += '?';
+        } else {
+            contentViewer += '&';
+        }
+
+        const contentViewerParameters: any = {
+            ref: dataUri
+        };
+
+        // open the content viewer
+        const contentViewerQuery: string = new URLSearchParams(contentViewerParameters).toString();
+        window.open(`${contentViewer}${contentViewerQuery}`);
+    }
+
+    replay(eventId: string): Observable<any> {
+        const payload: any = {
+            eventId
+        };
+
+        // TODO - add cluster node id in payload
+
+        return this.httpClient.post(`${ProvenanceService.API}/provenance-events/replays`, payload);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/index.ts
similarity index 50%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/index.ts
index 3789433cd3..0457b6a4b6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/index.ts
@@ -15,21 +15,24 @@
  * limitations under the License.
  */
 
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+/*
+  Parameter Contexts
+ */
+
+import { Action, combineReducers, createFeatureSelector } from '@ngrx/store';
+import { provenanceEventListingFeatureKey, ProvenanceEventListingState } from './provenance-event-listing';
+import { provenanceEventListingReducer } from './provenance-event-listing/provenance-event-listing.reducer';
+
+export const provenanceFeatureKey = 'provenance';
+
+export interface ProvenanceState {
+    [provenanceEventListingFeatureKey]: ProvenanceEventListingState;
+}
 
-export interface NiFiState {
-    router: RouterReducerState;
-    [userFeatureKey]: UserState;
-    [extensionTypesFeatureKey]: ExtensionTypesState;
+export function reducers(state: ProvenanceState | undefined, action: Action) {
+    return combineReducers({
+        [provenanceEventListingFeatureKey]: provenanceEventListingReducer
+    })(state, action);
 }
 
-export const rootReducers: ActionReducerMap<NiFiState> = {
-    router: routerReducer,
-    [userFeatureKey]: userReducer,
-    [extensionTypesFeatureKey]: extensionTypesReducer
-};
+export const selectProvenanceState = createFeatureSelector<ProvenanceState>(provenanceFeatureKey);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/index.ts
new file mode 100644
index 0000000000..7ffd66a323
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/index.ts
@@ -0,0 +1,100 @@
+/*
+ * 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 { ProvenanceEventSummary } from '../../../../state/shared';
+
+export const provenanceEventListingFeatureKey = 'provenanceEventListing';
+
+export interface ProvenanceOptionsResponse {
+    provenanceOptions: ProvenanceOptions;
+}
+
+export interface ProvenanceQueryParams {
+    flowFileUuid?: string;
+    componentId?: string;
+}
+
+export interface ProvenanceQueryResponse {
+    provenance: Provenance;
+}
+
+export interface SearchableField {
+    field: string;
+    id: string;
+    label: string;
+    type: string;
+}
+
+export interface ProvenanceOptions {
+    searchableFields: SearchableField[];
+}
+
+export interface ProvenanceSearchDialogRequest {
+    timeOffset: number;
+    options: ProvenanceOptions;
+    currentRequest: ProvenanceRequest;
+}
+
+export interface ProvenanceSearchValue {
+    value: string;
+    inverse: boolean;
+}
+
+export interface ProvenanceRequest {
+    searchTerms?: {
+        [key: string]: ProvenanceSearchValue;
+    };
+    clusterNodeId?: string;
+    startDate?: string;
+    endDate?: string;
+    minimumFileSize?: string;
+    maximumFileSize?: string;
+    maxResults: number;
+    summarize: boolean;
+    incrementalResults: boolean;
+}
+
+export interface ProvenanceResults {
+    provenanceEvents: ProvenanceEventSummary[];
+    total: string;
+    totalCount: number;
+    generated: string;
+    oldestEvent: string;
+    timeOffset: number;
+    errors: string[];
+}
+
+export interface Provenance {
+    id: string;
+    uri: string;
+    submissionTime: string;
+    expiration: string;
+    percentCompleted: number;
+    finished: boolean;
+    request: ProvenanceRequest;
+    results: ProvenanceResults;
+}
+
+export interface ProvenanceEventListingState {
+    options: ProvenanceOptions | null;
+    request: ProvenanceRequest | null;
+    provenance: Provenance | null;
+    saving: boolean;
+    loadedTimestamp: string;
+    error: string | null;
+    status: 'pending' | 'loading' | 'error' | 'success';
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.actions.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.actions.ts
new file mode 100644
index 0000000000..00ab086552
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.actions.ts
@@ -0,0 +1,78 @@
+/*
+ * 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 { ProvenanceOptionsResponse, ProvenanceQueryResponse, ProvenanceRequest } from './index';
+
+export const loadProvenanceOptions = createAction('[Provenance Event Listing] Load Provenance Options');
+
+export const loadProvenanceOptionsSuccess = createAction(
+    '[Provenance Event Listing] Load Provenance Options Success',
+    props<{ response: ProvenanceOptionsResponse }>()
+);
+
+export const submitProvenanceQuery = createAction(
+    '[Provenance Event Listing] Submit Provenance Query',
+    props<{ request: ProvenanceRequest }>()
+);
+
+export const resubmitProvenanceQuery = createAction(
+    '[Provenance Event Listing] Resubmit Provenance Query',
+    props<{ request: ProvenanceRequest }>()
+);
+
+export const submitProvenanceQuerySuccess = createAction(
+    '[Provenance Event Listing] Submit Provenance Query Success',
+    props<{ response: ProvenanceQueryResponse }>()
+);
+
+export const startPollingProvenanceQuery = createAction('[Provenance Event Listing] Start Polling Provenance Query');
+
+export const pollProvenanceQuery = createAction('[Provenance Event Listing] Poll Provenance Query');
+
+export const pollProvenanceQuerySuccess = createAction(
+    '[Provenance Event Listing] Poll Provenance Query Success',
+    props<{ response: ProvenanceQueryResponse }>()
+);
+
+export const stopPollingProvenanceQuery = createAction('[Provenance Event Listing] Stop Polling Provenance Query');
+
+export const deleteProvenanceQuery = createAction('[Provenance Event Listing] Delete Provenance Query');
+
+export const provenanceApiError = createAction(
+    '[Provenance Event Listing] Load Parameter Context Listing Error',
+    props<{ error: string }>()
+);
+
+export const openProvenanceEventDialog = createAction(
+    '[Provenance Event Listing] Open Provenance Event Dialog',
+    props<{ id: string }>()
+);
+
+export const openSearchDialog = createAction('[Provenance Event Listing] Open Search Dialog');
+
+export const saveProvenanceRequest = createAction(
+    '[Provenance Event Listing] Save Provenance Request',
+    props<{ request: ProvenanceRequest }>()
+);
+
+export const clearProvenanceRequest = createAction('[Provenance Event Listing] Clear Provenance Request');
+
+export const showOkDialog = createAction(
+    '[Provenance Event Listing] Show Ok Dialog',
+    props<{ title: string; message: string }>()
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.effects.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.effects.ts
new file mode 100644
index 0000000000..4c3725b226
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.effects.ts
@@ -0,0 +1,361 @@
+/*
+ * 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 * as ProvenanceEventListingActions from './provenance-event-listing.actions';
+import {
+    asyncScheduler,
+    catchError,
+    from,
+    interval,
+    map,
+    NEVER,
+    of,
+    switchMap,
+    take,
+    takeUntil,
+    tap,
+    withLatestFrom
+} from 'rxjs';
+import { MatDialog } from '@angular/material/dialog';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../../state';
+import { Router } from '@angular/router';
+import { OkDialog } from '../../../../ui/common/ok-dialog/ok-dialog.component';
+import { ProvenanceService } from '../../service/provenance.service';
+import {
+    selectClusterNodeId,
+    selectProvenanceId,
+    selectProvenanceOptions,
+    selectProvenanceRequest,
+    selectTimeOffset
+} from './provenance-event-listing.selectors';
+import { Provenance, ProvenanceRequest } from './index';
+import { ProvenanceSearchDialog } from '../../ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component';
+import { selectAbout } from '../../../../state/about/about.selectors';
+import { ProvenanceEventDialog } from '../../../../ui/common/provenance-event-dialog/provenance-event-dialog.component';
+import { CancelDialog } from '../../../../ui/common/cancel-dialog/cancel-dialog.component';
+
+@Injectable()
+export class ProvenanceEventListingEffects {
+    constructor(
+        private actions$: Actions,
+        private store: Store<NiFiState>,
+        private provenanceService: ProvenanceService,
+        private dialog: MatDialog,
+        private router: Router
+    ) {}
+
+    loadProvenanceOptions$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ProvenanceEventListingActions.loadProvenanceOptions),
+            switchMap((request) =>
+                from(this.provenanceService.getSearchOptions()).pipe(
+                    map((response) =>
+                        ProvenanceEventListingActions.loadProvenanceOptionsSuccess({
+                            response
+                        })
+                    ),
+                    catchError((error) =>
+                        of(
+                            ProvenanceEventListingActions.provenanceApiError({
+                                error: error.error
+                            })
+                        )
+                    )
+                )
+            )
+        )
+    );
+
+    submitProvenanceQuery$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ProvenanceEventListingActions.submitProvenanceQuery),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(this.provenanceService.submitProvenanceQuery(request)).pipe(
+                    map((response) =>
+                        ProvenanceEventListingActions.submitProvenanceQuerySuccess({
+                            response: {
+                                provenance: response.provenance
+                            }
+                        })
+                    ),
+                    catchError((error) => {
+                        this.store.dispatch(
+                            ProvenanceEventListingActions.showOkDialog({
+                                title: 'Error',
+                                message: error.error
+                            })
+                        );
+
+                        return of(
+                            ProvenanceEventListingActions.provenanceApiError({
+                                error: error.error
+                            })
+                        );
+                    })
+                )
+            )
+        )
+    );
+
+    resubmitProvenanceQuery = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ProvenanceEventListingActions.resubmitProvenanceQuery),
+            map((action) => action.request),
+            switchMap((request) => {
+                const dialogReference = this.dialog.open(CancelDialog, {
+                    data: {
+                        title: 'Provenance',
+                        message: 'Searching provenance events...'
+                    },
+                    disableClose: true,
+                    panelClass: 'small-dialog'
+                });
+
+                dialogReference.componentInstance.cancel.pipe(take(1)).subscribe(() => {
+                    this.store.dispatch(ProvenanceEventListingActions.stopPollingProvenanceQuery());
+                });
+
+                return of(ProvenanceEventListingActions.submitProvenanceQuery({ request }));
+            })
+        )
+    );
+
+    submitProvenanceQuerySuccess$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ProvenanceEventListingActions.submitProvenanceQuerySuccess),
+            map((action) => action.response),
+            switchMap((response) => {
+                const query: Provenance = response.provenance;
+                if (query.finished) {
+                    this.dialog.closeAll();
+                    return of(ProvenanceEventListingActions.deleteProvenanceQuery());
+                } else {
+                    return of(ProvenanceEventListingActions.startPollingProvenanceQuery());
+                }
+            })
+        )
+    );
+
+    startPollingProvenanceQuery$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ProvenanceEventListingActions.startPollingProvenanceQuery),
+            switchMap(() =>
+                interval(2000, asyncScheduler).pipe(
+                    takeUntil(this.actions$.pipe(ofType(ProvenanceEventListingActions.stopPollingProvenanceQuery)))
+                )
+            ),
+            switchMap(() => of(ProvenanceEventListingActions.pollProvenanceQuery()))
+        )
+    );
+
+    pollProvenanceQuery$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ProvenanceEventListingActions.pollProvenanceQuery),
+            withLatestFrom(this.store.select(selectProvenanceId), this.store.select(selectClusterNodeId)),
+            switchMap(([action, id, clusterNodeId]) => {
+                if (id) {
+                    return from(this.provenanceService.getProvenanceQuery(id, clusterNodeId)).pipe(
+                        map((response) =>
+                            ProvenanceEventListingActions.pollProvenanceQuerySuccess({
+                                response: {
+                                    provenance: response.provenance
+                                }
+                            })
+                        ),
+                        catchError((error) =>
+                            of(
+                                ProvenanceEventListingActions.provenanceApiError({
+                                    error: error.error
+                                })
+                            )
+                        )
+                    );
+                } else {
+                    return NEVER;
+                }
+            })
+        )
+    );
+
+    pollProvenanceQuerySuccess$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ProvenanceEventListingActions.pollProvenanceQuerySuccess),
+            map((action) => action.response),
+            switchMap((response) => {
+                const query: Provenance = response.provenance;
+                if (query.finished) {
+                    this.dialog.closeAll();
+                    return of(ProvenanceEventListingActions.stopPollingProvenanceQuery());
+                } else {
+                    return NEVER;
+                }
+            })
+        )
+    );
+
+    stopPollingProvenanceQuery$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ProvenanceEventListingActions.stopPollingProvenanceQuery),
+            switchMap((response) => of(ProvenanceEventListingActions.deleteProvenanceQuery()))
+        )
+    );
+
+    deleteProvenanceQuery$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(ProvenanceEventListingActions.deleteProvenanceQuery),
+                withLatestFrom(this.store.select(selectProvenanceId), this.store.select(selectClusterNodeId)),
+                tap(([action, id, clusterNodeId]) => {
+                    if (id) {
+                        this.provenanceService.deleteProvenanceQuery(id, clusterNodeId).subscribe();
+                    }
+                })
+            ),
+        { dispatch: false }
+    );
+
+    openSearchDialog$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(ProvenanceEventListingActions.openSearchDialog),
+                withLatestFrom(
+                    this.store.select(selectTimeOffset),
+                    this.store.select(selectProvenanceOptions),
+                    this.store.select(selectProvenanceRequest),
+                    this.store.select(selectAbout)
+                ),
+                tap(([request, timeOffset, options, currentRequest, about]) => {
+                    if (about) {
+                        const dialogReference = this.dialog.open(ProvenanceSearchDialog, {
+                            data: {
+                                timeOffset,
+                                options,
+                                currentRequest
+                            },
+                            panelClass: 'large-dialog'
+                        });
+
+                        dialogReference.componentInstance.timezone = about.timezone;
+
+                        dialogReference.componentInstance.submitSearchCriteria
+                            .pipe(take(1))
+                            .subscribe((request: ProvenanceRequest) => {
+                                if (request.searchTerms) {
+                                    const queryParams: any = {};
+                                    if (request.searchTerms['ProcessorID']) {
+                                        queryParams['componentId'] = request.searchTerms['ProcessorID'].value;
+                                    }
+                                    if (request.searchTerms['FlowFileUUID']) {
+                                        queryParams['flowFileUuid'] = request.searchTerms['FlowFileUUID'].value;
+                                    }
+
+                                    // if either of the supported query params are present in the query, update the url
+                                    if (Object.keys(queryParams).length > 0) {
+                                        this.router.navigate(['/provenance'], { queryParams });
+                                    }
+                                }
+
+                                this.store.dispatch(ProvenanceEventListingActions.saveProvenanceRequest({ request }));
+                            });
+                    }
+
+                    // TODO - if about hasn't loaded we should show an error
+                })
+            ),
+        { dispatch: false }
+    );
+
+    openProvenanceEventDialog$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(ProvenanceEventListingActions.openProvenanceEventDialog),
+                withLatestFrom(this.store.select(selectAbout)),
+                tap(([request, about]) => {
+                    this.provenanceService.getProvenanceEvent(request.id).subscribe({
+                        next: (response) => {
+                            const dialogReference = this.dialog.open(ProvenanceEventDialog, {
+                                data: {
+                                    event: response.provenanceEvent
+                                },
+                                panelClass: 'large-dialog'
+                            });
+
+                            dialogReference.componentInstance.contentViewerAvailable =
+                                about?.contentViewerUrl != null ?? false;
+
+                            dialogReference.componentInstance.downloadContent
+                                .pipe(takeUntil(dialogReference.afterClosed()))
+                                .subscribe((direction: string) => {
+                                    this.provenanceService.downloadContent(request.id, direction);
+                                });
+
+                            if (about) {
+                                dialogReference.componentInstance.viewContent
+                                    .pipe(takeUntil(dialogReference.afterClosed()))
+                                    .subscribe((direction: string) => {
+                                        this.provenanceService.viewContent(
+                                            about.uri,
+                                            about.contentViewerUrl,
+                                            request.id,
+                                            direction
+                                        );
+                                    });
+                            }
+
+                            dialogReference.componentInstance.replay
+                                .pipe(takeUntil(dialogReference.afterClosed()))
+                                .subscribe(() => {
+                                    this.provenanceService.replay(request.id).subscribe(() => {
+                                        this.store.dispatch(
+                                            ProvenanceEventListingActions.showOkDialog({
+                                                title: 'Provenance',
+                                                message: 'Successfully submitted replay request.'
+                                            })
+                                        );
+                                    });
+                                });
+                        },
+                        error: (error) => {
+                            // TODO - handle error
+                        }
+                    });
+                })
+            ),
+        { dispatch: false }
+    );
+
+    showOkDialog$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(ProvenanceEventListingActions.showOkDialog),
+                tap((request) => {
+                    this.dialog.open(OkDialog, {
+                        data: {
+                            title: request.title,
+                            message: request.message
+                        },
+                        panelClass: 'medium-dialog'
+                    });
+                })
+            ),
+        { dispatch: false }
+    );
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.reducer.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.reducer.ts
new file mode 100644
index 0000000000..c32db2c47d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.reducer.ts
@@ -0,0 +1,71 @@
+/*
+ * 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 { createReducer, on } from '@ngrx/store';
+import { ProvenanceEventListingState } from './index';
+import {
+    clearProvenanceRequest,
+    loadProvenanceOptionsSuccess,
+    pollProvenanceQuerySuccess,
+    provenanceApiError,
+    saveProvenanceRequest,
+    submitProvenanceQuery,
+    submitProvenanceQuerySuccess
+} from './provenance-event-listing.actions';
+
+export const initialState: ProvenanceEventListingState = {
+    options: null,
+    request: null,
+    provenance: null,
+    saving: false,
+    loadedTimestamp: '',
+    error: null,
+    status: 'pending'
+};
+
+export const provenanceEventListingReducer = createReducer(
+    initialState,
+    on(loadProvenanceOptionsSuccess, (state, { response }) => ({
+        ...state,
+        options: response.provenanceOptions
+    })),
+    on(submitProvenanceQuery, (state) => ({
+        ...state,
+        status: 'loading' as const
+    })),
+    on(submitProvenanceQuerySuccess, pollProvenanceQuerySuccess, (state, { response }) => ({
+        ...state,
+        provenance: response.provenance,
+        loadedTimestamp: response.provenance.results.generated,
+        error: null,
+        status: 'success' as const
+    })),
+    on(saveProvenanceRequest, (state, { request }) => ({
+        ...state,
+        request
+    })),
+    on(clearProvenanceRequest, (state) => ({
+        ...state,
+        request: null
+    })),
+    on(provenanceApiError, (state, { error }) => ({
+        ...state,
+        saving: false,
+        error,
+        status: 'error' as const
+    }))
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.selectors.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.selectors.ts
new file mode 100644
index 0000000000..8225e4ce9f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.selectors.ts
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createSelector } from '@ngrx/store';
+import { ProvenanceState, selectProvenanceState } from '../index';
+import {
+    Provenance,
+    provenanceEventListingFeatureKey,
+    ProvenanceEventListingState,
+    ProvenanceQueryParams,
+    ProvenanceRequest,
+    ProvenanceResults
+} from './index';
+import { selectCurrentRoute } from '../../../../state/router/router.selectors';
+
+export const selectProvenanceEventListingState = createSelector(
+    selectProvenanceState,
+    (state: ProvenanceState) => state[provenanceEventListingFeatureKey]
+);
+
+export const selectSearchableFieldsFromRoute = createSelector(selectCurrentRoute, (route) => {
+    if (route) {
+        const queryParameters: ProvenanceQueryParams = {};
+        if (route.queryParams.componentId) {
+            queryParameters.componentId = route.queryParams.componentId;
+        }
+        if (route.queryParams.flowFileUuid) {
+            queryParameters.flowFileUuid = route.queryParams.flowFileUuid;
+        }
+        return queryParameters;
+    }
+    return null;
+});
+
+export const selectProvenanceRequest = createSelector(
+    selectProvenanceEventListingState,
+    (state: ProvenanceEventListingState) => state.request
+);
+
+export const selectStatus = createSelector(
+    selectProvenanceEventListingState,
+    (state: ProvenanceEventListingState) => state.status
+);
+
+export const selectProvenanceOptions = createSelector(
+    selectProvenanceEventListingState,
+    (state: ProvenanceEventListingState) => state.options
+);
+
+export const selectLoadedTimestamp = createSelector(
+    selectProvenanceEventListingState,
+    (state: ProvenanceEventListingState) => state.loadedTimestamp
+);
+
+export const selectProvenance = createSelector(
+    selectProvenanceEventListingState,
+    (state: ProvenanceEventListingState) => state.provenance
+);
+
+export const selectProvenanceId = createSelector(selectProvenance, (state: Provenance | null) => state?.id);
+
+export const selectClusterNodeId = createSelector(
+    selectProvenanceRequest,
+    (state: ProvenanceRequest | null) => state?.clusterNodeId
+);
+
+export const selectProvenanceResults = createSelector(selectProvenance, (state: Provenance | null) => state?.results);
+
+export const selectTimeOffset = createSelector(
+    selectProvenanceResults,
+    (state: ProvenanceResults | undefined) => state?.timeOffset
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing-routing.module.ts
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing-routing.module.ts
index 3789433cd3..88da6f535d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing-routing.module.ts
@@ -15,21 +15,19 @@
  * limitations under the License.
  */
 
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { ProvenanceEventListing } from './provenance-event-listing.component';
 
-export interface NiFiState {
-    router: RouterReducerState;
-    [userFeatureKey]: UserState;
-    [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+const routes: Routes = [
+    {
+        path: '',
+        component: ProvenanceEventListing
+    }
+];
 
-export const rootReducers: ActionReducerMap<NiFiState> = {
-    router: routerReducer,
-    [userFeatureKey]: userReducer,
-    [extensionTypesFeatureKey]: extensionTypesReducer
-};
+@NgModule({
+    imports: [RouterModule.forChild(routes)],
+    exports: [RouterModule]
+})
+export class ProvenanceEventListingRoutingModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.html
new file mode 100644
index 0000000000..748cbe30c9
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.html
@@ -0,0 +1,43 @@
+<!--
+  ~ 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="flex flex-col h-full gap-y-2 text-sm" *ngIf="status$ | async; let status">
+    <div class="flex-1">
+        <ng-container *ngIf="provenance$ | async as provenance; else initialLoading">
+            <provenance-event-table
+                [events]="provenance.results.provenanceEvents"
+                [oldestEventAvailable]="provenance.results.oldestEvent"
+                [resultsMessage]="getResultsMessage(provenance)"
+                [hasRequest]="hasRequest(provenance.request)"
+                (openSearchCriteria)="openSearchCriteria()"
+                (openEventDialog)="openEventDialog($event)"
+                (clearRequest)="clearRequest()"></provenance-event-table>
+        </ng-container>
+        <ng-template #initialLoading>
+            <ngx-skeleton-loader count="3"></ngx-skeleton-loader>
+        </ng-template>
+    </div>
+    <div class="flex justify-between">
+        <div class="refresh-container flex items-center gap-x-2">
+            <button class="nifi-button" (click)="refreshParameterContextListing()">
+                <i class="fa fa-refresh" [class.fa-spin]="(status$ | async) === 'loading'"></i>
+            </button>
+            <div>Last updated:</div>
+            <div class="refresh-timestamp">{{ loadedTimestamp$ | async }}</div>
+        </div>
+    </div>
+</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.scss
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.scss
index 3789433cd3..2944f98194 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.scss
@@ -14,22 +14,3 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
-
-export interface NiFiState {
-    router: RouterReducerState;
-    [userFeatureKey]: UserState;
-    [extensionTypesFeatureKey]: ExtensionTypesState;
-}
-
-export const rootReducers: ActionReducerMap<NiFiState> = {
-    router: routerReducer,
-    [userFeatureKey]: userReducer,
-    [extensionTypesFeatureKey]: extensionTypesReducer
-};
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.spec.ts
new file mode 100644
index 0000000000..6b6b48eee5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.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 { ProvenanceEventListing } from './provenance-event-listing.component';
+import { provideMockStore } from '@ngrx/store/testing';
+import { initialState } from '../../state/provenance-event-listing/provenance-event-listing.reducer';
+
+describe('ParameterContextListing', () => {
+    let component: ProvenanceEventListing;
+    let fixture: ComponentFixture<ProvenanceEventListing>;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            declarations: [ProvenanceEventListing],
+            providers: [provideMockStore({ initialState })]
+        });
+        fixture = TestBed.createComponent(ProvenanceEventListing);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.ts
new file mode 100644
index 0000000000..075a8f0b77
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.ts
@@ -0,0 +1,177 @@
+/*
+ * 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 } from '@angular/core';
+import { Store } from '@ngrx/store';
+import {
+    Provenance,
+    ProvenanceEventListingState,
+    ProvenanceRequest,
+    ProvenanceResults
+} from '../../state/provenance-event-listing';
+import {
+    selectLoadedTimestamp,
+    selectProvenance,
+    selectProvenanceRequest,
+    selectSearchableFieldsFromRoute,
+    selectStatus
+} from '../../state/provenance-event-listing/provenance-event-listing.selectors';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { filter, map, take, tap } from 'rxjs';
+import {
+    clearProvenanceRequest,
+    openProvenanceEventDialog,
+    openSearchDialog,
+    resubmitProvenanceQuery,
+    saveProvenanceRequest
+} from '../../state/provenance-event-listing/provenance-event-listing.actions';
+import { ProvenanceSearchDialog } from './provenance-search-dialog/provenance-search-dialog.component';
+import { ProvenanceEventSummary } from '../../../../state/shared';
+
+@Component({
+    selector: 'provenance-event-listing',
+    templateUrl: './provenance-event-listing.component.html',
+    styleUrls: ['./provenance-event-listing.component.scss']
+})
+export class ProvenanceEventListing {
+    status$ = this.store.select(selectStatus);
+    loadedTimestamp$ = this.store.select(selectLoadedTimestamp);
+    provenance$ = this.store.select(selectProvenance);
+
+    request!: ProvenanceRequest;
+
+    constructor(private store: Store<ProvenanceEventListingState>) {
+        this.store
+            .select(selectSearchableFieldsFromRoute)
+            .pipe(
+                filter((queryParams) => queryParams != null),
+                // only consider the first searchable fields from route, subsequent changes to the route will be present in the saved request
+                take(1),
+                map((queryParams) => {
+                    const request: ProvenanceRequest = {
+                        incrementalResults: false,
+                        maxResults: 1000,
+                        summarize: true
+                    };
+
+                    if (queryParams) {
+                        if (queryParams.componentId || queryParams.flowFileUuid) {
+                            request.searchTerms = {};
+
+                            if (queryParams.componentId) {
+                                request.searchTerms['ProcessorID'] = {
+                                    value: queryParams.componentId,
+                                    inverse: false
+                                };
+                            }
+                            if (queryParams.flowFileUuid) {
+                                request.searchTerms['FlowFileUUID'] = {
+                                    value: queryParams.flowFileUuid,
+                                    inverse: false
+                                };
+                            }
+                        }
+                    }
+
+                    return request;
+                })
+            )
+            .subscribe((request) => {
+                this.store.dispatch(
+                    saveProvenanceRequest({
+                        request
+                    })
+                );
+            });
+
+        // any changes to the saved request will trigger a provenance query submission
+        this.store
+            .select(selectProvenanceRequest)
+            .pipe(
+                map((request) => {
+                    if (request) {
+                        return request;
+                    }
+
+                    const initialRequest: ProvenanceRequest = {
+                        incrementalResults: false,
+                        maxResults: 1000,
+                        summarize: true
+                    };
+
+                    return initialRequest;
+                }),
+                tap((request) => (this.request = request)),
+                takeUntilDestroyed()
+            )
+            .subscribe((request) => {
+                this.store.dispatch(
+                    resubmitProvenanceQuery({
+                        request
+                    })
+                );
+            });
+    }
+
+    getResultsMessage(provenance: Provenance): string {
+        const request: ProvenanceRequest = provenance.request;
+        const results: ProvenanceResults = provenance.results;
+
+        if (this.hasRequest(request)) {
+            if (results.totalCount >= ProvenanceSearchDialog.MAX_RESULTS) {
+                return `Showing ${ProvenanceSearchDialog.MAX_RESULTS} of ${results.totalCount} events that match the specified query, please refine the search.`;
+            } else {
+                return 'Showing the events that match the specified query.';
+            }
+        } else {
+            if (results.totalCount >= ProvenanceSearchDialog.MAX_RESULTS) {
+                return `Showing the most recent ${ProvenanceSearchDialog.MAX_RESULTS} of ${results.totalCount} events, please refine the search.`;
+            } else {
+                return 'Showing the most recent events.';
+            }
+        }
+    }
+
+    hasRequest(request: ProvenanceRequest): boolean {
+        const hasSearchTerms: boolean = !!request.searchTerms && Object.keys(request.searchTerms).length > 0;
+        return !!request.startDate || !!request.endDate || hasSearchTerms;
+    }
+
+    clearRequest(): void {
+        this.store.dispatch(clearProvenanceRequest());
+    }
+
+    openSearchCriteria(): void {
+        this.store.dispatch(openSearchDialog());
+    }
+
+    openEventDialog(event: ProvenanceEventSummary): void {
+        this.store.dispatch(
+            openProvenanceEventDialog({
+                id: event.id
+            })
+        );
+    }
+
+    refreshParameterContextListing(): void {
+        this.store.dispatch(
+            resubmitProvenanceQuery({
+                request: this.request
+            })
+        );
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.module.ts
new file mode 100644
index 0000000000..a0895569ff
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.module.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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ProvenanceEventListing } from './provenance-event-listing.component';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import { MatSortModule } from '@angular/material/sort';
+import { MatTableModule } from '@angular/material/table';
+import { NifiTooltipDirective } from '../../../../ui/common/tooltips/nifi-tooltip.directive';
+import { ProvenanceEventListingRoutingModule } from './provenance-event-listing-routing.module';
+import { ProvenanceEventTable } from './provenance-event-table/provenance-event-table.component';
+
+@NgModule({
+    declarations: [ProvenanceEventListing],
+    exports: [ProvenanceEventListing],
+    imports: [
+        CommonModule,
+        ProvenanceEventListingRoutingModule,
+        NgxSkeletonLoaderModule,
+        MatSortModule,
+        MatTableModule,
+        NifiTooltipDirective,
+        ProvenanceEventTable
+    ]
+})
+export class ProvenanceEventListingModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.html
new file mode 100644
index 0000000000..abea7075c8
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.html
@@ -0,0 +1,156 @@
+<!--
+  ~ 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="provenance-event-table h-full flex flex-col">
+    <div class="flex flex-col">
+        <div class="value font-bold">Displaying {{ filteredCount }} of {{ totalCount }}</div>
+        <div class="flex justify-between">
+            <div>
+                Oldest event available: <span class="value">{{ oldestEventAvailable }}</span>
+            </div>
+            <div>
+                {{ resultsMessage }}
+                <a *ngIf="hasRequest" (click)="clearRequestClicked()">Clear Search</a>
+            </div>
+        </div>
+        <div class="flex justify-between">
+            <form [formGroup]="filterForm">
+                <div class="flex pt-2">
+                    <div class="mr-2">
+                        <mat-form-field>
+                            <mat-label>Filter</mat-label>
+                            <input matInput type="text" class="small" formControlName="filterTerm" />
+                        </mat-form-field>
+                    </div>
+                    <div>
+                        <mat-form-field>
+                            <mat-label>Filter By</mat-label>
+                            <mat-select formControlName="filterColumn">
+                                <mat-option *ngFor="let option of filterColumnOptions" [value]="option"
+                                    >{{ option }}
+                                </mat-option>
+                            </mat-select>
+                        </mat-form-field>
+                    </div>
+                </div>
+            </form>
+            <div class="flex flex-col justify-center">
+                <button class="nifi-button" (click)="searchClicked()">
+                    <i class="fa fa-search"></i>
+                </button>
+            </div>
+        </div>
+    </div>
+    <div class="flex-1 relative">
+        <div class="listing-table border absolute inset-0 overflow-y-auto">
+            <table
+                mat-table
+                [dataSource]="dataSource"
+                matSort
+                matSortDisableClear
+                (matSortChange)="updateSort($event)"
+                [matSortActive]="sort.active"
+                [matSortDirection]="sort.direction">
+                <!-- More Details Column -->
+                <ng-container matColumnDef="moreDetails">
+                    <th mat-header-cell *matHeaderCellDef></th>
+                    <td mat-cell *matCellDef="let item">
+                        <div
+                            class="pointer fa fa-info-circle"
+                            title="View Details"
+                            (click)="viewDetailsClicked(item)"></div>
+                    </td>
+                </ng-container>
+
+                <!-- Name Column -->
+                <ng-container matColumnDef="eventTime">
+                    <th mat-header-cell *matHeaderCellDef mat-sort-header>Event Time</th>
+                    <td mat-cell *matCellDef="let item">
+                        {{ item.eventTime }}
+                    </td>
+                </ng-container>
+
+                <!-- Type Column -->
+                <ng-container matColumnDef="eventType">
+                    <th mat-header-cell *matHeaderCellDef mat-sort-header>Type</th>
+                    <td mat-cell *matCellDef="let item">
+                        {{ item.eventType }}
+                    </td>
+                </ng-container>
+
+                <!-- FlowFile UUID Column -->
+                <ng-container matColumnDef="flowFileUuid">
+                    <th mat-header-cell *matHeaderCellDef mat-sort-header>FlowFile UUID</th>
+                    <td mat-cell *matCellDef="let item">
+                        {{ item.flowFileUuid }}
+                    </td>
+                </ng-container>
+
+                <!-- File Size Column -->
+                <ng-container matColumnDef="fileSize">
+                    <th mat-header-cell *matHeaderCellDef mat-sort-header>File Size</th>
+                    <td mat-cell *matCellDef="let item">
+                        {{ item.fileSize }}
+                    </td>
+                </ng-container>
+
+                <!-- Component Name Column -->
+                <ng-container matColumnDef="componentName">
+                    <th mat-header-cell *matHeaderCellDef mat-sort-header>Component Name</th>
+                    <td mat-cell *matCellDef="let item">
+                        {{ item.componentName }}
+                    </td>
+                </ng-container>
+
+                <!-- Component Type Column -->
+                <ng-container matColumnDef="componentType">
+                    <th mat-header-cell *matHeaderCellDef mat-sort-header>Component Type</th>
+                    <td mat-cell *matCellDef="let item">
+                        {{ item.componentType }}
+                    </td>
+                </ng-container>
+
+                <!-- Actions Column -->
+                <ng-container matColumnDef="actions">
+                    <th mat-header-cell *matHeaderCellDef></th>
+                    <td mat-cell *matCellDef="let item">
+                        <div class="flex items-center gap-x-3">
+                            <!--                        <div-->
+                            <!--                            class="pointer icon icon-lineage"-->
+                            <!--                            *ngIf="canStop(item)"-->
+                            <!--                            (click)="stopClicked(item)"-->
+                            <!--                            title="Stop"></div>-->
+                            <div
+                                *ngIf="supportsGoTo(item)"
+                                class="pointer fa fa-long-arrow-right"
+                                title="Go To"
+                                [routerLink]="getComponentLink(item)"></div>
+                        </div>
+                    </td>
+                </ng-container>
+
+                <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
+                <tr
+                    mat-row
+                    *matRowDef="let row; let even = even; columns: displayedColumns"
+                    (click)="select(row)"
+                    [class.selected]="isSelected(row)"
+                    [class.even]="even"></tr>
+            </table>
+        </div>
+    </div>
+</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.scss
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.scss
index 3789433cd3..d95e7e66fb 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.scss
@@ -15,21 +15,13 @@
  * limitations under the License.
  */
 
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
-
-export interface NiFiState {
-    router: RouterReducerState;
-    [userFeatureKey]: UserState;
-    [extensionTypesFeatureKey]: ExtensionTypesState;
+.provenance-event-table {
+    .listing-table {
+        table {
+            .mat-column-actions {
+                min-width: 50px;
+                width: 50px;
+            }
+        }
+    }
 }
-
-export const rootReducers: ActionReducerMap<NiFiState> = {
-    router: routerReducer,
-    [userFeatureKey]: userReducer,
-    [extensionTypesFeatureKey]: extensionTypesReducer
-};
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.spec.ts
new file mode 100644
index 0000000000..497c2eb142
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.spec.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { ProvenanceEventTable } from './provenance-event-table.component';
+import { MatTableModule } from '@angular/material/table';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+describe('ProvenanceEventTable', () => {
+    let component: ProvenanceEventTable;
+    let fixture: ComponentFixture<ProvenanceEventTable>;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            imports: [ProvenanceEventTable, MatTableModule, BrowserAnimationsModule]
+        });
+        fixture = TestBed.createComponent(ProvenanceEventTable);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.ts
new file mode 100644
index 0000000000..d80c6f6d03
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.ts
@@ -0,0 +1,232 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
+import { MatTableDataSource, MatTableModule } from '@angular/material/table';
+import { MatSortModule, Sort } from '@angular/material/sort';
+import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component';
+import { BulletinsTip } from '../../../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
+import { ValidationErrorsTip } from '../../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component';
+import { NiFiCommon } from '../../../../../service/nifi-common.service';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatOptionModule } from '@angular/material/core';
+import { MatSelectModule } from '@angular/material/select';
+import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { NgForOf, NgIf } from '@angular/common';
+import { debounceTime } from 'rxjs';
+import { ProvenanceEventSummary } from '../../../../../state/shared';
+import { RouterLink } from '@angular/router';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+
+@Component({
+    selector: 'provenance-event-table',
+    standalone: true,
+    templateUrl: './provenance-event-table.component.html',
+    imports: [
+        MatTableModule,
+        MatSortModule,
+        MatFormFieldModule,
+        MatInputModule,
+        MatOptionModule,
+        MatSelectModule,
+        ReactiveFormsModule,
+        NgForOf,
+        NgIf,
+        RouterLink,
+        NgxSkeletonLoaderModule
+    ],
+    styleUrls: ['./provenance-event-table.component.scss', '../../../../../../assets/styles/listing-table.scss']
+})
+export class ProvenanceEventTable implements AfterViewInit {
+    @Input() set events(events: ProvenanceEventSummary[]) {
+        if (events) {
+            this.dataSource.data = this.sortEvents(events, this.sort);
+            this.dataSource.filterPredicate = (data: ProvenanceEventSummary, filter: string) => {
+                const filterArray = filter.split('|');
+                const filterTerm = filterArray[0];
+                const filterColumn = filterArray[1];
+
+                if (filterColumn === this.filterColumnOptions[0]) {
+                    return data.componentName.toLowerCase().indexOf(filterTerm.toLowerCase()) >= 0;
+                } else if (filterColumn === this.filterColumnOptions[1]) {
+                    return data.componentType.toLowerCase().indexOf(filterTerm.toLowerCase()) >= 0;
+                } else {
+                    return data.eventType.toLowerCase().indexOf(filterTerm.toLowerCase()) >= 0;
+                }
+            };
+            this.totalCount = events.length;
+            this.filteredCount = events.length;
+
+            // apply any filtering to the new data
+            const filterTerm = this.filterForm.get('filterTerm')?.value;
+            if (filterTerm?.length > 0) {
+                const filterColumn = this.filterForm.get('filterColumn')?.value;
+                this.applyFilter(filterTerm, filterColumn);
+            }
+        }
+    }
+    @Input() oldestEventAvailable!: string;
+    @Input() resultsMessage!: string;
+    @Input() hasRequest!: boolean;
+
+    @Output() openSearchCriteria: EventEmitter<void> = new EventEmitter<void>();
+    @Output() clearRequest: EventEmitter<void> = new EventEmitter<void>();
+    @Output() openEventDialog: EventEmitter<ProvenanceEventSummary> = new EventEmitter<ProvenanceEventSummary>();
+
+    protected readonly TextTip = TextTip;
+    protected readonly BulletinsTip = BulletinsTip;
+    protected readonly ValidationErrorsTip = ValidationErrorsTip;
+
+    // TODO - conditionally include the cluster column
+    displayedColumns: string[] = [
+        'moreDetails',
+        'eventTime',
+        'eventType',
+        'flowFileUuid',
+        'fileSize',
+        'componentName',
+        'componentType',
+        'actions'
+    ];
+    dataSource: MatTableDataSource<ProvenanceEventSummary> = new MatTableDataSource<ProvenanceEventSummary>();
+    selectedEventId: string | null = null;
+
+    sort: Sort = {
+        active: 'eventTime',
+        direction: 'desc'
+    };
+
+    filterForm: FormGroup;
+    filterColumnOptions: string[] = ['component name', 'component type', 'type'];
+    totalCount: number = 0;
+    filteredCount: number = 0;
+
+    constructor(private formBuilder: FormBuilder) {
+        this.filterForm = this.formBuilder.group({ filterTerm: '', filterColumn: this.filterColumnOptions[0] });
+    }
+
+    ngAfterViewInit(): void {
+        this.filterForm
+            .get('filterTerm')
+            ?.valueChanges.pipe(debounceTime(500))
+            .subscribe((filterTerm: string) => {
+                const filterColumn = this.filterForm.get('filterColumn')?.value;
+                this.applyFilter(filterTerm, filterColumn);
+            });
+
+        this.filterForm.get('filterColumn')?.valueChanges.subscribe((filterColumn: string) => {
+            const filterTerm = this.filterForm.get('filterTerm')?.value;
+            this.applyFilter(filterTerm, filterColumn);
+        });
+    }
+
+    updateSort(sort: Sort): void {
+        this.sort = sort;
+        this.dataSource.data = this.sortEvents(this.dataSource.data, sort);
+    }
+
+    sortEvents(events: ProvenanceEventSummary[], sort: Sort): ProvenanceEventSummary[] {
+        const data: ProvenanceEventSummary[] = events.slice();
+        return data.sort((a, b) => {
+            const isAsc = sort.direction === 'asc';
+
+            switch (sort.active) {
+                case 'eventTime':
+                    // event ideas are increasing, so we can use this simple number for sorting purposes
+                    // since we don't surface the timestamp as millis
+                    return (a.eventId - b.eventId) * (isAsc ? 1 : -1);
+                case 'eventType':
+                    return this.compare(a.eventType, b.eventType, isAsc);
+                case 'flowFileUuid':
+                    return this.compare(a.flowFileUuid, b.flowFileUuid, isAsc);
+                case 'fileSize':
+                    return (a.fileSizeBytes - b.fileSizeBytes) * (isAsc ? 1 : -1);
+                case 'componentName':
+                    return this.compare(a.componentName, b.componentName, isAsc);
+                case 'componentType':
+                    return this.compare(a.componentType, b.componentType, isAsc);
+                default:
+                    return 0;
+            }
+        });
+    }
+
+    private compare(a: string, b: string, isAsc: boolean): number {
+        if (a === b) {
+            return 0;
+        }
+
+        return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
+    }
+
+    applyFilter(filterTerm: string, filterColumn: string) {
+        this.dataSource.filter = `${filterTerm}|${filterColumn}`;
+        this.filteredCount = this.dataSource.filteredData.length;
+    }
+
+    clearRequestClicked(): void {
+        this.clearRequest.next();
+    }
+
+    searchClicked() {
+        this.openSearchCriteria.next();
+    }
+
+    viewDetailsClicked(event: ProvenanceEventSummary) {
+        this.openEventDialog.next(event);
+    }
+
+    select(event: ProvenanceEventSummary): void {
+        this.selectedEventId = event.id;
+    }
+
+    isSelected(event: ProvenanceEventSummary): boolean {
+        if (this.selectedEventId) {
+            return event.id == this.selectedEventId;
+        }
+        return false;
+    }
+
+    supportsGoTo(event: ProvenanceEventSummary): boolean {
+        if (event.groupId == null) {
+            return false;
+        }
+
+        if (event.componentId === 'Remote Output Port' || event.componentId === 'Remote Input Port') {
+            return false;
+        }
+
+        return true;
+    }
+
+    getComponentLink(event: ProvenanceEventSummary): string[] {
+        let link: string[];
+
+        if (event.groupId == event.componentId) {
+            link = ['/process-groups', event.componentId];
+        } else if (event.componentId === 'Connection' || event.componentId === 'Load Balanced Connection') {
+            link = ['/process-groups', event.groupId, 'Connection', event.componentId];
+        } else if (event.componentId === 'Output Port') {
+            link = ['/process-groups', event.groupId, 'OutputPort', event.componentId];
+        } else {
+            link = ['/process-groups', event.groupId, 'Processor', event.componentId];
+        }
+
+        return link;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.html
new file mode 100644
index 0000000000..086f0cb37a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.html
@@ -0,0 +1,76 @@
+<!--
+  ~ 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.
+  -->
+
+<h2 mat-dialog-title>Search Events</h2>
+<form class="search-events-form" [formGroup]="provenanceOptionsForm">
+    <mat-dialog-content>
+        <ng-container *ngFor="let searchableField of request.options.searchableFields">
+            <div [formGroupName]="searchableField.id">
+                <mat-form-field>
+                    <mat-label>{{ searchableField.label }}</mat-label>
+                    <input matInput formControlName="value" type="text" />
+                </mat-form-field>
+                <div class="-mt-6 mb-4">
+                    <mat-checkbox formControlName="inverse" name="inverse"> Exclude </mat-checkbox>
+                </div>
+            </div>
+        </ng-container>
+        <mat-form-field>
+            <mat-label>Date Range</mat-label>
+            <mat-date-range-input [rangePicker]="picker">
+                <input matStartDate formControlName="startDate" placeholder="Start date" />
+                <input matEndDate formControlName="endDate" placeholder="End date" />
+            </mat-date-range-input>
+            <mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
+            <mat-date-range-picker #picker></mat-date-range-picker>
+        </mat-form-field>
+        <div class="flex gap-x-3">
+            <div class="w-full flex flex-col">
+                <mat-form-field>
+                    <mat-label>Start Time ({{ timezone }})</mat-label>
+                    <input matInput type="text" formControlName="startTime" />
+                </mat-form-field>
+                <mat-form-field>
+                    <mat-label>Minimum File Size</mat-label>
+                    <input matInput type="text" formControlName="minFileSize" />
+                </mat-form-field>
+            </div>
+            <div class="w-full flex flex-col">
+                <mat-form-field>
+                    <mat-label>End Time ({{ timezone }})</mat-label>
+                    <input matInput type="text" formControlName="endTime" />
+                </mat-form-field>
+                <mat-form-field>
+                    <mat-label>Maximum File Size</mat-label>
+                    <input matInput type="text" formControlName="maxFileSize" />
+                </mat-form-field>
+            </div>
+        </div>
+    </mat-dialog-content>
+    <mat-dialog-actions align="end">
+        <button color="accent" mat-raised-button mat-dialog-close>Cancel</button>
+        <button
+            [disabled]="provenanceOptionsForm.invalid"
+            type="button"
+            color="primary"
+            (click)="searchClicked()"
+            mat-raised-button
+            mat-dialog-close>
+            <span>Search</span>
+        </button>
+    </mat-dialog-actions>
+</form>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.scss
similarity index 53%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.scss
index 3789433cd3..219a472003 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.scss
@@ -15,21 +15,16 @@
  * limitations under the License.
  */
 
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+@use '@angular/material' as mat;
 
-export interface NiFiState {
-    router: RouterReducerState;
-    [userFeatureKey]: UserState;
-    [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+.search-events-form {
+    @include mat.button-density(-1);
+
+    .mat-mdc-form-field {
+        width: 100%;
+    }
 
-export const rootReducers: ActionReducerMap<NiFiState> = {
-    router: routerReducer,
-    [userFeatureKey]: userReducer,
-    [extensionTypesFeatureKey]: extensionTypesReducer
-};
+    .mdc-text-field__input::-webkit-calendar-picker-indicator {
+        display: block !important;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.spec.ts
new file mode 100644
index 0000000000..881177b7e9
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.spec.ts
@@ -0,0 +1,85 @@
+/*
+ * 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 { ProvenanceSearchDialog } from './provenance-search-dialog.component';
+import { MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { MatNativeDateModule } from '@angular/material/core';
+
+describe('ProvenanceSearchDialog', () => {
+    let component: ProvenanceSearchDialog;
+    let fixture: ComponentFixture<ProvenanceSearchDialog>;
+
+    const data: any = {
+        timeOffset: -18000000,
+        options: {
+            searchableFields: [
+                {
+                    id: 'EventType',
+                    field: 'eventType',
+                    label: 'Event Type',
+                    type: 'STRING'
+                },
+                {
+                    id: 'FlowFileUUID',
+                    field: 'uuid',
+                    label: 'FlowFile UUID',
+                    type: 'STRING'
+                },
+                {
+                    id: 'Filename',
+                    field: 'filename',
+                    label: 'Filename',
+                    type: 'STRING'
+                },
+                {
+                    id: 'ProcessorID',
+                    field: 'processorId',
+                    label: 'Component ID',
+                    type: 'STRING'
+                },
+                {
+                    id: 'Relationship',
+                    field: 'relationship',
+                    label: 'Relationship',
+                    type: 'STRING'
+                }
+            ]
+        },
+        currentRequest: {
+            incrementalResults: false,
+            maxResults: 1000,
+            summarize: true
+        }
+    };
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            imports: [ProvenanceSearchDialog, BrowserAnimationsModule, MatNativeDateModule],
+            providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
+        });
+        fixture = TestBed.createComponent(ProvenanceSearchDialog);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.ts
new file mode 100644
index 0000000000..6474387adb
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.ts
@@ -0,0 +1,220 @@
+/*
+ * 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, EventEmitter, Inject, Input, Output } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { MatInputModule } from '@angular/material/input';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatButtonModule } from '@angular/material/button';
+import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
+import {
+    ProvenanceRequest,
+    ProvenanceSearchDialogRequest,
+    SearchableField
+} from '../../../state/provenance-event-listing';
+import { MatDatepickerModule } from '@angular/material/datepicker';
+import { NiFiCommon } from '../../../../../service/nifi-common.service';
+
+@Component({
+    selector: 'provenance-search-dialog',
+    standalone: true,
+    templateUrl: './provenance-search-dialog.component.html',
+    imports: [
+        ReactiveFormsModule,
+        MatDialogModule,
+        MatInputModule,
+        MatCheckboxModule,
+        MatButtonModule,
+        NgIf,
+        AsyncPipe,
+        NgForOf,
+        MatDatepickerModule
+    ],
+    styleUrls: ['./provenance-search-dialog.component.scss']
+})
+export class ProvenanceSearchDialog {
+    @Input() timezone!: string;
+    @Output() submitSearchCriteria: EventEmitter<ProvenanceRequest> = new EventEmitter<ProvenanceRequest>();
+
+    public static readonly MAX_RESULTS: number = 1000;
+    private static readonly DEFAULT_START_TIME: string = '00:00:00';
+    private static readonly DEFAULT_END_TIME: string = '23:59:59';
+
+    provenanceOptionsForm: FormGroup;
+
+    constructor(
+        @Inject(MAT_DIALOG_DATA) public request: ProvenanceSearchDialogRequest,
+        private formBuilder: FormBuilder,
+        private nifiCommon: NiFiCommon
+    ) {
+        const now = new Date();
+        this.clearTime(now);
+
+        let startDate: Date = now;
+        let startTime: string = ProvenanceSearchDialog.DEFAULT_START_TIME;
+        let endDate: Date = now;
+        let endTime: string = ProvenanceSearchDialog.DEFAULT_END_TIME;
+        let minFileSize: string = '';
+        let maxFileSize: string = '';
+
+        if (request.currentRequest) {
+            const requestedStartDate = request.currentRequest.startDate;
+            if (requestedStartDate) {
+                startDate = this.nifiCommon.parseDateTime(requestedStartDate);
+                this.clearTime(startDate);
+
+                const requestedStartDateTime = requestedStartDate?.split(' ');
+                if (requestedStartDateTime && requestedStartDateTime.length > 1) {
+                    startTime = requestedStartDateTime[1];
+                }
+            }
+
+            const requestedEndDate = request.currentRequest.endDate;
+            if (requestedEndDate) {
+                endDate = this.nifiCommon.parseDateTime(requestedEndDate);
+                this.clearTime(endDate);
+
+                const requestedEndDateTime = requestedEndDate?.split(' ');
+                if (requestedEndDateTime && requestedEndDateTime.length > 0) {
+                    endTime = requestedEndDateTime[1];
+                }
+            }
+
+            if (request.currentRequest.minimumFileSize) {
+                minFileSize = request.currentRequest.minimumFileSize;
+            }
+
+            if (request.currentRequest.maximumFileSize) {
+                maxFileSize = request.currentRequest.maximumFileSize;
+            }
+        }
+
+        this.provenanceOptionsForm = this.formBuilder.group({
+            startDate: new FormControl(startDate),
+            startTime: new FormControl(startTime),
+            endDate: new FormControl(endDate),
+            endTime: new FormControl(endTime),
+            minFileSize: new FormControl(minFileSize),
+            maxFileSize: new FormControl(maxFileSize)
+        });
+
+        const searchTerms: any = request.currentRequest?.searchTerms;
+
+        const searchableFields: SearchableField[] = request.options.searchableFields;
+        searchableFields.forEach((searchableField) => {
+            let value: string = '';
+            let inverse: boolean = false;
+            if (searchTerms && searchTerms[searchableField.id]) {
+                value = searchTerms[searchableField.id].value;
+                inverse = searchTerms[searchableField.id].inverse;
+            }
+
+            this.provenanceOptionsForm.addControl(
+                searchableField.id,
+                this.formBuilder.group({
+                    value: new FormControl(value),
+                    inverse: new FormControl(inverse)
+                })
+            );
+        });
+    }
+
+    private clearTime(date: Date): void {
+        date.setHours(0);
+        date.setMinutes(0);
+        date.setSeconds(0);
+        date.setMilliseconds(0);
+    }
+
+    searchClicked() {
+        const provenanceRequest: ProvenanceRequest = {
+            maxResults: ProvenanceSearchDialog.MAX_RESULTS,
+            summarize: true,
+            incrementalResults: false
+        };
+
+        const startDate: Date = this.provenanceOptionsForm.get('startDate')?.value;
+        if (startDate) {
+            // convert the date object into the format the api expects
+            const formatted: string = this.nifiCommon.formatDateTime(startDate);
+
+            // get just the date portion because the time is entered separately by the user
+            const formattedStartDateTime = formatted.split(' ');
+            if (formattedStartDateTime.length > 0) {
+                const formattedStartDate = formattedStartDateTime[0];
+
+                let startTime: string = this.provenanceOptionsForm.get('startTime')?.value;
+                if (!startTime) {
+                    startTime = ProvenanceSearchDialog.DEFAULT_START_TIME;
+                }
+
+                // combine all three pieces into the format the api requires
+                provenanceRequest.startDate = `${formattedStartDate} ${startTime} ${this.timezone}`;
+            }
+        }
+
+        const endDate: Date = this.provenanceOptionsForm.get('endDate')?.value;
+        if (endDate) {
+            // convert the date object into the format the api expects
+            const formatted: string = this.nifiCommon.formatDateTime(endDate);
+
+            // get just the date portion because the time is entered separately by the user
+            const formattedEndDateTime = formatted.split(' ');
+            if (formattedEndDateTime.length > 0) {
+                const formattedEndDate = formattedEndDateTime[0];
+
+                let endTime: string = this.provenanceOptionsForm.get('endTime')?.value;
+                if (!endTime) {
+                    endTime = ProvenanceSearchDialog.DEFAULT_END_TIME;
+                }
+
+                // combine all three pieces into the format the api requires
+                provenanceRequest.endDate = `${formattedEndDate} ${endTime} ${this.timezone}`;
+            }
+        }
+
+        const minFileSize: string = this.provenanceOptionsForm.get('minFileSize')?.value;
+        if (minFileSize) {
+            provenanceRequest.minimumFileSize = minFileSize;
+        }
+
+        const maxFileSize: string = this.provenanceOptionsForm.get('maxFileSize')?.value;
+        if (maxFileSize) {
+            provenanceRequest.maximumFileSize = maxFileSize;
+        }
+
+        const searchTerms: any = {};
+        const searchableFields: SearchableField[] = this.request.options.searchableFields;
+        searchableFields.forEach((searchableField) => {
+            // @ts-ignore
+            const searchableFieldForm: FormGroup = this.provenanceOptionsForm.get(searchableField.id);
+            if (searchableFieldForm) {
+                const searchableFieldValue: string = searchableFieldForm.get('value')?.value;
+                if (searchableFieldValue) {
+                    searchTerms[searchableField.id] = {
+                        value: searchableFieldValue,
+                        inverse: searchableFieldForm.get('inverse')?.value
+                    };
+                }
+            }
+        });
+        provenanceRequest.searchTerms = searchTerms;
+
+        this.submitSearchCriteria.next(provenanceRequest);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/about.service.ts
similarity index 53%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/about.service.ts
index 3789433cd3..af8c75c85b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/about.service.ts
@@ -15,21 +15,17 @@
  * limitations under the License.
  */
 
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+import { Injectable } from '@angular/core';
+import { Observable, throwError } from 'rxjs';
+import { HttpClient } from '@angular/common/http';
 
-export interface NiFiState {
-    router: RouterReducerState;
-    [userFeatureKey]: UserState;
-    [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+@Injectable({ providedIn: 'root' })
+export class AboutService {
+    private static readonly API: string = '../nifi-api';
+
+    constructor(private httpClient: HttpClient) {}
 
-export const rootReducers: ActionReducerMap<NiFiState> = {
-    router: routerReducer,
-    [userFeatureKey]: userReducer,
-    [extensionTypesFeatureKey]: extensionTypesReducer
-};
+    getAbout(): Observable<any> {
+        return this.httpClient.get(`${AboutService.API}/flow/about`);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/nifi-common.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/nifi-common.service.ts
index c09b87055d..6d2eee70c8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/nifi-common.service.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/nifi-common.service.ts
@@ -21,6 +21,11 @@ import { Injectable } from '@angular/core';
     providedIn: 'root'
 })
 export class NiFiCommon {
+    private static readonly MILLIS_PER_DAY: number = 86400000;
+    private static readonly MILLIS_PER_HOUR: number = 3600000;
+    private static readonly MILLIS_PER_MINUTE: number = 60000;
+    private static readonly MILLIS_PER_SECOND: number = 1000;
+
     constructor() {}
 
     /**
@@ -171,4 +176,144 @@ export class NiFiCommon {
         }
         return NiFiCommon.LEAD_TRAIL_WHITE_SPACE_REGEX.test(value);
     }
+
+    /**
+     * Pads the specified value to the specified width with the specified character.
+     * If the specified value is already wider than the specified width, the original
+     * value is returned.
+     *
+     * @param {integer} value
+     * @param {integer} width
+     * @param {string} character
+     * @returns {string}
+     */
+    pad(value: number, width: number, character: string): string {
+        let s: string = value + '';
+
+        // pad until wide enough
+        while (s.length < width) {
+            s = character + s;
+        }
+
+        return s;
+    }
+
+    /**
+     * Formats the specified DateTime.
+     *
+     * @param {Date} date
+     * @returns {String}
+     */
+    formatDateTime(date: Date): string {
+        return (
+            this.pad(date.getMonth() + 1, 2, '0') +
+            '/' +
+            this.pad(date.getDate(), 2, '0') +
+            '/' +
+            this.pad(date.getFullYear(), 2, '0') +
+            ' ' +
+            this.pad(date.getHours(), 2, '0') +
+            ':' +
+            this.pad(date.getMinutes(), 2, '0') +
+            ':' +
+            this.pad(date.getSeconds(), 2, '0') +
+            '.' +
+            this.pad(date.getMilliseconds(), 3, '0')
+        );
+    }
+
+    /**
+     * Parses the specified date time into a Date object. The resulting
+     * object does not account for timezone and should only be used for
+     * performing relative comparisons.
+     *
+     * @param {string} rawDateTime
+     * @returns {Date}
+     */
+    parseDateTime(rawDateTime: string): Date {
+        // handle non date values
+        if (!rawDateTime) {
+            return new Date();
+        }
+
+        // parse the date time
+        const dateTime: string[] = rawDateTime.split(/ /);
+
+        // ensure the correct number of tokens
+        if (dateTime.length !== 3) {
+            return new Date();
+        }
+
+        // get the date and time
+        const date: string[] = dateTime[0].split(/\//);
+        const time: string[] = dateTime[1].split(/:/);
+
+        // ensure the correct number of tokens
+        if (date.length !== 3 || time.length !== 3) {
+            return new Date();
+        }
+        const year: number = parseInt(date[2], 10);
+        const month: number = parseInt(date[0], 10) - 1; // new Date() accepts months 0 - 11
+        const day: number = parseInt(date[1], 10);
+        const hours: number = parseInt(time[0], 10);
+        const minutes: number = parseInt(time[1], 10);
+
+        // detect if there is millis
+        const secondsSpec: string[] = time[2].split(/\./);
+        const seconds: number = parseInt(secondsSpec[0], 10);
+        let milliseconds: number = 0;
+        if (secondsSpec.length === 2) {
+            milliseconds = parseInt(secondsSpec[1], 10);
+        }
+        return new Date(year, month, day, hours, minutes, seconds, milliseconds);
+    }
+
+    /**
+     * Formats the specified duration.
+     *
+     * @param {number} millis in millis
+     */
+    formatDuration(millis: number): string {
+        // don't support sub millisecond resolution
+        let duration: number = millis < 1 ? 0 : millis;
+
+        // determine the number of days in the specified duration
+        let days: number = duration / NiFiCommon.MILLIS_PER_DAY;
+        days = days >= 1 ? Math.trunc(days) : 0;
+        duration %= NiFiCommon.MILLIS_PER_DAY;
+
+        // remaining duration should be less than 1 day, get number of hours
+        let hours: number = duration / NiFiCommon.MILLIS_PER_HOUR;
+        hours = hours >= 1 ? Math.trunc(hours) : 0;
+        duration %= NiFiCommon.MILLIS_PER_HOUR;
+
+        // remaining duration should be less than 1 hour, get number of minutes
+        let minutes: number = duration / NiFiCommon.MILLIS_PER_MINUTE;
+        minutes = minutes >= 1 ? Math.trunc(minutes) : 0;
+        duration %= NiFiCommon.MILLIS_PER_MINUTE;
+
+        // remaining duration should be less than 1 minute, get number of seconds
+        let seconds: number = duration / NiFiCommon.MILLIS_PER_SECOND;
+        seconds = seconds >= 1 ? Math.trunc(seconds) : 0;
+
+        // remaining duration is the number millis (don't support sub millisecond resolution)
+        duration = Math.floor(duration % NiFiCommon.MILLIS_PER_SECOND);
+
+        // format the time
+        const time =
+            this.pad(hours, 2, '0') +
+            ':' +
+            this.pad(minutes, 2, '0') +
+            ':' +
+            this.pad(seconds, 2, '0') +
+            '.' +
+            this.pad(duration, 3, '0');
+
+        // only include days if appropriate
+        if (days > 0) {
+            return days + ' days and ' + time;
+        } else {
+            return time;
+        }
+    }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.actions.ts
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.actions.ts
index 3789433cd3..208427b30e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.actions.ts
@@ -15,21 +15,13 @@
  * limitations under the License.
  */
 
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+import { createAction, props } from '@ngrx/store';
+import { LoadAboutResponse } from './index';
 
-export interface NiFiState {
-    router: RouterReducerState;
-    [userFeatureKey]: UserState;
-    [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+export const loadAbout = createAction('[About] Load About');
 
-export const rootReducers: ActionReducerMap<NiFiState> = {
-    router: routerReducer,
-    [userFeatureKey]: userReducer,
-    [extensionTypesFeatureKey]: extensionTypesReducer
-};
+export const loadAboutSuccess = createAction('[About] Load About Success', props<{ response: LoadAboutResponse }>());
+
+export const aboutApiError = createAction('[About] About Api Error', props<{ error: string }>());
+
+export const clearAboutApiError = createAction('[User] Clear About Api Error');
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.effects.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.effects.ts
new file mode 100644
index 0000000000..6d6cb7835f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.effects.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 * as AboutActions from './about.actions';
+import { catchError, from, map, of, switchMap } from 'rxjs';
+import { AboutService } from '../../service/about.service';
+
+@Injectable()
+export class AboutEffects {
+    constructor(
+        private actions$: Actions,
+        private aboutService: AboutService
+    ) {}
+
+    loadAbout$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(AboutActions.loadAbout),
+            switchMap(() => {
+                return from(
+                    this.aboutService.getAbout().pipe(
+                        map((response) =>
+                            AboutActions.loadAboutSuccess({
+                                response: response
+                            })
+                        ),
+                        catchError((error) => of(AboutActions.aboutApiError({ error: error.error })))
+                    )
+                );
+            })
+        )
+    );
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.reducer.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.reducer.ts
new file mode 100644
index 0000000000..db98b4bb6d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.reducer.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 { createReducer, on } from '@ngrx/store';
+import { AboutState } from './index';
+import { aboutApiError, clearAboutApiError, loadAbout, loadAboutSuccess } from './about.actions';
+
+export const initialState: AboutState = {
+    about: null,
+    error: null,
+    status: 'pending'
+};
+
+export const aboutReducer = createReducer(
+    initialState,
+    on(loadAbout, (state) => ({
+        ...state,
+        status: 'loading' as const
+    })),
+    on(loadAboutSuccess, (state, { response }) => ({
+        ...state,
+        about: response.about,
+        error: null,
+        status: 'success' as const
+    })),
+    on(aboutApiError, (state, { error }) => ({
+        ...state,
+        error: error,
+        status: 'error' as const
+    })),
+    on(clearAboutApiError, (state) => ({
+        ...state,
+        error: null,
+        status: 'pending' as const
+    }))
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.selectors.ts
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.selectors.ts
index 3789433cd3..14fd41d990 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.selectors.ts
@@ -15,21 +15,9 @@
  * limitations under the License.
  */
 
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+import { createFeatureSelector, createSelector } from '@ngrx/store';
+import { aboutFeatureKey, AboutState } from './index';
 
-export interface NiFiState {
-    router: RouterReducerState;
-    [userFeatureKey]: UserState;
-    [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+export const selectAboutState = createFeatureSelector<AboutState>(aboutFeatureKey);
 
-export const rootReducers: ActionReducerMap<NiFiState> = {
-    router: routerReducer,
-    [userFeatureKey]: userReducer,
-    [extensionTypesFeatureKey]: extensionTypesReducer
-};
+export const selectAbout = createSelector(selectAboutState, (state: AboutState) => state.about);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/index.ts
similarity index 53%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/index.ts
index 3789433cd3..ed3f499c14 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/index.ts
@@ -15,21 +15,26 @@
  * limitations under the License.
  */
 
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+export const aboutFeatureKey = 'about';
 
-export interface NiFiState {
-    router: RouterReducerState;
-    [userFeatureKey]: UserState;
-    [extensionTypesFeatureKey]: ExtensionTypesState;
+export interface LoadAboutResponse {
+    about: About;
 }
 
-export const rootReducers: ActionReducerMap<NiFiState> = {
-    router: routerReducer,
-    [userFeatureKey]: userReducer,
-    [extensionTypesFeatureKey]: extensionTypesReducer
-};
+export interface About {
+    title: string;
+    version: string;
+    uri: string;
+    contentViewerUrl: string;
+    timezone: string;
+    buildTag: string;
+    buildRevision: string;
+    buildBranch: string;
+    buildTimestamp: string;
+}
+
+export interface AboutState {
+    about: About | null;
+    error: string | null;
+    status: 'pending' | 'loading' | 'error' | 'success';
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
index 3789433cd3..f943193426 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
@@ -21,15 +21,19 @@ import { UserState, userFeatureKey } from './user';
 import { userReducer } from './user/user.reducer';
 import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
 import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+import { aboutFeatureKey, AboutState } from './about';
+import { aboutReducer } from './about/about.reducer';
 
 export interface NiFiState {
     router: RouterReducerState;
     [userFeatureKey]: UserState;
     [extensionTypesFeatureKey]: ExtensionTypesState;
+    [aboutFeatureKey]: AboutState;
 }
 
 export const rootReducers: ActionReducerMap<NiFiState> = {
     router: routerReducer,
     [userFeatureKey]: userReducer,
-    [extensionTypesFeatureKey]: extensionTypesReducer
+    [extensionTypesFeatureKey]: extensionTypesReducer,
+    [aboutFeatureKey]: aboutReducer
 };
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts
index a60037d12f..de07655740 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts
@@ -20,6 +20,11 @@ export interface OkDialogRequest {
     message: string;
 }
 
+export interface CancelDialogRequest {
+    title: string;
+    message: string;
+}
+
 export interface YesNoDialogRequest {
     title: string;
     message: string;
@@ -53,6 +58,65 @@ export interface EditControllerServiceDialogRequest {
     controllerService: ControllerServiceEntity;
 }
 
+export interface ProvenanceEventSummary {
+    id: string;
+    eventId: number;
+    eventTime: string;
+    eventType: string;
+    flowFileUuid: string;
+    fileSize: string;
+    fileSizeBytes: number;
+    clusterNodeId?: string;
+    clusterNodeAddress?: string;
+    groupId: string;
+    componentId: string;
+    componentType: string;
+    componentName: string;
+}
+
+export interface Attribute {
+    name: string;
+    value: string;
+    previousValue: string;
+}
+
+export interface ProvenanceEvent extends ProvenanceEventSummary {
+    eventDuration: string;
+    lineageDuration: number;
+    clusterNodeId: string;
+    clusterNodeAddress: string;
+    sourceSystemFlowFileId: string;
+    alternateIdentifierUri: string;
+    attributes: Attribute[];
+    parentUuids: string[];
+    childUuids: string[];
+    transitUri: string;
+    relationship: string;
+    details: string;
+    contentEqual: boolean;
+    inputContentAvailable: boolean;
+    inputContentClaimSection: string;
+    inputContentClaimContainer: string;
+    inputContentClaimIdentifier: string;
+    inputContentClaimOffset: number;
+    inputContentClaimFileSize: string;
+    inputContentClaimFileSizeBytes: number;
+    outputContentAvailable: boolean;
+    outputContentClaimSection: string;
+    outputContentClaimContainer: string;
+    outputContentClaimIdentifier: string;
+    outputContentClaimOffset: string;
+    outputContentClaimFileSize: string;
+    outputContentClaimFileSizeBytes: number;
+    replayAvailable: boolean;
+    replayExplanation: string;
+    sourceConnectionIdentifier: string;
+}
+
+export interface ProvenanceEventDialogRequest {
+    event: ProvenanceEvent;
+}
+
 export interface TextTipInput {
     text: string;
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.html
new file mode 100644
index 0000000000..da55132180
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.html
@@ -0,0 +1,24 @@
+<!--
+  ~ 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.
+  -->
+
+<h2 mat-dialog-title>{{ request.title }}</h2>
+<mat-dialog-content>
+    <div class="text-sm">{{ request.message }}</div>
+</mat-dialog-content>
+<mat-dialog-actions align="end">
+    <button type="button" (click)="cancelClicked()" mat-raised-button mat-dialog-close color="primary">Cancel</button>
+</mat-dialog-actions>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.scss
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.scss
index 3789433cd3..2944f98194 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.scss
@@ -14,22 +14,3 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
-
-export interface NiFiState {
-    router: RouterReducerState;
-    [userFeatureKey]: UserState;
-    [extensionTypesFeatureKey]: ExtensionTypesState;
-}
-
-export const rootReducers: ActionReducerMap<NiFiState> = {
-    router: routerReducer,
-    [userFeatureKey]: userReducer,
-    [extensionTypesFeatureKey]: extensionTypesReducer
-};
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.spec.ts
new file mode 100644
index 0000000000..b3ae96a090
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.spec.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { CancelDialog } from './cancel-dialog.component';
+import { MAT_DIALOG_DATA } from '@angular/material/dialog';
+
+describe('CancelDialog', () => {
+    let component: CancelDialog;
+    let fixture: ComponentFixture<CancelDialog>;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            imports: [CancelDialog],
+            providers: [
+                {
+                    provide: MAT_DIALOG_DATA,
+                    useValue: {
+                        title: 'Title',
+                        message: 'Message'
+                    }
+                }
+            ]
+        });
+        fixture = TestBed.createComponent(CancelDialog);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.ts
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.ts
index 3789433cd3..16a4583049 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.ts
@@ -15,21 +15,24 @@
  * limitations under the License.
  */
 
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+import { Component, EventEmitter, Inject, Output } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { MatButtonModule } from '@angular/material/button';
+import { CancelDialogRequest } from '../../../state/shared';
 
-export interface NiFiState {
-    router: RouterReducerState;
-    [userFeatureKey]: UserState;
-    [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+@Component({
+    selector: 'cancel-dialog',
+    standalone: true,
+    imports: [MatDialogModule, MatButtonModule],
+    templateUrl: './cancel-dialog.component.html',
+    styleUrls: ['./cancel-dialog.component.scss']
+})
+export class CancelDialog {
+    @Output() cancel: EventEmitter<void> = new EventEmitter<void>();
+
+    constructor(@Inject(MAT_DIALOG_DATA) public request: CancelDialogRequest) {}
 
-export const rootReducers: ActionReducerMap<NiFiState> = {
-    router: routerReducer,
-    [userFeatureKey]: userReducer,
-    [extensionTypesFeatureKey]: extensionTypesReducer
-};
+    cancelClicked(): void {
+        this.cancel.next();
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.ts
index 75b2ca8b1f..a2208ca3ba 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.ts
@@ -260,8 +260,8 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
                 type: property.descriptor.required
                     ? 'required'
                     : property.descriptor.dynamic
-                    ? 'userDefined'
-                    : 'optional'
+                      ? 'userDefined'
+                      : 'optional'
             };
 
             this.populateServiceLink(item);
@@ -307,8 +307,8 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
                     type: property.descriptor.required
                         ? 'required'
                         : property.descriptor.dynamic
-                        ? 'userDefined'
-                        : 'optional'
+                          ? 'userDefined'
+                          : 'optional'
                 };
 
                 this.populateServiceLink(item);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.html
new file mode 100644
index 0000000000..de1cdd511c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.html
@@ -0,0 +1,447 @@
+<!--
+  ~ 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.
+  -->
+
+<h2 mat-dialog-title>Provenance Event</h2>
+<div class="provenance-event">
+    <mat-dialog-content>
+        <mat-tab-group>
+            <mat-tab label="Details">
+                <div class="tab-content py-4">
+                    <div class="absolute inset-0 flex gap-x-4">
+                        <div class="w-full flex flex-col gap-y-3">
+                            <div class="flex flex-col">
+                                <div>Time</div>
+                                <ng-container
+                                    *ngTemplateOutlet="
+                                        formatValue;
+                                        context: { $implicit: request.event.eventTime }
+                                    "></ng-container>
+                            </div>
+                            <div class="flex flex-col">
+                                <div>Event Duration</div>
+                                <ng-container
+                                    *ngTemplateOutlet="
+                                        formatDuration;
+                                        context: { $implicit: request.event.eventDuration }
+                                    "></ng-container>
+                            </div>
+                            <div class="flex flex-col">
+                                <div>Lineage Duration</div>
+                                <ng-container
+                                    *ngTemplateOutlet="
+                                        formatDuration;
+                                        context: { $implicit: request.event.lineageDuration }
+                                    "></ng-container>
+                            </div>
+                            <div class="flex flex-col">
+                                <div>Type</div>
+                                <ng-container
+                                    *ngTemplateOutlet="
+                                        formatValue;
+                                        context: { $implicit: request.event.eventType }
+                                    "></ng-container>
+                            </div>
+                            <div class="flex flex-col">
+                                <div>FlowFile UUID</div>
+                                <ng-container
+                                    *ngTemplateOutlet="
+                                        formatValue;
+                                        context: { $implicit: request.event.flowFileUuid }
+                                    "></ng-container>
+                            </div>
+                            <div class="flex flex-col">
+                                <div>File Size</div>
+                                <ng-container
+                                    *ngTemplateOutlet="
+                                        formatValue;
+                                        context: {
+                                            $implicit: request.event.fileSize,
+                                            title: request.event.fileSizeBytes + ' bytes'
+                                        }
+                                    "></ng-container>
+                            </div>
+                            <div class="flex flex-col">
+                                <div>Component Id</div>
+                                <ng-container
+                                    *ngTemplateOutlet="
+                                        formatValue;
+                                        context: { $implicit: request.event.componentId }
+                                    "></ng-container>
+                            </div>
+                            <div class="flex flex-col">
+                                <div>Component Name</div>
+                                <ng-container
+                                    *ngTemplateOutlet="
+                                        formatValue;
+                                        context: { $implicit: request.event.componentName }
+                                    "></ng-container>
+                            </div>
+                            <div class="flex flex-col">
+                                <div>Component Type</div>
+                                <ng-container
+                                    *ngTemplateOutlet="
+                                        formatValue;
+                                        context: { $implicit: request.event.componentType }
+                                    "></ng-container>
+                            </div>
+                            <div class="flex flex-col">
+                                <div>Details</div>
+                                <ng-container
+                                    *ngTemplateOutlet="
+                                        formatValue;
+                                        context: { $implicit: request.event.details }
+                                    "></ng-container>
+                            </div>
+                            <ng-container *ngIf="request.event.eventType === 'RECEIVE'">
+                                <div class="flex flex-col">
+                                    <div>Source FlowFile Id</div>
+                                    <ng-container
+                                        *ngTemplateOutlet="
+                                            formatValue;
+                                            context: { $implicit: request.event.sourceSystemFlowFileId }
+                                        "></ng-container>
+                                </div>
+                                <div class="flex flex-col">
+                                    <div>Transit Uri</div>
+                                    <ng-container
+                                        *ngTemplateOutlet="
+                                            formatValue;
+                                            context: { $implicit: request.event.transitUri }
+                                        "></ng-container>
+                                </div>
+                            </ng-container>
+                            <ng-container *ngIf="request.event.eventType === 'SEND'">
+                                <div class="flex flex-col">
+                                    <div>Transit Uri</div>
+                                    <ng-container
+                                        *ngTemplateOutlet="
+                                            formatValue;
+                                            context: { $implicit: request.event.transitUri }
+                                        "></ng-container>
+                                </div>
+                            </ng-container>
+                            <ng-container *ngIf="request.event.eventType === 'ADDINFO'">
+                                <div class="flex flex-col">
+                                    <div>Alternate Identifier Uri</div>
+                                    <ng-container
+                                        *ngTemplateOutlet="
+                                            formatValue;
+                                            context: { $implicit: request.event.alternateIdentifierUri }
+                                        "></ng-container>
+                                </div>
+                            </ng-container>
+                            <ng-container *ngIf="request.event.eventType === 'ROUTE'">
+                                <div class="flex flex-col">
+                                    <div>Relationship</div>
+                                    <ng-container
+                                        *ngTemplateOutlet="
+                                            formatValue;
+                                            context: { $implicit: request.event.relationship }
+                                        "></ng-container>
+                                </div>
+                            </ng-container>
+                            <ng-container *ngIf="request.event.eventType === 'FETCH'">
+                                <div class="flex flex-col" *ngIf="request.event.eventType === 'FETCH'">
+                                    <div>Transit Uri</div>
+                                    <ng-container
+                                        *ngTemplateOutlet="
+                                            formatValue;
+                                            context: { $implicit: request.event.transitUri }
+                                        "></ng-container>
+                                </div>
+                            </ng-container>
+                            <ng-container *ngIf="request.event.clusterNodeId">
+                                <div class="flex flex-col">
+                                    <div>Node Address</div>
+                                    <ng-container
+                                        *ngTemplateOutlet="
+                                            formatValue;
+                                            context: { $implicit: request.event.clusterNodeAddress }
+                                        "></ng-container>
+                                </div>
+                            </ng-container>
+                            <ng-template #formatDuration let-duration>
+                                <ng-container *ngIf="duration != null; else noDuration">
+                                    <div class="value">{{ formatDurationValue(duration) }}</div>
+                                </ng-container>
+                                <ng-template #noDuration>
+                                    <div class="unset">No value set</div>
+                                </ng-template>
+                            </ng-template>
+                        </div>
+                        <div class="w-full flex flex-col gap-y-3">
+                            <div class="flex flex-col">
+                                <div class="event-header">
+                                    Parent FlowFiles ({{ request.event.parentUuids.length }})
+                                </div>
+                                <ng-container
+                                    *ngTemplateOutlet="
+                                        formatUuids;
+                                        context: { $implicit: request.event.parentUuids, emptyMessage: 'No parents' }
+                                    "></ng-container>
+                            </div>
+                            <div class="flex flex-col">
+                                <div class="event-header">Child FlowFiles ({{ request.event.childUuids.length }})</div>
+                                <ng-container
+                                    *ngTemplateOutlet="
+                                        formatUuids;
+                                        context: { $implicit: request.event.childUuids, emptyMessage: 'No children' }
+                                    "></ng-container>
+                            </div>
+                            <ng-template #formatUuids let-uuids let-emptyMessage="emptyMessage">
+                                <ng-container *ngIf="uuids.length > 0; else noUuids">
+                                    <div class="value">
+                                        <div *ngFor="let uuid of uuids">{{ uuid }}</div>
+                                    </div>
+                                </ng-container>
+                                <ng-template #noUuids>
+                                    <div class="unset">{{ emptyMessage }}</div>
+                                </ng-template>
+                            </ng-template>
+                        </div>
+                    </div>
+                </div>
+            </mat-tab>
+            <mat-tab label="Attributes">
+                <div class="tab-content py-4">
+                    <div class="absolute inset-0 flex flex-col gap-y-4">
+                        <div class="flex justify-between">
+                            <div class="event-header">Attribute Values</div>
+                            <div class="flex items-center gap-x-1">
+                                <mat-checkbox [(ngModel)]="onlyShowModifiedAttributes"></mat-checkbox>
+                                <div>Show modified attributes only</div>
+                            </div>
+                        </div>
+                        <div class="flex flex-col">
+                            <div *ngFor="let attribute of request.event.attributes">
+                                <div *ngIf="shouldShowAttribute(attribute)" class="mb-4 flex flex-col">
+                                    <div>{{ attribute.name }}</div>
+                                    <ng-container
+                                        *ngTemplateOutlet="
+                                            formatValue;
+                                            context: { $implicit: attribute.value }
+                                        "></ng-container>
+                                    <ng-container *ngIf="attributeValueChanged(attribute)">
+                                        <ng-container
+                                            *ngTemplateOutlet="
+                                                formatValue;
+                                                context: { $implicit: attribute.previousValue }
+                                            "></ng-container>
+                                    </ng-container>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </mat-tab>
+            <mat-tab label="Content">
+                <div class="tab-content py-4">
+                    <div class="absolute inset-0 flex flex-col gap-y-4">
+                        <div class="flex gap-x-4">
+                            <div class="w-full">
+                                <div class="event-header">Input Claim</div>
+                                <div class="flex flex-col gap-y-3">
+                                    <div>
+                                        <div>Container</div>
+                                        <ng-container
+                                            *ngTemplateOutlet="
+                                                formatContentValue;
+                                                context: { $implicit: request.event.inputContentClaimContainer }
+                                            "></ng-container>
+                                    </div>
+                                    <div>
+                                        <div>Section</div>
+                                        <ng-container
+                                            *ngTemplateOutlet="
+                                                formatContentValue;
+                                                context: { $implicit: request.event.inputContentClaimSection }
+                                            "></ng-container>
+                                    </div>
+                                    <div>
+                                        <div>Identifier</div>
+                                        <ng-container
+                                            *ngTemplateOutlet="
+                                                formatContentValue;
+                                                context: { $implicit: request.event.inputContentClaimIdentifier }
+                                            "></ng-container>
+                                    </div>
+                                    <div>
+                                        <div>Offset</div>
+                                        <ng-container
+                                            *ngTemplateOutlet="
+                                                formatContentValue;
+                                                context: { $implicit: request.event.inputContentClaimOffset }
+                                            "></ng-container>
+                                    </div>
+                                    <div>
+                                        <div>Size</div>
+                                        <ng-container
+                                            *ngTemplateOutlet="
+                                                formatContentValue;
+                                                context: {
+                                                    $implicit: request.event.inputContentClaimFileSize,
+                                                    title: request.event.inputContentClaimFileSizeBytes + ' bytes'
+                                                }
+                                            "></ng-container>
+                                    </div>
+                                    <div *ngIf="request.event.inputContentAvailable" class="flex">
+                                        <button
+                                            color="accent"
+                                            mat-raised-button
+                                            (click)="downloadContentClicked('input')">
+                                            <i class="fa fa-download"></i>
+                                            Download
+                                        </button>
+                                        <button
+                                            *ngIf="contentViewerAvailable"
+                                            class="ml-3"
+                                            color="accent"
+                                            mat-raised-button
+                                            (click)="viewContentClicked('input')">
+                                            <i class="fa fa-eye"></i>
+                                            View
+                                        </button>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="w-full">
+                                <div class="event-header">Output Claim</div>
+                                <div class="flex flex-col gap-y-3">
+                                    <div>
+                                        <div>Container</div>
+                                        <ng-container
+                                            *ngTemplateOutlet="
+                                                formatContentValue;
+                                                context: { $implicit: request.event.outputContentClaimContainer }
+                                            "></ng-container>
+                                    </div>
+                                    <div>
+                                        <div>Section</div>
+                                        <ng-container
+                                            *ngTemplateOutlet="
+                                                formatContentValue;
+                                                context: { $implicit: request.event.outputContentClaimSection }
+                                            "></ng-container>
+                                    </div>
+                                    <div>
+                                        <div>Identifier</div>
+                                        <ng-container
+                                            *ngTemplateOutlet="
+                                                formatContentValue;
+                                                context: { $implicit: request.event.outputContentClaimIdentifier }
+                                            "></ng-container>
+                                    </div>
+                                    <div>
+                                        <div>Offset</div>
+                                        <ng-container
+                                            *ngTemplateOutlet="
+                                                formatContentValue;
+                                                context: { $implicit: request.event.outputContentClaimOffset }
+                                            "></ng-container>
+                                    </div>
+                                    <div>
+                                        <div>Size</div>
+                                        <ng-container
+                                            *ngTemplateOutlet="
+                                                formatContentValue;
+                                                context: {
+                                                    $implicit: request.event.outputContentClaimFileSize,
+                                                    title: request.event.outputContentClaimFileSizeBytes + ' bytes'
+                                                }
+                                            "></ng-container>
+                                    </div>
+                                    <div *ngIf="request.event.outputContentAvailable" class="flex">
+                                        <button
+                                            color="accent"
+                                            mat-raised-button
+                                            (click)="downloadContentClicked('output')">
+                                            <i class="fa fa-download"></i>
+                                            Download
+                                        </button>
+                                        <button
+                                            *ngIf="contentViewerAvailable"
+                                            class="ml-3"
+                                            color="accent"
+                                            mat-raised-button
+                                            (click)="viewContentClicked('output')">
+                                            <i class="fa fa-eye"></i>
+                                            View
+                                        </button>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        <div class="flex flex-col">
+                            <div class="event-header">Replay</div>
+                            <div
+                                *ngIf="request.event.replayAvailable; else replayNotAvailable"
+                                class="flex flex-col gap-y-3">
+                                <div class="flex flex-col">
+                                    <div>Connection Id</div>
+                                    <ng-container
+                                        *ngTemplateOutlet="
+                                            formatContentValue;
+                                            context: { $implicit: request.event.sourceConnectionIdentifier }
+                                        "></ng-container>
+                                </div>
+                                <div>
+                                    <button color="accent" mat-raised-button mat-dialog-close (click)="replayClicked()">
+                                        <i class="fa fa-repeat"></i>
+                                        Replay
+                                    </button>
+                                </div>
+                            </div>
+                            <ng-template #replayNotAvailable>
+                                <div>{{ request.event.replayExplanation }}</div>
+                            </ng-template>
+                        </div>
+                    </div>
+                </div>
+            </mat-tab>
+        </mat-tab-group>
+        <ng-template #formatValue let-value let-title="title">
+            <ng-container *ngIf="value != null; else nullValue">
+                <ng-container *ngIf="value === ''; else nonEmptyValue">
+                    <div class="unset">Empty string set</div>
+                </ng-container>
+                <ng-template #nonEmptyValue>
+                    <div class="value" *ngIf="title == null; else valueWithTitle">{{ value }}</div>
+                    <ng-template #valueWithTitle>
+                        <div class="value" [title]="title">{{ value }}</div>
+                    </ng-template>
+                </ng-template>
+            </ng-container>
+            <ng-template #nullValue>
+                <div class="unset">No value set</div>
+            </ng-template>
+        </ng-template>
+        <ng-template #formatContentValue let-value let-title="title">
+            <ng-container *ngIf="value != null; else nullValue">
+                <div class="value" *ngIf="title == null; else valueWithTitle">{{ value }}</div>
+                <ng-template #valueWithTitle>
+                    <div class="value" [title]="title">{{ value }}</div>
+                </ng-template>
+            </ng-container>
+            <ng-template #nullValue>
+                <div class="unset">No value previously set</div>
+            </ng-template>
+        </ng-template>
+    </mat-dialog-content>
+    <mat-dialog-actions align="end">
+        <button color="primary" mat-raised-button mat-dialog-close>Ok</button>
+    </mat-dialog-actions>
+</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.scss
similarity index 53%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.scss
index 3789433cd3..688f7ebbb1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.scss
@@ -15,21 +15,35 @@
  * limitations under the License.
  */
 
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+@use '@angular/material' as mat;
 
-export interface NiFiState {
-    router: RouterReducerState;
-    [userFeatureKey]: UserState;
-    [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+.provenance-event {
+    @include mat.button-density(-1);
+
+    .mdc-dialog__content {
+        padding: 0 16px;
+        font-size: 14px;
+
+        .tab-content {
+            position: relative;
+            height: 475px;
+            overflow-y: auto;
 
-export const rootReducers: ActionReducerMap<NiFiState> = {
-    router: routerReducer,
-    [userFeatureKey]: userReducer,
-    [extensionTypesFeatureKey]: extensionTypesReducer
-};
+            .event-header {
+                color: #728e9b;
+                font-size: 15px;
+                font-family: 'Roboto Slab';
+                font-style: normal;
+                font-weight: bold;
+            }
+        }
+    }
+
+    .mat-mdc-form-field {
+        width: 100%;
+    }
+
+    .mdc-text-field__input::-webkit-calendar-picker-indicator {
+        display: block !important;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.spec.ts
new file mode 100644
index 0000000000..fb7f240034
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.spec.ts
@@ -0,0 +1,85 @@
+/*
+ * 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 { ProvenanceEventDialog } from './provenance-event-dialog.component';
+import { MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+describe('ProvenanceEventDialog', () => {
+    let component: ProvenanceEventDialog;
+    let fixture: ComponentFixture<ProvenanceEventDialog>;
+
+    const data: any = {
+        event: {
+            id: '67231',
+            eventId: 67231,
+            eventTime: '12/06/2023 13:24:14.934 EST',
+            lineageDuration: 80729691,
+            eventType: 'DROP',
+            flowFileUuid: '6908fd9d-9168-4da5-a92b-0414cdb5e3bc',
+            fileSize: '0 bytes',
+            fileSizeBytes: 0,
+            groupId: '36d207b9-018c-1000-6f5a-1b02d4517a78',
+            componentId: '36d293df-018c-1000-b438-9e0cd664f5aa',
+            componentType: 'Connection',
+            componentName: 'success',
+            attributes: [
+                {
+                    name: 'filename',
+                    value: '6908fd9d-9168-4da5-a92b-0414cdb5e3bc',
+                    previousValue: '6908fd9d-9168-4da5-a92b-0414cdb5e3bc'
+                },
+                {
+                    name: 'path',
+                    value: './',
+                    previousValue: './'
+                },
+                {
+                    name: 'uuid',
+                    value: '6908fd9d-9168-4da5-a92b-0414cdb5e3bc',
+                    previousValue: '6908fd9d-9168-4da5-a92b-0414cdb5e3bc'
+                }
+            ],
+            parentUuids: [],
+            childUuids: [],
+            details: 'FlowFile Queue emptied by admin',
+            contentEqual: false,
+            inputContentAvailable: false,
+            outputContentAvailable: false,
+            outputContentClaimFileSize: '0 bytes',
+            outputContentClaimFileSizeBytes: 0,
+            replayAvailable: true,
+            sourceConnectionIdentifier: '36d293df-018c-1000-b438-9e0cd664f5aa'
+        }
+    };
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            imports: [ProvenanceEventDialog, BrowserAnimationsModule],
+            providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
+        });
+        fixture = TestBed.createComponent(ProvenanceEventDialog);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.ts
new file mode 100644
index 0000000000..7d8f2176f9
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.ts
@@ -0,0 +1,98 @@
+/*
+ * 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, EventEmitter, Inject, Input, Output } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { MatInputModule } from '@angular/material/input';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatButtonModule } from '@angular/material/button';
+import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
+import { MatDatepickerModule } from '@angular/material/datepicker';
+import { NiFiCommon } from '../../../service/nifi-common.service';
+import { MatTabsModule } from '@angular/material/tabs';
+import { Attribute, ProvenanceEvent, ProvenanceEventDialogRequest } from '../../../state/shared';
+
+@Component({
+    selector: 'provenance-event-dialog',
+    standalone: true,
+    templateUrl: './provenance-event-dialog.component.html',
+    imports: [
+        ReactiveFormsModule,
+        MatDialogModule,
+        MatInputModule,
+        MatCheckboxModule,
+        MatButtonModule,
+        NgIf,
+        AsyncPipe,
+        NgForOf,
+        MatDatepickerModule,
+        MatTabsModule,
+        NgTemplateOutlet,
+        FormsModule
+    ],
+    styleUrls: ['./provenance-event-dialog.component.scss']
+})
+export class ProvenanceEventDialog {
+    @Input() contentViewerAvailable!: boolean;
+
+    @Output() downloadContent: EventEmitter<string> = new EventEmitter<string>();
+    @Output() viewContent: EventEmitter<string> = new EventEmitter<string>();
+    @Output() replay: EventEmitter<void> = new EventEmitter<void>();
+
+    onlyShowModifiedAttributes: boolean = false;
+
+    constructor(
+        @Inject(MAT_DIALOG_DATA) public request: ProvenanceEventDialogRequest,
+        private nifiCommon: NiFiCommon
+    ) {}
+
+    formatDurationValue(duration: number): string {
+        if (duration === 0) {
+            return '< 1 sec';
+        }
+
+        return this.nifiCommon.formatDuration(duration);
+    }
+
+    attributeValueChanged(attribute: Attribute): boolean {
+        return attribute.value != attribute.previousValue;
+    }
+
+    shouldShowAttribute(attribute: Attribute): boolean {
+        // if the attribute value has changed, show it
+        if (this.attributeValueChanged(attribute)) {
+            return true;
+        }
+
+        // attribute value hasn't changed, only show when
+        // the user does not want to only see modified attributes
+        return !this.onlyShowModifiedAttributes;
+    }
+
+    downloadContentClicked(direction: string): void {
+        this.downloadContent.next(direction);
+    }
+
+    viewContentClicked(direction: string): void {
+        this.viewContent.next(direction);
+    }
+
+    replayClicked(): void {
+        this.replay.next();
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss
index edce9be5e7..7632849f0b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss
@@ -20,11 +20,11 @@
 @use '@angular/material' as mat;
 
 // Plus imports for other components in your app.
-@import '~roboto-fontface/css/roboto/roboto-fontface.css';
-@import 'assets/fonts/flowfont/flowfont.css';
-@import '~font-awesome/css/font-awesome.min.css';
-@import '~codemirror/lib/codemirror.css';
-@import '~codemirror/addon/hint/show-hint.css';
+@use 'roboto-fontface/css/roboto/roboto-fontface.css';
+@use 'assets/fonts/flowfont/flowfont.css';
+@use 'font-awesome/css/font-awesome.min.css';
+@use 'codemirror/lib/codemirror.css';
+@use 'codemirror/addon/hint/show-hint.css';
 
 $fontPrimary: 'Roboto', sans-serif;
 $fontSecondary: 'Robot Slab', sans-serif;