[NIFI-13099] status history error handling (#8703)

* [NIFI-13099] - Error handling for Status History

* Error handling for current user

* Error handling for extension types

* Error handling for flow configuration

* Error handling for component state

* Error handling for cluster summary

* Error handling for System Diagnostics

* review feedback

* use SystemDiagnosticsActions.systemDiagnosticsSnackbarError

* review feedback

* review feedback

* use snackbar

This closes #8703
This commit is contained in:
Rob Fellows 2024-04-29 16:56:38 -04:00 committed by GitHub
parent a230eba67b
commit 658e83b7ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 247 additions and 222 deletions

View File

@ -21,7 +21,7 @@
</header> </header>
<div class="px-5 flex-1 flex flex-col gap-y-2"> <div class="px-5 flex-1 flex flex-col gap-y-2">
<h3 class="text-xl bold primary-color">NiFi Cluster</h3> <h3 class="text-xl bold primary-color">NiFi Cluster</h3>
<error-banner></error-banner>
@if (getTabLinks(); as tabs) { @if (getTabLinks(); as tabs) {
<!-- Don't show the tab bar if there is only 1 tab to show --> <!-- Don't show the tab bar if there is only 1 tab to show -->
<div class="cluster-tabs" [class.hidden]="tabs.length === 1"> <div class="cluster-tabs" [class.hidden]="tabs.length === 1">

View File

@ -33,6 +33,7 @@ import { CurrentUser } from '../../../state/current-user';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { selectCurrentRoute } from '../../../state/router/router.selectors'; import { selectCurrentRoute } from '../../../state/router/router.selectors';
import { resetSystemDiagnostics } from '../../../state/system-diagnostics/system-diagnostics.actions'; import { resetSystemDiagnostics } from '../../../state/system-diagnostics/system-diagnostics.actions';
import { clearBannerErrors } from '../../../state/error/error.actions';
interface TabLink { interface TabLink {
label: string; label: string;
@ -82,6 +83,7 @@ export class Cluster implements OnInit, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.store.dispatch(resetClusterState()); this.store.dispatch(resetClusterState());
this.store.dispatch(resetSystemDiagnostics()); this.store.dispatch(resetSystemDiagnostics());
this.store.dispatch(clearBannerErrors());
} }
refresh() { refresh() {

View File

@ -26,6 +26,7 @@ import { ClusterListingEffects } from '../state/cluster-listing/cluster-listing.
import { ClusterRoutingModule } from './cluster-routing.module'; import { ClusterRoutingModule } from './cluster-routing.module';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { MatIconButton } from '@angular/material/button'; import { MatIconButton } from '@angular/material/button';
import { ErrorBanner } from '../../../ui/common/error-banner/error-banner.component';
@NgModule({ @NgModule({
declarations: [Cluster], declarations: [Cluster],
@ -37,7 +38,8 @@ import { MatIconButton } from '@angular/material/button';
StoreModule.forFeature(clusterFeatureKey, reducers), StoreModule.forFeature(clusterFeatureKey, reducers),
EffectsModule.forFeature(ClusterListingEffects), EffectsModule.forFeature(ClusterListingEffects),
MatTabsModule, MatTabsModule,
MatIconButton MatIconButton,
ErrorBanner
] ]
}) })
export class ClusterModule {} export class ClusterModule {}

View File

@ -16,14 +16,13 @@
*/ */
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects'; import { Actions, createEffect, ofType } from '@ngrx/effects';
import { ActionCreator, Creator, Store } from '@ngrx/store'; import { ActionCreator, Creator, Store } from '@ngrx/store';
import { NiFiState } from '../../../../state'; import { NiFiState } from '../../../../state';
import { ErrorHelper } from '../../../../service/error-helper.service'; import { ErrorHelper } from '../../../../service/error-helper.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import * as ClusterListingActions from './cluster-listing.actions'; import * as ClusterListingActions from './cluster-listing.actions';
import { catchError, from, map, of, switchMap, take, tap } from 'rxjs'; import { catchError, filter, from, map, of, switchMap, take, tap } from 'rxjs';
import { SystemDiagnosticsService } from '../../../../service/system-diagnostics.service';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { selectClusterListingStatus } from './cluster-listing.selectors'; import { selectClusterListingStatus } from './cluster-listing.selectors';
import { reloadSystemDiagnostics } from '../../../../state/system-diagnostics/system-diagnostics.actions'; import { reloadSystemDiagnostics } from '../../../../state/system-diagnostics/system-diagnostics.actions';
@ -35,6 +34,7 @@ import { ClusterNodeDetailDialog } from '../../ui/cluster-node-listing/cluster-n
import * as ErrorActions from '../../../../state/error/error.actions'; import * as ErrorActions from '../../../../state/error/error.actions';
import { SelectClusterNodeRequest } from './index'; import { SelectClusterNodeRequest } from './index';
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors'; import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
import { concatLatestFrom } from '@ngrx/operators';
@Injectable() @Injectable()
export class ClusterListingEffects { export class ClusterListingEffects {
@ -43,7 +43,6 @@ export class ClusterListingEffects {
private store: Store<NiFiState>, private store: Store<NiFiState>,
private errorHelper: ErrorHelper, private errorHelper: ErrorHelper,
private router: Router, private router: Router,
private systemDiagnosticsService: SystemDiagnosticsService,
private clusterService: ClusterService, private clusterService: ClusterService,
private dialog: MatDialog private dialog: MatDialog
) {} ) {}
@ -51,12 +50,6 @@ export class ClusterListingEffects {
loadClusterListing$ = createEffect(() => loadClusterListing$ = createEffect(() =>
this.actions$.pipe( this.actions$.pipe(
ofType(ClusterListingActions.loadClusterListing), 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)]), concatLatestFrom(() => [this.store.select(selectClusterListingStatus)]),
switchMap(([, listingStatus]) => switchMap(([, listingStatus]) =>
from(this.clusterService.getClusterListing()).pipe( from(this.clusterService.getClusterListing()).pipe(
@ -69,6 +62,22 @@ export class ClusterListingEffects {
) )
); );
loadClusterListingSuccess$ = createEffect(() =>
this.actions$.pipe(
ofType(ClusterListingActions.loadClusterListingSuccess),
concatLatestFrom(() => this.store.select(selectCurrentUser)),
filter(([, currentUser]) => currentUser.systemPermissions.canRead),
map(() =>
reloadSystemDiagnostics({
request: {
nodewise: true,
errorStrategy: 'banner'
}
})
)
)
);
confirmAndDisconnectNode$ = createEffect( confirmAndDisconnectNode$ = createEffect(
() => () =>
this.actions$.pipe( this.actions$.pipe(

View File

@ -41,13 +41,6 @@ export const setDisconnectionAcknowledged = createAction(
props<{ disconnectionAcknowledged: boolean }>() props<{ disconnectionAcknowledged: boolean }>()
); );
export const clusterSummaryApiError = createAction(
`${CLUSTER_SUMMARY_STATE_PREFIX} Cluster Summary Api Error`,
props<{ error: string }>()
);
export const clearClusterSummaryApiError = createAction(`${CLUSTER_SUMMARY_STATE_PREFIX} Clear About Api Error`);
export const searchCluster = createAction( export const searchCluster = createAction(
`${CLUSTER_SUMMARY_STATE_PREFIX} Search Cluster`, `${CLUSTER_SUMMARY_STATE_PREFIX} Search Cluster`,
props<{ request: ClusterSearchRequest }>() props<{ request: ClusterSearchRequest }>()

View File

@ -19,6 +19,7 @@ import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects'; import { Actions, createEffect, ofType } from '@ngrx/effects';
import { concatLatestFrom } from '@ngrx/operators'; import { concatLatestFrom } from '@ngrx/operators';
import * as ClusterSummaryActions from './cluster-summary.actions'; import * as ClusterSummaryActions from './cluster-summary.actions';
import { acknowledgeClusterConnectionChange, setDisconnectionAcknowledged } from './cluster-summary.actions';
import { asyncScheduler, catchError, delay, filter, from, interval, map, of, switchMap, takeUntil, tap } from 'rxjs'; import { asyncScheduler, catchError, delay, filter, from, interval, map, of, switchMap, takeUntil, tap } from 'rxjs';
import { ClusterService } from '../../service/cluster.service'; import { ClusterService } from '../../service/cluster.service';
import { selectClusterSummary } from './cluster-summary.selectors'; import { selectClusterSummary } from './cluster-summary.selectors';
@ -27,7 +28,6 @@ import { Store } from '@ngrx/store';
import { ClusterSummary, ClusterSummaryState } from './index'; import { ClusterSummary, ClusterSummaryState } from './index';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import * as ErrorActions from '../error/error.actions'; import * as ErrorActions from '../error/error.actions';
import { acknowledgeClusterConnectionChange, setDisconnectionAcknowledged } from './cluster-summary.actions';
import { OkDialog } from '../../ui/common/ok-dialog/ok-dialog.component'; import { OkDialog } from '../../ui/common/ok-dialog/ok-dialog.component';
import { MEDIUM_DIALOG } from '../../index'; import { MEDIUM_DIALOG } from '../../index';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@ -66,7 +66,15 @@ export class ClusterSummaryEffects {
response response
}); });
}), }),
catchError((error) => of(ClusterSummaryActions.clusterSummaryApiError({ error: error.error }))) catchError((errorResponse: HttpErrorResponse) =>
of(
ErrorActions.snackBarError({
error: `Failed to load cluster summary - [${
errorResponse.error || errorResponse.status
}]`
})
)
)
) )
); );
}) })
@ -138,7 +146,13 @@ export class ClusterSummaryEffects {
}) })
), ),
catchError((errorResponse: HttpErrorResponse) => catchError((errorResponse: HttpErrorResponse) =>
of(ErrorActions.snackBarError({ error: errorResponse.error })) of(
ErrorActions.snackBarError({
error: `Failed to search cluster summary - [${
errorResponse.error || errorResponse.status
}]`
})
)
) )
); );
}) })

View File

@ -18,8 +18,6 @@
import { createReducer, on } from '@ngrx/store'; import { createReducer, on } from '@ngrx/store';
import { ClusterSummaryState } from './index'; import { ClusterSummaryState } from './index';
import { import {
clearClusterSummaryApiError,
clusterSummaryApiError,
loadClusterSummary, loadClusterSummary,
loadClusterSummarySuccess, loadClusterSummarySuccess,
searchClusterSuccess, searchClusterSuccess,
@ -30,7 +28,6 @@ export const initialState: ClusterSummaryState = {
disconnectionAcknowledged: false, disconnectionAcknowledged: false,
clusterSummary: null, clusterSummary: null,
searchResults: null, searchResults: null,
error: null,
status: 'pending' status: 'pending'
}; };
@ -43,19 +40,8 @@ export const clusterSummaryReducer = createReducer(
on(loadClusterSummarySuccess, (state, { response }) => ({ on(loadClusterSummarySuccess, (state, { response }) => ({
...state, ...state,
clusterSummary: response.clusterSummary, clusterSummary: response.clusterSummary,
error: null,
status: 'success' as const status: 'success' as const
})), })),
on(clusterSummaryApiError, (state, { error }) => ({
...state,
error,
status: 'error' as const
})),
on(clearClusterSummaryApiError, (state) => ({
...state,
error: null,
status: 'pending' as const
})),
on(searchClusterSuccess, (state, { response }) => ({ on(searchClusterSuccess, (state, { response }) => ({
...state, ...state,
searchResults: response searchResults: response

View File

@ -46,6 +46,5 @@ export interface ClusterSummaryState {
disconnectionAcknowledged: boolean; disconnectionAcknowledged: boolean;
clusterSummary: ClusterSummary | null; clusterSummary: ClusterSummary | null;
searchResults: ClusterSearchResults | null; searchResults: ClusterSearchResults | null;
error: string | null; status: 'pending' | 'loading' | 'success';
status: 'pending' | 'loading' | 'error' | 'success';
} }

View File

@ -32,11 +32,6 @@ export const loadComponentStateSuccess = createAction(
export const openComponentStateDialog = createAction(`${COMPONENT_STATE_PREFIX} Open Component State Dialog`); 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 clearComponentState = createAction(`${COMPONENT_STATE_PREFIX} Clear Component State`);
export const reloadComponentState = createAction(`${COMPONENT_STATE_PREFIX} Reload Component State`); export const reloadComponentState = createAction(`${COMPONENT_STATE_PREFIX} Reload Component State`);

View File

@ -21,14 +21,16 @@ import { concatLatestFrom } from '@ngrx/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { NiFiState } from '../index'; import { NiFiState } from '../index';
import * as ComponentStateActions from './component-state.actions'; import * as ComponentStateActions from './component-state.actions';
import { resetComponentState } from './component-state.actions';
import { catchError, from, map, of, switchMap, tap } from 'rxjs'; import { catchError, from, map, of, switchMap, tap } from 'rxjs';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ComponentStateService } from '../../service/component-state.service'; import { ComponentStateService } from '../../service/component-state.service';
import { ComponentStateDialog } from '../../ui/common/component-state/component-state.component'; import { ComponentStateDialog } from '../../ui/common/component-state/component-state.component';
import { resetComponentState } from './component-state.actions';
import { selectComponentUri } from './component-state.selectors'; import { selectComponentUri } from './component-state.selectors';
import { isDefinedAndNotNull } from '../shared'; import { isDefinedAndNotNull } from '../shared';
import { LARGE_DIALOG } from '../../index'; import { LARGE_DIALOG } from '../../index';
import * as ErrorActions from '../error/error.actions';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable() @Injectable()
export class ComponentStateEffects { export class ComponentStateEffects {
@ -53,10 +55,12 @@ export class ComponentStateEffects {
} }
}) })
), ),
catchError((error) => catchError((errorResponse: HttpErrorResponse) =>
of( of(
ComponentStateActions.componentStateApiError({ ErrorActions.snackBarError({
error: error.error error: `Failed to get the component state for ${request.componentName}. - [${
errorResponse.error || errorResponse.status
}]`
}) })
) )
) )
@ -99,10 +103,12 @@ export class ComponentStateEffects {
from( from(
this.componentStateService.clearComponentState({ componentUri }).pipe( this.componentStateService.clearComponentState({ componentUri }).pipe(
map(() => ComponentStateActions.reloadComponentState()), map(() => ComponentStateActions.reloadComponentState()),
catchError((error) => catchError((errorResponse: HttpErrorResponse) =>
of( of(
ComponentStateActions.componentStateApiError({ ErrorActions.addBannerError({
error: error.error error: `Failed to clear the component state. - [${
errorResponse.error || errorResponse.status
}]`
}) })
) )
) )
@ -126,10 +132,12 @@ export class ComponentStateEffects {
} }
}) })
), ),
catchError((error) => catchError((errorResponse: HttpErrorResponse) =>
of( of(
ComponentStateActions.componentStateApiError({ ErrorActions.addBannerError({
error: error.error error: `Failed to reload the component state. - [${
errorResponse.error || errorResponse.status
}]`
}) })
) )
) )

