[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 = [];
+ }
+ }
+}