mcgilman commented on code in PR #8965: URL: https://github.com/apache/nifi/pull/8965#discussion_r1811538388
########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.html: ########## @@ -0,0 +1,80 @@ +<!-- + ~ 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>Move Controller Service</h2> +<form class="controller-service-move-form" [formGroup]="moveControllerServiceForm"> + <mat-dialog-content> + <div class="py-4 flex gap-x-3"> + <div class="flex basis-1/2 flex-col gap-y-4 pr-4 overflow-hidden"> + <div> + <div>Service</div> + <div class="tertiary-color font-medium truncate" [title]="controllerService.component.name"> + {{ controllerService.component.name }} + </div> + </div> + <div> + <mat-form-field> + <mat-label>Process Group:</mat-label> + <mat-select formControlName="processGroups"> + @for (option of controllerServiceActionProcessGroups; track option) { + <div + nifiTooltip + [tooltipDisabled]="!option.disabled" + [tooltipComponentType]="TextTip" + [tooltipInputData]="option.description" + [delayClose]="false"> + <mat-option [value]="option.value" [disabled]="option.disabled"> + <div + *ngIf="option.description != undefined; else valid" + class="pointer fa fa-warning has-errors caution-color" + style="padding: 3px"></div> + <ng-template #valid> + <div nifiTooltip [delayClose]="false"></div> + </ng-template> + + {{ option.text }} + </mat-option> + </div> + } + </mat-select> + </mat-form-field> + </div> + </div> + <div class="flex basis-1/2 flex-col"> + <div> + Referencing Components + <i + class="fa fa-info-circle" + nifiTooltip + [tooltipComponentType]="TextTip" + tooltipInputData="Other components referencing this controller service."></i> + </div> + <div class="relative h-full border" style="min-height: 320px"> + <div class="absolute inset-0 overflow-y-auto p-1"> + <controller-service-references + [serviceReferences]="controllerService.component.referencingComponents" + [goToReferencingComponent]="goToReferencingComponent"></controller-service-references> + </div> + </div> + </div> + </div> + </mat-dialog-content> + <mat-dialog-actions align="end"> + <button mat-button mat-dialog-close>Cancel</button> + <button type="button" color="primary" (click)="submitForm()" [disabled]="disableSubmit" mat-button>Move</button> Review Comment: Can you use the state of the form instead of introducing a new field? I didn't try this out but maybe something like the following: ```suggestion <button type="button" color="primary" (click)="submitForm()" [disabled]="moveControllerServiceForm.invalid" mat-button>Move</button> ``` ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts: ########## @@ -0,0 +1,247 @@ +/* + * 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, Inject, Input } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + ControllerServiceEntity, + ControllerServiceReferencingComponent, + ControllerServiceReferencingComponentEntity +} from '../../../../state/shared'; +import { MatInputModule } from '@angular/material/input'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSelectModule } from '@angular/material/select'; +import { ControllerServiceApi } from '../../../../ui/common/controller-service/controller-service-api/controller-service-api.component'; +import { ControllerServiceReferences } from '../../../../ui/common/controller-service/controller-service-references/controller-service-references.component'; +import { NifiSpinnerDirective } from '../../../../ui/common/spinner/nifi-spinner.directive'; +import { TextTip, NifiTooltipDirective, SelectOption } from '@nifi/shared'; +import { Store } from '@ngrx/store'; +import { CloseOnEscapeDialog } from '@nifi/shared'; +import { moveControllerService } from '../../state/controller-services/controller-services.actions'; +import { NiFiState } from 'apps/nifi/src/app/state'; +import { MoveControllerServiceDialogRequestSuccess } from '../../state/controller-services'; +import { NgIf } from '@angular/common'; +import { BreadcrumbEntity } from '../../state/shared'; + +@Component({ + selector: 'move-controller-service', + standalone: true, + templateUrl: './move-controller-service.component.html', + imports: [ + ReactiveFormsModule, + MatDialogModule, + MatInputModule, + MatCheckboxModule, + MatButtonModule, + MatTabsModule, + MatOptionModule, + MatSelectModule, + ControllerServiceApi, + ControllerServiceReferences, + AsyncPipe, + NifiSpinnerDirective, + NifiTooltipDirective, + NgTemplateOutlet, + NgIf + ], + styleUrls: ['./move-controller-service.component.scss'] +}) +export class MoveControllerService extends CloseOnEscapeDialog { + @Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void; + protected readonly TextTip = TextTip; + protected controllerServiceActionProcessGroups: SelectOption[] = []; + protected disableSubmit: boolean = false; + + controllerService: ControllerServiceEntity; + + moveControllerServiceForm: FormGroup; + + constructor( + @Inject(MAT_DIALOG_DATA) public request: MoveControllerServiceDialogRequestSuccess, + private store: Store<NiFiState>, + private formBuilder: FormBuilder + ) { + super(); + + this.controllerService = request.controllerService; + + // build the form + this.moveControllerServiceForm = this.formBuilder.group({ + processGroups: new FormControl('Process Group', Validators.required) Review Comment: `Process Group` is not a valid value for this field. We could initialize the value to `null` but we're setting the value below. We could instead determine the value before creating the form and then just use the correct value when creating the `FormControl`. ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts: ########## @@ -0,0 +1,247 @@ +/* + * 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, Inject, Input } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + ControllerServiceEntity, + ControllerServiceReferencingComponent, + ControllerServiceReferencingComponentEntity +} from '../../../../state/shared'; +import { MatInputModule } from '@angular/material/input'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSelectModule } from '@angular/material/select'; +import { ControllerServiceApi } from '../../../../ui/common/controller-service/controller-service-api/controller-service-api.component'; +import { ControllerServiceReferences } from '../../../../ui/common/controller-service/controller-service-references/controller-service-references.component'; +import { NifiSpinnerDirective } from '../../../../ui/common/spinner/nifi-spinner.directive'; +import { TextTip, NifiTooltipDirective, SelectOption } from '@nifi/shared'; +import { Store } from '@ngrx/store'; +import { CloseOnEscapeDialog } from '@nifi/shared'; +import { moveControllerService } from '../../state/controller-services/controller-services.actions'; +import { NiFiState } from 'apps/nifi/src/app/state'; +import { MoveControllerServiceDialogRequestSuccess } from '../../state/controller-services'; +import { NgIf } from '@angular/common'; +import { BreadcrumbEntity } from '../../state/shared'; + +@Component({ + selector: 'move-controller-service', + standalone: true, + templateUrl: './move-controller-service.component.html', + imports: [ + ReactiveFormsModule, + MatDialogModule, + MatInputModule, + MatCheckboxModule, + MatButtonModule, + MatTabsModule, + MatOptionModule, + MatSelectModule, + ControllerServiceApi, + ControllerServiceReferences, + AsyncPipe, + NifiSpinnerDirective, + NifiTooltipDirective, + NgTemplateOutlet, + NgIf + ], + styleUrls: ['./move-controller-service.component.scss'] +}) +export class MoveControllerService extends CloseOnEscapeDialog { + @Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void; + protected readonly TextTip = TextTip; + protected controllerServiceActionProcessGroups: SelectOption[] = []; + protected disableSubmit: boolean = false; + + controllerService: ControllerServiceEntity; + + moveControllerServiceForm: FormGroup; + + constructor( + @Inject(MAT_DIALOG_DATA) public request: MoveControllerServiceDialogRequestSuccess, + private store: Store<NiFiState>, + private formBuilder: FormBuilder + ) { + super(); + + this.controllerService = request.controllerService; + + // build the form + this.moveControllerServiceForm = this.formBuilder.group({ + processGroups: new FormControl('Process Group', Validators.required) + }); + + const parentControllerServices = request.parentControllerServices.filter( + (cs) => cs.parentGroupId != request.controllerService.parentGroupId + ); + + const processGroups: SelectOption[] = []; + this.loadParentOption(request.breadcrumb, parentControllerServices, processGroups); + + this.loadChildOptions(request.childProcessGroupOptions, request.processGroupEntity, processGroups); + this.controllerServiceActionProcessGroups = processGroups; + + const firstEnabled = processGroups.findIndex((pg) => !pg.disabled); + if (firstEnabled != -1) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[firstEnabled].value); + } else { + if (processGroups.length > 0) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[0].value); + } + this.disableSubmit = true; + } + } + + loadParentOption( + breadcrumb: BreadcrumbEntity, + parentControllerServices: ControllerServiceEntity[], + processGroups: SelectOption[] + ) { + if (breadcrumb.parentBreadcrumb != undefined) { + const parentBreadcrumb = breadcrumb.parentBreadcrumb; + if (parentBreadcrumb.permissions.canRead && parentBreadcrumb.permissions.canWrite) { + const option: SelectOption = { + text: parentBreadcrumb.breadcrumb.name + ' (Parent)', Review Comment: Please convert to a template string. ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts: ########## @@ -0,0 +1,247 @@ +/* + * 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, Inject, Input } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + ControllerServiceEntity, + ControllerServiceReferencingComponent, + ControllerServiceReferencingComponentEntity +} from '../../../../state/shared'; +import { MatInputModule } from '@angular/material/input'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSelectModule } from '@angular/material/select'; +import { ControllerServiceApi } from '../../../../ui/common/controller-service/controller-service-api/controller-service-api.component'; +import { ControllerServiceReferences } from '../../../../ui/common/controller-service/controller-service-references/controller-service-references.component'; +import { NifiSpinnerDirective } from '../../../../ui/common/spinner/nifi-spinner.directive'; +import { TextTip, NifiTooltipDirective, SelectOption } from '@nifi/shared'; +import { Store } from '@ngrx/store'; +import { CloseOnEscapeDialog } from '@nifi/shared'; +import { moveControllerService } from '../../state/controller-services/controller-services.actions'; +import { NiFiState } from 'apps/nifi/src/app/state'; +import { MoveControllerServiceDialogRequestSuccess } from '../../state/controller-services'; +import { NgIf } from '@angular/common'; +import { BreadcrumbEntity } from '../../state/shared'; + +@Component({ + selector: 'move-controller-service', + standalone: true, + templateUrl: './move-controller-service.component.html', + imports: [ + ReactiveFormsModule, + MatDialogModule, + MatInputModule, + MatCheckboxModule, + MatButtonModule, + MatTabsModule, + MatOptionModule, + MatSelectModule, + ControllerServiceApi, + ControllerServiceReferences, + AsyncPipe, + NifiSpinnerDirective, + NifiTooltipDirective, + NgTemplateOutlet, + NgIf + ], + styleUrls: ['./move-controller-service.component.scss'] +}) +export class MoveControllerService extends CloseOnEscapeDialog { + @Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void; + protected readonly TextTip = TextTip; + protected controllerServiceActionProcessGroups: SelectOption[] = []; + protected disableSubmit: boolean = false; + + controllerService: ControllerServiceEntity; + + moveControllerServiceForm: FormGroup; + + constructor( + @Inject(MAT_DIALOG_DATA) public request: MoveControllerServiceDialogRequestSuccess, + private store: Store<NiFiState>, + private formBuilder: FormBuilder + ) { + super(); + + this.controllerService = request.controllerService; + + // build the form + this.moveControllerServiceForm = this.formBuilder.group({ + processGroups: new FormControl('Process Group', Validators.required) + }); + + const parentControllerServices = request.parentControllerServices.filter( + (cs) => cs.parentGroupId != request.controllerService.parentGroupId + ); + + const processGroups: SelectOption[] = []; + this.loadParentOption(request.breadcrumb, parentControllerServices, processGroups); + + this.loadChildOptions(request.childProcessGroupOptions, request.processGroupEntity, processGroups); + this.controllerServiceActionProcessGroups = processGroups; + + const firstEnabled = processGroups.findIndex((pg) => !pg.disabled); + if (firstEnabled != -1) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[firstEnabled].value); + } else { + if (processGroups.length > 0) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[0].value); + } + this.disableSubmit = true; + } + } + + loadParentOption( + breadcrumb: BreadcrumbEntity, + parentControllerServices: ControllerServiceEntity[], + processGroups: SelectOption[] + ) { + if (breadcrumb.parentBreadcrumb != undefined) { + const parentBreadcrumb = breadcrumb.parentBreadcrumb; + if (parentBreadcrumb.permissions.canRead && parentBreadcrumb.permissions.canWrite) { + const option: SelectOption = { + text: parentBreadcrumb.breadcrumb.name + ' (Parent)', + value: parentBreadcrumb.breadcrumb.id + }; + + let errorMsg = ''; + const descriptors = this.controllerService.component.descriptors; + for (const descriptor in descriptors) { + if (descriptors[descriptor].identifiesControllerService != undefined) { Review Comment: ```suggestion if (descriptors[descriptor].identifiesControllerService) { ``` ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts: ########## @@ -0,0 +1,247 @@ +/* + * 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, Inject, Input } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + ControllerServiceEntity, + ControllerServiceReferencingComponent, + ControllerServiceReferencingComponentEntity +} from '../../../../state/shared'; +import { MatInputModule } from '@angular/material/input'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSelectModule } from '@angular/material/select'; +import { ControllerServiceApi } from '../../../../ui/common/controller-service/controller-service-api/controller-service-api.component'; +import { ControllerServiceReferences } from '../../../../ui/common/controller-service/controller-service-references/controller-service-references.component'; +import { NifiSpinnerDirective } from '../../../../ui/common/spinner/nifi-spinner.directive'; +import { TextTip, NifiTooltipDirective, SelectOption } from '@nifi/shared'; +import { Store } from '@ngrx/store'; +import { CloseOnEscapeDialog } from '@nifi/shared'; +import { moveControllerService } from '../../state/controller-services/controller-services.actions'; +import { NiFiState } from 'apps/nifi/src/app/state'; +import { MoveControllerServiceDialogRequestSuccess } from '../../state/controller-services'; +import { NgIf } from '@angular/common'; +import { BreadcrumbEntity } from '../../state/shared'; + +@Component({ + selector: 'move-controller-service', + standalone: true, + templateUrl: './move-controller-service.component.html', + imports: [ + ReactiveFormsModule, + MatDialogModule, + MatInputModule, + MatCheckboxModule, + MatButtonModule, + MatTabsModule, + MatOptionModule, + MatSelectModule, + ControllerServiceApi, + ControllerServiceReferences, + AsyncPipe, + NifiSpinnerDirective, + NifiTooltipDirective, + NgTemplateOutlet, + NgIf + ], + styleUrls: ['./move-controller-service.component.scss'] +}) +export class MoveControllerService extends CloseOnEscapeDialog { + @Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void; + protected readonly TextTip = TextTip; + protected controllerServiceActionProcessGroups: SelectOption[] = []; + protected disableSubmit: boolean = false; + + controllerService: ControllerServiceEntity; + + moveControllerServiceForm: FormGroup; + + constructor( + @Inject(MAT_DIALOG_DATA) public request: MoveControllerServiceDialogRequestSuccess, + private store: Store<NiFiState>, + private formBuilder: FormBuilder + ) { + super(); + + this.controllerService = request.controllerService; + + // build the form + this.moveControllerServiceForm = this.formBuilder.group({ + processGroups: new FormControl('Process Group', Validators.required) + }); + + const parentControllerServices = request.parentControllerServices.filter( + (cs) => cs.parentGroupId != request.controllerService.parentGroupId + ); + + const processGroups: SelectOption[] = []; + this.loadParentOption(request.breadcrumb, parentControllerServices, processGroups); + + this.loadChildOptions(request.childProcessGroupOptions, request.processGroupEntity, processGroups); + this.controllerServiceActionProcessGroups = processGroups; + + const firstEnabled = processGroups.findIndex((pg) => !pg.disabled); + if (firstEnabled != -1) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[firstEnabled].value); + } else { + if (processGroups.length > 0) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[0].value); + } + this.disableSubmit = true; + } + } + + loadParentOption( + breadcrumb: BreadcrumbEntity, + parentControllerServices: ControllerServiceEntity[], + processGroups: SelectOption[] + ) { + if (breadcrumb.parentBreadcrumb != undefined) { + const parentBreadcrumb = breadcrumb.parentBreadcrumb; + if (parentBreadcrumb.permissions.canRead && parentBreadcrumb.permissions.canWrite) { + const option: SelectOption = { + text: parentBreadcrumb.breadcrumb.name + ' (Parent)', + value: parentBreadcrumb.breadcrumb.id + }; + + let errorMsg = ''; + const descriptors = this.controllerService.component.descriptors; + for (const descriptor in descriptors) { + if (descriptors[descriptor].identifiesControllerService != undefined) { + const controllerId = this.controllerService.component.properties[descriptors[descriptor].name]; + if ( + controllerId != null && + !parentControllerServices.some((service) => service.id == controllerId) + ) { + errorMsg += '[' + descriptors[descriptor].name + ']'; + } + } + } + + if (errorMsg != '') { + option.description = + 'The following properties reference controller services that would be out of scope for this ' + + 'process group: ' + + errorMsg; + option.disabled = true; + } else { + option.disabled = false; + } + + processGroups.push(option); + } + } + } + + loadChildOptions( + childProcessGroupOptions: SelectOption[], + currentProcessGroupEntity: any, + processGroups: SelectOption[] + ) { + const referencingComponents: ControllerServiceReferencingComponentEntity[] = + this.controllerService.component.referencingComponents; + childProcessGroupOptions.forEach((child: SelectOption) => { + const option: SelectOption = { + text: child.text, + value: child.value + }; + + const root = this.getProcessGroupById(currentProcessGroupEntity.component, child.value ?? ''); + let errorMsg = ''; + + if (root == null) { + option.description = 'Error loading process group root.'; + option.disabled = true; + processGroups.push(option); + } else { + for (const component of referencingComponents) { + if (!this.processGroupContainsComponent(root, component.component.groupId)) { + errorMsg += '[' + component.component.name + ']'; + } + } + + if (errorMsg != '') { + option.description = + 'The following components would be out of scope for this process group: ' + errorMsg; + option.disabled = true; + } else { + option.disabled = false; + } + + processGroups.push(option); + } + }); + } + + processGroupContainsComponent(processGroup: any, groupId: string): boolean { + if (processGroup.contents != undefined) { Review Comment: ```suggestion if (processGroup.contents) { ``` ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts: ########## @@ -0,0 +1,247 @@ +/* + * 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, Inject, Input } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + ControllerServiceEntity, + ControllerServiceReferencingComponent, + ControllerServiceReferencingComponentEntity +} from '../../../../state/shared'; +import { MatInputModule } from '@angular/material/input'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSelectModule } from '@angular/material/select'; +import { ControllerServiceApi } from '../../../../ui/common/controller-service/controller-service-api/controller-service-api.component'; +import { ControllerServiceReferences } from '../../../../ui/common/controller-service/controller-service-references/controller-service-references.component'; +import { NifiSpinnerDirective } from '../../../../ui/common/spinner/nifi-spinner.directive'; +import { TextTip, NifiTooltipDirective, SelectOption } from '@nifi/shared'; +import { Store } from '@ngrx/store'; +import { CloseOnEscapeDialog } from '@nifi/shared'; +import { moveControllerService } from '../../state/controller-services/controller-services.actions'; +import { NiFiState } from 'apps/nifi/src/app/state'; +import { MoveControllerServiceDialogRequestSuccess } from '../../state/controller-services'; +import { NgIf } from '@angular/common'; +import { BreadcrumbEntity } from '../../state/shared'; + +@Component({ + selector: 'move-controller-service', + standalone: true, + templateUrl: './move-controller-service.component.html', + imports: [ + ReactiveFormsModule, + MatDialogModule, + MatInputModule, + MatCheckboxModule, + MatButtonModule, + MatTabsModule, + MatOptionModule, + MatSelectModule, + ControllerServiceApi, + ControllerServiceReferences, + AsyncPipe, + NifiSpinnerDirective, + NifiTooltipDirective, + NgTemplateOutlet, + NgIf + ], + styleUrls: ['./move-controller-service.component.scss'] +}) +export class MoveControllerService extends CloseOnEscapeDialog { + @Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void; + protected readonly TextTip = TextTip; + protected controllerServiceActionProcessGroups: SelectOption[] = []; + protected disableSubmit: boolean = false; + + controllerService: ControllerServiceEntity; + + moveControllerServiceForm: FormGroup; + + constructor( + @Inject(MAT_DIALOG_DATA) public request: MoveControllerServiceDialogRequestSuccess, + private store: Store<NiFiState>, + private formBuilder: FormBuilder + ) { + super(); + + this.controllerService = request.controllerService; + + // build the form + this.moveControllerServiceForm = this.formBuilder.group({ + processGroups: new FormControl('Process Group', Validators.required) + }); + + const parentControllerServices = request.parentControllerServices.filter( + (cs) => cs.parentGroupId != request.controllerService.parentGroupId + ); + + const processGroups: SelectOption[] = []; + this.loadParentOption(request.breadcrumb, parentControllerServices, processGroups); + + this.loadChildOptions(request.childProcessGroupOptions, request.processGroupEntity, processGroups); + this.controllerServiceActionProcessGroups = processGroups; + + const firstEnabled = processGroups.findIndex((pg) => !pg.disabled); + if (firstEnabled != -1) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[firstEnabled].value); + } else { + if (processGroups.length > 0) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[0].value); + } + this.disableSubmit = true; + } + } + + loadParentOption( + breadcrumb: BreadcrumbEntity, + parentControllerServices: ControllerServiceEntity[], + processGroups: SelectOption[] + ) { + if (breadcrumb.parentBreadcrumb != undefined) { + const parentBreadcrumb = breadcrumb.parentBreadcrumb; + if (parentBreadcrumb.permissions.canRead && parentBreadcrumb.permissions.canWrite) { + const option: SelectOption = { + text: parentBreadcrumb.breadcrumb.name + ' (Parent)', + value: parentBreadcrumb.breadcrumb.id + }; + + let errorMsg = ''; + const descriptors = this.controllerService.component.descriptors; + for (const descriptor in descriptors) { + if (descriptors[descriptor].identifiesControllerService != undefined) { + const controllerId = this.controllerService.component.properties[descriptors[descriptor].name]; + if ( + controllerId != null && + !parentControllerServices.some((service) => service.id == controllerId) + ) { + errorMsg += '[' + descriptors[descriptor].name + ']'; + } + } + } + + if (errorMsg != '') { + option.description = + 'The following properties reference controller services that would be out of scope for this ' + + 'process group: ' + + errorMsg; Review Comment: Please convert to a template string. ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts: ########## @@ -0,0 +1,247 @@ +/* + * 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, Inject, Input } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + ControllerServiceEntity, + ControllerServiceReferencingComponent, + ControllerServiceReferencingComponentEntity +} from '../../../../state/shared'; +import { MatInputModule } from '@angular/material/input'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSelectModule } from '@angular/material/select'; +import { ControllerServiceApi } from '../../../../ui/common/controller-service/controller-service-api/controller-service-api.component'; +import { ControllerServiceReferences } from '../../../../ui/common/controller-service/controller-service-references/controller-service-references.component'; +import { NifiSpinnerDirective } from '../../../../ui/common/spinner/nifi-spinner.directive'; +import { TextTip, NifiTooltipDirective, SelectOption } from '@nifi/shared'; +import { Store } from '@ngrx/store'; +import { CloseOnEscapeDialog } from '@nifi/shared'; +import { moveControllerService } from '../../state/controller-services/controller-services.actions'; +import { NiFiState } from 'apps/nifi/src/app/state'; +import { MoveControllerServiceDialogRequestSuccess } from '../../state/controller-services'; +import { NgIf } from '@angular/common'; +import { BreadcrumbEntity } from '../../state/shared'; + +@Component({ + selector: 'move-controller-service', + standalone: true, + templateUrl: './move-controller-service.component.html', + imports: [ + ReactiveFormsModule, + MatDialogModule, + MatInputModule, + MatCheckboxModule, + MatButtonModule, + MatTabsModule, + MatOptionModule, + MatSelectModule, + ControllerServiceApi, + ControllerServiceReferences, + AsyncPipe, + NifiSpinnerDirective, + NifiTooltipDirective, + NgTemplateOutlet, + NgIf + ], + styleUrls: ['./move-controller-service.component.scss'] +}) +export class MoveControllerService extends CloseOnEscapeDialog { Review Comment: Please move the `move-controller-service` folder under `src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/controller-service`. ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts: ########## @@ -0,0 +1,247 @@ +/* + * 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, Inject, Input } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + ControllerServiceEntity, + ControllerServiceReferencingComponent, + ControllerServiceReferencingComponentEntity +} from '../../../../state/shared'; +import { MatInputModule } from '@angular/material/input'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSelectModule } from '@angular/material/select'; +import { ControllerServiceApi } from '../../../../ui/common/controller-service/controller-service-api/controller-service-api.component'; +import { ControllerServiceReferences } from '../../../../ui/common/controller-service/controller-service-references/controller-service-references.component'; +import { NifiSpinnerDirective } from '../../../../ui/common/spinner/nifi-spinner.directive'; +import { TextTip, NifiTooltipDirective, SelectOption } from '@nifi/shared'; +import { Store } from '@ngrx/store'; +import { CloseOnEscapeDialog } from '@nifi/shared'; +import { moveControllerService } from '../../state/controller-services/controller-services.actions'; +import { NiFiState } from 'apps/nifi/src/app/state'; +import { MoveControllerServiceDialogRequestSuccess } from '../../state/controller-services'; +import { NgIf } from '@angular/common'; +import { BreadcrumbEntity } from '../../state/shared'; + +@Component({ + selector: 'move-controller-service', + standalone: true, + templateUrl: './move-controller-service.component.html', + imports: [ + ReactiveFormsModule, + MatDialogModule, + MatInputModule, + MatCheckboxModule, + MatButtonModule, + MatTabsModule, + MatOptionModule, + MatSelectModule, + ControllerServiceApi, + ControllerServiceReferences, + AsyncPipe, + NifiSpinnerDirective, + NifiTooltipDirective, + NgTemplateOutlet, + NgIf + ], + styleUrls: ['./move-controller-service.component.scss'] +}) +export class MoveControllerService extends CloseOnEscapeDialog { + @Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void; + protected readonly TextTip = TextTip; + protected controllerServiceActionProcessGroups: SelectOption[] = []; + protected disableSubmit: boolean = false; + + controllerService: ControllerServiceEntity; + + moveControllerServiceForm: FormGroup; + + constructor( + @Inject(MAT_DIALOG_DATA) public request: MoveControllerServiceDialogRequestSuccess, + private store: Store<NiFiState>, + private formBuilder: FormBuilder + ) { + super(); + + this.controllerService = request.controllerService; + + // build the form + this.moveControllerServiceForm = this.formBuilder.group({ + processGroups: new FormControl('Process Group', Validators.required) + }); + + const parentControllerServices = request.parentControllerServices.filter( + (cs) => cs.parentGroupId != request.controllerService.parentGroupId + ); + + const processGroups: SelectOption[] = []; + this.loadParentOption(request.breadcrumb, parentControllerServices, processGroups); + + this.loadChildOptions(request.childProcessGroupOptions, request.processGroupEntity, processGroups); + this.controllerServiceActionProcessGroups = processGroups; + + const firstEnabled = processGroups.findIndex((pg) => !pg.disabled); + if (firstEnabled != -1) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[firstEnabled].value); + } else { + if (processGroups.length > 0) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[0].value); + } + this.disableSubmit = true; + } + } + + loadParentOption( + breadcrumb: BreadcrumbEntity, + parentControllerServices: ControllerServiceEntity[], + processGroups: SelectOption[] + ) { + if (breadcrumb.parentBreadcrumb != undefined) { + const parentBreadcrumb = breadcrumb.parentBreadcrumb; + if (parentBreadcrumb.permissions.canRead && parentBreadcrumb.permissions.canWrite) { + const option: SelectOption = { + text: parentBreadcrumb.breadcrumb.name + ' (Parent)', + value: parentBreadcrumb.breadcrumb.id + }; + + let errorMsg = ''; + const descriptors = this.controllerService.component.descriptors; + for (const descriptor in descriptors) { + if (descriptors[descriptor].identifiesControllerService != undefined) { + const controllerId = this.controllerService.component.properties[descriptors[descriptor].name]; + if ( + controllerId != null && + !parentControllerServices.some((service) => service.id == controllerId) + ) { + errorMsg += '[' + descriptors[descriptor].name + ']'; + } + } + } + + if (errorMsg != '') { + option.description = + 'The following properties reference controller services that would be out of scope for this ' + + 'process group: ' + + errorMsg; + option.disabled = true; + } else { + option.disabled = false; + } + + processGroups.push(option); + } + } + } + + loadChildOptions( + childProcessGroupOptions: SelectOption[], + currentProcessGroupEntity: any, + processGroups: SelectOption[] + ) { + const referencingComponents: ControllerServiceReferencingComponentEntity[] = + this.controllerService.component.referencingComponents; + childProcessGroupOptions.forEach((child: SelectOption) => { + const option: SelectOption = { + text: child.text, + value: child.value + }; + + const root = this.getProcessGroupById(currentProcessGroupEntity.component, child.value ?? ''); + let errorMsg = ''; + + if (root == null) { + option.description = 'Error loading process group root.'; + option.disabled = true; + processGroups.push(option); + } else { + for (const component of referencingComponents) { + if (!this.processGroupContainsComponent(root, component.component.groupId)) { + errorMsg += '[' + component.component.name + ']'; Review Comment: Please convert to a template string if possible. ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts: ########## @@ -0,0 +1,247 @@ +/* + * 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, Inject, Input } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + ControllerServiceEntity, + ControllerServiceReferencingComponent, + ControllerServiceReferencingComponentEntity +} from '../../../../state/shared'; +import { MatInputModule } from '@angular/material/input'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSelectModule } from '@angular/material/select'; +import { ControllerServiceApi } from '../../../../ui/common/controller-service/controller-service-api/controller-service-api.component'; +import { ControllerServiceReferences } from '../../../../ui/common/controller-service/controller-service-references/controller-service-references.component'; +import { NifiSpinnerDirective } from '../../../../ui/common/spinner/nifi-spinner.directive'; +import { TextTip, NifiTooltipDirective, SelectOption } from '@nifi/shared'; +import { Store } from '@ngrx/store'; +import { CloseOnEscapeDialog } from '@nifi/shared'; +import { moveControllerService } from '../../state/controller-services/controller-services.actions'; +import { NiFiState } from 'apps/nifi/src/app/state'; +import { MoveControllerServiceDialogRequestSuccess } from '../../state/controller-services'; +import { NgIf } from '@angular/common'; +import { BreadcrumbEntity } from '../../state/shared'; + +@Component({ + selector: 'move-controller-service', + standalone: true, + templateUrl: './move-controller-service.component.html', + imports: [ + ReactiveFormsModule, + MatDialogModule, + MatInputModule, + MatCheckboxModule, + MatButtonModule, + MatTabsModule, + MatOptionModule, + MatSelectModule, + ControllerServiceApi, + ControllerServiceReferences, + AsyncPipe, + NifiSpinnerDirective, + NifiTooltipDirective, + NgTemplateOutlet, + NgIf + ], + styleUrls: ['./move-controller-service.component.scss'] +}) +export class MoveControllerService extends CloseOnEscapeDialog { + @Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void; + protected readonly TextTip = TextTip; + protected controllerServiceActionProcessGroups: SelectOption[] = []; + protected disableSubmit: boolean = false; + + controllerService: ControllerServiceEntity; + + moveControllerServiceForm: FormGroup; + + constructor( + @Inject(MAT_DIALOG_DATA) public request: MoveControllerServiceDialogRequestSuccess, + private store: Store<NiFiState>, + private formBuilder: FormBuilder + ) { + super(); + + this.controllerService = request.controllerService; + + // build the form + this.moveControllerServiceForm = this.formBuilder.group({ + processGroups: new FormControl('Process Group', Validators.required) + }); + + const parentControllerServices = request.parentControllerServices.filter( + (cs) => cs.parentGroupId != request.controllerService.parentGroupId + ); + + const processGroups: SelectOption[] = []; + this.loadParentOption(request.breadcrumb, parentControllerServices, processGroups); + + this.loadChildOptions(request.childProcessGroupOptions, request.processGroupEntity, processGroups); + this.controllerServiceActionProcessGroups = processGroups; + + const firstEnabled = processGroups.findIndex((pg) => !pg.disabled); + if (firstEnabled != -1) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[firstEnabled].value); + } else { + if (processGroups.length > 0) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[0].value); + } + this.disableSubmit = true; + } + } + + loadParentOption( + breadcrumb: BreadcrumbEntity, + parentControllerServices: ControllerServiceEntity[], + processGroups: SelectOption[] + ) { + if (breadcrumb.parentBreadcrumb != undefined) { + const parentBreadcrumb = breadcrumb.parentBreadcrumb; + if (parentBreadcrumb.permissions.canRead && parentBreadcrumb.permissions.canWrite) { + const option: SelectOption = { + text: parentBreadcrumb.breadcrumb.name + ' (Parent)', + value: parentBreadcrumb.breadcrumb.id + }; + + let errorMsg = ''; + const descriptors = this.controllerService.component.descriptors; + for (const descriptor in descriptors) { + if (descriptors[descriptor].identifiesControllerService != undefined) { + const controllerId = this.controllerService.component.properties[descriptors[descriptor].name]; + if ( + controllerId != null && + !parentControllerServices.some((service) => service.id == controllerId) + ) { + errorMsg += '[' + descriptors[descriptor].name + ']'; Review Comment: Please convert to a template string if possible. ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts: ########## @@ -0,0 +1,247 @@ +/* + * 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, Inject, Input } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + ControllerServiceEntity, + ControllerServiceReferencingComponent, + ControllerServiceReferencingComponentEntity +} from '../../../../state/shared'; +import { MatInputModule } from '@angular/material/input'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSelectModule } from '@angular/material/select'; +import { ControllerServiceApi } from '../../../../ui/common/controller-service/controller-service-api/controller-service-api.component'; +import { ControllerServiceReferences } from '../../../../ui/common/controller-service/controller-service-references/controller-service-references.component'; +import { NifiSpinnerDirective } from '../../../../ui/common/spinner/nifi-spinner.directive'; +import { TextTip, NifiTooltipDirective, SelectOption } from '@nifi/shared'; +import { Store } from '@ngrx/store'; +import { CloseOnEscapeDialog } from '@nifi/shared'; +import { moveControllerService } from '../../state/controller-services/controller-services.actions'; +import { NiFiState } from 'apps/nifi/src/app/state'; +import { MoveControllerServiceDialogRequestSuccess } from '../../state/controller-services'; +import { NgIf } from '@angular/common'; +import { BreadcrumbEntity } from '../../state/shared'; + +@Component({ + selector: 'move-controller-service', + standalone: true, + templateUrl: './move-controller-service.component.html', + imports: [ + ReactiveFormsModule, + MatDialogModule, + MatInputModule, + MatCheckboxModule, + MatButtonModule, + MatTabsModule, + MatOptionModule, + MatSelectModule, + ControllerServiceApi, + ControllerServiceReferences, + AsyncPipe, + NifiSpinnerDirective, + NifiTooltipDirective, + NgTemplateOutlet, + NgIf + ], + styleUrls: ['./move-controller-service.component.scss'] +}) +export class MoveControllerService extends CloseOnEscapeDialog { + @Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void; + protected readonly TextTip = TextTip; + protected controllerServiceActionProcessGroups: SelectOption[] = []; + protected disableSubmit: boolean = false; + + controllerService: ControllerServiceEntity; + + moveControllerServiceForm: FormGroup; + + constructor( + @Inject(MAT_DIALOG_DATA) public request: MoveControllerServiceDialogRequestSuccess, + private store: Store<NiFiState>, + private formBuilder: FormBuilder + ) { + super(); + + this.controllerService = request.controllerService; + + // build the form + this.moveControllerServiceForm = this.formBuilder.group({ + processGroups: new FormControl('Process Group', Validators.required) + }); + + const parentControllerServices = request.parentControllerServices.filter( + (cs) => cs.parentGroupId != request.controllerService.parentGroupId + ); + + const processGroups: SelectOption[] = []; + this.loadParentOption(request.breadcrumb, parentControllerServices, processGroups); + + this.loadChildOptions(request.childProcessGroupOptions, request.processGroupEntity, processGroups); + this.controllerServiceActionProcessGroups = processGroups; + + const firstEnabled = processGroups.findIndex((pg) => !pg.disabled); + if (firstEnabled != -1) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[firstEnabled].value); + } else { + if (processGroups.length > 0) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[0].value); + } + this.disableSubmit = true; + } + } + + loadParentOption( + breadcrumb: BreadcrumbEntity, + parentControllerServices: ControllerServiceEntity[], + processGroups: SelectOption[] + ) { + if (breadcrumb.parentBreadcrumb != undefined) { + const parentBreadcrumb = breadcrumb.parentBreadcrumb; + if (parentBreadcrumb.permissions.canRead && parentBreadcrumb.permissions.canWrite) { + const option: SelectOption = { + text: parentBreadcrumb.breadcrumb.name + ' (Parent)', + value: parentBreadcrumb.breadcrumb.id + }; + + let errorMsg = ''; + const descriptors = this.controllerService.component.descriptors; + for (const descriptor in descriptors) { + if (descriptors[descriptor].identifiesControllerService != undefined) { + const controllerId = this.controllerService.component.properties[descriptors[descriptor].name]; + if ( + controllerId != null && + !parentControllerServices.some((service) => service.id == controllerId) + ) { + errorMsg += '[' + descriptors[descriptor].name + ']'; + } + } + } + + if (errorMsg != '') { + option.description = + 'The following properties reference controller services that would be out of scope for this ' + + 'process group: ' + + errorMsg; + option.disabled = true; + } else { + option.disabled = false; + } + + processGroups.push(option); + } + } + } + + loadChildOptions( + childProcessGroupOptions: SelectOption[], + currentProcessGroupEntity: any, + processGroups: SelectOption[] + ) { + const referencingComponents: ControllerServiceReferencingComponentEntity[] = + this.controllerService.component.referencingComponents; + childProcessGroupOptions.forEach((child: SelectOption) => { + const option: SelectOption = { + text: child.text, + value: child.value + }; + + const root = this.getProcessGroupById(currentProcessGroupEntity.component, child.value ?? ''); + let errorMsg = ''; + + if (root == null) { + option.description = 'Error loading process group root.'; + option.disabled = true; + processGroups.push(option); + } else { + for (const component of referencingComponents) { + if (!this.processGroupContainsComponent(root, component.component.groupId)) { + errorMsg += '[' + component.component.name + ']'; + } + } + + if (errorMsg != '') { + option.description = + 'The following components would be out of scope for this process group: ' + errorMsg; + option.disabled = true; + } else { + option.disabled = false; + } + + processGroups.push(option); + } + }); + } + + processGroupContainsComponent(processGroup: any, groupId: string): boolean { + if (processGroup.contents != undefined) { + if (processGroup.id == groupId) { + return true; + } else { + for (const pg of processGroup.contents.processGroups) { + if (this.processGroupContainsComponent(pg, groupId)) { + return true; + } + } + return false; + } + } + return false; + } + + getProcessGroupById(root: any, processGroupId: string): any { + if (root != undefined) { + if (root.id == processGroupId) { + return root; + } else { + if (root.contents != undefined) { Review Comment: ```suggestion if (root.contents) { ``` ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts: ########## @@ -0,0 +1,247 @@ +/* + * 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, Inject, Input } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + ControllerServiceEntity, + ControllerServiceReferencingComponent, + ControllerServiceReferencingComponentEntity +} from '../../../../state/shared'; +import { MatInputModule } from '@angular/material/input'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSelectModule } from '@angular/material/select'; +import { ControllerServiceApi } from '../../../../ui/common/controller-service/controller-service-api/controller-service-api.component'; +import { ControllerServiceReferences } from '../../../../ui/common/controller-service/controller-service-references/controller-service-references.component'; +import { NifiSpinnerDirective } from '../../../../ui/common/spinner/nifi-spinner.directive'; +import { TextTip, NifiTooltipDirective, SelectOption } from '@nifi/shared'; +import { Store } from '@ngrx/store'; +import { CloseOnEscapeDialog } from '@nifi/shared'; +import { moveControllerService } from '../../state/controller-services/controller-services.actions'; +import { NiFiState } from 'apps/nifi/src/app/state'; +import { MoveControllerServiceDialogRequestSuccess } from '../../state/controller-services'; +import { NgIf } from '@angular/common'; +import { BreadcrumbEntity } from '../../state/shared'; + +@Component({ + selector: 'move-controller-service', + standalone: true, + templateUrl: './move-controller-service.component.html', + imports: [ + ReactiveFormsModule, + MatDialogModule, + MatInputModule, + MatCheckboxModule, + MatButtonModule, + MatTabsModule, + MatOptionModule, + MatSelectModule, + ControllerServiceApi, + ControllerServiceReferences, + AsyncPipe, + NifiSpinnerDirective, + NifiTooltipDirective, + NgTemplateOutlet, + NgIf + ], + styleUrls: ['./move-controller-service.component.scss'] +}) +export class MoveControllerService extends CloseOnEscapeDialog { + @Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void; + protected readonly TextTip = TextTip; + protected controllerServiceActionProcessGroups: SelectOption[] = []; + protected disableSubmit: boolean = false; + + controllerService: ControllerServiceEntity; + + moveControllerServiceForm: FormGroup; + + constructor( + @Inject(MAT_DIALOG_DATA) public request: MoveControllerServiceDialogRequestSuccess, + private store: Store<NiFiState>, + private formBuilder: FormBuilder + ) { + super(); + + this.controllerService = request.controllerService; + + // build the form + this.moveControllerServiceForm = this.formBuilder.group({ + processGroups: new FormControl('Process Group', Validators.required) + }); + + const parentControllerServices = request.parentControllerServices.filter( + (cs) => cs.parentGroupId != request.controllerService.parentGroupId + ); + + const processGroups: SelectOption[] = []; + this.loadParentOption(request.breadcrumb, parentControllerServices, processGroups); + + this.loadChildOptions(request.childProcessGroupOptions, request.processGroupEntity, processGroups); + this.controllerServiceActionProcessGroups = processGroups; + + const firstEnabled = processGroups.findIndex((pg) => !pg.disabled); + if (firstEnabled != -1) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[firstEnabled].value); + } else { + if (processGroups.length > 0) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[0].value); + } + this.disableSubmit = true; + } + } + + loadParentOption( + breadcrumb: BreadcrumbEntity, + parentControllerServices: ControllerServiceEntity[], + processGroups: SelectOption[] + ) { + if (breadcrumb.parentBreadcrumb != undefined) { + const parentBreadcrumb = breadcrumb.parentBreadcrumb; + if (parentBreadcrumb.permissions.canRead && parentBreadcrumb.permissions.canWrite) { + const option: SelectOption = { + text: parentBreadcrumb.breadcrumb.name + ' (Parent)', + value: parentBreadcrumb.breadcrumb.id + }; + + let errorMsg = ''; + const descriptors = this.controllerService.component.descriptors; + for (const descriptor in descriptors) { + if (descriptors[descriptor].identifiesControllerService != undefined) { + const controllerId = this.controllerService.component.properties[descriptors[descriptor].name]; + if ( + controllerId != null && + !parentControllerServices.some((service) => service.id == controllerId) + ) { + errorMsg += '[' + descriptors[descriptor].name + ']'; + } + } + } + + if (errorMsg != '') { + option.description = + 'The following properties reference controller services that would be out of scope for this ' + + 'process group: ' + + errorMsg; + option.disabled = true; + } else { + option.disabled = false; + } + + processGroups.push(option); + } + } + } + + loadChildOptions( + childProcessGroupOptions: SelectOption[], + currentProcessGroupEntity: any, + processGroups: SelectOption[] + ) { + const referencingComponents: ControllerServiceReferencingComponentEntity[] = + this.controllerService.component.referencingComponents; + childProcessGroupOptions.forEach((child: SelectOption) => { + const option: SelectOption = { + text: child.text, + value: child.value + }; + + const root = this.getProcessGroupById(currentProcessGroupEntity.component, child.value ?? ''); + let errorMsg = ''; + + if (root == null) { + option.description = 'Error loading process group root.'; + option.disabled = true; + processGroups.push(option); + } else { + for (const component of referencingComponents) { + if (!this.processGroupContainsComponent(root, component.component.groupId)) { + errorMsg += '[' + component.component.name + ']'; + } + } + + if (errorMsg != '') { + option.description = + 'The following components would be out of scope for this process group: ' + errorMsg; Review Comment: Please convert to a template string. ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts: ########## @@ -0,0 +1,247 @@ +/* + * 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, Inject, Input } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + ControllerServiceEntity, + ControllerServiceReferencingComponent, + ControllerServiceReferencingComponentEntity +} from '../../../../state/shared'; +import { MatInputModule } from '@angular/material/input'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSelectModule } from '@angular/material/select'; +import { ControllerServiceApi } from '../../../../ui/common/controller-service/controller-service-api/controller-service-api.component'; +import { ControllerServiceReferences } from '../../../../ui/common/controller-service/controller-service-references/controller-service-references.component'; +import { NifiSpinnerDirective } from '../../../../ui/common/spinner/nifi-spinner.directive'; +import { TextTip, NifiTooltipDirective, SelectOption } from '@nifi/shared'; +import { Store } from '@ngrx/store'; +import { CloseOnEscapeDialog } from '@nifi/shared'; +import { moveControllerService } from '../../state/controller-services/controller-services.actions'; +import { NiFiState } from 'apps/nifi/src/app/state'; +import { MoveControllerServiceDialogRequestSuccess } from '../../state/controller-services'; +import { NgIf } from '@angular/common'; +import { BreadcrumbEntity } from '../../state/shared'; + +@Component({ + selector: 'move-controller-service', + standalone: true, + templateUrl: './move-controller-service.component.html', + imports: [ + ReactiveFormsModule, + MatDialogModule, + MatInputModule, + MatCheckboxModule, + MatButtonModule, + MatTabsModule, + MatOptionModule, + MatSelectModule, + ControllerServiceApi, + ControllerServiceReferences, + AsyncPipe, + NifiSpinnerDirective, + NifiTooltipDirective, + NgTemplateOutlet, + NgIf + ], + styleUrls: ['./move-controller-service.component.scss'] +}) +export class MoveControllerService extends CloseOnEscapeDialog { + @Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void; + protected readonly TextTip = TextTip; + protected controllerServiceActionProcessGroups: SelectOption[] = []; + protected disableSubmit: boolean = false; + + controllerService: ControllerServiceEntity; + + moveControllerServiceForm: FormGroup; + + constructor( + @Inject(MAT_DIALOG_DATA) public request: MoveControllerServiceDialogRequestSuccess, + private store: Store<NiFiState>, + private formBuilder: FormBuilder + ) { + super(); + + this.controllerService = request.controllerService; + + // build the form + this.moveControllerServiceForm = this.formBuilder.group({ + processGroups: new FormControl('Process Group', Validators.required) + }); + + const parentControllerServices = request.parentControllerServices.filter( + (cs) => cs.parentGroupId != request.controllerService.parentGroupId + ); + + const processGroups: SelectOption[] = []; + this.loadParentOption(request.breadcrumb, parentControllerServices, processGroups); + + this.loadChildOptions(request.childProcessGroupOptions, request.processGroupEntity, processGroups); + this.controllerServiceActionProcessGroups = processGroups; + + const firstEnabled = processGroups.findIndex((pg) => !pg.disabled); + if (firstEnabled != -1) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[firstEnabled].value); + } else { + if (processGroups.length > 0) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[0].value); + } + this.disableSubmit = true; + } + } + + loadParentOption( + breadcrumb: BreadcrumbEntity, + parentControllerServices: ControllerServiceEntity[], + processGroups: SelectOption[] + ) { + if (breadcrumb.parentBreadcrumb != undefined) { + const parentBreadcrumb = breadcrumb.parentBreadcrumb; + if (parentBreadcrumb.permissions.canRead && parentBreadcrumb.permissions.canWrite) { + const option: SelectOption = { + text: parentBreadcrumb.breadcrumb.name + ' (Parent)', + value: parentBreadcrumb.breadcrumb.id + }; + + let errorMsg = ''; + const descriptors = this.controllerService.component.descriptors; + for (const descriptor in descriptors) { + if (descriptors[descriptor].identifiesControllerService != undefined) { + const controllerId = this.controllerService.component.properties[descriptors[descriptor].name]; + if ( + controllerId != null && + !parentControllerServices.some((service) => service.id == controllerId) + ) { + errorMsg += '[' + descriptors[descriptor].name + ']'; + } + } + } + + if (errorMsg != '') { + option.description = + 'The following properties reference controller services that would be out of scope for this ' + + 'process group: ' + + errorMsg; + option.disabled = true; + } else { + option.disabled = false; + } + + processGroups.push(option); + } + } + } + + loadChildOptions( + childProcessGroupOptions: SelectOption[], + currentProcessGroupEntity: any, + processGroups: SelectOption[] + ) { + const referencingComponents: ControllerServiceReferencingComponentEntity[] = + this.controllerService.component.referencingComponents; + childProcessGroupOptions.forEach((child: SelectOption) => { + const option: SelectOption = { + text: child.text, + value: child.value + }; + + const root = this.getProcessGroupById(currentProcessGroupEntity.component, child.value ?? ''); + let errorMsg = ''; + + if (root == null) { + option.description = 'Error loading process group root.'; + option.disabled = true; + processGroups.push(option); + } else { + for (const component of referencingComponents) { + if (!this.processGroupContainsComponent(root, component.component.groupId)) { + errorMsg += '[' + component.component.name + ']'; + } + } + + if (errorMsg != '') { + option.description = + 'The following components would be out of scope for this process group: ' + errorMsg; + option.disabled = true; + } else { + option.disabled = false; + } + + processGroups.push(option); + } + }); + } + + processGroupContainsComponent(processGroup: any, groupId: string): boolean { + if (processGroup.contents != undefined) { + if (processGroup.id == groupId) { + return true; + } else { + for (const pg of processGroup.contents.processGroups) { + if (this.processGroupContainsComponent(pg, groupId)) { + return true; + } + } + return false; + } + } + return false; + } + + getProcessGroupById(root: any, processGroupId: string): any { + if (root != undefined) { + if (root.id == processGroupId) { + return root; + } else { + if (root.contents != undefined) { + for (const pg of root.contents.processGroups) { + const result = this.getProcessGroupById(pg, processGroupId); + if (result != null) { Review Comment: ```suggestion if (result) { ``` ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/flow.service.ts: ########## @@ -118,6 +118,11 @@ export class FlowService implements PropertyDescriptorRetriever { return this.httpClient.get(`${FlowService.API}/process-groups/${id}`); } + getProcessGroupWithContent(id: string): Observable<any> { + const params = new HttpParams().set('includeContent', true); + return this.httpClient.get(`${FlowService.API}/process-groups/${id}`, { params: params }); + } Review Comment: This change is necessary for this feature to ensure that when moving a Controller Service it remains in scope for all components currently referencing it. However, I have some concerns about fetching the entire flow from the current Process Group when the Move dialog is opened. I wonder if we should consider a purpose built endpoint for does exactly what we need instead of returning everything. We've certainly seen some large flows over the years where I worry this could be problematic. CC @exceptionfactory ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts: ########## @@ -0,0 +1,247 @@ +/* + * 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, Inject, Input } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + ControllerServiceEntity, + ControllerServiceReferencingComponent, + ControllerServiceReferencingComponentEntity +} from '../../../../state/shared'; +import { MatInputModule } from '@angular/material/input'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSelectModule } from '@angular/material/select'; +import { ControllerServiceApi } from '../../../../ui/common/controller-service/controller-service-api/controller-service-api.component'; +import { ControllerServiceReferences } from '../../../../ui/common/controller-service/controller-service-references/controller-service-references.component'; +import { NifiSpinnerDirective } from '../../../../ui/common/spinner/nifi-spinner.directive'; +import { TextTip, NifiTooltipDirective, SelectOption } from '@nifi/shared'; +import { Store } from '@ngrx/store'; +import { CloseOnEscapeDialog } from '@nifi/shared'; +import { moveControllerService } from '../../state/controller-services/controller-services.actions'; +import { NiFiState } from 'apps/nifi/src/app/state'; +import { MoveControllerServiceDialogRequestSuccess } from '../../state/controller-services'; +import { NgIf } from '@angular/common'; +import { BreadcrumbEntity } from '../../state/shared'; + +@Component({ + selector: 'move-controller-service', + standalone: true, + templateUrl: './move-controller-service.component.html', + imports: [ + ReactiveFormsModule, + MatDialogModule, + MatInputModule, + MatCheckboxModule, + MatButtonModule, + MatTabsModule, + MatOptionModule, + MatSelectModule, + ControllerServiceApi, + ControllerServiceReferences, + AsyncPipe, + NifiSpinnerDirective, + NifiTooltipDirective, + NgTemplateOutlet, + NgIf + ], + styleUrls: ['./move-controller-service.component.scss'] +}) +export class MoveControllerService extends CloseOnEscapeDialog { + @Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void; + protected readonly TextTip = TextTip; + protected controllerServiceActionProcessGroups: SelectOption[] = []; + protected disableSubmit: boolean = false; + + controllerService: ControllerServiceEntity; + + moveControllerServiceForm: FormGroup; + + constructor( + @Inject(MAT_DIALOG_DATA) public request: MoveControllerServiceDialogRequestSuccess, + private store: Store<NiFiState>, + private formBuilder: FormBuilder + ) { + super(); + + this.controllerService = request.controllerService; + + // build the form + this.moveControllerServiceForm = this.formBuilder.group({ + processGroups: new FormControl('Process Group', Validators.required) + }); + + const parentControllerServices = request.parentControllerServices.filter( + (cs) => cs.parentGroupId != request.controllerService.parentGroupId + ); + + const processGroups: SelectOption[] = []; + this.loadParentOption(request.breadcrumb, parentControllerServices, processGroups); + + this.loadChildOptions(request.childProcessGroupOptions, request.processGroupEntity, processGroups); + this.controllerServiceActionProcessGroups = processGroups; + + const firstEnabled = processGroups.findIndex((pg) => !pg.disabled); + if (firstEnabled != -1) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[firstEnabled].value); + } else { + if (processGroups.length > 0) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[0].value); + } + this.disableSubmit = true; + } + } + + loadParentOption( + breadcrumb: BreadcrumbEntity, + parentControllerServices: ControllerServiceEntity[], + processGroups: SelectOption[] + ) { + if (breadcrumb.parentBreadcrumb != undefined) { + const parentBreadcrumb = breadcrumb.parentBreadcrumb; + if (parentBreadcrumb.permissions.canRead && parentBreadcrumb.permissions.canWrite) { + const option: SelectOption = { + text: parentBreadcrumb.breadcrumb.name + ' (Parent)', + value: parentBreadcrumb.breadcrumb.id + }; + + let errorMsg = ''; + const descriptors = this.controllerService.component.descriptors; + for (const descriptor in descriptors) { + if (descriptors[descriptor].identifiesControllerService != undefined) { + const controllerId = this.controllerService.component.properties[descriptors[descriptor].name]; + if ( + controllerId != null && + !parentControllerServices.some((service) => service.id == controllerId) + ) { + errorMsg += '[' + descriptors[descriptor].name + ']'; + } + } + } + + if (errorMsg != '') { + option.description = + 'The following properties reference controller services that would be out of scope for this ' + + 'process group: ' + + errorMsg; + option.disabled = true; + } else { + option.disabled = false; + } + + processGroups.push(option); + } + } + } + + loadChildOptions( + childProcessGroupOptions: SelectOption[], + currentProcessGroupEntity: any, + processGroups: SelectOption[] + ) { + const referencingComponents: ControllerServiceReferencingComponentEntity[] = + this.controllerService.component.referencingComponents; + childProcessGroupOptions.forEach((child: SelectOption) => { + const option: SelectOption = { + text: child.text, + value: child.value + }; + + const root = this.getProcessGroupById(currentProcessGroupEntity.component, child.value ?? ''); + let errorMsg = ''; + + if (root == null) { Review Comment: It may make this more readable if we swap the `if/else` blocks and update the conditional to: ```suggestion if (root) { ``` ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts: ########## @@ -0,0 +1,247 @@ +/* + * 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, Inject, Input } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { + ControllerServiceEntity, + ControllerServiceReferencingComponent, + ControllerServiceReferencingComponentEntity +} from '../../../../state/shared'; +import { MatInputModule } from '@angular/material/input'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatOptionModule } from '@angular/material/core'; +import { MatSelectModule } from '@angular/material/select'; +import { ControllerServiceApi } from '../../../../ui/common/controller-service/controller-service-api/controller-service-api.component'; +import { ControllerServiceReferences } from '../../../../ui/common/controller-service/controller-service-references/controller-service-references.component'; +import { NifiSpinnerDirective } from '../../../../ui/common/spinner/nifi-spinner.directive'; +import { TextTip, NifiTooltipDirective, SelectOption } from '@nifi/shared'; +import { Store } from '@ngrx/store'; +import { CloseOnEscapeDialog } from '@nifi/shared'; +import { moveControllerService } from '../../state/controller-services/controller-services.actions'; +import { NiFiState } from 'apps/nifi/src/app/state'; +import { MoveControllerServiceDialogRequestSuccess } from '../../state/controller-services'; +import { NgIf } from '@angular/common'; +import { BreadcrumbEntity } from '../../state/shared'; + +@Component({ + selector: 'move-controller-service', + standalone: true, + templateUrl: './move-controller-service.component.html', + imports: [ + ReactiveFormsModule, + MatDialogModule, + MatInputModule, + MatCheckboxModule, + MatButtonModule, + MatTabsModule, + MatOptionModule, + MatSelectModule, + ControllerServiceApi, + ControllerServiceReferences, + AsyncPipe, + NifiSpinnerDirective, + NifiTooltipDirective, + NgTemplateOutlet, + NgIf + ], + styleUrls: ['./move-controller-service.component.scss'] +}) +export class MoveControllerService extends CloseOnEscapeDialog { + @Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void; + protected readonly TextTip = TextTip; + protected controllerServiceActionProcessGroups: SelectOption[] = []; + protected disableSubmit: boolean = false; + + controllerService: ControllerServiceEntity; + + moveControllerServiceForm: FormGroup; + + constructor( + @Inject(MAT_DIALOG_DATA) public request: MoveControllerServiceDialogRequestSuccess, + private store: Store<NiFiState>, + private formBuilder: FormBuilder + ) { + super(); + + this.controllerService = request.controllerService; + + // build the form + this.moveControllerServiceForm = this.formBuilder.group({ + processGroups: new FormControl('Process Group', Validators.required) + }); + + const parentControllerServices = request.parentControllerServices.filter( + (cs) => cs.parentGroupId != request.controllerService.parentGroupId + ); + + const processGroups: SelectOption[] = []; + this.loadParentOption(request.breadcrumb, parentControllerServices, processGroups); + + this.loadChildOptions(request.childProcessGroupOptions, request.processGroupEntity, processGroups); + this.controllerServiceActionProcessGroups = processGroups; + + const firstEnabled = processGroups.findIndex((pg) => !pg.disabled); + if (firstEnabled != -1) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[firstEnabled].value); + } else { + if (processGroups.length > 0) { + this.moveControllerServiceForm.controls['processGroups'].setValue(processGroups[0].value); + } + this.disableSubmit = true; + } + } + + loadParentOption( + breadcrumb: BreadcrumbEntity, + parentControllerServices: ControllerServiceEntity[], + processGroups: SelectOption[] + ) { + if (breadcrumb.parentBreadcrumb != undefined) { + const parentBreadcrumb = breadcrumb.parentBreadcrumb; + if (parentBreadcrumb.permissions.canRead && parentBreadcrumb.permissions.canWrite) { + const option: SelectOption = { + text: parentBreadcrumb.breadcrumb.name + ' (Parent)', + value: parentBreadcrumb.breadcrumb.id + }; + + let errorMsg = ''; + const descriptors = this.controllerService.component.descriptors; + for (const descriptor in descriptors) { + if (descriptors[descriptor].identifiesControllerService != undefined) { + const controllerId = this.controllerService.component.properties[descriptors[descriptor].name]; + if ( + controllerId != null && + !parentControllerServices.some((service) => service.id == controllerId) + ) { + errorMsg += '[' + descriptors[descriptor].name + ']'; + } + } + } + + if (errorMsg != '') { + option.description = + 'The following properties reference controller services that would be out of scope for this ' + + 'process group: ' + + errorMsg; + option.disabled = true; + } else { + option.disabled = false; + } + + processGroups.push(option); + } + } + } + + loadChildOptions( + childProcessGroupOptions: SelectOption[], + currentProcessGroupEntity: any, + processGroups: SelectOption[] + ) { + const referencingComponents: ControllerServiceReferencingComponentEntity[] = + this.controllerService.component.referencingComponents; + childProcessGroupOptions.forEach((child: SelectOption) => { + const option: SelectOption = { + text: child.text, + value: child.value + }; + + const root = this.getProcessGroupById(currentProcessGroupEntity.component, child.value ?? ''); + let errorMsg = ''; + + if (root == null) { + option.description = 'Error loading process group root.'; + option.disabled = true; + processGroups.push(option); + } else { + for (const component of referencingComponents) { + if (!this.processGroupContainsComponent(root, component.component.groupId)) { + errorMsg += '[' + component.component.name + ']'; + } + } + + if (errorMsg != '') { + option.description = + 'The following components would be out of scope for this process group: ' + errorMsg; + option.disabled = true; + } else { + option.disabled = false; + } + + processGroups.push(option); + } + }); + } + + processGroupContainsComponent(processGroup: any, groupId: string): boolean { + if (processGroup.contents != undefined) { + if (processGroup.id == groupId) { + return true; + } else { + for (const pg of processGroup.contents.processGroups) { + if (this.processGroupContainsComponent(pg, groupId)) { + return true; + } + } + return false; + } + } + return false; + } + + getProcessGroupById(root: any, processGroupId: string): any { + if (root != undefined) { Review Comment: ```suggestion if (root) { ``` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
