Skip to content

feat(settings): per-product Coverage and Enabled controls on SP cards (closes #136) - #580

Merged
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
feat/issue-136-sp-card-fields
May 22, 2026
Merged

feat(settings): per-product Coverage and Enabled controls on SP cards (closes #136)#580
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
feat/issue-136-sp-card-fields

Conversation

@cristim

@cristim cristim commented May 20, 2026

Copy link
Copy Markdown
Member

Summary

  • Add Coverage % input and Enabled checkbox to each of the 4 Savings Plans cards (Compute, EC2 Instance, SageMaker, Database), matching the per-product settings already present on RI cards
  • SERVICE_FIELDS SP entries extended with coverageId/enabledId; TRACKED_FIELDS picks them up for dirty detection
  • loadGlobalSettings reads per-card coverage/enabled from service config, falling back to default_coverage when absent
  • saveGlobalSettings reads per-card coverage/enabled from DOM controls when present; non-SP cards continue to inherit global settings unchanged

Test plan

  • npx tsc --noEmit passes (no type errors)
  • npx jest passes (1900/1900, +3 new tests covering save-coverage, save-enabled, load-from-service-config)
  • HTML structural assertions extended to verify coverage/enabled IDs on all 4 SP card types
  • Manual: open Settings page, verify Coverage % and Enabled controls render on each SP card, save, reload, verify values persisted

Summary by CodeRabbit

  • New Features

    • Added per-plan-type coverage percentage configuration for AWS Savings Plans: Compute, EC2 Instance, SageMaker, and Database
    • Added individual enable/disable toggles for each Savings Plans type
  • Tests

    • Extended test suite to validate correct persistence and retrieval of per-plan-type Savings Plans coverage and enabled settings

Review Change Stack

…closes #136)

Add Coverage % and Enabled toggle to each of the four Savings Plans cards
(Compute, EC2 Instance, SageMaker, Database), matching the per-product
settings already available on RI cards.

- SERVICE_FIELDS SP entries extended with coverageId/enabledId
- TRACKED_FIELDS includes the new IDs for dirty-state detection
- loadGlobalSettings populates per-card coverage/enabled from service
  config, falling back to global default_coverage when absent
- saveGlobalSettings reads per-card coverage/enabled from DOM when the
  card exposes the controls; non-SP cards continue to inherit global settings
- index.html: Coverage % number input and Enabled checkbox added to each
  SP card in the settings form
- Tests: DOM fixture updated; 3 new tests cover save-coverage, save-enabled,
  and load-from-service-config paths; HTML structural assertions extended
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@cristim has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 9 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b20dbe35-c70a-4958-9aa2-ecc9ea1b8c4c

📥 Commits

Reviewing files that changed from the base of the PR and between 851bd5c and 822a4aa.

📒 Files selected for processing (2)
  • frontend/src/__tests__/settings.test.ts
  • frontend/src/settings.ts
📝 Walkthrough

Walkthrough

This PR implements per-plan-type AWS Savings Plans configuration controls. New HTML inputs for coverage percentage and enabled state are added to each of four plan cards. The service field registry and dirty-tracking infrastructure are extended, and load/save logic is implemented to persist these settings bidirectionally with appropriate fallback behavior.

Changes

Per-Plan Savings Plans Controls

Layer / File(s) Summary
UI Template & Service Registry
frontend/src/index.html, frontend/src/settings.ts
HTML adds coverage % input and enabled checkbox for each Savings Plans card (Compute, EC2 Instance, SageMaker, Database). SERVICE_FIELDS registry extended with coverageId and enabledId metadata for the four plan types.
Load Path & Dirty Tracking
frontend/src/settings.ts
TRACKED_FIELDS extended with per-plan coverage and enabled input IDs. loadGlobalSettings() populates card inputs from service config coverage/enabled with fallback to global default_coverage and enabled-checked state.
Save Path Implementation
frontend/src/settings.ts
saveGlobalSettings() per-service loop refactored to destructure field metadata, then updated to read coverage and enabled from card-specific DOM inputs when present, otherwise use base/default values.
Test Coverage
frontend/src/__tests__/settings.test.ts
Test fixture extended with new controls. Index.html assertions verify control element IDs exist. New tests validate saveGlobalSettings persists per-card coverage and enabled state, and loadGlobalSettings populates from config with fallbacks.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • #136: Directly implements the per-plan-type Savings Plans coverage and enabled controls specified in the issue, with full persistence and UI integration.

Poem

🐰 Four plans, four cards, now each has its own—
Coverage and toggle, not shared anymore shown!
Load and save dance, with defaults so kind,
Persistent sweet state, forever aligned.
Savings Plans shine with control so refined! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding per-product Coverage and Enabled controls to Savings Plans cards, with specific reference to issue #136.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-136-sp-card-fields

Comment @coderabbitai help to get the list of available commands and usage tips.

@cristim cristim added priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/s Hours type/feat New capability triaged Item has been triaged labels May 20, 2026
@cristim

cristim commented May 20, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/settings.ts (1)

2674-2688: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fallback is skipped when an SP service row is absent.

Line 2676 only populates SP coverage/enabled for services that exist in data.services. If a card has no row, coverage stays at the HTML default (80) instead of global.default_coverage, which can persist incorrect values on next save.

💡 Suggested fix
-    if (data.services) {
-      loadedServiceConfigs = data.services;
-      for (const svc of data.services) {
+    const services = data.services ?? [];
+    loadedServiceConfigs = services;
+    for (const svc of services) {
         const key = `${svc.provider}-${svc.service}`;
         const termEl = document.getElementById(`${key}-term`) as HTMLSelectElement | null;
         if (termEl) termEl.value = String(svc.term);
         const paymentEl = document.getElementById(`${key}-payment`) as HTMLSelectElement | null;
         if (paymentEl) paymentEl.value = svc.payment;
-        // Issue `#136`: populate per-product SP coverage and enabled fields when
-        // the card exposes them. Other service cards fall through (IDs absent).
-        const coverageEl = document.getElementById(`${key}-coverage`) as HTMLInputElement | null;
-        if (coverageEl) coverageEl.value = String(svc.coverage ?? data.global?.default_coverage ?? 80);
-        const enabledEl = document.getElementById(`${key}-enabled`) as HTMLInputElement | null;
-        if (enabledEl) enabledEl.checked = svc.enabled !== false;
-      }
-    }
+    }
+
+    // Ensure SP cards are populated even when a service row is absent.
+    const byKey = new Map(services.map(s => [`${s.provider}-${s.service}`, s] as const));
+    for (const field of SERVICE_FIELDS) {
+      const svc = byKey.get(`${field.provider}-${field.service}`);
+      if ('coverageId' in field && field.coverageId) {
+        const el = byId<HTMLInputElement>(field.coverageId);
+        if (el) el.value = String(svc?.coverage ?? data.global?.default_coverage ?? 80);
+      }
+      if ('enabledId' in field && field.enabledId) {
+        const el = byId<HTMLInputElement>(field.enabledId);
+        if (el) el.checked = svc?.enabled !== false;
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/settings.ts` around lines 2674 - 2688, The current loop over
data.services only sets coverage/enabled for services present in data.services,
so coverage falls back to the HTML default (80) when a service card exists but
has no row; to fix, initialize coverage/enabled for all service DOM rows first
(or iterate service DOM cards) and then overlay values from data.services: build
a map from data.services (e.g., loadedServiceConfigs) keyed by
`${svc.provider}-${svc.service}`, then iterate the DOM elements (or
querySelectorAll for elements with ids ending in `-coverage`/`-enabled`) compute
the same `key`, and set coverageEl.value = String(map[key]?.coverage ??
data.global?.default_coverage ?? 80) and enabledEl.checked = map[key]?.enabled
!== false so missing services get the global default rather than the HTML
default.
🧹 Nitpick comments (1)
frontend/src/__tests__/settings.test.ts (1)

691-710: ⚡ Quick win

Strengthen fallback assertions to avoid false positives.

This test currently passes even if fallback assignment is skipped, because both expected values (80 / true) equal fixture defaults. Seed a non-default initial DOM state and a non-80 global default before loadGlobalSettings().

✅ Suggested test hardening
     test('loadGlobalSettings populates SP coverage and enabled from service config (issue `#136`)', async () => {
+      // Make fallback observable (different from fixture defaults).
+      (document.getElementById('aws-savings-plans-ec2instance-coverage') as HTMLInputElement).value = '12';
+      (document.getElementById('aws-savings-plans-ec2instance-enabled') as HTMLInputElement).checked = false;
+
       (api.getConfig as jest.Mock).mockResolvedValue({
-        global: { enabled_providers: ['aws'], default_term: 3, default_payment: 'all-upfront', default_coverage: 80 },
+        global: { enabled_providers: ['aws'], default_term: 3, default_payment: 'all-upfront', default_coverage: 67 },
         services: [
           { provider: 'aws', service: 'savings-plans-compute', term: 1, payment: 'no-upfront', coverage: 65, enabled: false },
         ],
       });
       setupSettingsHandlers();
       await loadGlobalSettings();
@@
       const ec2Coverage = document.getElementById('aws-savings-plans-ec2instance-coverage') as HTMLInputElement;
       const ec2Enabled = document.getElementById('aws-savings-plans-ec2instance-enabled') as HTMLInputElement;
-      expect(ec2Coverage.value).toBe('80');
+      expect(ec2Coverage.value).toBe('67');
       expect(ec2Enabled.checked).toBe(true);
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/__tests__/settings.test.ts` around lines 691 - 710, The test
should be hardened so fallback behavior is actually verified: before calling
loadGlobalSettings() set the initial DOM inputs for the fallback cards (e.g.,
elements with ids 'aws-savings-plans-ec2instance-coverage' and
'aws-savings-plans-ec2instance-enabled') to values that differ from the fixture
defaults (e.g., coverage != '80' and enabled != true), and change the mocked
global default in api.getConfig to a non-80 value; then call
setupSettingsHandlers() and loadGlobalSettings() and assert the values were
updated to the new global defaults or service-specific values to ensure fallback
assignment ran (refer to loadGlobalSettings, setupSettingsHandlers and the
element ids used in the test).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@frontend/src/settings.ts`:
- Around line 2674-2688: The current loop over data.services only sets
coverage/enabled for services present in data.services, so coverage falls back
to the HTML default (80) when a service card exists but has no row; to fix,
initialize coverage/enabled for all service DOM rows first (or iterate service
DOM cards) and then overlay values from data.services: build a map from
data.services (e.g., loadedServiceConfigs) keyed by
`${svc.provider}-${svc.service}`, then iterate the DOM elements (or
querySelectorAll for elements with ids ending in `-coverage`/`-enabled`) compute
the same `key`, and set coverageEl.value = String(map[key]?.coverage ??
data.global?.default_coverage ?? 80) and enabledEl.checked = map[key]?.enabled
!== false so missing services get the global default rather than the HTML
default.

---

Nitpick comments:
In `@frontend/src/__tests__/settings.test.ts`:
- Around line 691-710: The test should be hardened so fallback behavior is
actually verified: before calling loadGlobalSettings() set the initial DOM
inputs for the fallback cards (e.g., elements with ids
'aws-savings-plans-ec2instance-coverage' and
'aws-savings-plans-ec2instance-enabled') to values that differ from the fixture
defaults (e.g., coverage != '80' and enabled != true), and change the mocked
global default in api.getConfig to a non-80 value; then call
setupSettingsHandlers() and loadGlobalSettings() and assert the values were
updated to the new global defaults or service-specific values to ensure fallback
assignment ran (refer to loadGlobalSettings, setupSettingsHandlers and the
element ids used in the test).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a042b4d-fa09-4880-b52b-13453b4e3596

📥 Commits

Reviewing files that changed from the base of the PR and between bc8831b and 851bd5c.

📒 Files selected for processing (3)
  • frontend/src/__tests__/settings.test.ts
  • frontend/src/index.html
  • frontend/src/settings.ts

…sent

loadGlobalSettings only populated per-product Savings Plans coverage and
enabled controls for services present in the config response. A SP card
whose service row was missing kept the HTML default (80) instead of
falling back to global.default_coverage, which then persisted an
incorrect value on the next save.

Iterate SERVICE_FIELDS (the source of truth for SP card IDs) and overlay
any matching service-config values, so every SP card gets the global
default when its row is absent. Harden the regression test with a
non-default seeded DOM state and a non-80 global default so the fallback
assignment is actually exercised.

Addresses CodeRabbit review on PR #580.
@cristim

cristim commented May 22, 2026

Copy link
Copy Markdown
Member Author

Addressed the CodeRabbit review (commit 822a4aa35).

Major (outside-diff) frontend/src/settings.ts 2674-2688 - Fallback skipped when an SP service row is absent: FIXED.

Confirmed real: loadGlobalSettings only populated per-product SP coverage/enabled for services present in data.services. A SP card whose service row was missing from the response kept the HTML default (80) instead of falling back to global.default_coverage, and that stale value was then persisted by the save path (settings.ts ~2933-2938) on the user's next Save.

Fix: split the term/payment loop from the SP-control population, then iterate SERVICE_FIELDS (the source of truth for SP card IDs) and overlay any matching service-config values keyed by ${provider}-${service}. Cards without a row now fall back to global.default_coverage (and enabled = true). This also covers the fresh-install path where data.services is absent entirely.

Nitpick frontend/src/__tests__/settings.test.ts 691-710 - Fallback assertions could pass on fixture defaults: FIXED.

Seeded a non-default initial DOM state on the fallback card (coverage = '12', enabled = false) and changed the mocked global default to 67 (non-80), so the assertions ('67' / true) now fail if the fallback assignment is skipped.

Verification: tsc --noEmit clean, webpack --mode production build clean, jest src/__tests__/settings.test.ts green (58 tests). (npm run lint is non-functional repo-wide: no ESLint config is committed anywhere in the repo, so it cannot run in any checkout; unrelated to this change.)

No out-of-scope follow-ups discovered.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

@cristim Thanks for the thorough follow-up! Both fixes look well-reasoned:

  • The fallback fix — iterating SERVICE_FIELDS as the authoritative source and overlaying service-config values (rather than only populating cards that happen to appear in data.services) is the right approach, and it cleanly handles both the missing-row and fresh-install paths.
  • The test hardening — seeding a non-default initial DOM state and using a distinct global default (67) ensures the assertions are genuinely sensitive to whether the fallback assignment runs. Good catch on the original weakness.

Triggering a fresh review of the updated commits now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim merged commit 1e751af into feat/multicloud-web-frontend May 22, 2026
6 checks passed
@cristim
cristim deleted the feat/issue-136-sp-card-fields branch June 3, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/feat New capability urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant