Skip to content

ux(settings): add payment option selector to AWS account overrides modal#72

Merged
cristim merged 1 commit intofeat/multicloud-web-frontendfrom
ux/account-overrides-payment-selector
Apr 27, 2026
Merged

ux(settings): add payment option selector to AWS account overrides modal#72
cristim merged 1 commit intofeat/multicloud-web-frontendfrom
ux/account-overrides-payment-selector

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented Apr 25, 2026

Summary

  • Replace the read-only Payment cell in the Settings → Accounts → Overrides panel with an editable <select> for AWS rows so users can set the payment option (No Upfront / Partial Upfront / All Upfront) per account, addressing the gap reported in ux(settings): account-level overrides panel missing payment option selector for AWS accounts #23.
  • The select defaults to "Inherit (default)" when no payment override is set; selecting a value sends a sparse PUT /api/accounts/:id/service-overrides/:provider/:service with { payment } only, so other override fields (term, coverage, …) are preserved by the backend's existing applyOverrideScalars logic.
  • When a payment value is already set, the Inherit option is disabled with a tooltip pointing the user to the existing Reset action — the backend has no clear-field channel and we don't want to silently leave the override in a no-op state.

Test plan

  • npx jest src/__tests__/settings-accounts.test.ts — 4 new tests cover render, save payload, Inherit-disabled when payment was preset, and the AWS-only guard for non-AWS rows.
  • npx jest — full frontend suite (1255 tests) green.
  • npm run typecheck
  • npm run build (production webpack)
  • Smoke test the deployed Lambda URL after merge: open Settings → Accounts → Overrides on an AWS account that has at least one service override and confirm the payment dropdown appears, persists across reload, and survives a no-op refresh.

Closes #23

The Settings → Accounts → Overrides panel previously listed each per-service
override as a static row showing Term / Payment / Coverage with no way to
edit the payment option. AWS teams running mixed payment strategies across
accounts (No Upfront in dev, All Upfront in prod) had no UI path to set this
per account — they had to call the API directly.

