NIFI-12767: Error handling in Provenance and Lineage (#8386)

* NIFI-12767:
- Error handling in Provenance and Lineage.

* NIFI-12767:
- Addressing review feedback.

* NIFI-12767:
- Restoring empty initial state for completed request that allows showing a banner error over an empty table instead of an empty page.

* NIFI-12767:
- Restoring error state to ensure that the loading spinner stops on error.

This closes #8386
This commit is contained in:
Matt Gilman 2024-02-14 11:11:26 -05:00 committed by GitHub
parent 4f030fefca
commit da8f86b7e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 465 additions and 321 deletions

View File

@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AddTenantToPolicyDialog } from './add-tenant-to-policy-dialog.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { AddTenantToPolicyDialogRequest } from '../../../state/access-policy';
describe('AddTenantToPolicyDialog', () => {
@ -68,7 +68,7 @@ describe('AddTenantToPolicyDialog', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AddTenantToPolicyDialog, BrowserAnimationsModule],
imports: [AddTenantToPolicyDialog, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
});
fixture = TestBed.createComponent(AddTenantToPolicyDialog);

View File

@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OverridePolicyDialog } from './override-policy-dialog.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('OverridePolicyDialog', () => {
let component: OverridePolicyDialog;
@ -26,7 +26,7 @@ describe('OverridePolicyDialog', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [OverridePolicyDialog, BrowserAnimationsModule]
imports: [OverridePolicyDialog, NoopAnimationsModule]
});
fixture = TestBed.createComponent(OverridePolicyDialog);
component = fixture.componentInstance;

View File

@ -23,7 +23,7 @@ import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/flow/flow.reducer';
import { CreateConnectionDialogRequest } from '../../../../../state/flow';
import { ComponentType, DocumentedType } from '../../../../../../../state/shared';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of } from 'rxjs';
describe('CreateConnection', () => {
@ -366,7 +366,7 @@ describe('CreateConnection', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CreateConnection, BrowserAnimationsModule],
imports: [CreateConnection, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(CreateConnection);

View File

@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DestinationProcessGroup } from './destination-process-group.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('DestinationProcessGroup', () => {
let component: DestinationProcessGroup;
@ -26,7 +26,7 @@ describe('DestinationProcessGroup', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule, DestinationProcessGroup]
imports: [NoopAnimationsModule, DestinationProcessGroup]
});
fixture = TestBed.createComponent(DestinationProcessGroup);
component = fixture.componentInstance;

View File

@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DestinationRemoteProcessGroup } from './destination-remote-process-group.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('DestinationRemoteProcessGroup', () => {
let component: DestinationRemoteProcessGroup;
@ -26,7 +26,7 @@ describe('DestinationRemoteProcessGroup', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule, DestinationRemoteProcessGroup]
imports: [NoopAnimationsModule, DestinationRemoteProcessGroup]
});
fixture = TestBed.createComponent(DestinationRemoteProcessGroup);
component = fixture.componentInstance;

View File

@ -24,7 +24,7 @@ import { ComponentType } from '../../../../../../../state/shared';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/flow/flow.reducer';
import { selectPrioritizerTypes } from '../../../../../../../state/extension-types/extension-types.selectors';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('EditConnectionComponent', () => {
let store: MockStore;
@ -129,7 +129,7 @@ describe('EditConnectionComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditConnectionComponent, BrowserAnimationsModule],
imports: [EditConnectionComponent, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
});

View File

@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SourceProcessGroup } from './source-process-group.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('SourceProcessGroup', () => {
let component: SourceProcessGroup;
@ -26,7 +26,7 @@ describe('SourceProcessGroup', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule, SourceProcessGroup]
imports: [NoopAnimationsModule, SourceProcessGroup]
});
fixture = TestBed.createComponent(SourceProcessGroup);
component = fixture.componentInstance;

View File

@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SourceRemoteProcessGroup } from './source-remote-process-group.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('SourceRemoteProcessGroup', () => {
let component: SourceRemoteProcessGroup;
@ -26,7 +26,7 @@ describe('SourceRemoteProcessGroup', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule, SourceRemoteProcessGroup]
imports: [NoopAnimationsModule, SourceRemoteProcessGroup]
});
fixture = TestBed.createComponent(SourceRemoteProcessGroup);
component = fixture.componentInstance;

View File

