feat(frontend/recs): add 'On-Demand Monthly' column to recommendations table#322
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds a new "On‑Demand Monthly" column to the recommendations table: computes the unsubsidised monthly baseline by reversing upfront amortization, exposes helpers (onDemandMonthly, effectiveMonthlySavings, effectiveSavingsPct), wires the column into sort/filter/header/row rendering, and adds unit tests for formatting, filtering, and sorting. (50 words) ChangesOn-Demand Monthly Column
Sequence Diagram(s)(Not included — UI-only column addition with local computation, no multi-component sequential flow.) Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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.
Inline comments:
In `@frontend/src/__tests__/recommendations.test.ts`:
- Around line 3510-3559: The tests scan all <td> text so they can be satisfied
by other columns; update the two assertions to target the actual
on_demand_monthly cell(s) and add a regression that a filter on
on_demand_monthly excludes nulls: in the two tests that use
baseRec()/loadRecommendations()/state.getRecommendations mock data, query the
cell(s) for the on_demand_monthly column (e.g. select using the same attribute
used for the header th[data-sort="on_demand_monthly"] to find the matching td
for that column) and assert the exact cell text is '$150' (when monthly_cost
present) or '—' (when monthly_cost null); then add a small follow-up test that
calls loadRecommendations with state.getRecommendations returning a null
monthly_cost row and applies state.setRecommendationsFilter({ on_demand_monthly:
{ kind: 'expr', expr: '0' } }) (or the existing filter wiring API) and assert
the null row does not match (no '$0' / no match), ensuring the non-matching
sentinel behavior is pinned.
In `@frontend/src/recommendations.ts`:
- Around line 357-360: The current fallback in on_demand_monthly
(on_demand_monthly: (r) => onDemandMonthly(r) ?? Number.POSITIVE_INFINITY)
causes null on_demand_monthly rows to appear first when sorting descending;
instead, update the sorting logic in groupsInSortOrder to explicitly handle
nulls so they always sort last regardless of direction: have on_demand_monthly
produce a pair/flag (or leave null) and modify the numeric comparator used by
groupsInSortOrder to check for nulls first (if one value is null and the other
isn’t, treat the null as greater so it goes last), otherwise compare numeric
values normally taking the sort direction into account; reference
on_demand_monthly, onDemandMonthly, and groupsInSortOrder when making this
change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 84785109-0085-47a0-bd81-b3c0d36ebf5c
📒 Files selected for processing (3)
frontend/src/__tests__/recommendations.test.tsfrontend/src/recommendations.tsfrontend/src/state.ts
|
Addressed CodeRabbit pass 1 findings:
@coderabbitai review |
|
Tip For best results, initiate chat on the files or code changes.
Triggering a fresh incremental review now. [review] |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
frontend/src/recommendations.ts (1)
357-360: ⚡ Quick winUpdate stale sort-behavior comment to match the comparator logic.
The inline note says nulls go to the top in descending order, but
groupsInSortOrdernow enforces null-last for both directions. Please update this comment to avoid future regressions.Suggested diff
- // POSITIVE_INFINITY places null rows at the bottom in ascending order and - // at the top in descending — consistent with monthly_cost. + // POSITIVE_INFINITY is used as the null sentinel; groupsInSortOrder + // applies an explicit null-last guard so nullish rows stay de-emphasised + // in both ascending and descending sorts.🤖 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/recommendations.ts` around lines 357 - 360, The comment on the on_demand_monthly mapper is stale: update the inline note around on_demand_monthly: (r) => onDemandMonthly(r) ?? Number.POSITIVE_INFINITY to state that null/undefined monthly_cost values are mapped to POSITIVE_INFINITY so they are sorted last (null-last) in both ascending and descending order because groupsInSortOrder enforces null-last for all directions; adjust the wording to remove the claim that nulls go to the top in descending order and explicitly mention null-last behavior consistent with groupsInSortOrder.
🤖 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.
Nitpick comments:
In `@frontend/src/recommendations.ts`:
- Around line 357-360: The comment on the on_demand_monthly mapper is stale:
update the inline note around on_demand_monthly: (r) => onDemandMonthly(r) ??
Number.POSITIVE_INFINITY to state that null/undefined monthly_cost values are
mapped to POSITIVE_INFINITY so they are sorted last (null-last) in both
ascending and descending order because groupsInSortOrder enforces null-last for
all directions; adjust the wording to remove the claim that nulls go to the top
in descending order and explicitly mention null-last behavior consistent with
groupsInSortOrder.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5873bc10-2e0a-4953-87d6-767d0684c72e
📒 Files selected for processing (2)
frontend/src/__tests__/recommendations.test.tsfrontend/src/recommendations.ts
…s table Adds a new on_demand_monthly column that shows the equivalent on-demand monthly cost, derived client-side as: on_demand_monthly = monthly_cost + savings + (upfront_cost / (term × 12)) - Renders formatCurrency for non-null monthly_cost; em-dash otherwise - Sortable (null rows sort to end via POSITIVE_INFINITY) - Filterable with numeric filter predicates (NaN for null avoids false = 0 matches) - Exports onDemandMonthly() helper for reuse and testability Closes #317
…sertions Two CodeRabbit findings addressed: 1. groupsInSortOrder: POSITIVE_INFINITY * direction = -Infinity for desc, placing null rows first. Add explicit null-last guard before the numeric comparator so nulls always sort to the end regardless of direction. Fixes monthly_cost and effective_savings_pct descending sort as well. 2. On-Demand Monthly rendering tests: assertions scanned all <td> elements and could pass from other columns. Switch to column-index pinning via th[data-sort="on_demand_monthly"] and add a filter regression test that pins the NaN sentinel (null monthly_cost must not match = 0 filter).
573927c to
331e73a
Compare
#322 (now merged) added the on_demand_monthly column to the recs table, bringing COLUMN_DEFS.length to 13. The visibility-feature test was written when the count was 12 — update it post-rebase so the suite stays green.
Three CR nitpicks, all in __tests__/recommendations.test.ts:
1. "changing the dropdown calls setCostPeriod and triggers rerender"
now seeds getCostPeriod with mockReturnValueOnce('monthly') +
mockReturnValue('hourly') and asserts the rerender actually
rebuilt the savings-column header to "Savings / hr". The previous
version would have passed even if the change handler stopped
rebuilding the DOM.
2. "invalid value in localStorage falls back to ..." now seeds the
in-memory state with a non-default value first (setCostPeriodFn
('hourly')) before injecting the invalid localStorage value, so
the test fails if getCostPeriod() incorrectly returns a stale
in-memory cache instead of re-validating localStorage on each
call. Also renamed for clarity ("static default, not prior
in-memory state").
3. Added two new tests in the "table cells scale with period"
describe block that pin the scaled-numeric contract:
- yearly sort orders savings cells by yearly-scaled value (asc):
two recs pre-sorted DESC are reordered ASC by the production
sort which routes through scaleCost. A no-op scale that
preserves input order would fail.
- hourly numeric filter compares the scaled (per-hour) savings,
not the raw monthly value: two recs (360 and 720 monthly) with
a ">0.75" filter — both pass the raw filter, only r-high passes
after hourly scaling (1.0 vs 0.5). A regression that drops the
scaleCost call inside numericCellValue would surface here.
Plus the rebase onto current feat/multicloud-web-frontend (PR #322's
on-demand monthly column landed there). The base column row now has
14 data columns; on_demand_monthly cell uses formatCostForPeriod so
it scales with the active period like savings/monthly_cost, and
SORTABLE_NUMERIC_COLUMNS.on_demand_monthly + numericCellValue.
on_demand_monthly are wired through scaleCost. Header label for
on_demand_monthly now period-scales via getColumnLabel
("On-Demand Monthly" / "On-Demand / hr" / etc.). TABLE_COL_COUNT
bumped from 14 to 15 to account for the extra column.
235 frontend tests pass.
…y) (#328) * feat(frontend/recs): cost-period selector (hourly/daily/monthly/yearly) Adds a "Show costs" dropdown in the recs toolbar letting users view all cost / savings / on-demand columns scaled to hourly · daily · monthly (default) · yearly. Engineering thinks hourly, finance thinks yearly, cost-optimisation review thinks monthly — instead of mentally multiplying by 24 × 30 / 12, one dropdown rescales the displayed values. Affects display values, headers, sort + filter (uses scaled values so a "< $1" filter at hourly does what it should), summary card, and fan-out totals. Does NOT scale: Upfront Cost (one-time), Effective % (already a ratio), Term (already in years). Conversion factors (consistent with industry quoting): hourly = monthly / (24 * 30) — 720 hrs/mo daily = monthly / 30 yearly = monthly * 12 Selection persists in localStorage (`cudly.recs.costPeriod`). When the persisted value is corrupted/invalid, getCostPeriod() falls back to the static default ('monthly') rather than to whatever leaked into the in-memory cache from a prior setCostPeriod() call — defensive against storage drift across versions. Tests: 50+ new test cases under "Issue #319: cost-period selector" plus "Issue #319: localStorage persistence". The persistence tests install a Map-backed `localStorage` shim per test (the global setup mocks localStorage as a noop) so we exercise the real persistence semantics. Closes #319 * test(frontend/recs): address CodeRabbit pass-1 nitpicks on PR #328 Three CR nitpicks, all in __tests__/recommendations.test.ts: 1. "changing the dropdown calls setCostPeriod and triggers rerender" now seeds getCostPeriod with mockReturnValueOnce('monthly') + mockReturnValue('hourly') and asserts the rerender actually rebuilt the savings-column header to "Savings / hr". The previous version would have passed even if the change handler stopped rebuilding the DOM. 2. "invalid value in localStorage falls back to ..." now seeds the in-memory state with a non-default value first (setCostPeriodFn ('hourly')) before injecting the invalid localStorage value, so the test fails if getCostPeriod() incorrectly returns a stale in-memory cache instead of re-validating localStorage on each call. Also renamed for clarity ("static default, not prior in-memory state"). 3. Added two new tests in the "table cells scale with period" describe block that pin the scaled-numeric contract: - yearly sort orders savings cells by yearly-scaled value (asc): two recs pre-sorted DESC are reordered ASC by the production sort which routes through scaleCost. A no-op scale that preserves input order would fail. - hourly numeric filter compares the scaled (per-hour) savings, not the raw monthly value: two recs (360 and 720 monthly) with a ">0.75" filter — both pass the raw filter, only r-high passes after hourly scaling (1.0 vs 0.5). A regression that drops the scaleCost call inside numericCellValue would surface here. Plus the rebase onto current feat/multicloud-web-frontend (PR #322's on-demand monthly column landed there). The base column row now has 14 data columns; on_demand_monthly cell uses formatCostForPeriod so it scales with the active period like savings/monthly_cost, and SORTABLE_NUMERIC_COLUMNS.on_demand_monthly + numericCellValue. on_demand_monthly are wired through scaleCost. Header label for on_demand_monthly now period-scales via getColumnLabel ("On-Demand Monthly" / "On-Demand / hr" / etc.). TABLE_COL_COUNT bumped from 14 to 15 to account for the extra column. 235 frontend tests pass. * fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Summary
Adds a new On-Demand Monthly column to the recommendations table, showing the equivalent on-demand monthly cost derived client-side:
formatCurrencyfor non-nullmonthly_cost; em-dash (—) when nullMonthly Cost)>,<,=— NaN for null avoids false= 0matches)onDemandMonthly()with 7 unit testsChanges
frontend/src/state.ts: Added'on_demand_monthly'toRecommendationsColumnIdunionfrontend/src/recommendations.ts: AddedonDemandMonthly()helper, wired intoSORTABLE_NUMERIC_COLUMNS,SORT_HEADER_LABELS,categoricalCellValue,numericCellValue,NUMERIC_COLUMNS,FILTERABLE_COLUMNS,buildVariantRowMarkup. IncrementedTABLE_COL_COUNTfrom 13 → 14.frontend/src/__tests__/recommendations.test.ts: Addeddescribe('onDemandMonthly', ...)with 7 unit tests, 3 rendering tests, updated column-count and filter-trigger tests.Coordination note
Sibling parallel work in flight: #318 (
feat/recs-column-visibility) refactors the two column-ordering arrays. If #318 merges first, will rebase and mechanically addon_demand_monthlyto the consolidated source.Will rebase if #318 (column-array refactor) merges first.
Closes #317
Summary by CodeRabbit
New Features
Tests