mirror of https://github.com/apache/nifi.git
[NIFI-13247] - Property Verification (#8857)
* [NIFI-13247] - Property Verification * Added map-table providing a property-table-like component for simple key/value pairs. * add missing license headers * address review feedback * update to leverage themed code-mirror styles This closes #8857
This commit is contained in:
parent
05d0d36e70
commit
7951b4be80
|
@ -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,
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -80,10 +80,7 @@ describe('EditLabel', () => {
|
|||
isDisconnectionAcknowledged: jest.fn()
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: null
|
||||
}
|
||||
{ provide: MatDialogRef, useValue: null }
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(EditLabel);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -106,10 +106,7 @@ describe('EditPort', () => {
|
|||
isDisconnectionAcknowledged: jest.fn()
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: null
|
||||
}
|
||||
{ provide: MatDialogRef, useValue: null }
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(EditPort);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -236,8 +236,9 @@
|
|||
</mat-tab>
|
||||
<mat-tab label="Properties">
|
||||
<mat-dialog-content>
|
||||
<div class="dialog-tab-content">
|
||||
<div class="dialog-tab-content flex gap-x-3">
|
||||
<property-table
|
||||
class="w-2/3"
|
||||
formControlName="properties"
|
||||
[createNewProperty]="createNewProperty"
|
||||
[createNewService]="createNewService"
|
||||
|
@ -249,6 +250,12 @@
|
|||
[supportsSensitiveDynamicProperties]="
|
||||
request.entity.component.supportsSensitiveDynamicProperties
|
||||
"></property-table>
|
||||
<property-verification
|
||||
class="w-1/3"
|
||||
[disabled]="readonly"
|
||||
[isVerifying]="(propertyVerificationStatus$ | async) === 'loading'"
|
||||
[results]="propertyVerificationResults$ | async"
|
||||
(verify)="verifyClicked(request.entity)"></property-verification>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
</mat-tab>
|
||||
|
|
|
@ -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<ConvertToParameterResponse>;
|
||||
@Input() goToService!: (serviceId: string) => void;
|
||||
@Input() saving$!: Observable<boolean>;
|
||||
|
||||
@Input() propertyVerificationResults$!: Observable<ConfigVerificationResult[]>;
|
||||
@Input() propertyVerificationStatus$: Observable<'pending' | 'loading' | 'success'> = of('pending');
|
||||
|
||||
@Output() verify: EventEmitter<VerifyPropertiesRequestContext> = new EventEmitter<VerifyPropertiesRequestContext>();
|
||||
@Output() editProcessor: EventEmitter<UpdateProcessorRequest> = new EventEmitter<UpdateProcessorRequest>();
|
||||
|
||||
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()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -87,14 +87,22 @@
|
|||
</mat-tab>
|
||||
<mat-tab label="Properties">
|
||||
<mat-dialog-content>
|
||||
<div class="dialog-tab-content">
|
||||
<div class="dialog-tab-content flex gap-x-3">
|
||||
<property-table
|
||||
class="w-2/3"
|
||||
formControlName="properties"
|
||||
[createNewProperty]="createNewProperty"
|
||||
[createNewService]="createNewService"
|
||||
[propertyHistory]="request.history"
|
||||
[supportsParameters]="false"
|
||||
[goToService]="goToService">
|
||||
</property-table>
|
||||
<property-verification
|
||||
class="w-1/3"
|
||||
[disabled]="readonly"
|
||||
[isVerifying]="(propertyVerificationStatus$ | async) === 'loading'"
|
||||
[results]="propertyVerificationResults$ | async"
|
||||
(verify)="verifyClicked(request.flowAnalysisRule)"></property-verification>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
</mat-tab>
|
||||
|
|
|
@ -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<InlineServiceCreationResponse>;
|
||||
@Input() saving$!: Observable<boolean>;
|
||||
@Input() goToService!: (serviceId: string) => void;
|
||||
@Input() propertyVerificationResults$!: Observable<ConfigVerificationResult[]>;
|
||||
@Input() propertyVerificationStatus$: Observable<'pending' | 'loading' | 'success'> = of('pending');
|
||||
|
||||
@Output() verify: EventEmitter<VerifyPropertiesRequestContext> = new EventEmitter<VerifyPropertiesRequestContext>();
|
||||
@Output() editFlowAnalysisRule: EventEmitter<UpdateFlowAnalysisRuleRequest> =
|
||||
new EventEmitter<UpdateFlowAnalysisRuleRequest>();
|
||||
|
||||
|
@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,13 +81,21 @@
|
|||
</mat-tab>
|
||||
<mat-tab label="Properties">
|
||||
<mat-dialog-content>
|
||||
<div class="dialog-tab-content">
|
||||
<div class="dialog-tab-content flex gap-x-3">
|
||||
<property-table
|
||||
class="w-2/3"
|
||||
formControlName="properties"
|
||||
[createNewProperty]="createNewProperty"
|
||||
[createNewService]="createNewService"
|
||||
[propertyHistory]="request.history"
|
||||
[supportsParameters]="false"
|
||||
[goToService]="goToService"></property-table>
|
||||
<property-verification
|
||||
class="w-1/3"
|
||||
[disabled]="readonly"
|
||||
[isVerifying]="(propertyVerificationStatus$ | async) === 'loading'"
|
||||
[results]="propertyVerificationResults$ | async"
|
||||
(verify)="verifyClicked(request.parameterProvider)"></property-verification>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
</mat-tab>
|
||||
|
|
|
@ -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<boolean>;
|
||||
@Input() propertyVerificationResults$!: Observable<ConfigVerificationResult[]>;
|
||||
@Input() propertyVerificationStatus$: Observable<'pending' | 'loading' | 'success'> = of('pending');
|
||||
|
||||
@Output() verify: EventEmitter<VerifyPropertiesRequestContext> = new EventEmitter<VerifyPropertiesRequestContext>();
|
||||
@Output() editParameterProvider: EventEmitter<UpdateParameterProviderRequest> =
|
||||
new EventEmitter<UpdateParameterProviderRequest>();
|
||||
|
||||
|
@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@
|
|||
[createNewProperty]="createNewProperty"
|
||||
[createNewService]="createNewService"
|
||||
[goToService]="goToService"
|
||||
[supportsParameters]="false"
|
||||
[supportsSensitiveDynamicProperties]="
|
||||
request.registryClient.component.supportsSensitiveDynamicProperties
|
||||
"></property-table>
|
||||
|
|
|
@ -112,17 +112,25 @@
|
|||
</mat-tab>
|
||||
<mat-tab label="Properties">
|
||||
<mat-dialog-content>
|
||||
<div class="dialog-tab-content">
|
||||
<div class="dialog-tab-content flex gap-x-3">
|
||||
<property-table
|
||||
class="w-2/3"
|
||||
formControlName="properties"
|
||||
[createNewProperty]="createNewProperty"
|
||||
[createNewService]="createNewService"
|
||||
[goToService]="goToService"
|
||||
[propertyHistory]="request.history"
|
||||
[supportsParameters]="false"
|
||||
[supportsSensitiveDynamicProperties]="
|
||||
request.reportingTask.component.supportsSensitiveDynamicProperties
|
||||
">
|
||||
</property-table>
|
||||
<property-verification
|
||||
class="w-1/3"
|
||||
[disabled]="readonly"
|
||||
[isVerifying]="(propertyVerificationStatus$ | async) === 'loading'"
|
||||
[results]="propertyVerificationResults$ | async"
|
||||
(verify)="verifyClicked(request.reportingTask)"></property-verification>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
</mat-tab>
|
||||
|
|
|
@ -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<boolean>;
|
||||
@Input() propertyVerificationResults$!: Observable<ConfigVerificationResult[]>;
|
||||
@Input() propertyVerificationStatus$: Observable<'pending' | 'loading' | 'success'> = of('pending');
|
||||
|
||||
@Output() verify: EventEmitter<VerifyPropertiesRequestContext> = new EventEmitter<VerifyPropertiesRequestContext>();
|
||||
@Output() editReportingTask: EventEmitter<UpdateReportingTaskRequest> =
|
||||
new EventEmitter<UpdateReportingTaskRequest>();
|
||||
|
||||
|
@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<MapTableEntry> {
|
||||
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);
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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<ConfigurationAnalysisResponse> {
|
||||
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<ConfigurationAnalysisResponse>;
|
||||
}
|
||||
|
||||
initiatePropertyVerification(request: InitiateVerificationRequest): Observable<PropertyVerificationResponse> {
|
||||
return this.httpClient.post(
|
||||
`${this.nifiCommon.stripProtocol(request.uri)}/config/verification-requests`,
|
||||
request.request
|
||||
) as Observable<PropertyVerificationResponse>;
|
||||
}
|
||||
|
||||
getPropertyVerificationRequest(requestId: string, uri: string): Observable<PropertyVerificationResponse> {
|
||||
return this.httpClient.get(
|
||||
`${this.nifiCommon.stripProtocol(uri)}/config/verification-requests/${requestId}`
|
||||
) as Observable<PropertyVerificationResponse>;
|
||||
}
|
||||
|
||||
deletePropertyVerificationRequest(requestId: string, uri: string) {
|
||||
return this.httpClient.delete(
|
||||
`${this.nifiCommon.stripProtocol(uri)}/config/verification-requests/${requestId}`
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<NiFiState> = {
|
||||
|
@ -71,5 +74,6 @@ export const rootReducers: ActionReducerMap<NiFiState> = {
|
|||
[systemDiagnosticsFeatureKey]: systemDiagnosticsReducer,
|
||||
[componentStateFeatureKey]: componentStateReducer,
|
||||
[documentationFeatureKey]: documentationReducer,
|
||||
[clusterSummaryFeatureKey]: clusterSummaryReducer
|
||||
[clusterSummaryFeatureKey]: clusterSummaryReducer,
|
||||
[propertyVerificationFeatureKey]: propertyVerificationReducer
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
}
|
|
@ -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 }>()
|
||||
);
|
|
@ -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<PropertyVerificationState>,
|
||||
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)
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
|
@ -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
|
||||
}))
|
||||
);
|
|
@ -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<PropertyVerificationState>(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
|
||||
);
|
|
@ -703,3 +703,8 @@ export interface OpenChangeComponentVersionDialogRequest {
|
|||
fetchRequest: FetchComponentVersionsRequest;
|
||||
componentVersions: DocumentedType[];
|
||||
}
|
||||
|
||||
export interface MapTableEntry {
|
||||
name: string;
|
||||
value: string | null;
|
||||
}
|
||||
|
|
|
@ -117,8 +117,9 @@
|
|||
</mat-tab>
|
||||
<mat-tab label="Properties">
|
||||
<mat-dialog-content>
|
||||
<div class="dialog-tab-content">
|
||||
<div class="dialog-tab-content flex gap-x-3">
|
||||
<property-table
|
||||
class="w-2/3"
|
||||
formControlName="properties"
|
||||
[createNewProperty]="createNewProperty"
|
||||
[createNewService]="createNewService"
|
||||
|
@ -127,9 +128,16 @@
|
|||
[convertToParameter]="convertToParameter"
|
||||
[goToService]="goToService"
|
||||
[propertyHistory]="request.history"
|
||||
[supportsParameters]="supportsParameters"
|
||||
[supportsSensitiveDynamicProperties]="
|
||||
request.controllerService.component.supportsSensitiveDynamicProperties
|
||||
"></property-table>
|
||||
<property-verification
|
||||
class="w-1/3"
|
||||
[disabled]="readonly"
|
||||
[isVerifying]="(propertyVerificationStatus$ | async) === 'loading'"
|
||||
[results]="propertyVerificationResults$ | async"
|
||||
(verify)="verifyClicked(request.controllerService)"></property-verification>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
</mat-tab>
|
||||
|
|
|
@ -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<boolean>;
|
||||
@Input() supportsParameters: boolean = true;
|
||||
@Input() propertyVerificationResults$!: Observable<ConfigVerificationResult[]>;
|
||||
@Input() propertyVerificationStatus$: Observable<'pending' | 'loading' | 'success'> = of('pending');
|
||||
|
||||
@Output() verify: EventEmitter<VerifyPropertiesRequestContext> = new EventEmitter<VerifyPropertiesRequestContext>();
|
||||
@Output() editControllerService: EventEmitter<UpdateControllerServiceRequest> =
|
||||
new EventEmitter<UpdateControllerServiceRequest>();
|
||||
|
||||
|
@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
-->
|
||||
|
||||
<div
|
||||
class="text-editor mat-elevation-z8 p-4 h-full"
|
||||
[style.width.px]="width"
|
||||
cdkDrag
|
||||
resizable
|
||||
(resized)="resized($event)">
|
||||
<form class="h-full" [formGroup]="textEditorForm" cdkTrapFocus [cdkTrapFocusAutoCapture]="!readonly">
|
||||
<div class="flex flex-col gap-y-3 h-full">
|
||||
<div class="flex flex-col gap-y-0.5 flex-1">
|
||||
<div class="editor flex-1" [class.blank]="blank">
|
||||
<ngx-codemirror
|
||||
formControlName="value"
|
||||
[options]="getOptions()"
|
||||
(mousedown)="preventDrag($event)"
|
||||
(codeMirrorLoaded)="codeMirrorLoaded($event)"></ngx-codemirror>
|
||||
</div>
|
||||
@if (!readonly) {
|
||||
<mat-checkbox
|
||||
color="primary"
|
||||
formControlName="setEmptyString"
|
||||
(mousedown)="preventDrag($event)"
|
||||
(change)="setEmptyStringChanged()"
|
||||
>Set empty string
|
||||
</mat-checkbox>
|
||||
}
|
||||
</div>
|
||||
<div class="flex justify-end items-center gap-x-2">
|
||||
@if (readonly) {
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
color="primary"
|
||||
(mousedown)="preventDrag($event)"
|
||||
(click)="cancelClicked()">
|
||||
Close
|
||||
</button>
|
||||
} @else {
|
||||
<button mat-button type="button" (mousedown)="preventDrag($event)" (click)="cancelClicked()">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
[disabled]="!textEditorForm.dirty || textEditorForm.invalid"
|
||||
(mousedown)="preventDrag($event)"
|
||||
type="button"
|
||||
color="primary"
|
||||
(click)="okClicked()"
|
||||
mat-button>
|
||||
Ok
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -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;
|
||||
}
|
|
@ -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<TextEditor>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TextEditor]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TextEditor);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -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<string | null> = new EventEmitter<string | null>();
|
||||
@Output() cancel: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<div class="map-table flex flex-col h-full gap-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="font-bold flex-1">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
@if (!isDisabled) {
|
||||
<div>
|
||||
<button mat-icon-button color="primary" type="button" (click)="newEntryClicked()">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="listing-table flex-1 relative">
|
||||
<div class="absolute inset-0 overflow-y-auto overflow-x-hidden">
|
||||
<table mat-table #mapTable [dataSource]="dataSource">
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef>Name</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<div class="flex justify-between items-center">
|
||||
<div
|
||||
class="whitespace-nowrap overflow-hidden text-ellipsis leading-normal"
|
||||
[title]="item.entry.name">
|
||||
{{ item.entry.name }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Value Column -->
|
||||
<ng-container matColumnDef="value">
|
||||
<th mat-header-cell *matHeaderCellDef>Value</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<div
|
||||
[id]="formatId(item)"
|
||||
class="pointer"
|
||||
cdkOverlayOrigin
|
||||
#trigger="cdkOverlayOrigin"
|
||||
(click)="openEditor(trigger, item, $event)">
|
||||
@if (isNull(item.entry.value)) {
|
||||
<div class="unset surface-color">No value set</div>
|
||||
} @else {
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
isEmptyString(item.entry.value) ? blank : nonBlank;
|
||||
context: { $implicit: item.entry.value }
|
||||
"></ng-container>
|
||||
<ng-template #blank>
|
||||
<div class="empty surface-color">Empty string set</div>
|
||||
</ng-template>
|
||||
<ng-template #nonBlank let-value>
|
||||
<div class="flex justify-between items-center">
|
||||
<div
|
||||
class="whitespace-nowrap overflow-hidden text-ellipsis leading-normal"
|
||||
[title]="value">
|
||||
{{ value }}
|
||||
</div>
|
||||
@if (hasExtraWhitespace(value)) {
|
||||
<div
|
||||
class="fa fa-info primary-color"
|
||||
nifiTooltip
|
||||
[tooltipComponentType]="TextTip"
|
||||
tooltipInputData="The specified value contains leading and/or trailing whitespace character(s). This could produce unexpected results if it was not intentional."></div>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<div class="flex items-center justify-end gap-x-2">
|
||||
@if (!isDisabled) {
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
[matMenuTriggerFor]="actionMenu"
|
||||
class="h-16 w-16 flex items-center justify-center icon global-menu">
|
||||
<i class="fa fa-ellipsis-v"></i>
|
||||
</button>
|
||||
}
|
||||
<mat-menu #actionMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="deleteProperty(item)">
|
||||
<i class="fa fa-trash primary-color mr-2"></i>
|
||||
Delete
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let row; let even = even; columns: displayedColumns"
|
||||
(click)="select(row)"
|
||||
[class.selected]="isSelected(row)"
|
||||
[class.even]="even"></tr>
|
||||
</table>
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="editorTrigger"
|
||||
[cdkConnectedOverlayPositions]="editorPositions"
|
||||
[cdkConnectedOverlayHasBackdrop]="true"
|
||||
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
|
||||
[cdkConnectedOverlayOpen]="editorOpen"
|
||||
(detach)="closeEditor()">
|
||||
<text-editor
|
||||
[item]="editorItem"
|
||||
[width]="editorWidth"
|
||||
[readonly]="isDisabled"
|
||||
(ok)="saveValue(editorItem, $event)"
|
||||
(cancel)="closeEditor()"></text-editor>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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.
|
||||
*/
|
|
@ -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<MapTable>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MapTable]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MapTable);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -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<MapTableEntry>;
|
||||
@Input() reportChangesOnly: boolean = false;
|
||||
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
itemLookup: Map<string, MapTableItem> = new Map<string, MapTableItem>();
|
||||
displayedColumns: string[] = ['name', 'value', 'actions'];
|
||||
dataSource: MatTableDataSource<MapTableItem> = new MatTableDataSource<MapTableItem>();
|
||||
selectedItem!: MapTableItem;
|
||||
|
||||
@ViewChildren('trigger') valueTriggers!: QueryList<CdkOverlayOrigin>;
|
||||
|
||||
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<MapTableItem>(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;
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<h2 mat-dialog-title>Add {{ data.entryTypeLabel || 'Entry' }}</h2>
|
||||
<form class="new-entry-form" [formGroup]="newEntryForm">
|
||||
<mat-dialog-content>
|
||||
<div class="mb-2">
|
||||
<mat-form-field>
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" type="text" />
|
||||
@if (name.invalid) {
|
||||
<mat-error>{{ getNameErrorMessage() }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button
|
||||
mat-button
|
||||
[disabled]="!newEntryForm.dirty || newEntryForm.invalid"
|
||||
(click)="addClicked()"
|
||||
color="primary">
|
||||
Ok
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<NewMapTableEntryDialog>;
|
||||
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();
|
||||
});
|
||||
});
|
|
@ -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<string> = new EventEmitter<string>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
<div class="font-bold">Required field</div>
|
||||
@if (!isDisabled) {
|
||||
<div>
|
||||
<!-- TODO Property Verification -->
|
||||
<button mat-icon-button color="primary" type="button" (click)="newPropertyClicked()">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
|
@ -181,7 +180,7 @@
|
|||
@if (hasAllowableValues(editorItem)) {
|
||||
<combo-editor
|
||||
[item]="editorItem"
|
||||
[parameters]="editorParameters"
|
||||
[parameters]="editorParameters || []"
|
||||
[width]="editorWidth"
|
||||
[readonly]="isDisabled"
|
||||
(ok)="savePropertyValue(editorItem, $event)"
|
||||
|
|
|
@ -114,6 +114,7 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
|
|||
@Input() goToService!: (serviceId: string) => 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)
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<h2 mat-dialog-title>Verifying Properties</h2>
|
||||
<div class="change-version-progress">
|
||||
<mat-dialog-content>
|
||||
<div class="flex flex-col gap-y-4">
|
||||
@if (verificationRequest$ | async; as verificationRequest) {
|
||||
<div class="accent-color font-medium">
|
||||
@if (verificationRequest.complete) {
|
||||
Complete
|
||||
} @else {
|
||||
{{ verificationRequest.state }}
|
||||
}
|
||||
</div>
|
||||
<div class="w-full flex flex-col items-center">
|
||||
<mat-progress-bar
|
||||
mode="determinate"
|
||||
[value]="verificationRequest.percentCompleted"></mat-progress-bar>
|
||||
<div class="accent-color font-medium">{{ verificationRequest.percentCompleted }}%</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
@if (verificationRequest$ | async; as verificationRequest) {
|
||||
<button mat-button mat-dialog-close (click)="stop(verificationRequest)" color="primary">Stop</button>
|
||||
}
|
||||
</mat-dialog-actions>
|
||||
</div>
|
|
@ -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.
|
||||
*/
|
|
@ -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<PropertyVerificationProgress>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PropertyVerificationProgress]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PropertyVerificationProgress);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -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<PropertyVerificationRequest | null> = of(null);
|
||||
@Output() stopVerification = new EventEmitter<PropertyVerificationRequest>();
|
||||
|
||||
stop(request: PropertyVerificationRequest) {
|
||||
this.stopVerification.next(request);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
-->
|
||||
|
||||
<h2 mat-dialog-title>Referenced Attributes</h2>
|
||||
<form class="referenced-attributes-form" [formGroup]="referencedAttributesForm">
|
||||
<mat-dialog-content>
|
||||
<div class="dialog-content max-h-96">
|
||||
<map-table formControlName="attributes" [createNew]="createNew" [reportChangesOnly]="false">
|
||||
<div class="flex flex-row justify-between items-center">
|
||||
<div>
|
||||
Enter Attribute Values
|
||||
<i
|
||||
class="fa fa-info-circle"
|
||||
nifiTooltip
|
||||
[tooltipComponentType]="TextTip"
|
||||
tooltipInputData="Supply attribute values that should be considered when performing property verification."></i>
|
||||
</div>
|
||||
<button
|
||||
mat-icon-button
|
||||
[disabled]="isEmpty()"
|
||||
color="primary"
|
||||
type="button"
|
||||
(click)="clearAttributesClicked()">
|
||||
<i class="fa fa-eraser"></i>
|
||||
</button>
|
||||
</div>
|
||||
</map-table>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button mat-button mat-dialog-close cdkFocusInitial (click)="verifyClicked()" color="primary">Verify</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
|
@ -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.
|
||||
*/
|
|
@ -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<ReferencedAttributesDialog>;
|
||||
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();
|
||||
});
|
||||
});
|
|
@ -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<MapTableEntry>;
|
||||
@Output() verify = new EventEmitter<any>();
|
||||
|
||||
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;
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<div class="property-verification flex flex-col h-full gap-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="font-bold">Verification</div>
|
||||
<div>
|
||||
<button mat-icon-button color="primary" type="button" [disabled]="disabled" (click)="verifyClicked()">
|
||||
<i class="fa fa-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full border p-2 overflow-y-auto">
|
||||
@if (disabled) {
|
||||
<div class="verification-disabled unset">Property verification is disabled</div>
|
||||
} @else if (isVerifying) {
|
||||
<div class="empty">Verifying properties...</div>
|
||||
} @else if (results.length > 0) {
|
||||
<div class="verification-results flex flex-col gap-y-2">
|
||||
@for (result of results; track results) {
|
||||
<div>
|
||||
<div class="flex gap-x-1">
|
||||
@switch (result.outcome) {
|
||||
@case (Outcome.SUCCESSFUL) {
|
||||
<div class="fa fa-check success-color text-lg"></div>
|
||||
}
|
||||
@case (Outcome.FAILED) {
|
||||
<div class="fa fa-times warn-color text-lg"></div>
|
||||
}
|
||||
@case (Outcome.SKIPPED) {
|
||||
<div class="fa fa-exclamation warn-color-lighter text-lg"></div>
|
||||
}
|
||||
}
|
||||
<div class="flex flex-col">
|
||||
<div class="font-bold">{{ result.verificationStepName }}</div>
|
||||
<div class="verification-explanation text-xs opacity-80">{{ result.explanation }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="empty">Click the button above to verify this component.</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<PropertyVerification>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PropertyVerification]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PropertyVerification);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -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<void> = new EventEmitter<void>();
|
||||
|
||||
verifyClicked(): void {
|
||||
this.verify.next();
|
||||
}
|
||||
|
||||
protected readonly Outcome = Outcome;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -31,10 +31,7 @@ describe('SystemDiagnosticsDialog', () => {
|
|||
imports: [SystemDiagnosticsDialog],
|
||||
providers: [
|
||||
provideMockStore({ initialState: initialSystemDiagnosticsState }),
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: null
|
||||
}
|
||||
{ provide: MatDialogRef, useValue: null }
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(SystemDiagnosticsDialog);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue