diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts index 637f4cb1c5..c83ec11456 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts @@ -46,6 +46,7 @@ import { MatSnackBarModule } from '@angular/material/snack-bar'; import { PipesModule } from './pipes/pipes.module'; import { DocumentationEffects } from './state/documentation/documentation.effects'; import { ClusterSummaryEffects } from './state/cluster-summary/cluster-summary.effects'; +import { PropertyVerificationEffects } from './state/property-verification/property-verification.effects'; import { loadingInterceptor } from './service/interceptors/loading.interceptor'; import { LoginConfigurationEffects } from './state/login-configuration/login-configuration.effects'; @@ -77,7 +78,8 @@ import { LoginConfigurationEffects } from './state/login-configuration/login-con SystemDiagnosticsEffects, ComponentStateEffects, DocumentationEffects, - ClusterSummaryEffects + ClusterSummaryEffects, + PropertyVerificationEffects ), StoreDevtoolsModule.instrument({ maxAge: 25, diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.ts index abc711fc48..bea2a726c7 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.ts @@ -54,6 +54,15 @@ import { LARGE_DIALOG, SMALL_DIALOG, XL_DIALOG } from '../../../../index'; import { ExtensionTypesService } from '../../../../service/extension-types.service'; import { ChangeComponentVersionDialog } from '../../../../ui/common/change-component-version-dialog/change-component-version-dialog'; import { FlowService } from '../../service/flow.service'; +import { + resetPropertyVerificationState, + verifyProperties +} from '../../../../state/property-verification/property-verification.actions'; +import { + selectPropertyVerificationResults, + selectPropertyVerificationStatus +} from '../../../../state/property-verification/property-verification.selectors'; +import { VerifyPropertiesRequestContext } from '../../../../state/property-verification'; @Injectable() export class ControllerServicesEffects { @@ -270,7 +279,7 @@ export class ControllerServicesEffects { const serviceId: string = request.id; const editDialogReference = this.dialog.open(EditControllerService, { - ...LARGE_DIALOG, + ...XL_DIALOG, data: request, id: serviceId }); @@ -280,6 +289,23 @@ export class ControllerServicesEffects { editDialogReference.componentInstance.createNewProperty = this.propertyTableHelperService.createNewProperty(request.id, this.controllerServiceService); + editDialogReference.componentInstance.verify + .pipe(takeUntil(editDialogReference.afterClosed())) + .subscribe((verificationRequest: VerifyPropertiesRequestContext) => { + this.store.dispatch( + verifyProperties({ + request: verificationRequest + }) + ); + }); + + editDialogReference.componentInstance.propertyVerificationResults$ = this.store.select( + selectPropertyVerificationResults + ); + editDialogReference.componentInstance.propertyVerificationStatus$ = this.store.select( + selectPropertyVerificationStatus + ); + const goTo = (commands: string[], destination: string): void => { if (editDialogReference.componentInstance.editControllerServiceForm.dirty) { const saveChangesDialogReference = this.dialog.open(YesNoDialog, { @@ -375,6 +401,9 @@ export class ControllerServicesEffects { }); editDialogReference.afterClosed().subscribe((response) => { + this.store.dispatch(ErrorActions.clearBannerErrors()); + this.store.dispatch(resetPropertyVerificationState()); + if (response != 'ROUTED') { this.store.dispatch( ControllerServicesActions.selectControllerService({ diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts index b09d4d71ec..2f59fb9540 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts @@ -136,6 +136,15 @@ import { ErrorHelper } from '../../../../service/error-helper.service'; import { selectConnectedStateChanged } from '../../../../state/cluster-summary/cluster-summary.selectors'; import { resetConnectedStateChanged } from '../../../../state/cluster-summary/cluster-summary.actions'; import { ChangeColorDialog } from '../../ui/canvas/change-color-dialog/change-color-dialog.component'; +import { + resetPropertyVerificationState, + verifyProperties +} from '../../../../state/property-verification/property-verification.actions'; +import { + selectPropertyVerificationResults, + selectPropertyVerificationStatus +} from '../../../../state/property-verification/property-verification.selectors'; +import { VerifyPropertiesRequestContext } from '../../../../state/property-verification'; @Injectable() export class FlowEffects { @@ -1291,7 +1300,7 @@ export class FlowEffects { const processorId: string = request.entity.id; const editDialogReference = this.dialog.open(EditProcessor, { - ...LARGE_DIALOG, + ...XL_DIALOG, data: request, id: processorId }); @@ -1301,6 +1310,23 @@ export class FlowEffects { editDialogReference.componentInstance.createNewProperty = this.propertyTableHelperService.createNewProperty(processorId, this.flowService); + editDialogReference.componentInstance.verify + .pipe(takeUntil(editDialogReference.afterClosed())) + .subscribe((verificationRequest: VerifyPropertiesRequestContext) => { + this.store.dispatch( + verifyProperties({ + request: verificationRequest + }) + ); + }); + + editDialogReference.componentInstance.propertyVerificationResults$ = this.store.select( + selectPropertyVerificationResults + ); + editDialogReference.componentInstance.propertyVerificationStatus$ = this.store.select( + selectPropertyVerificationStatus + ); + const goTo = (commands: string[], destination: string): void => { if (editDialogReference.componentInstance.editProcessorForm.dirty) { const saveChangesDialogReference = this.dialog.open(YesNoDialog, { @@ -1380,7 +1406,7 @@ export class FlowEffects { editDialogReference.afterClosed().subscribe((response) => { this.store.dispatch(ErrorActions.clearBannerErrors()); - + this.store.dispatch(resetPropertyVerificationState()); if (response != 'ROUTED') { this.store.dispatch( FlowActions.selectComponents({ diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/label/edit-label/edit-label.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/label/edit-label/edit-label.component.spec.ts index b23e76b07d..4584678bdb 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/label/edit-label/edit-label.component.spec.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/label/edit-label/edit-label.component.spec.ts @@ -80,10 +80,7 @@ describe('EditLabel', () => { isDisconnectionAcknowledged: jest.fn() } }, - { - provide: MatDialogRef, - useValue: null - } + { provide: MatDialogRef, useValue: null } ] }); fixture = TestBed.createComponent(EditLabel); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/port/create-port/create-port.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/port/create-port/create-port.component.spec.ts index 56be6c3ffd..fb92cae423 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/port/create-port/create-port.component.spec.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/port/create-port/create-port.component.spec.ts @@ -47,10 +47,7 @@ describe('CreatePort', () => { providers: [ { provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState }), - { - provide: MatDialogRef, - useValue: null - } + { provide: MatDialogRef, useValue: null } ] }); fixture = TestBed.createComponent(CreatePort); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/port/edit-port/edit-port.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/port/edit-port/edit-port.component.spec.ts index ed5de99cc3..8be6b597a5 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/port/edit-port/edit-port.component.spec.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/port/edit-port/edit-port.component.spec.ts @@ -106,10 +106,7 @@ describe('EditPort', () => { isDisconnectionAcknowledged: jest.fn() } }, - { - provide: MatDialogRef, - useValue: null - } + { provide: MatDialogRef, useValue: null } ] }); fixture = TestBed.createComponent(EditPort); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/process-group/create-process-group/create-process-group.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/process-group/create-process-group/create-process-group.component.spec.ts index ff6695a011..4c7b4b41e9 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/process-group/create-process-group/create-process-group.component.spec.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/process-group/create-process-group/create-process-group.component.spec.ts @@ -159,10 +159,7 @@ describe('CreateProcessGroup', () => { providers: [ { provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState }), - { - provide: MatDialogRef, - useValue: null - } + { provide: MatDialogRef, useValue: null } ] }); fixture = TestBed.createComponent(CreateProcessGroup); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/create-processor/create-processor.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/create-processor/create-processor.component.spec.ts index 22e5dba3fa..c03b06d5a8 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/create-processor/create-processor.component.spec.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/create-processor/create-processor.component.spec.ts @@ -63,10 +63,7 @@ describe('CreateProcessor', () => { providers: [ { provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState }), - { - provide: MatDialogRef, - useValue: null - } + { provide: MatDialogRef, useValue: null } ] }); fixture = TestBed.createComponent(CreateProcessor); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.html b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.html index 4d8474f35d..0626ac6d79 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.html +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.html @@ -236,8 +236,9 @@ -
+
+
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts index c537951249..68385e5c3a 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts @@ -25,7 +25,7 @@ import { AsyncPipe } from '@angular/common'; import { MatTabsModule } from '@angular/material/tabs'; import { MatOptionModule } from '@angular/material/core'; import { MatSelectModule } from '@angular/material/select'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { InlineServiceCreationRequest, InlineServiceCreationResponse, @@ -50,6 +50,12 @@ import { ClusterConnectionService } from '../../../../../../../service/cluster-c import { CanvasUtils } from '../../../../../service/canvas-utils.service'; import { ConvertToParameterResponse } from '../../../../../service/parameter-helper.service'; import { CloseOnEscapeDialog } from '../../../../../../../ui/common/close-on-escape-dialog/close-on-escape-dialog.component'; +import { PropertyVerification } from '../../../../../../../ui/common/property-verification/property-verification.component'; +import { + ConfigVerificationResult, + ModifiedProperties, + VerifyPropertiesRequestContext +} from '../../../../../../../state/property-verification'; @Component({ selector: 'edit-processor', @@ -70,7 +76,8 @@ import { CloseOnEscapeDialog } from '../../../../../../../ui/common/close-on-esc NifiTooltipDirective, RunDurationSlider, RelationshipSettings, - ErrorBanner + ErrorBanner, + PropertyVerification ], styleUrls: ['./edit-processor.component.scss'] }) @@ -86,6 +93,11 @@ export class EditProcessor extends CloseOnEscapeDialog { ) => Observable; @Input() goToService!: (serviceId: string) => void; @Input() saving$!: Observable; + + @Input() propertyVerificationResults$!: Observable; + @Input() propertyVerificationStatus$: Observable<'pending' | 'loading' | 'success'> = of('pending'); + + @Output() verify: EventEmitter = new EventEmitter(); @Output() editProcessor: EventEmitter = new EventEmitter(); protected readonly TextTip = TextTip; @@ -321,9 +333,7 @@ export class EditProcessor extends CloseOnEscapeDialog { const propertyControl: AbstractControl | null = this.editProcessorForm.get('properties'); if (propertyControl && propertyControl.dirty) { const properties: Property[] = propertyControl.value; - const values: { [key: string]: string | null } = {}; - properties.forEach((property) => (values[property.property] = property.value)); - payload.component.config.properties = values; + payload.component.config.properties = this.getModifiedProperties(); payload.component.config.sensitiveDynamicPropertyNames = properties .filter((property) => property.descriptor.dynamic && property.descriptor.sensitive) .map((property) => property.descriptor.name); @@ -345,7 +355,25 @@ export class EditProcessor extends CloseOnEscapeDialog { }); } + private getModifiedProperties(): ModifiedProperties { + const propertyControl: AbstractControl | null = this.editProcessorForm.get('properties'); + if (propertyControl && propertyControl.dirty) { + const properties: Property[] = propertyControl.value; + const values: { [key: string]: string | null } = {}; + properties.forEach((property) => (values[property.property] = property.value)); + return values; + } + return {}; + } + override isDirty(): boolean { return this.editProcessorForm.dirty; } + + verifyClicked(entity: any): void { + this.verify.next({ + entity, + properties: this.getModifiedProperties() + }); + } } diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/remote-process-group/create-remote-process-group/create-remote-process-group.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/remote-process-group/create-remote-process-group/create-remote-process-group.component.spec.ts index 55420fe53c..59f6d62c0f 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/remote-process-group/create-remote-process-group/create-remote-process-group.component.spec.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/remote-process-group/create-remote-process-group/create-remote-process-group.component.spec.ts @@ -47,10 +47,7 @@ describe('CreateRemoteProcessGroup', () => { providers: [ { provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState }), - { - provide: MatDialogRef, - useValue: null - } + { provide: MatDialogRef, useValue: null } ] }); fixture = TestBed.createComponent(CreateRemoteProcessGroup); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.effects.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.effects.ts index 84c8fc9a00..0f4fc9c793 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.effects.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.effects.ts @@ -38,9 +38,18 @@ import * as ErrorActions from '../../../../state/error/error.actions'; import { ErrorHelper } from '../../../../service/error-helper.service'; import { selectStatus } from './flow-analysis-rules.selectors'; import { HttpErrorResponse } from '@angular/common/http'; -import { LARGE_DIALOG, SMALL_DIALOG } from '../../../../index'; +import { LARGE_DIALOG, SMALL_DIALOG, XL_DIALOG } from '../../../../index'; import { ChangeComponentVersionDialog } from '../../../../ui/common/change-component-version-dialog/change-component-version-dialog'; import { ExtensionTypesService } from '../../../../service/extension-types.service'; +import { + resetPropertyVerificationState, + verifyProperties +} from '../../../../state/property-verification/property-verification.actions'; +import { + selectPropertyVerificationResults, + selectPropertyVerificationStatus +} from '../../../../state/property-verification/property-verification.selectors'; +import { VerifyPropertiesRequestContext } from '../../../../state/property-verification'; @Injectable() export class FlowAnalysisRulesEffects { @@ -254,7 +263,7 @@ export class FlowAnalysisRulesEffects { const ruleId: string = request.id; const editDialogReference = this.dialog.open(EditFlowAnalysisRule, { - ...LARGE_DIALOG, + ...XL_DIALOG, data: request, id: ruleId }); @@ -264,6 +273,23 @@ export class FlowAnalysisRulesEffects { editDialogReference.componentInstance.createNewProperty = this.propertyTableHelperService.createNewProperty(request.id, this.flowAnalysisRuleService); + editDialogReference.componentInstance.verify + .pipe(takeUntil(editDialogReference.afterClosed())) + .subscribe((verificationRequest: VerifyPropertiesRequestContext) => { + this.store.dispatch( + verifyProperties({ + request: verificationRequest + }) + ); + }); + + editDialogReference.componentInstance.propertyVerificationResults$ = this.store.select( + selectPropertyVerificationResults + ); + editDialogReference.componentInstance.propertyVerificationStatus$ = this.store.select( + selectPropertyVerificationStatus + ); + const goTo = (commands: string[], destination: string): void => { if (editDialogReference.componentInstance.editFlowAnalysisRuleForm.dirty) { const saveChangesDialogReference = this.dialog.open(YesNoDialog, { @@ -317,6 +343,7 @@ export class FlowAnalysisRulesEffects { editDialogReference.afterClosed().subscribe((response) => { this.store.dispatch(ErrorActions.clearBannerErrors()); + this.store.dispatch(resetPropertyVerificationState()); if (response != 'ROUTED') { this.store.dispatch( diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.effects.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.effects.ts index 03d385f8d9..51d60852e4 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.effects.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/management-controller-services/management-controller-services.effects.ts @@ -47,6 +47,15 @@ import { ErrorHelper } from '../../../../service/error-helper.service'; import { LARGE_DIALOG, SMALL_DIALOG, XL_DIALOG } from '../../../../index'; import { ChangeComponentVersionDialog } from '../../../../ui/common/change-component-version-dialog/change-component-version-dialog'; import { ExtensionTypesService } from '../../../../service/extension-types.service'; +import { + resetPropertyVerificationState, + verifyProperties +} from '../../../../state/property-verification/property-verification.actions'; +import { + selectPropertyVerificationResults, + selectPropertyVerificationStatus +} from '../../../../state/property-verification/property-verification.selectors'; +import { VerifyPropertiesRequestContext } from '../../../../state/property-verification'; @Injectable() export class ManagementControllerServicesEffects { @@ -98,7 +107,6 @@ export class ManagementControllerServicesEffects { }); dialogReference.componentInstance.saving$ = this.store.select(selectSaving); - dialogReference.componentInstance.createControllerService .pipe(take(1)) .subscribe((controllerServiceType) => { @@ -224,12 +232,13 @@ export class ManagementControllerServicesEffects { const serviceId: string = request.id; const editDialogReference = this.dialog.open(EditControllerService, { - ...LARGE_DIALOG, + ...XL_DIALOG, data: request, id: serviceId }); editDialogReference.componentInstance.saving$ = this.store.select(selectSaving); + editDialogReference.componentInstance.supportsParameters = false; editDialogReference.componentInstance.createNewProperty = this.propertyTableHelperService.createNewProperty( @@ -237,6 +246,23 @@ export class ManagementControllerServicesEffects { this.managementControllerServiceService ); + editDialogReference.componentInstance.verify + .pipe(takeUntil(editDialogReference.afterClosed())) + .subscribe((verificationRequest: VerifyPropertiesRequestContext) => { + this.store.dispatch( + verifyProperties({ + request: verificationRequest + }) + ); + }); + + editDialogReference.componentInstance.propertyVerificationResults$ = this.store.select( + selectPropertyVerificationResults + ); + editDialogReference.componentInstance.propertyVerificationStatus$ = this.store.select( + selectPropertyVerificationStatus + ); + const goTo = (commands: string[], destination: string): void => { if (editDialogReference.componentInstance.editControllerServiceForm.dirty) { const saveChangesDialogReference = this.dialog.open(YesNoDialog, { @@ -306,6 +332,7 @@ export class ManagementControllerServicesEffects { editDialogReference.afterClosed().subscribe((response) => { this.store.dispatch(ErrorActions.clearBannerErrors()); + this.store.dispatch(resetPropertyVerificationState()); if (response != 'ROUTED') { this.store.dispatch( diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.effects.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.effects.ts index 34fd96b2f3..bf8e45d244 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.effects.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/parameter-providers/parameter-providers.effects.ts @@ -57,6 +57,15 @@ import * as ErrorActions from '../../../../state/error/error.actions'; import { HttpErrorResponse } from '@angular/common/http'; import { ErrorHelper } from '../../../../service/error-helper.service'; import { LARGE_DIALOG, SMALL_DIALOG, XL_DIALOG } from '../../../../index'; +import { + resetPropertyVerificationState, + verifyProperties +} from '../../../../state/property-verification/property-verification.actions'; +import { + selectPropertyVerificationResults, + selectPropertyVerificationStatus +} from '../../../../state/property-verification/property-verification.selectors'; +import { VerifyPropertiesRequestContext } from '../../../../state/property-verification'; @Injectable() export class ParameterProvidersEffects { @@ -300,13 +309,30 @@ export class ParameterProvidersEffects { tap((request) => { const id = request.id; const editDialogReference = this.dialog.open(EditParameterProvider, { - ...LARGE_DIALOG, + ...XL_DIALOG, data: request, id }); editDialogReference.componentInstance.saving$ = this.store.select(selectSaving); + editDialogReference.componentInstance.verify + .pipe(takeUntil(editDialogReference.afterClosed())) + .subscribe((verificationRequest: VerifyPropertiesRequestContext) => { + this.store.dispatch( + verifyProperties({ + request: verificationRequest + }) + ); + }); + + editDialogReference.componentInstance.propertyVerificationResults$ = this.store.select( + selectPropertyVerificationResults + ); + editDialogReference.componentInstance.propertyVerificationStatus$ = this.store.select( + selectPropertyVerificationStatus + ); + const goTo = (commands: string[], destination: string) => { // confirm navigating away while changes are unsaved if (editDialogReference.componentInstance.editParameterProviderForm.dirty) { @@ -369,6 +395,7 @@ export class ParameterProvidersEffects { editDialogReference.afterClosed().subscribe((response) => { this.store.dispatch(ErrorActions.clearBannerErrors()); + this.store.dispatch(resetPropertyVerificationState()); if (response !== 'ROUTED') { this.store.dispatch( diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/reporting-tasks/reporting-tasks.effects.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/reporting-tasks/reporting-tasks.effects.ts index e770f6aa6d..6f78c90ea2 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/reporting-tasks/reporting-tasks.effects.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/reporting-tasks/reporting-tasks.effects.ts @@ -38,9 +38,18 @@ import * as ErrorActions from '../../../../state/error/error.actions'; import { ErrorHelper } from '../../../../service/error-helper.service'; import { selectStatus } from './reporting-tasks.selectors'; import { HttpErrorResponse } from '@angular/common/http'; -import { LARGE_DIALOG, SMALL_DIALOG } from '../../../../index'; +import { LARGE_DIALOG, SMALL_DIALOG, XL_DIALOG } from '../../../../index'; import { ChangeComponentVersionDialog } from '../../../../ui/common/change-component-version-dialog/change-component-version-dialog'; import { ExtensionTypesService } from '../../../../service/extension-types.service'; +import { + resetPropertyVerificationState, + verifyProperties +} from '../../../../state/property-verification/property-verification.actions'; +import { + selectPropertyVerificationResults, + selectPropertyVerificationStatus +} from '../../../../state/property-verification/property-verification.selectors'; +import { VerifyPropertiesRequestContext } from '../../../../state/property-verification'; @Injectable() export class ReportingTasksEffects { @@ -266,7 +275,7 @@ export class ReportingTasksEffects { const taskId: string = request.id; const editDialogReference = this.dialog.open(EditReportingTask, { - ...LARGE_DIALOG, + ...XL_DIALOG, data: request, id: taskId }); @@ -276,6 +285,23 @@ export class ReportingTasksEffects { editDialogReference.componentInstance.createNewProperty = this.propertyTableHelperService.createNewProperty(request.id, this.reportingTaskService); + editDialogReference.componentInstance.verify + .pipe(takeUntil(editDialogReference.afterClosed())) + .subscribe((verificationRequest: VerifyPropertiesRequestContext) => { + this.store.dispatch( + verifyProperties({ + request: verificationRequest + }) + ); + }); + + editDialogReference.componentInstance.propertyVerificationResults$ = this.store.select( + selectPropertyVerificationResults + ); + editDialogReference.componentInstance.propertyVerificationStatus$ = this.store.select( + selectPropertyVerificationStatus + ); + const goTo = (commands: string[], destination: string): void => { if (editDialogReference.componentInstance.editReportingTaskForm.dirty) { const saveChangesDialogReference = this.dialog.open(YesNoDialog, { @@ -329,6 +355,7 @@ export class ReportingTasksEffects { editDialogReference.afterClosed().subscribe((response) => { this.store.dispatch(ErrorActions.clearBannerErrors()); + this.store.dispatch(resetPropertyVerificationState()); if (response != 'ROUTED') { this.store.dispatch( diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/edit-flow-analysis-rule/edit-flow-analysis-rule.component.html b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/edit-flow-analysis-rule/edit-flow-analysis-rule.component.html index 6a5e2306a2..b6728423bc 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/edit-flow-analysis-rule/edit-flow-analysis-rule.component.html +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/edit-flow-analysis-rule/edit-flow-analysis-rule.component.html @@ -87,14 +87,22 @@ -
+
+
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/edit-flow-analysis-rule/edit-flow-analysis-rule.component.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/edit-flow-analysis-rule/edit-flow-analysis-rule.component.ts index a16f14eee1..8066002560 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/edit-flow-analysis-rule/edit-flow-analysis-rule.component.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/edit-flow-analysis-rule/edit-flow-analysis-rule.component.ts @@ -25,7 +25,7 @@ import { MatTabsModule } from '@angular/material/tabs'; import { MatOptionModule } from '@angular/material/core'; import { MatSelectModule } from '@angular/material/select'; import { AbstractControl, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { Client } from '../../../../../service/client.service'; import { InlineServiceCreationRequest, @@ -47,6 +47,12 @@ import { FlowAnalysisRuleTable } from '../flow-analysis-rule-table/flow-analysis import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component'; import { ClusterConnectionService } from '../../../../../service/cluster-connection.service'; import { CloseOnEscapeDialog } from '../../../../../ui/common/close-on-escape-dialog/close-on-escape-dialog.component'; +import { + ConfigVerificationResult, + ModifiedProperties, + VerifyPropertiesRequestContext +} from '../../../../../state/property-verification'; +import { PropertyVerification } from '../../../../../ui/common/property-verification/property-verification.component'; @Component({ selector: 'edit-flow-analysis-rule', @@ -66,7 +72,8 @@ import { CloseOnEscapeDialog } from '../../../../../ui/common/close-on-escape-di MatTooltipModule, NifiTooltipDirective, FlowAnalysisRuleTable, - ErrorBanner + ErrorBanner, + PropertyVerification ], styleUrls: ['./edit-flow-analysis-rule.component.scss'] }) @@ -75,6 +82,10 @@ export class EditFlowAnalysisRule extends CloseOnEscapeDialog { @Input() createNewService!: (request: InlineServiceCreationRequest) => Observable; @Input() saving$!: Observable; @Input() goToService!: (serviceId: string) => void; + @Input() propertyVerificationResults$!: Observable; + @Input() propertyVerificationStatus$: Observable<'pending' | 'loading' | 'success'> = of('pending'); + + @Output() verify: EventEmitter = new EventEmitter(); @Output() editFlowAnalysisRule: EventEmitter = new EventEmitter(); @@ -144,9 +155,7 @@ export class EditFlowAnalysisRule extends CloseOnEscapeDialog { const propertyControl: AbstractControl | null = this.editFlowAnalysisRuleForm.get('properties'); if (propertyControl && propertyControl.dirty) { const properties: Property[] = propertyControl.value; - const values: { [key: string]: string | null } = {}; - properties.forEach((property) => (values[property.property] = property.value)); - payload.component.properties = values; + payload.component.properties = this.getModifiedProperties(); payload.component.sensitiveDynamicPropertyNames = properties .filter((property) => property.descriptor.dynamic && property.descriptor.sensitive) .map((property) => property.descriptor.name); @@ -160,7 +169,25 @@ export class EditFlowAnalysisRule extends CloseOnEscapeDialog { protected readonly TextTip = TextTip; + private getModifiedProperties(): ModifiedProperties { + const propertyControl: AbstractControl | null = this.editFlowAnalysisRuleForm.get('properties'); + if (propertyControl && propertyControl.dirty) { + const properties: Property[] = propertyControl.value; + const values: { [key: string]: string | null } = {}; + properties.forEach((property) => (values[property.property] = property.value)); + return values; + } + return {}; + } + override isDirty(): boolean { return this.editFlowAnalysisRuleForm.dirty; } + + verifyClicked(entity: FlowAnalysisRuleEntity): void { + this.verify.next({ + entity, + properties: this.getModifiedProperties() + }); + } } diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/parameter-providers/edit-parameter-provider/edit-parameter-provider.component.html b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/parameter-providers/edit-parameter-provider/edit-parameter-provider.component.html index a9876a97ec..b6b5d5b50c 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/parameter-providers/edit-parameter-provider/edit-parameter-provider.component.html +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/parameter-providers/edit-parameter-provider/edit-parameter-provider.component.html @@ -81,13 +81,21 @@ -
+
+
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/parameter-providers/edit-parameter-provider/edit-parameter-provider.component.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/parameter-providers/edit-parameter-provider/edit-parameter-provider.component.ts index 9905d791ad..fddedbf4d3 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/parameter-providers/edit-parameter-provider/edit-parameter-provider.component.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/parameter-providers/edit-parameter-provider/edit-parameter-provider.component.ts @@ -21,7 +21,7 @@ import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; import { MatTabsModule } from '@angular/material/tabs'; import { MatButtonModule } from '@angular/material/button'; import { NifiSpinnerDirective } from '../../../../../ui/common/spinner/nifi-spinner.directive'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { InlineServiceCreationRequest, InlineServiceCreationResponse, @@ -47,6 +47,12 @@ import { ClusterConnectionService } from '../../../../../service/cluster-connect import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component'; import { NifiTooltipDirective } from '../../../../../ui/common/tooltips/nifi-tooltip.directive'; import { CloseOnEscapeDialog } from '../../../../../ui/common/close-on-escape-dialog/close-on-escape-dialog.component'; +import { + ConfigVerificationResult, + ModifiedProperties, + VerifyPropertiesRequestContext +} from '../../../../../state/property-verification'; +import { PropertyVerification } from '../../../../../ui/common/property-verification/property-verification.component'; @Component({ selector: 'edit-parameter-provider', @@ -64,7 +70,8 @@ import { CloseOnEscapeDialog } from '../../../../../ui/common/close-on-escape-di PropertyTable, ErrorBanner, CommonModule, - NifiTooltipDirective + NifiTooltipDirective, + PropertyVerification ], templateUrl: './edit-parameter-provider.component.html', styleUrls: ['./edit-parameter-provider.component.scss'] @@ -75,7 +82,10 @@ export class EditParameterProvider extends CloseOnEscapeDialog { @Input() goToService!: (serviceId: string) => void; @Input() goToReferencingParameterContext!: (parameterContextId: string) => void; @Input() saving$!: Observable; + @Input() propertyVerificationResults$!: Observable; + @Input() propertyVerificationStatus$: Observable<'pending' | 'loading' | 'success'> = of('pending'); + @Output() verify: EventEmitter = new EventEmitter(); @Output() editParameterProvider: EventEmitter = new EventEmitter(); @@ -132,9 +142,7 @@ export class EditParameterProvider extends CloseOnEscapeDialog { const propertyControl: AbstractControl | null = this.editParameterProviderForm.get('properties'); if (propertyControl && propertyControl.dirty) { const properties: Property[] = propertyControl.value; - const values: { [key: string]: string | null } = {}; - properties.forEach((property) => (values[property.property] = property.value)); - payload.component.properties = values; + payload.component.properties = this.getModifiedProperties(); payload.component.sensitiveDynamicPropertyNames = properties .filter((property) => property.descriptor.dynamic && property.descriptor.sensitive) .map((property) => property.descriptor.name); @@ -154,7 +162,25 @@ export class EditParameterProvider extends CloseOnEscapeDialog { protected readonly TextTip = TextTip; + private getModifiedProperties(): ModifiedProperties { + const propertyControl: AbstractControl | null = this.editParameterProviderForm.get('properties'); + if (propertyControl && propertyControl.dirty) { + const properties: Property[] = propertyControl.value; + const values: { [key: string]: string | null } = {}; + properties.forEach((property) => (values[property.property] = property.value)); + return values; + } + return {}; + } + override isDirty(): boolean { return this.editParameterProviderForm.dirty; } + + verifyClicked(entity: ParameterProviderEntity): void { + this.verify.next({ + entity, + properties: this.getModifiedProperties() + }); + } } diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/registry-clients/edit-registry-client/edit-registry-client.component.html b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/registry-clients/edit-registry-client/edit-registry-client.component.html index 0ac905fdb4..95bf78f38f 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/registry-clients/edit-registry-client/edit-registry-client.component.html +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/registry-clients/edit-registry-client/edit-registry-client.component.html @@ -64,6 +64,7 @@ [createNewProperty]="createNewProperty" [createNewService]="createNewService" [goToService]="goToService" + [supportsParameters]="false" [supportsSensitiveDynamicProperties]=" request.registryClient.component.supportsSensitiveDynamicProperties "> diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/edit-reporting-task/edit-reporting-task.component.html b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/edit-reporting-task/edit-reporting-task.component.html index 7bbe0f07b4..dc1ad19776 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/edit-reporting-task/edit-reporting-task.component.html +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/edit-reporting-task/edit-reporting-task.component.html @@ -112,17 +112,25 @@ -
+
+
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/edit-reporting-task/edit-reporting-task.component.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/edit-reporting-task/edit-reporting-task.component.ts index b204bb9ed7..273e41abb0 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/edit-reporting-task/edit-reporting-task.component.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/reporting-tasks/edit-reporting-task/edit-reporting-task.component.ts @@ -26,7 +26,7 @@ import { MatTabsModule } from '@angular/material/tabs'; import { MatOptionModule } from '@angular/material/core'; import { MatSelectModule } from '@angular/material/select'; import { AbstractControl, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { Client } from '../../../../../service/client.service'; import { ControllerServiceReferencingComponent, @@ -49,6 +49,12 @@ import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.com import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component'; import { ClusterConnectionService } from '../../../../../service/cluster-connection.service'; import { CloseOnEscapeDialog } from '../../../../../ui/common/close-on-escape-dialog/close-on-escape-dialog.component'; +import { + ConfigVerificationResult, + ModifiedProperties, + VerifyPropertiesRequestContext +} from '../../../../../state/property-verification'; +import { PropertyVerification } from '../../../../../ui/common/property-verification/property-verification.component'; @Component({ selector: 'edit-reporting-task', @@ -69,7 +75,8 @@ import { CloseOnEscapeDialog } from '../../../../../ui/common/close-on-escape-di NifiSpinnerDirective, MatTooltipModule, NifiTooltipDirective, - ErrorBanner + ErrorBanner, + PropertyVerification ], styleUrls: ['./edit-reporting-task.component.scss'] }) @@ -79,6 +86,10 @@ export class EditReportingTask extends CloseOnEscapeDialog { @Input() goToService!: (serviceId: string) => void; @Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void; @Input() saving$!: Observable; + @Input() propertyVerificationResults$!: Observable; + @Input() propertyVerificationStatus$: Observable<'pending' | 'loading' | 'success'> = of('pending'); + + @Output() verify: EventEmitter = new EventEmitter(); @Output() editReportingTask: EventEmitter = new EventEmitter(); @@ -184,9 +195,7 @@ export class EditReportingTask extends CloseOnEscapeDialog { const propertyControl: AbstractControl | null = this.editReportingTaskForm.get('properties'); if (propertyControl && propertyControl.dirty) { const properties: Property[] = propertyControl.value; - const values: { [key: string]: string | null } = {}; - properties.forEach((property) => (values[property.property] = property.value)); - payload.component.properties = values; + payload.component.properties = this.getModifiedProperties(); payload.component.sensitiveDynamicPropertyNames = properties .filter((property) => property.descriptor.dynamic && property.descriptor.sensitive) .map((property) => property.descriptor.name); @@ -221,4 +230,22 @@ export class EditReportingTask extends CloseOnEscapeDialog { override isDirty(): boolean { return this.editReportingTaskForm.dirty; } + + private getModifiedProperties(): ModifiedProperties { + const propertyControl: AbstractControl | null = this.editReportingTaskForm.get('properties'); + if (propertyControl && propertyControl.dirty) { + const properties: Property[] = propertyControl.value; + const values: { [key: string]: string | null } = {}; + properties.forEach((property) => (values[property.property] = property.value)); + return values; + } + return {}; + } + + verifyClicked(entity: ReportingTaskEntity): void { + this.verify.next({ + entity, + properties: this.getModifiedProperties() + }); + } } diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/users/ui/user-listing/user-access-policies/user-access-policies.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/users/ui/user-listing/user-access-policies/user-access-policies.component.spec.ts index 52e780f036..5fcba7ad63 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/users/ui/user-listing/user-access-policies/user-access-policies.component.spec.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/users/ui/user-listing/user-access-policies/user-access-policies.component.spec.ts @@ -55,10 +55,7 @@ describe('UserAccessPolicies', () => { imports: [UserAccessPolicies, NoopAnimationsModule], providers: [ { provide: MAT_DIALOG_DATA, useValue: data }, - { - provide: MatDialogRef, - useValue: null - } + { provide: MatDialogRef, useValue: null } ] }); fixture = TestBed.createComponent(UserAccessPolicies); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/map-table-helper.service.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/map-table-helper.service.ts new file mode 100644 index 0000000000..36f759b0f8 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/map-table-helper.service.ts @@ -0,0 +1,61 @@ +/* + * 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 { Injectable } from '@angular/core'; +import { MapTableEntry } from '../state/shared'; +import { Observable, of, switchMap, takeUntil } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; +import { SMALL_DIALOG } from '../index'; +import { + MapTableEntryData, + NewMapTableEntryDialog +} from '../ui/common/new-map-table-entry-dialog/new-map-table-entry-dialog.component'; + +@Injectable({ + providedIn: 'root' +}) +export class MapTableHelperService { + constructor(private dialog: MatDialog) {} + + /** + * Returns a function that can be used to pass into a MapTable to support creating a new entry. + * + * @param entryType string representing the type of Map Table Entry + */ + createNewEntry(entryType: string): (existingEntries: string[]) => Observable { + return (existingEntries: string[]) => { + const dialogRef = this.dialog.open(NewMapTableEntryDialog, { + ...SMALL_DIALOG, + data: { + existingEntries, + entryTypeLabel: entryType + } as MapTableEntryData + }); + return dialogRef.componentInstance.newEntry.pipe( + takeUntil(dialogRef.afterClosed()), + switchMap((name: string) => { + dialogRef.close(); + const newEntry: MapTableEntry = { + name, + value: null + }; + return of(newEntry); + }) + ); + }; + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/property-verification.service.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/property-verification.service.ts new file mode 100644 index 0000000000..51e07224c7 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/property-verification.service.ts @@ -0,0 +1,71 @@ +/* + * 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 { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Client } from './client.service'; +import { + ConfigurationAnalysisResponse, + InitiateVerificationRequest, + PropertyVerificationResponse, + VerifyPropertiesRequestContext +} from '../state/property-verification'; +import { Observable } from 'rxjs'; +import { NiFiCommon } from './nifi-common.service'; + +@Injectable({ providedIn: 'root' }) +export class PropertyVerificationService { + private static readonly API: string = '../nifi-api'; + + constructor( + private httpClient: HttpClient, + private client: Client, + private nifiCommon: NiFiCommon + ) {} + + getAnalysis(request: VerifyPropertiesRequestContext): Observable { + const body = { + configurationAnalysis: { + componentId: request.entity.id, + properties: request.properties + } + }; + return this.httpClient.post( + `${this.nifiCommon.stripProtocol(request.entity.uri)}/config/analysis`, + body + ) as Observable; + } + + initiatePropertyVerification(request: InitiateVerificationRequest): Observable { + return this.httpClient.post( + `${this.nifiCommon.stripProtocol(request.uri)}/config/verification-requests`, + request.request + ) as Observable; + } + + getPropertyVerificationRequest(requestId: string, uri: string): Observable { + return this.httpClient.get( + `${this.nifiCommon.stripProtocol(uri)}/config/verification-requests/${requestId}` + ) as Observable; + } + + deletePropertyVerificationRequest(requestId: string, uri: string) { + return this.httpClient.delete( + `${this.nifiCommon.stripProtocol(uri)}/config/verification-requests/${requestId}` + ); + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts index 7f2aee6f27..d0ac31c01a 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts @@ -41,6 +41,8 @@ import { clusterSummaryFeatureKey, ClusterSummaryState } from './cluster-summary import { clusterSummaryReducer } from './cluster-summary/cluster-summary.reducer'; import { loginConfigurationFeatureKey, LoginConfigurationState } from './login-configuration'; import { loginConfigurationReducer } from './login-configuration/login-configuration.reducer'; +import { propertyVerificationFeatureKey, PropertyVerificationState } from './property-verification'; +import { propertyVerificationReducer } from './property-verification/property-verification.reducer'; export interface NiFiState { [DEFAULT_ROUTER_FEATURENAME]: RouterReducerState; @@ -56,6 +58,7 @@ export interface NiFiState { [componentStateFeatureKey]: ComponentStateState; [documentationFeatureKey]: DocumentationState; [clusterSummaryFeatureKey]: ClusterSummaryState; + [propertyVerificationFeatureKey]: PropertyVerificationState; } export const rootReducers: ActionReducerMap = { @@ -71,5 +74,6 @@ export const rootReducers: ActionReducerMap = { [systemDiagnosticsFeatureKey]: systemDiagnosticsReducer, [componentStateFeatureKey]: componentStateReducer, [documentationFeatureKey]: documentationReducer, - [clusterSummaryFeatureKey]: clusterSummaryReducer + [clusterSummaryFeatureKey]: clusterSummaryReducer, + [propertyVerificationFeatureKey]: propertyVerificationReducer }; diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/property-verification/index.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/property-verification/index.ts new file mode 100644 index 0000000000..c400265653 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/property-verification/index.ts @@ -0,0 +1,93 @@ +/* + * 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. + */ + +export const propertyVerificationFeatureKey = 'propertyVerification'; + +export enum Outcome { + SUCCESSFUL = 'SUCCESSFUL', + FAILED = 'FAILED', + SKIPPED = 'SKIPPED' +} + +export interface ModifiedProperties { + [key: string]: string | null; +} + +export interface VerifyPropertiesRequestContext { + entity: any; + properties: ModifiedProperties; +} + +export interface ConfigurationAnalysisResponse { + requestContext: VerifyPropertiesRequestContext; + configurationAnalysis: ConfigurationAnalysis; +} + +export interface ConfigurationAnalysis { + componentId: string; + properties: any; + referencedAttributes: any; + supportsVerification: boolean; +} + +export interface ConfigVerificationResult { + outcome: Outcome; + verificationStepName: string; + explanation: string; +} + +export interface PropertyVerificationRequest { + complete: boolean; + componentId: string; + lastUpdated: string; + percentCompleted: number; + properties: any; + requestId: string; + uri: string; + state: string; + results?: ConfigVerificationResult[]; + failureReason?: string; +} + +export interface PropertyVerificationResponse { + request: PropertyVerificationRequest; +} + +export interface VerifyConfigRequest { + componentId: string; + properties: any; + attributes: any; + results?: ConfigVerificationResult[]; +} + +export interface VerifyConfigRequestEntity { + request: VerifyConfigRequest; +} + +export interface InitiateVerificationRequest { + uri: string; + request: VerifyConfigRequestEntity; +} + +export interface PropertyVerificationState { + activeRequest: PropertyVerificationRequest | null; + requestContext: VerifyPropertiesRequestContext | null; + configurationAnalysis: ConfigurationAnalysis | null; + results: ConfigVerificationResult[]; + attributes: { [key: string]: string } | null; + status: 'pending' | 'loading' | 'success'; +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/property-verification/property-verification.actions.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/property-verification/property-verification.actions.ts new file mode 100644 index 0000000000..540efe54ba --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/property-verification/property-verification.actions.ts @@ -0,0 +1,69 @@ +/* + * 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 { createAction, props } from '@ngrx/store'; +import { ConfigurationAnalysisResponse, PropertyVerificationResponse, VerifyPropertiesRequestContext } from './index'; + +const VERIFICATION_STATE_PREFIX = '[Property Verification State]'; + +export const verifyProperties = createAction( + `${VERIFICATION_STATE_PREFIX} Verify Properties`, + props<{ request: VerifyPropertiesRequestContext }>() +); + +export const getConfigurationAnalysisSuccess = createAction( + `${VERIFICATION_STATE_PREFIX} Get Configuration Analysis`, + props<{ response: ConfigurationAnalysisResponse }>() +); + +export const startPollingPropertyVerification = createAction( + `${VERIFICATION_STATE_PREFIX} Start Polling Property Verification` +); + +export const pollPropertyVerification = createAction(`${VERIFICATION_STATE_PREFIX} Poll Property Verification`); + +export const pollPropertyVerificationSuccess = createAction( + `${VERIFICATION_STATE_PREFIX} Poll Property Verification Success`, + props<{ response: PropertyVerificationResponse }>() +); + +export const stopPollingPropertyVerification = createAction( + `${VERIFICATION_STATE_PREFIX} Stop Polling Property Verification` +); + +export const propertyVerificationComplete = createAction(`${VERIFICATION_STATE_PREFIX} Property Verification Complete`); + +export const verifyPropertiesSuccess = createAction( + `${VERIFICATION_STATE_PREFIX} Verify Properties Success`, + props<{ response: PropertyVerificationResponse }>() +); + +export const verifyPropertiesComplete = createAction( + `${VERIFICATION_STATE_PREFIX} Verify Properties Complete`, + props<{ response: PropertyVerificationResponse }>() +); + +export const openPropertyVerificationProgressDialog = createAction( + `${VERIFICATION_STATE_PREFIX} Open Property Verification Progress` +); + +export const resetPropertyVerificationState = createAction(`${VERIFICATION_STATE_PREFIX} Reset`); + +export const initiatePropertyVerification = createAction( + `${VERIFICATION_STATE_PREFIX} Initiate Property Verification`, + props<{ response: ConfigurationAnalysisResponse }>() +); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/property-verification/property-verification.effects.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/property-verification/property-verification.effects.ts new file mode 100644 index 0000000000..0d7ecb197c --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/property-verification/property-verification.effects.ts @@ -0,0 +1,294 @@ +/* + * 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 { Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { ConfigurationAnalysisResponse, PropertyVerificationState } from './index'; +import { Store } from '@ngrx/store'; +import { MatDialog } from '@angular/material/dialog'; +import { ErrorHelper } from '../../service/error-helper.service'; +import * as VerificationActions from './property-verification.actions'; +import * as ErrorActions from '../error/error.actions'; +import { asyncScheduler, catchError, filter, from, interval, map, of, switchMap, take, takeUntil, tap } from 'rxjs'; +import { PropertyVerificationService } from '../../service/property-verification.service'; +import { HttpErrorResponse } from '@angular/common/http'; +import { concatLatestFrom } from '@ngrx/operators'; +import { isDefinedAndNotNull, MapTableEntry } from '../shared'; +import { + selectActivePropertyVerificationRequest, + selectPropertyVerificationAttributes, + selectPropertyVerificationRequestContext +} from './property-verification.selectors'; +import { PropertyVerificationProgress } from '../../ui/common/property-verification/common/property-verification-progress/property-verification-progress.component'; +import { MEDIUM_DIALOG, SMALL_DIALOG } from '../../index'; +import { ReferencedAttributesDialog } from '../../ui/common/property-verification/common/referenced-attributes-dialog/referenced-attributes-dialog.component'; +import { PropertyTableHelperService } from '../../service/property-table-helper.service'; +import { MapTableHelperService } from '../../service/map-table-helper.service'; + +@Injectable() +export class PropertyVerificationEffects { + constructor( + private actions$: Actions, + private store: Store, + private dialog: MatDialog, + private errorHelper: ErrorHelper, + private propertyVerificationService: PropertyVerificationService, + private propertyTableHelperService: PropertyTableHelperService, + private mapTableHelperService: MapTableHelperService + ) {} + + verifyProperties$ = createEffect(() => + this.actions$.pipe( + ofType(VerificationActions.verifyProperties), + map((action) => action.request), + switchMap((request) => { + // get the configuration analysis + return from(this.propertyVerificationService.getAnalysis(request)).pipe( + map((response) => + VerificationActions.getConfigurationAnalysisSuccess({ + response: { + requestContext: request, + configurationAnalysis: response.configurationAnalysis + } + }) + ), + catchError((errorResponse: HttpErrorResponse) => + of(ErrorActions.snackBarError({ error: this.errorHelper.getErrorString(errorResponse) })) + ) + ); + }) + ) + ); + + getConfigurationAnalysisSuccess_noExtraVerification$ = createEffect(() => + this.actions$.pipe( + ofType(VerificationActions.getConfigurationAnalysisSuccess), + map((action) => action.response), + filter((response) => !response.configurationAnalysis.supportsVerification), + switchMap((response) => { + return of(VerificationActions.initiatePropertyVerification({ response })); + }) + ) + ); + + getConfigurationAnalysisSuccess_extraVerification$ = createEffect( + () => + this.actions$.pipe( + ofType(VerificationActions.getConfigurationAnalysisSuccess), + map((action) => action.response), + filter((response) => response.configurationAnalysis.supportsVerification), + concatLatestFrom(() => this.store.select(selectPropertyVerificationAttributes)), + tap(([response, previousAttributes]) => { + let referencedAttributes: MapTableEntry[] = []; + if (previousAttributes) { + referencedAttributes = Object.entries(previousAttributes).map(([key, value]) => { + return { + name: key, + value: value + } as MapTableEntry; + }); + } + const dialogRef = this.dialog.open(ReferencedAttributesDialog, { + ...MEDIUM_DIALOG, + data: { + attributes: referencedAttributes + } + }); + + dialogRef.componentInstance.createNew = this.mapTableHelperService.createNewEntry('Attribute'); + + dialogRef.componentInstance.verify + .pipe(takeUntil(dialogRef.afterClosed())) + .subscribe((formData) => { + const attributesArray: MapTableEntry[] = formData.attributes || []; + const attributesMap: { [key: string]: string | null } = {}; + attributesArray.forEach((entry: MapTableEntry) => { + attributesMap[entry.name] = entry.value; + }); + const responseWithAttributes: ConfigurationAnalysisResponse = { + ...response, + configurationAnalysis: { + ...response.configurationAnalysis, + referencedAttributes: attributesMap + } + }; + this.store.dispatch( + VerificationActions.initiatePropertyVerification({ response: responseWithAttributes }) + ); + }); + }) + ), + { dispatch: false } + ); + + initiatePropertyVerification$ = createEffect(() => + this.actions$.pipe( + ofType(VerificationActions.initiatePropertyVerification), + map((action) => action.response), + switchMap((response) => { + this.store.dispatch(VerificationActions.openPropertyVerificationProgressDialog()); + // if the component does not support additional verification there is no need to prompt for attribute values + return from( + this.propertyVerificationService + .initiatePropertyVerification({ + request: { + request: { + properties: response.configurationAnalysis.properties, + componentId: response.configurationAnalysis.componentId, + attributes: response.configurationAnalysis.referencedAttributes + } + }, + uri: response.requestContext.entity.uri + }) + .pipe( + map((response) => { + return VerificationActions.verifyPropertiesSuccess({ response }); + }), + catchError((errorResponse: HttpErrorResponse) => + of( + ErrorActions.snackBarError({ + error: this.errorHelper.getErrorString(errorResponse) + }) + ) + ) + ) + ); + }) + ) + ); + + openPropertyVerificationProgressDialog$ = createEffect( + () => + this.actions$.pipe( + ofType(VerificationActions.openPropertyVerificationProgressDialog), + tap(() => { + const dialogRef = this.dialog.open(PropertyVerificationProgress, { + ...SMALL_DIALOG + }); + const verificationRequest$ = this.store + .select(selectActivePropertyVerificationRequest) + .pipe(isDefinedAndNotNull()); + dialogRef.componentInstance.verificationRequest$ = verificationRequest$; + + verificationRequest$ + .pipe( + takeUntil(dialogRef.afterClosed()), + isDefinedAndNotNull(), + filter((request) => request.complete) + ) + .subscribe((request) => { + if (request.failureReason) { + this.store.dispatch(ErrorActions.snackBarError({ error: request.failureReason })); + } + // close the dialog now that it is complete + dialogRef.close(); + }); + + dialogRef.componentInstance.stopVerification.pipe(take(1)).subscribe(() => { + this.store.dispatch(VerificationActions.stopPollingPropertyVerification()); + }); + }) + ), + { dispatch: false } + ); + + verifyPropertiesSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(VerificationActions.verifyPropertiesSuccess), + map((action) => action.response), + filter((response) => !response.request.complete), + switchMap(() => { + return of(VerificationActions.startPollingPropertyVerification()); + }) + ) + ); + + startPollingPropertyVerification$ = createEffect(() => + this.actions$.pipe( + ofType(VerificationActions.startPollingPropertyVerification), + switchMap(() => + interval(2000, asyncScheduler).pipe( + takeUntil(this.actions$.pipe(ofType(VerificationActions.stopPollingPropertyVerification))) + ) + ), + switchMap(() => of(VerificationActions.pollPropertyVerification())) + ) + ); + + pollPropertyVerification$ = createEffect(() => + this.actions$.pipe( + ofType(VerificationActions.pollPropertyVerification), + concatLatestFrom(() => [ + this.store.select(selectPropertyVerificationRequestContext).pipe(isDefinedAndNotNull()), + this.store.select(selectActivePropertyVerificationRequest).pipe(isDefinedAndNotNull()) + ]), + switchMap(([, requestContext, verifyRequest]) => { + return from( + this.propertyVerificationService + .getPropertyVerificationRequest(verifyRequest.requestId, requestContext.entity.uri) + .pipe( + map((response) => VerificationActions.pollPropertyVerificationSuccess({ response })), + catchError((errorResponse: HttpErrorResponse) => { + this.store.dispatch(VerificationActions.stopPollingPropertyVerification()); + return of( + ErrorActions.snackBarError({ + error: this.errorHelper.getErrorString(errorResponse) + }) + ); + }) + ) + ); + }) + ) + ); + + pollPropertyVerificationSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(VerificationActions.pollPropertyVerificationSuccess), + map((action) => action.response), + filter((response) => response.request.complete), + switchMap(() => of(VerificationActions.stopPollingPropertyVerification())) + ) + ); + + stopPollingPropertyVerification$ = createEffect(() => + this.actions$.pipe( + ofType(VerificationActions.stopPollingPropertyVerification), + concatLatestFrom(() => [ + this.store.select(selectPropertyVerificationRequestContext).pipe(isDefinedAndNotNull()), + this.store.select(selectActivePropertyVerificationRequest).pipe(isDefinedAndNotNull()) + ]), + switchMap(([, requestContext, verifyRequest]) => + from( + this.propertyVerificationService.deletePropertyVerificationRequest( + verifyRequest.requestId, + requestContext.entity.uri + ) + ).pipe( + map(() => VerificationActions.propertyVerificationComplete()), + catchError((errorResponse: HttpErrorResponse) => + of( + ErrorActions.snackBarError({ + error: this.errorHelper.getErrorString(errorResponse) + }) + ) + ) + ) + ) + ) + ); +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/property-verification/property-verification.reducer.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/property-verification/property-verification.reducer.ts new file mode 100644 index 0000000000..cd58587e45 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/property-verification/property-verification.reducer.ts @@ -0,0 +1,75 @@ +/* + * 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 { createReducer, on } from '@ngrx/store'; +import { PropertyVerificationState } from './index'; +import { + getConfigurationAnalysisSuccess, + initiatePropertyVerification, + pollPropertyVerificationSuccess, + propertyVerificationComplete, + resetPropertyVerificationState, + verifyPropertiesSuccess +} from './property-verification.actions'; +import { produce } from 'immer'; + +export const initialPropertyVerificationState: PropertyVerificationState = { + results: [], + status: 'pending', + requestContext: null, + activeRequest: null, + configurationAnalysis: null, + attributes: null +}; + +export const propertyVerificationReducer = createReducer( + initialPropertyVerificationState, + + on(initiatePropertyVerification, (state, { response }) => { + return produce(state, (draftState) => { + draftState.status = 'loading'; + draftState.configurationAnalysis = response.configurationAnalysis; + draftState.requestContext = response.requestContext; + if (response.configurationAnalysis.supportsVerification) { + // preserve the most recent attributes used to verify component + draftState.attributes = response.configurationAnalysis.referencedAttributes; + } + }); + }), + on(getConfigurationAnalysisSuccess, (state: PropertyVerificationState, { response }) => ({ + ...state, + configurationAnalysis: response.configurationAnalysis, + requestContext: response.requestContext + })), + + on(verifyPropertiesSuccess, pollPropertyVerificationSuccess, (state: PropertyVerificationState, { response }) => ({ + ...state, + activeRequest: response.request, + results: response.request.results || [] + })), + + on(propertyVerificationComplete, (state) => ({ + ...state, + activeRequest: null, + status: 'success' as const + })), + + on(resetPropertyVerificationState, (state) => ({ + ...initialPropertyVerificationState, + attributes: state.attributes // preserve attributes + })) +); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/property-verification/property-verification.selectors.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/property-verification/property-verification.selectors.ts new file mode 100644 index 0000000000..52e30d67a9 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/property-verification/property-verification.selectors.ts @@ -0,0 +1,47 @@ +/* + * 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 { createFeatureSelector, createSelector } from '@ngrx/store'; +import { propertyVerificationFeatureKey, PropertyVerificationState } from './index'; + +export const selectPropertyVerificationState = + createFeatureSelector(propertyVerificationFeatureKey); + +export const selectPropertyVerificationRequestContext = createSelector( + selectPropertyVerificationState, + (state: PropertyVerificationState) => state.requestContext +); + +export const selectActivePropertyVerificationRequest = createSelector( + selectPropertyVerificationState, + (state: PropertyVerificationState) => state.activeRequest +); + +export const selectPropertyVerificationResults = createSelector( + selectPropertyVerificationState, + (state: PropertyVerificationState) => state.results +); + +export const selectPropertyVerificationStatus = createSelector( + selectPropertyVerificationState, + (state: PropertyVerificationState) => state.status +); + +export const selectPropertyVerificationAttributes = createSelector( + selectPropertyVerificationState, + (state: PropertyVerificationState) => state.attributes +); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts index 969f97b7ad..cf9c0109a0 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts @@ -703,3 +703,8 @@ export interface OpenChangeComponentVersionDialogRequest { fetchRequest: FetchComponentVersionsRequest; componentVersions: DocumentedType[]; } + +export interface MapTableEntry { + name: string; + value: string | null; +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.html b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.html index 3b37d939d1..e7ccd6dc00 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.html +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.html @@ -117,8 +117,9 @@ -
+
+
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.ts index bf7cf4629d..58aba08840 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/controller-service/edit-controller-service/edit-controller-service.component.ts @@ -39,7 +39,7 @@ import { MatOptionModule } from '@angular/material/core'; import { MatSelectModule } from '@angular/material/select'; import { PropertyTable } from '../../property-table/property-table.component'; import { ControllerServiceApi } from '../controller-service-api/controller-service-api.component'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { ControllerServiceReferences } from '../controller-service-references/controller-service-references.component'; import { NifiSpinnerDirective } from '../../spinner/nifi-spinner.directive'; import { ErrorBanner } from '../../error-banner/error-banner.component'; @@ -48,6 +48,12 @@ import { TextTip } from '../../tooltips/text-tip/text-tip.component'; import { NifiTooltipDirective } from '../../tooltips/nifi-tooltip.directive'; import { ConvertToParameterResponse } from '../../../../pages/flow-designer/service/parameter-helper.service'; import { CloseOnEscapeDialog } from '../../close-on-escape-dialog/close-on-escape-dialog.component'; +import { PropertyVerification } from '../../property-verification/property-verification.component'; +import { + ConfigVerificationResult, + ModifiedProperties, + VerifyPropertiesRequestContext +} from '../../../../state/property-verification'; @Component({ selector: 'edit-controller-service', @@ -68,7 +74,8 @@ import { CloseOnEscapeDialog } from '../../close-on-escape-dialog/close-on-escap AsyncPipe, NifiSpinnerDirective, ErrorBanner, - NifiTooltipDirective + NifiTooltipDirective, + PropertyVerification ], styleUrls: ['./edit-controller-service.component.scss'] }) @@ -85,6 +92,11 @@ export class EditControllerService extends CloseOnEscapeDialog { @Input() goToService!: (serviceId: string) => void; @Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void; @Input() saving$!: Observable; + @Input() supportsParameters: boolean = true; + @Input() propertyVerificationResults$!: Observable; + @Input() propertyVerificationStatus$: Observable<'pending' | 'loading' | 'success'> = of('pending'); + + @Output() verify: EventEmitter = new EventEmitter(); @Output() editControllerService: EventEmitter = new EventEmitter(); @@ -167,9 +179,7 @@ export class EditControllerService extends CloseOnEscapeDialog { const propertyControl: AbstractControl | null = this.editControllerServiceForm.get('properties'); if (propertyControl && propertyControl.dirty) { const properties: Property[] = propertyControl.value; - const values: { [key: string]: string | null } = {}; - properties.forEach((property) => (values[property.property] = property.value)); - payload.component.properties = values; + payload.component.properties = this.getModifiedProperties(); payload.component.sensitiveDynamicPropertyNames = properties .filter((property) => property.descriptor.dynamic && property.descriptor.sensitive) .map((property) => property.descriptor.name); @@ -181,9 +191,27 @@ export class EditControllerService extends CloseOnEscapeDialog { }); } + private getModifiedProperties(): ModifiedProperties { + const propertyControl: AbstractControl | null = this.editControllerServiceForm.get('properties'); + if (propertyControl && propertyControl.dirty) { + const properties: Property[] = propertyControl.value; + const values: { [key: string]: string | null } = {}; + properties.forEach((property) => (values[property.property] = property.value)); + return values; + } + return {}; + } + protected readonly TextTip = TextTip; override isDirty(): boolean { return this.editControllerServiceForm.dirty; } + + verifyClicked(entity: ControllerServiceEntity): void { + this.verify.next({ + entity, + properties: this.getModifiedProperties() + }); + } } diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/extension-creation/extension-creation.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/extension-creation/extension-creation.component.spec.ts index c7f44d5336..a72f9326ed 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/extension-creation/extension-creation.component.spec.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/extension-creation/extension-creation.component.spec.ts @@ -28,12 +28,7 @@ describe('ExtensionCreation', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ExtensionCreation, NoopAnimationsModule], - providers: [ - { - provide: MatDialogRef, - useValue: null - } - ] + providers: [{ provide: MatDialogRef, useValue: null }] }); fixture = TestBed.createComponent(ExtensionCreation); component = fixture.componentInstance; diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/editors/text-editor/_text-editor.component-theme.scss b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/editors/text-editor/_text-editor.component-theme.scss new file mode 100644 index 0000000000..4028c7393f --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/editors/text-editor/_text-editor.component-theme.scss @@ -0,0 +1,32 @@ +/*! + * 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. + */ + +@use 'sass:map'; +@use '@angular/material' as mat; + +@mixin generate-theme($material-theme, $nifi-theme) { + .text-editor { + @include mat.button-density(-1); + + .editor { + &.blank { + border-color: var(--mdc-outlined-text-field-disabled-label-text-color); + cursor: not-allowed !important; + } + } + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/editors/text-editor/text-editor.component.html b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/editors/text-editor/text-editor.component.html new file mode 100644 index 0000000000..e5b3ccbb35 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/editors/text-editor/text-editor.component.html @@ -0,0 +1,71 @@ + + +
+
+
+
+
+ +
+ @if (!readonly) { + Set empty string + + } +
+
+ @if (readonly) { + + } @else { + + + } +
+
+
+
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/editors/text-editor/text-editor.component.scss b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/editors/text-editor/text-editor.component.scss new file mode 100644 index 0000000000..89db93d466 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/editors/text-editor/text-editor.component.scss @@ -0,0 +1,28 @@ +/* + * 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. + */ + +@use '@angular/material' as mat; + +.text-editor { + @include mat.button-density(-1); + + min-height: 220px; + min-width: 245px; + max-height: 100vh; + max-width: 100vw; + cursor: move; +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/editors/text-editor/text-editor.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/editors/text-editor/text-editor.component.spec.ts new file mode 100644 index 0000000000..29b5fa4d2c --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/editors/text-editor/text-editor.component.spec.ts @@ -0,0 +1,38 @@ +/* + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TextEditor } from './text-editor.component'; + +describe('TextEditor', () => { + let component: TextEditor; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TextEditor] + }).compileComponents(); + + fixture = TestBed.createComponent(TextEditor); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/editors/text-editor/text-editor.component.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/editors/text-editor/text-editor.component.ts new file mode 100644 index 0000000000..bffe2a7fa3 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/editors/text-editor/text-editor.component.ts @@ -0,0 +1,174 @@ +/* + * 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 { Component, EventEmitter, Input, Output, Renderer2, ViewContainerRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MapTableItem } from '../../map-table.component'; +import { AbstractControl, FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { Editor } from 'codemirror'; +import { CdkDrag } from '@angular/cdk/drag-drop'; +import { CdkTrapFocus } from '@angular/cdk/a11y'; +import { CodemirrorModule } from '@ctrl/ngx-codemirror'; +import { MatButton } from '@angular/material/button'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { NifiTooltipDirective } from '../../../tooltips/nifi-tooltip.directive'; +import { Resizable } from '../../../resizable/resizable.component'; + +@Component({ + selector: 'text-editor', + standalone: true, + imports: [ + CommonModule, + CdkDrag, + CdkTrapFocus, + CodemirrorModule, + MatButton, + MatCheckbox, + NifiTooltipDirective, + ReactiveFormsModule, + Resizable + ], + templateUrl: './text-editor.component.html', + styleUrl: './text-editor.component.scss' +}) +export class TextEditor { + @Input() set item(item: MapTableItem) { + this.textEditorForm.get('value')?.setValue(item.entry.value); + const isEmptyString: boolean = item.entry.value === ''; + this.textEditorForm.get('setEmptyString')?.setValue(isEmptyString); + this.setEmptyStringChanged(); + this.itemSet = true; + } + @Input() width!: number; + @Input() readonly: boolean = false; + + @Output() ok: EventEmitter = new EventEmitter(); + @Output() cancel: EventEmitter = new EventEmitter(); + + textEditorForm: FormGroup; + editor!: Editor; + blank = false; + itemSet = false; + + constructor( + private formBuilder: FormBuilder, + private viewContainerRef: ViewContainerRef, + private renderer: Renderer2 + ) { + this.textEditorForm = this.formBuilder.group({ + value: new FormControl(''), + setEmptyString: new FormControl(false) + }); + } + + cancelClicked(): void { + this.cancel.next(); + } + + okClicked(): void { + const valueControl: AbstractControl | null = this.textEditorForm.get('value'); + const emptyStringChecked: AbstractControl | null = this.textEditorForm.get('setEmptyString'); + if (valueControl && emptyStringChecked) { + const value = valueControl.value; + if (value === '') { + if (emptyStringChecked.value) { + this.ok.next(''); + } else { + this.ok.next(null); + } + } else { + this.ok.next(value); + } + } + } + + setEmptyStringChanged(): void { + const emptyStringChecked: AbstractControl | null = this.textEditorForm.get('setEmptyString'); + if (emptyStringChecked) { + this.blank = emptyStringChecked.value; + + if (emptyStringChecked.value) { + this.textEditorForm.get('value')?.setValue(''); + this.textEditorForm.get('value')?.disable(); + + if (this.editor) { + this.editor.setOption('readOnly', 'nocursor'); + } + } else { + this.textEditorForm.get('value')?.enable(); + + if (this.editor) { + this.editor.setOption('readOnly', false); + } + } + } + } + + resized(event: any): void { + // Note: We calculate the height of the codemirror to fit into an `.editor` overlay. The + // height of the codemirror needs to be set in order to handle large amounts of text in the codemirror editor. + // The height of the codemirror should be the height of the `.editor` overlay minus the 112px of spacing + // needed to display the 'Set Empty String' checkbox, the action buttons, + // and the resize handle. If the amount of spacing needed for additional UX is needed for the `.editor` is + // changed then this value should also be updated. + this.editor.setSize('100%', event.height - 112); + } + + preventDrag(event: MouseEvent): void { + event.stopPropagation(); + } + + codeMirrorLoaded(codeEditor: any): void { + this.editor = codeEditor.codeMirror; + // The `.text-editor` minimum height is set to 220px. This is the height of the `.editor` overlay. The + // height of the codemirror needs to be set in order to handle large amounts of text in the codemirror editor. + // The height of the codemirror should be the height of the `.editor` overlay minus the 112px of spacing + // needed to display the 'Set Empty String' checkbox, the action buttons, + // and the resize handle so the initial height of the codemirror when opening should be 108px for a 220px tall + // `.editor` overlay. If the initial height of that overlay changes then this initial height should also be + // updated. + this.editor.setSize('100%', 108); + + if (!this.readonly) { + this.editor.focus(); + this.editor.execCommand('selectAll'); + } + + // disabling of the input through the form isn't supported until codemirror + // has loaded so we must disable again if the value is an empty string + if (this.textEditorForm.get('setEmptyString')?.value) { + this.textEditorForm.get('value')?.disable(); + this.editor.setOption('readOnly', 'nocursor'); + } + } + + getOptions(): any { + return { + readOnly: this.readonly, + lineNumbers: true, + matchBrackets: true, + theme: 'nifi', + extraKeys: { + Enter: () => { + if (this.textEditorForm.dirty && this.textEditorForm.valid) { + this.okClicked(); + } + } + } + }; + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/map-table.component.html b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/map-table.component.html new file mode 100644 index 0000000000..e4fc63881a --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/map-table.component.html @@ -0,0 +1,139 @@ + + +
+
+
+ +
+ @if (!isDisabled) { +
+ +
+ } +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
Name +
+
+ {{ item.entry.name }} +
+
+
Value +
+ @if (isNull(item.entry.value)) { +
No value set
+ } @else { + + +
Empty string set
+
+ +
+
+ {{ value }} +
+ @if (hasExtraWhitespace(value)) { +
+ } +
+
+ } +
+
+
+ @if (!isDisabled) { + + } + + + +
+
+ + + +
+
+
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/map-table.component.scss b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/map-table.component.scss new file mode 100644 index 0000000000..b33f7cac34 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/map-table.component.scss @@ -0,0 +1,16 @@ +/*! + * 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. + */ diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/map-table.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/map-table.component.spec.ts new file mode 100644 index 0000000000..5129ff0d2f --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/map-table.component.spec.ts @@ -0,0 +1,38 @@ +/* + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MapTable } from './map-table.component'; + +describe('EditableMapTable', () => { + let component: MapTable; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MapTable] + }).compileComponents(); + + fixture = TestBed.createComponent(MapTable); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/map-table.component.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/map-table.component.ts new file mode 100644 index 0000000000..2776cc9e81 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/map-table/map-table.component.ts @@ -0,0 +1,392 @@ +/* + * 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 { + AfterViewInit, + ChangeDetectorRef, + Component, + DestroyRef, + forwardRef, + inject, + Input, + QueryList, + ViewChildren +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Observable, take } from 'rxjs'; +import { MapTableEntry } from '../../../state/shared'; +import { + MatCell, + MatCellDef, + MatColumnDef, + MatHeaderCell, + MatHeaderCellDef, + MatHeaderRow, + MatHeaderRowDef, + MatRow, + MatRowDef, + MatTable, + MatTableDataSource +} from '@angular/material/table'; +import { + CdkConnectedOverlay, + CdkOverlayOrigin, + ConnectionPositionPair, + OriginConnectionPosition, + OverlayConnectionPosition +} from '@angular/cdk/overlay'; +import { NiFiCommon } from '../../../service/nifi-common.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ComboEditor } from '../property-table/editors/combo-editor/combo-editor.component'; +import { MatIconButton } from '@angular/material/button'; +import { NifiTooltipDirective } from '../tooltips/nifi-tooltip.directive'; +import { TextTip } from '../tooltips/text-tip/text-tip.component'; +import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; +import { NfEditor } from '../property-table/editors/nf-editor/nf-editor.component'; +import { TextEditor } from './editors/text-editor/text-editor.component'; + +export interface MapTableItem { + entry: MapTableEntry; + id: number; + triggerEdit: boolean; + deleted: boolean; + dirty: boolean; + added: boolean; +} + +@Component({ + selector: 'map-table', + standalone: true, + imports: [ + CommonModule, + CdkConnectedOverlay, + CdkOverlayOrigin, + ComboEditor, + MatCell, + MatCellDef, + MatColumnDef, + MatHeaderCell, + MatIconButton, + MatTable, + MatHeaderCellDef, + NifiTooltipDirective, + MatMenuTrigger, + MatMenu, + MatMenuItem, + MatRow, + MatHeaderRow, + NfEditor, + MatRowDef, + MatHeaderRowDef, + TextEditor + ], + templateUrl: './map-table.component.html', + styleUrl: './map-table.component.scss', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapTable), + multi: true + } + ] +}) +export class MapTable implements AfterViewInit, ControlValueAccessor { + @Input() createNew!: (existingEntries: string[]) => Observable; + @Input() reportChangesOnly: boolean = false; + + private destroyRef = inject(DestroyRef); + + itemLookup: Map = new Map(); + displayedColumns: string[] = ['name', 'value', 'actions']; + dataSource: MatTableDataSource = new MatTableDataSource(); + selectedItem!: MapTableItem; + + @ViewChildren('trigger') valueTriggers!: QueryList; + + isDisabled = false; + isTouched = false; + onTouched!: () => void; + onChange!: (entries: MapTableEntry[]) => void; + editorOpen = false; + editorTrigger: any = null; + editorItem!: MapTableItem; + editorWidth = 0; + editorOffsetX = 0; + editorOffsetY = 0; + + private originPos: OriginConnectionPosition = { + originX: 'center', + originY: 'center' + }; + private editorOverlayPos: OverlayConnectionPosition = { + overlayX: 'center', + overlayY: 'center' + }; + public editorPositions: ConnectionPositionPair[] = []; + + constructor( + private changeDetector: ChangeDetectorRef, + private nifiCommon: NiFiCommon + ) {} + + ngAfterViewInit(): void { + this.initFilter(); + + this.valueTriggers.changes.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { + const item: MapTableItem | undefined = this.dataSource.data.find((item) => item.triggerEdit); + + if (item) { + const valueTrigger: CdkOverlayOrigin | undefined = this.valueTriggers.find( + (valueTrigger: CdkOverlayOrigin) => { + return this.formatId(item) == valueTrigger.elementRef.nativeElement.getAttribute('id'); + } + ); + + if (valueTrigger) { + // scroll into view + valueTrigger.elementRef.nativeElement.scrollIntoView({ block: 'center', behavior: 'instant' }); + + window.setTimeout(function () { + // trigger a click to start editing the new item + valueTrigger.elementRef.nativeElement.click(); + }, 0); + } + + item.triggerEdit = false; + } + }); + } + + initFilter(): void { + this.dataSource.filterPredicate = (data: MapTableItem) => this.isVisible(data); + this.dataSource.filter = ' '; + } + + isVisible(item: MapTableItem): boolean { + return !item.deleted; + } + + registerOnChange(onChange: (entries: MapTableEntry[]) => void): void { + this.onChange = onChange; + } + + registerOnTouched(onTouch: () => void): void { + this.onTouched = onTouch; + } + + setDisabledState(isDisabled: boolean): void { + this.isDisabled = isDisabled; + } + + writeValue(entries: MapTableEntry[]): void { + this.itemLookup.clear(); + + let i = 0; + let items: MapTableItem[] = []; + if (entries) { + items = entries.map((entry) => { + // create the property item + const item: MapTableItem = { + entry, + id: i++, + triggerEdit: false, + deleted: false, + added: false, + dirty: false + }; + + // store the entry item in a map for an efficient lookup later + this.itemLookup.set(entry.name, item); + return item; + }); + } + + this.setItems(items); + } + + private setItems(items: MapTableItem[]): void { + this.dataSource = new MatTableDataSource(items); + this.initFilter(); + } + + newEntryClicked(): void { + // filter out deleted properties in case the user needs to re-add one + const existingEntries: string[] = this.dataSource.data + .filter((item) => !item.deleted) + .map((item) => item.entry.name); + + // create the new property + this.createNew(existingEntries) + .pipe(take(1)) + .subscribe((entry) => { + const currentItems: MapTableItem[] = this.dataSource.data; + + const itemIndex: number = currentItems.findIndex( + (existingItem: MapTableItem) => existingItem.entry.name == entry.name + ); + + if (itemIndex > -1) { + const currentItem: MapTableItem = currentItems[itemIndex]; + const updatedItem: MapTableItem = { + ...currentItem, + entry, + triggerEdit: true, + deleted: false, + added: true, + dirty: true + }; + + this.itemLookup.set(entry.name, updatedItem); + + // if the user had previously deleted the entry, replace the matching entry item + currentItems[itemIndex] = updatedItem; + } else { + const i: number = currentItems.length; + const item: MapTableItem = { + entry, + id: i, + triggerEdit: true, + deleted: false, + added: true, + dirty: true + }; + + this.itemLookup.set(entry.name, item); + + // if this is a new entry, add it to the list + this.setItems([...currentItems, item]); + } + + this.handleChanged(); + }); + } + + formatId(item: MapTableItem): string { + return 'entry-' + item.id; + } + + isNull(value: string): boolean { + return value == null; + } + + isEmptyString(value: string): boolean { + return value == ''; + } + + hasExtraWhitespace(value: string): boolean { + return this.nifiCommon.hasLeadTrailWhitespace(value); + } + + openEditor(editorTrigger: any, item: MapTableItem, event: MouseEvent): void { + if (event.target) { + const target: HTMLElement = event.target as HTMLElement; + + // find the table cell regardless of the target of the click + const td: HTMLElement | null = target.closest('td'); + if (td) { + const { width } = td.getBoundingClientRect(); + + this.editorPositions.pop(); + this.editorItem = item; + this.editorTrigger = editorTrigger; + this.editorOpen = true; + + this.editorWidth = width + 100; + this.editorOffsetX = 8; + this.editorOffsetY = 80; + + this.editorPositions.push( + new ConnectionPositionPair( + this.originPos, + this.editorOverlayPos, + this.editorOffsetX, + this.editorOffsetY + ) + ); + } + } + } + + deleteProperty(item: MapTableItem): void { + if (!item.deleted) { + item.entry.value = null; + item.deleted = true; + item.dirty = true; + + this.handleChanged(); + } + } + + saveValue(item: MapTableItem, newValue: string | null): void { + if (item.entry.value != newValue) { + item.entry.value = newValue; + item.dirty = true; + + this.handleChanged(); + } + + this.closeEditor(); + } + + private handleChanged() { + // this is needed to trigger the filter to be reapplied + this.dataSource._updateChangeSubscription(); + this.changeDetector.markForCheck(); + + // mark the component as touched if not already + if (!this.isTouched) { + this.isTouched = true; + this.onTouched(); + } + + // emit the changes + this.onChange(this.serializeEntries()); + } + + private serializeEntries(): MapTableEntry[] { + const items: MapTableItem[] = this.dataSource.data; + + if (this.reportChangesOnly) { + // only include dirty items + return items + .filter((item) => item.dirty) + .filter((item) => !(item.added && item.deleted)) + .map((item) => item.entry); + } else { + // return all the items, even untouched ones. no need to return the deleted items though + return items.filter((item) => !item.deleted).map((item) => item.entry); + } + } + + closeEditor(): void { + this.editorOpen = false; + } + + select(item: MapTableItem): void { + this.selectedItem = item; + } + + isSelected(item: MapTableItem): boolean { + if (this.selectedItem) { + return item.entry.name == this.selectedItem.entry.name; + } + return false; + } + + protected readonly TextTip = TextTip; +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/new-map-table-entry-dialog/new-map-table-entry-dialog.component.html b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/new-map-table-entry-dialog/new-map-table-entry-dialog.component.html new file mode 100644 index 0000000000..5e83d8080f --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/new-map-table-entry-dialog/new-map-table-entry-dialog.component.html @@ -0,0 +1,41 @@ + + +

Add {{ data.entryTypeLabel || 'Entry' }}

+
+ +
+ + Name + + @if (name.invalid) { + {{ getNameErrorMessage() }} + } + +
+
+ + + + +
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/new-map-table-entry-dialog/new-map-table-entry-dialog.component.scss b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/new-map-table-entry-dialog/new-map-table-entry-dialog.component.scss new file mode 100644 index 0000000000..bc3ec7a29e --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/new-map-table-entry-dialog/new-map-table-entry-dialog.component.scss @@ -0,0 +1,30 @@ +/*! + * 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. + */ + +@use '@angular/material' as mat; + +.new-entry-form { + @include mat.button-density(-1); + + .mat-mdc-form-field { + width: 100%; + } + + .mat-mdc-form-field-error { + font-size: 12px; + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/new-map-table-entry-dialog/new-map-table-entry-dialog.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/new-map-table-entry-dialog/new-map-table-entry-dialog.component.spec.ts new file mode 100644 index 0000000000..9453399047 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/new-map-table-entry-dialog/new-map-table-entry-dialog.component.spec.ts @@ -0,0 +1,48 @@ +/* + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MapTableEntryData, NewMapTableEntryDialog } from './new-map-table-entry-dialog.component'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +describe('NewMapTableEntryDialog', () => { + let component: NewMapTableEntryDialog; + let fixture: ComponentFixture; + const data: MapTableEntryData = { + entryTypeLabel: '', + existingEntries: [] + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NewMapTableEntryDialog, NoopAnimationsModule], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: data }, + { provide: MatDialogRef, useValue: null } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(NewMapTableEntryDialog); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/new-map-table-entry-dialog/new-map-table-entry-dialog.component.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/new-map-table-entry-dialog/new-map-table-entry-dialog.component.ts new file mode 100644 index 0000000000..5ab0bb351a --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/new-map-table-entry-dialog/new-map-table-entry-dialog.component.ts @@ -0,0 +1,118 @@ +/* + * 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 { Component, EventEmitter, Inject, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, + ValidationErrors, + ValidatorFn, + Validators +} from '@angular/forms'; +import { + MAT_DIALOG_DATA, + MatDialogActions, + MatDialogClose, + MatDialogContent, + MatDialogTitle +} from '@angular/material/dialog'; +import { CloseOnEscapeDialog } from '../close-on-escape-dialog/close-on-escape-dialog.component'; +import { MatButton } from '@angular/material/button'; +import { MatError, MatFormField, MatLabel } from '@angular/material/form-field'; +import { MatInput } from '@angular/material/input'; +import { MatRadioButton, MatRadioGroup } from '@angular/material/radio'; + +export interface MapTableEntryData { + existingEntries: string[]; + entryTypeLabel?: string; +} + +@Component({ + selector: 'new-map-table-entry-dialog', + standalone: true, + imports: [ + CommonModule, + MatButton, + MatDialogActions, + MatDialogClose, + MatDialogContent, + MatDialogTitle, + MatError, + MatFormField, + MatInput, + MatLabel, + MatRadioButton, + MatRadioGroup, + ReactiveFormsModule + ], + templateUrl: './new-map-table-entry-dialog.component.html', + styleUrl: './new-map-table-entry-dialog.component.scss' +}) +export class NewMapTableEntryDialog extends CloseOnEscapeDialog { + @Output() newEntry: EventEmitter = new EventEmitter(); + + newEntryForm: FormGroup; + name: FormControl; + + constructor( + private formBuilder: FormBuilder, + @Inject(MAT_DIALOG_DATA) public data: MapTableEntryData + ) { + super(); + this.name = new FormControl(null, [ + Validators.required, + this.existingEntryValidator(this.data.existingEntries) + ]); + this.newEntryForm = formBuilder.group({ + name: this.name + }); + } + + private existingEntryValidator(existingEntries: string[]): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value; + if (value === '') { + return null; + } + if (existingEntries.includes(value)) { + return { + existingEntry: true + }; + } + return null; + }; + } + + getNameErrorMessage(): string { + if (this.name) { + if (this.name.hasError('required')) { + return 'Name is required.'; + } + + return this.name.hasError('existingEntry') ? 'Name already exists.' : ''; + } + return ''; + } + + addClicked() { + this.newEntry.next(this.name.value); + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.ts index a8bc49dfe8..3595a5563b 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.ts @@ -75,7 +75,7 @@ export class NfEditor implements OnDestroy { this.loadParameters(); } - @Input() set parameters(parameters: Parameter[]) { + @Input() set parameters(parameters: Parameter[] | null) { this._parameters = parameters; this.getParametersSet = true; @@ -99,7 +99,7 @@ export class NfEditor implements OnDestroy { blank = false; mode!: string; - _parameters!: Parameter[]; + _parameters!: Parameter[] | null; editor!: Editor; @@ -128,6 +128,7 @@ export class NfEditor implements OnDestroy { this.editor.setSize('100%', 108); if (!this.readonly) { + this.editor.focus(); this.editor.execCommand('selectAll'); } diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.html b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.html index fcf2872599..fe5f737f04 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.html +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.html @@ -20,7 +20,6 @@
Required field
@if (!isDisabled) {
- @@ -181,7 +180,7 @@ @if (hasAllowableValues(editorItem)) { void; @Input() supportsSensitiveDynamicProperties = false; @Input() propertyHistory: ComponentHistory | undefined; + @Input() supportsParameters: boolean = true; private static readonly PARAM_REF_REGEX: RegExp = /#{[a-zA-Z0-9-_. ]+}/; @@ -137,7 +138,7 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor { editorOpen = false; editorTrigger: any = null; editorItem!: PropertyItem; - editorParameters: Parameter[] = []; + editorParameters: Parameter[] | null = []; editorWidth = 0; editorOffsetX = 0; editorOffsetY = 0; @@ -319,7 +320,10 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor { this.initFilter(); } - private getParametersForItem(propertyItem: PropertyItem): Parameter[] { + private getParametersForItem(propertyItem: PropertyItem): Parameter[] | null { + if (!this.supportsParameters) { + return null; + } if (this.parameterContext?.permissions.canRead) { return this.parameterContext.component.parameters .map((parameterEntity) => parameterEntity.parameter) diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/property-verification-progress/property-verification-progress.component.html b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/property-verification-progress/property-verification-progress.component.html new file mode 100644 index 0000000000..783162ed6f --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/property-verification-progress/property-verification-progress.component.html @@ -0,0 +1,44 @@ + + +

Verifying Properties

+
+ +
+ @if (verificationRequest$ | async; as verificationRequest) { +
+ @if (verificationRequest.complete) { + Complete + } @else { + {{ verificationRequest.state }} + } +
+
+ +
{{ verificationRequest.percentCompleted }}%
+
+ } +
+
+ + @if (verificationRequest$ | async; as verificationRequest) { + + } + +
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/property-verification-progress/property-verification-progress.component.scss b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/property-verification-progress/property-verification-progress.component.scss new file mode 100644 index 0000000000..b33f7cac34 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/property-verification-progress/property-verification-progress.component.scss @@ -0,0 +1,16 @@ +/*! + * 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. + */ diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/property-verification-progress/property-verification-progress.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/property-verification-progress/property-verification-progress.component.spec.ts new file mode 100644 index 0000000000..3bc2a47169 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/property-verification-progress/property-verification-progress.component.spec.ts @@ -0,0 +1,38 @@ +/* + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PropertyVerificationProgress } from './property-verification-progress.component'; + +describe('PropertyVerificationProgress', () => { + let component: PropertyVerificationProgress; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PropertyVerificationProgress] + }).compileComponents(); + + fixture = TestBed.createComponent(PropertyVerificationProgress); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/property-verification-progress/property-verification-progress.component.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/property-verification-progress/property-verification-progress.component.ts new file mode 100644 index 0000000000..c9916bef02 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/property-verification-progress/property-verification-progress.component.ts @@ -0,0 +1,48 @@ +/* + * 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 { Component, EventEmitter, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatButton } from '@angular/material/button'; +import { MatDialogActions, MatDialogClose, MatDialogContent, MatDialogTitle } from '@angular/material/dialog'; +import { MatProgressBar } from '@angular/material/progress-bar'; +import { PropertyVerificationRequest } from '../../../../../state/property-verification'; +import { Observable, of } from 'rxjs'; + +@Component({ + selector: 'property-verification-progress', + standalone: true, + imports: [ + CommonModule, + MatButton, + MatDialogActions, + MatDialogClose, + MatDialogContent, + MatDialogTitle, + MatProgressBar + ], + templateUrl: './property-verification-progress.component.html', + styleUrl: './property-verification-progress.component.scss' +}) +export class PropertyVerificationProgress { + @Input() verificationRequest$: Observable = of(null); + @Output() stopVerification = new EventEmitter(); + + stop(request: PropertyVerificationRequest) { + this.stopVerification.next(request); + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/referenced-attributes-dialog/referenced-attributes-dialog.component.html b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/referenced-attributes-dialog/referenced-attributes-dialog.component.html new file mode 100644 index 0000000000..26d24a0f05 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/referenced-attributes-dialog/referenced-attributes-dialog.component.html @@ -0,0 +1,48 @@ + + +

Referenced Attributes

+
+ +
+ +
+
+ Enter Attribute Values + +
+ +
+
+
+
+ + + + +
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/referenced-attributes-dialog/referenced-attributes-dialog.component.scss b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/referenced-attributes-dialog/referenced-attributes-dialog.component.scss new file mode 100644 index 0000000000..b33f7cac34 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/referenced-attributes-dialog/referenced-attributes-dialog.component.scss @@ -0,0 +1,16 @@ +/*! + * 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. + */ diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/referenced-attributes-dialog/referenced-attributes-dialog.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/referenced-attributes-dialog/referenced-attributes-dialog.component.spec.ts new file mode 100644 index 0000000000..86cd7bf13c --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/referenced-attributes-dialog/referenced-attributes-dialog.component.spec.ts @@ -0,0 +1,46 @@ +/* + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReferencedAttributesDialog, ReferencedAttributesDialogData } from './referenced-attributes-dialog.component'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +describe('ReferencedAttributesDialog', () => { + let component: ReferencedAttributesDialog; + let fixture: ComponentFixture; + const data: ReferencedAttributesDialogData = { + attributes: [] + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ReferencedAttributesDialog], + providers: [ + { provide: MatDialogRef, useValue: null }, + { provide: MAT_DIALOG_DATA, useValue: data } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(ReferencedAttributesDialog); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/referenced-attributes-dialog/referenced-attributes-dialog.component.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/referenced-attributes-dialog/referenced-attributes-dialog.component.ts new file mode 100644 index 0000000000..c82dcf9c3e --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/common/referenced-attributes-dialog/referenced-attributes-dialog.component.ts @@ -0,0 +1,91 @@ +/* + * 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 { Component, EventEmitter, Inject, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { + MAT_DIALOG_DATA, + MatDialogActions, + MatDialogClose, + MatDialogContent, + MatDialogTitle +} from '@angular/material/dialog'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { MapTableEntry } from '../../../../../state/shared'; +import { MatButton, MatIconButton } from '@angular/material/button'; +import { NifiSpinnerDirective } from '../../../spinner/nifi-spinner.directive'; +import { MapTable } from '../../../map-table/map-table.component'; +import { NifiTooltipDirective } from '../../../tooltips/nifi-tooltip.directive'; +import { TextTip } from '../../../tooltips/text-tip/text-tip.component'; +import { CloseOnEscapeDialog } from '../../../close-on-escape-dialog/close-on-escape-dialog.component'; + +export interface ReferencedAttributesDialogData { + attributes: MapTableEntry[]; +} + +@Component({ + selector: 'referenced-attributes-dialog', + standalone: true, + imports: [ + CommonModule, + MatDialogTitle, + ReactiveFormsModule, + MatDialogContent, + MatDialogActions, + MatButton, + MatDialogClose, + NifiSpinnerDirective, + MapTable, + NifiTooltipDirective, + MatIconButton + ], + templateUrl: './referenced-attributes-dialog.component.html', + styleUrl: './referenced-attributes-dialog.component.scss' +}) +export class ReferencedAttributesDialog extends CloseOnEscapeDialog { + referencedAttributesForm: FormGroup; + + @Input() createNew!: (existingEntries: string[]) => Observable; + @Output() verify = new EventEmitter(); + + constructor( + private formBuilder: FormBuilder, + @Inject(MAT_DIALOG_DATA) private data: ReferencedAttributesDialogData + ) { + super(); + const attributes: MapTableEntry[] = data.attributes || []; + this.referencedAttributesForm = this.formBuilder.group({ + attributes: new FormControl(attributes) + }); + } + + verifyClicked() { + this.verify.next(this.referencedAttributesForm.value); + } + + clearAttributesClicked() { + this.referencedAttributesForm.reset(); + } + + isEmpty() { + const attributes = this.referencedAttributesForm.get('attributes')?.value || []; + return attributes.length === 0; + } + + protected readonly TextTip = TextTip; +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/property-verification.component.html b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/property-verification.component.html new file mode 100644 index 0000000000..4f5cf4c6a8 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/property-verification.component.html @@ -0,0 +1,60 @@ + + +
+
+
Verification
+
+ +
+
+
+ @if (disabled) { +
Property verification is disabled
+ } @else if (isVerifying) { +
Verifying properties...
+ } @else if (results.length > 0) { +
+ @for (result of results; track results) { +
+
+ @switch (result.outcome) { + @case (Outcome.SUCCESSFUL) { +
+ } + @case (Outcome.FAILED) { +
+ } + @case (Outcome.SKIPPED) { +
+ } + } +
+
{{ result.verificationStepName }}
+
{{ result.explanation }}
+
+
+
+ } +
+ } @else { +
Click the button above to verify this component.
+ } +
+
diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/property-verification.component.scss b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/property-verification.component.scss new file mode 100644 index 0000000000..b92d517be0 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/property-verification.component.scss @@ -0,0 +1,22 @@ +/*! + * 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. + */ + +.property-verification { + .verification-explanation { + word-break: break-word; + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/property-verification.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/property-verification.component.spec.ts new file mode 100644 index 0000000000..ac8cee046d --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/property-verification.component.spec.ts @@ -0,0 +1,38 @@ +/* + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PropertyVerification } from './property-verification.component'; + +describe('PropertyVerification', () => { + let component: PropertyVerification; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PropertyVerification] + }).compileComponents(); + + fixture = TestBed.createComponent(PropertyVerification); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/property-verification.component.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/property-verification.component.ts new file mode 100644 index 0000000000..309556e618 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-verification/property-verification.component.ts @@ -0,0 +1,50 @@ +/* + * 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 { Component, EventEmitter, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatIconButton } from '@angular/material/button'; +import { ConfigVerificationResult, Outcome } from '../../../state/property-verification'; + +@Component({ + selector: 'property-verification', + standalone: true, + imports: [CommonModule, MatIconButton], + templateUrl: './property-verification.component.html', + styleUrl: './property-verification.component.scss' +}) +export class PropertyVerification { + private _results: ConfigVerificationResult[] | null = null; + + @Input() set results(results: ConfigVerificationResult[] | null) { + this._results = results; + } + get results(): ConfigVerificationResult[] { + return this._results || []; + } + + @Input() isVerifying = false; + @Input() disabled = false; + + @Output() verify: EventEmitter = new EventEmitter(); + + verifyClicked(): void { + this.verify.next(); + } + + protected readonly Outcome = Outcome; +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/status-history/status-history.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/status-history/status-history.component.spec.ts index 1a042ea63d..bb0f136f4c 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/status-history/status-history.component.spec.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/status-history/status-history.component.spec.ts @@ -33,10 +33,7 @@ describe('StatusHistory', () => { providers: [ { provide: MAT_DIALOG_DATA, useValue: {} }, provideMockStore({ initialState }), - { - provide: MatDialogRef, - useValue: null - } + { provide: MatDialogRef, useValue: null } ] }); fixture = TestBed.createComponent(StatusHistory); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/system-diagnostics-dialog/system-diagnostics-dialog.component.spec.ts b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/system-diagnostics-dialog/system-diagnostics-dialog.component.spec.ts index 1e270ef183..4892bb2bb7 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/system-diagnostics-dialog/system-diagnostics-dialog.component.spec.ts +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/system-diagnostics-dialog/system-diagnostics-dialog.component.spec.ts @@ -31,10 +31,7 @@ describe('SystemDiagnosticsDialog', () => { imports: [SystemDiagnosticsDialog], providers: [ provideMockStore({ initialState: initialSystemDiagnosticsState }), - { - provide: MatDialogRef, - useValue: null - } + { provide: MatDialogRef, useValue: null } ] }); fixture = TestBed.createComponent(SystemDiagnosticsDialog); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss index d773e6ae4f..b422594c50 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss @@ -42,6 +42,7 @@ @use 'app/ui/common/tooltips/property-hint-tip/property-hint-tip.component-theme' as property-hint-tip; @use 'app/pages/summary/ui/processor-status-listing/processor-status-table/processor-status-table.component-theme' as processor-status-table; @use 'app/pages/flow-designer/ui/canvas/change-color-dialog/change-color-dialog.component-theme' as change-color-dialog; +@use 'app/ui/common/map-table/editors/text-editor/text-editor.component-theme' as text-editor; // Plus imports for other components in your app. @use 'assets/fonts/flowfont/flowfont.css'; @@ -96,6 +97,7 @@ @include property-hint-tip.generate-theme($material-theme-light, $nifi-theme-light); @include processor-status-table.generate-theme($nifi-theme-light); @include change-color-dialog.generate-theme($nifi-theme-light); +@include text-editor.generate-theme($material-theme-light, $nifi-theme-light); .dark-theme { // Include the dark theme color styles. @@ -126,4 +128,5 @@ @include property-hint-tip.generate-theme($material-theme-dark, $nifi-theme-dark); @include processor-status-table.generate-theme($nifi-theme-dark); @include change-color-dialog.generate-theme($nifi-theme-dark); + @include text-editor.generate-theme($material-theme-dark, $nifi-theme-dark); }