mirror of
https://github.com/apache/nifi.git
synced 2025-03-03 16:09:19 +00:00
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:
parent
4094c7f599
commit
439c59e733
@ -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';
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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 })))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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, () => ({
|
||||
|
@ -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
|
||||
|
@ -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) }} ({{
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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">
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -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`);
|
||||
|
@ -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) {
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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 }>()
|
||||
|
@ -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))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -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
|
||||
}))
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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`);
|
||||
|
||||
|
@ -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 }))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -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
|
||||
}))
|
||||
);
|
||||
|
@ -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);
|
||||
|
@ -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()">
|
||||
|
@ -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;
|
||||
|
@ -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']
|
||||
|
Loading…
x
Reference in New Issue
Block a user