This is an automated email from the ASF dual-hosted git repository.

rfellows pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new ebfb5bc12e NIFI-12401: Allow combo editor to reference parameters 
(#8068)
ebfb5bc12e is described below

commit ebfb5bc12e94563cedeb042a6571e537f99ca3d6
Author: Matt Gilman <[email protected]>
AuthorDate: Wed Nov 29 12:26:25 2023 -0500

    NIFI-12401: Allow combo editor to reference parameters (#8068)
    
    * NIFI-12401:
    - Allow combo editor to reference parameters.
    
    * NIFI-12401:
    - Addressing review feedback.
    - Handling corner cases where there is no parameter context and where there 
are no parameters in a bound parameter context.
    
    * NIFI-12401:
    - Fixing formatting issues.
    
    This closes #8068
---
 .../nifi-web-frontend/src/main/nifi/karma.conf.js  |  65 +++---
 .../nifi-web-frontend/src/main/nifi/package.json   |   2 +-
 .../canvas/ui/header/search/search.component.html  |   2 +-
 .../src/main/nifi/src/app/state/shared/index.ts    |   6 +-
 .../combo-editor/combo-editor.component.html       |  38 +++-
 .../combo-editor/combo-editor.component.spec.ts    | 160 ++++++++++++++-
 .../editors/combo-editor/combo-editor.component.ts | 226 +++++++++++++++++----
 .../editors/nf-editor/nf-editor.component.spec.ts  |  77 ++++++-
 .../editors/nf-editor/nf-editor.component.ts       |  13 +-
 .../property-table/property-table.component.html   |   1 +
 .../property-table/property-table.component.ts     |   6 +-
 11 files changed, 495 insertions(+), 101 deletions(-)

diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/karma.conf.js
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/karma.conf.js
index 5ffa7d98cb..568ec857ed 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/karma.conf.js
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/karma.conf.js
@@ -19,38 +19,35 @@
 // https://karma-runner.github.io/1.0/config/configuration-file.html
 
 module.exports = function (config) {
-  config.set({
-    basePath: '',
-    frameworks: ['jasmine', '@angular-devkit/build-angular'],
-    plugins: [
-      require('karma-jasmine'),
-      require('karma-chrome-launcher'),
-      require('karma-jasmine-html-reporter'),
-      require('karma-coverage'),
-      require('@angular-devkit/build-angular/plugins/karma')
-    ],
-    client: {
-      jasmine: {
-        // you can add configuration options for Jasmine here
-        // the possible options are listed at 
https://jasmine.github.io/api/edge/Configuration.html
-        // for example, you can disable the random execution with `random: 
false`
-        // or set a specific seed with `seed: 4321`
-      },
-      clearContext: false // leave Jasmine Spec Runner output visible in 
browser
-    },
-    jasmineHtmlReporter: {
-      suppressAll: true // removes the duplicated traces
-    },
-    coverageReporter: {
-      dir: require('path').join(__dirname, './coverage/nifi'),
-      subdir: '.',
-      reporters: [
-        { type: 'html' },
-        { type: 'text-summary' }
-      ]
-    },
-    reporters: ['progress', 'kjhtml'],
-    browsers: ['ChromeHeadless'],
-    restartOnFileChange: true
-  });
+    config.set({
+        basePath: '',
+        frameworks: ['jasmine', '@angular-devkit/build-angular'],
+        plugins: [
+            require('karma-jasmine'),
+            require('karma-chrome-launcher'),
+            require('karma-jasmine-html-reporter'),
+            require('karma-coverage'),
+            require('@angular-devkit/build-angular/plugins/karma')
+        ],
+        client: {
+            jasmine: {
+                // you can add configuration options for Jasmine here
+                // the possible options are listed at 
https://jasmine.github.io/api/edge/Configuration.html
+                // for example, you can disable the random execution with 
`random: false`
+                // or set a specific seed with `seed: 4321`
+            },
+            clearContext: false // leave Jasmine Spec Runner output visible in 
browser
+        },
+        jasmineHtmlReporter: {
+            suppressAll: true // removes the duplicated traces
+        },
+        coverageReporter: {
+            dir: require('path').join(__dirname, './coverage/nifi'),
+            subdir: '.',
+            reporters: [{ type: 'html' }, { type: 'text-summary' }]
+        },
+        reporters: ['progress', 'kjhtml'],
+        browsers: ['ChromeHeadless'],
+        restartOnFileChange: true
+    });
 };
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/package.json
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/package.json
index 7f54077afd..1f33078257 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/package.json
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/package.json
@@ -43,11 +43,11 @@
         "@angular-devkit/build-angular": "^16.2.0",
         "@angular/cli": "~16.2.0",
         "@angular/compiler-cli": "^16.2.0",
+        "@types/codemirror": "^5.60.13",
         "@types/d3": "^7.4.0",
         "@types/humanize-duration": "^3.27.1",
         "@types/jasmine": "~4.3.0",
         "@types/webfontloader": "^1.6.35",
-        "@types/codemirror": "^5.60.13",
         "autoprefixer": "^10.4.15",
         "eslint-config-prettier": "^9.0.0",
         "eslint-plugin-prettier": "^5.0.0",
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/canvas/ui/header/search/search.component.html
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/canvas/ui/header/search/search.component.html
index da09c7b8ce..4249a64961 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/canvas/ui/header/search/search.component.html
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/canvas/ui/header/search/search.component.html
@@ -190,7 +190,7 @@
                 </a>
                 <ng-template #resultLink>
                     <a [routerLink]="['/process-groups', 
result.parentGroup.id, path, result.id]">
-                        {{ result.name }}
+                        {{ result.name ? result.name : result.id }}
                     </a>
                 </ng-template>
             </li>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts
index 21f7077bc3..90d9490fa7 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts
@@ -171,6 +171,8 @@ export interface ParameterContextReference {
 
 export interface AffectedComponentEntity {
     permissions: Permissions;
+    id: string;
+    revision: Revision;
     bulletins: BulletinEntity[];
     component: AffectedComponent;
     processGroup: ProcessGroupName;
@@ -289,8 +291,8 @@ export interface PropertyDescriptor {
     name: string;
     displayName: string;
     description: string;
-    defaultValue: string;
-    allowableValues: AllowableValueEntity[];
+    defaultValue?: string;
+    allowableValues?: AllowableValueEntity[];
     required: boolean;
     sensitive: boolean;
     dynamic: boolean;
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.html
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.html
index dd28ec7df7..502d7693f4 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.html
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.html
@@ -24,7 +24,8 @@
                     formControlName="value"
                     [placeholder]="getComboPlaceholder()"
                     [panelClass]="'combo-panel'"
-                    (mousedown)="preventDrag($event)">
+                    (mousedown)="preventDrag($event)"
+                    (selectionChange)="allowableValueChanged($event.value)">
                     <ng-container *ngFor="let allowableValue of 
allowableValues">
                         <ng-container *ngIf="allowableValue.description; else 
noDescription">
                             <mat-option
@@ -49,6 +50,41 @@
                     </ng-container>
                 </mat-select>
             </div>
+            <div *ngIf="showParameterAllowableValues">
+                <div *ngIf="!parametersLoaded; else showParameters">
+                    <ngx-skeleton-loader count="1"></ngx-skeleton-loader>
+                </div>
+                <ng-template #showParameters>
+                    <mat-select
+                        class="combo"
+                        formControlName="parameterReference"
+                        [panelClass]="'combo-panel'"
+                        (mousedown)="preventDrag($event)">
+                        <ng-container *ngFor="let parameterAllowableValue of 
parameterAllowableValues">
+                            <ng-container 
*ngIf="parameterAllowableValue.description; else noDescription">
+                                <mat-option
+                                    [value]="parameterAllowableValue.id"
+                                    (mousedown)="preventDrag($event)"
+                                    nifiTooltip
+                                    [tooltipComponentType]="TextTip"
+                                    
[tooltipInputData]="getAllowableValueOptionTipData(parameterAllowableValue)"
+                                    [delayClose]="false">
+                                    <span class="option-text" 
[class.unset]="parameterAllowableValue.value == null">{{
+                                        parameterAllowableValue.displayName
+                                    }}</span>
+                                </mat-option>
+                            </ng-container>
+                            <ng-template #noDescription>
+                                <mat-option 
[value]="parameterAllowableValue.id" (mousedown)="preventDrag($event)">
+                                    <span class="option-text" 
[class.unset]="parameterAllowableValue.value == null">{{
+                                        parameterAllowableValue.displayName
+                                    }}</span>
+                                </mat-option>
+                            </ng-template>
+                        </ng-container>
+                    </mat-select>
+                </ng-template>
+            </div>
             <div class="flex justify-end items-center gap-x-2">
                 <button
                     color="accent"
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.spec.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.spec.ts
index 8397e0a0a4..a54496db20 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.spec.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.spec.ts
@@ -18,19 +18,67 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 
 import { ComboEditor } from './combo-editor.component';
+import { PropertyItem } from '../../property-table.component';
+import { Parameter } from '../../../../../state/shared';
+import { of } from 'rxjs';
 
 describe('ComboEditor', () => {
     let component: ComboEditor;
     let fixture: ComponentFixture<ComboEditor>;
 
+    let item: PropertyItem | null = null;
+    let parameters: Parameter[] = [
+        {
+            name: 'one',
+            description: 'Description for one.',
+            sensitive: false,
+            value: 'value',
+            provided: false,
+            referencingComponents: [],
+            parameterContext: {
+                id: '95d4f3d2-018b-1000-b7c7-b830c49a8026',
+                permissions: {
+                    canRead: true,
+                    canWrite: true
+                },
+                component: {
+                    id: '95d4f3d2-018b-1000-b7c7-b830c49a8026',
+                    name: 'params 1'
+                }
+            },
+            inherited: false
+        },
+        {
+            name: 'two',
+            description: 'Description for two.',
+            sensitive: false,
+            value: 'value',
+            provided: false,
+            referencingComponents: [],
+            parameterContext: {
+                id: '95d4f3d2-018b-1000-b7c7-b830c49a8026',
+                permissions: {
+                    canRead: true,
+                    canWrite: true
+                },
+                component: {
+                    id: '95d4f3d2-018b-1000-b7c7-b830c49a8026',
+                    name: 'params 1'
+                }
+            },
+            inherited: false
+        }
+    ];
+
     beforeEach(() => {
         TestBed.configureTestingModule({
             imports: [ComboEditor]
         });
         fixture = TestBed.createComponent(ComboEditor);
         component = fixture.componentInstance;
-        component.supportsParameters = false;
-        component.item = {
+
+        // re-establish the item before each test execution
+        item = {
             property: 'Destination',
             value: 'flowfile-attribute',
             descriptor: {
@@ -69,10 +117,114 @@ describe('ComboEditor', () => {
             dirty: false,
             type: 'required'
         };
-        fixture.detectChanges();
     });
 
     it('should create', () => {
-        expect(component).toBeTruthy();
+        if (item) {
+            component.item = item;
+            fixture.detectChanges();
+
+            expect(component).toBeTruthy();
+        }
+    });
+
+    it('verify combo value', () => {
+        if (item) {
+            component.item = item;
+            fixture.detectChanges();
+
+            const formValue = component.comboEditorForm.get('value')?.value;
+            
expect(component.itemLookup.get(formValue)?.value).toEqual(item.value);
+            
expect(component.comboEditorForm.get('parameterReference')).toBeNull();
+
+            spyOn(component.ok, 'next');
+            component.okClicked();
+            expect(component.ok.next).toHaveBeenCalledWith(item.value);
+        }
+    });
+
+    it('verify combo not required with null value and default', () => {
+        if (item) {
+            item.value = null;
+            item.descriptor.required = false;
+
+            component.item = item;
+            fixture.detectChanges();
+
+            const formValue = component.comboEditorForm.get('value')?.value;
+            
expect(component.itemLookup.get(formValue)?.value).toEqual(item.descriptor.defaultValue);
+            
expect(component.comboEditorForm.get('parameterReference')).toBeNull();
+
+            spyOn(component.ok, 'next');
+            component.okClicked();
+            
expect(component.ok.next).toHaveBeenCalledWith(item.descriptor.defaultValue);
+        }
+    });
+
+    it('verify combo not required with null value and no default', () => {
+        if (item) {
+            item.value = null;
+            item.descriptor.required = false;
+            item.descriptor.defaultValue = undefined;
+
+            component.item = item;
+            fixture.detectChanges();
+
+            const formValue = component.comboEditorForm.get('value')?.value;
+            
expect(component.itemLookup.get(formValue)?.value).toEqual(item.value);
+            
expect(component.comboEditorForm.get('parameterReference')).toBeNull();
+
+            spyOn(component.ok, 'next');
+            component.okClicked();
+            expect(component.ok.next).toHaveBeenCalledWith(item.value);
+        }
+    });
+
+    it('verify combo with parameter reference', () => {
+        if (item) {
+            item.value = '#{one}';
+
+            component.item = item;
+            component.getParameters = (sensitive: boolean) => {
+                return of(parameters);
+            };
+            fixture.detectChanges();
+            fixture.whenStable().then(() => {
+                const formValue = 
component.comboEditorForm.get('value')?.value;
+                
expect(component.itemLookup.get(formValue)?.value).toEqual(item?.value);
+                
expect(component.comboEditorForm.get('parameterReference')).toBeDefined();
+
+                const parameterReferenceValue = 
component.comboEditorForm.get('parameterReference')?.value;
+                
expect(component.itemLookup.get(parameterReferenceValue)?.value).toEqual(item?.value);
+
+                spyOn(component.ok, 'next');
+                component.okClicked();
+                expect(component.ok.next).toHaveBeenCalledWith(item?.value);
+            });
+        }
+    });
+
+    it('verify combo with missing parameter reference', () => {
+        if (item) {
+            item.value = '#{three}';
+
+            component.item = item;
+            component.getParameters = (sensitive: boolean) => {
+                return of(parameters);
+            };
+            fixture.detectChanges();
+            fixture.whenStable().then(() => {
+                const formValue = 
component.comboEditorForm.get('value')?.value;
+                
expect(component.itemLookup.get(formValue)?.value).toEqual('#{' + 
parameters[0].value + '}');
+                
expect(component.comboEditorForm.get('parameterReference')).toBeDefined();
+
+                const parameterReferenceValue = 
component.comboEditorForm.get('parameterReference')?.value;
+                
expect(component.itemLookup.get(parameterReferenceValue)?.value).toEqual(item?.value);
+
+                spyOn(component.ok, 'next');
+                component.okClicked();
+                expect(component.ok.next).toHaveBeenCalledWith('#{' + 
parameters[0].value + '}');
+            });
+        }
     });
 });
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.ts
index 8750382e8e..574c2e6d34 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/combo-editor/combo-editor.component.ts
@@ -25,12 +25,14 @@ import { MatButtonModule } from '@angular/material/button';
 import { MatCheckboxModule } from '@angular/material/checkbox';
 import { NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
 import { NifiTooltipDirective } from 
'../../../tooltips/nifi-tooltip.directive';
-import { PropertyDescriptor, AllowableValue, TextTipInput } from 
'../../../../../state/shared';
+import { AllowableValue, Parameter, PropertyDescriptor, TextTipInput } from 
'../../../../../state/shared';
 import { MatOptionModule } from '@angular/material/core';
 import { MatSelectModule } from '@angular/material/select';
 import { MatTooltipModule } from '@angular/material/tooltip';
 import { TextTip } from '../../../tooltips/text-tip/text-tip.component';
 import { A11yModule } from '@angular/cdk/a11y';
+import { Observable, take } from 'rxjs';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
 
 export interface AllowableValueItem extends AllowableValue {
     id: number;
@@ -55,57 +57,32 @@ export interface AllowableValueItem extends AllowableValue {
         NgForOf,
         MatTooltipModule,
         NgIf,
-        A11yModule
+        A11yModule,
+        NgxSkeletonLoaderModule
     ],
     styleUrls: ['./combo-editor.component.scss']
 })
 export class ComboEditor {
     @Input() set item(item: PropertyItem) {
-        this.itemLookup.clear();
-        this.descriptor = item.descriptor;
-        this.allowableValues = [];
-
-        let i: number = 0;
-        let selectedItem: AllowableValueItem | null = null;
-
-        if (!this.descriptor.required) {
-            const noValue: AllowableValueItem = {
-                id: i++,
-                displayName: 'No value',
-                value: null
-            };
-            this.itemLookup.set(noValue.id, noValue);
-            this.allowableValues.push(noValue);
-
-            if (noValue.value == item.value) {
-                selectedItem = noValue;
-            }
+        if (item.value != null) {
+            this.configuredValue = item.value;
+        } else if (item.descriptor.defaultValue != null) {
+            this.configuredValue = item.descriptor.defaultValue;
         }
 
-        const allowableValueItems: AllowableValueItem[] = 
this.descriptor.allowableValues.map(
-            (allowableValueEntity) => {
-                const allowableValue: AllowableValueItem = {
-                    ...allowableValueEntity.allowableValue,
-                    id: i++
-                };
-                this.itemLookup.set(allowableValue.id, allowableValue);
+        this.descriptor = item.descriptor;
+        this.sensitive = item.descriptor.sensitive;
 
-                if (allowableValue.value == item.value) {
-                    selectedItem = allowableValue;
-                }
+        this.itemSet = true;
+        this.initialAllowableValues();
+    }
 
-                return allowableValue;
-            }
-        );
-        this.allowableValues.push(...allowableValueItems);
+    @Input() set getParameters(getParameters: (sensitive: boolean) => 
Observable<Parameter[]>) {
+        this._getParameters = getParameters;
 
-        if (selectedItem) {
-            // mat-select does not have good support for options with null 
value so we've
-            // introduced a mapping to work around the shortcoming
-            this.comboEditorForm.get('value')?.setValue(selectedItem.id);
-        }
+        this.supportsParameters = getParameters != null;
+        this.initialAllowableValues();
     }
-    @Input() supportsParameters: boolean = false;
 
     @Output() ok: EventEmitter<any> = new EventEmitter<any>();
     @Output() cancel: EventEmitter<void> = new EventEmitter<void>();
@@ -113,21 +90,172 @@ export class ComboEditor {
     protected readonly TextTip = TextTip;
 
     itemLookup: Map<number, AllowableValueItem> = new Map<number, 
AllowableValueItem>();
+    referencesParametersId: number = -1;
+    configuredParameterId: number = -1;
 
     comboEditorForm: FormGroup;
     descriptor!: PropertyDescriptor;
     allowableValues!: AllowableValueItem[];
 
+    showParameterAllowableValues: boolean = false;
+    parameterAllowableValues!: AllowableValueItem[];
+
+    sensitive: boolean = false;
+    supportsParameters: boolean = false;
+    parametersLoaded: boolean = false;
+
+    itemSet: boolean = false;
+    configuredValue: string | null = null;
+    _getParameters!: (sensitive: boolean) => Observable<Parameter[]>;
+
     constructor(private formBuilder: FormBuilder) {
         this.comboEditorForm = this.formBuilder.group({
             value: new FormControl(null, Validators.required)
         });
     }
 
+    initialAllowableValues(): void {
+        if (this.itemSet) {
+            this.itemLookup.clear();
+            this.allowableValues = [];
+            this.referencesParametersId = -1;
+
+            let i: number = 0;
+            let selectedItem: AllowableValueItem | null = null;
+
+            if (!this.descriptor.required) {
+                const noValue: AllowableValueItem = {
+                    id: i++,
+                    displayName: 'No value',
+                    value: null
+                };
+                this.itemLookup.set(noValue.id, noValue);
+                this.allowableValues.push(noValue);
+
+                if (noValue.value == this.configuredValue) {
+                    selectedItem = noValue;
+                }
+            }
+
+            if (this.descriptor.allowableValues) {
+                const allowableValueItems: AllowableValueItem[] = 
this.descriptor.allowableValues.map(
+                    (allowableValueEntity) => {
+                        const allowableValue: AllowableValueItem = {
+                            ...allowableValueEntity.allowableValue,
+                            id: i++
+                        };
+                        this.itemLookup.set(allowableValue.id, allowableValue);
+
+                        if (allowableValue.value == this.configuredValue) {
+                            selectedItem = allowableValue;
+                        }
+
+                        return allowableValue;
+                    }
+                );
+                this.allowableValues.push(...allowableValueItems);
+            }
+
+            if (this.supportsParameters) {
+                this.parametersLoaded = false;
+
+                // parameters are supported so add the item to support showing
+                // and hiding the parameter options select
+                const referencesParameterOption: AllowableValueItem = {
+                    id: i++,
+                    displayName: 'Reference Parameter...',
+                    value: null
+                };
+                this.allowableValues.push(referencesParameterOption);
+                this.itemLookup.set(referencesParameterOption.id, 
referencesParameterOption);
+
+                // record the item of the item to more easily identify this 
item
+                this.referencesParametersId = referencesParameterOption.id;
+
+                // if the current value references a parameter auto select the
+                // references parameter item
+                if (this.referencesParameter(this.configuredValue)) {
+                    selectedItem = referencesParameterOption;
+
+                    // trigger allowable value changed to show the parameters
+                    this.allowableValueChanged(this.referencesParametersId);
+                }
+
+                this._getParameters(this.sensitive)
+                    .pipe(take(1))
+                    .subscribe((parameters) => {
+                        if (parameters.length > 0) {
+                            // capture the value of i which will be the id of 
the first
+                            // parameter
+                            this.configuredParameterId = i;
+
+                            // create allowable values for each parameter
+                            parameters.forEach((parameter) => {
+                                const parameterItem: AllowableValueItem = {
+                                    id: i++,
+                                    displayName: parameter.name,
+                                    value: '#{' + parameter.name + '}',
+                                    description: parameter.description
+                                };
+                                
this.parameterAllowableValues.push(parameterItem);
+                                this.itemLookup.set(parameterItem.id, 
parameterItem);
+
+                                // if the configured parameter is still 
available,
+                                // capture the id, so we can auto select it
+                                if (parameterItem.value === 
this.configuredValue) {
+                                    this.configuredParameterId = 
parameterItem.id;
+                                }
+                            });
+
+                            // if combo still set to reference a parameter, 
set the default value
+                            if (this.comboEditorForm.get('value')?.value == 
this.referencesParametersId) {
+                                
this.comboEditorForm.get('parameterReference')?.setValue(this.configuredParameterId);
+                            }
+                        }
+
+                        this.parametersLoaded = true;
+                    });
+            } else {
+                this.parameterAllowableValues = [];
+            }
+
+            if (selectedItem) {
+                // mat-select does not have good support for options with null 
value so we've
+                // introduced a mapping to work around the shortcoming
+                this.comboEditorForm.get('value')?.setValue(selectedItem.id);
+            }
+        }
+    }
+
+    referencesParameter(value: string | null): boolean {
+        if (value) {
+            return value.startsWith('#{') && value.endsWith('}');
+        }
+
+        return false;
+    }
+
     preventDrag(event: MouseEvent): void {
         event.stopPropagation();
     }
 
+    allowableValueChanged(value: number): void {
+        this.showParameterAllowableValues = value === 
this.referencesParametersId;
+
+        if (this.showParameterAllowableValues) {
+            if (this.configuredParameterId === -1) {
+                this.comboEditorForm.addControl('parameterReference', new 
FormControl(null, Validators.required));
+            } else {
+                this.comboEditorForm.addControl(
+                    'parameterReference',
+                    new FormControl(this.configuredParameterId, 
Validators.required)
+                );
+            }
+        } else {
+            this.comboEditorForm.removeControl('parameterReference');
+        }
+    }
+
     getComboPlaceholder(): string {
         const valueControl: AbstractControl | null = 
this.comboEditorForm.get('value');
         if (valueControl) {
@@ -150,7 +278,21 @@ export class ComboEditor {
         if (valueControl) {
             const selectedItem: AllowableValueItem | undefined = 
this.itemLookup.get(valueControl.value);
             if (selectedItem) {
-                this.ok.next(selectedItem.value);
+                // if the value currently references a parameter emit the 
parameter, get the parameter reference control and emit that value
+                if (selectedItem.id == this.referencesParametersId) {
+                    const parameterReferenceControl: AbstractControl | null =
+                        this.comboEditorForm.get('parameterReference');
+                    if (parameterReferenceControl) {
+                        const selectedParameterItem: AllowableValueItem | 
undefined = this.itemLookup.get(
+                            parameterReferenceControl.value
+                        );
+                        if (selectedParameterItem) {
+                            this.ok.next(selectedParameterItem.value);
+                        }
+                    }
+                } else {
+                    this.ok.next(selectedItem.value);
+                }
             }
         }
     }
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.spec.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.spec.ts
index d28d0a96a7..083a591e6b 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.spec.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.spec.ts
@@ -19,6 +19,7 @@ import { ComponentFixture, TestBed } from 
'@angular/core/testing';
 
 import { NfEditor } from './nf-editor.component';
 import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { PropertyItem } from '../../property-table.component';
 
 describe('NfEditor', () => {
     let component: NfEditor;
@@ -30,10 +31,84 @@ describe('NfEditor', () => {
         });
         fixture = TestBed.createComponent(NfEditor);
         component = fixture.componentInstance;
-        fixture.detectChanges();
     });
 
     it('should create', () => {
+        fixture.detectChanges();
         expect(component).toBeTruthy();
     });
+
+    it('verify value set', () => {
+        const value: string = 'my-group-id';
+        const item: PropertyItem = {
+            property: 'group.id',
+            value,
+            descriptor: {
+                name: 'group.id',
+                displayName: 'Group ID',
+                description:
+                    "A Group ID is used to identify consumers that are within 
the same consumer group. Corresponds to Kafka's 'group.id' property.",
+                required: true,
+                sensitive: false,
+                dynamic: false,
+                supportsEl: true,
+                expressionLanguageScope: 'Environment variables defined at JVM 
level and system properties',
+                dependencies: []
+            },
+            id: 3,
+            triggerEdit: false,
+            deleted: false,
+            added: false,
+            dirty: false,
+            type: 'required'
+        };
+
+        component.item = item;
+        fixture.detectChanges();
+
+        expect(component.nfEditorForm.get('value')?.value).toEqual(value);
+        expect(component.nfEditorForm.get('value')?.disabled).toBeFalse();
+        
expect(component.nfEditorForm.get('setEmptyString')?.value).toBeFalse();
+
+        spyOn(component.ok, 'next');
+        component.okClicked();
+        expect(component.ok.next).toHaveBeenCalledWith(value);
+    });
+
+    it('verify empty value set', () => {
+        const value: string = '';
+        const item: PropertyItem = {
+            property: 'group.id',
+            value,
+            descriptor: {
+                name: 'group.id',
+                displayName: 'Group ID',
+                description:
+                    "A Group ID is used to identify consumers that are within 
the same consumer group. Corresponds to Kafka's 'group.id' property.",
+                required: true,
+                sensitive: false,
+                dynamic: false,
+                supportsEl: true,
+                expressionLanguageScope: 'Environment variables defined at JVM 
level and system properties',
+                dependencies: []
+            },
+            id: 3,
+            triggerEdit: false,
+            deleted: false,
+            added: false,
+            dirty: false,
+            type: 'required'
+        };
+
+        component.item = item;
+        fixture.detectChanges();
+
+        expect(component.nfEditorForm.get('value')?.value).toEqual(value);
+        expect(component.nfEditorForm.get('value')?.disabled).toBeTruthy();
+        
expect(component.nfEditorForm.get('setEmptyString')?.value).toBeTruthy();
+
+        spyOn(component.ok, 'next');
+        component.okClicked();
+        expect(component.ok.next).toHaveBeenCalledWith(value);
+    });
 });
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.ts
index 6086b635f5..acf4feec45 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/editors/nf-editor/nf-editor.component.ts
@@ -15,18 +15,7 @@
  * limitations under the License.
  */
 
-import {
-    AfterViewInit,
-    Component,
-    ElementRef,
-    EventEmitter,
-    Input,
-    OnDestroy,
-    Output,
-    Renderer2,
-    ViewChild,
-    ViewContainerRef
-} from '@angular/core';
+import { Component, EventEmitter, Input, OnDestroy, Output, Renderer2, 
ViewContainerRef } from '@angular/core';
 import { PropertyItem } from '../../property-table.component';
 import { CdkDrag, CdkDragHandle } from '@angular/cdk/drag-drop';
 import { AbstractControl, FormBuilder, FormControl, FormGroup, 
ReactiveFormsModule, Validators } from '@angular/forms';
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.html
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.html
index c069eb5ad9..563f9cc304 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.html
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.html
@@ -145,6 +145,7 @@
             <combo-editor
                 *ngIf="hasAllowableValues(editorItem); else nfEditor"
                 [item]="editorItem"
+                [getParameters]="getParameters"
                 (ok)="savePropertyValue(editorItem, $event)"
                 (cancel)="closeEditor()"></combo-editor>
             <ng-template #nfEditor>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.ts
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.ts
index 36af490253..0900ead04d 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.ts
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.ts
@@ -347,8 +347,8 @@ export class PropertyTable implements AfterViewInit, 
ControlValueAccessor {
     }
 
     resolvePropertyValue(property: Property): string | null {
-        const allowableValues: AllowableValueEntity[] = 
property.descriptor.allowableValues;
-        if (this.nifiCommon.isEmpty(allowableValues)) {
+        const allowableValues: AllowableValueEntity[] | undefined = 
property.descriptor.allowableValues;
+        if (allowableValues == null || 
this.nifiCommon.isEmpty(allowableValues)) {
             return property.value;
         } else {
             const allowableValue: AllowableValueEntity | undefined = 
allowableValues.find(
@@ -392,7 +392,7 @@ export class PropertyTable implements AfterViewInit, 
ControlValueAccessor {
         // TODO - add Input() for supportsGoTo? currently only false in 
summary table
 
         const descriptor: PropertyDescriptor = item.descriptor;
-        if (item.value && descriptor.identifiesControllerService) {
+        if (item.value && descriptor.identifiesControllerService && 
descriptor.allowableValues) {
             return descriptor.allowableValues.some(
                 (entity: AllowableValueEntity) => entity.allowableValue.value 
== item.value
             );


Reply via email to