Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -793,4 +793,10 @@
)
);
}

// buttons
.mdc-button:disabled {
pointer-events: auto;
cursor: not-allowed;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@
[disabled]="applyDisabled()"
[matTooltipDisabled]="applyTooltip() === ''"
[matTooltip]="applyTooltip()"
[matTooltipShowDelay]="500"
matTooltipPosition="left"
(click)="onConfirm()"
data-qa="apply-button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
Expand All @@ -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';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ═══════════════════════════════════════════════════════
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 ?? '';
})
})
),

Expand Down
Loading