feat(home/chart): repoint per-service range bars to potential savings + move to Potential section by cristim · Pull Request #782 · LeanerCloud/cloud-commitments-cli · GitHub
Skip to content

feat(home/chart): repoint per-service range bars to potential savings + move to Potential section - #782

Merged
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
feat/769-potential-range-bars
May 28, 2026
Merged

cristim merged 2 commits into
feat/multicloud-web-frontendfrom
feat/769-potential-range-bars

Conversation

@cristim

@cristim cristim commented May 27, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Prior state confusion: #savings-by-service-section (the range bar chart from Add per-service savings-range bar chart on Home page (solid floor + lighter range, surface ByService data already on the wire) #765) was placed between the "Savings over time" trend line and "Potential Savings by Service" pie/bar chart. Its data source was historical data_points[].by_service values from /api/history/analytics, so the bars reflected realized past savings -- not potential future ones. The heading said "Savings range by service" with no temporal framing, making it easy to mistake for forward-looking data.

  • Move: The section now sits below #savings-chart-section so both potential-savings charts are adjacent and share the "Potential Savings" area of the Home page.

  • Re-data: renderSavingsByService now accepts LocalRecommendation[] (already fetched by loadDashboard via getRecommendations). A new computeServiceStatsFromRecs helper groups rows by rec.service and computes min/max of rec.savings within each group. Floor = min potential savings across recommendation rows for that service; upside = max - min. The call was moved out of loadSavingsTrendChart and into loadDashboard where recs is already in scope, eliminating the dependency on the analytics endpoint.

Design