This change replaces the read-only Payment cell with a `<select>` (AWS rows
only — Azure/GCP reservations don't expose distinct payment options) bound
to the existing `PUT /api/accounts/:id/service-overrides/:provider/:service`
endpoint. The handler's `applyOverrideScalars` already treats unset request
fields as "leave alone", so sending `{ payment }` preserves term/coverage/etc.

UX:
- "Inherit (default)" is the initial choice when an override has no payment
  set; selecting any of {No Upfront, Partial Upfront, All Upfront} writes
  the value via PUT.
- When a payment is already set, the Inherit option is disabled because the
  backend has no clear-field channel — the user is directed to the existing
  Reset action which removes the entire override. This is documented in the
  option's title attribute.
- Success/failure surface via toast; the select is rolled back to its prior
  value on error and the panel re-renders on success so all rows reflect
  fresh server state.

Tests in settings-accounts.test.ts cover render (Inherit + 3 AWS options
present), save (PUT receives only `{ payment }`), the Inherit-disabled
branch when payment was already set, and the AWS-only guard so Azure rows
keep the read-only cell.

Closes #23
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Apr 25, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

Warning

Rate limit exceeded

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

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 12 minutes and 41 seconds.

⌛ 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: 7926925a-5f91-44c9-871b-b247efd809a5

📥 Commits

Reviewing files that changed from the base of the PR and between c8f58e9 and 12c64f8.

📒 Files selected for processing (2)
  • frontend/src/__tests__/settings-accounts.test.ts
  • frontend/src/settings.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ux/account-overrides-payment-selector

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

✅ 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 ef0aec1 into feat/multicloud-web-frontend Apr 27, 2026
3 checks passed
cristim added a commit that referenced this pull request Apr 27, 2026
#104)

Settings → Accounts → Service overrides was a dead end for any account
with no overrides yet: the panel rendered "No service overrides set."
text with no UI affordance to create one. The only path was a direct
PUT /api/accounts/:id/service-overrides/:provider/:service curl. PR #72's
inline payment selector inherited the same dead end — it operates on
existing rows that nobody can create.

This commit adds an "Add override" modal flow:

- Panel empty-state for an AWS account auto-opens the create modal so
  users land directly on the create form. The Add Override button stays
  in the panel as a fallback if they cancel and want to retry without
  re-expanding the row.
- Populated panels gain an "Add override" button at the top so users
  can add another override for a different service.
- The modal lets users pick service (filtered to AWS_OVERRIDE_SERVICES,
  excluding services that already have an override for this account so
  UPSERT doesn't silently overwrite), and set Term / Payment / Coverage
  via Inherit-by-default selects + a 0–100 numeric input.
- Submit sends a sparse PUT — fields left as Inherit are omitted, so
  the backend's applyOverrideScalars treats them as "use global default".
- Submit blocks when no field is set (would persist a no-op override row)
  and when coverage is out of 0–100 range.

Non-AWS accounts (Azure / GCP) keep their existing read-only empty-state
text and gain neither the Add button nor the auto-open. Per-product
term/payment semantics differ for those providers and a follow-up issue
under #104 will track the modal expansion once the AWS UX settles.

The modal mirrors the existing account-modal pattern in index.html
(role=dialog, aria-modal, .modal-content, .modal-buttons) and the
open/close/submit code follows the same shape as openAccountModal /
closeAccountModal / handleAccountFormSubmit.

Tests: 10 new cases in settings-accounts.test.ts cover empty-state
auto-open, populated-state add button, sparse PUT semantics, term/coverage
coercion, no-fields rejection, coverage-range validation, save-success
panel reload, cancel-without-save, all-services-used disables submit,
and non-AWS empty state stays passive. Full suite: 1265 tests green.
cristim added a commit that referenced this pull request Apr 27, 2026
#104) (#106)

Settings → Accounts → Service overrides was a dead end for any account
with no overrides yet: the panel rendered "No service overrides set."
text with no UI affordance to create one. The only path was a direct
PUT /api/accounts/:id/service-overrides/:provider/:service curl. PR #72's
inline payment selector inherited the same dead end — it operates on
existing rows that nobody can create.

This commit adds an "Add override" modal flow:

- Panel empty-state for an AWS account auto-opens the create modal so
  users land directly on the create form. The Add Override button stays
  in the panel as a fallback if they cancel and want to retry without
  re-expanding the row.
- Populated panels gain an "Add override" button at the top so users
  can add another override for a different service.
- The modal lets users pick service (filtered to AWS_OVERRIDE_SERVICES,
  excluding services that already have an override for this account so
  UPSERT doesn't silently overwrite), and set Term / Payment / Coverage
  via Inherit-by-default selects + a 0–100 numeric input.
- Submit sends a sparse PUT — fields left as Inherit are omitted, so
  the backend's applyOverrideScalars treats them as "use global default".
- Submit blocks when no field is set (would persist a no-op override row)
  and when coverage is out of 0–100 range.

Non-AWS accounts (Azure / GCP) keep their existing read-only empty-state
text and gain neither the Add button nor the auto-open. Per-product
term/payment semantics differ for those providers and a follow-up issue
under #104 will track the modal expansion once the AWS UX settles.

The modal mirrors the existing account-modal pattern in index.html
(role=dialog, aria-modal, .modal-content, .modal-buttons) and the
open/close/submit code follows the same shape as openAccountModal /
closeAccountModal / handleAccountFormSubmit.

Tests: 10 new cases in settings-accounts.test.ts cover empty-state
auto-open, populated-state add button, sparse PUT semantics, term/coverage
coercion, no-fields rejection, coverage-range validation, save-success
panel reload, cancel-without-save, all-services-used disables submit,
and non-AWS empty state stays passive. Full suite: 1265 tests green.
cristim added a commit that referenced this pull request Apr 27, 2026
closes #122, #116) (#124)

The Overrides UX from #72 (inline payment edit) and #106 (create modal)
rendered each account's overrides into an inline expandable panel
appended below the entire accounts table. Two open panels stacked
without per-row attachment, so an override row created for one account
appeared visually adjacent to a different account's empty-state. From
the user's screenshot: an aws/ec2 override created for AWS 540... was
displayed under CUDly host (909...), with two unscoped "Add override"
buttons and a confusing "No service overrides yet for this account."
text in between.

Rework: every Overrides operation now lives inside a per-account modal
keyed by an explicit title. Clicking the row's Overrides button opens
account-overrides-modal whose title reads "Service overrides for
{account.name} ({external_id})". The modal body uses the same
loadOverridesPanel rendering function as before, so all four CRUD
operations carry forward unchanged:

- LIST: the existing overrides-table with Service / Term / Payment /
  Coverage / Reset, including #72's inline payment <select> in the
  Payment column.
- CREATE: the existing override-modal from #106 stacks on top of the
  account-overrides-modal when the user clicks "Add override". Saving
  reloads the parent table; cancelling leaves the parent open.
- EDIT-PAYMENT: same inline <select> as #72, now scoped inside the
  modal body so its loadOverridesPanel reload target is unambiguous.
- DELETE: Reset button + confirmDialog, identical to before.

renderAccountsList no longer creates an inline panel per row, no longer
collects them in a panels[] array, and no longer appends them after the
table — that whole inline-panel scaffolding is gone.

Empty-state UX is preserved: an AWS account with zero overrides still
auto-opens the inner create modal so users land directly on the form
in 1 click instead of 2.

Defensive: closeAccountOverridesModal also closes the inner create
modal so a programmatic close (or future ESC-to-close from #115)
doesn't leave an orphan modal whose Save would target a hidden parent.

Tests: existing #72 + #106 test helpers updated to look for
#account-overrides-modal-body instead of .account-overrides-panel.
Four new tests in a "Account overrides modal" describe block lock in
the behaviour: title-binds-to-account, switching-accounts-swaps-title,
close-clears-body, and a regression guard that .account-overrides-panel
no longer renders.

Full settings-accounts test count: 40/40 (was 36 + 4 new).
Full frontend suite: 1273/1273 green; typecheck + production build
clean.
@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/few Limited audience effort/s Hours type/feat New capability labels Apr 28, 2026
@cristim cristim deleted the ux/account-overrides-payment-selector branch April 29, 2026 10:08
cristim added a commit that referenced this pull request Apr 29, 2026
Closes #117.

Per-account service overrides have been a feature since migration
000011_cloud_accounts.up.sql (mid-2024) and now have full UI coverage
via #72 (inline payment edit) and #106 (create modal). Adds a new
"Per-Account Service Overrides" section to README between "Coverage
Percentage" and "Safety Features" covering:

- Concept + when to use vs the global Settings → Purchasing card.
- Web-UI walkthrough (Settings → Accounts → expand → Service overrides).
- AWS-only V1 boundary callout pointing at #109 for Azure/GCP.
- How to edit existing overrides — inline Payment per #72, Reset, and
  the #110 follow-up for inline Term/Coverage/Enabled.
- "Inherit" semantics: blank fields are not stored as a sentinel; the
  PUT request stays sparse and the engine reads the global default at
  evaluation time.
- API parity note: the override modal targets the same endpoint as
  scripted setups; both write the same row.

Also disables MD060 (table-column-style) in .markdownlint.yaml — same
rationale as PR #169 (landing here too in case the PRs merge in a
different order; both diffs are idempotent).
cristim added a commit that referenced this pull request Apr 29, 2026
…rules

Closes #107.

The two override UIs (inline payment selector from #72 and create-modal
from #106) accepted any (service, term, payment) combination — including
ones the global Settings → Purchasing cards already block as invalid.
Concrete example: RDS 3yr no-upfront is hidden in the global RDS card
via commitmentOptions.invalidCombinations, but the override modal saved
it cleanly and the inline selector let the user pick it.

Wired the override surfaces through the same commitmentOptions helpers
the global cards use:

1. **Inline payment selector (`buildPaymentOverrideSelect`)**: now calls
   `getValidPaymentOptions(provider, service, term)` and only renders
   options that pass. For an RDS row with term=3, no-upfront is gone.
   When term is unset (override row created without a term), falls back
   to the full AWS list — can't pre-validate without it. If the row's
   CURRENT payment somehow isn't in the valid set (e.g. saved via
   direct API curl before this filter shipped, or AWS tightened the
   rules), still renders it as a dedicated "(invalid for term Ny —
   Delete the override and recreate)" option so the row isn't silently
   mutated.

2. **Override-create modal (`openOverrideModal`)**: new
   `syncOverridePaymentOptions` helper repopulates the payment dropdown
   on every service or term change, dropping invalid combinations
   automatically and snapping the selection back to "Inherit" if the
   previously-chosen payment is no longer valid for the new (service,
   term) pair.

3. **Submit-side validation (`submitOverrideForm`)**: when both term
   and payment are explicitly set, refuses to save invalid combos even
   if a stale select option leaked through (e.g. browser back-button
   restoring a stale dropdown state). Mirror of the global Settings →
   Purchasing card's checkCommitmentOptionCombo.

Side change: exported `loadOverridesPanel` and `openOverrideModal` so
the new tests can drive the panel and modal directly. Both are coherent
units; just needed the export.

Drive-by: removed unused `formatRelativeTime` import in
`frontend/src/recommendations.ts` (pre-existing TS6133 blocking the
pre-commit Run frontend tests hook on this branch — same fix as PR

Tests:
  - `inline payment selector hides invalid options for RDS term=3 row`
    pins the RDS-no-3yr-no-upfront case.
  - `inline payment selector shows full list when term is unset`
    pins the fallback case.
  - `override-create modal hides invalid payment options on
    service+term change` pins the dependency loop and the reset-
    to-Inherit-when-stale snap.
  - All 40 existing settings-accounts tests still pass; 43 total.

Backend save-side guard (the issue's third bullet) is deferred — the
frontend gates close the practical bug; a backend mirror via the
existing `commitmentopts.Service.Validate` machinery is its own
PR-sized change touching `internal/api/handler_accounts.go` and would
need probe-data integration tests. Worth filing as a follow-up.
cristim added a commit that referenced this pull request Apr 30, 2026
…rules (closes #107) (#176)

* fix(ux/settings): override surfaces reuse commitmentOptions validity rules

Closes #107.

The two override UIs (inline payment selector from #72 and create-modal
from #106) accepted any (service, term, payment) combination — including
ones the global Settings → Purchasing cards already block as invalid.
Concrete example: RDS 3yr no-upfront is hidden in the global RDS card
via commitmentOptions.invalidCombinations, but the override modal saved
it cleanly and the inline selector let the user pick it.

Wired the override surfaces through the same commitmentOptions helpers
the global cards use:

1. **Inline payment selector (`buildPaymentOverrideSelect`)**: now calls
   `getValidPaymentOptions(provider, service, term)` and only renders
   options that pass. For an RDS row with term=3, no-upfront is gone.
   When term is unset (override row created without a term), falls back
   to the full AWS list — can't pre-validate without it. If the row's
   CURRENT payment somehow isn't in the valid set (e.g. saved via
   direct API curl before this filter shipped, or AWS tightened the
   rules), still renders it as a dedicated "(invalid for term Ny —
   Delete the override and recreate)" option so the row isn't silently
   mutated.

2. **Override-create modal (`openOverrideModal`)**: new
   `syncOverridePaymentOptions` helper repopulates the payment dropdown
   on every service or term change, dropping invalid combinations
   automatically and snapping the selection back to "Inherit" if the
   previously-chosen payment is no longer valid for the new (service,
   term) pair.

3. **Submit-side validation (`submitOverrideForm`)**: when both term
   and payment are explicitly set, refuses to save invalid combos even
   if a stale select option leaked through (e.g. browser back-button
   restoring a stale dropdown state). Mirror of the global Settings →
   Purchasing card's checkCommitmentOptionCombo.

Side change: exported `loadOverridesPanel` and `openOverrideModal` so
the new tests can drive the panel and modal directly. Both are coherent
units; just needed the export.

Drive-by: removed unused `formatRelativeTime` import in
`frontend/src/recommendations.ts` (pre-existing TS6133 blocking the
pre-commit Run frontend tests hook on this branch — same fix as PR

Tests:
  - `inline payment selector hides invalid options for RDS term=3 row`
    pins the RDS-no-3yr-no-upfront case.
  - `inline payment selector shows full list when term is unset`
    pins the fallback case.
  - `override-create modal hides invalid payment options on
    service+term change` pins the dependency loop and the reset-
    to-Inherit-when-stale snap.
  - All 40 existing settings-accounts tests still pass; 43 total.

Backend save-side guard (the issue's third bullet) is deferred — the
frontend gates close the practical bug; a backend mirror via the
existing `commitmentopts.Service.Validate` machinery is its own
PR-sized change touching `internal/api/handler_accounts.go` and would
need probe-data integration tests. Worth filing as a follow-up.

* fix(frontend): prevent override modal listener stacking

Scope override payment-option listeners to an AbortController per modal open so
reopening the modal cannot accumulate stale service/term handlers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/few Limited audience 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