Skip to content
Merged
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
108 changes: 107 additions & 1 deletion frontend/src/__tests__/settings-accounts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
loadAccountsForProvider,
loadOverridesPanel,
openOverrideModal,
setupSettingsHandlers
setupSettingsHandlers,
setGlobalDefaultsForTest,
} from '../settings';

jest.mock('../api', () => ({
Expand Down Expand Up @@ -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<HTMLOptionElement>('#override-term option[value=""]');
expect(termInherit?.textContent).toBe('Inherit (currently: 3 Years)');

const payInherit = document.querySelector<HTMLOptionElement>('#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<HTMLOptionElement>('#override-term option[value=""]');
expect(termInherit?.textContent).toBe('Inherit (currently: 1 Year)');

const payInherit = document.querySelector<HTMLOptionElement>('#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%)');
});
});
64 changes: 59 additions & 5 deletions frontend/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<HTMLOptionElement>('#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).
Expand Down Expand 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 —
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -2780,6 +2825,15 @@ export async function loadGlobalSettings(): Promise<void> {
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);

Expand Down
Loading