NIFI-12937: Adding error handling to the Parameter Context Listing page (#8555)

* NIFI-12937:
- Adding error handling to the Parameter Context Listing page.

* NIFI-12937:
- Addressing review feedback.

This closes #8555
This commit is contained in:
Matt Gilman 2024-03-26 15:07:14 -04:00 committed by GitHub
parent 039fd66911
commit 339a06a8c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 173 additions and 97 deletions

View File

@ -17,7 +17,7 @@ const target = {
console.log('Received Response from the Target:', proxyRes.statusCode, req.url); console.log('Received Response from the Target:', proxyRes.statusCode, req.url);
}); });
}, },
bypass: function(req, res, proxyOptions) { bypass: function (req) {
if (req.url.startsWith('/nifi/')) { if (req.url.startsWith('/nifi/')) {
return req.url; return req.url;
} }

View File

@ -89,6 +89,5 @@ export interface ParameterContextListingState {
updateRequestEntity: ParameterContextUpdateRequestEntity | null; updateRequestEntity: ParameterContextUpdateRequestEntity | null;
saving: boolean; saving: boolean;
loadedTimestamp: string; loadedTimestamp: string;
error: string | null; status: 'pending' | 'loading' | 'success';
status: 'pending' | 'loading' | 'error' | 'success';
} }

View File

@ -35,8 +35,13 @@ export const loadParameterContextsSuccess = createAction(
props<{ response: LoadParameterContextsResponse }>() props<{ response: LoadParameterContextsResponse }>()
); );
export const parameterContextListingApiError = createAction( export const parameterContextListingSnackbarApiError = createAction(
'[Parameter Context Listing] Load Parameter Context Listing Error', '[Parameter Context Listing] Load Parameter Context Listing Snackbar Api Error',
props<{ error: string }>()
);
export const parameterContextListingBannerApiError = createAction(
'[Parameter Context Listing] Load Parameter Context Listing Banner Api Error',
props<{ error: string }>() props<{ error: string }>()
); );
@ -100,6 +105,11 @@ export const deleteParameterContextUpdateRequest = createAction(
'[Parameter Context Listing] Delete Parameter Context Update Request' '[Parameter Context Listing] Delete Parameter Context Update Request'
); );
export const deleteParameterContextUpdateRequestSuccess = createAction(
'[Parameter Context Listing] Delete Parameter Context Update Request Success',
props<{ response: PollParameterContextUpdateSuccess }>()
);
export const editParameterContextComplete = createAction('[Parameter Context Listing] Edit Parameter Context Complete'); export const editParameterContextComplete = createAction('[Parameter Context Listing] Edit Parameter Context Complete');
export const promptParameterContextDeletion = createAction( export const promptParameterContextDeletion = createAction(

View File

@ -18,13 +18,14 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects'; import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
import * as ParameterContextListingActions from './parameter-context-listing.actions'; import * as ParameterContextListingActions from './parameter-context-listing.actions';
import * as ErrorActions from '../../../../state/error/error.actions';
import { import {
asyncScheduler, asyncScheduler,
catchError, catchError,
filter,
from, from,
interval, interval,
map, map,
NEVER,
Observable, Observable,
of, of,
switchMap, switchMap,
@ -39,15 +40,23 @@ import { Router } from '@angular/router';
import { ParameterContextService } from '../../service/parameter-contexts.service'; import { ParameterContextService } from '../../service/parameter-contexts.service';
import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component'; import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component';
import { EditParameterContext } from '../../ui/parameter-context-listing/edit-parameter-context/edit-parameter-context.component'; import { EditParameterContext } from '../../ui/parameter-context-listing/edit-parameter-context/edit-parameter-context.component';
import { selectParameterContexts, selectSaving, selectUpdateRequest } from './parameter-context-listing.selectors'; import {
selectParameterContexts,
selectParameterContextStatus,
selectSaving,
selectUpdateRequest
} from './parameter-context-listing.selectors';
import { import {
EditParameterRequest, EditParameterRequest,
EditParameterResponse, EditParameterResponse,
isDefinedAndNotNull,
Parameter, Parameter,
ParameterContextUpdateRequest ParameterContextUpdateRequest
} from '../../../../state/shared'; } from '../../../../state/shared';
import { EditParameterDialog } from '../../../../ui/common/edit-parameter-dialog/edit-parameter-dialog.component'; import { EditParameterDialog } from '../../../../ui/common/edit-parameter-dialog/edit-parameter-dialog.component';
import { OkDialog } from '../../../../ui/common/ok-dialog/ok-dialog.component'; import { OkDialog } from '../../../../ui/common/ok-dialog/ok-dialog.component';
import { ErrorHelper } from '../../../../service/error-helper.service';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable() @Injectable()
export class ParameterContextListingEffects { export class ParameterContextListingEffects {
@ -56,13 +65,15 @@ export class ParameterContextListingEffects {
private store: Store<NiFiState>, private store: Store<NiFiState>,
private parameterContextService: ParameterContextService, private parameterContextService: ParameterContextService,
private dialog: MatDialog, private dialog: MatDialog,
private router: Router private router: Router,
private errorHelper: ErrorHelper
) {} ) {}
loadParameterContexts$ = createEffect(() => loadParameterContexts$ = createEffect(() =>
this.actions$.pipe( this.actions$.pipe(
ofType(ParameterContextListingActions.loadParameterContexts), ofType(ParameterContextListingActions.loadParameterContexts),
switchMap(() => concatLatestFrom(() => this.store.select(selectParameterContextStatus)),
switchMap(([, status]) =>
from(this.parameterContextService.getParameterContexts()).pipe( from(this.parameterContextService.getParameterContexts()).pipe(
map((response) => map((response) =>
ParameterContextListingActions.loadParameterContextsSuccess({ ParameterContextListingActions.loadParameterContextsSuccess({
@ -72,12 +83,8 @@ export class ParameterContextListingEffects {
} }
}) })
), ),
catchError((error) => catchError((errorResponse: HttpErrorResponse) =>
of( of(this.errorHelper.handleLoadingError(status, errorResponse))
ParameterContextListingActions.parameterContextListingApiError({
error: error.error
})
)
) )
) )
) )
@ -121,13 +128,15 @@ export class ParameterContextListingEffects {
); );
}; };
dialogReference.componentInstance.addParameterContext.pipe(take(1)).subscribe((payload: any) => { dialogReference.componentInstance.addParameterContext
this.store.dispatch( .pipe(takeUntil(dialogReference.afterClosed()))
ParameterContextListingActions.createParameterContext({ .subscribe((payload: any) => {
request: { payload } this.store.dispatch(
}) ParameterContextListingActions.createParameterContext({
); request: { payload }
}); })
);
});
}) })
), ),
{ dispatch: false } { dispatch: false }
@ -146,13 +155,18 @@ export class ParameterContextListingEffects {
} }
}) })
), ),
catchError((error) => catchError((errorResponse: HttpErrorResponse) => {
of( if (this.errorHelper.showErrorInContext(errorResponse.status)) {
ParameterContextListingActions.parameterContextListingApiError({ return of(
error: error.error ParameterContextListingActions.parameterContextListingBannerApiError({
}) error: errorResponse.error
) })
) );
} else {
this.dialog.closeAll();
return of(this.errorHelper.fullScreenError(errorResponse));
}
})
) )
) )
) )
@ -169,6 +183,22 @@ export class ParameterContextListingEffects {
{ dispatch: false } { dispatch: false }
); );
parameterContextListingBannerApiError$ = createEffect(() =>
this.actions$.pipe(
ofType(ParameterContextListingActions.parameterContextListingBannerApiError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
)
);
parameterContextListingSnackbarApiError$ = createEffect(() =>
this.actions$.pipe(
ofType(ParameterContextListingActions.parameterContextListingSnackbarApiError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.snackBarError({ error })))
)
);
navigateToEditService$ = createEffect( navigateToEditService$ = createEffect(
() => () =>
this.actions$.pipe( this.actions$.pipe(
@ -194,13 +224,14 @@ export class ParameterContextListingEffects {
} }
}) })
), ),
catchError((error) => catchError((errorResponse: HttpErrorResponse) => {
of( this.router.navigate(['/parameter-contexts']);
ParameterContextListingActions.parameterContextListingApiError({ return of(
error: error.error ParameterContextListingActions.parameterContextListingSnackbarApiError({
error: errorResponse.error
}) })
) );
) })
) )
) )
) )
@ -281,7 +312,7 @@ export class ParameterContextListingEffects {
}; };
editDialogReference.componentInstance.editParameterContext editDialogReference.componentInstance.editParameterContext
.pipe(take(1)) .pipe(takeUntil(editDialogReference.afterClosed()))
.subscribe((payload: any) => { .subscribe((payload: any) => {
this.store.dispatch( this.store.dispatch(
ParameterContextListingActions.submitParameterContextUpdateRequest({ ParameterContextListingActions.submitParameterContextUpdateRequest({
@ -294,6 +325,8 @@ export class ParameterContextListingEffects {
}); });
editDialogReference.afterClosed().subscribe((response) => { editDialogReference.afterClosed().subscribe((response) => {
this.store.dispatch(ErrorActions.clearBannerErrors());
if (response != 'ROUTED') { if (response != 'ROUTED') {
this.store.dispatch( this.store.dispatch(
ParameterContextListingActions.selectParameterContext({ ParameterContextListingActions.selectParameterContext({
@ -323,13 +356,18 @@ export class ParameterContextListingEffects {
} }
}) })
), ),
catchError((error) => catchError((errorResponse: HttpErrorResponse) => {
of( if (this.errorHelper.showErrorInContext(errorResponse.status)) {
ParameterContextListingActions.parameterContextListingApiError({ return of(
error: error.error ParameterContextListingActions.parameterContextListingBannerApiError({
}) error: errorResponse.error
) })
) );
} else {
this.dialog.closeAll();
return of(this.errorHelper.fullScreenError(errorResponse));
}
})
) )
) )
) )
@ -369,29 +407,31 @@ export class ParameterContextListingEffects {
pollParameterContextUpdateRequest$ = createEffect(() => pollParameterContextUpdateRequest$ = createEffect(() =>
this.actions$.pipe( this.actions$.pipe(
ofType(ParameterContextListingActions.pollParameterContextUpdateRequest), ofType(ParameterContextListingActions.pollParameterContextUpdateRequest),
concatLatestFrom(() => this.store.select(selectUpdateRequest)), concatLatestFrom(() => this.store.select(selectUpdateRequest).pipe(isDefinedAndNotNull())),
switchMap(([, updateRequest]) => { switchMap(([, updateRequest]) =>
if (updateRequest) { from(this.parameterContextService.pollParameterContextUpdate(updateRequest.request)).pipe(
return from(this.parameterContextService.pollParameterContextUpdate(updateRequest.request)).pipe( map((response) =>
map((response) => ParameterContextListingActions.pollParameterContextUpdateRequestSuccess({
ParameterContextListingActions.pollParameterContextUpdateRequestSuccess({ response: {
response: { requestEntity: response
requestEntity: response }
} })
}) ),
), catchError((errorResponse: HttpErrorResponse) => {
catchError((error) => this.store.dispatch(ParameterContextListingActions.stopPollingParameterContextUpdateRequest());
of(
ParameterContextListingActions.parameterContextListingApiError({ if (this.errorHelper.showErrorInContext(errorResponse.status)) {
error: error.error return of(
ParameterContextListingActions.parameterContextListingBannerApiError({
error: errorResponse.error
}) })
) );
) } else {
); return of(this.errorHelper.fullScreenError(errorResponse));
} else { }
return NEVER; })
} )
}) )
) )
); );
@ -399,14 +439,8 @@ export class ParameterContextListingEffects {
this.actions$.pipe( this.actions$.pipe(
ofType(ParameterContextListingActions.pollParameterContextUpdateRequestSuccess), ofType(ParameterContextListingActions.pollParameterContextUpdateRequestSuccess),
map((action) => action.response), map((action) => action.response),
switchMap((response) => { filter((response) => response.requestEntity.request.complete),
const updateRequest: ParameterContextUpdateRequest = response.requestEntity.request; switchMap(() => of(ParameterContextListingActions.stopPollingParameterContextUpdateRequest()))
if (updateRequest.complete) {
return of(ParameterContextListingActions.stopPollingParameterContextUpdateRequest());
} else {
return NEVER;
}
})
) )
); );
@ -421,11 +455,19 @@ export class ParameterContextListingEffects {
() => () =>
this.actions$.pipe( this.actions$.pipe(
ofType(ParameterContextListingActions.deleteParameterContextUpdateRequest), ofType(ParameterContextListingActions.deleteParameterContextUpdateRequest),
concatLatestFrom(() => this.store.select(selectUpdateRequest)), concatLatestFrom(() => this.store.select(selectUpdateRequest).pipe(isDefinedAndNotNull())),
tap(([, updateRequest]) => { tap(([, updateRequest]) => {
if (updateRequest) { this.parameterContextService
this.parameterContextService.deleteParameterContextUpdate(updateRequest.request).subscribe(); .deleteParameterContextUpdate(updateRequest.request)
} .subscribe((response) => {
this.store.dispatch(
ParameterContextListingActions.deleteParameterContextUpdateRequestSuccess({
response: {
requestEntity: response
}
})
);
});
}) })
), ),
{ dispatch: false } { dispatch: false }
@ -472,7 +514,7 @@ export class ParameterContextListingEffects {
), ),
catchError((error) => catchError((error) =>
of( of(
ParameterContextListingActions.parameterContextListingApiError({ ParameterContextListingActions.parameterContextListingSnackbarApiError({
error: error.error error: error.error
}) })
) )

View File

@ -25,10 +25,12 @@ import {
editParameterContextComplete, editParameterContextComplete,
loadParameterContexts, loadParameterContexts,
loadParameterContextsSuccess, loadParameterContextsSuccess,
parameterContextListingApiError, parameterContextListingSnackbarApiError,
parameterContextListingBannerApiError,
pollParameterContextUpdateRequestSuccess, pollParameterContextUpdateRequestSuccess,
submitParameterContextUpdateRequest, submitParameterContextUpdateRequest,
submitParameterContextUpdateRequestSuccess submitParameterContextUpdateRequestSuccess,
deleteParameterContextUpdateRequestSuccess
} from './parameter-context-listing.actions'; } from './parameter-context-listing.actions';
import { ParameterContextUpdateRequestEntity, Revision } from '../../../../state/shared'; import { ParameterContextUpdateRequestEntity, Revision } from '../../../../state/shared';
@ -37,7 +39,6 @@ export const initialState: ParameterContextListingState = {
updateRequestEntity: null, updateRequestEntity: null,
saving: false, saving: false,
loadedTimestamp: '', loadedTimestamp: '',
error: null,
status: 'pending' status: 'pending'
}; };
@ -54,11 +55,9 @@ export const parameterContextListingReducer = createReducer(
error: null, error: null,
status: 'success' as const status: 'success' as const
})), })),
on(parameterContextListingApiError, (state, { error }) => ({ on(parameterContextListingSnackbarApiError, parameterContextListingBannerApiError, (state) => ({
...state, ...state,
saving: false, saving: false
error,
status: 'error' as const
})), })),
on(createParameterContext, (state) => ({ on(createParameterContext, (state) => ({
...state, ...state,
@ -74,10 +73,15 @@ export const parameterContextListingReducer = createReducer(
...state, ...state,
saving: true saving: true
})), })),
on(submitParameterContextUpdateRequestSuccess, pollParameterContextUpdateRequestSuccess, (state, { response }) => ({ on(
...state, submitParameterContextUpdateRequestSuccess,
updateRequestEntity: response.requestEntity pollParameterContextUpdateRequestSuccess,
})), deleteParameterContextUpdateRequestSuccess,
(state, { response }) => ({
...state,
updateRequestEntity: response.requestEntity
})
),
on(editParameterContextComplete, (state) => { on(editParameterContextComplete, (state) => {
return produce(state, (draftState) => { return produce(state, (draftState) => {
const updateRequestEntity: ParameterContextUpdateRequestEntity | null = draftState.updateRequestEntity; const updateRequestEntity: ParameterContextUpdateRequestEntity | null = draftState.updateRequestEntity;

View File

@ -54,6 +54,11 @@ export const selectParameterContexts = createSelector(
(state: ParameterContextListingState) => state.parameterContexts (state: ParameterContextListingState) => state.parameterContexts
); );
export const selectParameterContextStatus = createSelector(
selectParameterContextListingState,
(state: ParameterContextListingState) => state.status
);
export const selectContext = (id: string) => export const selectContext = (id: string) =>
createSelector(selectParameterContexts, (parameterContexts: ParameterContextEntity[]) => createSelector(selectParameterContexts, (parameterContexts: ParameterContextEntity[]) =>
parameterContexts.find((entity) => id == entity.id) parameterContexts.find((entity) => id == entity.id)

View File

@ -17,6 +17,7 @@
<h2 mat-dialog-title>{{ this.isNew ? 'Add' : 'Edit' }} Parameter Context</h2> <h2 mat-dialog-title>{{ this.isNew ? 'Add' : 'Edit' }} Parameter Context</h2>
<form class="parameter-context-edit-form" [formGroup]="editParameterContextForm"> <form class="parameter-context-edit-form" [formGroup]="editParameterContextForm">
<error-banner></error-banner>
@if ((updateRequest | async)!; as requestEntity) { @if ((updateRequest | async)!; as requestEntity) {
<mat-dialog-content> <mat-dialog-content>
<div class="results-content flex gap-x-8"> <div class="results-content flex gap-x-8">
@ -26,12 +27,21 @@
@for (updateStep of requestEntity.request.updateSteps; track updateStep) { @for (updateStep of requestEntity.request.updateSteps; track updateStep) {
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="value">{{ updateStep.description }}</div> <div class="value">{{ updateStep.description }}</div>
@if (updateStep.complete) { @if (updateStep.failureReason) {
<div class="fa fa-check complete"></div> <div class="fa fa-times warn-default"></div>
} @else { } @else {
<div class="fa fa-spin fa-circle-o-notch"></div> @if (updateStep.complete) {
<div class="fa fa-check complete"></div>
} @else {
<div class="fa fa-spin fa-circle-o-notch"></div>
}
} }
</div> </div>
@if (updateStep.failureReason) {
<div class="text-xs ml-2">
{{ updateStep.failureReason }}
</div>
}
} }
</div> </div>
</div> </div>

View File

@ -40,6 +40,7 @@ import { ProcessGroupReferences } from '../process-group-references/process-grou
import { ParameterContextInheritance } from '../parameter-context-inheritance/parameter-context-inheritance.component'; import { ParameterContextInheritance } from '../parameter-context-inheritance/parameter-context-inheritance.component';
import { ParameterReferences } from '../../../../../ui/common/parameter-references/parameter-references.component'; import { ParameterReferences } from '../../../../../ui/common/parameter-references/parameter-references.component';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
@Component({ @Component({
selector: 'edit-parameter-context', selector: 'edit-parameter-context',
@ -61,7 +62,8 @@ import { RouterLink } from '@angular/router';
ProcessGroupReferences, ProcessGroupReferences,
ParameterContextInheritance, ParameterContextInheritance,
ParameterReferences, ParameterReferences,
RouterLink RouterLink,
ErrorBanner
], ],
styleUrls: ['./edit-parameter-context.component.scss'] styleUrls: ['./edit-parameter-context.component.scss']
}) })

View File

@ -98,7 +98,7 @@ export class ParameterContextTable {
} }
canManageAccessPolicies(): boolean { canManageAccessPolicies(): boolean {
return this.flowConfiguration.supportsManagedAuthorizer && this.currentUser.tenantsPermissions.canRead; return this.flowConfiguration?.supportsManagedAuthorizer && this.currentUser.tenantsPermissions.canRead;
} }
canGoToParameterProvider(entity: ParameterContextEntity): boolean { canGoToParameterProvider(entity: ParameterContextEntity): boolean {

View File

@ -143,6 +143,6 @@
<div class="fa fa-check complete"></div> <div class="fa fa-check complete"></div>
</ng-template> </ng-template>
<ng-template #stepError> <ng-template #stepError>
<div class="fa fa-times text-red-400"></div> <div class="fa fa-times warn-default"></div>
</ng-template> </ng-template>
<ng-template #stepNotStarted><div class="w-3.5"></div></ng-template> <ng-template #stepNotStarted><div class="w-3.5"></div></ng-template>

View File

@ -164,6 +164,6 @@
<div class="fa fa-check complete"></div> <div class="fa fa-check complete"></div>
</ng-template> </ng-template>
<ng-template #stepError> <ng-template #stepError>
<div class="fa fa-times text-red-400"></div> <div class="fa fa-times warn-default"></div>
</ng-template> </ng-template>
<ng-template #stepNotStarted><div class="w-3.5"></div></ng-template> <ng-template #stepNotStarted><div class="w-3.5"></div></ng-template>

View File

@ -287,6 +287,7 @@
fill: $accent-palette-lighter !important; fill: $accent-palette-lighter !important;
} }
.warn-default,
.stale, .stale,
.locally-modified-and-stale { .locally-modified-and-stale {
color: $warn-palette-default !important; color: $warn-palette-default !important;

View File

@ -85,7 +85,10 @@
$color-palette: map.get($theme, $palette); $color-palette: map.get($theme, $palette);
$default: mat.get-color-from-palette($color-palette, default); $default: mat.get-color-from-palette($color-palette, default);
$high-contrast: mat.get-color-from-palette($color-palette, if(luminosity($default) > luminosity($surface), lighter, darker)); $high-contrast: mat.get-color-from-palette(
$color-palette,
if(luminosity($default) > luminosity($surface), lighter, darker)
);
$on-surface: ensure-contrast($default, $surface, $high-contrast); $on-surface: ensure-contrast($default, $surface, $high-contrast);
@return $on-surface; @return $on-surface;