[NIFI-13053] Cluster page (#8685)

* [NIFI-13053] - Cluster Node Table/Page
* Node listing

* System Listing

* Jvm Listing

* FlowFile Storage listing

* Content and Provenance Repo Storage listings

* Version listing

* review feedback

* only attempt to load system diagnostic info for cluster node view if the user has the proper permission.

* Move Cluster Summary loading/polling to the navigation component.

* restore user state resetting when users component is destroyed.

* reset state on cluster component destroy and reset system diagnostic state when user loses permission to it while on the cluster page.

This closes #8685 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/NodeDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/NodeDTO.java
index f5f3725..f428934 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/NodeDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/NodeDTO.java
@@ -42,6 +42,8 @@
     private String queued;
     private List<NodeEventDTO> events;
     private Date nodeStartTime;
+    private Integer flowFilesQueued;
+    private Long bytesQueued;
 
     /**
      * @return node's last heartbeat timestamp
@@ -203,4 +205,32 @@
     public void setNodeStartTime(Date nodeStartTime) {
         this.nodeStartTime = nodeStartTime;
     }
+
+    /**
+     * @return the number of FlowFiles that are queued up on the node
+     */
+    @Schema(description = "The number of FlowFiles that are queued up on the node",
+            accessMode = Schema.AccessMode.READ_ONLY
+    )
+    public Integer getFlowFilesQueued() {
+        return flowFilesQueued;
+    }
+
+    public void setFlowFilesQueued(Integer flowFilesQueued) {
+        this.flowFilesQueued = flowFilesQueued;
+    }
+
+    /**
+     * @return the total size of all FlowFiles that are queued up on the node
+     */
+    @Schema(description = "The total size of all FlowFiles that are queued up on the node",
+            accessMode = Schema.AccessMode.READ_ONLY
+    )
+    public Long getBytesQueued() {
+        return bytesQueued;
+    }
+
+    public void setFlowFileBytes(Long bytesQueued) {
+        this.bytesQueued = bytesQueued;
+    }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 09f991a..87c77d9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -4845,6 +4845,8 @@
            nodeDto.setNodeStartTime(new Date(nodeHeartbeat.getSystemStartTime()));
            nodeDto.setActiveThreadCount(nodeHeartbeat.getActiveThreadCount());
            nodeDto.setQueued(FormatUtils.formatCount(nodeHeartbeat.getFlowFileCount()) + " / " + FormatUtils.formatDataSize(nodeHeartbeat.getFlowFileBytes()));
+           nodeDto.setFlowFileBytes(nodeHeartbeat.getFlowFileBytes());
+           nodeDto.setFlowFilesQueued(nodeHeartbeat.getFlowFileCount());
        }
 
        // populate node events
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 c603a46..9415713 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
@@ -92,6 +92,11 @@
             )
     },
     {
+        path: 'cluster',
+        canMatch: [authenticationGuard],
+        loadChildren: () => import('./pages/cluster/feature/cluster.module').then((m) => m.ClusterModule)
+    },
+    {
         path: '',
         canMatch: [authenticationGuard],
         loadChildren: () =>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.ts
index 7fac3cb..f074bac 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/access-policies/feature/access-policies.component.ts
@@ -15,29 +15,11 @@
  * limitations under the License.
  */
 
-import { Component, OnDestroy, OnInit } from '@angular/core';
-import { Store } from '@ngrx/store';
-import { NiFiState } from '../../../state';
-import {
-    loadClusterSummary,
-    startClusterSummaryPolling,
-    stopClusterSummaryPolling
-} from '../../../state/cluster-summary/cluster-summary.actions';
+import { Component } from '@angular/core';
 
 @Component({
     selector: 'access-policies',
     templateUrl: './access-policies.component.html',
     styleUrls: ['./access-policies.component.scss']
 })
-export class AccessPolicies implements OnInit, OnDestroy {
-    constructor(private store: Store<NiFiState>) {}
-
-    ngOnInit(): void {
-        this.store.dispatch(loadClusterSummary());
-        this.store.dispatch(startClusterSummaryPolling());
-    }
-
-    ngOnDestroy(): void {
-        this.store.dispatch(stopClusterSummaryPolling());
-    }
-}
+export class AccessPolicies {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster-routing.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster-routing.module.ts
new file mode 100644
index 0000000..ad67e04
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster-routing.module.ts
@@ -0,0 +1,134 @@
+/*
+ * 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 { RouterModule, Routes } from '@angular/router';
+import { Cluster } from './cluster.component';
+import { NgModule } from '@angular/core';
+import { ClusterNodeListing } from '../ui/cluster-node-listing/cluster-node-listing.component';
+import { ClusterSystemListing } from '../ui/cluster-system-listing/cluster-system-listing.component';
+import { ClusterJvmListing } from '../ui/cluster-jvm-listing/cluster-jvm-listing.component';
+import { ClusterFlowFileStorageListing } from '../ui/cluster-flow-file-storage-listing/cluster-flow-file-storage-listing.component';
+import { ClusterContentStorageListing } from '../ui/cluster-content-storage-listing/cluster-content-storage-listing.component';
+import { ClusterProvenanceStorageListing } from '../ui/cluster-provenance-storage-listing/cluster-provenance-storage-listing.component';
+import { ClusterVersionListing } from '../ui/cluster-version-listing/cluster-version-listing.component';
+import { authorizationGuard } from '../../../service/guard/authorization.guard';
+import { CurrentUser } from '../../../state/current-user';
+
+const routes: Routes = [
+    {
+        path: '',
+        component: Cluster,
+        canMatch: [authorizationGuard((user: CurrentUser) => user.controllerPermissions.canRead)],
+        children: [
+            { path: '', pathMatch: 'full', redirectTo: 'nodes' },
+            {
+                path: 'nodes',
+                component: ClusterNodeListing,
+                children: [
+                    {
+                        path: ':id',
+                        component: ClusterNodeListing
+                    }
+                ]
+            },
+            {
+                path: 'system',
+                component: ClusterSystemListing,
+                canMatch: [authorizationGuard((user: CurrentUser) => user.systemPermissions.canRead, '/cluster')],
+                children: [
+                    {
+                        path: ':id',
+                        component: ClusterSystemListing
+                    }
+                ]
+            },
+            {
+                path: 'jvm',
+                component: ClusterJvmListing,
+                canMatch: [authorizationGuard((user: CurrentUser) => user.systemPermissions.canRead, '/cluster')],
+                children: [
+                    {
+                        path: ':id',
+                        component: ClusterJvmListing
+                    }
+                ]
+            },
+            {
+                path: 'flowfile-storage',
+                component: ClusterFlowFileStorageListing,
+                canMatch: [authorizationGuard((user: CurrentUser) => user.systemPermissions.canRead, '/cluster')],
+                children: [
+                    {
+                        path: ':id',
+                        component: ClusterFlowFileStorageListing
+                    }
+                ]
+            },
+            {
+                path: 'content-storage',
+                component: ClusterContentStorageListing,
+                canMatch: [authorizationGuard((user: CurrentUser) => user.systemPermissions.canRead, '/cluster')],
+                children: [
+                    {
+                        path: ':id',
+                        component: ClusterContentStorageListing,
+                        children: [
+                            {
+                                path: ':repo',
+                                component: ClusterContentStorageListing
+                            }
+                        ]
+                    }
+                ]
+            },
+            {
+                path: 'provenance-storage',
+                component: ClusterProvenanceStorageListing,
+                canMatch: [authorizationGuard((user: CurrentUser) => user.systemPermissions.canRead, '/cluster')],
+                children: [
+                    {
+                        path: ':id',
+                        component: ClusterProvenanceStorageListing,
+                        children: [
+                            {
+                                path: ':repo',
+                                component: ClusterProvenanceStorageListing
+                            }
+                        ]
+                    }
+                ]
+            },
+            {
+                path: 'versions',
+                component: ClusterVersionListing,
+                canMatch: [authorizationGuard((user: CurrentUser) => user.systemPermissions.canRead, '/cluster')],
+                children: [
+                    {
+                        path: ':id',
+                        component: ClusterVersionListing
+                    }
+                ]
+            }
+        ]
+    }
+];
+
+@NgModule({
+    imports: [RouterModule.forChild(routes)],
+    exports: [RouterModule]
+})
+export class ClusterRoutingModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster.component.html
new file mode 100644
index 0000000..5e07e2d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster.component.html
@@ -0,0 +1,58 @@
+<!--
+  ~ 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="pb-5 flex flex-col h-screen justify-between gap-y-5">
+    <header class="nifi-header">
+        <navigation></navigation>
+    </header>
+    <div class="px-5 flex-1 flex flex-col gap-y-2">
+        <h3 class="text-xl bold primary-color">NiFi Cluster</h3>
+
+        @if (getTabLinks(); as tabs) {
+            <!-- Don't show the tab bar if there is only 1 tab to show -->
+            <div class="cluster-tabs" [class.hidden]="tabs.length === 1">
+                <nav mat-tab-nav-bar color="primary" [tabPanel]="tabPanel">
+                    @for (tab of tabs; track tab) {
+                        <a
+                            mat-tab-link
+                            [routerLink]="[tab.link]"
+                            routerLinkActive
+                            #rla="routerLinkActive"
+                            [active]="rla.isActive">
+                            {{ tab.label }}
+                        </a>
+                    }
+                </nav>
+            </div>
+        }
+        <div class="pt-4 flex-1">
+            <mat-tab-nav-panel #tabPanel>
+                <router-outlet></router-outlet>
+            </mat-tab-nav-panel>
+        </div>
+
+        <div class="flex justify-between align-middle">
+            <div class="refresh-container flex items-center gap-x-2">
+                <button mat-icon-button color="primary" (click)="refresh()">
+                    <i class="fa fa-refresh" [class.fa-spin]="listingStatus() === 'loading'"></i>
+                </button>
+                <div>Last updated:</div>
+                <div class="accent-color font-medium">{{ loadedTimestamp() }}</div>
+            </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/pages/cluster/feature/cluster.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster.component.scss
new file mode 100644
index 0000000..f3be1ce
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster.component.scss
@@ -0,0 +1,20 @@
+/*!
+ * 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.
+ */
+
+.cluster-tabs {
+    border-bottom-width: 1px;
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster.component.spec.ts
new file mode 100644
index 0000000..033d7b1
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster.component.spec.ts
@@ -0,0 +1,69 @@
+/*
+ * 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 { Component } from '@angular/core';
+import { RouterTestingModule } from '@angular/router/testing';
+import { provideMockStore } from '@ngrx/store/testing';
+import { Cluster } from './cluster.component';
+import { initialClusterState } from '../state/cluster-listing/cluster-listing.reducer';
+import { ClusterNodeListing } from '../ui/cluster-node-listing/cluster-node-listing.component';
+import { MatTabsModule } from '@angular/material/tabs';
+import { selectClusterListing } from '../state/cluster-listing/cluster-listing.selectors';
+import { clusterListingFeatureKey } from '../state/cluster-listing';
+import { ClusterState } from '../state';
+
+describe('Cluster', () => {
+    let component: Cluster;
+    let fixture: ComponentFixture<Cluster>;
+
+    @Component({
+        selector: 'navigation',
+        standalone: true,
+        template: ''
+    })
+    class MockNavigation {}
+
+    beforeEach(() => {
+        const initialState: ClusterState = {
+            [clusterListingFeatureKey]: initialClusterState
+        };
+        TestBed.configureTestingModule({
+            declarations: [Cluster],
+            imports: [ClusterNodeListing, MatTabsModule, RouterTestingModule, MockNavigation],
+            providers: [
+                provideMockStore({
+                    initialState,
+                    selectors: [
+                        {
+                            selector: selectClusterListing,
+                            value: initialState[clusterListingFeatureKey]
+                        }
+                    ]
+                })
+            ]
+        });
+
+        fixture = TestBed.createComponent(Cluster);
+        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/cluster/feature/cluster.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster.component.ts
new file mode 100644
index 0000000..3e809ab
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster.component.ts
@@ -0,0 +1,119 @@
+/*
+ * 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 {
+    loadClusterListing,
+    navigateHome,
+    navigateToClusterNodeListing,
+    resetClusterState
+} from '../state/cluster-listing/cluster-listing.actions';
+import {
+    selectClusterListingLoadedTimestamp,
+    selectClusterListingStatus
+} from '../state/cluster-listing/cluster-listing.selectors';
+import { selectCurrentUser } from '../../../state/current-user/current-user.selectors';
+import { CurrentUser } from '../../../state/current-user';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { selectCurrentRoute } from '../../../state/router/router.selectors';
+import { resetSystemDiagnostics } from '../../../state/system-diagnostics/system-diagnostics.actions';
+
+interface TabLink {
+    label: string;
+    link: string;
+    restricted: boolean;
+}
+
+@Component({
+    selector: 'cluster',
+    templateUrl: './cluster.component.html',
+    styleUrls: ['./cluster.component.scss']
+})
+export class Cluster implements OnInit, OnDestroy {
+    private _currentUser!: CurrentUser;
+    private _tabLinks: TabLink[] = [
+        { label: 'Nodes', link: 'nodes', restricted: false },
+        { label: 'System', link: 'system', restricted: true },
+        { label: 'JVM', link: 'jvm', restricted: true },
+        { label: 'FlowFile Storage', link: 'flowfile-storage', restricted: true },
+        { label: 'Content Storage', link: 'content-storage', restricted: true },
+        { label: 'Provenance Storage', link: 'provenance-storage', restricted: true },
+        { label: 'Versions', link: 'versions', restricted: true }
+    ];
+
+    listingStatus = this.store.selectSignal(selectClusterListingStatus);
+    loadedTimestamp = this.store.selectSignal(selectClusterListingLoadedTimestamp);
+    currentUser$ = this.store.select(selectCurrentUser);
+    currentRoute = this.store.selectSignal(selectCurrentRoute);
+
+    private _userHasSystemReadAccess: boolean | null = null;
+
+    constructor(private store: Store<NiFiState>) {
+        this.currentUser$.pipe(takeUntilDestroyed()).subscribe((currentUser) => {
+            this._currentUser = currentUser;
+            if (!currentUser.controllerPermissions.canRead) {
+                this.store.dispatch(navigateHome());
+            } else {
+                this.evaluateCurrentTabForPermissions();
+            }
+        });
+    }
+
+    ngOnInit(): void {
+        this.store.dispatch(loadClusterListing());
+    }
+
+    ngOnDestroy(): void {
+        this.store.dispatch(resetClusterState());
+        this.store.dispatch(resetSystemDiagnostics());
+    }
+
+    refresh() {
+        this.store.dispatch(loadClusterListing());
+    }
+
+    getTabLinks() {
+        const canRead = this._userHasSystemReadAccess;
+        return this._tabLinks.filter((tabLink) => !tabLink.restricted || (tabLink.restricted && canRead));
+    }
+
+    private evaluateCurrentTabForPermissions() {
+        if (this._userHasSystemReadAccess !== null) {
+            // If the user is on a tab that requires system read permissions, but they have lost said permission while
+            // on that tab, route them to the nodes tab
+            if (!this._currentUser.systemPermissions.canRead && this._userHasSystemReadAccess) {
+                const link = this.getActiveTabLink();
+                if (!link || link.restricted) {
+                    this.store.dispatch(navigateToClusterNodeListing());
+                    this.store.dispatch(resetSystemDiagnostics());
+                }
+            } else if (this._currentUser.systemPermissions.canRead && !this._userHasSystemReadAccess) {
+                // the user has gained permission to see the system info. reload the data to make system info available.
+                this.refresh();
+            }
+        }
+        this._userHasSystemReadAccess = this._currentUser.systemPermissions.canRead;
+    }
+
+    private getActiveTabLink(): TabLink | undefined {
+        const route = this.currentRoute();
+        const path = route.routeConfig.path;
+        return this._tabLinks.find((tabLink) => tabLink.link === path);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster.module.ts
new file mode 100644
index 0000000..35f1b00
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/feature/cluster.module.ts
@@ -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.
+ */
+
+import { NgModule } from '@angular/core';
+import { Cluster } from './cluster.component';
+import { CommonModule } from '@angular/common';
+import { Navigation } from '../../../ui/common/navigation/navigation.component';
+import { StoreModule } from '@ngrx/store';
+import { clusterFeatureKey, reducers } from '../state';
+import { EffectsModule } from '@ngrx/effects';
+import { ClusterListingEffects } from '../state/cluster-listing/cluster-listing.effects';
+import { ClusterRoutingModule } from './cluster-routing.module';
+import { MatTabsModule } from '@angular/material/tabs';
+import { MatIconButton } from '@angular/material/button';
+
+@NgModule({
+    declarations: [Cluster],
+    exports: [Cluster],
+    imports: [
+        CommonModule,
+        Navigation,
+        ClusterRoutingModule,
+        StoreModule.forFeature(clusterFeatureKey, reducers),
+        EffectsModule.forFeature(ClusterListingEffects),
+        MatTabsModule,
+        MatIconButton
+    ]
+})
+export class ClusterModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/service/cluster.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/service/cluster.service.ts
new file mode 100644
index 0000000..ddac4f6
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/service/cluster.service.ts
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { ClusterListingResponse, ClusterNodeEntity } from '../state/cluster-listing';
+
+@Injectable({ providedIn: 'root' })
+export class ClusterService {
+    private static readonly API = '../nifi-api';
+
+    constructor(private httpClient: HttpClient) {}
+
+    getClusterListing(): Observable<ClusterListingResponse> {
+        return this.httpClient.get(`${ClusterService.API}/controller/cluster`) as Observable<ClusterListingResponse>;
+    }
+
+    disconnectNode(nodeId: string): Observable<ClusterNodeEntity> {
+        return this.httpClient.put(`${ClusterService.API}/controller/cluster/nodes/${nodeId}`, {
+            node: {
+                nodeId,
+                status: 'DISCONNECTING'
+            }
+        }) as Observable<ClusterNodeEntity>;
+    }
+
+    connectNode(nodeId: string): Observable<ClusterNodeEntity> {
+        return this.httpClient.put(`${ClusterService.API}/controller/cluster/nodes/${nodeId}`, {
+            node: {
+                nodeId,
+                status: 'CONNECTING'
+            }
+        }) as Observable<ClusterNodeEntity>;
+    }
+
+    offloadNode(nodeId: string): Observable<ClusterNodeEntity> {
+        return this.httpClient.put(`${ClusterService.API}/controller/cluster/nodes/${nodeId}`, {
+            node: {
+                nodeId,
+                status: 'OFFLOADING'
+            }
+        }) as Observable<ClusterNodeEntity>;
+    }
+
+    removeNode(nodeId: string) {
+        return this.httpClient.delete(`${ClusterService.API}/controller/cluster/nodes/${nodeId}`);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/cluster-listing/cluster-listing.actions.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/cluster-listing/cluster-listing.actions.ts
new file mode 100644
index 0000000..4fdb2bc
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/cluster-listing/cluster-listing.actions.ts
@@ -0,0 +1,127 @@
+/*
+ * 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 { ClusterListingEntity, ClusterNode, ClusterNodeEntity, SelectClusterNodeRequest } from './index';
+
+const CLUSTER_LISTING_PREFIX = '[Cluster Listing]';
+
+export const loadClusterListing = createAction(`${CLUSTER_LISTING_PREFIX} Load Cluster Listing`);
+
+export const loadClusterListingSuccess = createAction(
+    `${CLUSTER_LISTING_PREFIX} Load Cluster Listing Success`,
+    props<{ response: ClusterListingEntity }>()
+);
+
+export const confirmAndDisconnectNode = createAction(
+    `${CLUSTER_LISTING_PREFIX} Confirm And Disconnect Node`,
+    props<{ request: ClusterNode }>()
+);
+
+export const disconnectNode = createAction(
+    `${CLUSTER_LISTING_PREFIX} Disconnect Node`,
+    props<{ request: ClusterNode }>()
+);
+
+export const confirmAndConnectNode = createAction(
+    `${CLUSTER_LISTING_PREFIX} Confirm And Connect Node`,
+    props<{ request: ClusterNode }>()
+);
+
+export const connectNode = createAction(`${CLUSTER_LISTING_PREFIX} Connect Node`, props<{ request: ClusterNode }>());
+
+export const confirmAndOffloadNode = createAction(
+    `${CLUSTER_LISTING_PREFIX} Confirm And Offload Node`,
+    props<{ request: ClusterNode }>()
+);
+
+export const offloadNode = createAction(`${CLUSTER_LISTING_PREFIX} Offload Node`, props<{ request: ClusterNode }>());
+
+export const updateNodeSuccess = createAction(
+    `${CLUSTER_LISTING_PREFIX} Update Node Success`,
+    props<{ response: ClusterNodeEntity }>()
+);
+
+export const confirmAndRemoveNode = createAction(
+    `${CLUSTER_LISTING_PREFIX} Confirm And Remove Node`,
+    props<{ request: ClusterNode }>()
+);
+
+export const removeNode = createAction(`${CLUSTER_LISTING_PREFIX} Remove Node`, props<{ request: ClusterNode }>());
+
+export const removeNodeSuccess = createAction(
+    `${CLUSTER_LISTING_PREFIX} Remove Node Success`,
+    props<{ response: ClusterNode }>()
+);
+
+export const clusterNodeSnackbarError = createAction(
+    `${CLUSTER_LISTING_PREFIX} Cluster Node Snackbar Error`,
+    props<{ error: string }>()
+);
+
+export const selectClusterNode = createAction(
+    `${CLUSTER_LISTING_PREFIX} Select Cluster Node`,
+    props<{ request: SelectClusterNodeRequest }>()
+);
+export const selectSystemNode = createAction(
+    `${CLUSTER_LISTING_PREFIX} Select System Node`,
+    props<{ request: SelectClusterNodeRequest }>()
+);
+export const selectJvmNode = createAction(
+    `${CLUSTER_LISTING_PREFIX} Select JVM Node`,
+    props<{ request: SelectClusterNodeRequest }>()
+);
+export const selectFlowFileStorageNode = createAction(
+    `${CLUSTER_LISTING_PREFIX} Select FlowFile Storage Node`,
+    props<{ request: SelectClusterNodeRequest }>()
+);
+export const selectContentStorageNode = createAction(
+    `${CLUSTER_LISTING_PREFIX} Select Content Storage Node`,
+    props<{ request: SelectClusterNodeRequest }>()
+);
+export const selectProvenanceStorageNode = createAction(
+    `${CLUSTER_LISTING_PREFIX} Select Provenance Storage Node`,
+    props<{ request: SelectClusterNodeRequest }>()
+);
+export const selectVersionNode = createAction(
+    `${CLUSTER_LISTING_PREFIX} Select Version Node`,
+    props<{ request: SelectClusterNodeRequest }>()
+);
+
+export const clearClusterNodeSelection = createAction(`${CLUSTER_LISTING_PREFIX} Clear Cluster Node Selection`);
+export const clearSystemNodeSelection = createAction(`${CLUSTER_LISTING_PREFIX} Clear System Node Selection`);
+export const clearJvmNodeSelection = createAction(`${CLUSTER_LISTING_PREFIX} Clear JVM Node Selection`);
+export const clearFlowFileStorageNodeSelection = createAction(
+    `${CLUSTER_LISTING_PREFIX} Clear FlowFile Storage Node Selection`
+);
+export const clearContentStorageNodeSelection = createAction(
+    `${CLUSTER_LISTING_PREFIX} Clear Content Storage Node Selection`
+);
+export const clearProvenanceStorageNodeSelection = createAction(
+    `${CLUSTER_LISTING_PREFIX} Clear Provenance Storage Node Selection`
+);
+export const clearVersionsNodeSelection = createAction(`${CLUSTER_LISTING_PREFIX} Clear Versions Node Selection`);
+
+export const showClusterNodeDetails = createAction(
+    `${CLUSTER_LISTING_PREFIX} Show Cluster Node Details`,
+    props<{ request: ClusterNode }>()
+);
+
+export const navigateToClusterNodeListing = createAction(`${CLUSTER_LISTING_PREFIX} Navigate to Cluster Node Listing`);
+export const navigateHome = createAction(`${CLUSTER_LISTING_PREFIX} Navigate to Home`);
+
+export const resetClusterState = createAction(`${CLUSTER_LISTING_PREFIX} Reset Cluster State`);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/cluster-listing/cluster-listing.effects.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/cluster-listing/cluster-listing.effects.ts
new file mode 100644
index 0000000..96bfa32
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/cluster-listing/cluster-listing.effects.ts
@@ -0,0 +1,339 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Injectable } from '@angular/core';
+import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
+import { ActionCreator, Creator, Store } from '@ngrx/store';
+import { NiFiState } from '../../../../state';
+import { ErrorHelper } from '../../../../service/error-helper.service';
+import { Router } from '@angular/router';
+import * as ClusterListingActions from './cluster-listing.actions';
+import { catchError, from, map, of, switchMap, take, tap } from 'rxjs';
+import { SystemDiagnosticsService } from '../../../../service/system-diagnostics.service';
+import { HttpErrorResponse } from '@angular/common/http';
+import { selectClusterListingStatus } from './cluster-listing.selectors';
+import { reloadSystemDiagnostics } from '../../../../state/system-diagnostics/system-diagnostics.actions';
+import { ClusterService } from '../../service/cluster.service';
+import { MatDialog } from '@angular/material/dialog';
+import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component';
+import { LARGE_DIALOG, MEDIUM_DIALOG, SMALL_DIALOG } from '../../../../index';
+import { ClusterNodeDetailDialog } from '../../ui/cluster-node-listing/cluster-node-detail-dialog/cluster-node-detail-dialog.component';
+import * as ErrorActions from '../../../../state/error/error.actions';
+import { SelectClusterNodeRequest } from './index';
+import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
+
+@Injectable()
+export class ClusterListingEffects {
+    constructor(
+        private actions$: Actions,
+        private store: Store<NiFiState>,
+        private errorHelper: ErrorHelper,
+        private router: Router,
+        private systemDiagnosticsService: SystemDiagnosticsService,
+        private clusterService: ClusterService,
+        private dialog: MatDialog
+    ) {}
+
+    loadClusterListing$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ClusterListingActions.loadClusterListing),
+            concatLatestFrom(() => this.store.select(selectCurrentUser)),
+            tap(([, currentUser]) => {
+                if (currentUser.systemPermissions.canRead) {
+                    this.store.dispatch(reloadSystemDiagnostics({ request: { nodewise: true } }));
+                }
+            }),
+            concatLatestFrom(() => [this.store.select(selectClusterListingStatus)]),
+            switchMap(([, listingStatus]) =>
+                from(this.clusterService.getClusterListing()).pipe(
+                    map((response) => ClusterListingActions.loadClusterListingSuccess({ response: response.cluster })),
+                    catchError((errorResponse: HttpErrorResponse) =>
+                        of(this.errorHelper.handleLoadingError(listingStatus, errorResponse))
+                    )
+                )
+            )
+        )
+    );
+
+    confirmAndDisconnectNode$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(ClusterListingActions.confirmAndDisconnectNode),
+                map((action) => action.request),
+                tap((request) => {
+                    const nodeAddress = `${request.address}:${request.apiPort}`;
+                    const dialogRef = this.dialog.open(YesNoDialog, {
+                        ...SMALL_DIALOG,
+                        data: {
+                            title: 'Disconnect Node',
+                            message: `Disconnect '${nodeAddress}' from the cluster?`
+                        }
+                    });
+                    dialogRef.componentInstance.yes.pipe(take(1)).subscribe(() => {
+                        this.store.dispatch(ClusterListingActions.disconnectNode({ request }));
+                    });
+                })
+            ),
+        { dispatch: false }
+    );
+
+    disconnectNode$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ClusterListingActions.disconnectNode),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(this.clusterService.disconnectNode(request.nodeId)).pipe(
+                    map((entity) => {
+                        return ClusterListingActions.updateNodeSuccess({ response: entity });
+                    }),
+                    catchError((errorResponse: HttpErrorResponse) => {
+                        return of(ClusterListingActions.clusterNodeSnackbarError({ error: errorResponse.error }));
+                    })
+                )
+            )
+        )
+    );
+
+    confirmAndConnectNode$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(ClusterListingActions.confirmAndConnectNode),
+                map((action) => action.request),
+                tap((request) => {
+                    const nodeAddress = `${request.address}:${request.apiPort}`;
+                    const dialogRef = this.dialog.open(YesNoDialog, {
+                        ...SMALL_DIALOG,
+                        data: {
+                            title: 'Connect Node',
+                            message: `Connect '${nodeAddress}' to the cluster?`
+                        }
+                    });
+                    dialogRef.componentInstance.yes.pipe(take(1)).subscribe(() => {
+                        this.store.dispatch(ClusterListingActions.connectNode({ request }));
+                    });
+                })
+            ),
+        { dispatch: false }
+    );
+
+    connectNode$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ClusterListingActions.connectNode),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(this.clusterService.connectNode(request.nodeId)).pipe(
+                    map((entity) => {
+                        return ClusterListingActions.updateNodeSuccess({ response: entity });
+                    }),
+                    catchError((errorResponse: HttpErrorResponse) => {
+                        return of(ClusterListingActions.clusterNodeSnackbarError({ error: errorResponse.error }));
+                    })
+                )
+            )
+        )
+    );
+
+    confirmAndOffloadNode$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(ClusterListingActions.confirmAndOffloadNode),
+                map((action) => action.request),
+                tap((request) => {
+                    const nodeAddress = `${request.address}:${request.apiPort}`;
+                    const dialogRef = this.dialog.open(YesNoDialog, {
+                        ...SMALL_DIALOG,
+                        data: {
+                            title: 'Offload Node',
+                            message: `Offload '${nodeAddress}'?`
+                        }
+                    });
+                    dialogRef.componentInstance.yes.pipe(take(1)).subscribe(() => {
+                        this.store.dispatch(ClusterListingActions.offloadNode({ request }));
+                    });
+                })
+            ),
+        { dispatch: false }
+    );
+
+    offloadNode$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ClusterListingActions.offloadNode),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(this.clusterService.offloadNode(request.nodeId)).pipe(
+                    map((entity) => {
+                        return ClusterListingActions.updateNodeSuccess({ response: entity });
+                    }),
+                    catchError((errorResponse: HttpErrorResponse) => {
+                        return of(ClusterListingActions.clusterNodeSnackbarError({ error: errorResponse.error }));
+                    })
+                )
+            )
+        )
+    );
+
+    confirmAndRemoveNode$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(ClusterListingActions.confirmAndRemoveNode),
+                map((action) => action.request),
+                tap((request) => {
+                    const nodeAddress = `${request.address}:${request.apiPort}`;
+                    const dialogRef = this.dialog.open(YesNoDialog, {
+                        ...SMALL_DIALOG,
+                        data: {
+                            title: 'Remove Node',
+                            message: `Remove '${nodeAddress}' from the cluster?`
+                        }
+                    });
+                    dialogRef.componentInstance.yes.pipe(take(1)).subscribe(() => {
+                        this.store.dispatch(ClusterListingActions.removeNode({ request }));
+                    });
+                })
+            ),
+        { dispatch: false }
+    );
+
+    removeNode$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ClusterListingActions.removeNode),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(this.clusterService.removeNode(request.nodeId)).pipe(
+                    map(() => {
+                        return ClusterListingActions.removeNodeSuccess({ response: request });
+                    }),
+                    catchError((errorResponse: HttpErrorResponse) => {
+                        return of(ClusterListingActions.clusterNodeSnackbarError({ error: errorResponse.error }));
+                    })
+                )
+            )
+        )
+    );
+
+    private selectClusterNode = (
+        action: ActionCreator<any, Creator<any, { request: SelectClusterNodeRequest }>>,
+        path: string
+    ) =>
+        createEffect(
+            () =>
+                this.actions$.pipe(
+                    ofType(action),
+                    map((action) => action.request),
+                    tap((request) => {
+                        if (request.repository) {
+                            this.router.navigate(['/cluster', path, request.id, request.repository]);
+                        } else {
+                            this.router.navigate(['/cluster', path, request.id]);
+                        }
+                    })
+                ),
+            { dispatch: false }
+        );
+
+    selectClusterNode$ = this.selectClusterNode(ClusterListingActions.selectClusterNode, 'nodes');
+    selectSystemNode$ = this.selectClusterNode(ClusterListingActions.selectSystemNode, 'system');
+    selectJvmNode$ = this.selectClusterNode(ClusterListingActions.selectJvmNode, 'jvm');
+    selectFlowFileStorageNode$ = this.selectClusterNode(
+        ClusterListingActions.selectFlowFileStorageNode,
+        'flowfile-storage'
+    );
+    selectContentStorageNode$ = this.selectClusterNode(
+        ClusterListingActions.selectContentStorageNode,
+        'content-storage'
+    );
+    selectProvenanceStorageNode$ = this.selectClusterNode(
+        ClusterListingActions.selectProvenanceStorageNode,
+        'provenance-storage'
+    );
+    selectVersionNode$ = this.selectClusterNode(ClusterListingActions.selectVersionNode, 'versions');
+
+    private clearNodeSelection = (action: ActionCreator, path: string) =>
+        createEffect(
+            () =>
+                this.actions$.pipe(
+                    ofType(action),
+                    tap(() => {
+                        this.router.navigate(['/cluster', path]);
+                    })
+                ),
+            { dispatch: false }
+        );
+
+    clearClusterNodeSelection$ = this.clearNodeSelection(ClusterListingActions.clearClusterNodeSelection, 'nodes');
+    clearSystemNodeSelection$ = this.clearNodeSelection(ClusterListingActions.clearSystemNodeSelection, 'system');
+    clearJvmNodeSelection$ = this.clearNodeSelection(ClusterListingActions.clearJvmNodeSelection, 'jvm');
+    clearFlowFileStorageNodeSelection$ = this.clearNodeSelection(
+        ClusterListingActions.clearFlowFileStorageNodeSelection,
+        'flowfile-storage'
+    );
+    clearContentStorageNodeSelection$ = this.clearNodeSelection(
+        ClusterListingActions.clearContentStorageNodeSelection,
+        'content-storage'
+    );
+    clearProvenanceStorageNodeSelection$ = this.clearNodeSelection(
+        ClusterListingActions.clearProvenanceStorageNodeSelection,
+        'provenance-storage'
+    );
+    clearVersionsNodeSelection$ = this.clearNodeSelection(ClusterListingActions.clearVersionsNodeSelection, 'versions');
+
+    showClusterNodeDetails$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(ClusterListingActions.showClusterNodeDetails),
+                map((action) => action.request),
+                tap((request) => {
+                    const dimensions = request.events.length > 0 ? LARGE_DIALOG : MEDIUM_DIALOG;
+                    this.dialog.open(ClusterNodeDetailDialog, {
+                        ...dimensions,
+                        data: request
+                    });
+                })
+            ),
+        { dispatch: false }
+    );
+
+    clusterNodeSnackbarError$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(ClusterListingActions.clusterNodeSnackbarError),
+            map((action) => action.error),
+            switchMap((errorResponse) => of(ErrorActions.snackBarError({ error: errorResponse })))
+        )
+    );
+
+    navigateToClusterNodeListing$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(ClusterListingActions.navigateToClusterNodeListing),
+                tap(() => {
+                    this.router.navigate(['/cluster']);
+                })
+            ),
+        { dispatch: false }
+    );
+
+    navigateHome$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(ClusterListingActions.navigateHome),
+                tap(() => {
+                    this.router.navigate(['/']);
+                })
+            ),
+        { dispatch: false }
+    );
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/cluster-listing/cluster-listing.reducer.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/cluster-listing/cluster-listing.reducer.ts
new file mode 100644
index 0000000..aac1ede
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/cluster-listing/cluster-listing.reducer.ts
@@ -0,0 +1,82 @@
+/*
+ * 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 { ClusterListingState } from './index';
+import { createReducer, on } from '@ngrx/store';
+import {
+    connectNode,
+    disconnectNode,
+    loadClusterListing,
+    loadClusterListingSuccess,
+    removeNode,
+    removeNodeSuccess,
+    resetClusterState,
+    updateNodeSuccess
+} from './cluster-listing.actions';
+import { produce } from 'immer';
+
+export const initialClusterState: ClusterListingState = {
+    nodes: [],
+    loadedTimestamp: '',
+    status: 'pending',
+    saving: false
+};
+
+export const clusterListingReducer = createReducer(
+    initialClusterState,
+
+    on(loadClusterListing, (state) => ({
+        ...state,
+        status: 'loading' as const
+    })),
+
+    on(resetClusterState, () => ({
+        ...initialClusterState
+    })),
+
+    on(loadClusterListingSuccess, (state, { response }) => ({
+        ...state,
+        loadedTimestamp: response.generated,
+        nodes: response.nodes,
+        status: 'success' as const
+    })),
+
+    on(disconnectNode, connectNode, removeNode, (state) => ({
+        ...state,
+        saving: true
+    })),
+
+    on(updateNodeSuccess, (state, { response }) => {
+        return produce(state, (draftState) => {
+            const index = draftState.nodes.findIndex((node) => response.node.nodeId === node.nodeId);
+            if (index > -1) {
+                draftState.nodes[index] = response.node;
+            }
+            draftState.saving = false;
+        });
+    }),
+
+    on(removeNodeSuccess, (state, { response }) => {
+        return produce(state, (draftState) => {
+            const index = draftState.nodes.findIndex((node) => response.nodeId === node.nodeId);
+            if (index > -1) {
+                draftState.nodes.splice(index, 1);
+            }
+            draftState.saving = false;
+        });
+    })
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/cluster-listing/cluster-listing.selectors.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/cluster-listing/cluster-listing.selectors.ts
new file mode 100644
index 0000000..7e090d3
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/cluster-listing/cluster-listing.selectors.ts
@@ -0,0 +1,55 @@
+/*
+ * 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 { ClusterState, selectClusterState } from '../index';
+import { clusterListingFeatureKey, ClusterListingState } from './index';
+import { selectCurrentRoute } from '../../../../state/router/router.selectors';
+
+export const selectClusterListing = createSelector(
+    selectClusterState,
+    (state: ClusterState) => state[clusterListingFeatureKey]
+);
+
+export const selectClusterListingStatus = createSelector(
+    selectClusterListing,
+    (state: ClusterListingState) => state.status
+);
+
+export const selectClusterListingLoadedTimestamp = createSelector(
+    selectClusterListing,
+    (state: ClusterListingState) => state.loadedTimestamp
+);
+
+export const selectClusterListingNodes = createSelector(
+    selectClusterListing,
+    (state: ClusterListingState) => state.nodes
+);
+
+export const selectClusterNodeIdFromRoute = createSelector(selectCurrentRoute, (route) => {
+    if (route) {
+        return route.params.id;
+    }
+    return null;
+});
+
+export const selectClusterStorageRepositoryIdFromRoute = createSelector(selectCurrentRoute, (route) => {
+    if (route) {
+        return route.params.repo;
+    }
+    return null;
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/cluster-listing/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/cluster-listing/index.ts
new file mode 100644
index 0000000..668a3ca
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/cluster-listing/index.ts
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const clusterListingFeatureKey = 'clusterListing';
+
+export interface ClusterListingState {
+    nodes: ClusterNode[];
+    loadedTimestamp: string;
+    saving: boolean;
+    status: 'pending' | 'loading' | 'success';
+}
+
+export interface ClusterNodeEntity {
+    node: ClusterNode;
+}
+
+export interface ClusterNode {
+    nodeId: string;
+    address: string;
+    apiPort: number;
+    status: string;
+    roles: string[];
+    events: ClusterNodeEvent[];
+    activeThreadCount?: number;
+    queued?: string;
+    bytesQueued?: number;
+    flowFilesQueued?: number;
+    heartbeat?: string;
+    connectionRequested?: string;
+    nodeStartTime?: string;
+}
+
+export interface ClusterNodeEvent {
+    timestamp: string;
+    category: string;
+    message: string;
+}
+
+export interface ClusterListingResponse {
+    cluster: ClusterListingEntity;
+}
+
+export interface ClusterListingEntity {
+    nodes: ClusterNode[];
+    generated: string;
+}
+
+export interface SelectClusterNodeRequest {
+    id: string;
+    repository?: string;
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/index.ts
new file mode 100644
index 0000000..eabb748
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/state/index.ts
@@ -0,0 +1,34 @@
+/*
+ * 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 { Action, combineReducers, createFeatureSelector } from '@ngrx/store';
+import { clusterListingFeatureKey, ClusterListingState } from './cluster-listing';
+import { clusterListingReducer } from './cluster-listing/cluster-listing.reducer';
+
+export const clusterFeatureKey = 'cluster';
+
+export interface ClusterState {
+    [clusterListingFeatureKey]: ClusterListingState;
+}
+
+export function reducers(state: ClusterState | undefined, action: Action) {
+    return combineReducers({
+        [clusterListingFeatureKey]: clusterListingReducer
+    })(state, action);
+}
+
+export const selectClusterState = createFeatureSelector<ClusterState>(clusterFeatureKey);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-content-storage-listing/cluster-content-storage-listing.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-content-storage-listing/cluster-content-storage-listing.component.html
new file mode 100644
index 0000000..66feaf7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-content-storage-listing/cluster-content-storage-listing.component.html
@@ -0,0 +1,36 @@
+<!--
+  ~ 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.
+  -->
+
+<ng-container>
+    @if (isInitialLoading(loadedTimestamp())) {
+        <div>
+            <ngx-skeleton-loader count="3"></ngx-skeleton-loader>
+        </div>
+    } @else {
+        <repository-storage-table
+            [selectedId]="selectedClusterNodeId()"
+            [selectedRepositoryId]="selectedClusterRepoId()"
+            (clearSelection)="clearSelection()"
+            (selectComponent)="selectStorageNode($event)"
+            [loadedTimestamp]="loadedTimestamp()"
+            [listingStatus]="listingStatus()"
+            [components]="(components$ | async) || []"
+            [showRepoIdentifier]="true"
+            initialSortColumn="address"
+            initialSortDirection="asc"></repository-storage-table>
+    }
+</ng-container>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-content-storage-listing/cluster-content-storage-listing.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-content-storage-listing/cluster-content-storage-listing.component.scss
new file mode 100644
index 0000000..b33f7ca
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-content-storage-listing/cluster-content-storage-listing.component.scss
@@ -0,0 +1,16 @@
+/*!
+ * 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.
+ */
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-content-storage-listing/cluster-content-storage-listing.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-content-storage-listing/cluster-content-storage-listing.component.spec.ts
new file mode 100644
index 0000000..1b99acb
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-content-storage-listing/cluster-content-storage-listing.component.spec.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 { ClusterContentStorageListing } from './cluster-content-storage-listing.component';
+import { ClusterState } from '../../state';
+import { clusterListingFeatureKey } from '../../state/cluster-listing';
+import { initialClusterState } from '../../state/cluster-listing/cluster-listing.reducer';
+import { provideMockStore } from '@ngrx/store/testing';
+import { selectClusterListing } from '../../state/cluster-listing/cluster-listing.selectors';
+
+describe('ClusterContentStorageListing', () => {
+    let component: ClusterContentStorageListing;
+    let fixture: ComponentFixture<ClusterContentStorageListing>;
+
+    beforeEach(async () => {
+        const initialState: ClusterState = {
+            [clusterListingFeatureKey]: initialClusterState
+        };
+        await TestBed.configureTestingModule({
+            imports: [ClusterContentStorageListing],
+            providers: [
+                provideMockStore({
+                    initialState,
+                    selectors: [
+                        {
+                            selector: selectClusterListing,
+                            value: initialState[clusterListingFeatureKey]
+                        }
+                    ]
+                })
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ClusterContentStorageListing);
+        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/cluster/ui/cluster-content-storage-listing/cluster-content-storage-listing.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-content-storage-listing/cluster-content-storage-listing.component.ts
new file mode 100644
index 0000000..249b118
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-content-storage-listing/cluster-content-storage-listing.component.ts
@@ -0,0 +1,92 @@
+/*
+ * 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 { AsyncPipe } from '@angular/common';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import { RepositoryStorageTable } from '../common/repository-storage-table/repository-storage-table.component';
+import {
+    selectClusterListingLoadedTimestamp,
+    selectClusterListingStatus,
+    selectClusterNodeIdFromRoute,
+    selectClusterStorageRepositoryIdFromRoute
+} from '../../state/cluster-listing/cluster-listing.selectors';
+import { selectSystemNodeSnapshots } from '../../../../state/system-diagnostics/system-diagnostics.selectors';
+import { isDefinedAndNotNull } from '../../../../state/shared';
+import { map } from 'rxjs';
+import { ClusterNodeRepositoryStorageUsage } from '../../../../state/system-diagnostics';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../../state';
+import { initialClusterState } from '../../state/cluster-listing/cluster-listing.reducer';
+import {
+    clearContentStorageNodeSelection,
+    selectContentStorageNode
+} from '../../state/cluster-listing/cluster-listing.actions';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+
+@Component({
+    selector: 'cluster-content-storage-listing',
+    standalone: true,
+    imports: [AsyncPipe, NgxSkeletonLoaderModule, RepositoryStorageTable],
+    templateUrl: './cluster-content-storage-listing.component.html',
+    styleUrl: './cluster-content-storage-listing.component.scss'
+})
+export class ClusterContentStorageListing {
+    loadedTimestamp = this.store.selectSignal(selectClusterListingLoadedTimestamp);
+    listingStatus = this.store.selectSignal(selectClusterListingStatus);
+    selectedClusterNodeId = this.store.selectSignal(selectClusterNodeIdFromRoute);
+    selectedClusterRepoId = this.store.selectSignal(selectClusterStorageRepositoryIdFromRoute);
+    components$ = this.store.select(selectSystemNodeSnapshots).pipe(
+        takeUntilDestroyed(),
+        isDefinedAndNotNull(),
+        map((clusterNodes) => {
+            const expanded: ClusterNodeRepositoryStorageUsage[] = [];
+            return clusterNodes.reduce((acc, node) => {
+                const repos = node.snapshot.contentRepositoryStorageUsage.map((storage) => {
+                    return {
+                        address: node.address,
+                        apiPort: node.apiPort,
+                        nodeId: node.nodeId,
+                        repositoryStorageUsage: storage
+                    };
+                });
+                return [...acc, ...repos];
+            }, expanded);
+        })
+    );
+
+    constructor(private store: Store<NiFiState>) {}
+
+    isInitialLoading(loadedTimestamp: string): boolean {
+        return loadedTimestamp == initialClusterState.loadedTimestamp;
+    }
+
+    selectStorageNode(node: ClusterNodeRepositoryStorageUsage): void {
+        this.store.dispatch(
+            selectContentStorageNode({
+                request: {
+                    id: node.nodeId,
+                    repository: node.repositoryStorageUsage.identifier
+                }
+            })
+        );
+    }
+
+    clearSelection(): void {
+        this.store.dispatch(clearContentStorageNodeSelection());
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-flow-file-storage-listing/cluster-flow-file-storage-listing.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-flow-file-storage-listing/cluster-flow-file-storage-listing.component.html
new file mode 100644
index 0000000..7276ea2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-flow-file-storage-listing/cluster-flow-file-storage-listing.component.html
@@ -0,0 +1,34 @@
+<!--
+  ~ 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.
+  -->
+
+<ng-container>
+    @if (isInitialLoading(loadedTimestamp())) {
+        <div>
+            <ngx-skeleton-loader count="3"></ngx-skeleton-loader>
+        </div>
+    } @else {
+        <repository-storage-table
+            [selectedId]="selectedClusterNodeId()"
+            (clearSelection)="clearSelection()"
+            (selectComponent)="selectStorageNode($event)"
+            [loadedTimestamp]="loadedTimestamp()"
+            [listingStatus]="listingStatus()"
+            [components]="(components$ | async) || []"
+            initialSortColumn="address"
+            initialSortDirection="asc"></repository-storage-table>
+    }
+</ng-container>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-flow-file-storage-listing/cluster-flow-file-storage-listing.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-flow-file-storage-listing/cluster-flow-file-storage-listing.component.scss
new file mode 100644
index 0000000..b33f7ca
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-flow-file-storage-listing/cluster-flow-file-storage-listing.component.scss
@@ -0,0 +1,16 @@
+/*!
+ * 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.
+ */
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-flow-file-storage-listing/cluster-flow-file-storage-listing.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-flow-file-storage-listing/cluster-flow-file-storage-listing.component.spec.ts
new file mode 100644
index 0000000..e0bb7d5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-flow-file-storage-listing/cluster-flow-file-storage-listing.component.spec.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 { ClusterFlowFileStorageListing } from './cluster-flow-file-storage-listing.component';
+import { ClusterState } from '../../state';
+import { clusterListingFeatureKey } from '../../state/cluster-listing';
+import { initialClusterState } from '../../state/cluster-listing/cluster-listing.reducer';
+import { provideMockStore } from '@ngrx/store/testing';
+import { selectClusterListing } from '../../state/cluster-listing/cluster-listing.selectors';
+
+describe('ClusterFlowFileStorageListing', () => {
+    let component: ClusterFlowFileStorageListing;
+    let fixture: ComponentFixture<ClusterFlowFileStorageListing>;
+
+    beforeEach(async () => {
+        const initialState: ClusterState = {
+            [clusterListingFeatureKey]: initialClusterState
+        };
+        await TestBed.configureTestingModule({
+            imports: [ClusterFlowFileStorageListing],
+            providers: [
+                provideMockStore({
+                    initialState,
+                    selectors: [
+                        {
+                            selector: selectClusterListing,
+                            value: initialState[clusterListingFeatureKey]
+                        }
+                    ]
+                })
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ClusterFlowFileStorageListing);
+        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/cluster/ui/cluster-flow-file-storage-listing/cluster-flow-file-storage-listing.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-flow-file-storage-listing/cluster-flow-file-storage-listing.component.ts
new file mode 100644
index 0000000..91f338e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-flow-file-storage-listing/cluster-flow-file-storage-listing.component.ts
@@ -0,0 +1,79 @@
+/*
+ * 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 { RepositoryStorageTable } from '../common/repository-storage-table/repository-storage-table.component';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import {
+    selectClusterListingLoadedTimestamp,
+    selectClusterListingStatus,
+    selectClusterNodeIdFromRoute
+} from '../../state/cluster-listing/cluster-listing.selectors';
+import { selectSystemNodeSnapshots } from '../../../../state/system-diagnostics/system-diagnostics.selectors';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../../state';
+import { initialClusterState } from '../../state/cluster-listing/cluster-listing.reducer';
+import { ClusterNodeRepositoryStorageUsage } from '../../../../state/system-diagnostics';
+import { map } from 'rxjs';
+import { isDefinedAndNotNull } from '../../../../state/shared';
+import { AsyncPipe } from '@angular/common';
+import {
+    clearFlowFileStorageNodeSelection,
+    selectFlowFileStorageNode
+} from '../../state/cluster-listing/cluster-listing.actions';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+
+@Component({
+    selector: 'cluster-flow-file-storage-listing',
+    standalone: true,
+    imports: [RepositoryStorageTable, NgxSkeletonLoaderModule, AsyncPipe],
+    templateUrl: './cluster-flow-file-storage-listing.component.html',
+    styleUrl: './cluster-flow-file-storage-listing.component.scss'
+})
+export class ClusterFlowFileStorageListing {
+    loadedTimestamp = this.store.selectSignal(selectClusterListingLoadedTimestamp);
+    listingStatus = this.store.selectSignal(selectClusterListingStatus);
+    selectedClusterNodeId = this.store.selectSignal(selectClusterNodeIdFromRoute);
+    components$ = this.store.select(selectSystemNodeSnapshots).pipe(
+        takeUntilDestroyed(),
+        isDefinedAndNotNull(),
+        map((clusterNodes) => {
+            return clusterNodes.map((node) => {
+                return {
+                    address: node.address,
+                    apiPort: node.apiPort,
+                    nodeId: node.nodeId,
+                    repositoryStorageUsage: node.snapshot.flowFileRepositoryStorageUsage
+                } as ClusterNodeRepositoryStorageUsage;
+            });
+        })
+    );
+
+    constructor(private store: Store<NiFiState>) {}
+
+    isInitialLoading(loadedTimestamp: string): boolean {
+        return loadedTimestamp == initialClusterState.loadedTimestamp;
+    }
+
+    selectStorageNode(node: ClusterNodeRepositoryStorageUsage): void {
+        this.store.dispatch(selectFlowFileStorageNode({ request: { id: node.nodeId } }));
+    }
+
+    clearSelection(): void {
+        this.store.dispatch(clearFlowFileStorageNodeSelection());
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-listing.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-listing.component.html
new file mode 100644
index 0000000..ea7c030
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-listing.component.html
@@ -0,0 +1,34 @@
+<!--
+  ~ 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.
+  -->
+
+<ng-container>
+    @if (isInitialLoading(loadedTimestamp())) {
+        <div>
+            <ngx-skeleton-loader count="3"></ngx-skeleton-loader>
+        </div>
+    } @else {
+        <cluster-jvm-table
+            [selectedId]="selectedClusterNodeId()"
+            (clearSelection)="clearSelection()"
+            (selectComponent)="selectNode($event)"
+            [loadedTimestamp]="loadedTimestamp()"
+            [listingStatus]="listingStatus()"
+            [components]="nodes() || []"
+            initialSortColumn="address"
+            initialSortDirection="asc"></cluster-jvm-table>
+    }
+</ng-container>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-listing.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-listing.component.scss
new file mode 100644
index 0000000..b33f7ca
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-listing.component.scss
@@ -0,0 +1,16 @@
+/*!
+ * 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.
+ */
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-listing.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-listing.component.spec.ts
new file mode 100644
index 0000000..ed25d2f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-listing.component.spec.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 { ClusterJvmListing } from './cluster-jvm-listing.component';
+import { ClusterState } from '../../state';
+import { clusterListingFeatureKey } from '../../state/cluster-listing';
+import { initialClusterState } from '../../state/cluster-listing/cluster-listing.reducer';
+import { provideMockStore } from '@ngrx/store/testing';
+import { selectClusterListing } from '../../state/cluster-listing/cluster-listing.selectors';
+
+describe('ClusterJvmListing', () => {
+    let component: ClusterJvmListing;
+    let fixture: ComponentFixture<ClusterJvmListing>;
+
+    beforeEach(async () => {
+        const initialState: ClusterState = {
+            [clusterListingFeatureKey]: initialClusterState
+        };
+
+        await TestBed.configureTestingModule({
+            imports: [ClusterJvmListing],
+            providers: [
+                provideMockStore({
+                    initialState,
+                    selectors: [
+                        {
+                            selector: selectClusterListing,
+                            value: initialState[clusterListingFeatureKey]
+                        }
+                    ]
+                })
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ClusterJvmListing);
+        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/cluster/ui/cluster-jvm-listing/cluster-jvm-listing.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-listing.component.ts
new file mode 100644
index 0000000..95efb3c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-listing.component.ts
@@ -0,0 +1,60 @@
+/*
+ * 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 { ClusterSystemTable } from '../cluster-system-listing/cluster-system-table/cluster-system-table.component';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import { ClusterJvmTable } from './cluster-jvm-table/cluster-jvm-table.component';
+import {
+    selectClusterListingLoadedTimestamp,
+    selectClusterListingStatus,
+    selectClusterNodeIdFromRoute
+} from '../../state/cluster-listing/cluster-listing.selectors';
+import { selectSystemNodeSnapshots } from '../../../../state/system-diagnostics/system-diagnostics.selectors';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../../state';
+import { initialClusterState } from '../../state/cluster-listing/cluster-listing.reducer';
+import { NodeSnapshot } from '../../../../state/system-diagnostics';
+import { clearJvmNodeSelection, selectJvmNode } from '../../state/cluster-listing/cluster-listing.actions';
+
+@Component({
+    selector: 'cluster-jvm-listing',
+    standalone: true,
+    imports: [ClusterSystemTable, NgxSkeletonLoaderModule, ClusterJvmTable],
+    templateUrl: './cluster-jvm-listing.component.html',
+    styleUrl: './cluster-jvm-listing.component.scss'
+})
+export class ClusterJvmListing {
+    loadedTimestamp = this.store.selectSignal(selectClusterListingLoadedTimestamp);
+    listingStatus = this.store.selectSignal(selectClusterListingStatus);
+    selectedClusterNodeId = this.store.selectSignal(selectClusterNodeIdFromRoute);
+    nodes = this.store.selectSignal(selectSystemNodeSnapshots);
+
+    constructor(private store: Store<NiFiState>) {}
+
+    isInitialLoading(loadedTimestamp: string): boolean {
+        return loadedTimestamp == initialClusterState.loadedTimestamp;
+    }
+
+    selectNode(node: NodeSnapshot): void {
+        this.store.dispatch(selectJvmNode({ request: { id: node.nodeId } }));
+    }
+
+    clearSelection(): void {
+        this.store.dispatch(clearJvmNodeSelection());
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-table/cluster-jvm-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-table/cluster-jvm-table.component.html
new file mode 100644
index 0000000..6ef95f7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-table/cluster-jvm-table.component.html
@@ -0,0 +1,151 @@
+<!--
+  ~  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">
+    <div class="flex-1">
+        <ng-container>
+            <div class="jvm-node-table h-full flex flex-col">
+                <!-- allow filtering of the table -->
+                <cluster-table-filter
+                    [filteredCount]="filteredCount"
+                    [totalCount]="totalCount"
+                    [filterableColumns]="filterableColumns"
+                    filterColumn="address"
+                    (filterChanged)="applyFilter($event)"></cluster-table-filter>
+
+                <div class="flex-1 relative">
+                    <div class="listing-table overflow-y-auto absolute inset-0">
+                        <table
+                            mat-table
+                            [dataSource]="dataSource"
+                            matSort
+                            matSortDisableClear
+                            (matSortChange)="sortData($event)"
+                            [matSortActive]="initialSortColumn"
+                            [matSortDirection]="initialSortDirection">
+                            <!-- Node Address -->
+                            <ng-container matColumnDef="address">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Node Address">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Node Address</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="formatNodeAddress(item)">
+                                    {{ formatNodeAddress(item) }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Max Heap -->
+                            <ng-container matColumnDef="maxHeap">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Heap Max">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Heap Max</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.maxHeap">
+                                    {{ item.snapshot.maxHeap }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Total Heap -->
+                            <ng-container matColumnDef="totalHeap">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Heap Total">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Heap Total</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.totalHeap">
+                                    {{ item.snapshot.totalHeap }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Used Heap -->
+                            <ng-container matColumnDef="usedHeap">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Head Used">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Heap Used</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.usedHeap">
+                                    {{ item.snapshot.usedHeap }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Heap Utilization -->
+                            <ng-container matColumnDef="heapUtilization">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Heap Utilization">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">
+                                        Heap Utilization
+                                    </div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.heapUtilization">
+                                    {{ item.snapshot.heapUtilization }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Total Non Heap -->
+                            <ng-container matColumnDef="totalNonHeap">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Non-Heap Total">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">
+                                        Non-Heap Total
+                                    </div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.totalNonHeap">
+                                    {{ item.snapshot.totalNonHeap }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Used Heap -->
+                            <ng-container matColumnDef="usedNonHeap">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Non-Heap Used">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Non-Heap Used</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.usedNonHeap">
+                                    {{ item.snapshot.usedNonHeap }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Garbage Collection -->
+                            <ng-container matColumnDef="garbageCollection">
+                                <th mat-header-cell *matHeaderCellDef title="Garbage Collection">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">GC</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item">
+                                    <div
+                                        class="fa fa-info-circle"
+                                        nifiTooltip
+                                        [delayClose]="false"
+                                        [position]="getGcTooltipPosition()"
+                                        [tooltipComponentType]="GarbageCollectionTipComponent"
+                                        [tooltipInputData]="getGarbageCollectionTipData(item)"></div>
+                                </td>
+                            </ng-container>
+
+                            <!-- Uptime -->
+                            <ng-container matColumnDef="uptime">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Uptime">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Uptime</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.uptime">
+                                    {{ item.snapshot.uptime }}
+                                </td>
+                            </ng-container>
+                            <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
+                            <tr
+                                mat-row
+                                *matRowDef="let row; let even = even; columns: displayedColumns"
+                                [class.even]="even"
+                                (click)="select(row)"
+                                [class.selected]="isSelected(row)"></tr>
+                        </table>
+                    </div>
+                </div>
+            </div>
+        </ng-container>
+    </div>
+</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-table/cluster-jvm-table.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-table/cluster-jvm-table.component.scss
new file mode 100644
index 0000000..48b285a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-table/cluster-jvm-table.component.scss
@@ -0,0 +1,29 @@
+/*!
+ * 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.
+ */
+
+.jvm-node-table {
+    .listing-table {
+        table {
+            .mat-column-address {
+                width: 25%;
+            }
+            .mat-column-garbageCollection {
+                width: 40px;
+            }
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-table/cluster-jvm-table.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-table/cluster-jvm-table.component.spec.ts
new file mode 100644
index 0000000..b10ad52
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-table/cluster-jvm-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 { ClusterJvmTable } from './cluster-jvm-table.component';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+
+describe('ClusterJvmTable', () => {
+    let component: ClusterJvmTable;
+    let fixture: ComponentFixture<ClusterJvmTable>;
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [ClusterJvmTable, NoopAnimationsModule]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ClusterJvmTable);
+        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/cluster/ui/cluster-jvm-listing/cluster-jvm-table/cluster-jvm-table.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-table/cluster-jvm-table.component.ts
new file mode 100644
index 0000000..2170e42
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-jvm-listing/cluster-jvm-table/cluster-jvm-table.component.ts
@@ -0,0 +1,160 @@
+/*
+ * 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 { ClusterTable } from '../../common/cluster-table/cluster-table.component';
+import { NodeSnapshot } from '../../../../../state/system-diagnostics';
+import {
+    ClusterTableFilter,
+    ClusterTableFilterColumn
+} from '../../common/cluster-table-filter/cluster-table-filter.component';
+import { NiFiCommon } from '../../../../../service/nifi-common.service';
+import { MatSortModule, Sort } from '@angular/material/sort';
+import { MatTableModule } from '@angular/material/table';
+import { NifiTooltipDirective } from '../../../../../ui/common/tooltips/nifi-tooltip.directive';
+import { GarbageCollectionTipInput } from '../../../../../state/shared';
+import { GarbageCollectionTip } from '../../../../../ui/common/tooltips/garbage-collection-tip/garbage-collection-tip.component';
+import { ConnectedPosition } from '@angular/cdk/overlay';
+
+@Component({
+    selector: 'cluster-jvm-table',
+    standalone: true,
+    imports: [ClusterTableFilter, MatTableModule, MatSortModule, NifiTooltipDirective],
+    templateUrl: './cluster-jvm-table.component.html',
+    styleUrl: './cluster-jvm-table.component.scss'
+})
+export class ClusterJvmTable extends ClusterTable<NodeSnapshot> {
+    filterableColumns: ClusterTableFilterColumn[] = [{ key: 'address', label: 'Address' }];
+
+    displayedColumns: string[] = [
+        'address',
+        'maxHeap',
+        'totalHeap',
+        'usedHeap',
+        'heapUtilization',
+        'totalNonHeap',
+        'usedNonHeap',
+        'garbageCollection',
+        'uptime'
+    ];
+
+    constructor(private nifiCommon: NiFiCommon) {
+        super();
+    }
+
+    formatNodeAddress(item: NodeSnapshot): string {
+        return `${item.address}:${item.apiPort}`;
+    }
+
+    override filterPredicate(item: NodeSnapshot, filter: string): boolean {
+        const { filterTerm, filterColumn } = JSON.parse(filter);
+        if (filterTerm === '') {
+            return true;
+        }
+
+        let field = '';
+        switch (filterColumn) {
+            case 'address':
+                field = this.formatNodeAddress(item);
+                break;
+        }
+        return this.nifiCommon.stringContains(field, filterTerm, true);
+    }
+
+    override sortEntities(data: NodeSnapshot[], sort: Sort): any[] {
+        if (!data) {
+            return [];
+        }
+        return data.slice().sort((a, b) => {
+            const isAsc = sort.direction === 'asc';
+            let retVal = 0;
+            switch (sort.active) {
+                case 'address':
+                    retVal = this.nifiCommon.compareString(a.address, b.address);
+                    // check the port if the addresses are the same
+                    if (retVal === 0) {
+                        retVal = this.nifiCommon.compareNumber(a.apiPort, b.apiPort);
+                    }
+                    break;
+                case 'maxHeap':
+                    retVal = this.nifiCommon.compareNumber(a.snapshot.maxHeapBytes, b.snapshot.maxHeapBytes);
+                    break;
+                case 'totalHeap':
+                    retVal = this.nifiCommon.compareNumber(a.snapshot.totalHeapBytes, b.snapshot.totalHeapBytes);
+                    break;
+                case 'usedHeap':
+                    retVal = this.nifiCommon.compareNumber(a.snapshot.usedHeapBytes, b.snapshot.usedHeapBytes);
+                    break;
+                case 'heapUtilization':
+                    retVal = this.nifiCommon.compareNumber(
+                        a.snapshot.usedHeapBytes / a.snapshot.totalHeapBytes,
+                        b.snapshot.usedHeapBytes / b.snapshot.totalHeapBytes
+                    );
+                    break;
+                case 'totalNonHeap':
+                    retVal = this.nifiCommon.compareNumber(a.snapshot.totalNonHeapBytes, b.snapshot.totalNonHeapBytes);
+                    break;
+                case 'usedNonHeap':
+                    retVal = this.nifiCommon.compareNumber(a.snapshot.usedNonHeapBytes, b.snapshot.usedNonHeapBytes);
+                    break;
+                case 'uptime':
+                    retVal = this.nifiCommon.compareNumber(
+                        this.nifiCommon.parseDuration(a.snapshot.uptime),
+                        this.nifiCommon.parseDuration(b.snapshot.uptime)
+                    );
+                    break;
+                default:
+                    retVal = 0;
+            }
+            return retVal * (isAsc ? 1 : -1);
+        });
+    }
+
+    override supportsMultiValuedSort(): boolean {
+        return false;
+    }
+
+    select(item: NodeSnapshot): any {
+        this.selectComponent.next(item);
+    }
+
+    isSelected(item: NodeSnapshot): boolean {
+        if (this.selectedId) {
+            return this.selectedId === item.nodeId;
+        }
+        return false;
+    }
+
+    getGarbageCollectionTipData(item: NodeSnapshot): GarbageCollectionTipInput {
+        return {
+            garbageCollections: item.snapshot.garbageCollection
+        };
+    }
+
+    getGcTooltipPosition(): ConnectedPosition {
+        return {
+            originX: 'end',
+            originY: 'bottom',
+            overlayX: 'end',
+            overlayY: 'top',
+            offsetX: -8,
+            offsetY: 8
+        };
+    }
+
+    protected readonly GarbageCollectionTipComponent = GarbageCollectionTip;
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-detail-dialog/cluster-node-detail-dialog.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-detail-dialog/cluster-node-detail-dialog.component.html
new file mode 100644
index 0000000..ddbdab7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-detail-dialog/cluster-node-detail-dialog.component.html
@@ -0,0 +1,49 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~    http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<h2 mat-dialog-title>Node Details</h2>
+<mat-dialog-content>
+    <div class="node-details flex flex-col gap-y-4">
+        <div class="flex flex-col">
+            <div>Address</div>
+            <div class="accent-color font-medium">{{ node.address }}:{{ node.apiPort }}</div>
+        </div>
+        <div class="flex flex-col">
+            <div>Node Id</div>
+            <div class="accent-color font-medium">
+                {{ node.nodeId }}
+            </div>
+        </div>
+        <div class="flex flex-col">
+            <div>Node Events</div>
+            @if (node.events && node.events.length > 0) {
+                <div class="accent-color font-medium text-sm">
+                    <ul>
+                        @for (event of node.events; track event) {
+                            <li>{{ event.timestamp }} - {{ event.message }}</li>
+                        }
+                    </ul>
+                </div>
+            } @else {
+                <div class="unset nifi-surface-default">No Events</div>
+            }
+        </div>
+    </div>
+</mat-dialog-content>
+<mat-dialog-actions align="end">
+    <button type="button" color="primary" mat-button mat-dialog-close>Close</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/pages/cluster/ui/cluster-node-listing/cluster-node-detail-dialog/cluster-node-detail-dialog.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-detail-dialog/cluster-node-detail-dialog.component.scss
new file mode 100644
index 0000000..fafce69
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-detail-dialog/cluster-node-detail-dialog.component.scss
@@ -0,0 +1,23 @@
+/*!
+ * 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.
+ */
+
+.node-details {
+    ul {
+        list-style-type: disc;
+        list-style-position: inside;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-detail-dialog/cluster-node-detail-dialog.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-detail-dialog/cluster-node-detail-dialog.component.spec.ts
new file mode 100644
index 0000000..69b67e5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-detail-dialog/cluster-node-detail-dialog.component.spec.ts
@@ -0,0 +1,79 @@
+/*
+ * 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 { ClusterNodeDetailDialog } from './cluster-node-detail-dialog.component';
+import { MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { ClusterNode } from '../../../state/cluster-listing';
+
+describe('ClusterNodeDetailDialog', () => {
+    let component: ClusterNodeDetailDialog;
+    let fixture: ComponentFixture<ClusterNodeDetailDialog>;
+    const data: ClusterNode = {
+        nodeId: '53b34e14-e9bb-455c-ac5d-9dda95b63bb5',
+        address: 'localhost',
+        apiPort: 8443,
+        status: 'CONNECTED',
+        heartbeat: '04/18/2024 11:12:32 EDT',
+        roles: ['Primary Node', 'Cluster Coordinator'],
+        activeThreadCount: 0,
+        queued: '0 / 0 bytes',
+        events: [
+            {
+                timestamp: '04/18/2024 11:11:17 EDT',
+                category: 'INFO',
+                message: 'Received first heartbeat from connecting node. Node connected.'
+            },
+            {
+                timestamp: '04/18/2024 11:11:11 EDT',
+                category: 'INFO',
+                message: 'Connection requested from existing node. Setting status to connecting.'
+            },
+            {
+                timestamp: '04/18/2024 11:11:01 EDT',
+                category: 'INFO',
+                message: 'Connection requested from existing node. Setting status to connecting.'
+            }
+        ],
+        nodeStartTime: '04/18/2024 11:10:55 EDT',
+        flowFilesQueued: 0,
+        bytesQueued: 0
+    };
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [ClusterNodeDetailDialog],
+            providers: [
+                {
+                    provide: MAT_DIALOG_DATA,
+                    useValue: {
+                        request: data
+                    }
+                }
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ClusterNodeDetailDialog);
+        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/cluster/ui/cluster-node-listing/cluster-node-detail-dialog/cluster-node-detail-dialog.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-detail-dialog/cluster-node-detail-dialog.component.ts
new file mode 100644
index 0000000..53ac94e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-detail-dialog/cluster-node-detail-dialog.component.ts
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, Inject } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { MatButton } from '@angular/material/button';
+import { ClusterNode } from '../../../state/cluster-listing';
+
+@Component({
+    selector: 'cluster-node-detail-dialog',
+    standalone: true,
+    imports: [MatDialogModule, MatButton],
+    templateUrl: './cluster-node-detail-dialog.component.html',
+    styleUrl: './cluster-node-detail-dialog.component.scss'
+})
+export class ClusterNodeDetailDialog {
+    node: ClusterNode;
+
+    constructor(@Inject(MAT_DIALOG_DATA) public request: ClusterNode) {
+        this.node = request;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-listing.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-listing.component.html
new file mode 100644
index 0000000..31fbd04
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-listing.component.html
@@ -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.
+  -->
+
+<ng-container>
+    @if (isInitialLoading(loadedTimestamp())) {
+        <div>
+            <ngx-skeleton-loader count="3"></ngx-skeleton-loader>
+        </div>
+    } @else {
+        <cluster-node-table
+            initialSortColumn="address"
+            initialSortDirection="asc"
+            [components]="nodes()"
+            [listingStatus]="listingStatus()"
+            [loadedTimestamp]="loadedTimestamp()"
+            [selectedId]="selectedClusterNodeId()"
+            [currentUser]="currentUser()"
+            (connectNode)="connectNode($event)"
+            (disconnectNode)="disconnectNode($event)"
+            (offloadNode)="offloadNode($event)"
+            (removeNode)="removeNode($event)"
+            (selectComponent)="selectNode($event)"
+            (clearSelection)="clearSelection()"
+            (showDetail)="showDetail($event)"></cluster-node-table>
+    }
+</ng-container>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-listing.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-listing.component.scss
new file mode 100644
index 0000000..b33f7ca
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-listing.component.scss
@@ -0,0 +1,16 @@
+/*!
+ * 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.
+ */
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-listing.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-listing.component.spec.ts
new file mode 100644
index 0000000..117e765
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-listing.component.spec.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 { ClusterNodeListing } from './cluster-node-listing.component';
+import { provideMockStore } from '@ngrx/store/testing';
+import { initialClusterState } from '../../state/cluster-listing/cluster-listing.reducer';
+import { selectClusterListing } from '../../state/cluster-listing/cluster-listing.selectors';
+import { ClusterState } from '../../state';
+import { clusterListingFeatureKey } from '../../state/cluster-listing';
+
+describe('ClusterNodeListing', () => {
+    let component: ClusterNodeListing;
+    let fixture: ComponentFixture<ClusterNodeListing>;
+
+    beforeEach(async () => {
+        const initialState: ClusterState = {
+            [clusterListingFeatureKey]: initialClusterState
+        };
+        await TestBed.configureTestingModule({
+            imports: [ClusterNodeListing],
+            providers: [
+                provideMockStore({
+                    initialState,
+                    selectors: [
+                        {
+                            selector: selectClusterListing,
+                            value: initialState[clusterListingFeatureKey]
+                        }
+                    ]
+                })
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ClusterNodeListing);
+        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/cluster/ui/cluster-node-listing/cluster-node-listing.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-listing.component.ts
new file mode 100644
index 0000000..7484beb
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-listing.component.ts
@@ -0,0 +1,89 @@
+/*
+ * 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 { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import { initialClusterState } from '../../state/cluster-listing/cluster-listing.reducer';
+import {
+    selectClusterListingLoadedTimestamp,
+    selectClusterListingNodes,
+    selectClusterListingStatus,
+    selectClusterNodeIdFromRoute
+} from '../../state/cluster-listing/cluster-listing.selectors';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../../state';
+import { ClusterNodeTable } from './cluster-node-table/cluster-node-table.component';
+import { ClusterNode } from '../../state/cluster-listing';
+import {
+    clearClusterNodeSelection,
+    confirmAndConnectNode,
+    confirmAndDisconnectNode,
+    confirmAndOffloadNode,
+    confirmAndRemoveNode,
+    selectClusterNode,
+    showClusterNodeDetails
+} from '../../state/cluster-listing/cluster-listing.actions';
+import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
+
+@Component({
+    selector: 'cluster-node-listing',
+    standalone: true,
+    imports: [NgxSkeletonLoaderModule, ClusterNodeTable],
+    templateUrl: './cluster-node-listing.component.html',
+    styleUrl: './cluster-node-listing.component.scss'
+})
+export class ClusterNodeListing {
+    loadedTimestamp = this.store.selectSignal(selectClusterListingLoadedTimestamp);
+    listingStatus = this.store.selectSignal(selectClusterListingStatus);
+    nodes = this.store.selectSignal(selectClusterListingNodes);
+    selectedClusterNodeId = this.store.selectSignal(selectClusterNodeIdFromRoute);
+    currentUser = this.store.selectSignal(selectCurrentUser);
+
+    constructor(private store: Store<NiFiState>) {}
+
+    isInitialLoading(loadedTimestamp: string): boolean {
+        return loadedTimestamp == initialClusterState.loadedTimestamp;
+    }
+
+    disconnectNode(node: ClusterNode): void {
+        this.store.dispatch(confirmAndDisconnectNode({ request: node }));
+    }
+
+    connectNode(node: ClusterNode): void {
+        this.store.dispatch(confirmAndConnectNode({ request: node }));
+    }
+
+    removeNode(node: ClusterNode): void {
+        this.store.dispatch(confirmAndRemoveNode({ request: node }));
+    }
+
+    offloadNode(node: ClusterNode): void {
+        this.store.dispatch(confirmAndOffloadNode({ request: node }));
+    }
+
+    showDetail(node: ClusterNode): void {
+        this.store.dispatch(showClusterNodeDetails({ request: node }));
+    }
+
+    selectNode(node: ClusterNode): void {
+        this.store.dispatch(selectClusterNode({ request: { id: node.nodeId } }));
+    }
+
+    clearSelection(): void {
+        this.store.dispatch(clearClusterNodeSelection());
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-table/cluster-node-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-table/cluster-node-table.component.html
new file mode 100644
index 0000000..4141b8c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-table/cluster-node-table.component.html
@@ -0,0 +1,194 @@
+<!--
+  ~  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">
+    <div class="flex-1">
+        <ng-container>
+            <div class="cluster-node-table h-full flex flex-col">
+                <!-- allow filtering of the table -->
+                <cluster-table-filter
+                    [filteredCount]="filteredCount"
+                    [totalCount]="totalCount"
+                    [filterableColumns]="filterableColumns"
+                    filterColumn="address"
+                    (filterChanged)="applyFilter($event)"></cluster-table-filter>
+
+                <div class="flex-1 relative">
+                    <div class="listing-table overflow-y-auto absolute inset-0">
+                        <table
+                            mat-table
+                            [dataSource]="dataSource"
+                            matSort
+                            matSortDisableClear
+                            (matSortChange)="sortData($event)"
+                            [matSortActive]="initialSortColumn"
+                            [matSortDirection]="initialSortDirection">
+                            <!-- More Details Column -->
+                            <ng-container matColumnDef="moreDetails">
+                                <th mat-header-cell *matHeaderCellDef></th>
+                                <td mat-cell *matCellDef="let item">
+                                    <div class="flex items-center gap-x-2">
+                                        <div
+                                            class="pointer fa fa-info-circle primary-color"
+                                            (click)="moreDetail(item)"
+                                            title="Detail"></div>
+                                    </div>
+                                </td>
+                            </ng-container>
+
+                            <!-- Node Address -->
+                            <ng-container matColumnDef="address">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Node Address">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Node Address</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="formatNodeAddress(item)">
+                                    {{ formatNodeAddress(item) }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Active Thread Count -->
+                            <ng-container matColumnDef="activeThreadCount">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Active Thread Count">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">
+                                        Active Threads
+                                    </div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="formatActiveThreadCount(item)">
+                                    {{ formatActiveThreadCount(item) }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Queued column -->
+                            <ng-container matColumnDef="queued">
+                                <th
+                                    mat-header-cell
+                                    *matHeaderCellDef
+                                    mat-sort-header
+                                    title="Count / data size queued in the last 5 minutes">
+                                    <div
+                                        class="inline-block overflow-hidden overflow-ellipsis whitespace-nowrap space-x-1">
+                                        <span
+                                            [ngClass]="{
+                                                underline:
+                                                    multiSort.active === 'queued' && multiSort.sortValueIndex === 0
+                                            }"
+                                            >Queue</span
+                                        >
+                                        <span> / </span>
+                                        <span
+                                            [ngClass]="{
+                                                underline:
+                                                    multiSort.active === 'queued' && multiSort.sortValueIndex === 1
+                                            }"
+                                            >Size</span
+                                        >
+                                    </div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="formatQueued(item)">
+                                    {{ formatQueued(item) }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Status Column -->
+                            <ng-container matColumnDef="status">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Status">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Status</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="formatStatus(item)">
+                                    {{ formatStatus(item) }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Started At Column -->
+                            <ng-container matColumnDef="nodeStartTime">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Started At">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Started At</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="formatStartTime(item)">
+                                    @if (!item.nodeStartTime) {
+                                        <div class="unset nifi-surface-default">No value set</div>
+                                    } @else {
+                                        {{ formatStartTime(item) }}
+                                    }
+                                </td>
+                            </ng-container>
+
+                            <!-- Last Heartbeat Column -->
+                            <ng-container matColumnDef="heartbeat">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Last Heartbeat">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">
+                                        Last Heartbeat
+                                    </div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="formatHeartbeat(item)">
+                                    @if (!item.heartbeat) {
+                                        <div class="unset nifi-surface-default">No value set</div>
+                                    } @else {
+                                        {{ formatHeartbeat(item) }}
+                                    }
+                                </td>
+                            </ng-container>
+
+                            <ng-container matColumnDef="actions">
+                                <th mat-header-cell *matHeaderCellDef></th>
+                                <td mat-cell *matCellDef="let item">
+                                    <div class="flex items-center justify-end gap-x-2">
+                                        @if (item.status === 'CONNECTED' || item.status === 'CONNECTING') {
+                                            <div
+                                                class="pointer fa fa-power-off primary-color"
+                                                (click)="disconnect(item)"
+                                                title="Disconnect"></div>
+                                        } @else if (item.status === 'DISCONNECTED') {
+                                            <div
+                                                class="pointer fa fa-plug primary-color"
+                                                title="Connect"
+                                                (click)="connect(item)"></div>
+                                            <div
+                                                class="pointer fa fa-rotate-90 fa-upload primary-color"
+                                                title="Offload"
+                                                (click)="offload(item)"></div>
+                                            <div
+                                                class="pointer fa fa-trash primary-color"
+                                                title="Delete"
+                                                (click)="remove(item)"></div>
+                                        } @else if (item.status === 'OFFLOADED') {
+                                            <div
+                                                class="pointer fa fa-plug primary-color"
+                                                title="Connect"
+                                                (click)="connect(item)"></div>
+                                            <div
+                                                class="pointer fa fa-trash primary-color"
+                                                title="Delete"
+                                                (click)="remove(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"
+                                [class.even]="even"
+                                (click)="select(row)"
+                                [class.selected]="isSelected(row)"></tr>
+                        </table>
+                    </div>
+                </div>
+            </div>
+        </ng-container>
+    </div>
+</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-table/cluster-node-table.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-table/cluster-node-table.component.scss
new file mode 100644
index 0000000..60b5464
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-table/cluster-node-table.component.scss
@@ -0,0 +1,34 @@
+/*!
+ * 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.
+ */
+
+.cluster-node-table {
+    .listing-table {
+        table {
+            .mat-column-moreDetails {
+                width: 32px;
+            }
+
+            .mat-column-address {
+                width: 25%;
+            }
+
+            .mat-column-actions {
+                width: 74px;
+            }
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-table/cluster-node-table.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-table/cluster-node-table.component.spec.ts
new file mode 100644
index 0000000..a52a232
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-table/cluster-node-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 { ClusterNodeTable } from './cluster-node-table.component';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+
+describe('ClusterNodeTable', () => {
+    let component: ClusterNodeTable;
+    let fixture: ComponentFixture<ClusterNodeTable>;
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [ClusterNodeTable, NoopAnimationsModule]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ClusterNodeTable);
+        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/cluster/ui/cluster-node-listing/cluster-node-table/cluster-node-table.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-table/cluster-node-table.component.ts
new file mode 100644
index 0000000..592488d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-node-listing/cluster-node-table/cluster-node-table.component.ts
@@ -0,0 +1,221 @@
+/*
+ * 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, Input, Output } from '@angular/core';
+import { MatCell, MatHeaderCell, MatTableModule } from '@angular/material/table';
+import { ClusterNode } from '../../../state/cluster-listing';
+import { MatSortHeader, MatSortModule, Sort } from '@angular/material/sort';
+import { NgClass } from '@angular/common';
+import { NiFiCommon } from '../../../../../service/nifi-common.service';
+import { ClusterTable } from '../../common/cluster-table/cluster-table.component';
+import {
+    ClusterTableFilter,
+    ClusterTableFilterColumn
+} from '../../common/cluster-table-filter/cluster-table-filter.component';
+import { CurrentUser } from '../../../../../state/current-user';
+
+@Component({
+    selector: 'cluster-node-table',
+    standalone: true,
+    imports: [MatCell, MatHeaderCell, MatSortHeader, NgClass, MatTableModule, MatSortModule, ClusterTableFilter],
+    templateUrl: './cluster-node-table.component.html',
+    styleUrl: './cluster-node-table.component.scss'
+})
+export class ClusterNodeTable extends ClusterTable<ClusterNode> {
+    private _currentUser!: CurrentUser;
+
+    @Output() disconnectNode: EventEmitter<ClusterNode> = new EventEmitter<ClusterNode>();
+    @Output() connectNode: EventEmitter<ClusterNode> = new EventEmitter<ClusterNode>();
+    @Output() offloadNode: EventEmitter<ClusterNode> = new EventEmitter<ClusterNode>();
+    @Output() removeNode: EventEmitter<ClusterNode> = new EventEmitter<ClusterNode>();
+    @Output() showDetail: EventEmitter<ClusterNode> = new EventEmitter<ClusterNode>();
+
+    @Input() set currentUser(user: CurrentUser) {
+        this._currentUser = user;
+        const actionsColIndex = this.displayedColumns.findIndex((col) => col === 'actions');
+        if (user.controllerPermissions.canRead && user.controllerPermissions.canWrite) {
+            if (actionsColIndex < 0) {
+                this.displayedColumns.push('actions');
+            }
+        } else {
+            if (actionsColIndex >= 0) {
+                this.displayedColumns.splice(actionsColIndex, 1);
+            }
+        }
+    }
+    get currentUser() {
+        return this._currentUser;
+    }
+
+    filterableColumns: ClusterTableFilterColumn[] = [
+        { key: 'address', label: 'Address' },
+        { key: 'status', label: 'Status' }
+    ];
+
+    displayedColumns: string[] = [
+        'moreDetails',
+        'address',
+        'activeThreadCount',
+        'queued',
+        'status',
+        'nodeStartTime',
+        'heartbeat'
+    ];
+
+    constructor(private nifiCommon: NiFiCommon) {
+        super();
+    }
+
+    formatNodeAddress(item: ClusterNode): string {
+        return `${item.address}:${item.apiPort}`;
+    }
+
+    formatActiveThreadCount(item: ClusterNode): number | null {
+        return item.status === 'CONNECTED' ? item.activeThreadCount || 0 : null;
+    }
+
+    formatQueued(item: ClusterNode): string {
+        return item.queued || '';
+    }
+
+    formatStatus(item: ClusterNode): string {
+        let status = item.status;
+        if (item.roles.includes('Primary Node')) {
+            status += ', PRIMARY';
+        }
+        if (item.roles.includes('Cluster Coordinator')) {
+            status += ', COORDINATOR';
+        }
+        return status;
+    }
+
+    formatStartTime(item: ClusterNode): string {
+        return item.nodeStartTime || '';
+    }
+
+    formatHeartbeat(item: ClusterNode): string {
+        return item.heartbeat || '';
+    }
+
+    override filterPredicate(item: ClusterNode, filter: string): boolean {
+        const { filterTerm, filterColumn } = JSON.parse(filter);
+        if (filterTerm === '') {
+            return true;
+        }
+
+        let field = '';
+        switch (filterColumn) {
+            case 'address':
+                field = this.formatNodeAddress(item);
+                break;
+            case 'status':
+                field = this.formatStatus(item);
+                break;
+        }
+        return this.nifiCommon.stringContains(field, filterTerm, true);
+    }
+
+    override sortEntities(data: ClusterNode[], sort: Sort): ClusterNode[] {
+        if (!data) {
+            return [];
+        }
+        return data.slice().sort((a, b) => {
+            const isAsc = sort.direction === 'asc';
+            let retVal = 0;
+            switch (sort.active) {
+                case 'address':
+                    retVal = this.nifiCommon.compareString(a.address, b.address);
+                    // check the port if the addresses are the same
+                    if (retVal === 0) {
+                        retVal = this.nifiCommon.compareNumber(a.apiPort, b.apiPort);
+                    }
+                    break;
+                case 'activeThreadCount':
+                    retVal = this.nifiCommon.compareNumber(
+                        this.formatActiveThreadCount(a),
+                        this.formatActiveThreadCount(b)
+                    );
+                    break;
+                case 'status':
+                    retVal = this.nifiCommon.compareString(this.formatStatus(a), this.formatStatus(b));
+                    break;
+                case 'nodeStartTime':
+                    retVal = this.nifiCommon.compareNumber(
+                        this.nifiCommon.parseDateTime(a.nodeStartTime).getTime(),
+                        this.nifiCommon.parseDateTime(b.nodeStartTime).getTime()
+                    );
+                    break;
+                case 'heartbeat':
+                    retVal = this.nifiCommon.compareNumber(
+                        this.nifiCommon.parseDateTime(a.heartbeat).getTime(),
+                        this.nifiCommon.parseDateTime(b.heartbeat).getTime()
+                    );
+                    break;
+                case 'queued':
+                    if (this.multiSort.sortValueIndex === 0) {
+                        retVal = this.nifiCommon.compareNumber(a.flowFilesQueued, b.flowFilesQueued);
+                    } else {
+                        retVal = this.nifiCommon.compareNumber(a.bytesQueued, b.bytesQueued);
+                    }
+                    break;
+                default:
+                    retVal = 0;
+            }
+            return retVal * (isAsc ? 1 : -1);
+        });
+    }
+
+    override supportsMultiValuedSort(sort: Sort): boolean {
+        switch (sort.active) {
+            case 'queued':
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    disconnect(item: ClusterNode) {
+        this.disconnectNode.next(item);
+    }
+
+    connect(item: ClusterNode) {
+        this.connectNode.next(item);
+    }
+
+    offload(item: ClusterNode) {
+        this.offloadNode.next(item);
+    }
+
+    remove(item: ClusterNode) {
+        this.removeNode.next(item);
+    }
+
+    moreDetail(item: ClusterNode) {
+        this.showDetail.next(item);
+    }
+
+    select(item: ClusterNode) {
+        this.selectComponent.next(item);
+    }
+
+    isSelected(item: ClusterNode): boolean {
+        if (this.selectedId) {
+            return this.selectedId === item.nodeId;
+        }
+        return false;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-provenance-storage-listing/cluster-provenance-storage-listing.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-provenance-storage-listing/cluster-provenance-storage-listing.component.html
new file mode 100644
index 0000000..66feaf7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-provenance-storage-listing/cluster-provenance-storage-listing.component.html
@@ -0,0 +1,36 @@
+<!--
+  ~ 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.
+  -->
+
+<ng-container>
+    @if (isInitialLoading(loadedTimestamp())) {
+        <div>
+            <ngx-skeleton-loader count="3"></ngx-skeleton-loader>
+        </div>
+    } @else {
+        <repository-storage-table
+            [selectedId]="selectedClusterNodeId()"
+            [selectedRepositoryId]="selectedClusterRepoId()"
+            (clearSelection)="clearSelection()"
+            (selectComponent)="selectStorageNode($event)"
+            [loadedTimestamp]="loadedTimestamp()"
+            [listingStatus]="listingStatus()"
+            [components]="(components$ | async) || []"
+            [showRepoIdentifier]="true"
+            initialSortColumn="address"
+            initialSortDirection="asc"></repository-storage-table>
+    }
+</ng-container>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-provenance-storage-listing/cluster-provenance-storage-listing.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-provenance-storage-listing/cluster-provenance-storage-listing.component.scss
new file mode 100644
index 0000000..b33f7ca
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-provenance-storage-listing/cluster-provenance-storage-listing.component.scss
@@ -0,0 +1,16 @@
+/*!
+ * 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.
+ */
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-provenance-storage-listing/cluster-provenance-storage-listing.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-provenance-storage-listing/cluster-provenance-storage-listing.component.spec.ts
new file mode 100644
index 0000000..0264845
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-provenance-storage-listing/cluster-provenance-storage-listing.component.spec.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 { ClusterProvenanceStorageListing } from './cluster-provenance-storage-listing.component';
+import { ClusterState } from '../../state';
+import { clusterListingFeatureKey } from '../../state/cluster-listing';
+import { initialClusterState } from '../../state/cluster-listing/cluster-listing.reducer';
+import { provideMockStore } from '@ngrx/store/testing';
+import { selectClusterListing } from '../../state/cluster-listing/cluster-listing.selectors';
+
+describe('ClusterProvenanceStorageListing', () => {
+    let component: ClusterProvenanceStorageListing;
+    let fixture: ComponentFixture<ClusterProvenanceStorageListing>;
+
+    beforeEach(async () => {
+        const initialState: ClusterState = {
+            [clusterListingFeatureKey]: initialClusterState
+        };
+
+        await TestBed.configureTestingModule({
+            imports: [ClusterProvenanceStorageListing],
+            providers: [
+                provideMockStore({
+                    initialState,
+                    selectors: [
+                        {
+                            selector: selectClusterListing,
+                            value: initialState[clusterListingFeatureKey]
+                        }
+                    ]
+                })
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ClusterProvenanceStorageListing);
+        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/cluster/ui/cluster-provenance-storage-listing/cluster-provenance-storage-listing.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-provenance-storage-listing/cluster-provenance-storage-listing.component.ts
new file mode 100644
index 0000000..e31032b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-provenance-storage-listing/cluster-provenance-storage-listing.component.ts
@@ -0,0 +1,92 @@
+/*
+ * 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 {
+    selectClusterListingLoadedTimestamp,
+    selectClusterListingStatus,
+    selectClusterNodeIdFromRoute,
+    selectClusterStorageRepositoryIdFromRoute
+} from '../../state/cluster-listing/cluster-listing.selectors';
+import { selectSystemNodeSnapshots } from '../../../../state/system-diagnostics/system-diagnostics.selectors';
+import { isDefinedAndNotNull } from '../../../../state/shared';
+import { map } from 'rxjs';
+import { ClusterNodeRepositoryStorageUsage } from '../../../../state/system-diagnostics';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../../state';
+import { initialClusterState } from '../../state/cluster-listing/cluster-listing.reducer';
+import {
+    clearProvenanceStorageNodeSelection,
+    selectProvenanceStorageNode
+} from '../../state/cluster-listing/cluster-listing.actions';
+import { AsyncPipe } from '@angular/common';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import { RepositoryStorageTable } from '../common/repository-storage-table/repository-storage-table.component';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+
+@Component({
+    selector: 'cluster-provenance-storage-listing',
+    standalone: true,
+    imports: [AsyncPipe, NgxSkeletonLoaderModule, RepositoryStorageTable],
+    templateUrl: './cluster-provenance-storage-listing.component.html',
+    styleUrl: './cluster-provenance-storage-listing.component.scss'
+})
+export class ClusterProvenanceStorageListing {
+    loadedTimestamp = this.store.selectSignal(selectClusterListingLoadedTimestamp);
+    listingStatus = this.store.selectSignal(selectClusterListingStatus);
+    selectedClusterNodeId = this.store.selectSignal(selectClusterNodeIdFromRoute);
+    selectedClusterRepoId = this.store.selectSignal(selectClusterStorageRepositoryIdFromRoute);
+    components$ = this.store.select(selectSystemNodeSnapshots).pipe(
+        takeUntilDestroyed(),
+        isDefinedAndNotNull(),
+        map((clusterNodes) => {
+            const expanded: ClusterNodeRepositoryStorageUsage[] = [];
+            return clusterNodes.reduce((acc, node) => {
+                const repos = node.snapshot.provenanceRepositoryStorageUsage.map((storage) => {
+                    return {
+                        address: node.address,
+                        apiPort: node.apiPort,
+                        nodeId: node.nodeId,
+                        repositoryStorageUsage: storage
+                    };
+                });
+                return [...acc, ...repos];
+            }, expanded);
+        })
+    );
+
+    constructor(private store: Store<NiFiState>) {}
+
+    isInitialLoading(loadedTimestamp: string): boolean {
+        return loadedTimestamp == initialClusterState.loadedTimestamp;
+    }
+
+    selectStorageNode(node: ClusterNodeRepositoryStorageUsage): void {
+        this.store.dispatch(
+            selectProvenanceStorageNode({
+                request: {
+                    id: node.nodeId,
+                    repository: node.repositoryStorageUsage.identifier
+                }
+            })
+        );
+    }
+
+    clearSelection(): void {
+        this.store.dispatch(clearProvenanceStorageNodeSelection());
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-listing.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-listing.component.html
new file mode 100644
index 0000000..aba7db4
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-listing.component.html
@@ -0,0 +1,34 @@
+<!--
+  ~ 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.
+  -->
+
+<ng-container>
+    @if (isInitialLoading(loadedTimestamp())) {
+        <div>
+            <ngx-skeleton-loader count="3"></ngx-skeleton-loader>
+        </div>
+    } @else {
+        <cluster-system-table
+            [selectedId]="selectedClusterNodeId()"
+            (clearSelection)="clearSelection()"
+            (selectComponent)="selectNode($event)"
+            [loadedTimestamp]="loadedTimestamp()"
+            [listingStatus]="listingStatus()"
+            [components]="nodes() || []"
+            initialSortColumn="address"
+            initialSortDirection="asc"></cluster-system-table>
+    }
+</ng-container>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-listing.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-listing.component.scss
new file mode 100644
index 0000000..b33f7ca
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-listing.component.scss
@@ -0,0 +1,16 @@
+/*!
+ * 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.
+ */
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-listing.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-listing.component.spec.ts
new file mode 100644
index 0000000..d7d104f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-listing.component.spec.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 { ClusterSystemListing } from './cluster-system-listing.component';
+import { ClusterState } from '../../state';
+import { clusterListingFeatureKey } from '../../state/cluster-listing';
+import { initialClusterState } from '../../state/cluster-listing/cluster-listing.reducer';
+import { provideMockStore } from '@ngrx/store/testing';
+import { selectClusterListing } from '../../state/cluster-listing/cluster-listing.selectors';
+
+describe('ClusterSystemListing', () => {
+    let component: ClusterSystemListing;
+    let fixture: ComponentFixture<ClusterSystemListing>;
+
+    beforeEach(async () => {
+        const initialState: ClusterState = {
+            [clusterListingFeatureKey]: initialClusterState
+        };
+        await TestBed.configureTestingModule({
+            imports: [ClusterSystemListing],
+            providers: [
+                provideMockStore({
+                    initialState,
+                    selectors: [
+                        {
+                            selector: selectClusterListing,
+                            value: initialState[clusterListingFeatureKey]
+                        }
+                    ]
+                })
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ClusterSystemListing);
+        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/cluster/ui/cluster-system-listing/cluster-system-listing.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-listing.component.ts
new file mode 100644
index 0000000..2f8d310
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-listing.component.ts
@@ -0,0 +1,60 @@
+/*
+ * 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 {
+    selectClusterListingLoadedTimestamp,
+    selectClusterListingStatus,
+    selectClusterNodeIdFromRoute
+} from '../../state/cluster-listing/cluster-listing.selectors';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../../state';
+import { initialClusterState } from '../../state/cluster-listing/cluster-listing.reducer';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import { selectSystemNodeSnapshots } from '../../../../state/system-diagnostics/system-diagnostics.selectors';
+import { ClusterSystemTable } from './cluster-system-table/cluster-system-table.component';
+import { clearSystemNodeSelection, selectSystemNode } from '../../state/cluster-listing/cluster-listing.actions';
+import { ClusterNodeTable } from '../cluster-node-listing/cluster-node-table/cluster-node-table.component';
+import { NodeSnapshot } from '../../../../state/system-diagnostics';
+
+@Component({
+    selector: 'cluster-system-listing',
+    standalone: true,
+    imports: [NgxSkeletonLoaderModule, ClusterSystemTable, ClusterNodeTable],
+    templateUrl: './cluster-system-listing.component.html',
+    styleUrl: './cluster-system-listing.component.scss'
+})
+export class ClusterSystemListing {
+    loadedTimestamp = this.store.selectSignal(selectClusterListingLoadedTimestamp);
+    listingStatus = this.store.selectSignal(selectClusterListingStatus);
+    selectedClusterNodeId = this.store.selectSignal(selectClusterNodeIdFromRoute);
+    nodes = this.store.selectSignal(selectSystemNodeSnapshots);
+
+    constructor(private store: Store<NiFiState>) {}
+
+    isInitialLoading(loadedTimestamp: string): boolean {
+        return loadedTimestamp == initialClusterState.loadedTimestamp;
+    }
+
+    selectNode(node: NodeSnapshot): void {
+        this.store.dispatch(selectSystemNode({ request: { id: node.nodeId } }));
+    }
+
+    clearSelection(): void {
+        this.store.dispatch(clearSystemNodeSelection());
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-table/cluster-system-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-table/cluster-system-table.component.html
new file mode 100644
index 0000000..b45f0fb
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-table/cluster-system-table.component.html
@@ -0,0 +1,106 @@
+<!--
+  ~  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">
+    <div class="flex-1">
+        <ng-container>
+            <div class="system-node-table h-full flex flex-col">
+                <!-- allow filtering of the table -->
+                <cluster-table-filter
+                    [filteredCount]="filteredCount"
+                    [totalCount]="totalCount"
+                    [filterableColumns]="filterableColumns"
+                    filterColumn="address"
+                    (filterChanged)="applyFilter($event)"></cluster-table-filter>
+
+                <div class="flex-1 relative">
+                    <div class="listing-table overflow-y-auto absolute inset-0">
+                        <table
+                            mat-table
+                            [dataSource]="dataSource"
+                            matSort
+                            matSortDisableClear
+                            (matSortChange)="sortData($event)"
+                            [matSortActive]="initialSortColumn"
+                            [matSortDirection]="initialSortDirection">
+                            <!-- Node Address -->
+                            <ng-container matColumnDef="address">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Node Address">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Node Address</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="formatNodeAddress(item)">
+                                    {{ formatNodeAddress(item) }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Available Processors -->
+                            <ng-container matColumnDef="availableProcessors">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Cores">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Cores</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.availableProcessors">
+                                    {{ item.snapshot.availableProcessors }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Processor Load Average column -->
+                            <ng-container matColumnDef="processorLoadAverage">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Core Load Average">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">
+                                        Core Load Average
+                                    </div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.processorLoadAverage">
+                                    {{ item.snapshot.processorLoadAverage }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Total Threads column -->
+                            <ng-container matColumnDef="totalThreads">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Total Threads">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Total Threads</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.totalThreads">
+                                    {{ item.snapshot.totalThreads }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Daemon Threads column -->
+                            <ng-container matColumnDef="daemonThreads">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Daemon Threads">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">
+                                        Daemon Threads
+                                    </div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.daemonThreads">
+                                    {{ item.snapshot.daemonThreads }}
+                                </td>
+                            </ng-container>
+
+                            <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
+                            <tr
+                                mat-row
+                                *matRowDef="let row; let even = even; columns: displayedColumns"
+                                [class.even]="even"
+                                (click)="select(row)"
+                                [class.selected]="isSelected(row)"></tr>
+                        </table>
+                    </div>
+                </div>
+            </div>
+        </ng-container>
+    </div>
+</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-table/cluster-system-table.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-table/cluster-system-table.component.scss
new file mode 100644
index 0000000..a36464b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-table/cluster-system-table.component.scss
@@ -0,0 +1,26 @@
+/*!
+ * 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.
+ */
+
+.system-node-table {
+    .listing-table {
+        table {
+            .mat-column-address {
+                width: 25%;
+            }
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-table/cluster-system-table.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-table/cluster-system-table.component.spec.ts
new file mode 100644
index 0000000..2051faa
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-table/cluster-system-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 { ClusterSystemTable } from './cluster-system-table.component';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+
+describe('ClusterSystemTable', () => {
+    let component: ClusterSystemTable;
+    let fixture: ComponentFixture<ClusterSystemTable>;
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [ClusterSystemTable, NoopAnimationsModule]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ClusterSystemTable);
+        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/cluster/ui/cluster-system-listing/cluster-system-table/cluster-system-table.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-table/cluster-system-table.component.ts
new file mode 100644
index 0000000..5f344b9
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-system-listing/cluster-system-table/cluster-system-table.component.ts
@@ -0,0 +1,124 @@
+/*
+ * 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 {
+    ClusterTableFilter,
+    ClusterTableFilterColumn
+} from '../../common/cluster-table-filter/cluster-table-filter.component';
+import { NiFiCommon } from '../../../../../service/nifi-common.service';
+import { ClusterTable } from '../../common/cluster-table/cluster-table.component';
+import { MatSortModule, Sort } from '@angular/material/sort';
+import { NodeSnapshot } from '../../../../../state/system-diagnostics';
+import { MatTableModule } from '@angular/material/table';
+
+@Component({
+    selector: 'cluster-system-table',
+    standalone: true,
+    imports: [ClusterTableFilter, MatTableModule, MatSortModule],
+    templateUrl: './cluster-system-table.component.html',
+    styleUrl: './cluster-system-table.component.scss'
+})
+export class ClusterSystemTable extends ClusterTable<NodeSnapshot> {
+    filterableColumns: ClusterTableFilterColumn[] = [{ key: 'address', label: 'Address' }];
+
+    displayedColumns: string[] = [
+        'address',
+        'availableProcessors',
+        'processorLoadAverage',
+        'totalThreads',
+        'daemonThreads'
+    ];
+
+    constructor(private nifiCommon: NiFiCommon) {
+        super();
+    }
+
+    formatNodeAddress(item: NodeSnapshot): string {
+        return `${item.address}:${item.apiPort}`;
+    }
+
+    override filterPredicate(item: NodeSnapshot, filter: string): boolean {
+        const { filterTerm, filterColumn } = JSON.parse(filter);
+        if (filterTerm === '') {
+            return true;
+        }
+
+        let field = '';
+        switch (filterColumn) {
+            case 'address':
+                field = this.formatNodeAddress(item);
+                break;
+        }
+        return this.nifiCommon.stringContains(field, filterTerm, true);
+    }
+
+    override sortEntities(data: NodeSnapshot[], sort: Sort): any[] {
+        if (!data) {
+            return [];
+        }
+        return data.slice().sort((a, b) => {
+            const isAsc = sort.direction === 'asc';
+            let retVal = 0;
+            switch (sort.active) {
+                case 'address':
+                    retVal = this.nifiCommon.compareString(a.address, b.address);
+                    // check the port if the addresses are the same
+                    if (retVal === 0) {
+                        retVal = this.nifiCommon.compareNumber(a.apiPort, b.apiPort);
+                    }
+                    break;
+                case 'availableProcessors':
+                    retVal = this.nifiCommon.compareNumber(
+                        a.snapshot.availableProcessors,
+                        b.snapshot.availableProcessors
+                    );
+                    break;
+                case 'processorLoadAverage':
+                    retVal = this.nifiCommon.compareNumber(
+                        a.snapshot.processorLoadAverage,
+                        b.snapshot.processorLoadAverage
+                    );
+                    break;
+                case 'totalThreads':
+                    retVal = this.nifiCommon.compareNumber(a.snapshot.totalThreads, b.snapshot.totalThreads);
+                    break;
+                case 'daemonThreads':
+                    retVal = this.nifiCommon.compareNumber(a.snapshot.daemonThreads, b.snapshot.daemonThreads);
+                    break;
+                default:
+                    retVal = 0;
+            }
+            return retVal * (isAsc ? 1 : -1);
+        });
+    }
+
+    override supportsMultiValuedSort(): boolean {
+        return false;
+    }
+
+    select(item: NodeSnapshot): any {
+        this.selectComponent.next(item);
+    }
+
+    isSelected(item: NodeSnapshot): boolean {
+        if (this.selectedId) {
+            return this.selectedId === item.nodeId;
+        }
+        return false;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-listing.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-listing.component.html
new file mode 100644
index 0000000..96e1007
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-listing.component.html
@@ -0,0 +1,34 @@
+<!--
+  ~ 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.
+  -->
+
+<ng-container>
+    @if (isInitialLoading(loadedTimestamp())) {
+        <div>
+            <ngx-skeleton-loader count="3"></ngx-skeleton-loader>
+        </div>
+    } @else {
+        <cluster-version-table
+            [selectedId]="selectedClusterNodeId()"
+            (clearSelection)="clearSelection()"
+            (selectComponent)="selectNode($event)"
+            [loadedTimestamp]="loadedTimestamp()"
+            [listingStatus]="listingStatus()"
+            [components]="nodes() || []"
+            initialSortColumn="address"
+            initialSortDirection="asc"></cluster-version-table>
+    }
+</ng-container>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-listing.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-listing.component.scss
new file mode 100644
index 0000000..b33f7ca
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-listing.component.scss
@@ -0,0 +1,16 @@
+/*!
+ * 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.
+ */
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-listing.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-listing.component.spec.ts
new file mode 100644
index 0000000..441f77c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-listing.component.spec.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 { ClusterVersionListing } from './cluster-version-listing.component';
+import { ClusterState } from '../../state';
+import { clusterListingFeatureKey } from '../../state/cluster-listing';
+import { initialClusterState } from '../../state/cluster-listing/cluster-listing.reducer';
+import { provideMockStore } from '@ngrx/store/testing';
+import { selectClusterListing } from '../../state/cluster-listing/cluster-listing.selectors';
+
+describe('ClusterVersionListing', () => {
+    let component: ClusterVersionListing;
+    let fixture: ComponentFixture<ClusterVersionListing>;
+
+    beforeEach(async () => {
+        const initialState: ClusterState = {
+            [clusterListingFeatureKey]: initialClusterState
+        };
+
+        await TestBed.configureTestingModule({
+            imports: [ClusterVersionListing],
+            providers: [
+                provideMockStore({
+                    initialState,
+                    selectors: [
+                        {
+                            selector: selectClusterListing,
+                            value: initialState[clusterListingFeatureKey]
+                        }
+                    ]
+                })
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ClusterVersionListing);
+        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/cluster/ui/cluster-version-listing/cluster-version-listing.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-listing.component.ts
new file mode 100644
index 0000000..3be01d5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-listing.component.ts
@@ -0,0 +1,60 @@
+/*
+ * 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 {
+    selectClusterListingLoadedTimestamp,
+    selectClusterListingStatus,
+    selectClusterNodeIdFromRoute
+} from '../../state/cluster-listing/cluster-listing.selectors';
+import { selectSystemNodeSnapshots } from '../../../../state/system-diagnostics/system-diagnostics.selectors';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../../state';
+import { initialClusterState } from '../../state/cluster-listing/cluster-listing.reducer';
+import { NodeSnapshot } from '../../../../state/system-diagnostics';
+import { clearVersionsNodeSelection, selectVersionNode } from '../../state/cluster-listing/cluster-listing.actions';
+import { ClusterSystemTable } from '../cluster-system-listing/cluster-system-table/cluster-system-table.component';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import { ClusterVersionTable } from './cluster-version-table/cluster-version-table.component';
+
+@Component({
+    selector: 'cluster-version-listing',
+    standalone: true,
+    imports: [ClusterSystemTable, NgxSkeletonLoaderModule, ClusterVersionTable],
+    templateUrl: './cluster-version-listing.component.html',
+    styleUrl: './cluster-version-listing.component.scss'
+})
+export class ClusterVersionListing {
+    loadedTimestamp = this.store.selectSignal(selectClusterListingLoadedTimestamp);
+    listingStatus = this.store.selectSignal(selectClusterListingStatus);
+    selectedClusterNodeId = this.store.selectSignal(selectClusterNodeIdFromRoute);
+    nodes = this.store.selectSignal(selectSystemNodeSnapshots);
+
+    constructor(private store: Store<NiFiState>) {}
+
+    isInitialLoading(loadedTimestamp: string): boolean {
+        return loadedTimestamp == initialClusterState.loadedTimestamp;
+    }
+
+    selectNode(node: NodeSnapshot): void {
+        this.store.dispatch(selectVersionNode({ request: { id: node.nodeId } }));
+    }
+
+    clearSelection(): void {
+        this.store.dispatch(clearVersionsNodeSelection());
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-table/cluster-version-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-table/cluster-version-table.component.html
new file mode 100644
index 0000000..7b687a2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-table/cluster-version-table.component.html
@@ -0,0 +1,124 @@
+<!--
+  ~  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">
+    <div class="flex-1">
+        <ng-container>
+            <div class="cluster-version-table h-full flex flex-col">
+                <!-- allow filtering of the table -->
+                <cluster-table-filter
+                    [filteredCount]="filteredCount"
+                    [totalCount]="totalCount"
+                    [filterableColumns]="filterableColumns"
+                    filterColumn="address"
+                    (filterChanged)="applyFilter($event)"></cluster-table-filter>
+
+                <div class="flex-1 relative">
+                    <div class="listing-table overflow-y-auto absolute inset-0">
+                        <table
+                            mat-table
+                            [dataSource]="dataSource"
+                            matSort
+                            matSortDisableClear
+                            (matSortChange)="sortData($event)"
+                            [matSortActive]="initialSortColumn"
+                            [matSortDirection]="initialSortDirection">
+                            <!-- Node Address -->
+                            <ng-container matColumnDef="address">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Node Address">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Node Address</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="formatNodeAddress(item)">
+                                    {{ formatNodeAddress(item) }}
+                                </td>
+                            </ng-container>
+
+                            <!-- NiFi Version -->
+                            <ng-container matColumnDef="nifiVersion">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="NiFi Version">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">NiFi Version</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.versionInfo.niFiVersion">
+                                    {{ item.snapshot.versionInfo.niFiVersion }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Java Vendor -->
+                            <ng-container matColumnDef="javaVendor">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Java Vendor">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Java Vendor</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.versionInfo.javaVendor">
+                                    {{ item.snapshot.versionInfo.javaVendor }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Java Version -->
+                            <ng-container matColumnDef="javaVersion">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Java Version">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Java Version</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.versionInfo.javaVersion">
+                                    {{ item.snapshot.versionInfo.javaVersion }}
+                                </td>
+                            </ng-container>
+
+                            <!-- OS Name -->
+                            <ng-container matColumnDef="osName">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="OS Name">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">OS Name</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.versionInfo.osName">
+                                    {{ item.snapshot.versionInfo.osName }}
+                                </td>
+                            </ng-container>
+
+                            <!-- OS Version -->
+                            <ng-container matColumnDef="osVersion">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="OS Version">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">OS Version</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.versionInfo.osVersion">
+                                    {{ item.snapshot.versionInfo.osVersion }}
+                                </td>
+                            </ng-container>
+
+                            <!-- OS Architecture -->
+                            <ng-container matColumnDef="osArchitecture">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="OS Architecture">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">
+                                        OS Architecture
+                                    </div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.snapshot.versionInfo.osArchitecture">
+                                    {{ item.snapshot.versionInfo.osArchitecture }}
+                                </td>
+                            </ng-container>
+
+                            <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
+                            <tr
+                                mat-row
+                                *matRowDef="let row; let even = even; columns: displayedColumns"
+                                [class.even]="even"
+                                (click)="select(row)"
+                                [class.selected]="isSelected(row)"></tr>
+                        </table>
+                    </div>
+                </div>
+            </div>
+        </ng-container>
+    </div>
+</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-table/cluster-version-table.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-table/cluster-version-table.component.scss
new file mode 100644
index 0000000..c5dc8ca
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-table/cluster-version-table.component.scss
@@ -0,0 +1,26 @@
+/*!
+ * 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.
+ */
+
+.cluster-version-table {
+    .listing-table {
+        table {
+            .mat-column-address {
+                width: 25%;
+            }
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-table/cluster-version-table.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-table/cluster-version-table.component.spec.ts
new file mode 100644
index 0000000..a45cf09
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-table/cluster-version-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 { ClusterVersionTable } from './cluster-version-table.component';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+
+describe('ClusterVersionTable', () => {
+    let component: ClusterVersionTable;
+    let fixture: ComponentFixture<ClusterVersionTable>;
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [ClusterVersionTable, NoopAnimationsModule]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ClusterVersionTable);
+        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/cluster/ui/cluster-version-listing/cluster-version-table/cluster-version-table.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-table/cluster-version-table.component.ts
new file mode 100644
index 0000000..c0c5b49
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/cluster-version-listing/cluster-version-table/cluster-version-table.component.ts
@@ -0,0 +1,144 @@
+/*
+ * 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 {
+    ClusterTableFilter,
+    ClusterTableFilterColumn
+} from '../../common/cluster-table-filter/cluster-table-filter.component';
+import { MatTableModule } from '@angular/material/table';
+import { NiFiCommon } from '../../../../../service/nifi-common.service';
+import { NodeSnapshot } from '../../../../../state/system-diagnostics';
+import { ClusterTable } from '../../common/cluster-table/cluster-table.component';
+import { MatSortModule, Sort } from '@angular/material/sort';
+
+@Component({
+    selector: 'cluster-version-table',
+    standalone: true,
+    imports: [ClusterTableFilter, MatTableModule, MatSortModule],
+    templateUrl: './cluster-version-table.component.html',
+    styleUrl: './cluster-version-table.component.scss'
+})
+export class ClusterVersionTable extends ClusterTable<NodeSnapshot> {
+    filterableColumns: ClusterTableFilterColumn[] = [{ key: 'address', label: 'Address' }];
+
+    displayedColumns: string[] = [
+        'address',
+        'nifiVersion',
+        'javaVendor',
+        'javaVersion',
+        'osName',
+        'osVersion',
+        'osArchitecture'
+    ];
+
+    constructor(private nifiCommon: NiFiCommon) {
+        super();
+    }
+
+    formatNodeAddress(item: NodeSnapshot): string {
+        return `${item.address}:${item.apiPort}`;
+    }
+
+    override filterPredicate(item: NodeSnapshot, filter: string): boolean {
+        const { filterTerm, filterColumn } = JSON.parse(filter);
+        if (filterTerm === '') {
+            return true;
+        }
+
+        let field = '';
+        switch (filterColumn) {
+            case 'address':
+                field = this.formatNodeAddress(item);
+                break;
+        }
+        return this.nifiCommon.stringContains(field, filterTerm, true);
+    }
+
+    override sortEntities(data: NodeSnapshot[], sort: Sort): NodeSnapshot[] {
+        if (!data) {
+            return [];
+        }
+        return data.slice().sort((a, b) => {
+            const isAsc = sort.direction === 'asc';
+            let retVal = 0;
+            switch (sort.active) {
+                case 'address':
+                    retVal = this.nifiCommon.compareString(a.address, b.address);
+                    // check the port if the addresses are the same
+                    if (retVal === 0) {
+                        retVal = this.nifiCommon.compareNumber(a.apiPort, b.apiPort);
+                    }
+                    break;
+                case 'nifiVersion':
+                    retVal = this.nifiCommon.compareVersion(
+                        a.snapshot.versionInfo.niFiVersion,
+                        b.snapshot.versionInfo.niFiVersion
+                    );
+                    break;
+                case 'javaVendor':
+                    retVal = this.nifiCommon.compareString(
+                        a.snapshot.versionInfo.javaVendor,
+                        b.snapshot.versionInfo.javaVendor
+                    );
+                    break;
+                case 'javaVersion':
+                    retVal = this.nifiCommon.compareVersion(
+                        a.snapshot.versionInfo.javaVersion,
+                        b.snapshot.versionInfo.javaVersion
+                    );
+                    break;
+                case 'osName':
+                    retVal = this.nifiCommon.compareString(
+                        a.snapshot.versionInfo.osName,
+                        b.snapshot.versionInfo.osName
+                    );
+                    break;
+                case 'osVersion':
+                    retVal = this.nifiCommon.compareVersion(
+                        a.snapshot.versionInfo.osVersion,
+                        b.snapshot.versionInfo.osVersion
+                    );
+                    break;
+                case 'osArchitecture':
+                    retVal = this.nifiCommon.compareString(
+                        a.snapshot.versionInfo.osArchitecture,
+                        b.snapshot.versionInfo.osArchitecture
+                    );
+                    break;
+                default:
+                    retVal = 0;
+            }
+            return retVal * (isAsc ? 1 : -1);
+        });
+    }
+
+    override supportsMultiValuedSort(): boolean {
+        return false;
+    }
+
+    select(item: NodeSnapshot): any {
+        this.selectComponent.next(item);
+    }
+
+    isSelected(item: NodeSnapshot): boolean {
+        if (this.selectedId) {
+            return this.selectedId === item.nodeId;
+        }
+        return false;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/cluster-table-filter/cluster-table-filter.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/cluster-table-filter/cluster-table-filter.component.html
new file mode 100644
index 0000000..d5a29ae
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/cluster-table-filter/cluster-table-filter.component.html
@@ -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.
+-->
+
+<div>
+    <div class="accent-color font-medium {{ showFilterMatchedLabel ? 'visible' : 'invisible' }}">
+        Filter matched {{ filteredCount }} of {{ totalCount }}
+    </div>
+    <form [formGroup]="filterForm">
+        <div class="flex pt-1 gap-1 items-baseline">
+            <div>
+                <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">
+                        @for (option of filterableColumns; track option) {
+                            <mat-option [value]="option.key"> {{ option.label }}</mat-option>
+                        }
+                    </mat-select>
+                </mat-form-field>
+            </div>
+        </div>
+    </form>
+</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/cluster-table-filter/cluster-table-filter.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/cluster-table-filter/cluster-table-filter.component.scss
new file mode 100644
index 0000000..b33f7ca
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/cluster-table-filter/cluster-table-filter.component.scss
@@ -0,0 +1,16 @@
+/*!
+ * 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.
+ */
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/cluster-table-filter/cluster-table-filter.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/cluster-table-filter/cluster-table-filter.component.spec.ts
new file mode 100644
index 0000000..c3c26c1
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/cluster-table-filter/cluster-table-filter.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 { ClusterTableFilter } from './cluster-table-filter.component';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+
+describe('ClusterTableFilter', () => {
+    let component: ClusterTableFilter;
+    let fixture: ComponentFixture<ClusterTableFilter>;
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [ClusterTableFilter, NoopAnimationsModule]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ClusterTableFilter);
+        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/cluster/ui/common/cluster-table-filter/cluster-table-filter.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/cluster-table-filter/cluster-table-filter.component.ts
new file mode 100644
index 0000000..6ba422b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/cluster-table-filter/cluster-table-filter.component.ts
@@ -0,0 +1,137 @@
+/*
+ * 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, DestroyRef, EventEmitter, inject, Input, Output } from '@angular/core';
+import { MatCheckbox } from '@angular/material/checkbox';
+import { MatFormField, MatLabel } from '@angular/material/form-field';
+import { MatInput } from '@angular/material/input';
+import { MatOption } from '@angular/material/autocomplete';
+import { MatSelect } from '@angular/material/select';
+import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { debounceTime } from 'rxjs';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+
+export interface ClusterTableFilterColumn {
+    key: string;
+    label: string;
+}
+
+export interface ClusterTableFilterArgs {
+    filterTerm: string;
+    filterColumn: string;
+}
+
+export interface ClusterTableFilterContext extends ClusterTableFilterArgs {
+    changedField: string;
+}
+
+@Component({
+    selector: 'cluster-table-filter',
+    standalone: true,
+    imports: [MatCheckbox, MatFormField, MatInput, MatLabel, MatOption, MatSelect, ReactiveFormsModule],
+    templateUrl: './cluster-table-filter.component.html',
+    styleUrl: './cluster-table-filter.component.scss'
+})
+export class ClusterTableFilter implements AfterViewInit {
+    filterForm: FormGroup;
+    private _filteredCount = 0;
+    private _totalCount = 0;
+    private _initialFilterColumn = 'name';
+    private _filterableColumns: ClusterTableFilterColumn[] = [];
+    private destroyRef: DestroyRef = inject(DestroyRef);
+
+    showFilterMatchedLabel = false;
+
+    @Input() set filterableColumns(filterableColumns: ClusterTableFilterColumn[]) {
+        this._filterableColumns = filterableColumns;
+    }
+
+    get filterableColumns(): ClusterTableFilterColumn[] {
+        return this._filterableColumns;
+    }
+
+    @Input() includeStatusFilter = false;
+
+    @Input() set filterTerm(term: string) {
+        this.filterForm.get('filterTerm')?.value(term);
+    }
+
+    @Input() set filterColumn(column: string) {
+        this._initialFilterColumn = column;
+        if (this.filterableColumns?.length > 0) {
+            if (this.filterableColumns.findIndex((col) => col.key === column) >= 0) {
+                this.filterForm.get('filterColumn')?.setValue(column);
+            } else {
+                this.filterForm.get('filterColumn')?.setValue(this.filterableColumns[0].key);
+            }
+        } else {
+            this.filterForm.get('filterColumn')?.setValue(this._initialFilterColumn);
+        }
+    }
+
+    @Input() set filteredCount(count: number) {
+        this._filteredCount = count;
+    }
+
+    get filteredCount(): number {
+        return this._filteredCount;
+    }
+
+    @Input() set totalCount(total: number) {
+        this._totalCount = total;
+    }
+
+    get totalCount(): number {
+        return this._totalCount;
+    }
+
+    @Output() filterChanged: EventEmitter<ClusterTableFilterContext> = new EventEmitter<ClusterTableFilterContext>();
+
+    constructor(private formBuilder: FormBuilder) {
+        this.filterForm = this.formBuilder.group({
+            filterTerm: '',
+            filterColumn: this._initialFilterColumn || 'address'
+        });
+    }
+
+    ngAfterViewInit(): void {
+        this.filterForm
+            .get('filterTerm')
+            ?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed(this.destroyRef))
+            .subscribe((filterTerm: string) => {
+                const filterColumn = this.filterForm.get('filterColumn')?.value;
+                this.applyFilter(filterTerm, filterColumn, 'filterTerm');
+            });
+
+        this.filterForm
+            .get('filterColumn')
+            ?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
+            .subscribe((filterColumn: string) => {
+                const filterTerm = this.filterForm.get('filterTerm')?.value;
+                this.applyFilter(filterTerm, filterColumn, 'filterColumn');
+            });
+    }
+
+    applyFilter(filterTerm: string, filterColumn: string, changedField: string) {
+        this.filterChanged.next({
+            filterColumn,
+            filterTerm,
+            changedField
+        });
+        this.showFilterMatchedLabel = filterTerm?.length > 0;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/cluster-table/cluster-table.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/cluster-table/cluster-table.component.ts
new file mode 100644
index 0000000..b441ae4
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/cluster-table/cluster-table.component.ts
@@ -0,0 +1,163 @@
+/*
+ * 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, Input, Output } from '@angular/core';
+import { MatTableDataSource, MatTableModule } from '@angular/material/table';
+import { MatSortModule, Sort, SortDirection } from '@angular/material/sort';
+import { MultiSort } from '../../../../summary/ui/common';
+import { ClusterTableFilterContext } from '../cluster-table-filter/cluster-table-filter.component';
+
+@Component({
+    selector: 'cluster-table',
+    standalone: true,
+    imports: [MatTableModule, MatSortModule],
+    template: ''
+})
+export abstract class ClusterTable<T> {
+    private _listingStatus: string | null = null;
+    private _loadedTimestamp: string | null = null;
+    private _initialSortColumn!: string;
+    private _initialSortDirection: SortDirection = 'asc';
+    private _selectedId: string | null = null;
+    private _selectedRepoId: string | null = null;
+
+    totalCount = 0;
+    filteredCount = 0;
+
+    multiSort: MultiSort = {
+        active: this._initialSortColumn,
+        direction: this._initialSortDirection,
+        sortValueIndex: 0,
+        totalValues: 2
+    };
+
+    dataSource: MatTableDataSource<T> = new MatTableDataSource<T>();
+
+    abstract sortEntities(data: T[], sort: Sort): T[];
+
+    abstract supportsMultiValuedSort(sort: Sort): boolean;
+
+    abstract filterPredicate(item: T, filter: string): boolean;
+
+    sortData(sort: Sort) {
+        this.setMultiSort(sort);
+        this.dataSource.data = this.sortEntities(this.dataSource.data, sort);
+    }
+
+    @Input() set initialSortColumn(initialSortColumn: string) {
+        this._initialSortColumn = initialSortColumn;
+        this.multiSort = { ...this.multiSort, active: initialSortColumn };
+    }
+
+    get initialSortColumn() {
+        return this._initialSortColumn;
+    }
+
+    @Input() set initialSortDirection(initialSortDirection: SortDirection) {
+        this._initialSortDirection = initialSortDirection;
+        this.multiSort = { ...this.multiSort, direction: initialSortDirection };
+    }
+
+    get initialSortDirection() {
+        return this._initialSortDirection;
+    }
+
+    @Input() set components(components: T[]) {
+        if (components) {
+            this.dataSource.data = this.sortEntities(components, this.multiSort);
+            this.dataSource.filterPredicate = (data: T, filter: string) => this.filterPredicate(data, filter);
+
+            this.totalCount = components.length;
+            if (this.dataSource.filteredData.length > 0) {
+                this.filteredCount = this.dataSource.filteredData.length;
+            } else {
+                this.filteredCount = components.length;
+            }
+        }
+    }
+
+    @Input() set loadedTimestamp(value: string | null) {
+        this._loadedTimestamp = value;
+    }
+
+    get loadedTimestamp(): string | null {
+        return this._loadedTimestamp;
+    }
+
+    @Input() set listingStatus(value: string | null) {
+        this._listingStatus = value;
+    }
+
+    get listingStatus(): string | null {
+        return this._listingStatus;
+    }
+
+    @Input() set selectedId(selectedId: string | null) {
+        this._selectedId = selectedId;
+    }
+
+    get selectedId(): string | null {
+        return this._selectedId;
+    }
+
+    @Input() set selectedRepositoryId(selectedRepositoryId: string | null) {
+        this._selectedRepoId = selectedRepositoryId;
+    }
+
+    get selectedRepositoryId(): string | null {
+        return this._selectedRepoId;
+    }
+
+    @Output() selectComponent: EventEmitter<T> = new EventEmitter<T>();
+    @Output() clearSelection: EventEmitter<void> = new EventEmitter<void>();
+
+    selectNone() {
+        this.clearSelection.next();
+    }
+
+    applyFilter(filter: ClusterTableFilterContext) {
+        if (!filter || !this.dataSource) {
+            return;
+        }
+
+        this.dataSource.filter = JSON.stringify(filter);
+        this.filteredCount = this.dataSource.filteredData.length;
+        this.selectNone();
+    }
+
+    private setMultiSort(sort: Sort) {
+        const { active, direction, sortValueIndex, totalValues } = this.multiSort;
+
+        if (this.supportsMultiValuedSort(sort)) {
+            if (active === sort.active) {
+                // previous sort was of the same column
+                if (direction === 'desc' && sort.direction === 'asc') {
+                    // change from previous index to the next
+                    const newIndex = sortValueIndex + 1 >= totalValues ? 0 : sortValueIndex + 1;
+                    this.multiSort = { ...sort, sortValueIndex: newIndex, totalValues };
+                } else {
+                    this.multiSort = { ...sort, sortValueIndex, totalValues };
+                }
+            } else {
+                // sorting a different column, just reset
+                this.multiSort = { ...sort, sortValueIndex: 0, totalValues };
+            }
+        } else {
+            this.multiSort = { ...sort, sortValueIndex: 0, totalValues };
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/repository-storage-table/repository-storage-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/repository-storage-table/repository-storage-table.component.html
new file mode 100644
index 0000000..1e58763
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/repository-storage-table/repository-storage-table.component.html
@@ -0,0 +1,112 @@
+<!--
+  ~  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">
+    <div class="flex-1">
+        <ng-container>
+            <div class="repository-storage-table h-full flex flex-col">
+                <!-- allow filtering of the table -->
+                <cluster-table-filter
+                    [filteredCount]="filteredCount"
+                    [totalCount]="totalCount"
+                    [filterableColumns]="filterableColumns"
+                    filterColumn="address"
+                    (filterChanged)="applyFilter($event)"></cluster-table-filter>
+
+                <div class="flex-1 relative">
+                    <div class="listing-table overflow-y-auto absolute inset-0">
+                        <table
+                            mat-table
+                            [dataSource]="dataSource"
+                            matSort
+                            matSortDisableClear
+                            (matSortChange)="sortData($event)"
+                            [matSortActive]="initialSortColumn"
+                            [matSortDirection]="initialSortDirection">
+                            <!-- Node Address -->
+                            <ng-container matColumnDef="address">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Node Address">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Node Address</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="formatNodeAddress(item)">
+                                    {{ formatNodeAddress(item) }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Repository -->
+                            <ng-container matColumnDef="repository">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Repository">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Repository</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.repositoryStorageUsage.identifier">
+                                    {{ item.repositoryStorageUsage.identifier }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Total Space -->
+                            <ng-container matColumnDef="totalSpace">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Total Space">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Total Space</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.repositoryStorageUsage.totalSpace">
+                                    {{ item.repositoryStorageUsage.totalSpace }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Used Space -->
+                            <ng-container matColumnDef="usedSpace">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Used Space">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Used Space</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.repositoryStorageUsage.usedSpace">
+                                    {{ item.repositoryStorageUsage.usedSpace }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Free Space -->
+                            <ng-container matColumnDef="freeSpace">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Free Space">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Free Space</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.repositoryStorageUsage.freeSpace">
+                                    {{ item.repositoryStorageUsage.freeSpace }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Utilization -->
+                            <ng-container matColumnDef="utilization">
+                                <th mat-header-cell *matHeaderCellDef mat-sort-header title="Utilization">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Utilization</div>
+                                </th>
+                                <td mat-cell *matCellDef="let item" [title]="item.repositoryStorageUsage.utilization">
+                                    {{ item.repositoryStorageUsage.utilization }}
+                                </td>
+                            </ng-container>
+
+                            <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
+                            <tr
+                                mat-row
+                                *matRowDef="let row; let even = even; columns: displayedColumns"
+                                [class.even]="even"
+                                (click)="select(row)"
+                                [class.selected]="isSelected(row)"></tr>
+                        </table>
+                    </div>
+                </div>
+            </div>
+        </ng-container>
+    </div>
+</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/repository-storage-table/repository-storage-table.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/repository-storage-table/repository-storage-table.component.scss
new file mode 100644
index 0000000..36bb579
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/repository-storage-table/repository-storage-table.component.scss
@@ -0,0 +1,26 @@
+/*!
+ * 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.
+ */
+
+.repository-storage-table {
+    .listing-table {
+        table {
+            .mat-column-address {
+                width: 25%;
+            }
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/repository-storage-table/repository-storage-table.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/repository-storage-table/repository-storage-table.component.spec.ts
new file mode 100644
index 0000000..8f3196b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/repository-storage-table/repository-storage-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 { RepositoryStorageTable } from './repository-storage-table.component';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+
+describe('RepositoryStorageTable', () => {
+    let component: RepositoryStorageTable;
+    let fixture: ComponentFixture<RepositoryStorageTable>;
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [RepositoryStorageTable, NoopAnimationsModule]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(RepositoryStorageTable);
+        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/cluster/ui/common/repository-storage-table/repository-storage-table.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/repository-storage-table/repository-storage-table.component.ts
new file mode 100644
index 0000000..bed1ff6
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/cluster/ui/common/repository-storage-table/repository-storage-table.component.ts
@@ -0,0 +1,145 @@
+/*
+ * 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, Input } from '@angular/core';
+import { ClusterTable } from '../cluster-table/cluster-table.component';
+import { ClusterNodeRepositoryStorageUsage } from '../../../../../state/system-diagnostics';
+import { MatSortModule, Sort } from '@angular/material/sort';
+import { NiFiCommon } from '../../../../../service/nifi-common.service';
+import { ClusterTableFilter, ClusterTableFilterColumn } from '../cluster-table-filter/cluster-table-filter.component';
+import { MatTableModule } from '@angular/material/table';
+
+@Component({
+    selector: 'repository-storage-table',
+    standalone: true,
+    imports: [ClusterTableFilter, MatTableModule, MatSortModule],
+    templateUrl: './repository-storage-table.component.html',
+    styleUrl: './repository-storage-table.component.scss'
+})
+export class RepositoryStorageTable extends ClusterTable<ClusterNodeRepositoryStorageUsage> {
+    filterableColumns: ClusterTableFilterColumn[] = [{ key: 'address', label: 'Address' }];
+    displayedColumns: string[] = ['address', 'totalSpace', 'usedSpace', 'freeSpace', 'utilization'];
+
+    @Input() set showRepoIdentifier(value: boolean) {
+        if (value) {
+            if (this.filterableColumns.length === 1) {
+                this.filterableColumns.push({ key: 'repository', label: 'repository' });
+                this.displayedColumns.splice(1, 0, 'repository');
+            }
+        } else {
+            if (this.filterableColumns.length > 1) {
+                this.filterableColumns.splice(this.filterableColumns.length - 1, 1);
+                this.displayedColumns.splice(1, 1);
+            }
+        }
+    }
+
+    constructor(private nifiCommon: NiFiCommon) {
+        super();
+    }
+
+    override filterPredicate(item: ClusterNodeRepositoryStorageUsage, filter: string): boolean {
+        const { filterTerm, filterColumn } = JSON.parse(filter);
+        if (filterTerm === '') {
+            return true;
+        }
+
+        let field = '';
+        switch (filterColumn) {
+            case 'address':
+                field = this.formatNodeAddress(item);
+                break;
+            case 'repository':
+                field = item.repositoryStorageUsage.identifier || '';
+                break;
+        }
+        return this.nifiCommon.stringContains(field, filterTerm, true);
+    }
+
+    override sortEntities(data: ClusterNodeRepositoryStorageUsage[], sort: Sort): ClusterNodeRepositoryStorageUsage[] {
+        if (!data) {
+            return [];
+        }
+        return data.slice().sort((a, b) => {
+            const isAsc = sort.direction === 'asc';
+            let retVal = 0;
+            switch (sort.active) {
+                case 'address':
+                    retVal = this.nifiCommon.compareString(a.address, b.address);
+                    // check the port if the addresses are the same
+                    if (retVal === 0) {
+                        retVal = this.nifiCommon.compareNumber(a.apiPort, b.apiPort);
+                    }
+                    break;
+                case 'totalSpace':
+                    retVal = this.nifiCommon.compareNumber(
+                        a.repositoryStorageUsage.totalSpaceBytes,
+                        b.repositoryStorageUsage.totalSpaceBytes
+                    );
+                    break;
+                case 'usedSpace':
+                    retVal = this.nifiCommon.compareNumber(
+                        a.repositoryStorageUsage.usedSpaceBytes,
+                        b.repositoryStorageUsage.usedSpaceBytes
+                    );
+                    break;
+                case 'freeSpace':
+                    retVal = this.nifiCommon.compareNumber(
+                        a.repositoryStorageUsage.freeSpaceBytes,
+                        b.repositoryStorageUsage.freeSpaceBytes
+                    );
+                    break;
+                case 'utilization':
+                    retVal = this.nifiCommon.compareNumber(
+                        a.repositoryStorageUsage.usedSpaceBytes / a.repositoryStorageUsage.totalSpaceBytes,
+                        b.repositoryStorageUsage.usedSpaceBytes / b.repositoryStorageUsage.totalSpaceBytes
+                    );
+                    break;
+                case 'repository':
+                    retVal = this.nifiCommon.compareString(
+                        a.repositoryStorageUsage.identifier,
+                        b.repositoryStorageUsage.identifier
+                    );
+                    break;
+                default:
+                    retVal = 0;
+            }
+            return retVal * (isAsc ? 1 : -1);
+        });
+    }
+
+    override supportsMultiValuedSort(): boolean {
+        return false;
+    }
+
+    formatNodeAddress(item: ClusterNodeRepositoryStorageUsage): string {
+        return `${item.address}:${item.apiPort}`;
+    }
+
+    select(item: ClusterNodeRepositoryStorageUsage): any {
+        this.selectComponent.next(item);
+    }
+
+    isSelected(item: ClusterNodeRepositoryStorageUsage): boolean {
+        if (this.selectedId && this.selectedRepositoryId) {
+            return (
+                this.selectedId === item.nodeId && this.selectedRepositoryId === item.repositoryStorageUsage.identifier
+            );
+        }
+        return false;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-configuration-history/ui/flow-configuration-history-listing/flow-configuration-history-table/flow-configuration-history-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-configuration-history/ui/flow-configuration-history-listing/flow-configuration-history-table/flow-configuration-history-table.component.html
index 2bf6639..8b57673 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-configuration-history/ui/flow-configuration-history-listing/flow-configuration-history-table/flow-configuration-history-table.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-configuration-history/ui/flow-configuration-history-listing/flow-configuration-history-table/flow-configuration-history-table.component.html
@@ -44,7 +44,7 @@
             <!-- Name Column -->
             <ng-container matColumnDef="timestamp">
                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Date/Time</div>
+                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Date/Time</div>
                 </th>
                 <td mat-cell *matCellDef="let item" [title]="formatTimestamp(item)">
                     {{ formatTimestamp(item) }}
@@ -54,7 +54,7 @@
             <!-- Name Column -->
             <ng-container matColumnDef="sourceName">
                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
+                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
                 </th>
                 <td mat-cell *matCellDef="let item" [title]="formatName(item)">
                     <span
@@ -67,7 +67,7 @@
 
             <ng-container matColumnDef="sourceType">
                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Type</div>
+                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Type</div>
                 </th>
                 <td mat-cell *matCellDef="let item" [title]="formatType(item)">
                     {{ formatType(item) }}
@@ -76,7 +76,7 @@
 
             <ng-container matColumnDef="operation">
                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Operation</div>
+                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Operation</div>
                 </th>
                 <td mat-cell *matCellDef="let item" [title]="formatOperation(item)">
                     {{ formatOperation(item) }}
@@ -85,7 +85,7 @@
 
             <ng-container matColumnDef="userIdentity">
                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">User</div>
+                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">User</div>
                 </th>
                 <td mat-cell *matCellDef="let item" [title]="formatUser(item)">
                     {{ formatUser(item) }}
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/canvas.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/canvas.component.ts
index 4b0418d..1e545fc 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/canvas.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/canvas.component.ts
@@ -66,11 +66,6 @@
 import { concatLatestFrom } from '@ngrx/operators';
 import { selectUrl } from '../../../../state/router/router.selectors';
 import { Storage } from '../../../../service/storage.service';
-import {
-    loadClusterSummary,
-    startClusterSummaryPolling,
-    stopClusterSummaryPolling
-} from '../../../../state/cluster-summary/cluster-summary.actions';
 
 @Component({
     selector: 'fd-canvas',
@@ -287,9 +282,7 @@
         this.createSvg();
         this.canvasView.init(this.svg, this.canvas);
 
-        this.store.dispatch(loadClusterSummary());
         this.store.dispatch(startProcessGroupPolling());
-        this.store.dispatch(startClusterSummaryPolling());
     }
 
     private createSvg(): void {
@@ -599,6 +592,5 @@
     ngOnDestroy(): void {
         this.store.dispatch(resetFlowState());
         this.store.dispatch(stopProcessGroupPolling());
-        this.store.dispatch(stopClusterSummaryPolling());
     }
 }
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 5be3bff..d4323c8 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
@@ -32,6 +32,10 @@
 import { RouterTestingModule } from '@angular/router/testing';
 import { ClusterSummary } from '../../../../../state/cluster-summary';
 import { selectClusterSummary } from '../../../../../state/cluster-summary/cluster-summary.selectors';
+import { selectCurrentUser } from '../../../../../state/current-user/current-user.selectors';
+import * as fromUser from '../../../../../state/current-user/current-user.reducer';
+import { selectFlowConfiguration } from '../../../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../../../state/flow-configuration/flow-configuration.reducer';
 
 describe('HeaderComponent', () => {
     let component: HeaderComponent;
@@ -108,6 +112,14 @@
                         {
                             selector: selectControllerBulletins,
                             value: []
+                        },
+                        {
+                            selector: selectCurrentUser,
+                            value: fromUser.initialState.user
+                        },
+                        {
+                            selector: selectFlowConfiguration,
+                            value: fromFlowConfiguration.initialState.flowConfiguration
                         }
                     ]
                 })
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/parameter-contexts/feature/parameter-contexts.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/parameter-contexts/feature/parameter-contexts.component.ts
index 1e5faa9..8870220 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/parameter-contexts/feature/parameter-contexts.component.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/parameter-contexts/feature/parameter-contexts.component.ts
@@ -15,29 +15,11 @@
  * limitations under the License.
  */
 
-import { Component, OnDestroy, OnInit } from '@angular/core';
-import { Store } from '@ngrx/store';
-import { NiFiState } from '../../../state';
-import {
-    loadClusterSummary,
-    startClusterSummaryPolling,
-    stopClusterSummaryPolling
-} from '../../../state/cluster-summary/cluster-summary.actions';
+import { Component } from '@angular/core';
 
 @Component({
     selector: 'parameter-contexts',
     templateUrl: './parameter-contexts.component.html',
     styleUrls: ['./parameter-contexts.component.scss']
 })
-export class ParameterContexts implements OnInit, OnDestroy {
-    constructor(private store: Store<NiFiState>) {}
-
-    ngOnInit(): void {
-        this.store.dispatch(loadClusterSummary());
-        this.store.dispatch(startClusterSummaryPolling());
-    }
-
-    ngOnDestroy(): void {
-        this.store.dispatch(stopClusterSummaryPolling());
-    }
-}
+export class ParameterContexts {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/feature/settings.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/feature/settings.component.ts
index 10569fe..7e0dc39 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/feature/settings.component.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/feature/settings.component.ts
@@ -15,22 +15,17 @@
  * limitations under the License.
  */
 
-import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
 import { Store } from '@ngrx/store';
 import { NiFiState } from '../../../state';
 import { loadExtensionTypesForSettings } from '../../../state/extension-types/extension-types.actions';
-import {
-    loadClusterSummary,
-    startClusterSummaryPolling,
-    stopClusterSummaryPolling
-} from '../../../state/cluster-summary/cluster-summary.actions';
 
 @Component({
     selector: 'settings',
     templateUrl: './settings.component.html',
     styleUrls: ['./settings.component.scss']
 })
-export class Settings implements OnInit, OnDestroy {
+export class Settings implements OnInit {
     tabLinks: any[] = [
         {
             label: 'General',
@@ -61,12 +56,6 @@
     constructor(private store: Store<NiFiState>) {}
 
     ngOnInit(): void {
-        this.store.dispatch(loadClusterSummary());
-        this.store.dispatch(startClusterSummaryPolling());
         this.store.dispatch(loadExtensionTypesForSettings());
     }
-
-    ngOnDestroy(): void {
-        this.store.dispatch(stopClusterSummaryPolling());
-    }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/parameter-providers/parameter-providers-table/parameter-providers-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/parameter-providers/parameter-providers-table/parameter-providers-table.component.html
index be05fed..136bfac 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/parameter-providers/parameter-providers-table/parameter-providers-table.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/parameter-providers/parameter-providers-table/parameter-providers-table.component.html
@@ -55,7 +55,7 @@
                     <!-- Name Column -->
                     <ng-container matColumnDef="name">
                         <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                            <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
+                            <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
                         </th>
                         <td mat-cell *matCellDef="let item" [title]="formatName(item)">
                             <div
@@ -72,7 +72,7 @@
                     <!-- Type column -->
                     <ng-container matColumnDef="type">
                         <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                            <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Type</div>
+                            <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Type</div>
                         </th>
                         <td mat-cell *matCellDef="let item" [title]="formatType(item)">
                             {{ formatType(item) }}
@@ -82,7 +82,7 @@
                     <!-- Bundle column -->
                     <ng-container matColumnDef="bundle">
                         <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                            <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Bundle</div>
+                            <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Bundle</div>
                         </th>
                         <td mat-cell *matCellDef="let item" [title]="formatBundle(item)">
                             {{ formatBundle(item) }}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/connection-cluster-table/connection-cluster-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/connection-cluster-table/connection-cluster-table.component.html
index 786742a..427f878 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/connection-cluster-table/connection-cluster-table.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/connection-cluster-table/connection-cluster-table.component.html
@@ -28,7 +28,7 @@
             <!-- Node Column -->
             <ng-container matColumnDef="node">
                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Node</div>
+                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Node</div>
                 </th>
                 <td mat-cell *matCellDef="let item" [title]="formatNode(item)">
                     <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">{{ formatNode(item) }}</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/port-cluster-table/port-cluster-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/port-cluster-table/port-cluster-table.component.html
index 343ca89..6eb3666 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/port-cluster-table/port-cluster-table.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/port-cluster-table/port-cluster-table.component.html
@@ -28,7 +28,7 @@
             <!-- Node Column -->
             <ng-container matColumnDef="node">
                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Node</div>
+                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Node</div>
                 </th>
                 <td mat-cell *matCellDef="let item" [title]="formatNode(item)">
                     <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">{{ formatNode(item) }}</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/process-group-cluster-table/process-group-cluster-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/process-group-cluster-table/process-group-cluster-table.component.html
index 7e9303d..5e0143d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/process-group-cluster-table/process-group-cluster-table.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/process-group-cluster-table/process-group-cluster-table.component.html
@@ -28,7 +28,7 @@
             <!-- Node Column -->
             <ng-container matColumnDef="node">
                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Node</div>
+                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Node</div>
                 </th>
                 <td mat-cell *matCellDef="let item" [title]="formatNode(item)">
                     <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">{{ formatNode(item) }}</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/processor-cluster-table/processor-cluster-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/processor-cluster-table/processor-cluster-table.component.html
index bc1a591..2f131fc 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/processor-cluster-table/processor-cluster-table.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/processor-cluster-table/processor-cluster-table.component.html
@@ -28,7 +28,7 @@
             <!-- Node Column -->
             <ng-container matColumnDef="node">
                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Node</div>
+                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Node</div>
                 </th>
                 <td mat-cell *matCellDef="let item" [title]="formatNode(item)">
                     <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">{{ formatNode(item) }}</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/remote-process-group-cluster-table/remote-process-group-cluster-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/remote-process-group-cluster-table/remote-process-group-cluster-table.component.html
index 8cce945..66ece8a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/remote-process-group-cluster-table/remote-process-group-cluster-table.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/remote-process-group-cluster-table/remote-process-group-cluster-table.component.html
@@ -28,7 +28,7 @@
             <!-- Node Column -->
             <ng-container matColumnDef="node">
                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Node</div>
+                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Node</div>
                 </th>
                 <td mat-cell *matCellDef="let item" [title]="formatNode(item)">
                     <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">{{ formatNode(item) }}</div>
@@ -38,7 +38,7 @@
             <!-- Target URI Column -->
             <ng-container matColumnDef="uri">
                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Target URI</div>
+                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Target URI</div>
                 </th>
                 <td mat-cell *matCellDef="let item" [title]="formatUri(item)">
                     {{ formatUri(item) }}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/port-status-table/port-status-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/port-status-table/port-status-table.component.html
index 4555af4..88fdcf2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/port-status-table/port-status-table.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/common/port-status-table/port-status-table.component.html
@@ -50,7 +50,7 @@
                             <!-- Name Column -->
                             <ng-container matColumnDef="name">
                                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
                                 </th>
                                 <td mat-cell *matCellDef="let item" [title]="formatName(item)">
                                     {{ formatName(item) }}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/connection-status-listing/connection-status-table/connection-status-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/connection-status-listing/connection-status-table/connection-status-table.component.html
index 6ea1108..634b33e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/connection-status-listing/connection-status-table/connection-status-table.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/connection-status-listing/connection-status-table/connection-status-table.component.html
@@ -51,7 +51,7 @@
                             <!-- Name Column -->
                             <ng-container matColumnDef="name">
                                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
                                 </th>
                                 <td mat-cell *matCellDef="let item" [title]="formatName(item)">
                                     {{ formatName(item) }}
@@ -153,9 +153,7 @@
                             <!-- Source Column -->
                             <ng-container matColumnDef="sourceName">
                                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">
-                                        From Source
-                                    </div>
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">From Source</div>
                                 </th>
                                 <td mat-cell *matCellDef="let item" [title]="formatSource(item)">
                                     {{ formatSource(item) }}
@@ -194,7 +192,7 @@
                             <!-- Destination Column -->
                             <ng-container matColumnDef="destinationName">
                                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">
                                         To Destination
                                     </div>
                                 </th>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/process-group-status-listing/process-group-status-table/process-group-status-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/process-group-status-listing/process-group-status-table/process-group-status-table.component.html
index aed52d0..62bb03c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/process-group-status-listing/process-group-status-table/process-group-status-table.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/process-group-status-listing/process-group-status-table/process-group-status-table.component.html
@@ -59,10 +59,10 @@
                             <!-- Name Column -->
                             <ng-container matColumnDef="name">
                                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
                                 </th>
                                 <td mat-cell *matCellDef="let item" [title]="formatName(item)">
-                                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">
                                         {{ formatName(item) }}
                                     </div>
                                 </td>
@@ -71,9 +71,7 @@
                             <!-- Version State column -->
                             <ng-container matColumnDef="versionedFlowState">
                                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">
-                                        Version State
-                                    </div>
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Version State</div>
                                 </th>
                                 <td mat-cell *matCellDef="let item">
                                     <div class="flex items-center gap-x-1.5" [title]="formatVersionedFlowState(item)">
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/processor-status-listing/processor-status-table/processor-status-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/processor-status-listing/processor-status-table/processor-status-table.component.html
index e37ecc0..726a09c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/processor-status-listing/processor-status-table/processor-status-table.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/processor-status-listing/processor-status-table/processor-status-table.component.html
@@ -59,7 +59,7 @@
                             <!-- Name Column -->
                             <ng-container matColumnDef="name">
                                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
                                 </th>
                                 <td mat-cell *matCellDef="let item" [title]="formatName(item)">
                                     <div class="flex align-middle">
@@ -80,10 +80,10 @@
                             <!-- Type column -->
                             <ng-container matColumnDef="type">
                                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Type</div>
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Type</div>
                                 </th>
                                 <td mat-cell *matCellDef="let item" [title]="formatType(item)">
-                                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">
                                         {{ formatType(item) }}
                                     </div>
                                 </td>
@@ -92,9 +92,7 @@
                             <!-- Process Group column -->
                             <ng-container matColumnDef="processGroup">
                                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">
-                                        Process Group
-                                    </div>
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Process Group</div>
                                 </th>
                                 <td mat-cell *matCellDef="let item" [title]="formatProcessGroup(item)">
                                     {{ formatProcessGroup(item) }}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/remote-process-group-status-listing/remote-process-group-status-table/remote-process-group-status-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/remote-process-group-status-listing/remote-process-group-status-table/remote-process-group-status-table.component.html
index 7ea2730..618ef42 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/remote-process-group-status-listing/remote-process-group-status-table/remote-process-group-status-table.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/summary/ui/remote-process-group-status-listing/remote-process-group-status-table/remote-process-group-status-table.component.html
@@ -51,7 +51,7 @@
                             <!-- Name Column -->
                             <ng-container matColumnDef="name">
                                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
                                 </th>
                                 <td mat-cell *matCellDef="let item" [title]="formatName(item)">
                                     {{ formatName(item) }}
@@ -61,9 +61,7 @@
                             <!-- Target URI Column -->
                             <ng-container matColumnDef="uri">
                                 <th mat-header-cell *matHeaderCellDef mat-sort-header>
-                                    <div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">
-                                        Target URI
-                                    </div>
+                                    <div class="overflow-ellipsis overflow-hidden whitespace-nowrap">Target URI</div>
                                 </th>
                                 <td mat-cell *matCellDef="let item" [title]="formatUri(item)">
                                     {{ formatUri(item) }}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/users/feature/users.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/users/feature/users.component.ts
index 0023350..ede226a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/users/feature/users.component.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/users/feature/users.component.ts
@@ -15,31 +15,20 @@
  * limitations under the License.
  */
 
-import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Component, OnDestroy } from '@angular/core';
 import { Store } from '@ngrx/store';
 import { NiFiState } from '../../../state';
 import { resetUsersState } from '../state/user-listing/user-listing.actions';
-import {
-    loadClusterSummary,
-    startClusterSummaryPolling,
-    stopClusterSummaryPolling
-} from '../../../state/cluster-summary/cluster-summary.actions';
 
 @Component({
     selector: 'users',
     templateUrl: './users.component.html',
     styleUrls: ['./users.component.scss']
 })
-export class Users implements OnInit, OnDestroy {
+export class Users implements OnDestroy {
     constructor(private store: Store<NiFiState>) {}
 
-    ngOnInit(): void {
-        this.store.dispatch(loadClusterSummary());
-        this.store.dispatch(startClusterSummaryPolling());
-    }
-
     ngOnDestroy(): void {
         this.store.dispatch(resetUsersState());
-        this.store.dispatch(stopClusterSummaryPolling());
     }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/guard/authorization.guard.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/guard/authorization.guard.ts
index 5708f48..3de1b62 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/guard/authorization.guard.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/guard/authorization.guard.ts
@@ -22,7 +22,10 @@
 import { CurrentUser, CurrentUserState } from '../../state/current-user';
 import { selectCurrentUser } from '../../state/current-user/current-user.selectors';
 
-export const authorizationGuard = (authorizationCheck: (user: CurrentUser) => boolean): CanMatchFn => {
+export const authorizationGuard = (
+    authorizationCheck: (user: CurrentUser) => boolean,
+    fallbackUrl?: string
+): CanMatchFn => {
     return () => {
         const router: Router = inject(Router);
         const store: Store<CurrentUserState> = inject(Store<CurrentUserState>);
@@ -34,7 +37,7 @@
                 }
 
                 // TODO - replace with 403 error page
-                return router.parseUrl('/');
+                return router.parseUrl(fallbackUrl || '/');
             })
         );
     };
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 9a7c7ab..5c7af76 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
@@ -288,8 +288,8 @@
      * @param a
      * @param b
      */
-    public compareString(a: string | null, b: string | null): number {
-        if (a === b) {
+    public compareString(a: string | null | undefined, b: string | null | undefined): number {
+        if (a == b) {
             return 0;
         }
         return (a || '').localeCompare(b || '');
@@ -301,8 +301,87 @@
      * @param a
      * @param b
      */
-    public compareNumber(a: number | null, b: number | null): number {
-        return (a || 0) - (b || 0);
+    public compareNumber(a: number | null | undefined, b: number | null | undefined): number {
+        // nulls last
+        return (
+            (this.isDefinedAndNotNull(a) ? a || 0 : Number.MIN_VALUE) -
+            (this.isDefinedAndNotNull(b) ? b || 0 : Number.MIN_VALUE)
+        );
+    }
+
+    public compareVersion(aRawVersion: string, bRawVersion: string): number {
+        if (aRawVersion === bRawVersion) {
+            return 0;
+        }
+
+        // attempt to parse the raw strings
+        const aTokens = aRawVersion.split(/-/);
+        const bTokens = bRawVersion.split(/-/);
+
+        // ensure there is at least one token
+        if (aTokens.length >= 1 && bTokens.length >= 1) {
+            const aVersionTokens = aTokens[0].split(/\./);
+            const bVersionTokens = bTokens[0].split(/\./);
+
+            // ensure both versions have at least one token
+            if (aVersionTokens.length >= 1 && bVersionTokens.length >= 1) {
+                // find the number of tokens a and b have in common
+                const commonTokenLength = Math.min(aVersionTokens.length, bVersionTokens.length);
+
+                // consider all tokens in common
+                for (let i = 0; i < commonTokenLength; i++) {
+                    const aVersionSegment = parseInt(aVersionTokens[i], 10);
+                    const bVersionSegment = parseInt(bVersionTokens[i], 10);
+
+                    // if both are non-numeric, consider the next token
+                    if (isNaN(aVersionSegment) && isNaN(bVersionSegment)) {
+                        continue;
+                    } else if (isNaN(aVersionSegment)) {
+                        // NaN is considered less
+                        return -1;
+                    } else if (isNaN(bVersionSegment)) {
+                        // NaN is considered less
+                        return 1;
+                    }
+
+                    // if a version at any point does not match
+                    if (aVersionSegment !== bVersionSegment) {
+                        return aVersionSegment - bVersionSegment;
+                    }
+                }
+
+                if (aVersionTokens.length === bVersionTokens.length) {
+                    if (aTokens.length === bTokens.length) {
+                        // same version for all tokens so consider the trailing bits (1.1-RC vs 1.1-SNAPSHOT)
+                        const aExtraBits = this.substringAfterFirst(aRawVersion, aTokens[0]);
+                        const bExtraBits = this.substringAfterFirst(bRawVersion, bTokens[0]);
+                        return aExtraBits === bExtraBits ? 0 : aExtraBits > bExtraBits ? 1 : -1;
+                    } else {
+                        // in this case, extra bits means it's consider less than no extra bits (1.1 vs 1.1-SNAPSHOT)
+                        return bTokens.length - aTokens.length;
+                    }
+                } else {
+                    // same version for all tokens in common (ie 1.1 vs 1.1.1)
+                    return aVersionTokens.length - bVersionTokens.length;
+                }
+            } else if (aVersionTokens.length >= 1) {
+                // presence of version tokens is considered greater
+                return 1;
+            } else if (bVersionTokens.length >= 1) {
+                // presence of version tokens is considered greater
+                return -1;
+            } else {
+                return 0;
+            }
+        } else if (aTokens.length >= 1) {
+            // presence of tokens is considered greater
+            return 1;
+        } else if (bTokens.length >= 1) {
+            // presence of tokens is considered greater
+            return -1;
+        } else {
+            return 0;
+        }
     }
 
     /**
@@ -375,7 +454,7 @@
      * @param {string} rawDateTime
      * @returns {Date}
      */
-    parseDateTime(rawDateTime: string): Date {
+    parseDateTime(rawDateTime: string | null | undefined): Date {
         // handle non date values
         if (!rawDateTime) {
             return new Date();
@@ -414,6 +493,45 @@
     }
 
     /**
+     * Parses the specified duration and returns the total number of millis.
+     *
+     * @param {string} rawDuration
+     * @returns {number}        The number of millis
+     */
+    parseDuration(rawDuration: string) {
+        const duration = rawDuration.split(/:/);
+
+        // ensure the appropriate number of tokens
+        if (duration.length !== 3) {
+            return 0;
+        }
+
+        // detect if there is millis
+        const seconds = duration[2].split(/\./);
+        if (seconds.length === 2) {
+            return new Date(
+                1970,
+                0,
+                1,
+                parseInt(duration[0], 10),
+                parseInt(duration[1], 10),
+                parseInt(seconds[0], 10),
+                parseInt(seconds[1], 10)
+            ).getTime();
+        } else {
+            return new Date(
+                1970,
+                0,
+                1,
+                parseInt(duration[0], 10),
+                parseInt(duration[1], 10),
+                parseInt(duration[2], 10),
+                0
+            ).getTime();
+        }
+    }
+
+    /**
      * Formats the specified duration.
      *
      * @param {number} millis in millis
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 5f81b66..4da0029 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
@@ -16,6 +16,7 @@
  */
 
 import { filter, Observable } from 'rxjs';
+import { GarbageCollection } from '../system-diagnostics';
 
 export function isDefinedAndNotNull<T>() {
     return (source$: Observable<null | undefined | T>) =>
@@ -269,6 +270,10 @@
     explicitRestrictions: ExplicitRestriction[];
 }
 
+export interface GarbageCollectionTipInput {
+    garbageCollections: GarbageCollection[];
+}
+
 export interface Permissions {
     canRead: boolean;
     canWrite: boolean;
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/system-diagnostics/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/system-diagnostics/index.ts
index 92b7408..dcc03c4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/system-diagnostics/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/system-diagnostics/index.ts
@@ -79,7 +79,7 @@
     totalHeap: string;
     totalHeapBytes: number;
     totalNonHeap: string;
-    totalNonHeapBytes: string;
+    totalNonHeapBytes: number;
     totalThreads: number;
     uptime: string;
     usedHeap: string;
@@ -103,3 +103,10 @@
     error: string | null;
     status: 'pending' | 'loading' | 'error' | 'success';
 }
+
+export interface ClusterNodeRepositoryStorageUsage {
+    address: string;
+    apiPort: number;
+    nodeId: string;
+    repositoryStorageUsage: RepositoryStorageUsage;
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/system-diagnostics/system-diagnostics.selectors.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/system-diagnostics/system-diagnostics.selectors.ts
index 7b96f86..a5f3680 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/system-diagnostics/system-diagnostics.selectors.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/system-diagnostics/system-diagnostics.selectors.ts
@@ -39,3 +39,8 @@
     selectSystemDiagnosticsState,
     (state: SystemDiagnosticsState) => state.status
 );
+
+export const selectSystemNodeSnapshots = createSelector(
+    selectSystemDiagnosticsState,
+    (state: SystemDiagnosticsState) => state.systemDiagnostics?.nodeSnapshots
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/advanced-ui/advanced-ui.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/advanced-ui/advanced-ui.component.spec.ts
index 25739ca..a388bb5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/advanced-ui/advanced-ui.component.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/advanced-ui/advanced-ui.component.spec.ts
@@ -23,6 +23,12 @@
 import { provideMockStore } from '@ngrx/store/testing';
 import { initialState } from '../../../state/documentation/documentation.reducer';
 import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { selectCurrentUser } from '../../../state/current-user/current-user.selectors';
+import * as fromUser from '../../../state/current-user/current-user.reducer';
+import { selectClusterSummary } from '../../../state/cluster-summary/cluster-summary.selectors';
+import * as fromClusterSummary from '../../../state/cluster-summary/cluster-summary.reducer';
+import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
 
 describe('AdvancedUi', () => {
     let component: AdvancedUi;
@@ -38,7 +44,25 @@
     beforeEach(() => {
         TestBed.configureTestingModule({
             imports: [AdvancedUi, HttpClientTestingModule, RouterTestingModule, MockNavigation],
-            providers: [provideMockStore({ initialState })]
+            providers: [
+                provideMockStore({
+                    initialState,
+                    selectors: [
+                        {
+                            selector: selectCurrentUser,
+                            value: fromUser.initialState.user
+                        },
+                        {
+                            selector: selectClusterSummary,
+                            value: fromClusterSummary.initialState
+                        },
+                        {
+                            selector: selectFlowConfiguration,
+                            value: fromFlowConfiguration.initialState.flowConfiguration
+                        }
+                    ]
+                })
+            ]
         });
         fixture = TestBed.createComponent(AdvancedUi);
         component = fixture.componentInstance;
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/advanced-ui/advanced-ui.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/advanced-ui/advanced-ui.component.ts
index eb59bda..0b3ce30 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/advanced-ui/advanced-ui.component.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/advanced-ui/advanced-ui.component.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { Component, OnDestroy, OnInit, SecurityContext } from '@angular/core';
+import { Component, SecurityContext } from '@angular/core';
 import { NiFiState } from '../../../state';
 import { Store } from '@ngrx/store';
 import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@@ -24,11 +24,6 @@
 import { Navigation } from '../navigation/navigation.component';
 import { selectRouteData } from '../../../state/router/router.selectors';
 import { AdvancedUiParams, isDefinedAndNotNull } from '../../../state/shared';
-import {
-    loadClusterSummary,
-    startClusterSummaryPolling,
-    stopClusterSummaryPolling
-} from '../../../state/cluster-summary/cluster-summary.actions';
 import { selectDisconnectionAcknowledged } from '../../../state/cluster-summary/cluster-summary.selectors';
 
 @Component({
@@ -38,7 +33,7 @@
     imports: [Navigation],
     styleUrls: ['./advanced-ui.component.scss']
 })
-export class AdvancedUi implements OnInit, OnDestroy {
+export class AdvancedUi {
     frameSource!: SafeResourceUrl | null;
 
     private params: AdvancedUiParams | null = null;
@@ -77,15 +72,6 @@
             });
     }
 
-    ngOnInit(): void {
-        this.store.dispatch(loadClusterSummary());
-        this.store.dispatch(startClusterSummaryPolling());
-    }
-
-    ngOnDestroy(): void {
-        this.store.dispatch(stopClusterSummaryPolling());
-    }
-
     private getFrameSource(params: AdvancedUiParams): SafeResourceUrl | null {
         const queryParams: string = new HttpParams()
             .set('id', params.id)
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.html
index fc4ebdf..73405af 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.html
@@ -29,7 +29,7 @@
             </div>
             <ng-content></ng-content>
         </div>
-        @if (currentUser$ | async; as user) {
+        @if (currentUser(); as user) {
             <div class="flex justify-between items-center gap-x-1">
                 <div class="flex flex-col justify-between items-end gap-y-1">
                     <div class="current-user">{{ user.identity }}</div>
@@ -90,10 +90,16 @@
                         <i class="fa fa-fw mr-2"></i>
                         Parameter Contexts
                     </button>
-                    <button mat-menu-item class="global-menu-item">
-                        <i class="fa fa-fw fa-cubes primary-color mr-2"></i>
-                        Cluster
-                    </button>
+                    @if (clusterSummary()?.clustered) {
+                        <button
+                            mat-menu-item
+                            class="global-menu-item"
+                            [disabled]="!user.controllerPermissions.canRead"
+                            [routerLink]="['/cluster']">
+                            <i class="fa fa-fw fa-cubes primary-color mr-2"></i>
+                            Cluster
+                        </button>
+                    }
                     <button mat-menu-item class="global-menu-item" [routerLink]="['/flow-configuration-history']">
                         <i class="fa fa-fw fa-history primary-color mr-2"></i>
                         Flow Configuration History
@@ -110,8 +116,8 @@
                         <i class="fa fa-fw mr-2"></i>
                         System Diagnostics
                     </button>
-                    @if (flowConfiguration$ | async; as flowConfiguration) {
-                        @if (flowConfiguration.supportsManagedAuthorizer) {
+                    @if (flowConfiguration(); as flowConfig) {
+                        @if (flowConfig.supportsManagedAuthorizer) {
                             <mat-divider></mat-divider>
                             <button
                                 mat-menu-item
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.spec.ts
index 2edfc45..05ea83f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.spec.ts
@@ -22,6 +22,12 @@
 import { initialState } from '../../../state/current-user/current-user.reducer';
 import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { RouterTestingModule } from '@angular/router/testing';
+import { selectCurrentUser } from '../../../state/current-user/current-user.selectors';
+import * as fromUser from '../../../state/current-user/current-user.reducer';
+import { selectClusterSummary } from '../../../state/cluster-summary/cluster-summary.selectors';
+import * as fromClusterSummary from '../../../state/cluster-summary/cluster-summary.reducer';
+import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
+import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
 
 describe('Navigation', () => {
     let component: Navigation;
@@ -32,7 +38,21 @@
             imports: [Navigation, HttpClientTestingModule, RouterTestingModule],
             providers: [
                 provideMockStore({
-                    initialState
+                    initialState,
+                    selectors: [
+                        {
+                            selector: selectCurrentUser,
+                            value: fromUser.initialState.user
+                        },
+                        {
+                            selector: selectClusterSummary,
+                            value: fromClusterSummary.initialState
+                        },
+                        {
+                            selector: selectFlowConfiguration,
+                            value: fromFlowConfiguration.initialState.flowConfiguration
+                        }
+                    ]
                 })
             ]
         });
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.ts
index 382951b..8faba64 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/navigation/navigation.component.ts
@@ -33,10 +33,16 @@
 import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
 import { Storage } from '../../../service/storage.service';
 import { MatCheckboxModule } from '@angular/material/checkbox';
-import { OS_SETTING, LIGHT_THEME, DARK_THEME, ThemingService } from '../../../service/theming.service';
+import { DARK_THEME, LIGHT_THEME, OS_SETTING, ThemingService } from '../../../service/theming.service';
 import { loadFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.actions';
 import { startCurrentUserPolling, stopCurrentUserPolling } from '../../../state/current-user/current-user.actions';
 import { loadAbout, openAboutDialog } from '../../../state/about/about.actions';
+import {
+    loadClusterSummary,
+    startClusterSummaryPolling,
+    stopClusterSummaryPolling
+} from '../../../state/cluster-summary/cluster-summary.actions';
+import { selectClusterSummary } from '../../../state/cluster-summary/cluster-summary.selectors';
 
 @Component({
     selector: 'navigation',
@@ -61,8 +67,9 @@
     LIGHT_THEME: string = LIGHT_THEME;
     DARK_THEME: string = DARK_THEME;
     OS_SETTING: string = OS_SETTING;
-    currentUser$ = this.store.select(selectCurrentUser);
-    flowConfiguration$ = this.store.select(selectFlowConfiguration);
+    currentUser = this.store.selectSignal(selectCurrentUser);
+    flowConfiguration = this.store.selectSignal(selectFlowConfiguration);
+    clusterSummary = this.store.selectSignal(selectClusterSummary);
 
     constructor(
         private store: Store<NiFiState>,
@@ -86,11 +93,14 @@
     ngOnInit(): void {
         this.store.dispatch(loadAbout());
         this.store.dispatch(loadFlowConfiguration());
+        this.store.dispatch(loadClusterSummary());
         this.store.dispatch(startCurrentUserPolling());
+        this.store.dispatch(startClusterSummaryPolling());
     }
 
     ngOnDestroy(): void {
         this.store.dispatch(stopCurrentUserPolling());
+        this.store.dispatch(stopClusterSummaryPolling());
     }
 
     allowLogin(user: CurrentUser): boolean {
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/tooltips/garbage-collection-tip/garbage-collection-tip.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/tooltips/garbage-collection-tip/garbage-collection-tip.component.html
new file mode 100644
index 0000000..b1d1015
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/tooltips/garbage-collection-tip/garbage-collection-tip.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.
+  -->
+
+<div class="tooltip">
+    <ul>
+        @for (gc of garbageCollections; track gc) {
+            <li>{{ gc.name }} - {{ gc.collectionCount }} ({{ gc.collectionTime }})</li>
+        }
+    </ul>
+</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/tooltips/garbage-collection-tip/garbage-collection-tip.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/tooltips/garbage-collection-tip/garbage-collection-tip.component.scss
new file mode 100644
index 0000000..b33f7ca
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/tooltips/garbage-collection-tip/garbage-collection-tip.component.scss
@@ -0,0 +1,16 @@
+/*!
+ * 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.
+ */
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/tooltips/garbage-collection-tip/garbage-collection-tip.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/tooltips/garbage-collection-tip/garbage-collection-tip.component.spec.ts
new file mode 100644
index 0000000..7544683
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/tooltips/garbage-collection-tip/garbage-collection-tip.component.spec.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 { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { GarbageCollectionTip } from './garbage-collection-tip.component';
+
+describe('GarbageCollectionTip', () => {
+    let component: GarbageCollectionTip;
+    let fixture: ComponentFixture<GarbageCollectionTip>;
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [GarbageCollectionTip]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(GarbageCollectionTip);
+        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/tooltips/garbage-collection-tip/garbage-collection-tip.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/tooltips/garbage-collection-tip/garbage-collection-tip.component.ts
new file mode 100644
index 0000000..a0cee29
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/tooltips/garbage-collection-tip/garbage-collection-tip.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, Input } from '@angular/core';
+import { GarbageCollectionTipInput } from '../../../../state/shared';
+import { GarbageCollection } from '../../../../state/system-diagnostics';
+
+@Component({
+    selector: 'garbage-collection-tip',
+    standalone: true,
+    imports: [],
+    templateUrl: './garbage-collection-tip.component.html',
+    styleUrl: './garbage-collection-tip.component.scss'
+})
+export class GarbageCollectionTip {
+    garbageCollections: GarbageCollection[] = [];
+
+    @Input() set data(data: GarbageCollectionTipInput | undefined) {
+        if (data?.garbageCollections) {
+            this.garbageCollections = [...data.garbageCollections];
+            this.garbageCollections.sort((a, b) => {
+                return b.collectionCount - a.collectionCount;
+            });
+        } else {
+            this.garbageCollections = [];
+        }
+    }
+}