This is an automated email from the ASF dual-hosted git repository.
mcgilman 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 9fa515bdfc4 NIFI-16206 connector toggle to handle "true"/"false"
strings as booleans (#11546)
9fa515bdfc4 is described below
commit 9fa515bdfc4c10ab2b54ba20a427b51603b1b2bf
Author: Scott Aslan <[email protected]>
AuthorDate: Tue Aug 25 09:57:03 2026 -0400
NIFI-16206 connector toggle to handle "true"/"false" strings as booleans
(#11546)
* NIFI-16206 connector toggle to handle "true"/"false" strings as booleans
* address review feedback
* address review feedback
---
.../connector-property-input.component.spec.ts | 10 ++
.../connector-property-input.component.ts | 8 +-
.../connector-configuration-step.component.spec.ts | 110 ++++++++++++++++++-
.../connector-configuration-step.component.ts | 30 ++++--
.../connector-wizard/step-dependency.utils.spec.ts | 24 +++++
.../connector-wizard/step-dependency.utils.ts | 10 +-
.../property-group-card.component.spec.ts | 119 ++++++++++++++++++---
.../property-group-card.component.ts | 20 ++--
.../src/services/value-reference.helper.spec.ts | 48 +++++++++
.../shared/src/services/value-reference.helper.ts | 23 +++-
.../shared/src/utils/connector-validation.utils.ts | 6 +-
.../src/utils/dependency-value.utils.spec.ts | 45 ++++++++
.../shared/src/utils/dependency-value.utils.ts | 39 +++++++
13 files changed, 435 insertions(+), 57 deletions(-)
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.spec.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.spec.ts
index 2b7a75001ee..101e5f53f44 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.spec.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.spec.ts
@@ -489,6 +489,16 @@ describe('ConnectorPropertyInput', () => {
const toggle =
fixture.debugElement.query(By.css('[data-qa="property-input-boolean"]'));
expect(toggle.componentInstance.checked).toBe(false);
});
+
+ it('does not coerce values for non-BOOLEAN properties', async () => {
+ const { inputComponent } = await setup({
+ property: makeProp({ type: 'STRING' })
+ });
+
+ inputComponent.writeValue('false');
+
+ expect(inputComponent.formControl.value).toBe('false');
+ });
});
describe('STRING_LIST rendering', () => {
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.ts
index ba3d5a5f8ff..711b91a7147 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.ts
@@ -47,6 +47,7 @@ import {
} from '../../types';
import { SearchableSelect } from
'../searchable-select/searchable-select.component';
import { AssetUpload } from '../asset-upload/asset-upload.component';
+import { toBooleanValue } from '../../services/value-reference.helper';
import { StringListOrphansStrippedEvent } from
'./connector-property-input.types';
/**
@@ -189,10 +190,9 @@ export class ConnectorPropertyInput implements
ControlValueAccessor, DoCheck, On
}
writeValue(value: unknown): void {
- let normalized = value;
- if (this.property()?.type === 'BOOLEAN') {
- normalized = value === true || value === 'true';
- }
+ // BOOLEAN values may still arrive as the wire strings "true"/"false".
MatSlideToggle
+ // coerces with `!!value`, which would render "false" as checked, so
normalize first.
+ const normalized = this.property()?.type === 'BOOLEAN' ?
toBooleanValue(value) : value;
this.formControl.setValue(normalized, { emitEvent: false });
if (this.property()?.type === 'SECRET') {
this.selectOptions = this.computeSelectOptions();
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.spec.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.spec.ts
index 1e785529297..8b74f7a83d3 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.spec.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.spec.ts
@@ -328,6 +328,34 @@ describe('SharedConnectorConfigurationStep', () => {
expect(component.stepForm.get('enabled')?.value).toBe(false);
});
+ // defaultValue and saved values are wire strings even for BOOLEAN.
They must be coerced
+ // to real booleans, otherwise mat-slide-toggle's `!!value` renders
"false" as checked.
+ it('coerces a BOOLEAN descriptor defaultValue of "false" to the
boolean false', async () => {
+ const stepConfig = makeStepConfig('test-step', [
+ makeProp('enabled', { type: 'BOOLEAN', defaultValue: 'false' })
+ ]);
+ const { component } = await setup({ stepConfig });
+ expect(component.stepForm.get('enabled')?.value).toBe(false);
+ });
+
+ it('coerces a BOOLEAN descriptor defaultValue of "true" to the boolean
true', async () => {
+ const stepConfig = makeStepConfig('test-step', [
+ makeProp('enabled', { type: 'BOOLEAN', defaultValue: 'true' })
+ ]);
+ const { component } = await setup({ stepConfig });
+ expect(component.stepForm.get('enabled')?.value).toBe(true);
+ });
+
+ it('coerces a saved BOOLEAN value of "false" to the boolean false',
async () => {
+ const stepConfig = makeStepConfig(
+ 'test-step',
+ [makeProp('enabled', { type: 'BOOLEAN', defaultValue: 'true'
})],
+ { enabled: { value: 'false', valueType: 'STRING_LITERAL' } }
+ );
+ const { component } = await setup({ stepConfig });
+ expect(component.stepForm.get('enabled')?.value).toBe(false);
+ });
+
it('defaults STRING_LIST properties to empty array', async () => {
const stepConfig = makeStepConfig('test-step', [makeProp('tags', {
type: 'STRING_LIST' })]);
const { component } = await setup({ stepConfig });
@@ -400,15 +428,53 @@ describe('SharedConnectorConfigurationStep', () => {
dependencies: [{ propertyName: 'mode', dependentValues:
['advanced'] }]
})
]);
- const { component } = await setup({ stepConfig });
+ const { component, fixture } = await setup({ stepConfig });
+ // setupFormSubscription is deferred with Promise.resolve() — same
wait as the form-changes tests
+ await fixture.whenStable();
component.setPropertyValue('mode', 'advanced');
- component['computeAllPropertyVisibility']();
expect(component.stepForm.get('advanced-setting')?.enabled).toBe(true);
expect(component.isPropertyVisible(makeProp('advanced-setting'))).toBe(true);
});
+ // A BOOLEAN toggle writes a native boolean into the form while
dependentValues stays
+ // string[] from the API, so the comparison has to survive the type
difference.
+ it('shows a property gated on a BOOLEAN once the toggle is turned on',
async () => {
+ const stepConfig = makeStepConfig('test-step', [
+ makeProp('enableImageExtraction', { type: 'BOOLEAN',
defaultValue: 'false' }),
+ makeProp('extractionMode', {
+ dependencies: [{ propertyName: 'enableImageExtraction',
dependentValues: ['true'] }]
+ })
+ ]);
+ const { component, fixture } = await setup({ stepConfig });
+ await fixture.whenStable();
+
+
expect(component.isPropertyVisible(makeProp('extractionMode'))).toBe(false);
+
+ component.setPropertyValue('enableImageExtraction', true);
+
+
expect(component.stepForm.get('extractionMode')?.enabled).toBe(true);
+
expect(component.isPropertyVisible(makeProp('extractionMode'))).toBe(true);
+ });
+
+ it('hides a property gated on a BOOLEAN again once the toggle is
turned back off', async () => {
+ const stepConfig = makeStepConfig('test-step', [
+ makeProp('enableImageExtraction', { type: 'BOOLEAN',
defaultValue: 'true' }),
+ makeProp('extractionMode', {
+ dependencies: [{ propertyName: 'enableImageExtraction',
dependentValues: ['true'] }]
+ })
+ ]);
+ const { component, fixture } = await setup({ stepConfig });
+ await fixture.whenStable();
+
+
expect(component.isPropertyVisible(makeProp('extractionMode'))).toBe(true);
+
+ component.setPropertyValue('enableImageExtraction', false);
+
+
expect(component.isPropertyVisible(makeProp('extractionMode'))).toBe(false);
+ });
+
it('hides a property when its dependency has no dependentValues and
the parent is empty', async () => {
const stepConfig = makeStepConfig('test-step', [
makeProp('optional-host'),
@@ -425,10 +491,10 @@ describe('SharedConnectorConfigurationStep', () => {
makeProp('optional-host'),
makeProp('host-port', { dependencies: [{ propertyName:
'optional-host' }] })
]);
- const { component } = await setup({ stepConfig });
+ const { component, fixture } = await setup({ stepConfig });
+ await fixture.whenStable();
component.setPropertyValue('optional-host', 'myhost.com');
- component['computeAllPropertyVisibility']();
expect(component.stepForm.get('host-port')?.enabled).toBe(true);
});
@@ -825,6 +891,42 @@ describe('SharedConnectorConfigurationStep', () => {
const result = component.getConfigurationForSave();
expect(result.isDirty).toBe(false);
});
+
+ // initializeForm coerces BOOLEAN wire strings to real booleans.
buildChangedConfiguration
+ // must use the same coercion when comparing against descriptor
defaults, or an untouched
+ // "false" default would always appear changed (string "false" !==
boolean false).
+ it('does not include an untouched BOOLEAN whose defaultValue was the
wire string "false"', async () => {
+ const stepConfig = makeStepConfig('test-step', [
+ makeProp('enabled', { type: 'BOOLEAN', defaultValue: 'false'
}),
+ makeProp('host')
+ ]);
+ const { component } = await setup({ stepConfig });
+
+ component.stepForm.get('host')?.setValue('changed-host');
+ component.stepForm.markAsDirty();
+
+ const result = component.getConfigurationForSave();
+ const propertyValues =
result.configuration?.propertyGroupConfigurations[0]?.propertyValues ?? {};
+ expect(propertyValues['enabled']).toBeUndefined();
+ expect(propertyValues['host']).toBeDefined();
+ });
+
+ it('does not include an untouched BOOLEAN whose saved value was the
wire string "false"', async () => {
+ const stepConfig = makeStepConfig(
+ 'test-step',
+ [makeProp('enabled', { type: 'BOOLEAN', defaultValue: 'true'
}), makeProp('host')],
+ { enabled: { value: 'false', valueType: 'STRING_LITERAL' } }
+ );
+ const { component } = await setup({ stepConfig });
+
+ component.stepForm.get('host')?.setValue('changed-host');
+ component.stepForm.markAsDirty();
+
+ const result = component.getConfigurationForSave();
+ const propertyValues =
result.configuration?.propertyGroupConfigurations[0]?.propertyValues ?? {};
+ expect(propertyValues['enabled']).toBeUndefined();
+ expect(propertyValues['host']).toBeDefined();
+ });
});
// ═══════════════════════════════════════════════════════
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.ts
index 35902ab1b04..dd2ba8a9c2d 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.ts
@@ -47,7 +47,13 @@ import { StringListOrphansStrippedEvent } from
'../../connector-property-input/c
import { ErrorBanner } from '../../error-banner/error-banner.component';
import { StatusBanner } from '../../status-banner/status-banner.component';
import { StatusBannerDescriptionDirective } from
'../../status-banner/status-banner.directives';
-import { fromValueReference, toValueReference, SecretReferenceOptions } from
'../../../services/value-reference.helper';
+import {
+ fromValueReference,
+ toValueReference,
+ toBooleanValue,
+ SecretReferenceOptions
+} from '../../../services/value-reference.helper';
+import { isDependencyValueSatisfied } from
'../../../utils/dependency-value.utils';
import {
AssetInfo,
AssetReference,
@@ -472,7 +478,11 @@ export class SharedConnectorConfigurationStep implements
SaveableStep, OnInit, O
const validators = this.buildValidators(property);
formConfig[property.name] = [formValue, validators];
} else {
- const currentValue = unsavedValue ?? apiValue ??
property.defaultValue ?? fallbackValue;
+ const resolvedValue = unsavedValue ?? apiValue ??
property.defaultValue ?? fallbackValue;
+ // The descriptor's defaultValue is a wire string even for
BOOLEAN, so coerce
+ // before it enters the form tree: the toggle renders
`!!value` and would show
+ // a stored "false" as checked.
+ const currentValue = property.type === 'BOOLEAN' ?
toBooleanValue(resolvedValue) : resolvedValue;
const validators = this.buildValidators(property);
formConfig[property.name] = [currentValue, validators];
}
@@ -648,13 +658,7 @@ export class SharedConnectorConfigurationStep implements
SaveableStep, OnInit, O
const dependentValue = dependentControl.value;
- if (dependency.dependentValues &&
dependency.dependentValues.length > 0) {
- // If specific values are required, check if current value
matches
- return dependency.dependentValues.includes(dependentValue as
string);
- } else {
- // If no specific values, just check if dependent property has
any value
- return dependentValue !== null && dependentValue !== undefined
&& dependentValue !== '';
- }
+ return isDependencyValueSatisfied(dependentValue,
dependency.dependentValues);
});
}
@@ -898,8 +902,12 @@ export class SharedConnectorConfigurationStep implements
SaveableStep, OnInit, O
let originalValue = apiValue ??
property.defaultValue ?? fallbackValue;
- // Normalize assets so comparison matches the form
value shape
- if (property.type === 'ASSET') {
+ // Normalize so comparison matches the form value
shape
+ if (property.type === 'BOOLEAN') {
+ // initializeForm coerces BOOLEAN values to
real booleans; without the
+ // same coercion here a stored "false" would
always look changed.
+ originalValue = toBooleanValue(originalValue);
+ } else if (property.type === 'ASSET') {
// fromValueReference returns
AssetReference|null; form stores string|null
originalValue = originalValue?.id ?? null;
} else if (property.type === 'ASSET_LIST') {
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.spec.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.spec.ts
index 8379fdce2d3..9205c79aaf5 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.spec.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.spec.ts
@@ -121,6 +121,30 @@ describe('Step Dependency Utils', () => {
expect(isStepDependencySatisfied(dependency, {}, {},
visibleSteps)).toBe(false);
});
+
+ // A BOOLEAN toggle puts a native boolean into the form, while
dependentValues is always
+ // string[] from the API. Both directions of the toggle must be
evaluated correctly.
+ it('should satisfy a boolean dependency when the unsaved value is the
boolean true', () => {
+ const dependency = { stepName: 'Step1', propertyName:
'enableCortex', dependentValues: ['true'] };
+ const stepConfigurations = {
+ Step1: createStepConfig('Step1', [], { group1: { enableCortex:
'false' } })
+ };
+ const unsavedValues = { Step1: { enableCortex: true } };
+ const visibleSteps = new Set(['Step1']);
+
+ expect(isStepDependencySatisfied(dependency, stepConfigurations,
unsavedValues, visibleSteps)).toBe(true);
+ });
+
+ it('should not satisfy a boolean dependency when the unsaved value is
the boolean false', () => {
+ const dependency = { stepName: 'Step1', propertyName:
'enableCortex', dependentValues: ['true'] };
+ const stepConfigurations = {
+ Step1: createStepConfig('Step1', [], { group1: { enableCortex:
'true' } })
+ };
+ const unsavedValues = { Step1: { enableCortex: false } };
+ const visibleSteps = new Set(['Step1']);
+
+ expect(isStepDependencySatisfied(dependency, stepConfigurations,
unsavedValues, visibleSteps)).toBe(false);
+ });
});
describe('getVisibleStepNames', () => {
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.ts
index 696b596f013..be0f84a4ef0 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/step-dependency.utils.ts
@@ -17,6 +17,7 @@
import { ConfigurationStepConfiguration, ConfigurationStepDependency,
ConnectorPropertyFormValue } from '../../types';
import { fromValueReference } from '../../services/value-reference.helper';
+import { isDependencyValueSatisfied } from
'../../utils/dependency-value.utils';
/**
* Get the current value of a property from a step.
@@ -87,14 +88,7 @@ export function isStepDependencySatisfied(
// Get the current property value
const value = getPropertyValue(stepName, propertyName, stepConfigurations,
unsavedStepValues);
- // Evaluate based on dependentValues
- if (!dependentValues || dependentValues.length === 0) {
- // No specific values required - any non-empty value satisfies
- return value !== null && value !== undefined && value !== '';
- } else {
- // Value must be in the allowed list
- return value != null && dependentValues.includes(String(value));
- }
+ return isDependencyValueSatisfied(value, dependentValues);
}
/**
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.spec.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.spec.ts
index b0b39c907eb..da84c658280 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.spec.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.spec.ts
@@ -146,6 +146,18 @@ describe('PropertyGroupCard', () => {
expect(component.hasValue('Host')).toBe(false);
});
+ it('should return true when no value reference exists but the
descriptor has a default', async () => {
+ const { component } = await setup({
+ propertyGroup: makeGroup({
+ propertyDescriptors: {
+ Enabled: { name: 'Enabled', type: 'BOOLEAN', required:
false, defaultValue: 'false' }
+ },
+ propertyValues: {}
+ })
+ });
+ expect(component.hasValue('Enabled')).toBe(true);
+ });
+
it('should return false for STRING_LITERAL with null value', async ()
=> {
const { component } = await setup({
propertyGroup: makeGroup({
@@ -168,40 +180,66 @@ describe('PropertyGroupCard', () => {
expect(component.hasValue('Host')).toBe(false);
});
- it('should return true for SECRET_REFERENCE regardless of value',
async () => {
+ it('should return true for a SECRET_REFERENCE with a non-empty
composite key', async () => {
const { component } = await setup({
propertyGroup: makeGroup({
+ propertyDescriptors: {
+ Password: { name: 'Password', type: 'SECRET',
required: true }
+ },
propertyValues: {
- Host: { valueType: 'SECRET_REFERENCE' }
+ Password: {
+ valueType: 'SECRET_REFERENCE',
+ secretProviderId: 'provider-1',
+ secretProviderName: 'Vault',
+ fullyQualifiedSecretName: 'group.my-secret'
+ }
}
})
});
- expect(component.hasValue('Host')).toBe(true);
+ expect(component.hasValue('Password')).toBe(true);
+ });
+
+ it('should return false for a SECRET with only a descriptor default
and no saved reference', async () => {
+ const { component } = await setup({
+ propertyGroup: makeGroup({
+ propertyDescriptors: {
+ Password: { name: 'Password', type: 'SECRET',
required: true, defaultValue: 'changeme' }
+ },
+ propertyValues: {}
+ })
+ });
+ expect(component.hasValue('Password')).toBe(false);
});
- it('should return true for ASSET_REFERENCE with entries', async () => {
+ it('should return true for an ASSET with assetReferences', async () =>
{
const { component } = await setup({
propertyGroup: makeGroup({
+ propertyDescriptors: {
+ Cert: { name: 'Cert', type: 'ASSET', required: false }
+ },
propertyValues: {
- Host: {
+ Cert: {
valueType: 'ASSET_REFERENCE',
assetReferences: [{ id: 'asset-1', name:
'cert.pem' }]
}
}
})
});
- expect(component.hasValue('Host')).toBe(true);
+ expect(component.hasValue('Cert')).toBe(true);
});
- it('should return false for ASSET_REFERENCE with empty array', async
() => {
+ it('should return false for an ASSET with empty assetReferences',
async () => {
const { component } = await setup({
propertyGroup: makeGroup({
+ propertyDescriptors: {
+ Cert: { name: 'Cert', type: 'ASSET', required: false }
+ },
propertyValues: {
- Host: { valueType: 'ASSET_REFERENCE', assetReferences:
[] }
+ Cert: { valueType: 'ASSET_REFERENCE', assetReferences:
[] }
}
})
});
- expect(component.hasValue('Host')).toBe(false);
+ expect(component.hasValue('Cert')).toBe(false);
});
});
@@ -214,19 +252,42 @@ describe('PropertyGroupCard', () => {
it('should return masked text for SECRET_REFERENCE', async () => {
const { component } = await setup({
propertyGroup: makeGroup({
+ propertyDescriptors: {
+ Password: { name: 'Password', type: 'SECRET',
required: true }
+ },
propertyValues: {
- Host: { valueType: 'SECRET_REFERENCE' }
+ Password: {
+ valueType: 'SECRET_REFERENCE',
+ secretProviderId: 'provider-1',
+ secretProviderName: 'Vault',
+ fullyQualifiedSecretName: 'group.my-secret'
+ }
}
})
});
-
expect(component.getDisplayValueForProperty('Host')).toBe('••••••••');
+
expect(component.getDisplayValueForProperty('Password')).toBe('••••••••');
+ });
+
+ it('should not leak a SECRET descriptor default as visible text',
async () => {
+ const { component } = await setup({
+ propertyGroup: makeGroup({
+ propertyDescriptors: {
+ Password: { name: 'Password', type: 'SECRET',
required: true, defaultValue: 'changeme' }
+ },
+ propertyValues: {}
+ })
+ });
+
expect(component.getDisplayValueForProperty('Password')).toBe('••••••••');
});
it('should return comma-separated asset names for ASSET_REFERENCE',
async () => {
const { component } = await setup({
propertyGroup: makeGroup({
+ propertyDescriptors: {
+ Cert: { name: 'Cert', type: 'ASSET', required: false }
+ },
propertyValues: {
- Host: {
+ Cert: {
valueType: 'ASSET_REFERENCE',
assetReferences: [
{ id: 'a1', name: 'cert.pem' },
@@ -236,21 +297,24 @@ describe('PropertyGroupCard', () => {
}
})
});
-
expect(component.getDisplayValueForProperty('Host')).toBe('cert.pem, key.pem');
+
expect(component.getDisplayValueForProperty('Cert')).toBe('cert.pem, key.pem');
});
it('should fall back to asset id when name is missing', async () => {
const { component } = await setup({
propertyGroup: makeGroup({
+ propertyDescriptors: {
+ Cert: { name: 'Cert', type: 'ASSET', required: false }
+ },
propertyValues: {
- Host: {
+ Cert: {
valueType: 'ASSET_REFERENCE',
assetReferences: [{ id: 'a1' }]
}
}
})
});
- expect(component.getDisplayValueForProperty('Host')).toBe('a1');
+ expect(component.getDisplayValueForProperty('Cert')).toBe('a1');
});
it('should return empty string when no value reference exists', async
() => {
@@ -259,6 +323,18 @@ describe('PropertyGroupCard', () => {
});
expect(component.getDisplayValueForProperty('Host')).toBe('');
});
+
+ it('should return the descriptor default when no value reference
exists', async () => {
+ const { component } = await setup({
+ propertyGroup: makeGroup({
+ propertyDescriptors: {
+ Enabled: { name: 'Enabled', type: 'BOOLEAN', required:
false, defaultValue: 'false' }
+ },
+ propertyValues: {}
+ })
+ });
+
expect(component.getDisplayValueForProperty('Enabled')).toBe('false');
+ });
});
describe('fieldErrors computed signal', () => {
@@ -348,6 +424,19 @@ describe('PropertyGroupCard', () => {
expect(unsetLabels[0].nativeElement.textContent.trim()).toBe('No
value set');
});
+ it('should display descriptor defaults when property values are
absent', async () => {
+ const { query } = await setup({
+ propertyGroup: makeGroup({
+ propertyDescriptors: {
+ Enabled: { name: 'Enabled', type: 'BOOLEAN', required:
false, defaultValue: 'false' }
+ },
+ propertyValues: {}
+ })
+ });
+ const value =
query('mat-card-content').query(By.css('.tertiary-color'));
+ expect(value.nativeElement.textContent.trim()).toBe('false');
+ });
+
it('should display the property value for properties with values',
async () => {
const { query } = await setup();
const content = query('mat-card-content');
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.ts
index 188d73ba8be..bd7b093dda1 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/property-group-card/property-group-card.component.ts
@@ -25,6 +25,7 @@ import {
ConnectorPropertyDescriptor,
PropertyGroupConfiguration
} from '../../types';
+import { hasPropertyValue } from '../../utils/connector-validation.utils';
/**
* Read-only display card for a property group's configured values.
@@ -74,22 +75,21 @@ export class PropertyGroupCard {
hasValue(propertyName: string): boolean {
const valueRef = this.propertyGroup().propertyValues?.[propertyName];
- if (!valueRef) return false;
- if (valueRef.valueType === 'SECRET_REFERENCE') return true;
- if (valueRef.valueType === 'ASSET_REFERENCE') {
- return (valueRef.assetReferences?.length ?? 0) > 0;
- }
- return valueRef.value !== null && valueRef.value !== undefined &&
valueRef.value !== '';
+ const descriptor = this.getDescriptor(propertyName);
+ return hasPropertyValue(valueRef, descriptor?.type ?? 'STRING',
descriptor?.defaultValue);
}
getDisplayValueForProperty(propertyName: string): string {
const valueRef = this.propertyGroup().propertyValues?.[propertyName];
- if (!valueRef) return '';
- if (valueRef.valueType === 'SECRET_REFERENCE') return '••••••••';
- if (valueRef.valueType === 'ASSET_REFERENCE') {
+ const descriptor = this.getDescriptor(propertyName);
+
+ // Mask by type, not just a saved SECRET_REFERENCE, so a SECRET
defaultValue cannot leak.
+ if (descriptor?.type === 'SECRET' || valueRef?.valueType ===
'SECRET_REFERENCE') return '••••••••';
+ if (valueRef?.valueType === 'ASSET_REFERENCE') {
const refs = valueRef.assetReferences;
return refs?.map((r: AssetReference) => r.name || r.id).join(', ')
|| '';
}
- return valueRef.value ?? '';
+
+ return valueRef?.value ?? descriptor?.defaultValue ?? '';
}
}
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/services/value-reference.helper.spec.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/services/value-reference.helper.spec.ts
new file mode 100644
index 00000000000..df5894ea5a6
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/services/value-reference.helper.spec.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { fromValueReference, toBooleanValue } from './value-reference.helper';
+
+describe('Value Reference Helper', () => {
+ describe('toBooleanValue', () => {
+ it('coerces true values case-insensitively to match
Boolean.parseBoolean', () => {
+ expect(toBooleanValue(true)).toBe(true);
+ expect(toBooleanValue('true')).toBe(true);
+ expect(toBooleanValue('True')).toBe(true);
+ expect(toBooleanValue('TRUE')).toBe(true);
+ });
+
+ it('coerces all other values to false', () => {
+ expect(toBooleanValue(false)).toBe(false);
+ expect(toBooleanValue('false')).toBe(false);
+ expect(toBooleanValue('anything')).toBe(false);
+ expect(toBooleanValue(null)).toBe(false);
+ expect(toBooleanValue(undefined)).toBe(false);
+ });
+ });
+
+ describe('fromValueReference', () => {
+ it('normalizes BOOLEAN string values case-insensitively', () => {
+ expect(fromValueReference({ value: 'TRUE', valueType:
'STRING_LITERAL' }, 'BOOLEAN')).toBe(true);
+ expect(fromValueReference({ value: 'false', valueType:
'STRING_LITERAL' }, 'BOOLEAN')).toBe(false);
+ });
+
+ it('preserves a null BOOLEAN value so callers can use the descriptor
default', () => {
+ expect(fromValueReference({ value: null, valueType:
'STRING_LITERAL' }, 'BOOLEAN')).toBeNull();
+ });
+ });
+});
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/services/value-reference.helper.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/services/value-reference.helper.ts
index e0450edca68..5b677403167 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/services/value-reference.helper.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/services/value-reference.helper.ts
@@ -37,6 +37,20 @@ export interface SecretReferenceOptions {
fullyQualifiedSecretName: string;
}
+/**
+ * Coerces a BOOLEAN property value into a real boolean.
+ *
+ * BOOLEAN values cross the wire as strings (both
+ * `ConnectorValueReferenceDTO.value` and
`ConnectorPropertyDescriptorDTO.defaultValue`
+ * are declared as String server-side). Comparison is case-insensitive to match
+ * `Boolean.parseBoolean`, which the connector framework uses when reading
BOOLEAN values.
+ * `MatSlideToggle.writeValue` coerces with a bare `!!value`, so the string
`"false"` is truthy
+ * and would otherwise render the toggle as checked.
+ */
+export function toBooleanValue(value: unknown): boolean {
+ return value === true || (typeof value === 'string' && value.toLowerCase()
=== 'true');
+}
+
/**
* Creates a ConnectorValueReference from a primitive form value.
* Supports STRING_LITERAL, ASSET_REFERENCE, and SECRET_REFERENCE value types.
@@ -127,7 +141,7 @@ export function toValueReference(
* for use with multi-select form controls.
*
* @param valueRef The ConnectorValueReference from the API, or a plain
primitive value
- * @param propertyType Optional property type - when 'STRING_LIST', splits
comma-separated values into array
+ * @param propertyType Optional property type used to normalize BOOLEAN,
STRING_LIST, and ASSET values
* @returns The primitive value suitable for display
*/
export function fromValueReference(
@@ -196,6 +210,13 @@ export function fromValueReference(
return [];
}
+ // For BOOLEAN, coerce the wire string ("true"/"false") into a real
boolean so the
+ // toggle renders the stored value rather than the truthiness of a
non-empty string.
+ // null/undefined is preserved so callers can still fall back to the
descriptor default.
+ if (propertyType === 'BOOLEAN' && rawValue !== null && rawValue !==
undefined) {
+ return toBooleanValue(rawValue);
+ }
+
// For STRING_LIST, split comma-separated string into array for
multi-select
if (propertyType === 'STRING_LIST' && typeof rawValue === 'string') {
return rawValue
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/utils/connector-validation.utils.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/utils/connector-validation.utils.ts
index 04ee9bccfa6..15895d54f7b 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/utils/connector-validation.utils.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/utils/connector-validation.utils.ts
@@ -23,6 +23,7 @@ import {
PropertyType,
buildSecretKey
} from '../types';
+import { isDependencyValueSatisfied } from './dependency-value.utils';
/**
* Check if a property has a meaningful value set.
@@ -139,10 +140,7 @@ function evaluatePropertyVisibility(
const dependentValue = findPropertyValue(dependency.propertyName,
propertyGroups);
- if (dependency.dependentValues && dependency.dependentValues.length >
0) {
- return dependentValue !== null &&
dependency.dependentValues.includes(dependentValue);
- }
- return dependentValue !== null && dependentValue !== '';
+ return isDependencyValueSatisfied(dependentValue,
dependency.dependentValues);
});
}
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/utils/dependency-value.utils.spec.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/utils/dependency-value.utils.spec.ts
new file mode 100644
index 00000000000..e24b9d04018
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/utils/dependency-value.utils.spec.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { isDependencyValueSatisfied } from './dependency-value.utils';
+
+describe('Dependency Value Utils', () => {
+ it('matches native booleans against string dependent values', () => {
+ expect(isDependencyValueSatisfied(true, ['true'])).toBe(true);
+ expect(isDependencyValueSatisfied(false, ['true'])).toBe(false);
+ expect(isDependencyValueSatisfied(false, ['false'])).toBe(true);
+ });
+
+ it('matches string values case-sensitively', () => {
+ expect(isDependencyValueSatisfied('true', ['true'])).toBe(true);
+ expect(isDependencyValueSatisfied('TRUE', ['true'])).toBe(false);
+ expect(isDependencyValueSatisfied('value2', ['value1',
'value2'])).toBe(true);
+ });
+
+ it('treats any non-empty value as satisfying an empty dependent values
list', () => {
+ expect(isDependencyValueSatisfied('anything', [])).toBe(true);
+ expect(isDependencyValueSatisfied(false, undefined)).toBe(true);
+ expect(isDependencyValueSatisfied('', [])).toBe(false);
+ expect(isDependencyValueSatisfied(null, [])).toBe(false);
+ expect(isDependencyValueSatisfied(undefined, [])).toBe(false);
+ });
+
+ it('does not match null or undefined against a dependent values list', ()
=> {
+ expect(isDependencyValueSatisfied(null, ['true'])).toBe(false);
+ expect(isDependencyValueSatisfied(undefined,
['undefined'])).toBe(false);
+ });
+});
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/utils/dependency-value.utils.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/utils/dependency-value.utils.ts
new file mode 100644
index 00000000000..f7fcee4fe33
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/utils/dependency-value.utils.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { ConnectorPropertyFormValue } from '../types';
+
+/**
+ * Evaluates whether a dependency condition is met by a property's current
value.
+ *
+ * The API represents dependent values as strings, while live form values can
be native
+ * booleans. Dependency matching remains case-sensitive to match backend
dependency evaluators.
+ *
+ * @param value The dependent property's current value
+ * @param dependentValues Values that satisfy the dependency; empty means any
non-empty value
+ * @returns whether the dependency condition is met
+ */
+export function isDependencyValueSatisfied(
+ value: ConnectorPropertyFormValue | undefined,
+ dependentValues: string[] | undefined | null
+): boolean {
+ if (!dependentValues || dependentValues.length === 0) {
+ return value !== null && value !== undefined && value !== '';
+ }
+
+ return value !== null && value !== undefined &&
dependentValues.includes(String(value));
+}