mcgilman commented on code in PR #8965:
URL: https://github.com/apache/nifi/pull/8965#discussion_r1794291334


##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts:
##########
@@ -0,0 +1,237 @@
+/*
+ * 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[] = [];
+
+    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);
+        }
+    }
+
+    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 = '';
+            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 {
+        console.log('a');
+        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) {
+                            return result;
+                        }
+                    }
+                }
+                return null;
+            }
+        }
+        return null;
+    }
+
+    submitForm() {
+        if (this.moveControllerServiceForm.get('processGroups')?.value != 
'Process Group') {

Review Comment:
   Once the Submit button is disabled, I _think_ we should be able to remove 
this check here as the form would never be submitted.



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts:
##########
@@ -0,0 +1,232 @@
+/*
+ * 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 { MoveControllerServiceDialogRequest } 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[] = [];
+
+    controllerService: ControllerServiceEntity;
+
+    moveControllerServiceForm: FormGroup;
+
+    constructor(
+        @Inject(MAT_DIALOG_DATA) public request: 
MoveControllerServiceDialogRequest,
+        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[] = [];
+        if (request.breadcrumb != undefined) {
+            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);
+        }
+    }
+
+    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 ?? 
'');

Review Comment:
   Thanks for fixing the recursion issue. However `getProcessGroupById` returns 
null in a couple scenario. That response value is used as though it's 
guaranteed non-null. We should protect against this.



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.html:
##########
@@ -0,0 +1,82 @@
+<!--
+  ~ 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-2/3 flex-col gap-y-4 pr-4 overflow-hidden">
+                <div>
+                    <div>Service</div>
+                    <div
+                        class="accent-color font-medium overflow-ellipsis 
overflow-hidden whitespace-nowrap"
+                        [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/3 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()" 
mat-button>Move</button>

Review Comment:
   This button should be disabled when the form is not valid. For instance, 
when only disabled options are present and the user cannot make a selection.



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.ts:
##########
@@ -0,0 +1,237 @@
+/*
+ * 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[] = [];
+
+    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);
+        }
+    }
+
+    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 = '';
+            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 {
+        console.log('a');

Review Comment:
   I think this was left in inadvertently. 



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.html:
##########
@@ -0,0 +1,82 @@
+<!--
+  ~ 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-2/3 flex-col gap-y-4 pr-4 overflow-hidden">
+                <div>
+                    <div>Service</div>
+                    <div
+                        class="accent-color font-medium overflow-ellipsis 
overflow-hidden whitespace-nowrap"
+                        [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/3 flex-col">

Review Comment:
   ```suggestion
               <div class="flex basis-1/2 flex-col">
   ```



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.html:
##########
@@ -0,0 +1,82 @@
+<!--
+  ~ 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-2/3 flex-col gap-y-4 pr-4 overflow-hidden">

Review Comment:
   ```suggestion
               <div class="flex basis-1/2 flex-col gap-y-4 pr-4 
overflow-hidden">
   ```
   
   Thoughts on splitting the dialog space, half for the Group selection and 
half for the References?



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/move-controller-service/move-controller-service.component.html:
##########
@@ -0,0 +1,82 @@
+<!--
+  ~ 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-2/3 flex-col gap-y-4 pr-4 overflow-hidden">
+                <div>
+                    <div>Service</div>
+                    <div
+                        class="accent-color font-medium overflow-ellipsis 
overflow-hidden whitespace-nowrap"

Review Comment:
   ```suggestion
                           class="tertiary-color font-medium truncate"
   ```



-- 
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]


Reply via email to