[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>
<div class="px-5 flex-1 flex flex-col gap-y-2">
<h3 class="text-xl bold primary-color">NiFi Cluster</h3>
<error-banner></error-banner>
@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">

View File

@ -33,6 +33,7 @@ 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';
import { clearBannerErrors } from '../../../state/error/error.actions';
interface TabLink {
label: string;
@ -82,6 +83,7 @@ export class Cluster implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.store.dispatch(resetClusterState());
this.store.dispatch(resetSystemDiagnostics());
this.store.dispatch(clearBannerErrors());
}
refresh() {

View File

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

View File

@ -16,14 +16,13 @@
*/
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 { 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 { catchError, filter, from, map, of, switchMap, take, tap } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
import { selectClusterListingStatus } from './cluster-listing.selectors';
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 { SelectClusterNodeRequest } from './index';
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
import { concatLatestFrom } from '@ngrx/operators';
@Injectable()
export class ClusterListingEffects {
@ -43,7 +43,6 @@ export class ClusterListingEffects {
private store: Store<NiFiState>,
private errorHelper: ErrorHelper,
private router: Router,
private systemDiagnosticsService: SystemDiagnosticsService,
private clusterService: ClusterService,
private dialog: MatDialog
) {}
@ -51,12 +50,6 @@ export class ClusterListingEffects {
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(
@ -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(
() =>
this.actions$.pipe(

View File

@ -41,13 +41,6 @@ export const setDisconnectionAcknowledged = createAction(
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(
`${CLUSTER_SUMMARY_STATE_PREFIX} Search Cluster`,
props<{ request: ClusterSearchRequest }>()

View File

@ -19,6 +19,7 @@ import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { concatLatestFrom } from '@ngrx/operators';
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 { ClusterService } from '../../service/cluster.service';
import { selectClusterSummary } from './cluster-summary.selectors';
@ -27,7 +28,6 @@ import { Store } from '@ngrx/store';
import { ClusterSummary, ClusterSummaryState } from './index';
import { HttpErrorResponse } from '@angular/common/http';
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 { MEDIUM_DIALOG } from '../../index';
import { MatDialog } from '@angular/material/dialog';
@ -66,7 +66,15 @@ export class ClusterSummaryEffects {
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) =>
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 { ClusterSummaryState } from './index';
import {
clearClusterSummaryApiError,
clusterSummaryApiError,
loadClusterSummary,
loadClusterSummarySuccess,
searchClusterSuccess,
@ -30,7 +28,6 @@ export const initialState: ClusterSummaryState = {
disconnectionAcknowledged: false,
clusterSummary: null,
searchResults: null,
error: null,
status: 'pending'
};
@ -43,19 +40,8 @@ export const clusterSummaryReducer = createReducer(
on(loadClusterSummarySuccess, (state, { response }) => ({
...state,
clusterSummary: response.clusterSummary,
error: null,
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 }) => ({
...state,
searchResults: response

View File

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

View File

@ -32,11 +32,6 @@ export const loadComponentStateSuccess = createAction(
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`);

View File

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

View File

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

View File

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

View File

@ -25,10 +25,6 @@ export const loadCurrentUserSuccess = createAction(
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 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 { asyncScheduler, catchError, from, interval, map, of, switchMap, takeUntil } from 'rxjs';
import { CurrentUserService } from '../../service/current-user.service';
import { ErrorHelper } from '../../service/error-helper.service';
@Injectable()
export class CurrentUserEffects {
constructor(
private actions$: Actions,
private userService: CurrentUserService
private userService: CurrentUserService,
private errorHelper: ErrorHelper
) {}
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 { CurrentUserState } from './index';
import { Permissions } from '../shared';
import {
clearCurrentUserApiError,
loadCurrentUser,
loadCurrentUserSuccess,
currentUserApiError
} from './current-user.actions';
import { loadCurrentUser, loadCurrentUserSuccess } from './current-user.actions';
export const NO_PERMISSIONS: Permissions = {
canRead: false,
@ -45,7 +40,6 @@ export const initialState: CurrentUserState = {
tenantsPermissions: NO_PERMISSIONS,
componentRestrictionPermissions: []
},
error: null,
status: 'pending'
};
@ -58,17 +52,6 @@ export const currentUserReducer = createReducer(
on(loadCurrentUserSuccess, (state, { response }) => ({
...state,
user: response.user,
error: null,
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 {
user: CurrentUser;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
status: 'pending' | 'loading' | 'success';
}

View File

@ -21,6 +21,7 @@ import {
LoadExtensionTypesForPoliciesResponse,
LoadExtensionTypesForSettingsResponse
} from './index';
import { HttpErrorResponse } from '@angular/common/http';
export const loadExtensionTypesForCanvas = createAction('[Extension Types] Load Extension Types For Canvas');
@ -45,7 +46,5 @@ export const loadExtensionTypesForPoliciesSuccess = createAction(
export const extensionTypesApiError = createAction(
'[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 { catchError, combineLatest, map, of, switchMap } from 'rxjs';
import { ExtensionTypesService } from '../../service/extension-types.service';
import * as ErrorActions from '../error/error.actions';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable()
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 { ExtensionTypesState } from './index';
import {
clearExtensionTypesApiError,
extensionTypesApiError,
loadExtensionTypesForCanvas,
loadExtensionTypesForCanvasSuccess,
loadExtensionTypesForPoliciesSuccess,
@ -34,7 +32,6 @@ export const initialState: ExtensionTypesState = {
registryClientTypes: [],
flowAnalysisRuleTypes: [],
parameterProviderTypes: [],
error: null,
status: 'pending'
};
@ -49,7 +46,6 @@ export const extensionTypesReducer = createReducer(
processorTypes: response.processorTypes,
controllerServiceTypes: response.controllerServiceTypes,
prioritizerTypes: response.prioritizers,
error: null,
status: 'success' as const
})),
on(loadExtensionTypesForSettingsSuccess, (state, { response }) => ({
@ -59,7 +55,6 @@ export const extensionTypesReducer = createReducer(
registryClientTypes: response.registryClientTypes,
parameterProviderTypes: response.parameterProviderTypes,
flowAnalysisRuleTypes: response.flowAnalysisRuleTypes,
error: null,
status: 'success' as const
})),
on(loadExtensionTypesForPoliciesSuccess, (state, { response }) => ({
@ -69,17 +64,6 @@ export const extensionTypesReducer = createReducer(
reportingTaskTypes: response.reportingTaskTypes,
parameterProviderTypes: response.parameterProviderTypes,
flowAnalysisRuleTypes: response.flowAnalysisRuleTypes,
error: null,
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[];
flowAnalysisRuleTypes: DocumentedType[];
parameterProviderTypes: DocumentedType[];
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
status: 'pending' | 'loading' | 'success';
}

View File

@ -24,10 +24,3 @@ export const loadFlowConfigurationSuccess = createAction(
'[Flow Configuration] Load Flow Configuration Success',
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 { catchError, from, map, of, switchMap } from 'rxjs';
import { FlowConfigurationService } from '../../service/flow-configuration.service';
import * as ErrorActions from '../error/error.actions';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable()
export class FlowConfigurationEffects {
@ -39,8 +41,14 @@ export class FlowConfigurationEffects {
response
})
),
catchError((error) =>
of(FlowConfigurationActions.flowConfigurationApiError({ error: error.error }))
catchError((errorResponse: HttpErrorResponse) =>
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 { FlowConfigurationState } from './index';
import {
flowConfigurationApiError,
flowConfigurationAboutApiError,
loadFlowConfiguration,
loadFlowConfigurationSuccess
} from './flow-configuration.actions';
import { loadFlowConfiguration, loadFlowConfigurationSuccess } from './flow-configuration.actions';
export const initialState: FlowConfigurationState = {
flowConfiguration: null,
error: null,
status: 'pending'
};
@ -39,17 +33,6 @@ export const flowConfigurationReducer = createReducer(
on(loadFlowConfigurationSuccess, (state, { response }) => ({
...state,
flowConfiguration: response.flowConfiguration,
error: null,
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 {
flowConfiguration: FlowConfiguration | null;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
status: 'pending' | 'loading' | 'success';
}

View File

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

View File

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

View File

@ -25,6 +25,9 @@ import { StatusHistoryRequest } from './index';
import { catchError, filter, from, map, of, switchMap, tap } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
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()
export class StatusHistoryEffects {
@ -32,7 +35,8 @@ export class StatusHistoryEffects {
private actions$: Actions,
private store: Store<NiFiState>,
private statusHistoryService: StatusHistoryService,
private dialog: MatDialog
private dialog: MatDialog,
private errorHelper: ErrorHelper
) {}
reloadComponentStatusHistory$ = createEffect(() =>
@ -55,12 +59,8 @@ export class StatusHistoryEffects {
}
})
),
catchError((error) =>
of(
StatusHistoryActions.statusHistoryApiError({
error: error.error
})
)
catchError((errorResponse: HttpErrorResponse) =>
this.bannerOrFullScreenError(errorResponse)
)
)
)
@ -86,13 +86,7 @@ export class StatusHistoryEffects {
}
})
),
catchError((error) =>
of(
StatusHistoryActions.statusHistoryApiError({
error: error.error
})
)
)
catchError((errorResponse: HttpErrorResponse) => this.bannerOrFullScreenError(errorResponse))
)
)
)
@ -119,12 +113,8 @@ export class StatusHistoryEffects {
}
})
),
catchError((error) =>
of(
StatusHistoryActions.statusHistoryApiError({
error: error.error
})
)
catchError((errorResponse: HttpErrorResponse) =>
this.snackBarOrFullScreenError(errorResponse)
)
)
)
@ -150,13 +140,7 @@ export class StatusHistoryEffects {
}
})
),
catchError((error) =>
of(
StatusHistoryActions.statusHistoryApiError({
error: error.error
})
)
)
catchError((errorResponse: HttpErrorResponse) => this.snackBarOrFullScreenError(errorResponse))
)
)
)
@ -210,4 +194,30 @@ export class StatusHistoryEffects {
),
{ 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 {
clearStatusHistory,
reloadStatusHistory,
getStatusHistoryAndOpenDialog,
loadStatusHistorySuccess,
statusHistoryApiError,
viewStatusHistoryComplete,
reloadStatusHistory,
reloadStatusHistorySuccess,
getStatusHistoryAndOpenDialog
statusHistoryBannerError,
viewNodeStatusHistoryComplete,
viewStatusHistoryComplete
} from './status-history.actions';
export const initialState: StatusHistoryState = {
statusHistory: {} as StatusHistoryEntity,
status: 'pending',
error: null,
loadedTimestamp: ''
};
@ -44,15 +44,13 @@ export const statusHistoryReducer = createReducer(
on(loadStatusHistorySuccess, reloadStatusHistorySuccess, (state, { response }) => ({
...state,
error: null,
status: 'success' as const,
loadedTimestamp: response.statusHistory.statusHistory.generated,
statusHistory: response.statusHistory
})),
on(statusHistoryApiError, (state, { error }) => ({
on(statusHistoryBannerError, (state) => ({
...state,
error,
status: 'error' as const
})),
@ -60,7 +58,7 @@ export const statusHistoryReducer = createReducer(
...initialState
})),
on(viewStatusHistoryComplete, () => ({
on(viewStatusHistoryComplete, viewNodeStatusHistoryComplete, () => ({
...initialState
}))
);

View File

@ -91,6 +91,7 @@ export interface SystemDiagnosticSnapshot {
export interface SystemDiagnosticsRequest {
nodewise: boolean;
errorStrategy?: 'banner' | 'snackbar';
}
export interface SystemDiagnosticsResponse {
@ -100,7 +101,6 @@ export interface SystemDiagnosticsResponse {
export interface SystemDiagnosticsState {
systemDiagnostics: SystemDiagnostics | null;
loadedTimestamp: string;
error: string | null;
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 systemDiagnosticsApiError = createAction(
`${SYSTEM_DIAGNOSTICS_PREFIX} Load System Diagnostics Error`,
export const systemDiagnosticsSnackbarError = createAction(
`${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 }>()
);

View File

@ -26,6 +26,8 @@ import { catchError, from, map, of, switchMap, tap } from 'rxjs';
import { SystemDiagnosticsRequest } from './index';
import { SystemDiagnosticsDialog } from '../../ui/common/system-diagnostics-dialog/system-diagnostics-dialog.component';
import { LARGE_DIALOG } from '../../index';
import * as ErrorActions from '../error/error.actions';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable()
export class SystemDiagnosticsEffects {
@ -49,13 +51,24 @@ export class SystemDiagnosticsEffects {
}
})
),
catchError((error) =>
of(
SystemDiagnosticsActions.systemDiagnosticsApiError({
error: error.error
catchError((errorResponse: HttpErrorResponse) => {
if (request.errorStrategy === 'snackbar') {
return of(
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(
SystemDiagnosticsActions.systemDiagnosticsApiError({
error: error.error
SystemDiagnosticsActions.systemDiagnosticsSnackbarError({
error: `Failed to load System Diagnostics. - [${
errorResponse.error || errorResponse.status
}]`
})
)
)
@ -110,4 +125,20 @@ export class SystemDiagnosticsEffects {
),
{ 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 { createReducer, on } from '@ngrx/store';
import {
reloadSystemDiagnostics,
loadSystemDiagnosticsSuccess,
resetSystemDiagnostics,
systemDiagnosticsApiError,
viewSystemDiagnosticsComplete,
getSystemDiagnosticsAndOpenDialog,
reloadSystemDiagnosticsSuccess
loadSystemDiagnosticsSuccess,
reloadSystemDiagnostics,
reloadSystemDiagnosticsSuccess,
resetSystemDiagnostics,
systemDiagnosticsBannerError,
systemDiagnosticsSnackbarError,
viewSystemDiagnosticsComplete
} from './system-diagnostics.actions';
export const initialSystemDiagnosticsState: SystemDiagnosticsState = {
systemDiagnostics: null,
status: 'pending',
error: null,
loadedTimestamp: ''
};
@ -44,15 +44,13 @@ export const systemDiagnosticsReducer = createReducer(
on(loadSystemDiagnosticsSuccess, reloadSystemDiagnosticsSuccess, (state, { response }) => ({
...state,
error: null,
status: 'success' as const,
loadedTimestamp: response.systemDiagnostics.aggregateSnapshot.statsLastRefreshed,
systemDiagnostics: response.systemDiagnostics
})),
on(systemDiagnosticsApiError, (state, { error }) => ({
on(systemDiagnosticsBannerError, systemDiagnosticsSnackbarError, (state) => ({
...state,
error,
status: 'error' as const
})),

View File

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

View File

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

View File

@ -15,7 +15,7 @@
* 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 { MatDialogModule } from '@angular/material/dialog';
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 { MatInputModule } from '@angular/material/input';
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({
selector: 'component-state',
@ -54,11 +56,12 @@ import { selectClusterSummary } from '../../../state/cluster-summary/cluster-sum
AsyncPipe,
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule
MatInputModule,
ErrorBanner
],
styleUrls: ['./component-state.component.scss']
})
export class ComponentStateDialog implements AfterViewInit {
export class ComponentStateDialog implements AfterViewInit, OnDestroy {
@Input() initialSortColumn: 'key' | 'value' = 'key';
@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[] {
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">
@if (statusHistoryState$ | async; as statusHistoryState) {
<h2 mat-dialog-title>Status History</h2>
<error-banner></error-banner>
<div class="status-history flex flex-col grow">
<mat-dialog-content class="grow flex flex-1">
<form [formGroup]="statusHistoryForm" class="flex flex-1 h-full">

View File

@ -15,7 +15,7 @@
* 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 { StatusHistoryService } from '../../../service/status-history.service';
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 { StatusHistoryChart } from './status-history-chart/status-history-chart.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ErrorBanner } from '../error-banner/error-banner.component';
import { clearBannerErrors } from '../../../state/error/error.actions';
@Component({
selector: 'status-history',
@ -68,11 +70,12 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
MatCheckboxModule,
Resizable,
StatusHistoryChart,
NgStyle
NgStyle,
ErrorBanner
],
styleUrls: ['./status-history.component.scss']
})
export class StatusHistory implements OnInit, AfterViewInit {
export class StatusHistory implements OnInit, OnDestroy, AfterViewInit {
request: StatusHistoryRequest;
statusHistoryState$ = this.store.select(selectStatusHistoryState);
componentDetails$ = this.store.select(selectStatusHistoryComponentDetails);
@ -197,6 +200,10 @@ export class StatusHistory implements OnInit, AfterViewInit {
});
}
ngOnDestroy(): void {
this.store.dispatch(clearBannerErrors());
}
ngAfterViewInit(): void {
// when the selected descriptor changes, update the chart
this.statusHistoryForm

View File

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

View File

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