diff --git a/frontend/src/__tests__/settings-accounts.test.ts b/frontend/src/__tests__/settings-accounts.test.ts index 5fa990a68..f32f79275 100644 --- a/frontend/src/__tests__/settings-accounts.test.ts +++ b/frontend/src/__tests__/settings-accounts.test.ts @@ -650,14 +650,22 @@ describe('Overrides panel — AWS payment selector', () => { expect(api.saveAccountServiceOverride).not.toHaveBeenCalled(); }); - test('non-AWS rows render the existing read-only payment cell, no for the overrides panel. Issue #23. @@ -629,15 +645,16 @@ function buildPaymentOverrideSelect( select.appendChild(inheritOpt); // Filter the payment dropdown to only the (term, payment) combinations - // AWS actually supports for THIS service. e.g. RDS rejects 3yr no-upfront, - // so an RDS override with term=3 must not list no-upfront. Per #107. - // Falls back to the full AWS list when term is missing (override row was - // saved without a term) — the global default's term will resolve at + // the provider/service actually supports. e.g. RDS rejects 3yr no-upfront; + // Azure has upfront/monthly only; GCP has monthly only. Per #107. + // Falls back to the full provider payment list when term is missing (override + // row was saved without a term) — the global default's term will resolve at // recommendation time, and we can't pre-validate without it. const overrideTerm = override.term ?? 0; + const providerPayments = getCommitmentConfig(override.provider).payments; const validPayments = overrideTerm > 0 ? getValidPaymentOptions(override.provider, override.service, overrideTerm) - : AWS_PAYMENT_OPTIONS.map(p => ({ value: p.value, label: p.label })); + : providerPayments.map(p => ({ value: p.value, label: p.label })); for (const { value, label } of validPayments) { const opt = document.createElement('option'); @@ -705,9 +722,6 @@ async function handlePaymentOverrideChange( message: `Payment override updated for ${override.provider}/${override.service}.`, kind: 'success', }); - // The inline payment selector only renders for AWS rows (per the - // o.provider === 'aws' guard in loadOverridesPanel's row loop), so - // override.provider is always 'aws' at this call site — cast is safe. await loadOverridesPanel(accountId, panel, override.provider as AccountProvider); await refreshRecommendationsAfterOverrideChange(); } catch (err) { @@ -969,12 +983,6 @@ function buildEnabledOverrideCheckbox( `Enable ${override.provider}/${override.service} override`, ); - if (override.provider !== 'aws') { - input.disabled = true; - input.title = 'Per-provider override editing is AWS-only — Azure/GCP coming with the provider-aware modal'; - return input; - } - input.addEventListener('change', () => { void handleEnabledOverrideChange(accountId, override, input, panel, row); }); @@ -1031,9 +1039,10 @@ async function handleEnabledOverrideChange( * Populated state: render the existing table + an "Add override" button at * the top so users can add another override for a different service. * - * The Add Override flow is AWS-only for now; Azure/GCP keep the read-only - * empty-state text since their per-product term/payment semantics differ - * (issue #104 follow-up tracks Azure/GCP modal support). + * Issue #109: Azure and GCP accounts now also get the "Add override" button + * and full inline editing (term, payment, coverage, enabled) on existing rows. + * Provider-specific payment options are derived via getCommitmentConfig so + * GCP shows "Monthly only" and Azure shows "Upfront / Monthly". */ /** * Open the per-account overrides modal (issue #122). Replaces the inline @@ -1080,14 +1089,13 @@ export async function loadOverridesPanel(accountId: string, panel: HTMLElement, const overrides = await api.listAccountServiceOverrides(accountId); panel.textContent = ''; - const canCreate = provider === 'aws'; + // Issue #109: all providers can now create overrides. + const canCreate = true; if (!overrides || overrides.length === 0) { const msg = document.createElement('p'); msg.className = 'help-text'; - msg.textContent = canCreate - ? 'No service overrides yet for this account.' - : 'No service overrides set. All services use global defaults.'; + msg.textContent = 'No service overrides yet for this account.'; panel.appendChild(msg); if (canCreate) { // No overrides yet \u2014 auto-open the modal so the user lands directly @@ -1143,37 +1151,24 @@ export async function loadOverridesPanel(accountId: string, panel: HTMLElement, const serviceTd = tr.insertCell(); serviceTd.textContent = `${o.provider}/${o.service}`; - // Term cell: editable for all providers (issue #109 extends + // the AWS-only #110 inline editing to Azure/GCP). const termTd = tr.insertCell(); - if (o.provider === 'aws') { - termTd.appendChild(buildTermOverrideSelect(accountId, o, panel)); - } else { - termTd.textContent = o.term !== undefined ? `${o.term}yr` : '\u2014'; - } + termTd.appendChild(buildTermOverrideSelect(accountId, o, panel)); - // Payment cell: editable for all providers. GCP's payment + // dropdown only shows "Monthly"; Azure shows upfront/monthly. The + // underlying getValidPaymentOptions already handles per-provider + // filtering, so the same builder works for all three clouds. const paymentTd = tr.insertCell(); - if (o.provider === 'aws') { - paymentTd.appendChild(buildPaymentOverrideSelect(accountId, o, panel)); - } else { - paymentTd.textContent = o.payment ?? '\u2014'; - } + paymentTd.appendChild(buildPaymentOverrideSelect(accountId, o, panel)); - // Coverage cell: editable numeric for AWS (issue #110); - // read-only text for Azure/GCP. + // Coverage cell: editable numeric for all providers (issue #109). const coverageTd = tr.insertCell(); - if (o.provider === 'aws') { - coverageTd.appendChild(buildCoverageOverrideInput(accountId, o, panel)); - } else { - coverageTd.textContent = o.coverage !== undefined ? `${o.coverage}%` : '\u2014'; - } + coverageTd.appendChild(buildCoverageOverrideInput(accountId, o, panel)); // Enabled cell: per-row checkbox toggling account_service_overrides.enabled. - // AWS-editable, disabled (with tooltip) for non-AWS to keep the column - // alignment consistent across provider types. Issue #110. + // Editable for all providers. Issue `#110`, extended to Azure/GCP in `#109`. const enabledTd = tr.insertCell(); enabledTd.appendChild(buildEnabledOverrideCheckbox(accountId, o, panel, tr)); @@ -1311,9 +1306,9 @@ export function openOverrideModal( const errEl = document.getElementById('override-form-error'); if (errEl) errEl.textContent = ''; - // Populate the service dropdown, excluding services this account already - // has an override for. AWS-only for now (issue #104 follow-up tracks - // Azure/GCP). + // Populate the service dropdown with provider-specific services, excluding + // any that already have an override for this account. Issue #109 extends + // this to Azure and GCP (previously AWS-only per issue #104 follow-up). const used = new Set( existingOverrides .filter(o => o.provider === provider) @@ -1322,7 +1317,7 @@ export function openOverrideModal( const select = document.getElementById('override-service') as HTMLSelectElement | null; if (select) { select.replaceChildren(); - const available = AWS_OVERRIDE_SERVICES.filter(s => !used.has(s.value)); + const available = getOverrideServicesForProvider(provider).filter(s => !used.has(s.value)); if (available.length === 0) { const opt = document.createElement('option'); opt.value = ''; @@ -1382,11 +1377,12 @@ function syncOverridePaymentOptions(provider: string): void { paymentSel.appendChild(inheritOpt); // When service or term is unset, can't run the validity check yet — - // show the full AWS payment list so the user can choose; the + // show the full provider payment list so the user can choose; the // submit-side guard will catch any invalid combo on Save. + const provPayments = getCommitmentConfig(provider).payments; const candidates = (service && Number.isFinite(term) && term > 0) ? getValidPaymentOptions(provider, service, term).map(p => ({ value: p.value, label: p.label })) - : AWS_PAYMENT_OPTIONS.map(p => ({ value: p.value, label: p.label })); + : provPayments.map(p => ({ value: p.value, label: p.label })); for (const { value, label } of candidates) { const opt = document.createElement('option');