NIFI-15028 - Ensure parameter context edit async polling stops after cancelling the update (#10357)
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/parameter-helper.service.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/parameter-helper.service.ts
index 14ef171..156cc82 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/parameter-helper.service.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/parameter-helper.service.ts
@@ -90,10 +90,11 @@
convertToParameterDialogReference.componentInstance.saving$ =
this.store.select(selectParameterSaving);
- convertToParameterDialogReference.componentInstance.exit.pipe(
- takeUntil(convertToParameterDialogReference.afterClosed()),
- tap(() => ParameterActions.stopPollingParameterContextUpdateRequest())
- );
+ convertToParameterDialogReference.componentInstance.exit
+ .pipe(takeUntil(convertToParameterDialogReference.afterClosed()))
+ .subscribe(() =>
+ this.store.dispatch(ParameterActions.stopPollingParameterContextUpdateRequest())
+ );
return convertToParameterDialogReference.componentInstance.editParameter.pipe(
takeUntil(convertToParameterDialogReference.afterClosed()),
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/index.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/index.ts
index 76cca5f..fb0bcdc 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/index.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/index.ts
@@ -57,5 +57,6 @@
updateRequestEntity: ParameterContextUpdateRequestEntity | null;
saving: boolean;
loadedTimestamp: string;
+ deleteUpdateRequestInitiated: boolean;
status: 'pending' | 'loading' | 'success';
}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.effects.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.effects.spec.ts
new file mode 100644
index 0000000..698fd46
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.effects.spec.ts
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { TestBed } from '@angular/core/testing';
+import { provideMockActions } from '@ngrx/effects/testing';
+import { Action } from '@ngrx/store';
+import { ReplaySubject, of } from 'rxjs';
+import { provideMockStore } from '@ngrx/store/testing';
+import { MatDialog } from '@angular/material/dialog';
+import { Router } from '@angular/router';
+
+import { ParameterContextListingEffects } from './parameter-context-listing.effects';
+import * as ParameterContextListingActions from './parameter-context-listing.actions';
+import { ParameterContextService } from '../../service/parameter-contexts.service';
+import { ErrorHelper } from '../../../../service/error-helper.service';
+import { Storage, NiFiCommon } from '@nifi/shared';
+import { initialState } from './parameter-context-listing.reducer';
+import { ParameterContextUpdateRequest, ParameterContextUpdateRequestEntity } from '../../../../state/shared';
+
+describe('ParameterContextListingEffects', () => {
+ interface SetupOptions {
+ updateRequest?: ParameterContextUpdateRequestEntity | null;
+ deleteUpdateRequestInitiated?: boolean;
+ }
+
+ let action$: ReplaySubject<Action>;
+
+ function createMockUpdateRequest(): ParameterContextUpdateRequest {
+ return {
+ requestId: 'test-request-id',
+ uri: 'http://localhost:8080/test-uri',
+ lastUpdated: '2023-01-01T00:00:00Z',
+ complete: false,
+ failureReason: undefined,
+ percentComponent: 50,
+ state: 'In Progress',
+ updateSteps: [],
+ parameterContext: {} as any,
+ referencingComponents: []
+ };
+ }
+
+ async function setup({ updateRequest = null, deleteUpdateRequestInitiated = false }: SetupOptions = {}) {
+ await TestBed.configureTestingModule({
+ providers: [
+ ParameterContextListingEffects,
+ provideMockActions(() => action$),
+ provideMockStore({
+ initialState: {
+ parameterContextListing: {
+ ...initialState,
+ updateRequestEntity: updateRequest,
+ deleteUpdateRequestInitiated
+ }
+ }
+ }),
+ { provide: ParameterContextService, useValue: { deleteParameterContextUpdate: jest.fn() } },
+ { provide: MatDialog, useValue: { open: jest.fn() } },
+ { provide: Router, useValue: { navigate: jest.fn() } },
+ { provide: ErrorHelper, useValue: { getErrorString: jest.fn() } },
+ { provide: Storage, useValue: { setItem: jest.fn() } },
+ { provide: NiFiCommon, useValue: { stripProtocol: jest.fn() } }
+ ]
+ }).compileComponents();
+
+ const effects = TestBed.inject(ParameterContextListingEffects);
+ const parameterContextService = TestBed.inject(ParameterContextService) as jest.Mocked<ParameterContextService>;
+ action$ = new ReplaySubject<Action>();
+
+ return { effects, parameterContextService };
+ }
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should create', async () => {
+ const { effects } = await setup();
+ expect(effects).toBeTruthy();
+ });
+
+ describe('stopPollingParameterContextUpdateRequest$', () => {
+ it('should dispatch deleteParameterContextUpdateRequest when triggered', async () => {
+ const { effects } = await setup();
+
+ action$.next(ParameterContextListingActions.stopPollingParameterContextUpdateRequest());
+
+ effects.stopPollingParameterContextUpdateRequest$.subscribe((action) => {
+ expect(action).toEqual(ParameterContextListingActions.deleteParameterContextUpdateRequest());
+ });
+ });
+ });
+
+ describe('deleteParameterContextUpdateRequest$', () => {
+ it('should call service when deleteUpdateRequestInitiated is false', async () => {
+ const mockUpdateRequest = createMockUpdateRequest();
+ const mockResponse = { request: mockUpdateRequest };
+
+ const { effects, parameterContextService } = await setup({
+ updateRequest: { request: mockUpdateRequest, parameterContextRevision: { version: 1 } },
+ deleteUpdateRequestInitiated: false
+ });
+
+ parameterContextService.deleteParameterContextUpdate.mockReturnValue(of(mockResponse));
+
+ action$.next(ParameterContextListingActions.deleteParameterContextUpdateRequest());
+
+ effects.deleteParameterContextUpdateRequest$.subscribe(() => {
+ expect(parameterContextService.deleteParameterContextUpdate).toHaveBeenCalledWith(mockUpdateRequest);
+ });
+ });
+
+ it('should call service when deleteUpdateRequestInitiated is true', async () => {
+ const mockUpdateRequest = createMockUpdateRequest();
+ const mockResponse = { request: mockUpdateRequest };
+
+ const { effects, parameterContextService } = await setup({
+ updateRequest: { request: mockUpdateRequest, parameterContextRevision: { version: 1 } },
+ deleteUpdateRequestInitiated: true
+ });
+
+ parameterContextService.deleteParameterContextUpdate.mockReturnValue(of(mockResponse));
+
+ action$.next(ParameterContextListingActions.deleteParameterContextUpdateRequest());
+
+ effects.deleteParameterContextUpdateRequest$.subscribe(() => {
+ expect(parameterContextService.deleteParameterContextUpdate).toHaveBeenCalledWith(mockUpdateRequest);
+ });
+ });
+ });
+
+ describe('pollParameterContextUpdateRequestSuccess$', () => {
+ it('should dispatch stopPolling when request is complete', async () => {
+ const completeUpdateRequest = createMockUpdateRequest();
+ completeUpdateRequest.complete = true;
+
+ const response = {
+ requestEntity: {
+ request: completeUpdateRequest,
+ parameterContextRevision: { version: 1 }
+ }
+ };
+
+ const { effects } = await setup();
+
+ action$.next(ParameterContextListingActions.pollParameterContextUpdateRequestSuccess({ response }));
+
+ effects.pollParameterContextUpdateRequestSuccess$.subscribe((action) => {
+ expect(action).toEqual(ParameterContextListingActions.stopPollingParameterContextUpdateRequest());
+ });
+ });
+
+ it('should not dispatch when request is incomplete', async () => {
+ const incompleteUpdateRequest = createMockUpdateRequest();
+ incompleteUpdateRequest.complete = false;
+
+ const response = {
+ requestEntity: {
+ request: incompleteUpdateRequest,
+ parameterContextRevision: { version: 1 }
+ }
+ };
+
+ const { effects } = await setup();
+
+ const emissions: any[] = [];
+ effects.pollParameterContextUpdateRequestSuccess$.subscribe((action) => {
+ emissions.push(action);
+ });
+
+ action$.next(ParameterContextListingActions.pollParameterContextUpdateRequestSuccess({ response }));
+
+ // Since the effect is synchronous with filter, we can check immediately
+ expect(emissions).toEqual([]);
+ });
+ });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.effects.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.effects.ts
index 5f4542c..1ca35bf 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.effects.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.effects.ts
@@ -44,7 +44,8 @@
selectParameterContexts,
selectParameterContextStatus,
selectSaving,
- selectUpdateRequest
+ selectUpdateRequest,
+ selectDeleteUpdateRequestInitiated
} from './parameter-context-listing.selectors';
import { EditParameterRequest, EditParameterResponse, ParameterContextUpdateRequest } from '../../../../state/shared';
import { EditParameterDialog } from '../../../../ui/common/edit-parameter-dialog/edit-parameter-dialog.component';
@@ -383,6 +384,14 @@
);
});
+ editDialogReference.componentInstance.cancelUpdateRequest
+ .pipe(takeUntil(editDialogReference.afterClosed()))
+ .subscribe(() => {
+ this.store.dispatch(
+ ParameterContextListingActions.stopPollingParameterContextUpdateRequest()
+ );
+ });
+
editDialogReference.afterClosed().subscribe((response) => {
if (response != 'ROUTED') {
this.store.dispatch(
@@ -504,6 +513,8 @@
stopPollingParameterContextUpdateRequest$ = createEffect(() =>
this.actions$.pipe(
ofType(ParameterContextListingActions.stopPollingParameterContextUpdateRequest),
+ concatLatestFrom(() => this.store.select(selectDeleteUpdateRequestInitiated)),
+ filter(([, deleteUpdateRequestInitiated]) => !deleteUpdateRequestInitiated),
switchMap(() => of(ParameterContextListingActions.deleteParameterContextUpdateRequest()))
)
);
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.reducer.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.reducer.ts
index 4677631..21d9e67 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.reducer.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.reducer.ts
@@ -30,7 +30,8 @@
pollParameterContextUpdateRequestSuccess,
submitParameterContextUpdateRequest,
submitParameterContextUpdateRequestSuccess,
- deleteParameterContextUpdateRequestSuccess
+ deleteParameterContextUpdateRequestSuccess,
+ deleteParameterContextUpdateRequest
} from './parameter-context-listing.actions';
import { ParameterContextUpdateRequestEntity } from '../../../../state/shared';
import { Revision } from '@nifi/shared';
@@ -40,6 +41,7 @@
updateRequestEntity: null,
saving: false,
loadedTimestamp: '',
+ deleteUpdateRequestInitiated: false,
status: 'pending'
};
@@ -83,6 +85,10 @@
updateRequestEntity: response.requestEntity
})
),
+ on(deleteParameterContextUpdateRequest, (state) => ({
+ ...state,
+ deleteUpdateRequestInitiated: true
+ })),
on(deleteParameterContextUpdateRequestSuccess, (state) => ({
...state,
saving: false
@@ -93,25 +99,30 @@
if (updateRequestEntity) {
const revision: Revision = updateRequestEntity.parameterContextRevision;
- const parameterContext: any = updateRequestEntity.request.parameterContext;
- const componentIndex: number = draftState.parameterContexts.findIndex(
- (f: any) => parameterContext.id === f.id
- );
- if (componentIndex > -1) {
- draftState.parameterContexts[componentIndex] = {
- ...draftState.parameterContexts[componentIndex],
- revision: {
- ...revision
- },
- component: {
- ...parameterContext
- }
- };
+ // update state if completed, otherwise there won't be a parameter context on the request
+ if (updateRequestEntity.request.complete) {
+ const parameterContext: any = updateRequestEntity.request.parameterContext;
+
+ const componentIndex: number = draftState.parameterContexts.findIndex(
+ (f: any) => parameterContext.id === f.id
+ );
+ if (componentIndex > -1) {
+ draftState.parameterContexts[componentIndex] = {
+ ...draftState.parameterContexts[componentIndex],
+ revision: {
+ ...revision
+ },
+ component: {
+ ...parameterContext
+ }
+ };
+ }
}
draftState.updateRequestEntity = null;
draftState.saving = false;
+ draftState.deleteUpdateRequestInitiated = false;
}
});
}),
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.selectors.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.selectors.ts
index 802a7f5..755dafe 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.selectors.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/parameter-contexts/state/parameter-context-listing/parameter-context-listing.selectors.ts
@@ -64,3 +64,8 @@
createSelector(selectParameterContexts, (parameterContexts: ParameterContextEntity[]) =>
parameterContexts.find((entity) => id == entity.id)
);
+
+export const selectDeleteUpdateRequestInitiated = createSelector(
+ selectParameterContextListingState,
+ (state: ParameterContextListingState) => state.deleteUpdateRequestInitiated
+);
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.html b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.html
index 421cd7a..9ef15ca 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.html
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.html
@@ -179,7 +179,7 @@
@if (requestEntity.request.complete) {
<button mat-flat-button mat-dialog-close>Close</button>
} @else {
- <button mat-button mat-dialog-close>Cancel</button>
+ <button mat-button mat-dialog-close (click)="cancelUpdateRequest.emit()">Cancel</button>
}
</mat-dialog-actions>
} @else {
@@ -188,7 +188,7 @@
@if (readonly) {
<button mat-flat-button mat-dialog-close>Close</button>
} @else {
- <button mat-button mat-dialog-close>Cancel</button>
+ <button mat-button mat-dialog-close (click)="cancelUpdateRequest.emit()">Cancel</button>
<button
[disabled]="
!editParameterContextForm.dirty ||
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.spec.ts
index cd44eb4..8f99f51 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.spec.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.spec.ts
@@ -16,6 +16,7 @@
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { EventEmitter } from '@angular/core';
import { EditParameterContext } from './edit-parameter-context.component';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@@ -260,6 +261,19 @@
expect(component).toBeTruthy();
});
+ it('should have cancelUpdateRequest EventEmitter', () => {
+ expect(component.cancelUpdateRequest).toBeDefined();
+ expect(component.cancelUpdateRequest).toBeInstanceOf(EventEmitter);
+ });
+
+ it('should emit cancelUpdateRequest when called', () => {
+ const spy = jest.spyOn(component.cancelUpdateRequest, 'emit');
+
+ component.cancelUpdateRequest.emit();
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
describe('inheritsParameters', () => {
it('should return true if parameters are inherited', () => {
const parameters: ParameterEntity[] = [
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.ts
index 718b9a8..4ed38d9 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/parameter-context/edit-parameter-context/edit-parameter-context.component.ts
@@ -88,6 +88,7 @@
@Output() addParameterContext: EventEmitter<any> = new EventEmitter<any>();
@Output() editParameterContext: EventEmitter<any> = new EventEmitter<any>();
+ @Output() cancelUpdateRequest: EventEmitter<any> = new EventEmitter<any>();
editParameterContextForm: FormGroup;
readonly: boolean;