= new EventEmitter();
@Output() startReportingTask: EventEmitter = new EventEmitter();
@Output() configureReportingTask: EventEmitter = new EventEmitter();
+ @Output() viewStateReportingTask: EventEmitter = new EventEmitter();
@Output() stopReportingTask: EventEmitter = new EventEmitter();
protected readonly TextTip = TextTip;
@@ -233,6 +234,10 @@ export class ReportingTaskTable {
return this.canRead(entity) && this.canWrite(entity) && entity.component.persistsState === true;
}
+ viewStateClicked(entity: ReportingTaskEntity): void {
+ this.viewStateReportingTask.next(entity);
+ }
+
canManageAccessPolicies(): boolean {
return this.flowConfiguration.supportsManagedAuthorizer && this.currentUser.tenantsPermissions.canRead;
}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.html
index 771e6bc830..3414c21b1d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.html
@@ -33,6 +33,7 @@
[currentUser]="currentUser"
[flowConfiguration]="(flowConfiguration$ | async)!"
(configureReportingTask)="configureReportingTask($event)"
+ (viewStateReportingTask)="viewStateReportingTask($event)"
(selectReportingTask)="selectReportingTask($event)"
(deleteReportingTask)="deleteReportingTask($event)"
(stopReportingTask)="stopReportingTask($event)"
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.ts
index c35e36fee8..fd8c6471cf 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/reporting-tasks.component.ts
@@ -42,6 +42,7 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import { NiFiState } from '../../../../state';
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
+import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions';
@Component({
selector: 'reporting-tasks',
@@ -127,6 +128,19 @@ export class ReportingTasks implements OnInit, OnDestroy {
);
}
+ viewStateReportingTask(entity: ReportingTaskEntity): void {
+ const canClear: boolean = entity.status.runStatus === 'STOPPED' && entity.status.activeThreadCount === 0;
+ this.store.dispatch(
+ getComponentStateAndOpenDialog({
+ request: {
+ componentUri: entity.uri,
+ componentName: entity.component.name,
+ canClear
+ }
+ })
+ );
+ }
+
startReportingTask(entity: ReportingTaskEntity): void {
this.store.dispatch(
startReportingTask({
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/component-state.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/component-state.service.ts
new file mode 100644
index 0000000000..f87d93b7b4
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/component-state.service.ts
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { ClearComponentStateRequest, LoadComponentStateRequest } from '../state/component-state';
+import { Observable } from 'rxjs';
+import { NiFiCommon } from './nifi-common.service';
+
+@Injectable({ providedIn: 'root' })
+export class ComponentStateService {
+ constructor(
+ private httpClient: HttpClient,
+ private nifiCommon: NiFiCommon
+ ) {}
+
+ /**
+ * The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
+ * does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
+ *
+ * https://stackoverflow.com/a/59586462
+ *
+ * @param url
+ * @private
+ */
+ private stripProtocol(url: string): string {
+ return this.nifiCommon.substringAfterFirst(url, ':');
+ }
+
+ getComponentState(request: LoadComponentStateRequest): Observable {
+ return this.httpClient.get(`${this.stripProtocol(request.componentUri)}/state`);
+ }
+
+ clearComponentState(request: ClearComponentStateRequest): Observable {
+ return this.httpClient.post(`${this.stripProtocol(request.componentUri)}/state/clear-requests`, {});
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.actions.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.actions.ts
new file mode 100644
index 0000000000..bb2ee31219
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.actions.ts
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createAction, props } from '@ngrx/store';
+import { ComponentStateRequest, ComponentStateResponse } from './index';
+
+const COMPONENT_STATE_PREFIX = '[Component State]';
+
+export const getComponentStateAndOpenDialog = createAction(
+ `${COMPONENT_STATE_PREFIX} Get Component State and Open Dialog`,
+ props<{ request: ComponentStateRequest }>()
+);
+
+export const loadComponentStateSuccess = createAction(
+ `${COMPONENT_STATE_PREFIX} Load Component State Success`,
+ props<{ response: ComponentStateResponse }>()
+);
+
+export const openComponentStateDialog = createAction(`${COMPONENT_STATE_PREFIX} Open Component State Dialog`);
+
+export const componentStateApiError = createAction(
+ `${COMPONENT_STATE_PREFIX} Component State API error`,
+ props<{ error: string }>()
+);
+
+export const clearComponentState = createAction(`${COMPONENT_STATE_PREFIX} Clear Component State`);
+
+export const reloadComponentState = createAction(`${COMPONENT_STATE_PREFIX} Reload Component State`);
+
+export const reloadComponentStateSuccess = createAction(
+ `${COMPONENT_STATE_PREFIX} Reload Component State Success`,
+ props<{ response: ComponentStateResponse }>()
+);
+
+export const resetComponentState = createAction(`${COMPONENT_STATE_PREFIX} Reset Component State`);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.effects.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.effects.ts
new file mode 100644
index 0000000000..5f02df85a2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.effects.ts
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Injectable } from '@angular/core';
+import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../index';
+import * as ComponentStateActions from './component-state.actions';
+import { catchError, from, map, of, switchMap, tap } from 'rxjs';
+import { MatDialog } from '@angular/material/dialog';
+import { ComponentStateService } from '../../service/component-state.service';
+import { ComponentStateDialog } from '../../ui/common/component-state/component-state.component';
+import { resetComponentState } from './component-state.actions';
+import { selectComponentUri } from './component-state.selectors';
+import { isDefinedAndNotNull } from '../shared';
+
+@Injectable()
+export class ComponentStateEffects {
+ constructor(
+ private actions$: Actions,
+ private store: Store,
+ private componentStateService: ComponentStateService,
+ private dialog: MatDialog
+ ) {}
+
+ getComponentStateAndOpenDialog$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ComponentStateActions.getComponentStateAndOpenDialog),
+ map((action) => action.request),
+ switchMap((request) =>
+ from(
+ this.componentStateService.getComponentState({ componentUri: request.componentUri }).pipe(
+ map((response: any) =>
+ ComponentStateActions.loadComponentStateSuccess({
+ response: {
+ componentState: response.componentState
+ }
+ })
+ ),
+ catchError((error) =>
+ of(
+ ComponentStateActions.componentStateApiError({
+ error: error.error
+ })
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+
+ loadComponentStateSuccess$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ComponentStateActions.loadComponentStateSuccess),
+ map((action) => action.response),
+ switchMap((response) => of(ComponentStateActions.openComponentStateDialog()))
+ )
+ );
+
+ openComponentStateDialog$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(ComponentStateActions.openComponentStateDialog),
+ tap(() => {
+ const dialogReference = this.dialog.open(ComponentStateDialog, {
+ panelClass: 'large-dialog'
+ });
+
+ dialogReference.afterClosed().subscribe((response) => {
+ this.store.dispatch(resetComponentState());
+ });
+ })
+ ),
+ { dispatch: false }
+ );
+
+ clearComponentState$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ComponentStateActions.clearComponentState),
+ concatLatestFrom(() => this.store.select(selectComponentUri).pipe(isDefinedAndNotNull())),
+ switchMap(([action, componentUri]) =>
+ from(
+ this.componentStateService.clearComponentState({ componentUri }).pipe(
+ map((response: any) => ComponentStateActions.reloadComponentState()),
+ catchError((error) =>
+ of(
+ ComponentStateActions.componentStateApiError({
+ error: error.error
+ })
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+
+ reloadComponentState$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ComponentStateActions.reloadComponentState),
+ concatLatestFrom(() => this.store.select(selectComponentUri).pipe(isDefinedAndNotNull())),
+ switchMap(([action, componentUri]) =>
+ from(
+ this.componentStateService.getComponentState({ componentUri }).pipe(
+ map((response: any) =>
+ ComponentStateActions.reloadComponentStateSuccess({
+ response: {
+ componentState: response.componentState
+ }
+ })
+ ),
+ catchError((error) =>
+ of(
+ ComponentStateActions.componentStateApiError({
+ error: error.error
+ })
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.reducer.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.reducer.ts
new file mode 100644
index 0000000000..d8ee39c266
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.reducer.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 { ComponentStateState } from './index';
+import { createReducer, on } from '@ngrx/store';
+import {
+ resetComponentState,
+ loadComponentStateSuccess,
+ componentStateApiError,
+ getComponentStateAndOpenDialog,
+ reloadComponentStateSuccess
+} from './component-state.actions';
+
+export const initialState: ComponentStateState = {
+ componentName: null,
+ componentUri: null,
+ componentState: null,
+ canClear: null,
+ status: 'pending',
+ error: null
+};
+
+export const componentStateReducer = createReducer(
+ initialState,
+ on(getComponentStateAndOpenDialog, (state, { request }) => ({
+ ...state,
+ componentName: request.componentName,
+ componentUri: request.componentUri,
+ canClear: request.canClear,
+ status: 'loading' as const
+ })),
+ on(loadComponentStateSuccess, reloadComponentStateSuccess, (state, { response }) => ({
+ ...state,
+ error: null,
+ status: 'success' as const,
+ componentState: response.componentState
+ })),
+ on(componentStateApiError, (state, { error }) => ({
+ ...state,
+ error,
+ status: 'error' as const
+ })),
+ on(resetComponentState, (state) => ({
+ ...initialState
+ }))
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.selectors.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.selectors.ts
new file mode 100644
index 0000000000..e418e0d8a9
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/component-state.selectors.ts
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createFeatureSelector, createSelector } from '@ngrx/store';
+import { componentStateFeatureKey, ComponentStateState } from './index';
+
+export const selectComponentStateState = createFeatureSelector(componentStateFeatureKey);
+
+export const selectComponentState = createSelector(
+ selectComponentStateState,
+ (state: ComponentStateState) => state.componentState
+);
+
+export const selectComponentName = createSelector(
+ selectComponentStateState,
+ (state: ComponentStateState) => state.componentName
+);
+
+export const selectComponentUri = createSelector(
+ selectComponentStateState,
+ (state: ComponentStateState) => state.componentUri
+);
+
+export const selectCanClear = createSelector(selectComponentStateState, (state: ComponentStateState) => state.canClear);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/index.ts
new file mode 100644
index 0000000000..7335e99fba
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/component-state/index.ts
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const componentStateFeatureKey = 'componentState';
+
+export interface ComponentStateRequest {
+ componentName: string;
+ componentUri: string;
+ canClear: boolean;
+}
+
+export interface LoadComponentStateRequest {
+ componentUri: string;
+}
+
+export interface ClearComponentStateRequest {
+ componentUri: string;
+}
+
+export interface ComponentStateResponse {
+ componentState: ComponentState;
+}
+
+export interface StateEntry {
+ key: string;
+ value: string;
+ clusterNodeId?: string;
+ clusterNodeAddress?: string;
+}
+
+export interface StateMap {
+ scope: string;
+ state: StateEntry[];
+ totalEntryCount: number;
+}
+
+export interface ComponentState {
+ componentId: string;
+ localState?: StateMap;
+ clusterState?: StateMap;
+ stateDescription: string;
+}
+
+export interface ComponentStateState {
+ componentName: string | null;
+ componentUri: string | null;
+ componentState: ComponentState | null;
+ canClear: boolean | null;
+ error: string | null;
+ status: 'pending' | 'loading' | 'error' | 'success';
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
index e4b7452e89..4ae6619448 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
@@ -31,6 +31,8 @@ import { systemDiagnosticsFeatureKey, SystemDiagnosticsState } from './system-di
import { systemDiagnosticsReducer } from './system-diagnostics/system-diagnostics.reducer';
import { flowConfigurationFeatureKey, FlowConfigurationState } from './flow-configuration';
import { flowConfigurationReducer } from './flow-configuration/flow-configuration.reducer';
+import { componentStateFeatureKey, ComponentStateState } from './component-state';
+import { componentStateReducer } from './component-state/component-state.reducer';
export interface NiFiState {
router: RouterReducerState;
@@ -41,6 +43,7 @@ export interface NiFiState {
[statusHistoryFeatureKey]: StatusHistoryState;
[controllerServiceStateFeatureKey]: ControllerServiceState;
[systemDiagnosticsFeatureKey]: SystemDiagnosticsState;
+ [componentStateFeatureKey]: ComponentStateState;
}
export const rootReducers: ActionReducerMap = {
@@ -51,5 +54,6 @@ export const rootReducers: ActionReducerMap = {
[flowConfigurationFeatureKey]: flowConfigurationReducer,
[statusHistoryFeatureKey]: statusHistoryReducer,
[controllerServiceStateFeatureKey]: controllerServiceStateReducer,
- [systemDiagnosticsFeatureKey]: systemDiagnosticsReducer
+ [systemDiagnosticsFeatureKey]: systemDiagnosticsReducer,
+ [componentStateFeatureKey]: componentStateReducer
};
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/status-history/status-history.reducer.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/status-history/status-history.reducer.ts
index 7f41618bf3..3f565207e0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/status-history/status-history.reducer.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/status-history/status-history.reducer.ts
@@ -26,7 +26,6 @@ import {
reloadStatusHistorySuccess,
getStatusHistoryAndOpenDialog
} from './status-history.actions';
-import { produce } from 'immer';
export const initialState: StatusHistoryState = {
statusHistory: {} as StatusHistoryEntity,
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.html
new file mode 100644
index 0000000000..b04dbeea01
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.html
@@ -0,0 +1,86 @@
+
+
+
+
Component State
+
+
+
+
Name
+
{{ componentName }}
+
+
+
Description
+
+ {{ stateDescription }}
+
+
+
+
+
+
+
+
+
Key
+
+ {{ item.key }}
+
+
+
+
+
+
Value
+
+ {{ item.value }}
+
+
+
+
+
+
+
+
+
Showing partial results
+
+
+
+
+
+
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.scss
new file mode 100644
index 0000000000..17606c83f5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.scss
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@use '@angular/material' as mat;
+
+.component-state-dialog {
+ @include mat.button-density(-1);
+
+ width: 760px;
+
+ .listing-table {
+ table {
+ .mat-column-key {
+ width: 200px;
+ }
+ }
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.spec.ts
new file mode 100644
index 0000000000..033fe2a105
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.spec.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 { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ComponentStateDialog } from './component-state.component';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { provideMockStore } from '@ngrx/store/testing';
+import { initialState } from '../../../state/component-state/component-state.reducer';
+
+describe('ComponentStateDialog', () => {
+ let component: ComponentStateDialog;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [ComponentStateDialog, BrowserAnimationsModule],
+ providers: [provideMockStore({ initialState })]
+ });
+ fixture = TestBed.createComponent(ComponentStateDialog);
+ 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/component-state/component-state.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.ts
new file mode 100644
index 0000000000..8f37bb96c7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/component-state/component-state.component.ts
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AfterViewInit, Component, Input } from '@angular/core';
+import { MatButtonModule } from '@angular/material/button';
+import { MatDialogModule } from '@angular/material/dialog';
+import { MatTableDataSource, MatTableModule } from '@angular/material/table';
+import { NiFiCommon } from '../../../service/nifi-common.service';
+import { MatSortModule, Sort } from '@angular/material/sort';
+import { AsyncPipe, NgIf } from '@angular/common';
+import { NifiTooltipDirective } from '../tooltips/nifi-tooltip.directive';
+import { NifiSpinnerDirective } from '../spinner/nifi-spinner.directive';
+import { ComponentStateState, StateEntry, StateMap } from '../../../state/component-state';
+import { Store } from '@ngrx/store';
+import { clearComponentState } from '../../../state/component-state/component-state.actions';
+import {
+ selectCanClear,
+ selectComponentName,
+ selectComponentState
+} from '../../../state/component-state/component-state.selectors';
+import { isDefinedAndNotNull } from '../../../state/shared';
+import { debounceTime, Observable } from 'rxjs';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+
+@Component({
+ selector: 'component-state',
+ standalone: true,
+ templateUrl: './component-state.component.html',
+ imports: [
+ MatButtonModule,
+ MatDialogModule,
+ MatTableModule,
+ MatSortModule,
+ NgIf,
+ NifiTooltipDirective,
+ NifiSpinnerDirective,
+ AsyncPipe,
+ ReactiveFormsModule,
+ MatFormFieldModule,
+ MatInputModule
+ ],
+ styleUrls: ['./component-state.component.scss', '../../../../assets/styles/listing-table.scss']
+})
+export class ComponentStateDialog implements AfterViewInit {
+ @Input() initialSortColumn: 'key' | 'value' = 'key';
+ @Input() initialSortDirection: 'asc' | 'desc' = 'asc';
+
+ componentName$: Observable = this.store.select(selectComponentName).pipe(isDefinedAndNotNull());
+ canClear$: Observable = this.store.select(selectCanClear).pipe(isDefinedAndNotNull());
+
+ // TODO - need to include scope column when clustered
+ displayedColumns: string[] = ['key', 'value'];
+ dataSource: MatTableDataSource = new MatTableDataSource();
+
+ filterForm: FormGroup;
+
+ stateDescription: string = '';
+ totalEntries: number = 0;
+ filteredEntries: number = 0;
+ partialResults: boolean = false;
+
+ constructor(
+ private store: Store,
+ private formBuilder: FormBuilder,
+ private nifiCommon: NiFiCommon
+ ) {
+ this.filterForm = this.formBuilder.group({ filterTerm: '' });
+
+ this.store
+ .select(selectComponentState)
+ .pipe(isDefinedAndNotNull(), takeUntilDestroyed())
+ .subscribe((componentState) => {
+ this.stateDescription = componentState.stateDescription;
+
+ const stateItems: StateEntry[] = [];
+ if (componentState.localState) {
+ const localStateItems: StateEntry[] = this.processStateMap(componentState.localState);
+ stateItems.push(...localStateItems);
+ }
+ if (componentState.clusterState) {
+ const clusterStateItems: StateEntry[] = this.processStateMap(componentState.clusterState);
+ stateItems.push(...clusterStateItems);
+ }
+
+ this.dataSource.data = this.sortStateEntries(stateItems, {
+ active: this.initialSortColumn,
+ direction: this.initialSortDirection
+ });
+ this.filteredEntries = stateItems.length;
+
+ // apply any filtering to the new data
+ const filterTerm = this.filterForm.get('filterTerm')?.value;
+ if (filterTerm?.length > 0) {
+ this.applyFilter(filterTerm);
+ }
+ });
+ }
+
+ ngAfterViewInit(): void {
+ this.filterForm
+ .get('filterTerm')
+ ?.valueChanges.pipe(debounceTime(500))
+ .subscribe((filterTerm: string) => {
+ this.applyFilter(filterTerm);
+ });
+ }
+
+ processStateMap(stateMap: StateMap): StateEntry[] {
+ const stateItems: StateEntry[] = stateMap.state ? stateMap.state : [];
+
+ if (stateItems.length !== stateMap.totalEntryCount) {
+ this.partialResults = true;
+ }
+
+ this.totalEntries += stateMap.totalEntryCount;
+
+ return stateItems;
+ }
+
+ applyFilter(filterTerm: string) {
+ this.dataSource.filter = filterTerm.trim().toLowerCase();
+ this.filteredEntries = this.dataSource.filteredData.length;
+ }
+
+ sortData(sort: Sort) {
+ this.dataSource.data = this.sortStateEntries(this.dataSource.data, sort);
+ }
+
+ private sortStateEntries(data: StateEntry[], sort: Sort): StateEntry[] {
+ if (!data) {
+ return [];
+ }
+
+ return data.slice().sort((a, b) => {
+ const isAsc = sort.direction === 'asc';
+ let retVal = 0;
+ switch (sort.active) {
+ case 'key':
+ retVal = this.nifiCommon.compareString(a.key, b.key);
+ break;
+ case 'value':
+ retVal = this.nifiCommon.compareString(a.value, b.value);
+ break;
+ }
+ return retVal * (isAsc ? 1 : -1);
+ });
+ }
+
+ clearState(): void {
+ this.store.dispatch(clearComponentState());
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/controller-service-table/controller-service-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/controller-service-table/controller-service-table.component.html
index 03076a9a03..0ae491ed2f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/controller-service-table/controller-service-table.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/controller-service-table/controller-service-table.component.html
@@ -141,7 +141,11 @@
*ngIf="canDelete(item)"
(click)="deleteClicked(item, $event)"
title="Delete">