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 ?? '';
+                })
             })
         ),
 

Reply via email to