This is an automated email from the ASF dual-hosted git repository.
scottyaslan 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 bd91691bf46 NIFI-15908: Disabling the Apply button in the Connector
wizard when t… (#11222)
bd91691bf46 is described below
commit bd91691bf463f59f73d89846e2a03f1e61e606a6
Author: Matt Gilman <[email protected]>
AuthorDate: Tue May 12 14:24:21 2026 -0400
NIFI-15908: Disabling the Apply button in the Connector wizard when t…
(#11222)
* NIFI-15908: Disabling the Apply button in the Connector wizard when there
are no pending edits.
* NIFI-15908: Addressing review feedback.
---
.../libs/shared/src/assets/styles/_app.scss | 6 +++
...ector-configuration-summary-step.component.html | 1 +
...or-configuration-summary-step.component.spec.ts | 42 ++++++++++++++++
...nnector-configuration-summary-step.component.ts | 31 +++++++++++-
.../connector-wizard.component.html | 2 +
.../connector-wizard.component.spec.ts | 2 +
.../connector-wizard.store.spec.ts | 57 ++++++++++++++++++++++
.../connector-wizard/connector-wizard.store.ts | 2 +
.../with-connector-wizard.feature.ts | 24 ++++++++-
9 files changed, 165 insertions(+), 2 deletions(-)
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/assets/styles/_app.scss
b/nifi-frontend/src/main/frontend/libs/shared/src/assets/styles/_app.scss
index 5c91e1ee9d4..c74845bf33d 100644
--- a/nifi-frontend/src/main/frontend/libs/shared/src/assets/styles/_app.scss
+++ b/nifi-frontend/src/main/frontend/libs/shared/src/assets/styles/_app.scss
@@ -793,4 +793,10 @@
)
);
}
+
+ // buttons
+ .mdc-button:disabled {
+ pointer-events: auto;
+ cursor: not-allowed;
+ }
}
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-configuration-summary-step/connector-configuration-summary-step.component.html
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-configuration-summary-step/connector-configuration-summary-step.component.html
index 021564da348..f820085d978 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-configuration-summary-step/connector-configuration-summary-step.component.html
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-configuration-summary-step/connector-configuration-summary-step.component.html
@@ -185,6 +185,7 @@
[disabled]="applyDisabled()"
[matTooltipDisabled]="applyTooltip() === ''"
[matTooltip]="applyTooltip()"
+ [matTooltipShowDelay]="500"
matTooltipPosition="left"
(click)="onConfirm()"
data-qa="apply-button"
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-configuration-summary-step/connector-configuration-summary-step.component.spec.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-configuration-summary-step/connector-configuration-summary-step.component.spec.ts
index a2c0b6bbab4..c044cbb63ef 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-configuration-summary-step/connector-configuration-summary-step.component.spec.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-configuration-summary-step/connector-configuration-summary-step.component.spec.ts
@@ -1045,6 +1045,48 @@ describe('ConnectorConfigurationSummaryStep', () => {
const applyButton =
fixture.debugElement.query(By.css('[data-qa="apply-button"]'));
expect(applyButton.nativeElement.disabled).toBe(true);
});
+
+ it('should disable apply when applyAllowed is false even with
verificationPassed=true', () => {
+ fixture.componentRef.setInput('verificationPassed', true);
+ fixture.componentRef.setInput('applyAllowed', false);
+ fixture.componentRef.setInput('applyDisabledReason', 'No
pending changes');
+ fixture.detectChanges();
+
+ const applyButton =
fixture.debugElement.query(By.css('[data-qa="apply-button"]'));
+ expect(applyButton.nativeElement.disabled).toBe(true);
+ });
+
+ it('should surface the applyDisabledReason as the apply tooltip
when not allowed', () => {
+ fixture.componentRef.setInput('verificationPassed', true);
+ fixture.componentRef.setInput('applyAllowed', false);
+ fixture.componentRef.setInput('applyDisabledReason', 'No
pending changes');
+ fixture.detectChanges();
+
+ expect(component.applyTooltip()).toBe('No pending changes');
+ });
+
+ it('should keep the verify-required tooltip when apply is allowed
but verification has not passed', () => {
+ fixture.componentRef.setInput('verificationPassed', null);
+ fixture.componentRef.setInput('applyAllowed', true);
+ fixture.componentRef.setInput('applyDisabledReason', '');
+ fixture.detectChanges();
+
+ expect(component.applyTooltip()).toBe('Run verification before
applying');
+ });
+
+ it('should fall through to the verify-required tooltip when
applyAllowed=false but no reason is provided', () => {
+ fixture.componentRef.setInput('verificationPassed', null);
+ fixture.componentRef.setInput('applyAllowed', false);
+ fixture.componentRef.setInput('applyDisabledReason', '');
+ fixture.detectChanges();
+
+ expect(component.applyTooltip()).toBe('Run verification before
applying');
+ });
+
+ it('should default applyAllowed to true so existing callers are
unaffected', () => {
+ expect(component.applyAllowed()).toBe(true);
+ expect(component.applyDisabledReason()).toBe('');
+ });
});
describe('Step verification status icons', () => {
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-configuration-summary-step/connector-configuration-summary-step.component.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-configuration-summary-step/connector-configuration-summary-step.component.ts
index b8b61c019bf..eb89c778d7d 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-configuration-summary-step/connector-configuration-summary-step.component.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-configuration-summary-step/connector-configuration-summary-step.component.ts
@@ -58,6 +58,18 @@ export class ConnectorConfigurationSummaryStep {
stepVerificationResults = input<{ [stepName: string]:
ConfigVerificationResult[] }>({});
stepNameMapping = input<{ [displayName: string]: string[] }>({});
verifyAllError = input<string | null>(null);
+ /**
+ * Whether the backend permits applying updates. Defaults to true so
callers that
+ * have not yet adopted the input continue to behave as before. When false
(e.g. the
+ * connector reports `APPLY_UPDATES` as not allowed because there are no
pending
+ * changes or the connector is mid-update), the Apply button is disabled.
+ */
+ applyAllowed = input(true);
+ /**
+ * Backend-supplied reason describing why apply is not allowed. Surfaced
verbatim as
+ * the Apply button tooltip when {@link applyAllowed} is false. May be
empty.
+ */
+ applyDisabledReason = input('');
// Signal outputs
confirm = output<void>();
@@ -66,10 +78,27 @@ export class ConnectorConfigurationSummaryStep {
verify = output<void>();
applyDisabled = computed(() => {
- return this.loading() || this.applying() || this.verifying() ||
this.verificationPassed() !== true;
+ return (
+ this.loading() ||
+ this.applying() ||
+ this.verifying() ||
+ this.verificationPassed() !== true ||
+ !this.applyAllowed()
+ );
});
+ /**
+ * The Apply button's tooltip. Returns one of:
+ * - the backend-supplied {@link applyDisabledReason} verbatim when apply
is not
+ * allowed; takes precedence so the user sees the authoritative reason
+ * (e.g. "No pending changes", "Connector is updating");
+ * - the verify-required hint when verification has not succeeded;
+ * - empty string otherwise.
+ */
applyTooltip = computed(() => {
+ if (!this.applyAllowed() && this.applyDisabledReason()) {
+ return this.applyDisabledReason();
+ }
if (this.verificationPassed() !== true) {
return 'Run verification before applying';
}
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.component.html
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.component.html
index 82738740164..5fc28f4016b 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.component.html
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.component.html
@@ -81,6 +81,8 @@
[stepVerificationResults]="wizardStore.stepVerificationResults()"
[verifyAllError]="wizardStore.verifyAllError()"
[stepNameMapping]="stepNameMapping()"
+
[applyAllowed]="wizardStore.applyUpdatesAllowed()"
+
[applyDisabledReason]="wizardStore.applyUpdatesDisabledReason()"
(confirm)="onApplyConfiguration()"
(dismiss)="navigateBack.emit()"
(previous)="onPreviousStep()"
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.component.spec.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.component.spec.ts
index 33afe98aed9..3e30ce55635 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.component.spec.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.component.spec.ts
@@ -53,6 +53,8 @@ function createMockStore(state: MockStoreState = {}) {
currentVerifyingStepName: signal<string | null>(null),
stepVerificationResults: signal<Record<string, unknown>>({}),
verifyAllError: signal<string | null>(null),
+ applyUpdatesAllowed: signal<boolean>(false),
+ applyUpdatesDisabledReason: signal<string>(''),
bannerErrors: signal<string[]>([]),
initializeWithConnector: vi.fn(),
loadSecrets: vi.fn(),
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.store.spec.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.store.spec.ts
index cfc8d788fd8..3bc2dfba6a1 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.store.spec.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.store.spec.ts
@@ -263,6 +263,63 @@ describe('StandardConnectorWizardStore', () => {
});
});
+ // ═══════════════════════════════════════════════════════
+ // applyUpdatesAllowed / applyUpdatesDisabledReason
+ // ═══════════════════════════════════════════════════════
+
+ describe('applyUpdatesAllowed / applyUpdatesDisabledReason', () => {
+ it('returns false with empty reason when no connector is set', () => {
+ const { store } = setup();
+ expect(store.applyUpdatesAllowed()).toBe(false);
+ expect(store.applyUpdatesDisabledReason()).toBe('');
+ });
+
+ it('returns false with empty reason when APPLY_UPDATES action is
missing', () => {
+ const { store } = setup();
+ store.initializeWithConnector(makeConnector());
+ expect(store.applyUpdatesAllowed()).toBe(false);
+ expect(store.applyUpdatesDisabledReason()).toBe('');
+ });
+
+ it('reflects allowed=true when backend permits APPLY_UPDATES', () => {
+ const { store } = setup();
+ const connector = makeConnector();
+ connector.component.availableActions = [
+ { name: 'APPLY_UPDATES', description: 'Apply updates',
allowed: true }
+ ];
+ store.initializeWithConnector(connector);
+ expect(store.applyUpdatesAllowed()).toBe(true);
+ expect(store.applyUpdatesDisabledReason()).toBe('');
+ });
+
+ it('surfaces the backend reason when APPLY_UPDATES is not allowed', ()
=> {
+ const { store } = setup();
+ const connector = makeConnector();
+ connector.component.availableActions = [
+ {
+ name: 'APPLY_UPDATES',
+ description: 'Apply updates',
+ allowed: false,
+ reasonNotAllowed: 'No pending changes'
+ }
+ ];
+ store.initializeWithConnector(connector);
+ expect(store.applyUpdatesAllowed()).toBe(false);
+ expect(store.applyUpdatesDisabledReason()).toBe('No pending
changes');
+ });
+
+ it('returns empty reason when APPLY_UPDATES is not allowed but no
reason is provided', () => {
+ const { store } = setup();
+ const connector = makeConnector();
+ connector.component.availableActions = [
+ { name: 'APPLY_UPDATES', description: 'Apply updates',
allowed: false }
+ ];
+ store.initializeWithConnector(connector);
+ expect(store.applyUpdatesAllowed()).toBe(false);
+ expect(store.applyUpdatesDisabledReason()).toBe('');
+ });
+ });
+
// ═══════════════════════════════════════════════════════
// Per-step signal factories
// ═══════════════════════════════════════════════════════
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.store.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.store.ts
index 0873276932e..79ec58f01fc 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.store.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.store.ts
@@ -69,6 +69,8 @@ export abstract class ConnectorWizardStore {
abstract readonly verificationPassed: Signal<boolean | null>;
abstract readonly currentVerifyingStepName: Signal<string | null>;
abstract readonly verifyAllError: Signal<string | null>;
+ abstract readonly applyUpdatesAllowed: Signal<boolean>;
+ abstract readonly applyUpdatesDisabledReason: Signal<string>;
// --------------- Per-step signal factories ---------------
abstract stepConfiguration(stepName: string):
Signal<ConfigurationStepConfiguration | null>;
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/with-connector-wizard.feature.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/with-connector-wizard.feature.ts
index 90883231f19..6cae43e97da 100644
---
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/with-connector-wizard.feature.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/with-connector-wizard.feature.ts
@@ -68,6 +68,7 @@ import {
initialConnectorWizardState
} from './connector-wizard.types';
import { getVisibleStepNames } from './step-dependency.utils';
+import { getConnectorAction } from '../../utils/connector-permissions.utils';
/**
* Reusable SignalStore feature encapsulating all shared connector wizard
logic.
@@ -90,7 +91,28 @@ export function withConnectorWizard() {
allStepsVerifying: computed(() =>
allStepsVerification().verifying),
verificationPassed: computed(() =>
allStepsVerification().passed),
currentVerifyingStepName: computed(() =>
allStepsVerification().currentStepName),
- verifyAllError: computed(() => allStepsVerification().error)
+ verifyAllError: computed(() => allStepsVerification().error),
+ /**
+ * Whether the backend currently permits applying the
connector's working
+ * configuration. Derived from the `APPLY_UPDATES` action on
the connector
+ * entity. The backend reports `allowed: false` with reason
+ * `"No pending changes"` when there is nothing to apply, and
with reason
+ * `"Connector is updating"` while a previous apply is still
in progress.
+ */
+ applyUpdatesAllowed: computed(() => {
+ const c = connector();
+ if (!c) return false;
+ return getConnectorAction(c, 'APPLY_UPDATES')?.allowed ??
false;
+ }),
+ /**
+ * Backend-supplied reason describing why apply is not
allowed. Empty when
+ * apply is allowed or the action is missing entirely.
+ */
+ applyUpdatesDisabledReason: computed(() => {
+ const c = connector();
+ if (!c) return '';
+ return getConnectorAction(c,
'APPLY_UPDATES')?.reasonNotAllowed ?? '';
+ })
})
),