Skip to content

Commit f6d88ae

Browse files
authored
NIFI-14965: Adding the property value tooltip that helps see embedded… (#10304)
* NIFI-14965: Adding the property value tooltip that helps see embedded parameter values. * NIFI-14965: Addressing review feedback. This closes #10304
1 parent 7a9c858 commit f6d88ae

File tree

8 files changed

+334
-2
lines changed

8 files changed

+334
-2
lines changed

nifi-frontend/src/main/frontend/apps/nifi/src/app/state/shared/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@ export interface BulletinsTipInput {
245245
bulletins: BulletinEntity[];
246246
}
247247

248+
export interface PropertyValueTipInput {
249+
parameters: ParameterEntity[];
250+
property: Property;
251+
}
252+
248253
export interface PropertyTipInput {
249254
descriptor: PropertyDescriptor;
250255
propertyHistory?: PropertyHistory;

nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/property-table.component.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@
8484
<div class="flex justify-between items-center">
8585
<div
8686
class="whitespace-nowrap overflow-hidden text-ellipsis"
87-
[title]="resolvedValue">
87+
nifiTooltip
88+
[tooltipComponentType]="PropertyValueTip"
89+
[tooltipInputData]="getPropertyValueTipData(item)"
90+
[position]="tooltipPosition"
91+
[delayClose]="false">
8892
{{ resolvedValue }}
8993
</div>
9094
@if (hasExtraWhitespace(resolvedValue)) {

nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/property-table/property-table.component.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@ import {
4242
Property,
4343
PropertyDependency,
4444
PropertyDescriptor,
45-
PropertyTipInput
45+
PropertyTipInput,
46+
PropertyValueTipInput
4647
} from '../../../state/shared';
4748
import { PropertyTip } from '../tooltips/property-tip/property-tip.component';
4849
import { NfEditor } from './editors/nf-editor/nf-editor.component';
4950
import {
5051
CdkConnectedOverlay,
5152
CdkOverlayOrigin,
53+
ConnectedPosition,
5254
ConnectionPositionPair,
5355
OriginConnectionPosition,
5456
OverlayConnectionPosition
@@ -59,6 +61,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5961
import { ConvertToParameterResponse } from '../../../pages/flow-designer/service/parameter-helper.service';
6062
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
6163
import { PropertyItem } from './property-item';
64+
import { PropertyValueTip } from '../tooltips/property-value-tip/property-value-tip.component';
6265

6366
@Component({
6467
selector: 'property-table',
@@ -138,6 +141,14 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
138141
};
139142
public editorPositions: ConnectionPositionPair[] = [];
140143

144+
tooltipPosition: ConnectedPosition = {
145+
originX: 'start',
146+
originY: 'bottom',
147+
overlayX: 'start',
148+
overlayY: 'top',
149+
offsetY: 4
150+
};
151+
141152
constructor(
142153
private changeDetector: ChangeDetectorRef,
143154
private nifiCommon: NiFiCommon
@@ -431,6 +442,13 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
431442
};
432443
}
433444

445+
getPropertyValueTipData(item: PropertyItem): PropertyValueTipInput {
446+
return {
447+
property: item,
448+
parameters: this.parameterContext?.component?.parameters || []
449+
};
450+
}
451+
434452
hasAllowableValues(item: PropertyItem): boolean {
435453
return Array.isArray(item.descriptor.allowableValues);
436454
}
@@ -617,4 +635,6 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
617635
}
618636
return false;
619637
}
638+
639+
protected readonly PropertyValueTip = PropertyValueTip;
620640
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!--
2+
~ Licensed to the Apache Software Foundation (ASF) under one or more
3+
~ contributor license agreements. See the NOTICE file distributed with
4+
~ this work for additional information regarding copyright ownership.
5+
~ The ASF licenses this file to You under the Apache License, Version 2.0
6+
~ (the "License"); you may not use this file except in compliance with
7+
~ the License. You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<div class="tooltip property-value-tip overflow-hidden">
19+
<div class="line-clamp-[10] font-mono text-sm whitespace-pre max-h-52 truncate">{{ data?.property?.value }}</div>
20+
21+
@if (parameterReferences.length > 0) {
22+
<div class="mt-4">Parameter values:</div>
23+
<table class="w-full min-w-72">
24+
@for (param of parameterReferences; track param.name) {
25+
<tr>
26+
<td class="font-bold pr-4 leading-4">{{ param.name }}</td>
27+
<td class="line-clamp-[10] leading-4 pl-4 whitespace-pre font-mono text-sm">
28+
<div class="truncate max-w-xs">
29+
{{ param.value }}
30+
</div>
31+
</td>
32+
</tr>
33+
}
34+
</table>
35+
}
36+
</div>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { ComponentFixture, TestBed } from '@angular/core/testing';
19+
import { PropertyValueTip } from './property-value-tip.component';
20+
import { PropertyValueTipInput } from '../../../../state/shared';
21+
22+
describe('PropertyValueTip', () => {
23+
let component: PropertyValueTip;
24+
let fixture: ComponentFixture<PropertyValueTip>;
25+
26+
beforeEach(async () => {
27+
await TestBed.configureTestingModule({
28+
imports: [PropertyValueTip]
29+
}).compileComponents();
30+
31+
fixture = TestBed.createComponent(PropertyValueTip);
32+
component = fixture.componentInstance;
33+
fixture.detectChanges();
34+
});
35+
36+
it('should create', () => {
37+
expect(component).toBeTruthy();
38+
});
39+
40+
describe('extractParameterReferences', () => {
41+
function buildDescriptor(overrides: Partial<any> = {}) {
42+
return {
43+
name: 'prop',
44+
displayName: 'Prop',
45+
description: 'desc',
46+
required: false,
47+
sensitive: false,
48+
dynamic: false,
49+
supportsEl: true,
50+
expressionLanguageScope: '',
51+
dependencies: [],
52+
...overrides
53+
};
54+
}
55+
56+
it('should return early when property is sensitive', () => {
57+
const data: PropertyValueTipInput = {
58+
property: {
59+
property: 'prop',
60+
value: "#{'PARAM_A'}",
61+
descriptor: buildDescriptor({ sensitive: true })
62+
},
63+
parameters: [{ parameter: { name: 'PARAM_A', description: '', sensitive: false, value: '1' } }]
64+
};
65+
66+
component.data = data;
67+
fixture.detectChanges();
68+
69+
expect(component.parameterReferences.length).toBe(0);
70+
});
71+
72+
it('should match quoted and unquoted parameter references', () => {
73+
const data: PropertyValueTipInput = {
74+
property: {
75+
property: 'prop',
76+
value: 'start #{PARAM_A} mid #{\'PARAM_B\'} end #{"PARAM_C"}',
77+
descriptor: buildDescriptor()
78+
},
79+
parameters: [
80+
{ parameter: { name: 'PARAM_A', description: '', sensitive: false, value: 'a' } },
81+
{ parameter: { name: 'PARAM_B', description: '', sensitive: false, value: 'b' } },
82+
{ parameter: { name: 'PARAM_C', description: '', sensitive: false, value: 'c' } }
83+
]
84+
};
85+
86+
component.data = data;
87+
fixture.detectChanges();
88+
89+
const names = component.parameterReferences.map((p) => p.name);
90+
expect(names).toEqual(['PARAM_A', 'PARAM_B', 'PARAM_C']);
91+
});
92+
93+
it('should ignore sensitive parameters in regex construction', () => {
94+
const data: PropertyValueTipInput = {
95+
property: {
96+
property: 'prop',
97+
value: '#{PARAM_A} #{PARAM_SEC}',
98+
descriptor: buildDescriptor()
99+
},
100+
parameters: [
101+
{ parameter: { name: 'PARAM_A', description: '', sensitive: false, value: 'a' } },
102+
{ parameter: { name: 'PARAM_SEC', description: '', sensitive: true, value: 'secret' } }
103+
]
104+
};
105+
106+
component.data = data;
107+
fixture.detectChanges();
108+
109+
const names = component.parameterReferences.map((p) => p.name);
110+
expect(names).toEqual(['PARAM_A']);
111+
});
112+
113+
it('should capture multiple occurrences of the same parameter', () => {
114+
const data: PropertyValueTipInput = {
115+
property: {
116+
property: 'prop',
117+
value: "#{PARAM_A} and again #{'PARAM_A'}",
118+
descriptor: buildDescriptor()
119+
},
120+
parameters: [{ parameter: { name: 'PARAM_A', description: '', sensitive: false, value: 'a' } }]
121+
};
122+
123+
component.data = data;
124+
fixture.detectChanges();
125+
126+
expect(component.parameterReferences.length).toBe(2);
127+
expect(component.parameterReferences[0].name).toBe('PARAM_A');
128+
expect(component.parameterReferences[1].name).toBe('PARAM_A');
129+
});
130+
131+
it('should handle null or empty property values', () => {
132+
const dataNull: PropertyValueTipInput = {
133+
property: {
134+
property: 'prop',
135+
value: null,
136+
descriptor: buildDescriptor()
137+
},
138+
parameters: [{ parameter: { name: 'PARAM_A', description: '', sensitive: false, value: 'a' } }]
139+
};
140+
141+
component.data = dataNull;
142+
fixture.detectChanges();
143+
expect(component.parameterReferences.length).toBe(0);
144+
145+
const dataEmpty: PropertyValueTipInput = {
146+
property: {
147+
property: 'prop',
148+
value: '',
149+
descriptor: buildDescriptor()
150+
},
151+
parameters: [{ parameter: { name: 'PARAM_A', description: '', sensitive: false, value: 'a' } }]
152+
};
153+
154+
component.data = dataEmpty;
155+
fixture.detectChanges();
156+
expect(component.parameterReferences.length).toBe(0);
157+
});
158+
});
159+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { Component, Input } from '@angular/core';
19+
import { PropertyValueTipInput } from '../../../../state/shared';
20+
import { Parameter } from '@nifi/shared';
21+
22+
@Component({
23+
selector: 'property-value-tip',
24+
standalone: true,
25+
templateUrl: './property-value-tip.component.html',
26+
styleUrl: './property-value-tip.component.scss'
27+
})
28+
export class PropertyValueTip {
29+
private _data: PropertyValueTipInput | undefined;
30+
private parameterRegex = new RegExp('^$');
31+
32+
@Input() set data(data: PropertyValueTipInput | undefined) {
33+
this._data = data;
34+
this.extractParameterReferences();
35+
}
36+
get data(): PropertyValueTipInput | undefined {
37+
return this._data;
38+
}
39+
40+
parameterReferences: Parameter[] = [];
41+
42+
private extractParameterReferences() {
43+
if (this._data?.property.descriptor.sensitive) {
44+
return;
45+
}
46+
47+
const propertyValue = this._data?.property.value || null;
48+
49+
// get all the non-sensitive parameters
50+
const parameters = this.data?.parameters
51+
.filter((parameter) => !parameter.parameter.sensitive)
52+
.map((parameter) => parameter.parameter);
53+
54+
if (propertyValue && parameters && parameters.length > 0) {
55+
this.parameterReferences = [];
56+
57+
// build up the regex that will match any parameter in a string, even if it is quoted
58+
const allParamsRegex = parameters.reduce((regex, param, idx) => {
59+
if (idx > 0) {
60+
regex += '|';
61+
}
62+
const quoteCaptureGroupIndex = idx * 2 + 1;
63+
regex += `#{(['"]?)(${param.name})\\${quoteCaptureGroupIndex}}`;
64+
return regex;
65+
}, '');
66+
this.parameterRegex = new RegExp(allParamsRegex, 'gm');
67+
68+
let matched;
69+
while ((matched = this.parameterRegex.exec(propertyValue)) !== null) {
70+
// pull out the parameter name matched from the capturing groups, ignore any quote group
71+
const paramName = matched.splice(1).find((match) => !!match && match !== "'" && match !== '"');
72+
73+
// get the Parameter object that was matched
74+
const param = parameters.find((param) => param.name === paramName);
75+
76+
// if matched, add it to the list of parameter references
77+
if (param) {
78+
this.parameterReferences.push(param);
79+
}
80+
}
81+
}
82+
}
83+
}

nifi-frontend/src/main/frontend/libs/shared/src/components/codemirror/autocomplete/parameter-tip/parameter-tip.component.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@
1919
@if (data?.parameter; as parameter) {
2020
<div class="flex flex-col gap-y-3">
2121
<div class="parameter-name text-lg font-bold">{{ parameter.name }}</div>
22+
@if (!parameter.sensitive) {
23+
@if (parameter.value === null) {
24+
<div class="unset neutral-color">No value set</div>
25+
} @else if (parameter.value === '') {
26+
<div class="unset neutral-color">Empty string set</div>
27+
} @else {
28+
<div class="line-clamp-[20] leading-4 whitespace-pre font-mono text-sm truncate">{{ parameter.value }}</div>
29+
}
30+
}
2231
@if (hasDescription(parameter)) {
2332
<div>{{ parameter.description }}</div>
2433
} @else {

0 commit comments

Comments
 (0)