View File

@ -18,11 +18,10 @@
import { ComponentStateState } from './index'; import { ComponentStateState } from './index';
import { createReducer, on } from '@ngrx/store'; import { createReducer, on } from '@ngrx/store';
import { import {
resetComponentState,
loadComponentStateSuccess,
componentStateApiError,
getComponentStateAndOpenDialog, getComponentStateAndOpenDialog,
reloadComponentStateSuccess loadComponentStateSuccess,
reloadComponentStateSuccess,
resetComponentState
} from './component-state.actions'; } from './component-state.actions';
export const initialState: ComponentStateState = { export const initialState: ComponentStateState = {
@ -30,8 +29,7 @@ export const initialState: ComponentStateState = {
componentUri: null, componentUri: null,
componentState: null, componentState: null,
canClear: null, canClear: null,
status: 'pending', status: 'pending'
error: null
}; };
export const componentStateReducer = createReducer( export const componentStateReducer = createReducer(
@ -45,15 +43,9 @@ export const componentStateReducer = createReducer(
})), })),
on(loadComponentStateSuccess, reloadComponentStateSuccess, (state, { response }) => ({ on(loadComponentStateSuccess, reloadComponentStateSuccess, (state, { response }) => ({
...state, ...state,
error: null,
status: 'success' as const, status: 'success' as const,
componentState: response.componentState componentState: response.componentState
})), })),
on(componentStateApiError, (state, { error }) => ({
...state,
error,
status: 'error' as const
})),
on(resetComponentState, () => ({ on(resetComponentState, () => ({
...initialState ...initialState
})) }))

View File

@ -66,6 +66,5 @@ export interface ComponentStateState {
componentUri: string | null; componentUri: string | null;
componentState: ComponentState | null; componentState: ComponentState | null;
canClear: boolean | null; canClear: boolean | null;
error: string | null; status: 'pending' | 'loading' | 'success';
status: 'pending' | 'loading' | 'error' | 'success';
} }

View File

@ -25,10 +25,6 @@ export const loadCurrentUserSuccess = createAction(
props<{ response: LoadCurrentUserResponse }>() props<{ response: LoadCurrentUserResponse }>()
); );
export const currentUserApiError = createAction('[Current User] Current User Api Error', props<{ error: string }>());
export const clearCurrentUserApiError = createAction('[Current User] Clear Current User Api Error');
export const startCurrentUserPolling = createAction('[Current User] Start Current User Polling'); export const startCurrentUserPolling = createAction('[Current User] Start Current User Polling');
export const stopCurrentUserPolling = createAction('[Current User] Stop Current User Polling'); export const stopCurrentUserPolling = createAction('[Current User] Stop Current User Polling');

View File

@ -20,12 +20,14 @@ import { Actions, createEffect, ofType } from '@ngrx/effects';
import * as UserActions from './current-user.actions'; import * as UserActions from './current-user.actions';
import { asyncScheduler, catchError, from, interval, map, of, switchMap, takeUntil } from 'rxjs'; import { asyncScheduler, catchError, from, interval, map, of, switchMap, takeUntil } from 'rxjs';
import { CurrentUserService } from '../../service/current-user.service'; import { CurrentUserService } from '../../service/current-user.service';
import { ErrorHelper } from '../../service/error-helper.service';
@Injectable() @Injectable()
export class CurrentUserEffects { export class CurrentUserEffects {
constructor( constructor(
private actions$: Actions, private actions$: Actions,
private userService: CurrentUserService private userService: CurrentUserService,
private errorHelper: ErrorHelper
) {} ) {}
loadCurrentUser$ = createEffect(() => loadCurrentUser$ = createEffect(() =>
@ -41,7 +43,7 @@ export class CurrentUserEffects {
} }
}) })
), ),
catchError((error) => of(UserActions.currentUserApiError({ error: error.error }))) catchError((error) => of(this.errorHelper.fullScreenError(error)))
) )
); );
}) })

View File

@ -18,12 +18,7 @@
import { createReducer, on } from '@ngrx/store'; import { createReducer, on } from '@ngrx/store';
import { CurrentUserState } from './index'; import { CurrentUserState } from './index';
import { Permissions } from '../shared'; import { Permissions } from '../shared';
import { import { loadCurrentUser, loadCurrentUserSuccess } from './current-user.actions';
clearCurrentUserApiError,
loadCurrentUser,
loadCurrentUserSuccess,
currentUserApiError
} from './current-user.actions';
export const NO_PERMISSIONS: Permissions = { export const NO_PERMISSIONS: Permissions = {
canRead: false, canRead: false,
@ -45,7 +40,6 @@ export const initialState: CurrentUserState = {
tenantsPermissions: NO_PERMISSIONS, tenantsPermissions: NO_PERMISSIONS,
componentRestrictionPermissions: [] componentRestrictionPermissions: []
}, },
error: null,
status: 'pending' status: 'pending'
}; };
@ -58,17 +52,6 @@ export const currentUserReducer = createReducer(
on(loadCurrentUserSuccess, (state, { response }) => ({ on(loadCurrentUserSuccess, (state, { response }) => ({
...state, ...state,
user: response.user, user: response.user,
error: null,
status: 'success' as const status: 'success' as const
})),
on(currentUserApiError, (state, { error }) => ({
...state,
error: error,
status: 'error' as const
})),
on(clearCurrentUserApiError, (state) => ({
...state,
error: null,
status: 'pending' as const
})) }))
); );

