diff --git a/frontend/src/__tests__/settings-accounts.test.ts b/frontend/src/__tests__/settings-accounts.test.ts index d482a4b11..f50145a01 100644 --- a/frontend/src/__tests__/settings-accounts.test.ts +++ b/frontend/src/__tests__/settings-accounts.test.ts @@ -6,7 +6,8 @@ import { loadAccountsForProvider, loadOverridesPanel, openOverrideModal, - setupSettingsHandlers + setupSettingsHandlers, + setGlobalDefaultsForTest, } from '../settings'; jest.mock('../api', () => ({ @@ -1711,3 +1712,108 @@ describe('deleteAccount — pending-executions 409 handling (issue #606)', () => expect(errorCalls.length).toBeGreaterThan(0); }); }); + +// --------------------------------------------------------------------------- +// Issue #112 — "Inherit (currently: X)" labels in the override modal +// --------------------------------------------------------------------------- + +describe('Override modal Inherit labels (issue #112)', () => { + /** Minimal DOM for openOverrideModal: just the fields the function touches. */ + function buildOverrideModalDOM(): void { + document.body.replaceChildren(); + + const modal = document.createElement('div'); + modal.id = 'override-modal'; + modal.className = 'modal hidden'; + + const form = document.createElement('form'); + form.id = 'override-form'; + + const acctIdInput = document.createElement('input'); + acctIdInput.type = 'hidden'; + acctIdInput.id = 'override-account-id'; + form.appendChild(acctIdInput); + + const provInput = document.createElement('input'); + provInput.type = 'hidden'; + provInput.id = 'override-provider'; + form.appendChild(provInput); + + const svcSel = document.createElement('select'); + svcSel.id = 'override-service'; + form.appendChild(svcSel); + + const termSel = document.createElement('select'); + termSel.id = 'override-term'; + const termInherit = document.createElement('option'); + termInherit.value = ''; + termInherit.textContent = 'Inherit (use global default)'; + termSel.appendChild(termInherit); + for (const v of ['1', '3']) { + const o = document.createElement('option'); + o.value = v; + termSel.appendChild(o); + } + form.appendChild(termSel); + + const paySel = document.createElement('select'); + paySel.id = 'override-payment'; + form.appendChild(paySel); + + const covInput = document.createElement('input'); + covInput.type = 'number'; + covInput.id = 'override-coverage'; + covInput.placeholder = 'Inherit (use global default)'; + form.appendChild(covInput); + + const errEl = document.createElement('p'); + errEl.id = 'override-form-error'; + form.appendChild(errEl); + + const submitBtn = document.createElement('button'); + submitBtn.type = 'submit'; + form.appendChild(submitBtn); + + modal.appendChild(form); + document.body.appendChild(modal); + } + + afterEach(() => { + // Reset to default so other tests are not affected. + setGlobalDefaultsForTest({ term: 3, payment: 'all-upfront', coverage: 80 }); + }); + + test('term and payment inherit options reflect the current global defaults (3yr / all-upfront)', () => { + buildOverrideModalDOM(); + setGlobalDefaultsForTest({ term: 3, payment: 'all-upfront', coverage: 80 }); + + const panel = document.createElement('div'); + openOverrideModal('acc-1', 'aws', [], panel); + + const termInherit = document.querySelector('#override-term option[value=""]'); + expect(termInherit?.textContent).toBe('Inherit (currently: 3 Years)'); + + const payInherit = document.querySelector('#override-payment option[value=""]'); + expect(payInherit?.textContent).toBe('Inherit (currently: All Upfront)'); + + const covInput = document.getElementById('override-coverage') as HTMLInputElement; + expect(covInput.placeholder).toBe('Inherit (currently: 80%)'); + }); + + test('inherit labels update when globals differ from defaults (1yr / no-upfront / 70%)', () => { + buildOverrideModalDOM(); + setGlobalDefaultsForTest({ term: 1, payment: 'no-upfront', coverage: 70 }); + + const panel = document.createElement('div'); + openOverrideModal('acc-1', 'aws', [], panel); + + const termInherit = document.querySelector('#override-term option[value=""]'); + expect(termInherit?.textContent).toBe('Inherit (currently: 1 Year)'); + + const payInherit = document.querySelector('#override-payment option[value=""]'); + expect(payInherit?.textContent).toBe('Inherit (currently: No Upfront)'); + + const covInput = document.getElementById('override-coverage') as HTMLInputElement; + expect(covInput.placeholder).toBe('Inherit (currently: 70%)'); + }); +}); diff --git a/frontend/src/settings.ts b/frontend/src/settings.ts index 8f76f8e8a..91f71a89b 100644 --- a/frontend/src/settings.ts +++ b/frontend/src/settings.ts @@ -36,6 +36,15 @@ type AccountProvider = 'aws' | 'azure' | 'gcp'; let cachedSourceCloud: string | undefined; +// Cached global defaults surfaced by loadGlobalSettings. Used by the override +// modal (issue #112) to show "Inherit (currently: X)" labels so the user can +// see what they'd be overriding without switching tabs. +let cachedGlobalDefaults: { term: number; payment: string; coverage: number } = { + term: 3, + payment: 'all-upfront', + coverage: 80, +}; + // In-flight cache for /api/config so each modal open does not refetch (issue // #130d). The promise is captured the first time getConfig() is requested // and reused for the lifetime of the page; resetConfigCache() lets tests @@ -60,6 +69,14 @@ export function resetConfigCache(): void { cachedConfigPromise = undefined; } +/** + * Override the cached global defaults used for the "Inherit (currently: X)" + * labels. Test-only; production code updates the cache via loadGlobalSettings. + */ +export function setGlobalDefaultsForTest(defaults: { term: number; payment: string; coverage: number }): void { + cachedGlobalDefaults = { ...defaults }; +} + // sessionStorage key for the in-progress (unsaved) AWS External ID. Persists // across modal close/reopen cycles within the same session so the operator // can copy the value into AWS, close the modal, and come back to the same @@ -646,7 +663,9 @@ function buildPaymentOverrideSelect( const inheritOpt = document.createElement('option'); inheritOpt.value = ''; - inheritOpt.textContent = 'Inherit (default)'; + // Issue #112: show the current global payment so the user knows what + // "Inherit" means without switching to the Purchasing tab. + inheritOpt.textContent = inheritCurrentlyLabel(paymentLabel(cachedGlobalDefaults.payment)); select.appendChild(inheritOpt); // Filter the payment dropdown to only the (term, payment) combinations @@ -768,7 +787,9 @@ function buildTermOverrideSelect( const inheritOpt = document.createElement('option'); inheritOpt.value = ''; - inheritOpt.textContent = 'Inherit (default)'; + // Issue #112: show the current global term so the user knows what + // "Inherit" means without switching to the Purchasing tab. + inheritOpt.textContent = inheritCurrentlyLabel(termLabel(String(cachedGlobalDefaults.term))); select.appendChild(inheritOpt); // Filter the term dropdown to the (term, payment) combinations AWS @@ -1451,6 +1472,20 @@ export function openOverrideModal( const errEl = document.getElementById('override-form-error'); if (errEl) errEl.textContent = ''; + // Issue #112: update the "Inherit" option on the term select to show the + // current global default so the user knows what they'd be inheriting. + const termInheritOpt = document.querySelector('#override-term option[value=""]'); + if (termInheritOpt) { + termInheritOpt.textContent = inheritCurrentlyLabel(termLabel(String(cachedGlobalDefaults.term))); + } + + // Issue #112: update the coverage placeholder to show the current global + // default. Uses .placeholder (plain text assignment), not innerHTML -- safe. + const coverageInput = document.getElementById('override-coverage') as HTMLInputElement | null; + if (coverageInput) { + coverageInput.placeholder = inheritCurrentlyLabel(`${cachedGlobalDefaults.coverage}%`); + } + // 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). @@ -1514,11 +1549,11 @@ function syncOverridePaymentOptions(provider: string): void { const previous = paymentSel.value; paymentSel.replaceChildren(); - // Always include the "Inherit (default)" option — corresponds to leaving - // the field unset on the sparse PUT. + // Always include the "Inherit (currently: X)" option — corresponds to + // leaving the field unset on the sparse PUT (issue #112). const inheritOpt = document.createElement('option'); inheritOpt.value = ''; - inheritOpt.textContent = 'Inherit (default)'; + inheritOpt.textContent = inheritCurrentlyLabel(paymentLabel(cachedGlobalDefaults.payment)); paymentSel.appendChild(inheritOpt); // When service or term is unset, can't run the validity check yet — @@ -2549,6 +2584,16 @@ function termLabel(value: string): string { return value === '1' ? '1 Year' : value === '3' ? '3 Years' : `${value} Years`; } +/** + * Build the "Inherit (currently: X)" label for override-modal "Inherit" + * options (issue #112). `currentValue` must already be human-readable (e.g., + * "3 Years", "All Upfront", "80%") -- callers are responsible for formatting. + * The value is injected via textContent, not innerHTML, so no escaping needed. + */ +function inheritCurrentlyLabel(currentValue: string): string { + return `Inherit (currently: ${currentValue})`; +} + function paymentLabel(value: string): string { switch (value) { case 'no-upfront': return 'No Upfront'; @@ -2780,6 +2825,15 @@ export async function loadGlobalSettings(): Promise { const coverageInput = document.getElementById('setting-default-coverage') as HTMLInputElement | null; if (coverageInput) coverageInput.value = String(data.global.default_coverage || 80); + // Cache defaults for the override modal "Inherit (currently: X)" labels + // (issue #112). Must be updated here so reopening the override modal + // after a Settings save reflects any in-session changes. + cachedGlobalDefaults = { + term: data.global.default_term || 3, + payment: data.global.default_payment || 'all-upfront', + coverage: data.global.default_coverage || 80, + }; + const notifyDaysInput = document.getElementById('setting-notification-days') as HTMLInputElement | null; if (notifyDaysInput) notifyDaysInput.value = String(data.global.notification_days_before || 3);