Layout choice: stacked (range bars below the existing #savings-chart-section). Both sections convey different views of potential savings -- the existing chart shows current vs potential totals per service; the range bars show the spread of individual recommendation options. Keeping both provides complementary detail.

Floor/range derivation: per the user's clarification, min = min(rec.savings) and max = max(rec.savings) across all recommendation rows for a service. Each row is treated as one "option." When the backend ships per-variant rows (1yr/3yr x payment option), the range will widen automatically without a code change. The TODO comment in computeServiceStatsFromRecs references #769.

Tooltip enrichment: the new tooltip surfaces Min option and Max option labels (e.g. "1yr no_upfront", "3yr all_upfront") so users can identify which term/payment combination drives the floor and ceiling.

Test plan

  • computeServiceStatsFromRecs unit tests: empty input, single rec (zero upside), two recs same service (min/max/count), minLabel/maxLabel tracking, multi-service independence, samples accumulation.
  • renderSavingsByService DOM tests updated to use Recommendation fixtures (not SavingsDataPoint).
  • Dataset label assertions updated: "Floor" -> "Min potential", "Range" -> "Upside".
  • Heading reset assertion updated to new copy.
  • Integration test: loadDashboard wires range bars to recs, not analytics data (recs present + analytics empty -> chart renders).
  • Removed obsolete loadSavingsTrendChart re-render tests (that coupling no longer exists).
  • npm run typecheck: exit 0, no errors.
  • jest --testPathPattern=dashboard.test: 69 pass, 0 fail.

Summary by CodeRabbit

  • Feature Changes
    • Updated "Potential Savings by Service" chart to display min and max potential savings across recommendation options
    • Enhanced chart tooltips with min/max potential values and associated option labels
    • Reordered chart sections on the dashboard for improved visual layout
    • Refined empty-state messaging and chart labels for clarity

Review Change Stack

… + move to Potential section

- Move #savings-by-service-section below #savings-chart-section so both
  live in the Potential Savings area of the Home page. Previously the
  range bars sat in the current/historical savings group despite showing
  forward-looking data.

- Re-data the chart: renderSavingsByService now accepts
  LocalRecommendation[] instead of SavingsDataPoint[]. A new
  computeServiceStatsFromRecs helper groups recommendation rows by
  service.service and computes min/max of rec.savings within each group.
  Floor = min potential savings for that service; upside = max - min.
  Wired in loadDashboard alongside renderSavingsChart (both have recs
  in scope). Removed the now-redundant calls from loadSavingsTrendChart.

- Update copy: heading -> "Potential savings range per service",
  dataset labels -> "Min potential"/"Upside", tooltip lines use
  "Min potential / Max potential / Options / Min option / Max option".
  Empty state -> "No recommendations available yet."

- Tests: replace SavingsDataPoint fixtures with Recommendation fixtures
  throughout the renderSavingsByService DOM suite. Add
  computeServiceStatsFromRecs unit tests (empty, single, multi-rec,
  minLabel/maxLabel tracking, samples). Add integration test asserting
  loadDashboard wires the bar chart to recs not analytics data. Remove
  obsolete loadSavingsTrendChart re-render tests.

- Drop medianOf helper (no longer called after tooltip update).

NOTE: current recommendations carry a single savings field per row, not
per-payment-option variants. Min/max within a service therefore reflects
the range across different recommendation rows (e.g. different regions or
resource types). Per-variant breakdowns (1yr/3yr x no-upfront/all-upfront)
will further widen the range automatically once the backend ships them.

Closes #769
@cristim cristim added priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/all-users Affects every user enhancement New feature or request triaged Item has been triaged labels May 27, 2026
@coderabbitai

coderabbitai Bot commented May 27, 2026 •

Copy link
Copy Markdown
Contributor

@cristim

cristim commented May 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 27, 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.

Actionable comments posted: 3

🤖 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__/dashboard.test.ts`:
- Around line 1434-1438: The test fixture passed to (api.getDashboardSummary as
jest.Mock).mockResolvedValue uses snake_case keys that don't match the
DashboardSummary contract; update the object to use the exact DashboardSummary
field names (e.g., potentialMonthlySavings, currentMonthlySavings,
totalRecommendations, activeReservations, targetCoverage, ytdSavings, byService)
so the mocked response matches the typed contract used by the code under test
(adjust any nested keys similarly).

In `@frontend/src/dashboard.ts`:
- Around line 712-713: The label construction uses rec.payment which is optional
and can produce "undefined" in labels; update the code that builds label (the
const label = ... line) to guard against undefined by using a default or
conditional—e.g., use rec.payment ?? '' or build the string only when
rec.payment exists (e.g., rec.payment ? `${rec.term}yr ${rec.payment}` :
`${rec.term}yr`) so labels never contain "undefined"; locate this change around
the label variable near stats and svc references.

In `@frontend/src/index.html`:
- Line 103: The empty-state text for the element with id
"savings-by-service-empty" and classes "empty hidden" is misleading because it
displays when recommendations exist but none have positive savings; update the
text content to accurately reflect that case (for example, "No recommendations
with positive savings." or similar) so users understand recommendations exist
but no positive-savings items are available; modify the inner text of the <p
id="savings-by-service-empty"> element accordingly and keep existing
classes/visibility logic intact.
🪄 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: 6d46d3b7-950f-4836-bd5b-d87a16b2253f

📥 Commits

Reviewing files that changed from the base of the PR and between d986b4d and 0c3729b.

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

Comment thread frontend/src/__tests__/dashboard.test.ts
Comment thread frontend/src/dashboard.ts Outdated
Comment thread frontend/src/index.html Outdated
… copy + update test fixture

Guard rec.payment in computeServiceStatsFromRecs: when the field is absent or
whitespace-only the tooltip label previously rendered as "1yr undefined"; it now
falls back to "1yr unspecified". A new unit test asserts the fallback for both
undefined and empty-string payment values, and verifies the string "undefined"
never appears in the label.

Correct the savings-by-service empty-state paragraph: the previous text ("No
recommendations available yet.") was misleading because the element is also shown
when recommendations exist but none carry positive potential savings; the updated
copy ("No positive potential savings found for current recommendations.") matches
the actual emit condition.

Align the getDashboardSummary mock fixture in dashboard.test.ts with the current
DashboardSummary contract: drop the removed fields current_monthly_savings and
active_reservations, and add the current fields active_commitments and
committed_monthly.
@cristim

cristim commented May 28, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 28, 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.

@cristim
cristim merged commit c72601e into feat/multicloud-web-frontend May 28, 2026
6 checks passed
@cristim

cristim commented May 28, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request impact/all-users Affects every user priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant