NIFI-13136: Allowing users to unset optional property values (#8734)

* NIFI-13136:
- Allowing users to unset optional property values.
- Only selecting value and applying focus if it is not read only.

* NIFI-13136:
- Addressing review feedback.
- Adding styles to disabled editor input.
- Fixing show hint/autocomplete in production build.

This closes #8734
This commit is contained in:
Matt Gilman 2024-05-03 15:18:27 -04:00 committed by GitHub
parent 37937ffa15
commit 914e2b1057
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 74 additions and 38 deletions

View File

@ -28,7 +28,6 @@ import { MatSelectModule } from '@angular/material/select';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ParameterContextEntity, SelectOption } from '../../../../../../../state/shared'; import { ParameterContextEntity, SelectOption } from '../../../../../../../state/shared';
import { Client } from '../../../../../../../service/client.service'; import { Client } from '../../../../../../../service/client.service';
import { PropertyTable } from '../../../../../../../ui/common/property-table/property-table.component';
import { NifiSpinnerDirective } from '../../../../../../../ui/common/spinner/nifi-spinner.directive'; import { NifiSpinnerDirective } from '../../../../../../../ui/common/spinner/nifi-spinner.directive';
import { NifiTooltipDirective } from '../../../../../../../ui/common/tooltips/nifi-tooltip.directive'; import { NifiTooltipDirective } from '../../../../../../../ui/common/tooltips/nifi-tooltip.directive';
import { TextTip } from '../../../../../../../ui/common/tooltips/text-tip/text-tip.component'; import { TextTip } from '../../../../../../../ui/common/tooltips/text-tip/text-tip.component';
@ -51,7 +50,6 @@ import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-b
MatOptionModule, MatOptionModule,
MatSelectModule, MatSelectModule,
AsyncPipe, AsyncPipe,
PropertyTable,
NifiSpinnerDirective, NifiSpinnerDirective,
NifiTooltipDirective, NifiTooltipDirective,
FormsModule, FormsModule,

View File

@ -27,6 +27,8 @@ import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../../../state/error/error.reducer'; import { initialState } from '../../../../../../../state/error/error.reducer';
import { ClusterConnectionService } from '../../../../../../../service/cluster-connection.service'; import { ClusterConnectionService } from '../../../../../../../service/cluster-connection.service';
import 'codemirror/addon/hint/show-hint';
describe('EditProcessor', () => { describe('EditProcessor', () => {
let component: EditProcessor; let component: EditProcessor;
let fixture: ComponentFixture<EditProcessor>; let fixture: ComponentFixture<EditProcessor>;

View File

@ -27,6 +27,8 @@ import { initialState } from '../../../state/parameter-context-listing/parameter
import { ClusterConnectionService } from '../../../../../service/cluster-connection.service'; import { ClusterConnectionService } from '../../../../../service/cluster-connection.service';
import { ParameterContextEntity } from '../../../../../state/shared'; import { ParameterContextEntity } from '../../../../../state/shared';
import 'codemirror/addon/hint/show-hint';
describe('EditParameterContext', () => { describe('EditParameterContext', () => {
let component: EditParameterContext; let component: EditParameterContext;
let fixture: ComponentFixture<EditParameterContext>; let fixture: ComponentFixture<EditParameterContext>;

View File

@ -26,6 +26,8 @@ import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/error/error.reducer'; import { initialState } from '../../../../../state/error/error.reducer';
import { ClusterConnectionService } from '../../../../../service/cluster-connection.service'; import { ClusterConnectionService } from '../../../../../service/cluster-connection.service';
import 'codemirror/addon/hint/show-hint';
describe('EditFlowAnalysisRule', () => { describe('EditFlowAnalysisRule', () => {
let component: EditFlowAnalysisRule; let component: EditFlowAnalysisRule;
let fixture: ComponentFixture<EditFlowAnalysisRule>; let fixture: ComponentFixture<EditFlowAnalysisRule>;

View File

@ -26,6 +26,8 @@ import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/error/error.reducer'; import { initialState } from '../../../../../state/error/error.reducer';
import { ClusterConnectionService } from '../../../../../service/cluster-connection.service'; import { ClusterConnectionService } from '../../../../../service/cluster-connection.service';
import 'codemirror/addon/hint/show-hint';
describe('EditRegistryClient', () => { describe('EditRegistryClient', () => {
let component: EditRegistryClient; let component: EditRegistryClient;
let fixture: ComponentFixture<EditRegistryClient>; let fixture: ComponentFixture<EditRegistryClient>;

View File

@ -26,6 +26,8 @@ import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/error/error.reducer'; import { initialState } from '../../../../../state/error/error.reducer';
import { ClusterConnectionService } from '../../../../../service/cluster-connection.service'; import { ClusterConnectionService } from '../../../../../service/cluster-connection.service';
import 'codemirror/addon/hint/show-hint';
describe('EditReportingTask', () => { describe('EditReportingTask', () => {
let component: EditReportingTask; let component: EditReportingTask;
let fixture: ComponentFixture<EditReportingTask>; let fixture: ComponentFixture<EditReportingTask>;

View File

@ -28,7 +28,6 @@ import { AsyncPipe, NgTemplateOutlet } from '@angular/common';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { PropertyTable } from '../../property-table/property-table.component';
import { ControllerServiceApi } from '../controller-service-api/controller-service-api.component'; import { ControllerServiceApi } from '../controller-service-api/controller-service-api.component';
import { ControllerServiceReferences } from '../controller-service-references/controller-service-references.component'; import { ControllerServiceReferences } from '../controller-service-references/controller-service-references.component';
import { NifiSpinnerDirective } from '../../spinner/nifi-spinner.directive'; import { NifiSpinnerDirective } from '../../spinner/nifi-spinner.directive';
@ -59,7 +58,6 @@ import {
MatTabsModule, MatTabsModule,
MatOptionModule, MatOptionModule,
MatSelectModule, MatSelectModule,
PropertyTable,
ControllerServiceApi, ControllerServiceApi,
ControllerServiceReferences, ControllerServiceReferences,
AsyncPipe, AsyncPipe,

View File

@ -26,6 +26,8 @@ import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../state/error/error.reducer'; import { initialState } from '../../../../state/error/error.reducer';
import { ClusterConnectionService } from '../../../../service/cluster-connection.service'; import { ClusterConnectionService } from '../../../../service/cluster-connection.service';
import 'codemirror/addon/hint/show-hint';
describe('EditControllerService', () => { describe('EditControllerService', () => {
let component: EditControllerService; let component: EditControllerService;
let fixture: ComponentFixture<EditControllerService>; let fixture: ComponentFixture<EditControllerService>;

View File

@ -30,7 +30,6 @@ import { AsyncPipe, NgTemplateOutlet } from '@angular/common';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { PropertyTable } from '../../property-table/property-table.component';
import { ControllerServiceApi } from '../controller-service-api/controller-service-api.component'; import { ControllerServiceApi } from '../controller-service-api/controller-service-api.component';
import { ControllerServiceReferences } from '../controller-service-references/controller-service-references.component'; import { ControllerServiceReferences } from '../controller-service-references/controller-service-references.component';
import { NifiSpinnerDirective } from '../../spinner/nifi-spinner.directive'; import { NifiSpinnerDirective } from '../../spinner/nifi-spinner.directive';
@ -67,7 +66,6 @@ import {
MatTabsModule, MatTabsModule,
MatOptionModule, MatOptionModule,
MatSelectModule, MatSelectModule,
PropertyTable,
ControllerServiceApi, ControllerServiceApi,
ControllerServiceReferences, ControllerServiceReferences,
AsyncPipe, AsyncPipe,

View File

@ -61,17 +61,14 @@
@include mat.button-density(-1); @include mat.button-density(-1);
.nf-editor { .nf-editor {
border-color: var(--mdc-outlined-text-field-label-text-color);
&.blank {
border-color: var(--mdc-outlined-text-field-disabled-label-text-color);
}
.CodeMirror { .CodeMirror {
background-color: if($is-dark, $nifi-theme-surface-palette-darker, $nifi-theme-surface-palette-lighter); background-color: if($is-dark, $nifi-theme-surface-palette-darker, $nifi-theme-surface-palette-lighter);
&.blank {
background: $material-theme-primary-palette-default;
color: if(
$is-dark,
$material-theme-primary-palette-darker,
$material-theme-primary-palette-lighter
);
}
} }
.CodeMirror-code { .CodeMirror-code {

View File

@ -18,7 +18,6 @@
import { ComponentRef, Injectable, Renderer2, ViewContainerRef } from '@angular/core'; import { ComponentRef, Injectable, Renderer2, ViewContainerRef } from '@angular/core';
import * as CodeMirror from 'codemirror'; import * as CodeMirror from 'codemirror';
import { Editor, Hint, Hints, StringStream } from 'codemirror'; import { Editor, Hint, Hints, StringStream } from 'codemirror';
import 'codemirror/addon/hint/show-hint';
import { ElFunction, Parameter } from '../../../../../../state/shared'; import { ElFunction, Parameter } from '../../../../../../state/shared';
import { ParameterTip } from '../../../../tooltips/parameter-tip/parameter-tip.component'; import { ParameterTip } from '../../../../tooltips/parameter-tip/parameter-tip.component';
import { ElService } from './el.service'; import { ElService } from './el.service';

View File

@ -21,7 +21,7 @@
cdkDrag cdkDrag
resizable resizable
(resized)="resized()"> (resized)="resized()">
<form class="h-full" [formGroup]="nfEditorForm" cdkTrapFocus cdkTrapFocusAutoCapture> <form class="h-full" [formGroup]="nfEditorForm" cdkTrapFocus [cdkTrapFocusAutoCapture]="!readonly">
<div class="flex flex-col gap-y-3 h-full"> <div class="flex flex-col gap-y-3 h-full">
<div class="flex justify-end"> <div class="flex justify-end">
<div <div
@ -47,10 +47,9 @@
</ng-template> </ng-template>
</div> </div>
<div class="flex flex-col gap-y-0.5 flex-1"> <div class="flex flex-col gap-y-0.5 flex-1">
<div class="nf-editor flex-1" #nfEditorContainer> <div class="nf-editor flex-1" [class.blank]="blank">
<ngx-codemirror <ngx-codemirror
[options]="getOptions()" [options]="getOptions()"
[autoFocus]="true"
formControlName="value" formControlName="value"
(mousedown)="preventDrag($event)" (mousedown)="preventDrag($event)"
(codeMirrorLoaded)="codeMirrorLoaded($event)"></ngx-codemirror> (codeMirrorLoaded)="codeMirrorLoaded($event)"></ngx-codemirror>

View File

@ -31,7 +31,8 @@
.nf-editor { .nf-editor {
min-height: 100px; min-height: 100px;
min-width: 210px; min-width: 210px;
border: 1px solid; border-width: 1px;
border-style: solid;
cursor: default; cursor: default;
.CodeMirror { .CodeMirror {
@ -40,11 +41,6 @@
font-family: monospace; font-family: monospace;
cursor: default; cursor: default;
line-height: normal; line-height: normal;
&.blank {
border-width: 1px;
border-style: solid;
}
} }
.CodeMirror-scroll { .CodeMirror-scroll {

View File

@ -21,6 +21,8 @@ import { NfEditor } from './nf-editor.component';
import { HttpClientTestingModule } from '@angular/common/http/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing';
import { PropertyItem } from '../../property-table.component'; import { PropertyItem } from '../../property-table.component';
import 'codemirror/addon/hint/show-hint';
describe('NfEditor', () => { describe('NfEditor', () => {
let component: NfEditor; let component: NfEditor;
let fixture: ComponentFixture<NfEditor>; let fixture: ComponentFixture<NfEditor>;

View File

@ -57,15 +57,16 @@ import { Resizable } from '../../../resizable/resizable.component';
export class NfEditor implements OnDestroy { export class NfEditor implements OnDestroy {
@Input() set item(item: PropertyItem) { @Input() set item(item: PropertyItem) {
this.nfEditorForm.get('value')?.setValue(item.value); this.nfEditorForm.get('value')?.setValue(item.value);
if (item.descriptor.required) {
const isEmptyString: boolean = item.value == ''; this.nfEditorForm.get('value')?.addValidators(Validators.required);
this.nfEditorForm.get('setEmptyString')?.setValue(isEmptyString);
if (isEmptyString) {
this.nfEditorForm.get('value')?.disable();
} else { } else {
this.nfEditorForm.get('value')?.enable(); this.nfEditorForm.get('value')?.removeValidators(Validators.required);
} }
const isEmptyString: boolean = item.value === '';
this.nfEditorForm.get('setEmptyString')?.setValue(isEmptyString);
this.setEmptyStringChanged();
this.supportsEl = item.descriptor.supportsEl; this.supportsEl = item.descriptor.supportsEl;
this.sensitive = item.descriptor.sensitive; this.sensitive = item.descriptor.sensitive;
this.mode = this.supportsEl ? this.nfel.getLanguageId() : this.nfpr.getLanguageId(); this.mode = this.supportsEl ? this.nfel.getLanguageId() : this.nfpr.getLanguageId();
@ -83,7 +84,7 @@ export class NfEditor implements OnDestroy {
@Input() width!: number; @Input() width!: number;
@Input() readonly: boolean = false; @Input() readonly: boolean = false;
@Output() ok: EventEmitter<string> = new EventEmitter<string>(); @Output() ok: EventEmitter<string | null> = new EventEmitter<string | null>();
@Output() cancel: EventEmitter<void> = new EventEmitter<void>(); @Output() cancel: EventEmitter<void> = new EventEmitter<void>();
protected readonly PropertyHintTip = PropertyHintTip; protected readonly PropertyHintTip = PropertyHintTip;
@ -95,6 +96,7 @@ export class NfEditor implements OnDestroy {
sensitive = false; sensitive = false;
supportsEl = false; supportsEl = false;
supportsParameters = false; supportsParameters = false;
blank = false;
mode!: string; mode!: string;
_parameters!: Parameter[]; _parameters!: Parameter[];
@ -109,7 +111,7 @@ export class NfEditor implements OnDestroy {
private nfpr: NfPr private nfpr: NfPr
) { ) {
this.nfEditorForm = this.formBuilder.group({ this.nfEditorForm = this.formBuilder.group({
value: new FormControl('', Validators.required), value: new FormControl(''),
setEmptyString: new FormControl(false) setEmptyString: new FormControl(false)
}); });
} }
@ -117,7 +119,17 @@ export class NfEditor implements OnDestroy {
codeMirrorLoaded(codeEditor: any): void { codeMirrorLoaded(codeEditor: any): void {
this.editor = codeEditor.codeMirror; this.editor = codeEditor.codeMirror;
this.editor.setSize('100%', '100%'); this.editor.setSize('100%', '100%');
this.editor.execCommand('selectAll');
if (!this.readonly) {
this.editor.execCommand('selectAll');
}
// disabling of the input through the form isn't supported until codemirror
// has loaded so we must disable again if the value is an empty string
if (this.nfEditorForm.get('setEmptyString')?.value) {
this.nfEditorForm.get('value')?.disable();
this.editor.setOption('readOnly', 'nocursor');
}
} }
loadParameters(): void { loadParameters(): void {
@ -164,7 +176,9 @@ export class NfEditor implements OnDestroy {
extraKeys: { extraKeys: {
'Ctrl-Space': 'autocomplete', 'Ctrl-Space': 'autocomplete',
Enter: () => { Enter: () => {
this.okClicked(); if (this.nfEditorForm.dirty && this.nfEditorForm.valid) {
this.okClicked();
}
} }
} }
}; };
@ -188,6 +202,8 @@ export class NfEditor implements OnDestroy {
setEmptyStringChanged(): void { setEmptyStringChanged(): void {
const emptyStringChecked: AbstractControl | null = this.nfEditorForm.get('setEmptyString'); const emptyStringChecked: AbstractControl | null = this.nfEditorForm.get('setEmptyString');
if (emptyStringChecked) { if (emptyStringChecked) {
this.blank = emptyStringChecked.value;
if (emptyStringChecked.value) { if (emptyStringChecked.value) {
this.nfEditorForm.get('value')?.setValue(''); this.nfEditorForm.get('value')?.setValue('');
this.nfEditorForm.get('value')?.disable(); this.nfEditorForm.get('value')?.disable();
@ -207,8 +223,18 @@ export class NfEditor implements OnDestroy {
okClicked(): void { okClicked(): void {
const valueControl: AbstractControl | null = this.nfEditorForm.get('value'); const valueControl: AbstractControl | null = this.nfEditorForm.get('value');
if (valueControl) { const emptyStringChecked: AbstractControl | null = this.nfEditorForm.get('setEmptyString');
this.ok.next(valueControl.value); if (valueControl && emptyStringChecked) {
const value = valueControl.value;
if (value === '') {
if (emptyStringChecked.value) {
this.ok.next('');
} else {
this.ok.next(null);
}
} else {
this.ok.next(value);
}
} }
} }

View File

@ -19,6 +19,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PropertyTable } from './property-table.component'; import { PropertyTable } from './property-table.component';
import 'codemirror/addon/hint/show-hint';
describe('PropertyTable', () => { describe('PropertyTable', () => {
let component: PropertyTable; let component: PropertyTable;
let fixture: ComponentFixture<PropertyTable>; let fixture: ComponentFixture<PropertyTable>;

View File

@ -554,7 +554,7 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
} }
} }
savePropertyValue(item: PropertyItem, newValue: string): void { savePropertyValue(item: PropertyItem, newValue: string | null): void {
if (item.value != newValue) { if (item.value != newValue) {
item.value = newValue; item.value = newValue;
item.dirty = true; item.dirty = true;

View File

@ -19,6 +19,15 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module'; import { AppModule } from './app/app.module';
import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/addon/fold/brace-fold';
import 'codemirror/addon/fold/comment-fold';
import 'codemirror/addon/fold/foldcode';
import 'codemirror/addon/fold/foldgutter';
import 'codemirror/addon/fold/markdown-fold';
import 'codemirror/addon/fold/xml-fold';
import 'codemirror/addon/hint/show-hint';
platformBrowserDynamic() platformBrowserDynamic()
.bootstrapModule(AppModule) .bootstrapModule(AppModule)
.catch((err) => console.error(err)); .catch((err) => console.error(err));