View File

@ -45,6 +45,5 @@ export interface CurrentUser {
export interface CurrentUserState { export interface CurrentUserState {
user: CurrentUser; user: CurrentUser;
error: string | null; status: 'pending' | 'loading' | 'success';
status: 'pending' | 'loading' | 'error' | 'success';
} }

View File

@ -21,6 +21,7 @@ import {
LoadExtensionTypesForPoliciesResponse, LoadExtensionTypesForPoliciesResponse,
LoadExtensionTypesForSettingsResponse LoadExtensionTypesForSettingsResponse
} from './index'; } from './index';
import { HttpErrorResponse } from '@angular/common/http';
export const loadExtensionTypesForCanvas = createAction('[Extension Types] Load Extension Types For Canvas'); export const loadExtensionTypesForCanvas = createAction('[Extension Types] Load Extension Types For Canvas');
@ -45,7 +46,5 @@ export const loadExtensionTypesForPoliciesSuccess = createAction(
export const extensionTypesApiError = createAction( export const extensionTypesApiError = createAction(
'[Extension Types] Extension Types Api Error', '[Extension Types] Extension Types Api Error',
props<{ error: string }>() props<{ error: HttpErrorResponse }>()
); );
export const clearExtensionTypesApiError = createAction('[Extension Types] Clear Extension Types Api Error');

View File

@ -20,6 +20,8 @@ import { Actions, createEffect, ofType } from '@ngrx/effects';
import * as ExtensionTypesActions from './extension-types.actions'; import * as ExtensionTypesActions from './extension-types.actions';
import { catchError, combineLatest, map, of, switchMap } from 'rxjs'; import { catchError, combineLatest, map, of, switchMap } from 'rxjs';
import { ExtensionTypesService } from '../../service/extension-types.service'; import { ExtensionTypesService } from '../../service/extension-types.service';
import * as ErrorActions from '../error/error.actions';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable() @Injectable()
export class ExtensionTypesEffects { export class ExtensionTypesEffects {
@ -46,7 +48,7 @@ export class ExtensionTypesEffects {
} }
}) })
), ),
catchError((error) => of(ExtensionTypesActions.extensionTypesApiError({ error: error.error }))) catchError((error) => of(ExtensionTypesActions.extensionTypesApiError({ error })))
) )
) )
) )
@ -81,7 +83,7 @@ export class ExtensionTypesEffects {
} }
}) })
), ),
catchError((error) => of(ExtensionTypesActions.extensionTypesApiError({ error: error.error }))) catchError((error) => of(ExtensionTypesActions.extensionTypesApiError({ error })))
) )
) )
) )
@ -116,7 +118,25 @@ export class ExtensionTypesEffects {
} }
}) })
), ),
catchError((error) => of(ExtensionTypesActions.extensionTypesApiError({ error: error.error }))) catchError((errorResponse: HttpErrorResponse) =>
of(ExtensionTypesActions.extensionTypesApiError({ error: errorResponse.error }))
)
)
)
)
);
extensionTypesApiError$ = createEffect(() =>
this.actions$.pipe(
ofType(ExtensionTypesActions.extensionTypesApiError),
map((action) => action.error),
switchMap((errorResponse: HttpErrorResponse) =>
of(
ErrorActions.snackBarError({
error: `Failed to load extension types. You may not be able to create new Processors, Controller Services, Reporting Tasks, Parameter Providers, or Flow Analysis Rules. - [${
errorResponse.error || errorResponse.status
}]`
})
) )
) )
) )

View File

@ -18,8 +18,6 @@
import { createReducer, on } from '@ngrx/store'; import { createReducer, on } from '@ngrx/store';
import { ExtensionTypesState } from './index'; import { ExtensionTypesState } from './index';
import { import {
clearExtensionTypesApiError,
extensionTypesApiError,
loadExtensionTypesForCanvas, loadExtensionTypesForCanvas,
loadExtensionTypesForCanvasSuccess, loadExtensionTypesForCanvasSuccess,
loadExtensionTypesForPoliciesSuccess, loadExtensionTypesForPoliciesSuccess,
@ -34,7 +32,6 @@ export const initialState: ExtensionTypesState = {
registryClientTypes: [], registryClientTypes: [],
flowAnalysisRuleTypes: [], flowAnalysisRuleTypes: [],
parameterProviderTypes: [], parameterProviderTypes: [],
error: null,
status: 'pending' status: 'pending'
}; };
@ -49,7 +46,6 @@ export const extensionTypesReducer = createReducer(
processorTypes: response.processorTypes, processorTypes: response.processorTypes,
controllerServiceTypes: response.controllerServiceTypes, controllerServiceTypes: response.controllerServiceTypes,
prioritizerTypes: response.prioritizers, prioritizerTypes: response.prioritizers,
error: null,
status: 'success' as const status: 'success' as const
})), })),
on(loadExtensionTypesForSettingsSuccess, (state, { response }) => ({ on(loadExtensionTypesForSettingsSuccess, (state, { response }) => ({
@ -59,7 +55,6 @@ export const extensionTypesReducer = createReducer(
registryClientTypes: response.registryClientTypes, registryClientTypes: response.registryClientTypes,
parameterProviderTypes: response.parameterProviderTypes, parameterProviderTypes: response.parameterProviderTypes,
flowAnalysisRuleTypes: response.flowAnalysisRuleTypes, flowAnalysisRuleTypes: response.flowAnalysisRuleTypes,
error: null,
status: 'success' as const status: 'success' as const
})), })),
on(loadExtensionTypesForPoliciesSuccess, (state, { response }) => ({ on(loadExtensionTypesForPoliciesSuccess, (state, { response }) => ({
@ -69,17 +64,6 @@ export const extensionTypesReducer = createReducer(
reportingTaskTypes: response.reportingTaskTypes, reportingTaskTypes: response.reportingTaskTypes,
parameterProviderTypes: response.parameterProviderTypes, parameterProviderTypes: response.parameterProviderTypes,
flowAnalysisRuleTypes: response.flowAnalysisRuleTypes, flowAnalysisRuleTypes: response.flowAnalysisRuleTypes,
error: null,
status: 'success' as const status: 'success' as const
})),
on(extensionTypesApiError, (state, { error }) => ({
...state,
error: error,
status: 'error' as const
})),
on(clearExtensionTypesApiError, (state) => ({
...state,
error: null,
status: 'pending' as const
})) }))
); );

View File

@ -49,6 +49,5 @@ export interface ExtensionTypesState {
registryClientTypes: DocumentedType[]; registryClientTypes: DocumentedType[];
flowAnalysisRuleTypes: DocumentedType[]; flowAnalysisRuleTypes: DocumentedType[];
parameterProviderTypes: DocumentedType[]; parameterProviderTypes: DocumentedType[];
error: string | null; status: 'pending' | 'loading' | 'success';
status: 'pending' | 'loading' | 'error' | 'success';
} }

View File

@ -24,10 +24,3 @@ export const loadFlowConfigurationSuccess = createAction(
'[Flow Configuration] Load Flow Configuration Success', '[Flow Configuration] Load Flow Configuration Success',
props<{ response: LoadFlowConfigurationResponse }>() props<{ response: LoadFlowConfigurationResponse }>()
); );
export const flowConfigurationApiError = createAction(
'[Flow Configuration] Flow Configuration Api Error',
props<{ error: string }>()
);
export const flowConfigurationAboutApiError = createAction('[Flow Configuration] Clear Flow Configuration Api Error');

View File

@ -20,6 +20,8 @@ import { Actions, createEffect, ofType } from '@ngrx/effects';
import * as FlowConfigurationActions from './flow-configuration.actions'; import * as FlowConfigurationActions from './flow-configuration.actions';
import { catchError, from, map, of, switchMap } from 'rxjs'; import { catchError, from, map, of, switchMap } from 'rxjs';
import { FlowConfigurationService } from '../../service/flow-configuration.service'; import { FlowConfigurationService } from '../../service/flow-configuration.service';
import * as ErrorActions from '../error/error.actions';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable() @Injectable()
export class FlowConfigurationEffects { export class FlowConfigurationEffects {
@ -39,8 +41,14 @@ export class FlowConfigurationEffects {
response response
}) })
), ),
catchError((error) => catchError((errorResponse: HttpErrorResponse) =>
of(FlowConfigurationActions.flowConfigurationApiError({ error: error.error })) of(
ErrorActions.snackBarError({
error: `Failed to load Flow Configuration. - [${
errorResponse.error || errorResponse.status
}]`
})
)
) )
) )
); );

View File

@ -17,16 +17,10 @@
import { createReducer, on } from '@ngrx/store'; import { createReducer, on } from '@ngrx/store';
import { FlowConfigurationState } from './index'; import { FlowConfigurationState } from './index';
import { import { loadFlowConfiguration, loadFlowConfigurationSuccess } from './flow-configuration.actions';
flowConfigurationApiError,
flowConfigurationAboutApiError,
loadFlowConfiguration,
loadFlowConfigurationSuccess
} from './flow-configuration.actions';
export const initialState: FlowConfigurationState = { export const initialState: FlowConfigurationState = {
flowConfiguration: null, flowConfiguration: null,
error: null,
status: 'pending' status: 'pending'
}; };
@ -39,17 +33,6 @@ export const flowConfigurationReducer = createReducer(
on(loadFlowConfigurationSuccess, (state, { response }) => ({ on(loadFlowConfigurationSuccess, (state, { response }) => ({
...state, ...state,
flowConfiguration: response.flowConfiguration, flowConfiguration: response.flowConfiguration,
error: null,
status: 'success' as const status: 'success' as const
})),
on(flowConfigurationApiError, (state, { error }) => ({
...state,
error: error,
status: 'error' as const
})),
on(flowConfigurationAboutApiError, (state) => ({
...state,
error: null,
status: 'pending' as const
})) }))
); );

View File

@ -34,6 +34,5 @@ export interface FlowConfiguration {
export interface FlowConfigurationState { export interface FlowConfigurationState {
flowConfiguration: FlowConfiguration | null; flowConfiguration: FlowConfiguration | null;
error: string | null; status: 'pending' | 'loading' | 'success';
status: 'pending' | 'loading' | 'error' | 'success';
} }

View File

@ -75,6 +75,5 @@ export interface StatusHistoryResponse {
export interface StatusHistoryState { export interface StatusHistoryState {
statusHistory: StatusHistoryEntity; statusHistory: StatusHistoryEntity;
loadedTimestamp: string; loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success'; status: 'pending' | 'loading' | 'error' | 'success';
} }

View File

@ -50,8 +50,8 @@ export const openStatusHistoryDialog = createAction(
props<{ request: StatusHistoryRequest | NodeStatusHistoryRequest }>() props<{ request: StatusHistoryRequest | NodeStatusHistoryRequest }>()
); );
export const statusHistoryApiError = createAction( export const statusHistoryBannerError = createAction(
`${STATUS_HISTORY_PREFIX} Load Status History error`, `${STATUS_HISTORY_PREFIX} Status History Banner Error`,
props<{ error: string }>() props<{ error: string }>()
); );

View File

@ -25,6 +25,9 @@ import { StatusHistoryRequest } from './index';
import { catchError, filter, from, map, of, switchMap, tap } from 'rxjs'; import { catchError, filter, from, map, of, switchMap, tap } from 'rxjs';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { StatusHistory } from '../../ui/common/status-history/status-history.component'; import { StatusHistory } from '../../ui/common/status-history/status-history.component';
import * as ErrorActions from '../../state/error/error.actions';
import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHelper } from '../../service/error-helper.service';
@Injectable() @Injectable()
export class StatusHistoryEffects { export class StatusHistoryEffects {
@ -32,7 +35,8 @@ export class StatusHistoryEffects {
private actions$: Actions, private actions$: Actions,
private store: Store<NiFiState>, private store: Store<NiFiState>,
private statusHistoryService: StatusHistoryService, private statusHistoryService: StatusHistoryService,
private dialog: MatDialog private dialog: MatDialog,
private errorHelper: ErrorHelper
) {} ) {}
reloadComponentStatusHistory$ = createEffect(() => reloadComponentStatusHistory$ = createEffect(() =>
@ -55,12 +59,8 @@ export class StatusHistoryEffects {
} }
}) })
), ),
catchError((error) => catchError((errorResponse: HttpErrorResponse) =>
of( this.bannerOrFullScreenError(errorResponse)
StatusHistoryActions.statusHistoryApiError({
error: error.error
})
)
) )
) )
) )
@ -86,13 +86,7 @@ export class StatusHistoryEffects {
} }
}) })
), ),
catchError((error) => catchError((errorResponse: HttpErrorResponse) => this.bannerOrFullScreenError(errorResponse))
of(
StatusHistoryActions.statusHistoryApiError({
error: error.error
})
)
)
) )
) )
) )
@ -119,12 +113,8 @@ export class StatusHistoryEffects {
} }
}) })
), ),
catchError((error) => catchError((errorResponse: HttpErrorResponse) =>
of( this.snackBarOrFullScreenError(errorResponse)
StatusHistoryActions.statusHistoryApiError({
error: error.error
})
)
) )
) )
) )
@ -150,13 +140,7 @@ export class StatusHistoryEffects {
} }
}) })
), ),
catchError((error) => catchError((errorResponse: HttpErrorResponse) => this.snackBarOrFullScreenError(errorResponse))
of(
StatusHistoryActions.statusHistoryApiError({
error: error.error
})
)
)
) )
) )
) )
@ -210,4 +194,30 @@ export class StatusHistoryEffects {
), ),
{ dispatch: false } { dispatch: false }
); );
statusHistoryBannerError$ = createEffect(() =>
this.actions$.pipe(
ofType(StatusHistoryActions.statusHistoryBannerError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
)
);
private bannerOrFullScreenError(errorResponse: HttpErrorResponse) {
if (this.errorHelper.showErrorInContext(errorResponse.status)) {
const error = `Failed to reload Status History. - [${errorResponse.error || errorResponse.status}]`;
return of(StatusHistoryActions.statusHistoryBannerError({ error }));
} else {
return of(ErrorActions.fullScreenError(errorResponse.error));
}
}
private snackBarOrFullScreenError(errorResponse: HttpErrorResponse) {
if (this.errorHelper.showErrorInContext(errorResponse.status)) {
const error = `Failed to load Status History. - [${errorResponse.error || errorResponse.status}]`;
return of(ErrorActions.snackBarError({ error }));
} else {
return of(ErrorActions.fullScreenError(errorResponse.error));
}
}
} }

