Skip to content

feat(frontend): loading skeletons across data fetchers (#344 T3) - #346

Merged
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
feat/issue-344-T3-skeletons
May 13, 2026
Merged

feat(frontend): loading skeletons across data fetchers (#344 T3)#346
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
feat/issue-344-T3-skeletons

Conversation

@cristim

@cristim cristim commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

Issue #344 (UI revamp phase 2) — task T3 of 4: shimmer skeleton placeholders for every data-fetcher module.

Before this PR: dashboard / plans / history / recommendations panels sat blank during fetch; riexchange flashed plain "Loading…" text. After: every fetch renders a skeleton matching the target's eventual shape (KPI tiles for the dashboard, table rows for plans/history/recs/riexchange, block for upcoming-purchases). The success render replaces children for a clean handoff; the error path tears the skeleton down before rendering the error so a shimmer never sits next to a stale failure message.

What ships

  • frontend/src/lib/skeleton.tscreateElement-only helpers (skeletonBox, skeletonText, skeletonTile, skeletonRow) + show/teardown lifecycle (showSkeletonTiles, showSkeletonRows, showSkeletonBlock, teardownSkeleton, isSkeletonActive). data-skeleton-active marker on the container makes teardown idempotent and lets the error path detect a pending skeleton without disturbing already-rendered content.
  • frontend/src/styles/base.css.skeleton linear-gradient + @keyframes cudly-shimmer (1.6s ease-in-out infinite). prefers-reduced-motion drops the animation but keeps a static muted background so the "loading" affordance still reads visually.
  • Five module updatesdashboard.ts, plans.ts, history.ts, recommendations.ts, riexchange.ts — synchronous skeleton render at fetch start, teardown on error. The dashboard's error path also migrated from innerHTML string-template to createElement/textContent (XSS-safe by construction).

Lifecycle contract (per plan.md §T3)

  • Show: synchronously at fetch start. No "wait 100ms before showing" debounce — showing immediately is cheap and avoids flicker.
  • Hide on success: the render function replaces the container's children → implicit teardown.
  • Hide on error: each module's catch block calls teardownSkeleton(container) before rendering the <p class="error"> — verified by new regression tests in dashboard.test.ts.
  • Reduced motion: media query drops the shimmer, keeps the muted background.

Verification

  • Tests: 1630/1630 pass (1 skipped, unchanged). 13 new tests in skeleton.test.ts + 2 new dashboard skeleton-lifecycle regression tests.
  • Coverage: 80.93% stmts / 68.22% branches / 71.02% funcs / 82.70% lines — improved vs the post-T2 baseline (80.76 / 68.08 / 70.77 / 82.54).
  • Build: 522 KiB (was 519 KiB after T2; +0.6%). Well within the ±10% gate from the 512 KiB baseline.
  • Manual smoke: I'll re-validate in the docker-compose stack after a quick rebuild — every section's first load now shows the skeleton, then transitions to real content.

Not in this PR

  • T4 (Plan-builder drawer on Opportunities) — lands next.

Closes neither #344 nor any other issue on its own — T4 closes the phase-2 epic.

Summary by CodeRabbit

  • New Features

    • Animated loading skeleton placeholders added across dashboard, history, plans, recommendations, and RI exchange to show meaningful placeholders while data loads.
    • Skeletons are inserted immediately before fetches to reduce layout shift.
  • Bug Fixes

    • Skeletons are reliably removed on error or success to avoid lingering shimmer; error messages render safely without injecting HTML.
  • Tests

    • Comprehensive tests for skeleton rendering, lifecycle, and teardown.

Review Change Stack

Replace empty-panel-during-fetch with shimmer skeleton placeholders in
the 5 data-fetcher modules. Before: dashboard/plans/history/recs sat
blank while the (sometimes multi-second) Promise.all resolved, then
content popped in with no visual continuity. riexchange flashed plain
"Loading…" text. After: every fetch renders a skeleton in the target
container's shape (KPI tiles, table rows, block) at fetch start; the
success render replaces children for a clean handoff; the error path
calls teardownSkeleton before showing the error so a shimmer never
sits next to a stale error message.

- new: frontend/src/lib/skeleton.ts (createElement helpers + show/teardown
  lifecycle; uses data-skeleton-active marker so teardown is idempotent
  and safe to call from both success and error paths)
- new: skeleton.test.ts (13 tests for primitives + lifecycle)
- updated: dashboard.ts, plans.ts, history.ts, recommendations.ts,
  riexchange.ts — synchronous skeleton render before fetch, teardown
  on error
- updated: base.css — .skeleton shimmer keyframes + .skeleton-tile +
  prefers-reduced-motion drops animation, keeps static muted bg
- updated: dashboard.ts error path migrated from innerHTML string-
  templating to createElement/textContent (XSS-safe by construction)
- updated: dashboard.test.ts — 2 new regression tests for the
  skeleton lifecycle (error path tears down; success path replaces
  with real KPI tiles)

Verification: 1630/1630 tests pass / 1 skipped. Coverage 80.93% stmts /
82.70% lines (improved from 80.76 / 82.54 baseline). Build 522 KiB
(within ±10% of 512 baseline).
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a922da6f-312b-4aa4-8646-5842b1ad7be0

📥 Commits

Reviewing files that changed from the base of the PR and between 5e3f78b and 5708083.

📒 Files selected for processing (6)
  • frontend/src/__tests__/skeleton.test.ts
  • frontend/src/history.ts
  • frontend/src/lib/skeleton.ts
  • frontend/src/plans.ts
  • frontend/src/recommendations.ts
  • frontend/src/riexchange.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • frontend/src/history.ts
  • frontend/src/plans.ts
  • frontend/src/recommendations.ts
  • frontend/src/tests/skeleton.test.ts
  • frontend/src/lib/skeleton.ts

📝 Walkthrough

Walkthrough

This PR adds a skeleton UI system: DOM helper primitives and lifecycle, shimmer CSS, Jest tests, and integration of skeleton placeholders across dashboard, history, plans, recommendations, and RI exchange pages to show and teardown placeholders during async loads.

Changes

Loading Skeleton UI System

Layer / File(s) Summary
Skeleton library primitives and CSS
frontend/src/lib/skeleton.ts, frontend/src/styles/base.css
Exports skeleton element constructors (skeletonBox, skeletonText, skeletonTile, skeletonRow), container population helpers (showSkeletonTiles, showSkeletonRows, showSkeletonBlock), and lifecycle utilities (teardownSkeleton, isSkeletonActive). CSS adds shimmer animation keyframes, flex-column stacking, tile sizing, and reduced-motion override.
Skeleton primitives test suite
frontend/src/__tests__/skeleton.test.ts
Jest tests validate element rendering (structure, ARIA attributes, inline styles), line/row clamping behavior, active-marker lifecycle, non-destructive teardown, and safe replacement semantics across successive show calls.
Dashboard skeleton integration
frontend/src/dashboard.ts, frontend/src/__tests__/dashboard.test.ts
Dashboard imports skeleton helpers, renders summary tiles and upcoming-list skeleton before fetches, tears down on error (replacing unsafe innerHTML with safe textContent for error messages). Dashboard tests verify skeleton teardown on failure and tile rendering after success.
Table and list skeleton integration
frontend/src/history.ts, frontend/src/plans.ts, frontend/src/recommendations.ts, frontend/src/riexchange.ts
History displays 8-row skeleton; plans shows tile skeleton for plan list and row skeleton for planned purchases; recommendations renders table skeleton; RI exchange replaces static loading text with dynamic skeleton rows. All tear down skeleton UI before displaying errors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related issues

  • LeanerCloud/CUDly#344: "Loading skeletons" work item aligns directly with this PR's new skeleton helper module and integrations.

Possibly related PRs

  • LeanerCloud/CUDly#311: Both modify frontend/src/dashboard.ts's loadDashboard() flow and touch related loading/error-handling patterns.

Suggested labels

severity/low, impact/many, effort/m

Poem

"I hop and shimmy, tiny paws at play,
I build soft boxes while your loads delay,
Three tiles, five rows, a gentle glowing crest,
I tidy up crumbs when errors need rest.
— Your rabbit dev, cheering UI zest 🐰✨"

🚥 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 summarizes the main change: implementing loading skeleton placeholders across multiple frontend data-fetcher modules as part of the UI revamp.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-344-T3-skeletons

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

@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-quarter Within the quarter impact/all-users Affects every user effort/l Weeks type/feat New capability labels May 13, 2026
@cristim

cristim commented May 13, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 13, 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: 4

🧹 Nitpick comments (1)
frontend/src/plans.ts (1)

32-39: ⚡ Quick win

Move planned-purchases skeleton setup into loadPlannedPurchases().

Right now the skeleton is only shown when loadPlans() calls loadPlannedPurchases(). Direct calls to loadPlannedPurchases() (e.g., after actions) won’t show loading state.

🤖 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/plans.ts` around lines 32 - 39, The planned-purchases skeleton
setup currently lives only in the top-level initialization (next to plansList)
so it runs only when loadPlans() triggers loadPlannedPurchases(); move the logic
that finds the plannedContainer and calls showSkeletonRows(plannedContainer, 5,
11) into the start of loadPlannedPurchases() (and remove the duplicate from the
top-level init), ensuring loadPlannedPurchases() shows the skeleton whenever it
is invoked directly; keep the plans list skeleton (showSkeletonTiles) where it
is or move similarly if needed, and ensure any early exits in
loadPlannedPurchases() clear or hide the skeleton on error/after data render.
🤖 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/history.ts`:
- Around line 161-167: The skeleton columns passed to showSkeletonRows are
incorrect: the code in the history initializer calls showSkeletonRows(listEl, 8,
10) but renderHistoryList actually renders 11 columns, producing a layout
mismatch; update the call to showSkeletonRows(listEl, 8, 11) (locate the call
near the listEl = document.getElementById('history-list') block) so the skeleton
column count matches renderHistoryList's table column count.

In `@frontend/src/lib/skeleton.ts`:
- Around line 89-96: The showSkeletonTiles function (and the similar
showSkeletonRows / showSkeletonInline helpers) currently call
teardownSkeleton(container) which only removes children if a skeleton marker is
present, so on first render existing content can be left and skeletons appended;
modify these show* functions to always remove existing children before appending
skeletons — either call a new/updated teardownSkeleton that force-clears
children when invoked (no marker check) or explicitly clear container.innerHTML
/ removeChild loop at the start of showSkeletonTiles (and the other
showSkeleton* functions) so skeletons replace any existing content and then set
container.dataset['skeletonActive'] = '1'.

In `@frontend/src/recommendations.ts`:
- Around line 240-247: The skeleton is using a hardcoded cols=10 which can
mismatch the actual table; instead compute the column count dynamically from the
rendered recommendations table (e.g., count the header cells / visible columns
including the checkbox) and pass that value to showSkeletonRows. Locate the code
around listEl and showSkeletonRows in recommendations.ts, replace the fixed 10
with a runtime-derived cols (e.g., count of header <th> or first-row cells for
'#recommendations-list') so the skeleton rows match the table shape.

In `@frontend/src/riexchange.ts`:
- Around line 84-87: The skeleton column count passed to showSkeletonRows
doesn't match the actual rendered tables (they have 8 columns), so update each
call to showSkeletonRows used for the RI tables to pass 8 columns instead of 7;
specifically, find the showSkeletonRows invocations tied to renderRIsTable (and
the other RI table rendering) and change their second argument from 7 to 8 so
the skeleton shape aligns with the real table layout.

---

Nitpick comments:
In `@frontend/src/plans.ts`:
- Around line 32-39: The planned-purchases skeleton setup currently lives only
in the top-level initialization (next to plansList) so it runs only when
loadPlans() triggers loadPlannedPurchases(); move the logic that finds the
plannedContainer and calls showSkeletonRows(plannedContainer, 5, 11) into the
start of loadPlannedPurchases() (and remove the duplicate from the top-level
init), ensuring loadPlannedPurchases() shows the skeleton whenever it is invoked
directly; keep the plans list skeleton (showSkeletonTiles) where it is or move
similarly if needed, and ensure any early exits in loadPlannedPurchases() clear
or hide the skeleton on error/after data render.
🪄 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: 60e16177-3281-434f-9a65-418156485160

📥 Commits

Reviewing files that changed from the base of the PR and between 83b09bf and 5e3f78b.

📒 Files selected for processing (9)
  • frontend/src/__tests__/dashboard.test.ts
  • frontend/src/__tests__/skeleton.test.ts
  • frontend/src/dashboard.ts
  • frontend/src/history.ts
  • frontend/src/lib/skeleton.ts
  • frontend/src/plans.ts
  • frontend/src/recommendations.ts
  • frontend/src/riexchange.ts
  • frontend/src/styles/base.css

Comment thread frontend/src/history.ts
Comment thread frontend/src/lib/skeleton.ts
Comment thread frontend/src/recommendations.ts
Comment thread frontend/src/riexchange.ts Outdated
@cristim

cristim commented May 13, 2026

Copy link
Copy Markdown
Member Author

CI pre-commit run 25797170214 passed on commit 5e3f78b. All checks green.

CodeRabbit pass-1 review surfaced 4 actionable issues + 1 nitpick on
the loading-skeleton landing. Fix all 5:

1. lib/skeleton.ts: show* helpers were calling teardownSkeleton, which
   only wipes children when the marker is already set. On the FIRST
   render (no marker yet), a container that already held real content
   would end up with skeletons appended below the real content rather
   than replacing it. Factor activateSkeleton() that unconditionally
   clears children + sets the marker; route all show* helpers through
   it. Add 3 regression tests covering this exact scenario.

2. history.ts:167: skeleton column count was 10 but the rendered
   history table has 11 columns (Status / Date / Provider / Service /
   Type / Region / Count / Term / Upfront Cost / Monthly Savings /
   Plan). Bump to 11.

3. recommendations.ts:247: hardcoded cols=10 didn't match the actual
   rendered table (checkbox column + dynamic visible-data columns).
   Compute `1 + visibleColumns().length` so toggling Columns ▾ keeps
   the skeleton aligned with the eventual table shape.

4. riexchange.ts: both showSkeletonRows calls used incorrect column
   counts (7 and 5) while both rendered tables have 8 columns. Align
   both to 8.

5. plans.ts (nitpick): planned-purchases skeleton moved out of
   loadPlans into loadPlannedPurchases so direct callers of that fetch
   (refresh after a single action) also get the loading affordance.

Verification: 1633/1634 tests pass / 1 skipped (3 new skeleton
regression tests added). Coverage 80.93% / 82.70% (parity). Build 522
KiB (unchanged).
@cristim

cristim commented May 13, 2026

Copy link
Copy Markdown
Member Author

Pushed fix 5708083d addressing all 4 actionable CR findings + the 1 nitpick from pass-1:

  1. lib/skeleton.ts — show* helpers now unconditionally clear container children (factored activateSkeleton helper) so a first render with pre-existing real content gets replaced cleanly instead of having skeletons appended below. Added 3 regression tests covering this exact scenario.
  2. history.ts — skeleton col count 10 → 11 (matches the 11-th <th> in renderHistoryList).
  3. recommendations.ts — hardcoded cols=10 replaced with 1 + visibleColumns().length so toggling Columns ▾ keeps the skeleton aligned with the eventual table.
  4. riexchange.ts — both showSkeletonRows calls realigned to 8 cols (both tables render 8 columns).
  5. plans.ts (nitpick) — planned-purchases skeleton moved into loadPlannedPurchases so direct callers also get the loading affordance.

Verification: 1633/1634 tests pass / 1 skipped (3 new regression tests). Coverage 80.93% / 82.70% (parity). Build 522 KiB unchanged.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

@cristim Thanks for the thorough follow-up! All five fixes look well-scoped:

  • The activateSkeleton refactor in lib/skeleton.ts is a clean resolution — unconditional clearing prevents the append-below bug regardless of prior container state.
  • The column-count corrections (history.ts → 11, riexchange.ts → 8, recommendations.ts → dynamic via visibleColumns()) will keep skeletons visually aligned with the real tables.
  • Moving the planned-purchases skeleton into loadPlannedPurchases directly is the right call for reusability.

Triggering a fresh review pass on commit 5708083d 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 commented May 13, 2026

Copy link
Copy Markdown
Member Author

CR pass-2 clean. CodeRabbit acked all five fixes on commit 5708083d and the follow-up incremental review found nothing new to flag (status events: Review in progress → Review completed → Review skipped). PR is ready for human merge.

@cristim
cristim merged commit 4e2c57f into feat/multicloud-web-frontend May 13, 2026
6 checks passed
@cristim
cristim deleted the feat/issue-344-T3-skeletons branch June 3, 2026 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/l Weeks impact/all-users Affects every user priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/feat New capability urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant