mcgilman commented on code in PR #11248:
URL: https://github.com/apache/nifi/pull/11248#discussion_r3243920922
##########
nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.html:
##########
@@ -18,12 +18,15 @@
@if (prop) {
@switch (prop.type) {
@case ('BOOLEAN') {
- <mat-checkbox [formControl]="formControl"
data-qa="property-input-boolean">
- <span class="text-sm">{{ prop.name }}</span>
+ <div class="flex flex-col">
+ <div class="flex gap-x-2 items-center">
+ <span class="text-sm">{{ prop.name }}</span>
+ <mat-slide-toggle [formControl]="formControl"
data-qa="property-input-boolean"></mat-slide-toggle>
+ </div>
@if (prop.description) {
<span class="text-xs tertiary-color block">{{
prop.description }}</span>
}
- </mat-checkbox>
+ </div>
Review Comment:
**#1 — Accessibility regression: the new `<mat-slide-toggle>` is unlabeled.**
The previous `<mat-checkbox>` wrapped the property name as projected
content, which Material associates with the inner control via
`aria-labelledby`. The new `<mat-slide-toggle>` is empty — the property name
lives in a sibling `<span>`, with no `aria-label`, `aria-labelledby`, or
projected content. A screen reader will announce this toggle as unlabeled.
Other slide-toggle usages in this codebase (e.g.
`bulletin-board.component.html`) project the label inside the toggle, which is
the established convention.
Could we project the label inside the toggle? That restores the
screen-reader association the previous checkbox provided, and as a bonus the
whole label becomes a click target:
```html
<div class="flex flex-col">
<mat-slide-toggle
[formControl]="formControl"
labelPosition="before"
data-qa="property-input-boolean">
<span class="text-sm">{{ prop.name }}</span>
</mat-slide-toggle>
@if (prop.description) {
<span class="text-xs tertiary-color block">{{ prop.description
}}</span>
}
</div>
```
##########
nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.ts:
##########
@@ -339,10 +364,45 @@ export class ConnectorPropertyInput implements
ControlValueAccessor, DoCheck, On
return this.property()?.type === 'STRING_LIST' &&
this.shouldUseSelect();
}
+ /**
+ * Whether the property should be rendered as a slide toggle (BOOLEAN).
+ */
+ shouldUseToggle(): boolean {
+ return this.property()?.type === 'BOOLEAN';
+ }
Review Comment:
**#2 — `shouldUseToggle()` appears to be dead code.**
I can't find a caller for this method — the template uses `@switch
(prop.type) { @case ('BOOLEAN') }` and no test references it. Could we either
remove it, or switch the template over to use it for symmetry with the other
`shouldUseX()` predicates in the file? Whichever is preferred, having both an
unused predicate and a switch-case is a maintenance trap.
##########
nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.ts:
##########
@@ -402,15 +462,28 @@ export class ConnectorPropertyInput implements
ControlValueAccessor, DoCheck, On
}
/**
- * Compute SearchableSelectOptions from whichever source is available:
- * dynamic values when successfully fetched, otherwise static values.
+ * Compute SearchableSelectOptions from whichever source is available.
+ *
+ * - SECRET: options come from availableSecrets, valued by a composite key
+ * (providerId::providerName::fullyQualifiedName) and grouped by provider
+ * (or "Provider - Group" when a provider owns multiple groups). Saved
+ * values that no longer match an available secret are surfaced as
disabled
+ * "(no longer available)" options. When the saved value still matches
but
+ * the provider name has changed, the form value is rewritten to the new
+ * composite key after the next render.
+ * - Otherwise: dynamic values when successfully fetched, falling back to
+ * the descriptor's static allowableValues.
Review Comment:
**#8 — The expanded doc comment could also note the side-effect channel and
the untracked `formControl.value` dependency.**
The rewrite is a real improvement — much clearer than the previous
two-liner. While it now explicitly mentions the form-value rewrite, I think
it's worth also calling out:
- that **this is a side effect** scheduled from a method named "compute"
(i.e. callers shouldn't assume option-compute is pure — see related comment on
lines 539–545), and
- that `formControl.value` is read here but is **not** tracked by the
`effect()` that triggers recompute (see related comment on lines 133–135), so
options can briefly fall out of sync with the form value until one of the
tracked signals re-emits.
Both are non-obvious from the method body and tend to trip up future readers.
##########
nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.ts:
##########
@@ -426,6 +499,71 @@ export class ConnectorPropertyInput implements
ControlValueAccessor, DoCheck, On
}));
}
+ private computeSecretSelectOptions(): SearchableSelectOption<string>[] {
+ const options: SearchableSelectOption<string>[] = [];
+ const secrets: Secret[] | null = this.availableSecrets();
+
+ if (secrets && secrets.length > 0) {
+ const providerGroups = new Map<string, Set<string>>();
+ for (const s of secrets) {
+ if (!providerGroups.has(s.providerName)) {
+ providerGroups.set(s.providerName, new Set());
+ }
+ providerGroups.get(s.providerName)!.add(s.groupName);
+ }
+
+ for (const s of secrets) {
+ const groupCount = providerGroups.get(s.providerName)?.size ??
1;
+ const group = groupCount > 1 ? `${s.providerName} -
${s.groupName}` : s.providerName;
+ options.push({
+ value: buildSecretKey(s.providerId, s.providerName,
s.fullyQualifiedName),
+ label: s.name,
+ description: s.description,
+ group
+ });
+ }
+ }
+
+ const currentValue = this.formControl.value;
+ if (currentValue && secrets) {
+ const parsed = parseSecretKey(currentValue);
+ const matching = secrets.find(
+ (s) => s.providerId === parsed.providerId &&
s.fullyQualifiedName === parsed.fullyQualifiedName
+ );
+ if (matching) {
+ const expected = buildSecretKey(
+ matching.providerId,
+ matching.providerName,
+ matching.fullyQualifiedName
+ );
+ if (currentValue !== expected) {
+ // Provider name changed since saving - update form value
to match current secret.
+ // Use afterNextRender to safely modify form value after
change detection completes.
+ runInInjectionContext(this.injector, () => {
+ afterNextRender(() =>
this.formControl.setValue(expected, { emitEvent: true }));
+ });
+ }
+ } else {
+ options.push({
+ value: currentValue,
+ label: `${parsed.fullyQualifiedName} (no longer
available)`,
+ disabled: true,
+ group: parsed.providerName
+ });
+ }
+ } else if (currentValue) {
+ const parsed = parseSecretKey(currentValue);
+ options.push({
+ value: currentValue,
+ label: `${parsed.fullyQualifiedName} (no longer available)`,
+ disabled: true,
+ group: parsed.providerName
+ });
Review Comment:
**#5 — Pre-load orphan label is misleading.**
While `availableSecrets() === null` and `secretsLoading() === true`, the
saved value is rendered as `"<fqn> (no longer available)"` on a disabled
select. The disable state stops the user from acting on it, but the wording is
alarming during what is just a transient load — and if the load eventually
returns the secret, the user briefly saw it labeled as missing for no reason.
Could we only append the `(no longer available)` suffix once the load has
actually completed (`secretsLoading() === false && secretsError() === null`)?
While loading, render the FQN without the suffix. The pre-load orphan test
would need an updated expectation.
##########
nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.ts:
##########
@@ -114,6 +130,9 @@ export class ConnectorPropertyInput implements
ControlValueAccessor, DoCheck, On
effect(() => {
const prop = this.property();
this.dynamicAllowableValuesState();
+ this.availableSecrets();
+ this.secretsLoading();
+ this.secretsError();
Review Comment:
**#4 — `selectOptions` does not recompute when `formControl.value` changes.**
The effect reads `property`, `dynamicAllowableValuesState`,
`availableSecrets`, `secretsLoading`, and `secretsError`, but
`formControl.value` isn't a signal and isn't tracked here.
`computeSecretSelectOptions` reads `formControl.value` to decide whether to
append a disabled orphan entry.
Consequence: if the user opens the dropdown with an orphan saved value and
then selects a real secret, the disabled `"<fqn> (no longer available)"` entry
lingers in `selectOptions` until the parent next re-emits one of the watched
signals. The displayed selection is correct (searchable-select renders it from
the value), but the stale orphan in the list is confusing.
Could we subscribe to `formControl.valueChanges` (with
`takeUntilDestroyed(this.destroyRef)`) and recompute `selectOptions` on each
emission? Or mirror the value into a signal at write time so the existing
effect picks it up.
##########
nifi-frontend/src/main/frontend/libs/shared/src/components/connector-property-input/connector-property-input.component.ts:
##########
@@ -426,6 +499,71 @@ export class ConnectorPropertyInput implements
ControlValueAccessor, DoCheck, On
}));
}
+ private computeSecretSelectOptions(): SearchableSelectOption<string>[] {
+ const options: SearchableSelectOption<string>[] = [];
+ const secrets: Secret[] | null = this.availableSecrets();
+
+ if (secrets && secrets.length > 0) {
+ const providerGroups = new Map<string, Set<string>>();
+ for (const s of secrets) {
+ if (!providerGroups.has(s.providerName)) {
+ providerGroups.set(s.providerName, new Set());
+ }
+ providerGroups.get(s.providerName)!.add(s.groupName);
+ }
+
+ for (const s of secrets) {
+ const groupCount = providerGroups.get(s.providerName)?.size ??
1;
+ const group = groupCount > 1 ? `${s.providerName} -
${s.groupName}` : s.providerName;
+ options.push({
+ value: buildSecretKey(s.providerId, s.providerName,
s.fullyQualifiedName),
+ label: s.name,
+ description: s.description,
+ group
+ });
+ }
+ }
+
+ const currentValue = this.formControl.value;
+ if (currentValue && secrets) {
+ const parsed = parseSecretKey(currentValue);
+ const matching = secrets.find(
+ (s) => s.providerId === parsed.providerId &&
s.fullyQualifiedName === parsed.fullyQualifiedName
+ );
+ if (matching) {
+ const expected = buildSecretKey(
+ matching.providerId,
+ matching.providerName,
+ matching.fullyQualifiedName
+ );
+ if (currentValue !== expected) {
+ // Provider name changed since saving - update form value
to match current secret.
+ // Use afterNextRender to safely modify form value after
change detection completes.
+ runInInjectionContext(this.injector, () => {
+ afterNextRender(() =>
this.formControl.setValue(expected, { emitEvent: true }));
+ });
+ }
Review Comment:
**#3 — Form-value mutation as a side effect inside
`computeSecretSelectOptions()`.**
A method named `computeSecretSelectOptions` mutating the form control is
surprising and makes the function harder to reason about. There's also a subtle
risk: every effect re-run that observes `currentValue !== expected` schedules
another `afterNextRender` callback. If the effect re-runs more than once before
the first callback flushes (e.g. parent re-emits `availableSecrets` twice
quickly), multiple identical `setValue` calls will fire one after another, each
with `emitEvent: true`.
Could we lift this auto-fix into its own `effect()` (or a small
`reconcileFormValueWithSecrets()` method) so option-compute stays pure, and
guard with a single-shot flag that resets on property-name change? Something
like:
```ts
private autoFixScheduled = false;
// In the constructor, alongside the existing effect:
effect(() => {
const secrets = this.availableSecrets();
if (this.property()?.type !== 'SECRET' || !secrets) return;
if (this.autoFixScheduled) return;
// ...derive expected, compare to formControl.value, schedule once...
});
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]