@ -23,7 +23,7 @@ import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ComponentType } from '../../../../../../../state/shared';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/flow/flow.reducer';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { EMPTY } from 'rxjs';
describe('ImportFromRegistry', () => {
@ -111,7 +111,7 @@ describe('ImportFromRegistry', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ImportFromRegistry, BrowserAnimationsModule],
imports: [ImportFromRegistry, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(ImportFromRegistry);

View File

@ -23,7 +23,7 @@ import { ComponentType } from '../../../../../../../state/shared';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/flow/flow.reducer';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('CreatePort', () => {
let component: CreatePort;
@ -43,7 +43,7 @@ describe('CreatePort', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CreatePort, BrowserAnimationsModule],
imports: [CreatePort, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(CreatePort);

View File

@ -23,7 +23,7 @@ import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ComponentType } from '../../../../../../../state/shared';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/flow/flow.reducer';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('EditPort', () => {
let component: EditPort;
@ -95,7 +95,7 @@ describe('EditPort', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditPort, BrowserAnimationsModule],
imports: [EditPort, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(EditPort);

View File

@ -23,7 +23,7 @@ import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ComponentType } from '../../../../../../../state/shared';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/flow/flow.reducer';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('CreateProcessGroup', () => {
let component: CreateProcessGroup;
@ -155,7 +155,7 @@ describe('CreateProcessGroup', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CreateProcessGroup, BrowserAnimationsModule],
imports: [CreateProcessGroup, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(CreateProcessGroup);

View File

@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EditProcessGroup } from './edit-process-group.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('EditProcessGroup', () => {
let component: EditProcessGroup;
@ -106,7 +106,7 @@ describe('EditProcessGroup', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditProcessGroup, BrowserAnimationsModule],
imports: [EditProcessGroup, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
});
fixture = TestBed.createComponent(EditProcessGroup);

View File

@ -23,7 +23,7 @@ import { ComponentType } from '../../../../../../../state/shared';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/flow/flow.reducer';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('GroupComponents', () => {
let component: GroupComponents;
@ -860,7 +860,7 @@ describe('GroupComponents', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [GroupComponents, BrowserAnimationsModule],
imports: [GroupComponents, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(GroupComponents);

View File

@ -23,7 +23,7 @@ import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../../../state/extension-types/extension-types.reducer';
import { ComponentType } from '../../../../../../../state/shared';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('CreateProcessor', () => {
let component: CreateProcessor;
@ -59,7 +59,7 @@ describe('CreateProcessor', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CreateProcessor, BrowserAnimationsModule],
imports: [CreateProcessor, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(CreateProcessor);

View File

@ -21,7 +21,7 @@ import { EditProcessor } from './edit-processor.component';
import { EditComponentDialogRequest } from '../../../../../state/flow';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ComponentType } from '../../../../../../../state/shared';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Component } from '@angular/core';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../../../state/error/error.reducer';
@ -731,7 +731,7 @@ describe('EditProcessor', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditProcessor, MockErrorBanner, BrowserAnimationsModule],
imports: [EditProcessor, MockErrorBanner, NoopAnimationsModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
provideMockStore({

View File

@ -26,7 +26,7 @@ import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('LoginForm', () => {
let component: LoginForm;
@ -37,7 +37,7 @@ describe('LoginForm', () => {
declarations: [LoginForm],
imports: [
HttpClientTestingModule,
BrowserAnimationsModule,
NoopAnimationsModule,
MatFormFieldModule,
RouterModule,
RouterTestingModule,

View File

@ -21,7 +21,7 @@ import { EditParameterContext } from './edit-parameter-context.component';
import { EditParameterContextRequest, ParameterContextEntity } from '../../../state/parameter-context-listing';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { of } from 'rxjs';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../state/parameter-context-listing/parameter-context-listing.reducer';
@ -234,7 +234,7 @@ describe('EditParameterContext', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditParameterContext, BrowserAnimationsModule],
imports: [EditParameterContext, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(EditParameterContext);

View File

@ -18,7 +18,6 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { NiFiCommon } from '../../../service/nifi-common.service';
import { ProvenanceRequest } from '../state/provenance-event-listing';
import { LineageRequest } from '../state/lineage';
@ -26,10 +25,7 @@ import { LineageRequest } from '../state/lineage';
export class ProvenanceService {
private static readonly API: string = '../nifi-api';
constructor(
private httpClient: HttpClient,
private nifiCommon: NiFiCommon
) {}
constructor(private httpClient: HttpClient) {}
getSearchOptions(): Observable<any> {
return this.httpClient.get(`${ProvenanceService.API}/provenance/search-options`);

View File

@ -66,7 +66,7 @@ export interface Lineage {
}
export interface LineageState {
lineage: Lineage | null;
error: string | null;
activeLineage: Lineage | null;
completedLineage: Lineage;
status: 'pending' | 'loading' | 'error' | 'success';
}

View File

@ -40,6 +40,8 @@ export const stopPollingLineageQuery = createAction('[Lineage] Stop Polling Line
export const deleteLineageQuery = createAction('[Lineage] Delete Lineage Query');
export const deleteLineageQuerySuccess = createAction('[Lineage] Delete Lineage Query Success');
export const lineageApiError = createAction(
'[Lineage] Load Parameter Context Listing Error',
props<{ error: string }>()

View File

@ -18,15 +18,18 @@
import { Injectable } from '@angular/core';
import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
import * as LineageActions from './lineage.actions';
import * as ProvenanceActions from '../provenance-event-listing/provenance-event-listing.actions';
import { asyncScheduler, catchError, from, interval, map, NEVER, of, switchMap, takeUntil, tap } from 'rxjs';
import { asyncScheduler, catchError, filter, from, interval, map, of, switchMap, takeUntil, tap } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
import { ProvenanceService } from '../../service/provenance.service';
import { Lineage } from './index';
import { selectClusterNodeId } from '../provenance-event-listing/provenance-event-listing.selectors';
import { selectLineageId } from './lineage.selectors';
import { selectActiveLineageId } from './lineage.selectors';
import * as ErrorActions from '../../../../state/error/error.actions';
import { ErrorHelper } from '../../../../service/error-helper.service';
import { HttpErrorResponse } from '@angular/common/http';
import { isDefinedAndNotNull } from '../../../../state/shared';
@Injectable()
export class LineageEffects {
@ -34,6 +37,7 @@ export class LineageEffects {
private actions$: Actions,
private store: Store<NiFiState>,
private provenanceService: ProvenanceService,
private errorHelper: ErrorHelper,
private dialog: MatDialog
) {}
@ -50,19 +54,18 @@ export class LineageEffects {
}
})
),
catchError((error) => {
this.store.dispatch(
ProvenanceActions.showOkDialog({
title: 'Error',
message: error.error
})
);
catchError((errorResponse: HttpErrorResponse) => {
if (this.errorHelper.showErrorInContext(errorResponse.status)) {
return of(
LineageActions.lineageApiError({
error: errorResponse.error
})
);
} else {
this.store.dispatch(LineageActions.stopPollingLineageQuery());
return of(
LineageActions.lineageApiError({
error: error.error
})
);
return of(this.errorHelper.fullScreenError(errorResponse));
}
})
)
)
@ -100,29 +103,34 @@ export class LineageEffects {
pollLineageQuery$ = createEffect(() =>
this.actions$.pipe(
ofType(LineageActions.pollLineageQuery),
concatLatestFrom(() => [this.store.select(selectLineageId), this.store.select(selectClusterNodeId)]),
switchMap(([, id, clusterNodeId]) => {
if (id) {
return from(this.provenanceService.getLineageQuery(id, clusterNodeId)).pipe(
map((response) =>
LineageActions.pollLineageQuerySuccess({
response: {
lineage: response.lineage
}
})
),
catchError((error) =>
of(
concatLatestFrom(() => [
this.store.select(selectActiveLineageId).pipe(isDefinedAndNotNull()),
this.store.select(selectClusterNodeId)
]),
switchMap(([, id, clusterNodeId]) =>
from(this.provenanceService.getLineageQuery(id, clusterNodeId)).pipe(
map((response) =>
LineageActions.pollLineageQuerySuccess({
response: {
lineage: response.lineage
}
})
),
catchError((errorResponse: HttpErrorResponse) => {
if (this.errorHelper.showErrorInContext(errorResponse.status)) {
return of(
LineageActions.lineageApiError({
error: error.error
error: errorResponse.error
})
)
)
);
} else {
return NEVER;
}
})
);
} else {
this.store.dispatch(LineageActions.stopPollingLineageQuery());
return of(this.errorHelper.fullScreenError(errorResponse));
}
})
)
)
)
);
@ -130,15 +138,8 @@ export class LineageEffects {
this.actions$.pipe(
ofType(LineageActions.pollLineageQuerySuccess),
map((action) => action.response),
switchMap((response) => {
const query: Lineage = response.lineage;
if (query.finished) {
this.dialog.closeAll();
return of(LineageActions.stopPollingLineageQuery());
} else {
return NEVER;
}
})
filter((response) => response.lineage.finished),
switchMap(() => of(LineageActions.stopPollingLineageQuery()))
)
);
@ -149,17 +150,26 @@ export class LineageEffects {
)
);
deleteLineageQuery$ = createEffect(
() =>
this.actions$.pipe(
ofType(LineageActions.deleteLineageQuery),
concatLatestFrom(() => [this.store.select(selectLineageId), this.store.select(selectClusterNodeId)]),
tap(([, id, clusterNodeId]) => {
if (id) {
this.provenanceService.deleteLineageQuery(id, clusterNodeId).subscribe();
}
})
),
{ dispatch: false }
deleteLineageQuery$ = createEffect(() =>
this.actions$.pipe(
ofType(LineageActions.deleteLineageQuery),
concatLatestFrom(() => [this.store.select(selectActiveLineageId), this.store.select(selectClusterNodeId)]),
tap(([, id, clusterNodeId]) => {
if (id) {
this.provenanceService.deleteLineageQuery(id, clusterNodeId).subscribe();
}
}),
switchMap(() => of(LineageActions.deleteLineageQuerySuccess()))
)
);
lineageApiError$ = createEffect(() =>
this.actions$.pipe(
ofType(LineageActions.lineageApiError),
tap(() => {
this.store.dispatch(LineageActions.stopPollingLineageQuery());
}),
switchMap(({ error }) => of(ErrorActions.addBannerError({ error })))
)
);
}

View File

@ -18,16 +18,32 @@
import { createReducer, on } from '@ngrx/store';
import { LineageState } from './index';
import {
deleteLineageQuerySuccess,
lineageApiError,
pollLineageQuerySuccess,
resetLineage,
submitLineageQuery,
submitLineageQuerySuccess
} from './lineage.actions';
import { produce } from 'immer';
export const initialState: LineageState = {
lineage: null,
error: null,
activeLineage: null,
completedLineage: {
id: '',
uri: '',
submissionTime: '',
expiration: '',
percentCompleted: 0,
finished: false,
request: {
lineageRequestType: 'FLOWFILE'
},
results: {
nodes: [],
links: []
}
},
status: 'pending'
};
@ -40,15 +56,24 @@ export const lineageReducer = createReducer(
...state,
status: 'loading' as const
})),
on(submitLineageQuerySuccess, pollLineageQuerySuccess, (state, { response }) => ({
on(submitLineageQuerySuccess, pollLineageQuerySuccess, (state, { response }) => {
return produce(state, (draftState) => {
const lineage = response.lineage;
draftState.activeLineage = lineage;
// if the query has finished save it as completed, the active query will be reset after deletion
if (lineage.finished) {
draftState.completedLineage = lineage;
draftState.status = 'success' as const;
}
});
}),
on(deleteLineageQuerySuccess, (state) => ({
...state,
lineage: response.lineage,
error: null,
status: 'success' as const
activeLineage: null
})),
on(lineageApiError, (state, { error }) => ({
on(lineageApiError, (state) => ({
...state,
error,
status: 'error' as const
}))
);

View File

@ -24,8 +24,11 @@ export const selectLineageState = createSelector(
(state: ProvenanceState) => state[lineageFeatureKey]
);
export const selectStatus = createSelector(selectLineageState, (state: LineageState) => state.status);
export const selectActiveLineage = createSelector(selectLineageState, (state: LineageState) => state.activeLineage);
export const selectLineage = createSelector(selectLineageState, (state: LineageState) => state.lineage);
export const selectCompletedLineage = createSelector(
selectLineageState,
(state: LineageState) => state.completedLineage
);
export const selectLineageId = createSelector(selectLineage, (state: Lineage | null) => state?.id);
export const selectActiveLineageId = createSelector(selectActiveLineage, (state: Lineage | null) => state?.id);

View File

@ -103,8 +103,8 @@ export interface Provenance {
export interface ProvenanceEventListingState {
options: ProvenanceOptions | null;
request: ProvenanceRequest | null;
provenance: Provenance | null;
activeProvenance: Provenance | null;
completedProvenance: Provenance;
loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
}

View File

@ -61,6 +61,8 @@ export const stopPollingProvenanceQuery = createAction('[Provenance Event Listin
export const deleteProvenanceQuery = createAction('[Provenance Event Listing] Delete Provenance Query');
export const deleteProvenanceQuerySuccess = createAction('[Provenance Event Listing] Delete Provenance Query Success');
export const provenanceApiError = createAction(
'[Provenance Event Listing] Provenance Api Error',
props<{ error: string }>()

View File

@ -18,7 +18,7 @@
import { Injectable } from '@angular/core';
import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
import * as ProvenanceEventListingActions from './provenance-event-listing.actions';
import { asyncScheduler, catchError, from, interval, map, NEVER, of, switchMap, take, takeUntil, tap } from 'rxjs';
import { asyncScheduler, catchError, filter, from, interval, map, of, switchMap, take, takeUntil, tap } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
@ -27,7 +27,7 @@ import { OkDialog } from '../../../../ui/common/ok-dialog/ok-dialog.component';
import { ProvenanceService } from '../../service/provenance.service';
import {
selectClusterNodeId,
selectProvenanceId,
selectActiveProvenanceId,
selectProvenanceOptions,
selectProvenanceRequest,
selectTimeOffset
@ -37,6 +37,10 @@ import { ProvenanceSearchDialog } from '../../ui/provenance-event-listing/proven
import { selectAbout } from '../../../../state/about/about.selectors';
import { ProvenanceEventDialog } from '../../../../ui/common/provenance-event-dialog/provenance-event-dialog.component';
import { CancelDialog } from '../../../../ui/common/cancel-dialog/cancel-dialog.component';
import * as ErrorActions from '../../../../state/error/error.actions';
import { ErrorHelper } from '../../../../service/error-helper.service';
import { HttpErrorResponse } from '@angular/common/http';
import { isDefinedAndNotNull } from '../../../../state/shared';
@Injectable()
export class ProvenanceEventListingEffects {
@ -44,6 +48,7 @@ export class ProvenanceEventListingEffects {
private actions$: Actions,
private store: Store<NiFiState>,
private provenanceService: ProvenanceService,
private errorHelper: ErrorHelper,
private dialog: MatDialog,
private router: Router
) {}
@ -58,10 +63,14 @@ export class ProvenanceEventListingEffects {
response
})
),
catchError((error) =>
catchError(() =>
of(
ProvenanceEventListingActions.provenanceApiError({
error: error.error
ProvenanceEventListingActions.loadProvenanceOptionsSuccess({
response: {
provenanceOptions: {
searchableFields: []
}
}
})
)
)
@ -74,38 +83,6 @@ export class ProvenanceEventListingEffects {
this.actions$.pipe(
ofType(ProvenanceEventListingActions.submitProvenanceQuery),
map((action) => action.request),
switchMap((request) =>
from(this.provenanceService.submitProvenanceQuery(request)).pipe(
map((response) =>
ProvenanceEventListingActions.submitProvenanceQuerySuccess({
response: {
provenance: response.provenance
}
})
),
catchError((error) => {
this.store.dispatch(
ProvenanceEventListingActions.showOkDialog({
title: 'Error',
message: error.error
})
);
return of(
ProvenanceEventListingActions.provenanceApiError({
error: error.error
})
);
})
)
)
)
);
resubmitProvenanceQuery = createEffect(() =>
this.actions$.pipe(
ofType(ProvenanceEventListingActions.resubmitProvenanceQuery),
map((action) => action.request),
switchMap((request) => {
const dialogReference = this.dialog.open(CancelDialog, {
data: {
@ -120,6 +97,37 @@ export class ProvenanceEventListingEffects {
this.store.dispatch(ProvenanceEventListingActions.stopPollingProvenanceQuery());
});
return from(this.provenanceService.submitProvenanceQuery(request)).pipe(
map((response) =>
ProvenanceEventListingActions.submitProvenanceQuerySuccess({
response: {
provenance: response.provenance
}
})
),
catchError((errorResponse: HttpErrorResponse) => {
if (this.errorHelper.showErrorInContext(errorResponse.status)) {
return of(
ProvenanceEventListingActions.provenanceApiError({
error: errorResponse.error
})
);
} else {
this.store.dispatch(ProvenanceEventListingActions.stopPollingProvenanceQuery());
return of(this.errorHelper.fullScreenError(errorResponse));
}
})
);
})
)
);
resubmitProvenanceQuery = createEffect(() =>
this.actions$.pipe(
ofType(ProvenanceEventListingActions.resubmitProvenanceQuery),
map((action) => action.request),
switchMap((request) => {
return of(ProvenanceEventListingActions.submitProvenanceQuery({ request }));
})
)
@ -156,29 +164,34 @@ export class ProvenanceEventListingEffects {
pollProvenanceQuery$ = createEffect(() =>
this.actions$.pipe(
ofType(ProvenanceEventListingActions.pollProvenanceQuery),
concatLatestFrom(() => [this.store.select(selectProvenanceId), this.store.select(selectClusterNodeId)]),
switchMap(([, id, clusterNodeId]) => {
if (id) {
return from(this.provenanceService.getProvenanceQuery(id, clusterNodeId)).pipe(
map((response) =>
ProvenanceEventListingActions.pollProvenanceQuerySuccess({
response: {
provenance: response.provenance
}
})
),
catchError((error) =>
of(
concatLatestFrom(() => [
this.store.select(selectActiveProvenanceId).pipe(isDefinedAndNotNull()),
this.store.select(selectClusterNodeId)
]),
switchMap(([, id, clusterNodeId]) =>
from(this.provenanceService.getProvenanceQuery(id, clusterNodeId)).pipe(
map((response) =>
ProvenanceEventListingActions.pollProvenanceQuerySuccess({
response: {
provenance: response.provenance
}
})
),
catchError((errorResponse: HttpErrorResponse) => {
if (this.errorHelper.showErrorInContext(errorResponse.status)) {
return of(
ProvenanceEventListingActions.provenanceApiError({
error: error.error
error: errorResponse.error
})
)
)
);
} else {
return NEVER;
}
})
);
} else {
this.store.dispatch(ProvenanceEventListingActions.stopPollingProvenanceQuery());
return of(this.errorHelper.fullScreenError(errorResponse));
}
})
)
)
)
);
@ -186,15 +199,8 @@ export class ProvenanceEventListingEffects {
this.actions$.pipe(
ofType(ProvenanceEventListingActions.pollProvenanceQuerySuccess),
map((action) => action.response),
switchMap((response) => {
const query: Provenance = response.provenance;
if (query.finished) {
this.dialog.closeAll();
return of(ProvenanceEventListingActions.stopPollingProvenanceQuery());
} else {
return NEVER;
}
})
filter((response) => response.provenance.finished),
switchMap(() => of(ProvenanceEventListingActions.stopPollingProvenanceQuery()))
)
);
@ -205,18 +211,22 @@ export class ProvenanceEventListingEffects {
)
);
deleteProvenanceQuery$ = createEffect(
() =>
this.actions$.pipe(
ofType(ProvenanceEventListingActions.deleteProvenanceQuery),
concatLatestFrom(() => [this.store.select(selectProvenanceId), this.store.select(selectClusterNodeId)]),
tap(([, id, clusterNodeId]) => {
if (id) {
this.provenanceService.deleteProvenanceQuery(id, clusterNodeId).subscribe();
}
})
),
{ dispatch: false }
deleteProvenanceQuery$ = createEffect(() =>
this.actions$.pipe(
ofType(ProvenanceEventListingActions.deleteProvenanceQuery),
concatLatestFrom(() => [
this.store.select(selectActiveProvenanceId),
this.store.select(selectClusterNodeId)
]),
tap(([, id, clusterNodeId]) => {
this.dialog.closeAll();
if (id) {
this.provenanceService.deleteProvenanceQuery(id, clusterNodeId).subscribe();
}
}),
switchMap(() => of(ProvenanceEventListingActions.deleteProvenanceQuerySuccess()))
)
);
openSearchDialog$ = createEffect(
@ -227,44 +237,40 @@ export class ProvenanceEventListingEffects {
this.store.select(selectTimeOffset),
this.store.select(selectProvenanceOptions),
this.store.select(selectProvenanceRequest),
this.store.select(selectAbout)
this.store.select(selectAbout).pipe(isDefinedAndNotNull())
]),
tap(([, timeOffset, options, currentRequest, about]) => {
if (about) {
const dialogReference = this.dialog.open(ProvenanceSearchDialog, {
data: {
timeOffset,
options,
currentRequest
},
panelClass: 'large-dialog'
});
const dialogReference = this.dialog.open(ProvenanceSearchDialog, {
data: {
timeOffset,
options,
currentRequest
},
panelClass: 'large-dialog'
});
dialogReference.componentInstance.timezone = about.timezone;
dialogReference.componentInstance.timezone = about.timezone;
dialogReference.componentInstance.submitSearchCriteria
.pipe(take(1))
.subscribe((request: ProvenanceRequest) => {
if (request.searchTerms) {
const queryParams: any = {};
if (request.searchTerms['ProcessorID']) {
queryParams['componentId'] = request.searchTerms['ProcessorID'].value;
}
if (request.searchTerms['FlowFileUUID']) {
queryParams['flowFileUuid'] = request.searchTerms['FlowFileUUID'].value;
}
// if either of the supported query params are present in the query, update the url
if (Object.keys(queryParams).length > 0) {
this.router.navigate(['/provenance'], { queryParams });
}
dialogReference.componentInstance.submitSearchCriteria
.pipe(take(1))
.subscribe((request: ProvenanceRequest) => {
if (request.searchTerms) {
const queryParams: any = {};
if (request.searchTerms['ProcessorID']) {
queryParams['componentId'] = request.searchTerms['ProcessorID'].value;
}
if (request.searchTerms['FlowFileUUID']) {
queryParams['flowFileUuid'] = request.searchTerms['FlowFileUUID'].value;
}
this.store.dispatch(ProvenanceEventListingActions.saveProvenanceRequest({ request }));
});
}
// if either of the supported query params are present in the query, update the url
if (Object.keys(queryParams).length > 0) {
this.router.navigate(['/provenance'], { queryParams });
}
}
// TODO - if about hasn't loaded we should show an error
this.store.dispatch(ProvenanceEventListingActions.saveProvenanceRequest({ request }));
});
})
),
{ dispatch: false }
@ -311,18 +317,31 @@ export class ProvenanceEventListingEffects {
dialogReference.componentInstance.replay
.pipe(takeUntil(dialogReference.afterClosed()))
.subscribe(() => {
this.provenanceService.replay(request.id).subscribe(() => {
this.store.dispatch(
ProvenanceEventListingActions.showOkDialog({
title: 'Provenance',
message: 'Successfully submitted replay request.'
})
);
dialogReference.close();
this.provenanceService.replay(request.id).subscribe({
next: () => {
this.store.dispatch(
ProvenanceEventListingActions.showOkDialog({
title: 'Provenance',
message: 'Successfully submitted replay request.'
})
);
},
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(
ErrorActions.snackBarError({ error: errorResponse.error })
);
}
});
});
},
error: () => {
// TODO - handle error
error: (errorResponse: HttpErrorResponse) => {
if (this.errorHelper.showErrorInContext(errorResponse.status)) {
this.store.dispatch(ErrorActions.snackBarError({ error: errorResponse.error }));
} else {
this.store.dispatch(this.errorHelper.fullScreenError(errorResponse));
}
}
});
})
@ -337,9 +356,14 @@ export class ProvenanceEventListingEffects {
map((action) => action.request),
tap((request) => {
if (request.eventId) {
this.provenanceService.getProvenanceEvent(request.eventId).subscribe((response) => {
const event: any = response.provenanceEvent;
this.router.navigate(this.getEventComponentLink(event.groupId, event.componentId));
this.provenanceService.getProvenanceEvent(request.eventId).subscribe({
next: (response) => {
const event: any = response.provenanceEvent;
this.router.navigate(this.getEventComponentLink(event.groupId, event.componentId));
},
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(ErrorActions.snackBarError({ error: errorResponse.error }));
}
});
} else if (request.groupId && request.componentId) {
this.router.navigate(this.getEventComponentLink(request.groupId, request.componentId));
@ -349,6 +373,16 @@ export class ProvenanceEventListingEffects {
{ dispatch: false }
);
provenanceApiError$ = createEffect(() =>
this.actions$.pipe(
ofType(ProvenanceEventListingActions.provenanceApiError),
tap(() => {
this.store.dispatch(ProvenanceEventListingActions.stopPollingProvenanceQuery());
}),
switchMap(({ error }) => of(ErrorActions.addBannerError({ error })))
)
);
showOkDialog$ = createEffect(
() =>
this.actions$.pipe(

View File

@ -19,6 +19,7 @@ import { createReducer, on } from '@ngrx/store';
import { ProvenanceEventListingState } from './index';
import {
clearProvenanceRequest,
deleteProvenanceQuerySuccess,
loadProvenanceOptionsSuccess,
pollProvenanceQuerySuccess,
provenanceApiError,
@ -27,13 +28,35 @@ import {
submitProvenanceQuery,
submitProvenanceQuerySuccess
} from './provenance-event-listing.actions';
import { produce } from 'immer';
export const initialState: ProvenanceEventListingState = {
options: null,
request: null,
provenance: null,
loadedTimestamp: '',
error: null,
activeProvenance: null,
completedProvenance: {
id: '',
uri: '',
submissionTime: '',
expiration: '',
percentCompleted: 0,
finished: false,
request: {
maxResults: 0,
summarize: true,
incrementalResults: false
},
results: {
provenanceEvents: [],
total: '',
totalCount: 0,
generated: 'N/A',
oldestEvent: 'N/A',
timeOffset: 0,
errors: []
}
},
loadedTimestamp: 'N/A',
status: 'pending'
};
@ -50,12 +73,22 @@ export const provenanceEventListingReducer = createReducer(
...state,
status: 'loading' as const
})),
on(submitProvenanceQuerySuccess, pollProvenanceQuerySuccess, (state, { response }) => ({
on(submitProvenanceQuerySuccess, pollProvenanceQuerySuccess, (state, { response }) => {
return produce(state, (draftState) => {
const provenance = response.provenance;
draftState.activeProvenance = provenance;
// if the query has finished save it as completed, the active query will be reset after deletion
if (provenance.finished) {
draftState.completedProvenance = provenance;
draftState.loadedTimestamp = provenance.results.generated;
draftState.status = 'success' as const;
}
});
}),
on(deleteProvenanceQuerySuccess, (state) => ({
...state,
provenance: response.provenance,
loadedTimestamp: response.provenance.results.generated,
error: null,
status: 'success' as const
activeProvenance: null
})),
on(saveProvenanceRequest, (state, { request }) => ({
...state,
@ -65,9 +98,8 @@ export const provenanceEventListingReducer = createReducer(
...state,
request: null
})),
on(provenanceApiError, (state, { error }) => ({
on(provenanceApiError, (state) => ({
...state,
error,
status: 'error' as const
}))
);

View File

@ -66,19 +66,27 @@ export const selectLoadedTimestamp = createSelector(
(state: ProvenanceEventListingState) => state.loadedTimestamp
);
export const selectProvenance = createSelector(
export const selectActiveProvenance = createSelector(
selectProvenanceEventListingState,
(state: ProvenanceEventListingState) => state.provenance
(state: ProvenanceEventListingState) => state.activeProvenance
);
export const selectProvenanceId = createSelector(selectProvenance, (state: Provenance | null) => state?.id);
export const selectCompletedProvenance = createSelector(
selectProvenanceEventListingState,
(state: ProvenanceEventListingState) => state.completedProvenance
);
export const selectActiveProvenanceId = createSelector(selectActiveProvenance, (state: Provenance | null) => state?.id);
export const selectClusterNodeId = createSelector(
selectProvenanceRequest,
(state: ProvenanceRequest | null) => state?.clusterNodeId
);
export const selectProvenanceResults = createSelector(selectProvenance, (state: Provenance | null) => state?.results);
export const selectProvenanceResults = createSelector(
selectCompletedProvenance,
(state: Provenance | null) => state?.results
);
export const selectTimeOffset = createSelector(
selectProvenanceResults,

View File

@ -33,6 +33,7 @@
(resubmitProvenanceQuery)="resubmitProvenanceQuery()"
(clearRequest)="clearRequest()"
(queryLineage)="queryLineage($event)"
(clearBannerErrors)="clearBannerErrors()"
(resetLineage)="resetLineage()"></provenance-event-table>
</ng-container>
<ng-template #initialLoading>

View File

@ -27,7 +27,7 @@ import {
} from '../../state/provenance-event-listing';
import {
selectLoadedTimestamp,
selectProvenance,
selectCompletedProvenance,
selectProvenanceRequest,
selectSearchableFieldsFromRoute,
selectStatus
@ -46,7 +46,8 @@ import {
import { ProvenanceSearchDialog } from './provenance-search-dialog/provenance-search-dialog.component';
import { resetLineage, submitLineageQuery } from '../../state/lineage/lineage.actions';
import { LineageRequest } from '../../state/lineage';
import { selectLineage } from '../../state/lineage/lineage.selectors';
import { selectCompletedLineage } from '../../state/lineage/lineage.selectors';
import { clearBannerErrors } from '../../../../state/error/error.actions';
@Component({
selector: 'provenance-event-listing',
@ -56,8 +57,8 @@ import { selectLineage } from '../../state/lineage/lineage.selectors';
export class ProvenanceEventListing implements OnDestroy {
status$ = this.store.select(selectStatus);
loadedTimestamp$ = this.store.select(selectLoadedTimestamp);
provenance$ = this.store.select(selectProvenance);
lineage$ = this.store.select(selectLineage);
provenance$ = this.store.select(selectCompletedProvenance);
lineage$ = this.store.select(selectCompletedLineage);
request!: ProvenanceRequest;
stateReset = false;
@ -200,6 +201,10 @@ export class ProvenanceEventListing implements OnDestroy {
);
}
clearBannerErrors(): void {
this.store.dispatch(clearBannerErrors());
}
resetLineage(): void {
this.store.dispatch(resetLineage());
}
@ -208,5 +213,6 @@ export class ProvenanceEventListing implements OnDestroy {
this.stateReset = true;
this.store.dispatch(resetProvenanceState());
this.store.dispatch(resetLineage());
this.store.dispatch(clearBannerErrors());
}
}

View File

@ -15,7 +15,8 @@
~ limitations under the License.
-->
<div class="provenance-event-table h-full">
<div class="provenance-event-table h-full flex flex-col gap-y-2">
<error-banner></error-banner>
<div [class.hidden]="showLineage" class="h-full flex flex-col gap-y-2">
<div class="flex flex-col">
<div [class.invisible]="!filterApplied" class="value font-bold">

View File

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

View File

@ -38,6 +38,7 @@ import { LineageComponent } from './lineage/lineage.component';
import { GoToProvenanceEventSourceRequest, ProvenanceEventRequest } from '../../../state/provenance-event-listing';
import { MatSliderModule } from '@angular/material/slider';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
@Component({
selector: 'provenance-event-table',
@ -58,7 +59,8 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
AsyncPipe,
MatPaginatorModule,
LineageComponent,
MatSliderModule
MatSliderModule,
ErrorBanner
],
styleUrls: ['./provenance-event-table.component.scss']
})
@ -149,6 +151,7 @@ export class ProvenanceEventTable implements AfterViewInit {
@Output() resubmitProvenanceQuery: EventEmitter<void> = new EventEmitter<void>();
@Output() queryLineage: EventEmitter<LineageRequest> = new EventEmitter<LineageRequest>();
@Output() resetLineage: EventEmitter<void> = new EventEmitter<void>();
@Output() clearBannerErrors: EventEmitter<void> = new EventEmitter<void>();
protected readonly TextTip = TextTip;
protected readonly BulletinsTip = BulletinsTip;
@ -305,11 +308,7 @@ export class ProvenanceEventTable implements AfterViewInit {
return false;
}
if (event.componentId === 'Remote Output Port' || event.componentId === 'Remote Input Port') {
return false;
}
return true;
return !(event.componentId === 'Remote Output Port' || event.componentId === 'Remote Input Port');
}
goToClicked(event: ProvenanceEventSummary): void {
@ -327,6 +326,8 @@ export class ProvenanceEventTable implements AfterViewInit {
this.eventId = event.id;
this.showLineage = true;
this.clearBannerErrors.next();
this.submitLineageQuery({
lineageRequestType: 'FLOWFILE',
uuid: event.flowFileUuid,
@ -345,6 +346,7 @@ export class ProvenanceEventTable implements AfterViewInit {
this.eventTimestampStep = 1;
this.initialEventTimestampThreshold = 0;
this.currentEventTimestampThreshold = 0;
this.clearBannerErrors.next();
this.resetLineage.next();
}

View File

@ -29,6 +29,11 @@
</div>
</div>
</ng-container>
<ng-container *ngIf="request.options.searchableFields.length === 0">
<div class="unset mb-5">
No searchable fields are available. Search criteria based on date, time, and file size still available.
</div>
</ng-container>
<mat-form-field>
<mat-label>Date Range</mat-label>
<mat-date-range-input [rangePicker]="picker">

View File

@ -20,6 +20,10 @@
.search-events-form {
@include mat.button-density(-1);
mat-dialog-content {
min-height: 450px;
}
.mat-mdc-form-field {
width: 100%;
}

View File

@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProvenanceSearchDialog } from './provenance-search-dialog.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatNativeDateModule } from '@angular/material/core';
describe('ProvenanceSearchDialog', () => {
@ -71,7 +71,7 @@ describe('ProvenanceSearchDialog', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ProvenanceSearchDialog, BrowserAnimationsModule, MatNativeDateModule],
imports: [ProvenanceSearchDialog, NoopAnimationsModule, MatNativeDateModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
});
fixture = TestBed.createComponent(ProvenanceSearchDialog);

View File

@ -102,7 +102,7 @@ export interface FlowFileDialogRequest {
export interface QueueListingState {
activeListingRequest: ListingRequest | null;
completedListingRequest: ListingRequest | null;
completedListingRequest: ListingRequest;
connectionLabel: string;
loadedTimestamp: string;
status: 'pending' | 'loading' | 'error' | 'success';

View File

@ -66,13 +66,13 @@ export const queueListingReducer = createReducer(
on(submitQueueListingRequestSuccess, pollQueueListingRequestSuccess, (state, { response }) => {
return produce(state, (draftState) => {
const listingRequest = response.requestEntity.listingRequest;
draftState.activeListingRequest = listingRequest;
// if the query has finished save it as completed, the active query will be reset after deletion
if (listingRequest.finished) {
draftState.completedListingRequest = listingRequest;
draftState.loadedTimestamp = listingRequest.lastUpdated;
draftState.status = 'success' as const;
} else {
draftState.activeListingRequest = listingRequest;
}
});
}),

View File

@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FlowFileDialog } from './flowfile-dialog.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { FlowFileDialogRequest } from '../../../state/queue-listing';
describe('FlowFileDialog', () => {
@ -46,7 +46,7 @@ describe('FlowFileDialog', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FlowFileDialog, BrowserAnimationsModule],
imports: [FlowFileDialog, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
});
fixture = TestBed.createComponent(FlowFileDialog);

View File

@ -19,7 +19,7 @@ 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 { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Component } from '@angular/core';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/error/error.reducer';
@ -37,7 +37,7 @@ describe('FlowFileTable', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FlowFileTable, MockErrorBanner, MatTableModule, BrowserAnimationsModule],
imports: [FlowFileTable, MockErrorBanner, MatTableModule, NoopAnimationsModule],
providers: [
provideMockStore({
initialState

View File

@ -627,7 +627,9 @@ export class ParameterProvidersEffects {
concatLatestFrom(() => this.store.select(selectApplyParameterProviderParametersRequest)),
tap(([, updateRequest]) => {
if (updateRequest) {
this.parameterProviderService.deleteParameterProviderParametersUpdateRequest(updateRequest);
this.parameterProviderService
.deleteParameterProviderParametersUpdateRequest(updateRequest)
.subscribe();
}
})
),

View File

@ -21,7 +21,7 @@ import { CreateFlowAnalysisRule } from './create-flow-analysis-rule.component';
import { CreateFlowAnalysisRuleDialogRequest } from '../../../state/flow-analysis-rules';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { provideMockStore } from '@ngrx/store/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { initialState } from '../../../state/flow-analysis-rules/flow-analysis-rules.reducer';
describe('CreateFlowAnalysisRule', () => {
@ -47,7 +47,7 @@ describe('CreateFlowAnalysisRule', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CreateFlowAnalysisRule, BrowserAnimationsModule],
imports: [CreateFlowAnalysisRule, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(CreateFlowAnalysisRule);

View File

@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EditFlowAnalysisRule } from './edit-flow-analysis-rule.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { EditFlowAnalysisRuleDialogRequest } from '../../../state/flow-analysis-rules';
import { Component } from '@angular/core';
import { provideMockStore } from '@ngrx/store/testing';
@ -101,7 +101,7 @@ describe('EditFlowAnalysisRule', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditFlowAnalysisRule, MockErrorBanner, BrowserAnimationsModule],
imports: [EditFlowAnalysisRule, MockErrorBanner, NoopAnimationsModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
provideMockStore({

View File

@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FlowAnalysisRuleTable } from './flow-analysis-rule-table.component';
import { MatTableModule } from '@angular/material/table';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('FlowAnalysisRuleTable', () => {
let component: FlowAnalysisRuleTable;
@ -27,7 +27,7 @@ describe('FlowAnalysisRuleTable', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule, MatTableModule, FlowAnalysisRuleTable]
imports: [NoopAnimationsModule, MatTableModule, FlowAnalysisRuleTable]
});
fixture = TestBed.createComponent(FlowAnalysisRuleTable);
component = fixture.componentInstance;

View File

@ -23,7 +23,7 @@ import { initialState } from '../../../state/general/general.reducer';
import { MatFormFieldModule } from '@angular/material/form-field';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('GeneralForm', () => {
let component: GeneralForm;
@ -32,7 +32,7 @@ describe('GeneralForm', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [GeneralForm],
imports: [BrowserAnimationsModule, MatFormFieldModule, MatInputModule, FormsModule, ReactiveFormsModule],
imports: [NoopAnimationsModule, MatFormFieldModule, MatInputModule, FormsModule, ReactiveFormsModule],
providers: [
provideMockStore({
initialState

View File

@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { CreateRegistryClient } from './create-registry-client.component';
import { CreateRegistryClientDialogRequest } from '../../../state/registry-clients';
@ -43,7 +43,7 @@ describe('CreateRegistryClient', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CreateRegistryClient, BrowserAnimationsModule],
imports: [CreateRegistryClient, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
});
fixture = TestBed.createComponent(CreateRegistryClient);

View File

@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { EditRegistryClient } from './edit-registry-client.component';
import { EditRegistryClientDialogRequest } from '../../../state/registry-clients';
import { Component } from '@angular/core';
@ -114,7 +114,7 @@ describe('EditRegistryClient', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditRegistryClient, MockErrorBanner, BrowserAnimationsModule],
imports: [EditRegistryClient, MockErrorBanner, NoopAnimationsModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
provideMockStore({

View File

@ -20,7 +20,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RegistryClientTable } from './registry-client-table.component';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('RegistryClientTable', () => {
let component: RegistryClientTable;
@ -29,7 +29,7 @@ describe('RegistryClientTable', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [RegistryClientTable],
imports: [MatTableModule, MatSortModule, BrowserAnimationsModule]
imports: [MatTableModule, MatSortModule, NoopAnimationsModule]
});
fixture = TestBed.createComponent(RegistryClientTable);
component = fixture.componentInstance;

View File

@ -22,7 +22,7 @@ import { CreateReportingTaskDialogRequest } from '../../../state/reporting-tasks
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/extension-types/extension-types.reducer';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('CreateReportingTask', () => {
let component: CreateReportingTask;
@ -46,7 +46,7 @@ describe('CreateReportingTask', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CreateReportingTask, BrowserAnimationsModule],
imports: [CreateReportingTask, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(CreateReportingTask);

View File

@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EditReportingTask } from './edit-reporting-task.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { EditReportingTaskDialogRequest } from '../../../state/reporting-tasks';
import { Component } from '@angular/core';
import { provideMockStore } from '@ngrx/store/testing';
@ -394,7 +394,7 @@ describe('EditReportingTask', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditReportingTask, MockErrorBanner, BrowserAnimationsModule],
imports: [EditReportingTask, MockErrorBanner, NoopAnimationsModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
provideMockStore({

View File

@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserAccessPolicies } from './user-access-policies.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { UserAccessPoliciesDialogRequest } from '../../../state/user-listing';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
@ -52,7 +52,7 @@ describe('UserAccessPolicies', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [UserAccessPolicies, BrowserAnimationsModule],
imports: [UserAccessPolicies, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
});
fixture = TestBed.createComponent(UserAccessPolicies);

View File

@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentStateDialog } from './component-state.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../state/component-state/component-state.reducer';
@ -28,7 +28,7 @@ describe('ComponentStateDialog', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ComponentStateDialog, BrowserAnimationsModule],
imports: [ComponentStateDialog, NoopAnimationsModule],
providers: [provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(ComponentStateDialog);

View File

@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ControllerServiceTable } from './controller-service-table.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('ControllerServiceTable', () => {
let component: ControllerServiceTable;
@ -26,7 +26,7 @@ describe('ControllerServiceTable', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [BrowserAnimationsModule, ControllerServiceTable]
imports: [NoopAnimationsModule, ControllerServiceTable]
});
fixture = TestBed.createComponent(ControllerServiceTable);
component = fixture.componentInstance;

View File

@ -21,7 +21,7 @@ import { CreateControllerService } from './create-controller-service.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../state/extension-types/extension-types.reducer';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { CreateControllerServiceDialogRequest } from '../../../../state/shared';
describe('CreateControllerService', () => {
@ -56,7 +56,7 @@ describe('CreateControllerService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CreateControllerService, BrowserAnimationsModule],
imports: [CreateControllerService, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(CreateControllerService);

View File

@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DisableControllerService } from './disable-controller-service.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../state/contoller-service-state/controller-service-state.reducer';
import { ComponentType, SetEnableControllerServiceDialogRequest } from '../../../../state/shared';
@ -341,7 +341,7 @@ describe('EnableControllerService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [DisableControllerService, BrowserAnimationsModule],
imports: [DisableControllerService, NoopAnimationsModule],
providers: [provideMockStore({ initialState }), { provide: MAT_DIALOG_DATA, useValue: data }]
});
fixture = TestBed.createComponent(DisableControllerService);

View File

@ -20,7 +20,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EditControllerService } from './edit-controller-service.component';
import { EditControllerServiceDialogRequest } from '../../../../state/shared';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Component } from '@angular/core';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../state/error/error.reducer';
@ -553,7 +553,7 @@ describe('EditControllerService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditControllerService, MockErrorBanner, BrowserAnimationsModule],
imports: [EditControllerService, MockErrorBanner, NoopAnimationsModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
provideMockStore({

View File

@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EnableControllerService } from './enable-controller-service.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../state/contoller-service-state/controller-service-state.reducer';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
@ -341,7 +341,7 @@ describe('EnableControllerService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EnableControllerService, BrowserAnimationsModule, MatDialogModule],
imports: [EnableControllerService, NoopAnimationsModule, MatDialogModule],
providers: [provideMockStore({ initialState }), { provide: MAT_DIALOG_DATA, useValue: data }]
});
fixture = TestBed.createComponent(EnableControllerService);

View File

@ -20,7 +20,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EditParameterDialog } from './edit-parameter-dialog.component';
import { EditParameterRequest } from '../../../state/shared';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('EditParameterDialog', () => {
let component: EditParameterDialog;
@ -51,7 +51,7 @@ describe('EditParameterDialog', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditParameterDialog, BrowserAnimationsModule],
imports: [EditParameterDialog, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
});
fixture = TestBed.createComponent(EditParameterDialog);

View File

@ -20,7 +20,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
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 { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Component } from '@angular/core';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../state/error/error.reducer';
@ -794,7 +794,7 @@ describe('EditTenantDialog', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditTenantDialog, MockErrorBanner, BrowserAnimationsModule],
imports: [EditTenantDialog, MockErrorBanner, NoopAnimationsModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
provideMockStore({

View File

@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ExtensionCreation } from './extension-creation.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('ExtensionCreation', () => {
let component: ExtensionCreation;
@ -26,7 +26,7 @@ describe('ExtensionCreation', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ExtensionCreation, BrowserAnimationsModule]
imports: [ExtensionCreation, NoopAnimationsModule]
});
fixture = TestBed.createComponent(ExtensionCreation);
component = fixture.componentInstance;

View File

@ -21,7 +21,7 @@ import { NewPropertyDialog } from './new-property-dialog.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { NewPropertyDialogRequest } from '../../../state/shared';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('NewPropertyDialog', () => {
let component: NewPropertyDialog;
@ -34,7 +34,7 @@ describe('NewPropertyDialog', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NewPropertyDialog, BrowserAnimationsModule, FormsModule, ReactiveFormsModule],
imports: [NewPropertyDialog, NoopAnimationsModule, FormsModule, ReactiveFormsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
});
fixture = TestBed.createComponent(NewPropertyDialog);

View File

@ -21,7 +21,7 @@ import { ComboEditor } from './combo-editor.component';
import { PropertyItem } from '../../property-table.component';
import { Parameter } from '../../../../../state/shared';
import { of } from 'rxjs';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('ComboEditor', () => {
let component: ComboEditor;
@ -73,7 +73,7 @@ describe('ComboEditor', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ComboEditor, BrowserAnimationsModule]
imports: [ComboEditor, NoopAnimationsModule]
});
fixture = TestBed.createComponent(ComboEditor);
component = fixture.componentInstance;

View File

@ -399,11 +399,7 @@
"></ng-container>
</div>
<div>
<button
color="primary"
mat-stroked-button
mat-dialog-close
(click)="replayClicked()">
<button color="primary" mat-stroked-button (click)="replayClicked()">
<i class="fa fa-repeat"></i>
Replay
</button>

View File

@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProvenanceEventDialog } from './provenance-event-dialog.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('ProvenanceEventDialog', () => {
let component: ProvenanceEventDialog;
@ -71,7 +71,7 @@ describe('ProvenanceEventDialog', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ProvenanceEventDialog, BrowserAnimationsModule],
imports: [ProvenanceEventDialog, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
});
fixture = TestBed.createComponent(ProvenanceEventDialog);