NIFI-12742: Error Handling in Summary, Users, and Queue Listing (#8366)

- Error handling in Users.
- Error handling in Summary.
- Error handling in Queue Listing.
- Addressing review feedback.
- Dispatching delete success to ensure the active request is reset upon completion.

This closes #8366
This commit is contained in:
Matt Gilman 2024-02-09 09:37:07 -05:00 committed by GitHub
parent 4094c7f599
commit 439c59e733
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 244 additions and 131 deletions

View File

@ -101,9 +101,9 @@ export interface FlowFileDialogRequest {
}
export interface QueueListingState {
requestEntity: ListingRequestEntity | null;
activeListingRequest: ListingRequest | null;
completedListingRequest: ListingRequest | null;
connectionLabel: string;
loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
}

View File

@ -68,6 +68,8 @@ export const stopPollingQueueListingRequest = createAction(`${QUEUE_PREFIX} Stop
export const deleteQueueListingRequest = createAction(`${QUEUE_PREFIX} Delete Queue Listing Request`);
export const deleteQueueListingRequestSuccess = createAction(`${QUEUE_PREFIX} Delete Queue Listing Request Success`);
export const viewFlowFile = createAction(`${QUEUE_PREFIX} View FlowFile`, props<{ request: ViewFlowFileRequest }>());
export const openFlowFileDialog = createAction(

View File

@ -21,7 +21,7 @@ import * as QueueListingActions from './queue-listing.actions';
import { Store } from '@ngrx/store';
import { CanvasState } from '../../../flow-designer/state';
import { asyncScheduler, catchError, filter, from, interval, map, of, switchMap, take, takeUntil, tap } from 'rxjs';
import { selectConnectionIdFromRoute, selectListingRequestEntity } from './queue-listing.selectors';
import { selectConnectionIdFromRoute, selectActiveListingRequest } from './queue-listing.selectors';
import { QueueService } from '../../service/queue.service';
import { ListingRequest } from './index';
import { CancelDialog } from '../../../../ui/common/cancel-dialog/cancel-dialog.component';
@ -30,6 +30,10 @@ import { selectAbout } from '../../../../state/about/about.selectors';
import { FlowFileDialog } from '../../ui/queue-listing/flowfile-dialog/flowfile-dialog.component';
import { NiFiCommon } from '../../../../service/nifi-common.service';
import { isDefinedAndNotNull } from '../../../../state/shared';
import { HttpErrorResponse } from '@angular/common/http';
import * as ErrorActions from '../../../../state/error/error.actions';
import { ErrorHelper } from '../../../../service/error-helper.service';
import { stopPollingQueueListingRequest } from './queue-listing.actions';
@Injectable()
export class QueueListingEffects {
@ -37,6 +41,7 @@ export class QueueListingEffects {
private actions$: Actions,
private store: Store<CanvasState>,
private queueService: QueueService,
private errorHelper: ErrorHelper,
private dialog: MatDialog,
private nifiCommon: NiFiCommon
) {}
@ -103,13 +108,19 @@ export class QueueListingEffects {
}
})
),
catchError((error) =>
of(
QueueListingActions.queueListingApiError({
error: error.error
})
)
)
catchError((errorResponse: HttpErrorResponse) => {
if (this.errorHelper.showErrorInContext(errorResponse.status)) {
return of(
QueueListingActions.queueListingApiError({
error: errorResponse.error
})
);
} else {
this.store.dispatch(stopPollingQueueListingRequest());
return of(this.errorHelper.fullScreenError(errorResponse));
}
})
);
})
)
@ -155,9 +166,9 @@ export class QueueListingEffects {
pollQueueListingRequest$ = createEffect(() =>
this.actions$.pipe(
ofType(QueueListingActions.pollQueueListingRequest),
concatLatestFrom(() => this.store.select(selectListingRequestEntity).pipe(isDefinedAndNotNull())),
switchMap(([, requestEntity]) => {
return from(this.queueService.pollQueueListingRequest(requestEntity.listingRequest)).pipe(
concatLatestFrom(() => this.store.select(selectActiveListingRequest).pipe(isDefinedAndNotNull())),
switchMap(([, listingRequest]) => {
return from(this.queueService.pollQueueListingRequest(listingRequest)).pipe(
map((response) =>
QueueListingActions.pollQueueListingRequestSuccess({
response: {
@ -165,13 +176,19 @@ export class QueueListingEffects {
}
})
),
catchError((error) =>
of(
QueueListingActions.queueListingApiError({
error: error.error
})
)
)
catchError((errorResponse: HttpErrorResponse) => {
if (this.errorHelper.showErrorInContext(errorResponse.status)) {
return of(
QueueListingActions.queueListingApiError({
error: errorResponse.error
})
);
} else {
this.store.dispatch(stopPollingQueueListingRequest());
return of(this.errorHelper.fullScreenError(errorResponse));
}
})
);
})
)
@ -193,20 +210,23 @@ export class QueueListingEffects {
)
);
deleteQueueListingRequest$ = createEffect(
() =>
this.actions$.pipe(
ofType(QueueListingActions.deleteQueueListingRequest),
concatLatestFrom(() => this.store.select(selectListingRequestEntity)),
tap(([, requestEntity]) => {
this.dialog.closeAll();
deleteQueueListingRequest$ = createEffect(() =>
this.actions$.pipe(
ofType(QueueListingActions.deleteQueueListingRequest),
concatLatestFrom(() => this.store.select(selectActiveListingRequest)),
tap(([, listingRequest]) => {
this.dialog.closeAll();
if (requestEntity) {
this.queueService.deleteQueueListingRequest(requestEntity.listingRequest).subscribe();
}
})
),
{ dispatch: false }
if (listingRequest) {
this.queueService.deleteQueueListingRequest(listingRequest).subscribe({
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(ErrorActions.snackBarError({ error: errorResponse.error }));
}
});
}
}),
switchMap(() => of(QueueListingActions.deleteQueueListingRequestSuccess()))
)
);
viewFlowFile$ = createEffect(() =>
@ -222,10 +242,10 @@ export class QueueListingEffects {
}
})
),
catchError((error) =>
catchError((errorResponse: HttpErrorResponse) =>
of(
QueueListingActions.queueListingApiError({
error: error.error
ErrorActions.snackBarError({
error: errorResponse.error
})
)
)
@ -298,12 +318,13 @@ export class QueueListingEffects {
{ dispatch: false }
);
queueListingApiError$ = createEffect(
() =>
this.actions$.pipe(
ofType(QueueListingActions.queueListingApiError),
tap(() => this.dialog.closeAll())
),
{ dispatch: false }
queueListingApiError$ = createEffect(() =>
this.actions$.pipe(
ofType(QueueListingActions.queueListingApiError),
tap(() => {
this.store.dispatch(QueueListingActions.stopPollingQueueListingRequest());
}),
switchMap(({ error }) => of(ErrorActions.addBannerError({ error })))
)
);
}

View File

@ -23,14 +23,33 @@ import {
submitQueueListingRequestSuccess,
resetQueueListingState,
queueListingApiError,
loadConnectionLabelSuccess
loadConnectionLabelSuccess,
deleteQueueListingRequestSuccess
} from './queue-listing.actions';
import { produce } from 'immer';
export const initialState: QueueListingState = {
requestEntity: null,
activeListingRequest: null,
completedListingRequest: {
id: '',
uri: '',
submissionTime: '',
lastUpdated: '',
percentCompleted: 100,
finished: true,
failureReason: '',
maxResults: 0,
sourceRunning: false,
destinationRunning: false,
state: '',
queueSize: {
objectCount: 0,
byteCount: 0
},
flowFileSummaries: []
},
connectionLabel: 'Connection',
loadedTimestamp: 'N/A',
error: null,
status: 'pending'
};
@ -44,16 +63,25 @@ export const queueListingReducer = createReducer(
...state,
status: 'loading' as const
})),
on(submitQueueListingRequestSuccess, pollQueueListingRequestSuccess, (state, { response }) => ({
on(submitQueueListingRequestSuccess, pollQueueListingRequestSuccess, (state, { response }) => {
return produce(state, (draftState) => {
const listingRequest = response.requestEntity.listingRequest;
if (listingRequest.finished) {
draftState.completedListingRequest = listingRequest;
draftState.loadedTimestamp = listingRequest.lastUpdated;
draftState.status = 'success' as const;
} else {
draftState.activeListingRequest = listingRequest;
}
});
}),
on(deleteQueueListingRequestSuccess, (state) => ({
...state,
requestEntity: response.requestEntity,
loadedTimestamp: response.requestEntity.listingRequest.lastUpdated,
error: null,
status: 'success' as const
activeListingRequest: null
})),
on(queueListingApiError, (state, { error }) => ({
on(queueListingApiError, (state) => ({
...state,
error,
status: 'error' as const
})),
on(resetQueueListingState, () => ({

View File

@ -25,15 +25,18 @@ export const selectQueueListingState = createSelector(
(state: QueueState) => state[queueListingFeatureKey]
);
export const selectListingRequestEntity = createSelector(
export const selectActiveListingRequest = createSelector(
selectQueueListingState,
(state: QueueListingState) => state.requestEntity
(state: QueueListingState) => state.activeListingRequest
);
export const selectCompletedListingRequest = createSelector(
selectQueueListingState,
(state: QueueListingState) => state.completedListingRequest
);
export const selectStatus = createSelector(selectQueueListingState, (state: QueueListingState) => state.status);
export const selectError = createSelector(selectQueueListingState, (state: QueueListingState) => state.error);
export const selectConnectionLabel = createSelector(
selectQueueListingState,
(state: QueueListingState) => state.connectionLabel

View File

@ -17,6 +17,7 @@
<div class="flowfile-table h-full flex flex-col gap-y-2">
<h3 class="text-xl bold queue-listing-header">{{ connectionLabel }}</h3>
<error-banner></error-banner>
<div class="flex justify-between">
<div class="value">
Display {{ displayObjectCount }} of {{ formatCount(queueSizeObjectCount) }} ({{

View File

@ -20,14 +20,29 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FlowFileTable } from './flowfile-table.component';
import { MatTableModule } from '@angular/material/table';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Component } from '@angular/core';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/error/error.reducer';
describe('FlowFileTable', () => {
let component: FlowFileTable;
let fixture: ComponentFixture<FlowFileTable>;
@Component({
selector: 'error-banner',
standalone: true,
template: ''
})
class MockErrorBanner {}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FlowFileTable, MatTableModule, BrowserAnimationsModule]
imports: [FlowFileTable, MockErrorBanner, MatTableModule, BrowserAnimationsModule],
providers: [
provideMockStore({
initialState
})
]
});
fixture = TestBed.createComponent(FlowFileTable);
component = fixture.componentInstance;

View File

@ -25,12 +25,13 @@ import { NgForOf, NgIf } from '@angular/common';
import { RouterLink } from '@angular/router';
import { FlowFileSummary, ListingRequest } from '../../../state/queue-listing';
import { CurrentUser } from '../../../../../state/current-user';
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
@Component({
selector: 'flowfile-table',
standalone: true,
templateUrl: './flowfile-table.component.html',
imports: [MatTableModule, NgForOf, NgIf, RouterLink],
imports: [MatTableModule, NgForOf, NgIf, RouterLink, ErrorBanner],
styleUrls: ['./flowfile-table.component.scss']
})
export class FlowFileTable {

View File

@ -17,23 +17,20 @@
<div class="flex flex-col gap-y-2 h-full" *ngIf="status$ | async; let status">
<div class="flex-1">
<div class="value" *ngIf="status === 'error'; else noError">
{{ error$ | async }}
</div>
<ng-template #noError>
<ng-container *ngIf="listingRequestEntity$ | async as entity; else initialLoading">
<ng-container *ngIf="listingRequest$ | async as listingRequest; else initialLoading">
<ng-container *ngIf="about$ | async as about">
<flowfile-table
[connectionLabel]="(connectionLabel$ | async)!"
[listingRequest]="entity.listingRequest"
[listingRequest]="listingRequest"
[currentUser]="(currentUser$ | async)!"
[contentViewerAvailable]="contentViewerAvailable((about$ | async)!)"
[contentViewerAvailable]="contentViewerAvailable(about)"
(viewFlowFile)="viewFlowFile($event)"
(downloadContent)="downloadContent($event)"
(viewContent)="viewContent($event)"></flowfile-table>
</ng-container>
<ng-template #initialLoading>
<ngx-skeleton-loader count="3"></ngx-skeleton-loader>
</ng-template>
</ng-container>
<ng-template #initialLoading>
<ngx-skeleton-loader count="3"></ngx-skeleton-loader>
</ng-template>
</div>
<div class="flex justify-between">

View File

@ -21,8 +21,7 @@ import { distinctUntilChanged, filter } from 'rxjs';
import {
selectConnectionIdFromRoute,
selectConnectionLabel,
selectError,
selectListingRequestEntity,
selectCompletedListingRequest,
selectLoadedTimestamp,
selectStatus
} from '../../state/queue-listing/queue-listing.selectors';
@ -41,6 +40,7 @@ import { NiFiState } from '../../../../state';
import { selectAbout } from '../../../../state/about/about.selectors';
import { About } from '../../../../state/about';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { clearBannerErrors } from '../../../../state/error/error.actions';
@Component({
selector: 'queue-listing',
@ -49,10 +49,9 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
})
export class QueueListing implements OnDestroy {
status$ = this.store.select(selectStatus);
error$ = this.store.select(selectError);
connectionLabel$ = this.store.select(selectConnectionLabel);
loadedTimestamp$ = this.store.select(selectLoadedTimestamp);
listingRequestEntity$ = this.store.select(selectListingRequestEntity);
listingRequest$ = this.store.select(selectCompletedListingRequest);
currentUser$ = this.store.select(selectCurrentUser);
about$ = this.store.select(selectAbout);
@ -104,5 +103,6 @@ export class QueueListing implements OnDestroy {
ngOnDestroy(): void {
this.store.dispatch(resetQueueListingState());
this.store.dispatch(clearBannerErrors());
}
}

View File

@ -26,6 +26,7 @@ import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { queueFeatureKey, reducers } from '../../state';
import { QueueListingEffects } from '../../state/queue-listing/queue-listing.effects';
import { ErrorBanner } from '../../../../ui/common/error-banner/error-banner.component';
@NgModule({
declarations: [QueueListing],
@ -37,7 +38,8 @@ import { QueueListingEffects } from '../../state/queue-listing/queue-listing.eff
NifiTooltipDirective,
FlowFileTable,
StoreModule.forFeature(queueFeatureKey, reducers),
EffectsModule.forFeature(QueueListingEffects)
EffectsModule.forFeature(QueueListingEffects),
ErrorBanner
]
})
export class QueueListingModule {}

View File

@ -17,17 +17,13 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Client } from '../../../service/client.service';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ClusterSummaryService {
private static readonly API: string = '../nifi-api';
constructor(
private httpClient: HttpClient,
private client: Client
) {}
constructor(private httpClient: HttpClient) {}
getClusterSummary(): Observable<any> {
return this.httpClient.get(`${ClusterSummaryService.API}/flow/cluster/summary`);

View File

@ -17,17 +17,13 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Client } from '../../../service/client.service';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ProcessGroupStatusService {
private static readonly API: string = '../nifi-api';
constructor(
private httpClient: HttpClient,
private client: Client
) {}
constructor(private httpClient: HttpClient) {}
getProcessGroupsStatus(recursive?: boolean): Observable<any> {
if (recursive) {

View File

@ -199,6 +199,5 @@ export interface SummaryListingState {
connectionStatusSnapshots: ConnectionStatusSnapshotEntity[];
remoteProcessGroupStatusSnapshots: RemoteProcessGroupStatusSnapshotEntity[];
loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
status: 'pending' | 'loading' | 'success';
}

View File

@ -37,11 +37,6 @@ export const loadSummaryListingSuccess = createAction(
props<{ response: SummaryListingResponse }>()
);
export const summaryListingApiError = createAction(
`${SUMMARY_LISTING_PREFIX} Load Summary Listing error`,
props<{ error: string }>()
);
export const selectProcessorStatus = createAction(
`${SUMMARY_LISTING_PREFIX} Select Processor Status`,
props<{ request: SelectProcessorStatusRequest }>()

View File

@ -16,7 +16,7 @@
*/
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
import { ClusterSummaryService } from '../../service/cluster-summary.service';
@ -27,6 +27,9 @@ import * as StatusHistoryActions from '../../../../state/status-history/status-h
import { catchError, combineLatest, filter, map, of, switchMap, tap } from 'rxjs';
import { Router } from '@angular/router';
import { ComponentType } from '../../../../state/shared';
import { ErrorHelper } from '../../../../service/error-helper.service';
import { HttpErrorResponse } from '@angular/common/http';
import { selectSummaryListingStatus } from './summary-listing.selectors';
@Injectable()
export class SummaryListingEffects {
@ -35,6 +38,7 @@ export class SummaryListingEffects {
private store: Store<NiFiState>,
private clusterSummaryService: ClusterSummaryService,
private pgStatusService: ProcessGroupStatusService,
private errorHelper: ErrorHelper,
private router: Router
) {}
@ -42,7 +46,8 @@ export class SummaryListingEffects {
this.actions$.pipe(
ofType(SummaryListingActions.loadSummaryListing),
map((action) => action.recursive),
switchMap((recursive) =>
concatLatestFrom(() => this.store.select(selectSummaryListingStatus)),
switchMap(([recursive, listingStatus]) =>
combineLatest([
this.clusterSummaryService.getClusterSummary(),
this.pgStatusService.getProcessGroupsStatus(recursive)
@ -55,7 +60,9 @@ export class SummaryListingEffects {
}
})
),
catchError((error) => of(SummaryListingActions.summaryListingApiError({ error: error.error })))
catchError((errorResponse: HttpErrorResponse) =>
of(this.errorHelper.handleLoadingError(listingStatus, errorResponse))
)
)
)
)

View File

@ -25,12 +25,7 @@ import {
RemoteProcessGroupStatusSnapshotEntity,
SummaryListingState
} from './index';
import {
loadSummaryListing,
loadSummaryListingSuccess,
resetSummaryState,
summaryListingApiError
} from './summary-listing.actions';
import { loadSummaryListing, loadSummaryListingSuccess, resetSummaryState } from './summary-listing.actions';
export const initialState: SummaryListingState = {
clusterSummary: null,
@ -42,7 +37,6 @@ export const initialState: SummaryListingState = {
connectionStatusSnapshots: [],
remoteProcessGroupStatusSnapshots: [],
status: 'pending',
error: null,
loadedTimestamp: ''
};
@ -85,7 +79,6 @@ export const summaryListingReducer = createReducer(
return {
...state,
error: null,
status: 'success' as const,
loadedTimestamp: response.status.processGroupStatus.statsLastRefreshed,
processGroupStatus: response.status,
@ -99,12 +92,6 @@ export const summaryListingReducer = createReducer(
};
}),
on(summaryListingApiError, (state, { error }) => ({
...state,
error,
status: 'error' as const
})),
on(resetSummaryState, () => ({
...initialState
}))

View File

@ -116,6 +116,5 @@ export interface UserListingState {
userGroups: UserGroupEntity[];
saving: boolean;
loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
status: 'pending' | 'loading' | 'success';
}

View File

@ -44,7 +44,12 @@ export const loadTenantsSuccess = createAction(
props<{ response: LoadTenantsSuccess }>()
);
export const usersApiError = createAction(`${USER_PREFIX} Users Api Error`, props<{ error: string }>());
export const usersApiSnackbarError = createAction(
`${USER_PREFIX} Users Api Snackbar Error`,
props<{ error: string }>()
);
export const usersApiBannerError = createAction(`${USER_PREFIX} Users Api Banner Error`, props<{ error: string }>());
export const openCreateTenantDialog = createAction(`${USER_PREFIX} Open Create Tenant Dialog`);

View File

@ -26,12 +26,15 @@ import { MatDialog } from '@angular/material/dialog';
import { UsersService } from '../../service/users.service';
import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component';
import { EditTenantDialog } from '../../../../ui/common/edit-tenant/edit-tenant-dialog.component';
import { selectSaving, selectUserGroups, selectUsers } from './user-listing.selectors';
import { selectSaving, selectStatus, selectUserGroups, selectUsers } from './user-listing.selectors';
import { EditTenantRequest, UserGroupEntity } from '../../../../state/shared';
import { selectTenant } from './user-listing.actions';
import { Client } from '../../../../service/client.service';
import { NiFiCommon } from '../../../../service/nifi-common.service';
import { UserAccessPolicies } from '../../ui/user-listing/user-access-policies/user-access-policies.component';
import * as ErrorActions from '../../../../state/error/error.actions';
import { ErrorHelper } from '../../../../service/error-helper.service';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable()
export class UserListingEffects {
@ -44,13 +47,15 @@ export class UserListingEffects {
private store: Store<NiFiState>,
private router: Router,
private usersService: UsersService,
private errorHelper: ErrorHelper,
private dialog: MatDialog
) {}
loadTenants$ = createEffect(() =>
this.actions$.pipe(
ofType(UserListingActions.loadTenants),
switchMap(() =>
concatLatestFrom(() => this.store.select(selectStatus)),
switchMap(([, status]) =>
combineLatest([this.usersService.getUsers(), this.usersService.getUserGroups()]).pipe(
map(([usersResponse, userGroupsResponse]) =>
UserListingActions.loadTenantsSuccess({
@ -61,12 +66,8 @@ export class UserListingEffects {
}
})
),
catchError((error) =>
of(
UserListingActions.usersApiError({
error: error.error
})
)
catchError((errorResponse: HttpErrorResponse) =>
of(this.errorHelper.handleLoadingError(status, errorResponse))
)
)
)
@ -155,7 +156,10 @@ export class UserListingEffects {
}
})
),
catchError((error) => of(UserListingActions.usersApiError({ error: error.error })))
catchError((errorResponse: HttpErrorResponse) => {
this.dialog.closeAll();
return of(UserListingActions.usersApiSnackbarError({ error: errorResponse.error }));
})
)
)
)
@ -219,6 +223,22 @@ export class UserListingEffects {
)
);
usersApiBannerError$ = createEffect(() =>
this.actions$.pipe(
ofType(UserListingActions.usersApiBannerError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
)
);
usersApiSnackbarError$ = createEffect(() =>
this.actions$.pipe(
ofType(UserListingActions.usersApiSnackbarError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.snackBarError({ error })))
)
);
awaitUpdateUserGroupsForCreateUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UserListingActions.createUserSuccess),
@ -263,7 +283,10 @@ export class UserListingEffects {
}
})
),
catchError((error) => of(UserListingActions.usersApiError({ error: error.error })))
catchError((errorResponse: HttpErrorResponse) => {
this.dialog.closeAll();
return of(UserListingActions.usersApiSnackbarError({ error: errorResponse.error }));
})
)
)
)
@ -360,6 +383,8 @@ export class UserListingEffects {
});
dialogReference.afterClosed().subscribe(() => {
this.store.dispatch(ErrorActions.clearBannerErrors());
this.store.dispatch(
selectTenant({
id: request.user.id
@ -385,7 +410,9 @@ export class UserListingEffects {
}
})
),
catchError((error) => of(UserListingActions.usersApiError({ error: error.error })))
catchError((errorResponse: HttpErrorResponse) =>
of(UserListingActions.usersApiBannerError({ error: errorResponse.error }))
)
)
)
)
@ -560,6 +587,8 @@ export class UserListingEffects {
});
dialogReference.afterClosed().subscribe(() => {
this.store.dispatch(ErrorActions.clearBannerErrors());
this.store.dispatch(
selectTenant({
id: request.userGroup.id
@ -585,7 +614,9 @@ export class UserListingEffects {
}
})
),
catchError((error) => of(UserListingActions.usersApiError({ error: error.error })))
catchError((errorResponse: HttpErrorResponse) =>
of(UserListingActions.usersApiBannerError({ error: errorResponse.error }))
)
)
)
)
@ -674,7 +705,9 @@ export class UserListingEffects {
switchMap((request) =>
from(this.usersService.deleteUser(request.user)).pipe(
map(() => UserListingActions.loadTenants()),
catchError((error) => of(UserListingActions.usersApiError({ error: error.error })))
catchError((errorResponse: HttpErrorResponse) =>
of(UserListingActions.usersApiSnackbarError({ error: errorResponse.error }))
)
)
)
)
@ -713,7 +746,9 @@ export class UserListingEffects {
switchMap((request) =>
from(this.usersService.deleteUserGroup(request.userGroup)).pipe(
map(() => UserListingActions.loadTenants()),
catchError((error) => of(UserListingActions.usersApiError({ error: error.error })))
catchError((errorResponse: HttpErrorResponse) =>
of(UserListingActions.usersApiSnackbarError({ error: errorResponse.error }))
)
)
)
)

View File

@ -28,7 +28,9 @@ import {
updateUser,
updateUserComplete,
updateUserGroup,
updateUserGroupSuccess
updateUserGroupSuccess,
usersApiBannerError,
usersApiSnackbarError
} from './user-listing.actions';
export const initialState: UserListingState = {
@ -36,7 +38,6 @@ export const initialState: UserListingState = {
userGroups: [],
saving: false,
loadedTimestamp: '',
error: null,
status: 'pending'
};
@ -54,7 +55,6 @@ export const userListingReducer = createReducer(
users: response.users,
userGroups: response.userGroups,
loadedTimestamp: response.loadedTimestamp,
error: null,
status: 'success' as const
})),
on(createUser, updateUser, createUserGroup, updateUserGroup, (state) => ({
@ -76,5 +76,9 @@ export const userListingReducer = createReducer(
on(updateUserGroupSuccess, (state, { response }) => ({
...state,
saving: response.requestId == null ? false : state.saving
})),
on(usersApiSnackbarError, usersApiBannerError, (state) => ({
...state,
saving: false
}))
);

View File

@ -24,6 +24,8 @@ export const selectUserListingState = createSelector(selectUserState, (state: Us
export const selectSaving = createSelector(selectUserListingState, (state: UserListingState) => state.saving);
export const selectStatus = createSelector(selectUserListingState, (state: UserListingState) => state.status);
export const selectUsers = createSelector(selectUserListingState, (state: UserListingState) => state.users);
export const selectUserGroups = createSelector(selectUserListingState, (state: UserListingState) => state.userGroups);

View File

@ -17,6 +17,7 @@
<h2 mat-dialog-title>{{ isNew ? 'Add' : 'Edit' }} {{ isUser ? 'User' : 'User Group' }}</h2>
<form class="edit-tenant-form" [formGroup]="editTenantForm">
<error-banner></error-banner>
<mat-dialog-content>
<div class="mb-6">
<mat-radio-group formControlName="tenantType" (change)="tenantTypeChanged()">

View File

@ -21,6 +21,9 @@ import { EditTenantDialog } from './edit-tenant-dialog.component';
import { EditTenantRequest } from '../../../state/shared';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Component } from '@angular/core';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../state/error/error.reducer';
describe('EditTenantDialog', () => {
let component: EditTenantDialog;
@ -782,10 +785,22 @@ describe('EditTenantDialog', () => {
]
};
@Component({
selector: 'error-banner',
standalone: true,
template: ''
})
class MockErrorBanner {}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditTenantDialog, BrowserAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
imports: [EditTenantDialog, MockErrorBanner, BrowserAnimationsModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
provideMockStore({
initialState
})
]
});
fixture = TestBed.createComponent(EditTenantDialog);
component = fixture.componentInstance;

View File

@ -40,6 +40,7 @@ import { Observable } from 'rxjs';
import { MatListModule } from '@angular/material/list';
import { Client } from '../../../service/client.service';
import { NiFiCommon } from '../../../service/nifi-common.service';
import { ErrorBanner } from '../error-banner/error-banner.component';
@Component({
selector: 'edit-tenant-dialog',
@ -57,7 +58,8 @@ import { NiFiCommon } from '../../../service/nifi-common.service';
NgIf,
AsyncPipe,
MatListModule,
NgForOf
NgForOf,
ErrorBanner
],
templateUrl: './edit-tenant-dialog.component.html',
styleUrls: ['./edit-tenant-dialog.component.scss']