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


##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/index.ts:
##########
@@ -15,8 +15,10 @@
  * limitations under the License.
  */
 
+import { SelectOption } from '@nifi/shared';
 import { ControllerServiceEntity, ParameterContextReferenceEntity } from 
'../../../../state/shared';
 import { BreadcrumbEntity } from '../shared';
+import { Revision } from './../../../../state/shared/index';

Review Comment:
   ```suggestion
   import { Revision } from '../../../../state/shared';
   ```



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.ts:
##########
@@ -63,7 +66,8 @@ import {
 } from 
'../../../../state/property-verification/property-verification.selectors';
 import { VerifyPropertiesRequestContext } from 
'../../../../state/property-verification';
 import { BackNavigation } from '../../../../state/navigation';
-import { NiFiCommon, Storage } from '@nifi/shared';
+import { NiFiCommon, Storage, SelectOption, ComponentType, LARGE_DIALOG, 
SMALL_DIALOG, XL_DIALOG } from '@nifi/shared';
+import { ComponentEntity } from './../flow/index';

Review Comment:
   ```suggestion
   import { ComponentEntity } from '../flow';
   ```



##########
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) {

Review Comment:
   Under what conditions is `request.breadcrumb` `undefined`? I think the store 
has an initial value but even if that isn't the case we could update the 
`concatLatestFrom` with the following:
   
   `this.store.select(selectBreadcrumb).pipe(isDefinedAndNotNull())`



##########
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:
   It appears this was happening when `childProcessGroupOptions` contained more 
than one option. The recursive logic in `getProcessGroupById` (and other 
methods with similar structure) isn't correct. When there are multiple children 
the following code
   
   ```
                       for (const pg of root.contents.processGroups) {
                           return this.getProcessGroupById(pg, processGroupId);
                       }
   ```
   
   will return the result from the first child and not attempt to process any 
subsequent children.



##########
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:
   `getProcessGroupById` will return null for a couple different scenarios. 
When this happens calling `processGroupContainsComponent` will error with the 
following. The unhandled error leaves the UI in a weird state because the 
overlay is active but the dialog isn't shown.
   
   ```
   TypeError: Cannot read properties of null (reading 'contents')
       at _MoveControllerService.processGroupContainsComponent 
(move-controller-service.component.ts:184:26)
       at move-controller-service.component.ts:166:27
       at Array.forEach (<anonymous>)
       at _MoveControllerService.loadChildOptions 
(move-controller-service.component.ts:157:34)
       at new _MoveControllerService 
(move-controller-service.component.ts:99:14)
       at NodeInjectorFactory.MoveControllerService_Factory [as factory] 
(move-controller-service.component.ts:231:5)
       at getNodeInjectable (core.mjs:6060:44)
       at createRootComponent (core.mjs:17231:35)
       at ComponentFactory.create (core.mjs:17091:29)
       at ViewContainerRef2.createComponent (core.mjs:17498:47) 
   ```



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/controller-services/controller-services.effects.ts:
##########
@@ -651,6 +666,101 @@ export class ControllerServicesEffects {
         { dispatch: false }
     );
 
+    openMoveControllerServiceDialog$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                
ofType(ControllerServicesActions.openMoveControllerServiceDialog),
+                map((action) => action.request),
+                concatLatestFrom(() => [
+                    this.store.select(selectCurrentProcessGroupId),
+                    this.store.select(selectChildProcessGroupOptions),
+                    this.store.select(selectServices),
+                    this.store.select(selectBreadcrumb)
+                ]),
+                concatMap(
+                    ([request, currentProcessGroupId, 
childProcessGroupOptions, controllerServices, breadcrumb]) =>
+                        
combineLatest([this.flowService.getProcessGroupWithContent(currentProcessGroupId)]).pipe(
+                            map(([processGroupEntity]) => {
+                                return {
+                                    request,
+                                    currentProcessGroupId,
+                                    childProcessGroupOptions,
+                                    processGroupEntity,
+                                    controllerServices,
+                                    breadcrumb
+                                };
+                            })
+                        )
+                ),
+                tap((request) => {
+                    const clone = Object.assign({}, request.request);
+                    clone.processGroupEntity = request.processGroupEntity;
+                    clone.childProcessGroupOptions = 
request.childProcessGroupOptions;
+                    clone.parentControllerServices = 
request.controllerServices;
+                    clone.breadcrumb = request.breadcrumb;

Review Comment:
   Why is a clone being created here?



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