View File

@ -19,18 +19,18 @@ import { StatusHistoryEntity, StatusHistoryState } from './index';
import { createReducer, on } from '@ngrx/store'; import { createReducer, on } from '@ngrx/store';
import { import {
clearStatusHistory, clearStatusHistory,
reloadStatusHistory, getStatusHistoryAndOpenDialog,
loadStatusHistorySuccess, loadStatusHistorySuccess,
statusHistoryApiError, reloadStatusHistory,
viewStatusHistoryComplete,
reloadStatusHistorySuccess, reloadStatusHistorySuccess,
getStatusHistoryAndOpenDialog statusHistoryBannerError,
viewNodeStatusHistoryComplete,
viewStatusHistoryComplete
} from './status-history.actions'; } from './status-history.actions';
export const initialState: StatusHistoryState = { export const initialState: StatusHistoryState = {
statusHistory: {} as StatusHistoryEntity, statusHistory: {} as StatusHistoryEntity,
status: 'pending', status: 'pending',
error: null,
loadedTimestamp: '' loadedTimestamp: ''
}; };
@ -44,15 +44,13 @@ export const statusHistoryReducer = createReducer(
on(loadStatusHistorySuccess, reloadStatusHistorySuccess, (state, { response }) => ({ on(loadStatusHistorySuccess, reloadStatusHistorySuccess, (state, { response }) => ({
...state, ...state,
error: null,
status: 'success' as const, status: 'success' as const,
loadedTimestamp: response.statusHistory.statusHistory.generated, loadedTimestamp: response.statusHistory.statusHistory.generated,
statusHistory: response.statusHistory statusHistory: response.statusHistory
})), })),
on(statusHistoryApiError, (state, { error }) => ({ on(statusHistoryBannerError, (state) => ({
...state, ...state,
error,
status: 'error' as const status: 'error' as const
})), })),
@ -60,7 +58,7 @@ export const statusHistoryReducer = createReducer(
...initialState ...initialState
})), })),
on(viewStatusHistoryComplete, () => ({ on(viewStatusHistoryComplete, viewNodeStatusHistoryComplete, () => ({
...initialState ...initialState
})) }))
); );

View File

@ -91,6 +91,7 @@ export interface SystemDiagnosticSnapshot {
export interface SystemDiagnosticsRequest { export interface SystemDiagnosticsRequest {
nodewise: boolean; nodewise: boolean;
errorStrategy?: 'banner' | 'snackbar';
} }
export interface SystemDiagnosticsResponse { export interface SystemDiagnosticsResponse {
@ -100,7 +101,6 @@ export interface SystemDiagnosticsResponse {
export interface SystemDiagnosticsState { export interface SystemDiagnosticsState {
systemDiagnostics: SystemDiagnostics | null; systemDiagnostics: SystemDiagnostics | null;
loadedTimestamp: string; loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success'; status: 'pending' | 'loading' | 'error' | 'success';
} }

View File

@ -42,8 +42,13 @@ export const getSystemDiagnosticsAndOpenDialog = createAction(
export const openSystemDiagnosticsDialog = createAction(`${SYSTEM_DIAGNOSTICS_PREFIX} Open System Diagnostics Dialog`); export const openSystemDiagnosticsDialog = createAction(`${SYSTEM_DIAGNOSTICS_PREFIX} Open System Diagnostics Dialog`);
export const systemDiagnosticsApiError = createAction( export const systemDiagnosticsSnackbarError = createAction(
`${SYSTEM_DIAGNOSTICS_PREFIX} Load System Diagnostics Error`, `${SYSTEM_DIAGNOSTICS_PREFIX} Load System Diagnostics Snackbar Error`,
props<{ error: string }>()
);
export const systemDiagnosticsBannerError = createAction(
`${SYSTEM_DIAGNOSTICS_PREFIX} Load System Diagnostics Banner Error`,
props<{ error: string }>() props<{ error: string }>()
); );

View File

@ -26,6 +26,8 @@ import { catchError, from, map, of, switchMap, tap } from 'rxjs';
import { SystemDiagnosticsRequest } from './index'; import { SystemDiagnosticsRequest } from './index';
import { SystemDiagnosticsDialog } from '../../ui/common/system-diagnostics-dialog/system-diagnostics-dialog.component'; import { SystemDiagnosticsDialog } from '../../ui/common/system-diagnostics-dialog/system-diagnostics-dialog.component';
import { LARGE_DIALOG } from '../../index'; import { LARGE_DIALOG } from '../../index';
import * as ErrorActions from '../error/error.actions';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable() @Injectable()
export class SystemDiagnosticsEffects { export class SystemDiagnosticsEffects {
@ -49,13 +51,24 @@ export class SystemDiagnosticsEffects {
} }
}) })
), ),
catchError((error) => catchError((errorResponse: HttpErrorResponse) => {
of( if (request.errorStrategy === 'snackbar') {
SystemDiagnosticsActions.systemDiagnosticsApiError({ return of(
error: error.error SystemDiagnosticsActions.systemDiagnosticsSnackbarError({
error: `Failed to reload System Diagnostics. - [${
errorResponse.error || errorResponse.status
}]`
})
);
}
return of(
SystemDiagnosticsActions.systemDiagnosticsBannerError({
error: `Failed to reload System Diagnostics. - [${
errorResponse.error || errorResponse.status
}]`
}) })
) );
) })
) )
) )
) )
@ -74,10 +87,12 @@ export class SystemDiagnosticsEffects {
} }
}) })
), ),
catchError((error) => catchError((errorResponse: HttpErrorResponse) =>
of( of(
SystemDiagnosticsActions.systemDiagnosticsApiError({ SystemDiagnosticsActions.systemDiagnosticsSnackbarError({
error: error.error error: `Failed to load System Diagnostics. - [${
errorResponse.error || errorResponse.status
}]`
}) })
) )
) )
@ -110,4 +125,20 @@ export class SystemDiagnosticsEffects {
), ),
{ dispatch: false } { dispatch: false }
); );
systemDiagnosticsBannerError$ = createEffect(() =>
this.actions$.pipe(
ofType(SystemDiagnosticsActions.systemDiagnosticsBannerError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
)
);
systemDiagnosticsSnackbarError$ = createEffect(() =>
this.actions$.pipe(
ofType(SystemDiagnosticsActions.systemDiagnosticsSnackbarError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.snackBarError({ error })))
)
);
} }

View File

@ -18,19 +18,19 @@
import { SystemDiagnosticsState } from './index'; import { SystemDiagnosticsState } from './index';
import { createReducer, on } from '@ngrx/store'; import { createReducer, on } from '@ngrx/store';
import { import {
reloadSystemDiagnostics,
loadSystemDiagnosticsSuccess,
resetSystemDiagnostics,
systemDiagnosticsApiError,
viewSystemDiagnosticsComplete,
getSystemDiagnosticsAndOpenDialog, getSystemDiagnosticsAndOpenDialog,
reloadSystemDiagnosticsSuccess loadSystemDiagnosticsSuccess,
reloadSystemDiagnostics,
reloadSystemDiagnosticsSuccess,
resetSystemDiagnostics,
systemDiagnosticsBannerError,
systemDiagnosticsSnackbarError,
viewSystemDiagnosticsComplete
} from './system-diagnostics.actions'; } from './system-diagnostics.actions';
export const initialSystemDiagnosticsState: SystemDiagnosticsState = { export const initialSystemDiagnosticsState: SystemDiagnosticsState = {
systemDiagnostics: null, systemDiagnostics: null,
status: 'pending', status: 'pending',
error: null,
loadedTimestamp: '' loadedTimestamp: ''
}; };
@ -44,15 +44,13 @@ export const systemDiagnosticsReducer = createReducer(
on(loadSystemDiagnosticsSuccess, reloadSystemDiagnosticsSuccess, (state, { response }) => ({ on(loadSystemDiagnosticsSuccess, reloadSystemDiagnosticsSuccess, (state, { response }) => ({
...state, ...state,
error: null,
status: 'success' as const, status: 'success' as const,
loadedTimestamp: response.systemDiagnostics.aggregateSnapshot.statsLastRefreshed, loadedTimestamp: response.systemDiagnostics.aggregateSnapshot.statsLastRefreshed,
systemDiagnostics: response.systemDiagnostics systemDiagnostics: response.systemDiagnostics
})), })),
on(systemDiagnosticsApiError, (state, { error }) => ({ on(systemDiagnosticsBannerError, systemDiagnosticsSnackbarError, (state) => ({
...state, ...state,
error,
status: 'error' as const status: 'error' as const
})), })),

View File

@ -30,11 +30,6 @@ export const selectSystemDiagnosticsLoadedTimestamp = createSelector(
(state: SystemDiagnosticsState) => state.loadedTimestamp (state: SystemDiagnosticsState) => state.loadedTimestamp
); );
export const selectSystemDiagnosticsError = createSelector(
selectSystemDiagnosticsState,
(state: SystemDiagnosticsState) => state.error
);
export const selectSystemDiagnosticsStatus = createSelector( export const selectSystemDiagnosticsStatus = createSelector(
selectSystemDiagnosticsState, selectSystemDiagnosticsState,
(state: SystemDiagnosticsState) => state.status (state: SystemDiagnosticsState) => state.status

View File

@ -17,6 +17,7 @@
<div class="component-state-dialog" tabindex="0"> <div class="component-state-dialog" tabindex="0">
<h2 mat-dialog-title>Component State</h2> <h2 mat-dialog-title>Component State</h2>
<error-banner></error-banner>
<mat-dialog-content> <mat-dialog-content>
<div class="flex flex-col justify-between gap-y-5"> <div class="flex flex-col justify-between gap-y-5">
@if (componentName$ | async; as componentName) { @if (componentName$ | async; as componentName) {

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { AfterViewInit, Component, DestroyRef, inject, Input } from '@angular/core'; import { AfterViewInit, Component, DestroyRef, inject, Input, OnDestroy } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatTableDataSource, MatTableModule } from '@angular/material/table';
@ -39,6 +39,8 @@ import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { selectClusterSummary } from '../../../state/cluster-summary/cluster-summary.selectors'; import { selectClusterSummary } from '../../../state/cluster-summary/cluster-summary.selectors';
import { ErrorBanner } from '../error-banner/error-banner.component';
import { clearBannerErrors } from '../../../state/error/error.actions';
@Component({ @Component({
selector: 'component-state', selector: 'component-state',
@ -54,11 +56,12 @@ import { selectClusterSummary } from '../../../state/cluster-summary/cluster-sum
AsyncPipe, AsyncPipe,
ReactiveFormsModule, ReactiveFormsModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule MatInputModule,
ErrorBanner
], ],
styleUrls: ['./component-state.component.scss'] styleUrls: ['./component-state.component.scss']
}) })
export class ComponentStateDialog implements AfterViewInit { export class ComponentStateDialog implements AfterViewInit, OnDestroy {
@Input() initialSortColumn: 'key' | 'value' = 'key'; @Input() initialSortColumn: 'key' | 'value' = 'key';
@Input() initialSortDirection: 'asc' | 'desc' = 'asc'; @Input() initialSortDirection: 'asc' | 'desc' = 'asc';
@ -140,6 +143,10 @@ export class ComponentStateDialog implements AfterViewInit {
}); });
} }
ngOnDestroy(): void {
this.store.dispatch(clearBannerErrors());
}
processStateMap(stateMap: StateMap, clusterState: boolean): StateItem[] { processStateMap(stateMap: StateMap, clusterState: boolean): StateItem[] {
const stateEntries: StateEntry[] = stateMap.state ? stateMap.state : []; const stateEntries: StateEntry[] = stateMap.state ? stateMap.state : [];

View File

@ -17,6 +17,7 @@
<div resizable (resized)="resized()" [minHeight]="720" [minWidth]="887" class="flex flex-col status-history-container"> <div resizable (resized)="resized()" [minHeight]="720" [minWidth]="887" class="flex flex-col status-history-container">
@if (statusHistoryState$ | async; as statusHistoryState) { @if (statusHistoryState$ | async; as statusHistoryState) {
<h2 mat-dialog-title>Status History</h2> <h2 mat-dialog-title>Status History</h2>
<error-banner></error-banner>
<div class="status-history flex flex-col grow"> <div class="status-history flex flex-col grow">
<mat-dialog-content class="grow flex flex-1"> <mat-dialog-content class="grow flex flex-1">
<form [formGroup]="statusHistoryForm" class="flex flex-1 h-full"> <form [formGroup]="statusHistoryForm" class="flex flex-1 h-full">

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { AfterViewInit, Component, DestroyRef, inject, Inject, OnInit } from '@angular/core'; import { AfterViewInit, Component, DestroyRef, inject, Inject, OnDestroy, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { StatusHistoryService } from '../../../service/status-history.service'; import { StatusHistoryService } from '../../../service/status-history.service';
import { AsyncPipe, NgStyle } from '@angular/common'; import { AsyncPipe, NgStyle } from '@angular/common';
@ -51,6 +51,8 @@ import { Resizable } from '../resizable/resizable.component';
import { Instance, NIFI_NODE_CONFIG, Stats } from './index'; import { Instance, NIFI_NODE_CONFIG, Stats } from './index';
import { StatusHistoryChart } from './status-history-chart/status-history-chart.component'; import { StatusHistoryChart } from './status-history-chart/status-history-chart.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ErrorBanner } from '../error-banner/error-banner.component';
import { clearBannerErrors } from '../../../state/error/error.actions';
@Component({ @Component({
selector: 'status-history', selector: 'status-history',
@ -68,11 +70,12 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
MatCheckboxModule, MatCheckboxModule,
Resizable, Resizable,
StatusHistoryChart, StatusHistoryChart,
NgStyle NgStyle,
ErrorBanner
], ],
styleUrls: ['./status-history.component.scss'] styleUrls: ['./status-history.component.scss']
}) })
export class StatusHistory implements OnInit, AfterViewInit { export class StatusHistory implements OnInit, OnDestroy, AfterViewInit {
request: StatusHistoryRequest; request: StatusHistoryRequest;
statusHistoryState$ = this.store.select(selectStatusHistoryState); statusHistoryState$ = this.store.select(selectStatusHistoryState);
componentDetails$ = this.store.select(selectStatusHistoryComponentDetails); componentDetails$ = this.store.select(selectStatusHistoryComponentDetails);
@ -197,6 +200,10 @@ export class StatusHistory implements OnInit, AfterViewInit {
}); });
} }
ngOnDestroy(): void {
this.store.dispatch(clearBannerErrors());
}
ngAfterViewInit(): void { ngAfterViewInit(): void {
// when the selected descriptor changes, update the chart // when the selected descriptor changes, update the chart
this.statusHistoryForm this.statusHistoryForm

View File

@ -17,6 +17,7 @@
@if ((systemDiagnostics$ | async)?.aggregateSnapshot; as systemDiagnostics) { @if ((systemDiagnostics$ | async)?.aggregateSnapshot; as systemDiagnostics) {
<h2 mat-dialog-title>System Diagnostics</h2> <h2 mat-dialog-title>System Diagnostics</h2>
<error-banner></error-banner>
<div class="system-diagnostics"> <div class="system-diagnostics">
<mat-dialog-content> <mat-dialog-content>
<div class="dialog-content"> <div class="dialog-content">

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
@ -33,6 +33,8 @@ import { TextTip } from '../tooltips/text-tip/text-tip.component';
import { NifiTooltipDirective } from '../tooltips/nifi-tooltip.directive'; import { NifiTooltipDirective } from '../tooltips/nifi-tooltip.directive';
import { isDefinedAndNotNull } from '../../../state/shared'; import { isDefinedAndNotNull } from '../../../state/shared';
import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar';
import { ErrorBanner } from '../error-banner/error-banner.component';
import { clearBannerErrors } from '../../../state/error/error.actions';
@Component({ @Component({
selector: 'system-diagnostics-dialog', selector: 'system-diagnostics-dialog',
@ -43,12 +45,13 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
MatDialogModule, MatDialogModule,
MatButtonModule, MatButtonModule,
NifiTooltipDirective, NifiTooltipDirective,
MatProgressBarModule MatProgressBarModule,
ErrorBanner
], ],
templateUrl: './system-diagnostics-dialog.component.html', templateUrl: './system-diagnostics-dialog.component.html',
styleUrls: ['./system-diagnostics-dialog.component.scss'] styleUrls: ['./system-diagnostics-dialog.component.scss']
}) })
export class SystemDiagnosticsDialog implements OnInit { export class SystemDiagnosticsDialog implements OnInit, OnDestroy {
systemDiagnostics$ = this.store.select(selectSystemDiagnostics); systemDiagnostics$ = this.store.select(selectSystemDiagnostics);
loadedTimestamp$ = this.store.select(selectSystemDiagnosticsLoadedTimestamp); loadedTimestamp$ = this.store.select(selectSystemDiagnosticsLoadedTimestamp);
status$ = this.store.select(selectSystemDiagnosticsStatus); status$ = this.store.select(selectSystemDiagnosticsStatus);
@ -69,11 +72,16 @@ export class SystemDiagnosticsDialog implements OnInit {
}); });
} }
ngOnDestroy(): void {
this.store.dispatch(clearBannerErrors());
}
refreshSystemDiagnostics() { refreshSystemDiagnostics() {
this.store.dispatch( this.store.dispatch(
reloadSystemDiagnostics({ reloadSystemDiagnostics({
request: { request: {
nodewise: false nodewise: false,
errorStrategy: 'banner'
} }
}) })
); );