Skip to content

feat(frontend/recs): per-column show/hide with Columns toolbar dropdown#326

Merged
cristim merged 8 commits into
feat/multicloud-web-frontendfrom
feat/recs-column-visibility
May 11, 2026
Merged

feat(frontend/recs): per-column show/hide with Columns toolbar dropdown#326
cristim merged 8 commits into
feat/multicloud-web-frontendfrom
feat/recs-column-visibility

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented May 6, 2026

Summary

  • Adds a "Columns ▾" button in the recommendations filter-status bar that opens a portal popover for per-column show/hide toggling
  • 8 toggleable columns: Region, Count, Term, Payment, Monthly Savings, Upfront Cost, Monthly Cost, Effective %; the 4 fixed "cell identity anchors" (Provider, Account, Service, Resource Type) are always shown (v1 — see feat(frontend/recs): per-column show/hide for the recommendations table #318 for v2 roadmap)
  • Persists selections to localStorage with a versioned schema (cudly.recs.columnVisibility.v1); unknown/stale keys are silently filtered for forward compatibility
  • Sort-hidden indicator in the filter-status bar warns when the active sort column is currently hidden
  • Full keyboard accessibility: Escape closes and returns focus; Enter/Space opens; click-outside dismisses

Implementation details

  • state.ts: hiddenColumns Set + getHiddenColumns / setHiddenColumns
  • TOGGLEABLE_COLUMNS / TOGGLEABLE_COLUMN_KEYS derived from COLUMN_DEFS (Commit 1 refactor)
  • loadColumnVisibility / saveColumnVisibility with whitelisting guard
  • resetColumnVisibilityState() exported for test isolation
  • visibleColumns() computed once per render, passed to buildListMarkup + buildVariantRowMarkup
  • summaryColspan = 1 + visible-toggleable-count (replaces hardcoded TABLE_COL_COUNT - 4)
  • mountColumnsButton: mount-once lifecycle, label shows "(N hidden)", aria-pressed reflects active state; re-syncs open-popover checkboxes on each render
  • 14 new tests via localStorageMock covering all load/save edge-cases

Test plan

  • npm run build passes with no TS errors
  • npm test -- --testPathPattern=recommendations — 181 tests pass
  • Manual: open the app, verify "Columns ▾" button appears above the table
  • Manual: toggle a column hidden — verify it disappears from header and rows
  • Manual: reload the page — verify hidden columns persist from localStorage
  • Manual: hide the sort column — verify sort-hidden note appears
  • Manual: Escape key closes popover and returns focus to the button
  • Manual: click-outside closes popover

Closes #318

Summary by CodeRabbit

  • New Features

    • Toggle individual column visibility in the recommendations table; key identity columns remain fixed
    • Visibility preferences persist across sessions and can be reset
    • Column visibility popover in the filter/status bar with hidden-column count and a note when sorting by a hidden column
    • Headers, rows and multi-variant summary cells adapt to currently visible columns
  • Style

    • UI styles for the visibility button, popover, and related states
  • Tests

    • Coverage for persistence, malformed data handling, fixed vs toggleable columns, and hidden-column rendering in grouped rows

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e2590a68-6f24-4f5e-88a8-4470aab0dcaa

📥 Commits

Reviewing files that changed from the base of the PR and between be348de and b665e1b.

📒 Files selected for processing (4)
  • frontend/src/__tests__/recommendations.test.ts
  • frontend/src/recommendations.ts
  • frontend/src/state.ts
  • frontend/src/styles/components.css
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/src/styles/components.css
  • frontend/src/state.ts
  • frontend/src/tests/recommendations.test.ts

📝 Walkthrough

Walkthrough

Adds per-column show/hide to the recommendations table: ColumnDef model, in-memory hidden-columns accessors, versioned localStorage load/save/reset APIs, a visibility popover UI, render updates honoring visible columns, CSS, and tests verifying behavior.

Changes

Column Visibility Feature

Layer / File(s) Summary
Data Shape & Column Model
frontend/src/recommendations.ts
ColumnDef, COLUMN_DEFS, and TOGGLEABLE_COLUMNS added; numeric-kind derived from COLUMN_DEFS; old static lists removed.
Cell Rendering & Row Assembly
frontend/src/recommendations.ts
Added renderColumnCell, updated buildVariantRowMarkup and buildListMarkup to accept visible ColumnDef lists for aligned header/row rendering.
Summary & Header Adjustments
frontend/src/recommendations.ts
Compute visibleToggleableCols, dynamic summaryColspan, conditional multi-variant summary composition, and generate sortable <th> from visibleCols.
Persistence Layer
frontend/src/recommendations.ts
loadColumnVisibility, saveColumnVisibility, resetColumnVisibilityState implemented with versioned localStorage schema and validation.
Render Pipeline Integration
frontend/src/recommendations.ts
Seed hidden-column state on first render, compute visibleCols once per render, and pass into buildListMarkup.
Visibility UI & Popover
frontend/src/recommendations.ts
Mount-once "Columns ▾" button, popover open/close and outside/Esc handling, checkbox list from TOGGLEABLE_COLUMNS, and hidden-count label; show note when sort column is hidden.
State Management
frontend/src/state.ts
Add hiddenColumns set, getHiddenColumns() and setHiddenColumns() which filter out fixed anchor columns.
Styling
frontend/src/styles/components.css
Add .column-visibility-btn, .column-visibility-popover, .sort-hidden-note styles; tidy adjacent hover block.
Tests & Validation
frontend/src/__tests__/recommendations.test.ts
Import new APIs/metadata, mock getHiddenColumns/setHiddenColumns, reset mocks, add DOM assertions for hidden columns in multi-variant summaries, and a Column visibility test suite covering load/save/reset and schema/shape validation.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as Visibility Popover
    participant State as State Module
    participant Storage as localStorage
    participant Render as Render Engine

    User->>UI: Click "Columns" button
    UI->>State: getHiddenColumns()
    State-->>UI: current hidden set
    UI->>User: Display popover with toggles

    User->>UI: Toggle checkbox
    UI->>State: setHiddenColumns(updated set)
    State->>Storage: saveColumnVisibility(updated set)
    Storage-->>State: persisted

    UI->>Render: Request re-render
    Render->>State: getHiddenColumns()
    State-->>Render: current hidden set
    Render->>Render: Filter COLUMN_DEFS → visibleCols
    Render->>User: Updated table with visibleCols
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #165: Implements the same per-column visibility feature (toggle UI, localStorage persistence, render filtering) and could be linked to this PR.

Possibly related PRs

  • LeanerCloud/CUDly#322: Touches the same recommendations column model and state; related changes overlap in frontend/src/recommendations.ts and frontend/src/state.ts.
  • LeanerCloud/CUDly#242: Related—modifies column behavior and helpers that interact with the new COLUMN_DEFS model.
  • LeanerCloud/CUDly#160: Related—refactors recommendations rendering and introduces other per-column client-side behaviors that intersect with visibility state.

Poem

🐰 I hopped by the table, all columns in view,
A tiny menu whispered, "Hide what you don't pursue."
With checkboxes ticked and defaults kept near,
Rows breathed easier — the view stayed clear.
A carrot-sized cheer for tidy tables here!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main feature: per-column visibility toggling via a Columns toolbar dropdown in the recommendations table.
Linked Issues check ✅ Passed The implementation satisfies all coding requirements from issue #318: column show/hide UI, hidden column removal from DOM, sort/filter preservation, localStorage persistence with schema versioning, keyboard accessibility, and unified column definitions.
Out of Scope Changes check ✅ Passed All changes align with issue #318 scope: column visibility UI, DOM removal of hidden columns, persistence, and keyboard accessibility. Out-of-scope items (drag-to-reorder, per-cloud defaults, per-view persistence) are not addressed.

✏️ 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/recs-column-visibility

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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/low Minor harm urgency/this-quarter Within the quarter impact/many Affects most users effort/m Days type/feat New capability labels May 6, 2026
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 6, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

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

2068-2121: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Hidden toggleable columns still appear in collapsed summary rows

Line 2120 and Line 2121 always render Region / Savings / Upfront / Term text even when those columns are hidden, so hide/show is not consistently applied across all row <tr> variants.

Proposed fix
   const visibleToggleableCols = visibleCols.filter((c) => TOGGLEABLE_COLUMN_KEYS.has(c.key));
   const summaryColspan = 1 + visibleToggleableCols.length;
+  const visibleKeys = new Set(visibleCols.map((c) => c.key));

@@
-    rows.push(`
+    const identityParts: string[] = [
+      `${escapeHtml(rep.resource_type)}${rep.engine ? ` (${escapeHtml(rep.engine)})` : ''}`,
+    ];
+    if (visibleKeys.has('region')) identityParts.push(escapeHtml(rep.region));
+    identityParts.push(`${variants.length} variants`);
+
+    const rangeParts: string[] = [];
+    if (visibleKeys.has('savings')) rangeParts.push(`${savingsDisplay}/mo`);
+    if (visibleKeys.has('upfront_cost')) rangeParts.push(`upfront: ${upfrontDisplay}`);
+    if (visibleKeys.has('term')) rangeParts.push(`term: ${termDisplay}`);
+
+    rows.push(`
   <tr class="rec-cell-summary-row" data-cell-key="${escapeHtml(key)}">
@@
     <td colspan="${summaryColspan}" class="rec-cell-summary-content">
-      <span class="rec-cell-identity">${escapeHtml(rep.resource_type)}${rep.engine ? ` (${escapeHtml(rep.engine)})` : ''} &mdash; ${escapeHtml(rep.region)} &mdash; ${variants.length} variants</span>
-      <span class="rec-cell-range">${savingsDisplay} &middot; upfront: ${upfrontDisplay} &middot; term: ${termDisplay}</span>
+      <span class="rec-cell-identity">${identityParts.join(' &mdash; ')}</span>
+      ${rangeParts.length > 0 ? `<span class="rec-cell-range">${rangeParts.join(' &middot; ')}</span>` : ''}
     </td>
   </tr>`);
🤖 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 2068 - 2121, Summary: The
collapsed summary row always renders region/savings/upfront/term text regardless
of column visibility; update rec-cell-summary-content rendering to respect
visibleCols/TOGGLEABLE_COLUMN_KEYS and summaryColspan. Fix: inside the
multi-variant branch where you build the summary row (code using variables
summaryColspan, summary, savingsDisplay, upfrontDisplay, termDisplay, chevron,
and rep), conditionally include the region span and the rec-cell-range elements
only if their corresponding column keys are present in visibleCols (use
visibleCols.some(c => c.key === '<key>') or TOGGLEABLE_COLUMN_KEYS.has(...) on
the right keys), and recompute summaryColspan based on visibleToggleableCols so
colspan matches; ensure selectedVariant logic is preserved but only injects
savings/upfront/term markup when those columns are visible.
🧹 Nitpick comments (1)
frontend/src/state.ts (1)

167-169: 💤 Low value

Return a defensive copy for consistency with other getters.

Other state getters in this file (getSelectedRecommendationIDs, getVisibleRecommendations, getRecommendationsColumnFilters) return defensive copies to prevent callers from mutating internal state. getHiddenColumns returns the internal Set reference directly. While ReadonlySet prevents TypeScript-level mutations, at runtime a caller could bypass this via casting.

Suggested fix
 export function getHiddenColumns(): ReadonlySet<RecommendationsColumnId> {
-  return hiddenColumns;
+  return new Set(hiddenColumns);
 }
🤖 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/state.ts` around lines 167 - 169, getHiddenColumns currently
returns the internal Set reference (hiddenColumns) which can be mutated at
runtime; change it to return a defensive copy by constructing and returning a
new Set from hiddenColumns (while preserving the declared return type
ReadonlySet<RecommendationsColumnId>) so callers receive a copy and cannot
mutate internal state; update the return in getHiddenColumns to return a new Set
created from hiddenColumns.
🤖 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/recommendations.ts`:
- Around line 1458-1548: The tab deactivation MutationObserver currently only
calls closePopover(), leaving the visibility popover (managed by
openVisibilityPopover and closeVisibilityPopover) open after navigation; update
the tab deactivation/observer callback to also call closeVisibilityPopover()
wherever closePopover() is invoked so the visibility popover is closed and its
listeners removed when the recommendations tab deactivates. Ensure you reference
the openVisibilityPopover and closeVisibilityPopover symbols when making the
change so the visibility popover lifecycle is cleaned up consistently.

---

Outside diff comments:
In `@frontend/src/recommendations.ts`:
- Around line 2068-2121: Summary: The collapsed summary row always renders
region/savings/upfront/term text regardless of column visibility; update
rec-cell-summary-content rendering to respect visibleCols/TOGGLEABLE_COLUMN_KEYS
and summaryColspan. Fix: inside the multi-variant branch where you build the
summary row (code using variables summaryColspan, summary, savingsDisplay,
upfrontDisplay, termDisplay, chevron, and rep), conditionally include the region
span and the rec-cell-range elements only if their corresponding column keys are
present in visibleCols (use visibleCols.some(c => c.key === '<key>') or
TOGGLEABLE_COLUMN_KEYS.has(...) on the right keys), and recompute summaryColspan
based on visibleToggleableCols so colspan matches; ensure selectedVariant logic
is preserved but only injects savings/upfront/term markup when those columns are
visible.

---

Nitpick comments:
In `@frontend/src/state.ts`:
- Around line 167-169: getHiddenColumns currently returns the internal Set
reference (hiddenColumns) which can be mutated at runtime; change it to return a
defensive copy by constructing and returning a new Set from hiddenColumns (while
preserving the declared return type ReadonlySet<RecommendationsColumnId>) so
callers receive a copy and cannot mutate internal state; update the return in
getHiddenColumns to return a new Set created from hiddenColumns.
🪄 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: edcab9ca-4520-4c48-9f44-aea3cb656a7a

📥 Commits

Reviewing files that changed from the base of the PR and between 7032056 and 3296a1f.

📒 Files selected for processing (4)
  • frontend/src/__tests__/recommendations.test.ts
  • frontend/src/recommendations.ts
  • frontend/src/state.ts
  • frontend/src/styles/components.css

Comment thread frontend/src/recommendations.ts
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 6, 2026

Addressed the latest CodeRabbit comments in 6eeb34b:

  • Collapsed recommendation summary rows now only include values for visible columns.
  • Tab deactivation closes both filter and column visibility popovers.
  • Hidden-column state getter now returns a defensive copy.

Verification:

  • npm run typecheck
  • npx jest recommendations.test.ts --runInBand --watchman=false --coverage=false
  • npm test -- --runInBand --watchman=false
  • npm run build
  • SKIP=terraform_validate pre-commit run --all-files

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Tip

For best results, initiate chat on the files or code changes.

@cristim: Thanks for the thorough update and verification! The three fixes look well-considered:

  • Collapsed summary rows — scoping values to visible columns ensures the colspan math stays consistent with the rendered header.
  • Tab deactivation — closing both popovers on tab switch prevents stale UI state when switching tabs.
  • Defensive copy in getHiddenColumns — returning a copy of the Set prevents external callers from mutating internal state directly, which is a good defensive practice.

Triggering a fresh review of the latest commit now.

[review]

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 6, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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__/recommendations.test.ts`:
- Line 3627: The test sets a sticky mock implementation for
state.getHiddenColumns via (state.getHiddenColumns as
jest.Mock).mockReturnValue(...) which isn't cleared by jest.clearAllMocks();
update the describe block's beforeEach to explicitly reset or re-mock
getHiddenColumns to return a new empty Set (e.g., call (state.getHiddenColumns
as jest.Mock).mockReturnValue(new Set()) or .mockReset() and then
.mockReturnValue(...)) so loadRecommendations() and other tests don't inherit
the previous sticky return value.

In `@frontend/src/recommendations.ts`:
- Around line 1545-1549: The deferred setTimeout may try to add a null
visOutsideClickHandler because closeVisibilityPopover() can clear it before the
callback runs; within openVisibilityPopover_ capture the current
visOutsideClickHandler into a local const (e.g., const handler =
visOutsideClickHandler) and use that captured reference inside the setTimeout
callback, and guard adding only if handler is non-null before calling
document.addEventListener; this preserves the intended tick delay while avoiding
the race and the non-null assertion.

In `@frontend/src/state.ts`:
- Around line 171-172: setHiddenColumns currently accepts any
RecommendationsColumnId which allows callers to hide fixed anchor columns;
change it to enforce the same whitelist used by loadColumnVisibility by either
(a) narrowing the parameter type to the existing ToggleableColumnId (or create
one) so callers cannot pass fixed columns, or (b) filter the incoming hidden set
inside setHiddenColumns to remove provider, account, service, and resource_type
before assigning hiddenColumns; update the function signature or perform the
filter and keep the assignment to hiddenColumns = new Set(filtered) so fixed
columns remain visible.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: efa60bc2-361a-4e0b-be9c-f641b47cfaa8

📥 Commits

Reviewing files that changed from the base of the PR and between 3296a1f and 6eeb34b.

📒 Files selected for processing (3)
  • frontend/src/__tests__/recommendations.test.ts
  • frontend/src/recommendations.ts
  • frontend/src/state.ts

Comment thread frontend/src/__tests__/recommendations.test.ts
Comment thread frontend/src/recommendations.ts
Comment thread frontend/src/state.ts Outdated
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 3 file(s) based on 3 unresolved review comments.

Files modified:

  • frontend/src/__tests__/recommendations.test.ts
  • frontend/src/recommendations.ts
  • frontend/src/state.ts

Commit: 863581620c394dc82a0c2c774c68521531350a7b

The changes have been pushed to the feat/recs-column-visibility branch.

Time taken: 3m 50s

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 6, 2026

Rebased onto current feat/multicloud-web-frontend (which now includes #322's on_demand_monthly column). Conflicts were:

  • recommendations.ts: 4 conflict regions where the column-array consolidation refactor met the new column-map entries from feat(frontend/recs): add 'On-Demand Monthly' column to recommendations table #322. Resolved by adding on_demand_monthly to COLUMN_DEFS, dropping the obsolete FILTERABLE_COLUMNS static array (now derived), and adding an on_demand_monthly case to renderColumnCell (uses onDemandMonthly(rec) directly instead of pre-computing odm/odmText in buildVariantRowMarkup).
  • recommendations.test.ts: import-line conflict, kept both new symbol sets. Updated the COLUMN_DEFS.length assertion from 12 → 13 to account for the new column.

Re: the 3 actionable findings from the prior review — verified each against current code; all already addressed:

  1. Visibility popover not closed on tab deactivaterecommendations.ts:1492-1499 already calls both closePopover() and closeVisibilityPopover() when the tab loses active.
  2. Race condition on visOutsideClickHandlerrecommendations.ts:1606-1611 already captures the handler in a closure const + null-checks before addEventListener.
  3. setHiddenColumns invariantstate.ts:171-176 already filters out the fixed anchor columns (provider/account/service/resource_type) before assigning to hiddenColumns.

Plus the test sticky-mock concern — recommendations.test.ts:3935-3936 already resets state.getHiddenColumns to new Set() in beforeEach after jest.clearAllMocks().

Full suite: 1551/1551 passing. Build clean.

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Tip

For best results, initiate chat on the files or code changes.

@cristim: Thanks for the detailed rebase summary — the conflict resolutions look clean and consistent with the column-driven design:

  • on_demand_monthly in COLUMN_DEFS — adding it here keeps the single source of truth intact and avoids a separate static array entry.
  • Dropping FILTERABLE_COLUMNS static array — deriving it from COLUMN_DEFS is the right call; it eliminates a future divergence risk when more columns are added.
  • renderColumnCell case for on_demand_monthly — using onDemandMonthly(rec) directly (rather than pre-computing in buildVariantRowMarkup) is consistent with how other columns are handled in the cell renderer.
  • COLUMN_DEFS.length assertion 12 → 13 — straightforward update to match the new column count.

Good to see the three prior findings are confirmed present in current code, and the test beforeEach reset guards against sticky mock state across the column-visibility suite. Triggering a fresh review now.

[review]

@cristim cristim force-pushed the feat/recs-column-visibility branch from 4641b47 to 1a1e78e Compare May 6, 2026 20:21
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 6, 2026

Rebased again — #328 (cost-period selector) merged into base, which created fresh conflicts. Resolved across 3 files:

frontend/src/recommendations.ts

  • COLUMN_DEFS metadata kept; STATIC_COLUMN_LABELS is now derived from it (filtering out the 3 period-varying cost columns: savings, monthly_cost, on_demand_monthly) so the source-of-truth still drives display while getColumnLabel() keeps owning the per-period suffix logic.
  • renderColumnCell is now period-aware: period: CostPeriod threaded through ctx, and the savings / monthly_cost / on_demand_monthly cases call formatCostForPeriod(value, ctx.period) instead of raw formatCurrency. upfront_cost stays period-invariant.
  • buildVariantRowMarkup resolves the period once per row and passes it via ctx (avoids re-reading state.getCostPeriod() on every cell).
  • The visibility filter-status-bar block now mounts BOTH the cost-period selector (feat(frontend/recs): cost-period selector (hourly/daily/monthly/yearly), default monthly #319) AND the "Columns ▾" button (feat(frontend/recs): per-column show/hide for the recommendations table #318); sort-hidden indicator uses getColumnLabel(sortCol, currentPeriod) so the note label reflects the active period.

frontend/src/state.ts

  • Cost-period state (#319) and column-visibility state (#318) are both kept as separate, orthogonal sections.

frontend/src/__tests__/recommendations.test.ts

Note: the prior fixup commit fix(test): update COLUMN_DEFS-length assertion was dropped during rebase ("patch contents already upstream") — the same change is now part of the visibility-feature commit instead.

Full suite: 1592/1592 passing. Build clean.

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Tip

For best results, initiate chat on the files or code changes.

@cristim: The rebase resolutions look clean and well-reasoned:

  • STATIC_COLUMN_LABELS derived from COLUMN_DEFS — filtering out the 3 period-varying cost columns (savings, monthly_cost, on_demand_monthly) is the right call; it keeps COLUMN_DEFS as the single source of truth while correctly deferring per-period label suffixes to getColumnLabel().
  • Period threaded through ctx — resolving getCostPeriod() once per row in buildVariantRowMarkup and passing it via ctx is a clean optimization; it avoids repeated state reads per cell and makes the data flow explicit.
  • upfront_cost staying period-invariant — correct, since upfront is a one-time cost that doesn't scale with monthly/annual period selection.
  • sort-hidden indicator using getColumnLabel(sortCol, currentPeriod) — good catch; the hidden-column note label should reflect the active period so it's accurate (e.g., "Monthly Savings" vs "Annual Savings").
  • Orthogonal state sections in state.ts — keeping cost-period (#319) and column-visibility (#318) as separate sections is good separation of concerns and avoids entanglement between the two features.
  • Coexisting mock factories and test suites — 1592/1592 passing with both feature mock sets in place is a solid signal.

Triggering a fresh review now.

[review]

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 7, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
frontend/src/recommendations.ts (1)

1756-1761: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent stale outside-click handler attachment after fast close.

At Line 1757, the deferred callback can still attach an old handler after closeVisibilityPopover() ran, which leaves an untracked document listener and can make later popovers close unexpectedly.

Proposed fix
   const handler = visOutsideClickHandler;
   setTimeout(() => {
-    if (handler) {
+    if (handler && openVisibilityPopover && visOutsideClickHandler === handler) {
       document.addEventListener('mousedown', handler);
     }
   }, 0);
🤖 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 1756 - 1761, The deferred
attachment may add a stale visOutsideClickHandler after closeVisibilityPopover()
runs; to fix, capture the handler as you do (visOutsideClickHandler) but before
calling document.addEventListener verify that the current visOutsideClickHandler
still equals that captured handler (or that the popover is still open) so you
don't attach an outdated listener—update the setTimeout callback in the block
that sets handler to perform this equality/check against visOutsideClickHandler
(and ensure closeVisibilityPopover clears visOutsideClickHandler when closing).
🧹 Nitpick comments (2)
frontend/src/__tests__/recommendations.test.ts (1)

4752-4767: ⚡ Quick win

Tighten the toggleable-columns assertion to pin the full contract.

This test currently checks exclusions and several inclusions, but not the full set size or on_demand_monthly. Pinning the exact set will catch future drift/rebase misses earlier.

Suggested test hardening
   test('TOGGLEABLE_COLUMNS excludes fixed identity columns', () => {
     const keys = TOGGLEABLE_COLUMNS.map((c) => c.key);
@@
       expect(keys).toContain('monthly_cost');
+      expect(keys).toContain('on_demand_monthly');
       expect(keys).toContain('effective_savings_pct');
+      expect(keys.length).toBe(9);
   });
🤖 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__/recommendations.test.ts` around lines 4752 - 4767, The
test 'TOGGLEABLE_COLUMNS excludes fixed identity columns' currently asserts a
few inclusions/exclusions but not the full contract; update it to assert the
exact toggleable keys set by building an expectedKeys array (including
on_demand_monthly plus the other toggleables like region, count, term, payment,
savings, upfront_cost, monthly_cost, effective_savings_pct) and then assert
equality with TOGGLEABLE_COLUMNS.map(c => c.key) (either by comparing sorted
arrays or checking length and set equality) so the test pins the full contract
for TOGGLEABLE_COLUMNS.
frontend/src/recommendations.ts (1)

1660-1663: ⚡ Quick win

Sync aria-expanded/aria-haspopup for the Columns trigger.

The popover trigger currently uses aria-pressed, but it doesn’t expose open/closed dialog state. Adding aria-haspopup="dialog" and toggling aria-expanded on open/close improves keyboard/screen-reader behavior.

Proposed refactor
 interface VisibilityPopoverState {
   el: HTMLDivElement;
   checkboxes: Map<state.RecommendationsColumnId, HTMLInputElement>;
+  trigger: HTMLElement;
 }
@@
 function closeVisibilityPopover(): void {
   if (!openVisibilityPopover) return;
-  openVisibilityPopover.el.remove();
+  const { el, trigger } = openVisibilityPopover;
+  el.remove();
+  trigger.setAttribute('aria-expanded', 'false');
   openVisibilityPopover = null;
@@
 function openVisibilityPopover_(anchor: HTMLElement): void {
@@
-  openVisibilityPopover = { el: popover, checkboxes };
+  openVisibilityPopover = { el: popover, checkboxes, trigger: anchor };
+  anchor.setAttribute('aria-expanded', 'true');
   positionPopover(popover, anchor);
@@
   if (!btn) {
@@
     btn = document.createElement('button');
     btn.type = 'button';
     btn.className = 'column-visibility-btn';
+    btn.setAttribute('aria-haspopup', 'dialog');
+    btn.setAttribute('aria-expanded', 'false');
     bar.appendChild(btn);

Also applies to: 1669-1681, 1683-1697, 1766-1787

🤖 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 1660 - 1663, The Columns
popover trigger should expose dialog state to assistive tech: on the popover
trigger element (the element associated with VisibilityPopoverState.el / the
Columns trigger in frontend/src/recommendations.ts) add aria-haspopup="dialog"
and ensure aria-expanded is set to "true" when the popover opens and "false"
when it closes; update the open/close/toggle handlers that show the popover
(e.g., the functions that create/show and hide the VisibilityPopoverState.el or
the toggle handler currently using aria-pressed) to set aria-expanded
accordingly (and you may stop relying on aria-pressed for open state if
present). Ensure both the initial render of the trigger and every path that
hides/shows the popover update aria-expanded so screen readers receive
consistent state.
🤖 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/styles/components.css`:
- Around line 907-910: The .recommendations-filter-status .sort-hidden-note rule
uses low-contrast color `#888` and small font-size; update the CSS to use a darker
color token/value (e.g., `#666` or your design system's dark-muted token) so the
note meets readability/accessibility contrast requirements while keeping the
existing font-size and font-style; locate the rule for
.recommendations-filter-status .sort-hidden-note and replace the color value
accordingly.

---

Duplicate comments:
In `@frontend/src/recommendations.ts`:
- Around line 1756-1761: The deferred attachment may add a stale
visOutsideClickHandler after closeVisibilityPopover() runs; to fix, capture the
handler as you do (visOutsideClickHandler) but before calling
document.addEventListener verify that the current visOutsideClickHandler still
equals that captured handler (or that the popover is still open) so you don't
attach an outdated listener—update the setTimeout callback in the block that
sets handler to perform this equality/check against visOutsideClickHandler (and
ensure closeVisibilityPopover clears visOutsideClickHandler when closing).

---

Nitpick comments:
In `@frontend/src/__tests__/recommendations.test.ts`:
- Around line 4752-4767: The test 'TOGGLEABLE_COLUMNS excludes fixed identity
columns' currently asserts a few inclusions/exclusions but not the full
contract; update it to assert the exact toggleable keys set by building an
expectedKeys array (including on_demand_monthly plus the other toggleables like
region, count, term, payment, savings, upfront_cost, monthly_cost,
effective_savings_pct) and then assert equality with TOGGLEABLE_COLUMNS.map(c =>
c.key) (either by comparing sorted arrays or checking length and set equality)
so the test pins the full contract for TOGGLEABLE_COLUMNS.

In `@frontend/src/recommendations.ts`:
- Around line 1660-1663: The Columns popover trigger should expose dialog state
to assistive tech: on the popover trigger element (the element associated with
VisibilityPopoverState.el / the Columns trigger in
frontend/src/recommendations.ts) add aria-haspopup="dialog" and ensure
aria-expanded is set to "true" when the popover opens and "false" when it
closes; update the open/close/toggle handlers that show the popover (e.g., the
functions that create/show and hide the VisibilityPopoverState.el or the toggle
handler currently using aria-pressed) to set aria-expanded accordingly (and you
may stop relying on aria-pressed for open state if present). Ensure both the
initial render of the trigger and every path that hides/shows the popover update
aria-expanded so screen readers receive consistent state.
🪄 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: 8491b282-a5e2-4a18-a952-4d8b09eb1712

📥 Commits

Reviewing files that changed from the base of the PR and between 6eeb34b and 1a1e78e.

📒 Files selected for processing (4)
  • frontend/src/__tests__/recommendations.test.ts
  • frontend/src/recommendations.ts
  • frontend/src/state.ts
  • frontend/src/styles/components.css
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/state.ts

Comment thread frontend/src/styles/components.css
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 11, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 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 added a commit that referenced this pull request May 11, 2026
- recommendations.ts: in openVisibilityPopover_, the deferred
  document.addEventListener could attach a stale outside-click handler
  if closeVisibilityPopover() ran in the same tick (clearing
  visOutsideClickHandler before the setTimeout callback fired). Guard
  the attach with a re-check that the popover is still open and the
  live handler still equals the captured one.
- recommendations.test.ts: pin the full TOGGLEABLE_COLUMNS contract
  (include on_demand_monthly and assert length === 9) so future column
  additions can't quietly drift past the test.

Addresses CodeRabbit feedback on PR #326.
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 11, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 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 added a commit that referenced this pull request May 11, 2026
PR #333 (closes #332) landed a botched merge of `unset_falls_through`
in TestNewSenderFromEnvironment_EmailEnabled — two overlapping copies
of the same sub-test ended up concatenated (one missing its closing
braces, one preceded by stray fragments), plus duplicate
`ctx :=` / `sender, err :=` lines. The result didn't parse:

  gofmt -l internal/email/factory_test.go
  internal/email/factory_test.go:309:2: missing ',' before newline ...
  internal/email/factory_test.go:312:6: expected '(', found TestNewSenderWithConfig_AWS

This trips `gofmt` and `go vet` in the pre-commit workflow on every
open PR against `feat/multicloud-web-frontend` (e.g. #326, #335, #336).

Keep the `prev/hadPrev` version of the sub-test (the one that actually
does `os.Unsetenv` first, which is the case the test name describes),
drop the orphaned `orig/hadOrig` fragment, and remove the duplicate
ctx/sender declarations.

Verified locally: gofmt clean, `go vet ./internal/email/...` clean,
`go test ./internal/email/...` 306 tests pass.
cristim and others added 8 commits May 11, 2026 23:11
Replace three separate column enumerations (SORT_HEADER_LABELS, NUMERIC_COLUMNS,
FILTERABLE_COLUMNS) with a single COLUMN_DEFS array that carries key, label, and
kind (numeric | categorical) for each of the 12 data columns.

Derive SORT_HEADER_LABELS and NUMERIC_COLUMNS from COLUMN_DEFS so future column
additions/removals are a one-line edit in COLUMN_DEFS with no separate label or
type maintenance.

Introduce renderColumnCell() that maps a column key to its <td> HTML, and
rework buildVariantRowMarkup to iterate COLUMN_DEFS (or a caller-provided subset)
rather than a hardcoded template literal.  The optional `cols` parameter (default
COLUMN_DEFS) is the extension point for the per-column visibility feature (#318)
landing in the next commit.

Replace the hardcoded TABLE_COL_COUNT = 13 constant with `1 + COLUMN_DEFS.length`
so the summary-row colspan stays correct automatically.

Behaviour is unchanged: same columns, same order, same HTML output.  All 1515
existing tests pass.
…opdown

Add a "Columns ▾" button in the recommendations filter-status bar that
opens a portal popover with per-column checkboxes for the 8 toggleable
columns (region, count, term, payment, savings, upfront_cost,
monthly_cost, effective_savings_pct). Provider / Account / Service /
Resource Type are "cell identity anchors" and always shown (v1).

- state.ts: hiddenColumns Set + getHiddenColumns/setHiddenColumns
- TOGGLEABLE_COLUMNS / TOGGLEABLE_COLUMN_KEYS from COLUMN_DEFS
- loadColumnVisibility / saveColumnVisibility with versioned localStorage
  schema (cudly.recs.columnVisibility.v1, schemaVersion: 1); unknown
  keys are whitelisted-out for forward compatibility
- resetColumnVisibilityState() exported for tests (seed flag + state)
- visibleColumns() computed once per renderRecommendationsList call,
  passed through to buildListMarkup + buildVariantRowMarkup
- summaryColspan = 1 + visible-toggleable-count (replaces hardcoded
  TABLE_COL_COUNT - 4; TABLE_COL_COUNT removed as no longer needed)
- mountColumnsButton: mount-once lifecycle, label shows "(N hidden)",
  aria-pressed reflects active state; re-syncs open-popover checkboxes
- Sort-hidden indicator in filter-status bar warns when the sort column
  is currently hidden
- Full keyboard accessibility: Escape closes and returns focus; Enter/
  Space opens; click-outside dismisses
- 14 new tests covering load/save/edge-cases via localStorageMock

Closes #318
Update recommendation summary rows to only include values for visible
columns, close the column visibility popover when the tab deactivates,
and return a defensive copy of hidden column state.
Fixed 3 file(s) based on 3 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…hidden-note

Commit 64cc856 (CodeRabbit auto-fix) introduced two bugs in the
.sort-hidden-note block:
- backtick-quoted color value (`#666` → #666)
- stray extra closing brace that broke webpack's CSS parser

Both caused the frontend build and pre-commit checks to fail.
- recommendations.ts: in openVisibilityPopover_, the deferred
  document.addEventListener could attach a stale outside-click handler
  if closeVisibilityPopover() ran in the same tick (clearing
  visOutsideClickHandler before the setTimeout callback fired). Guard
  the attach with a re-check that the popover is still open and the
  live handler still equals the captured one.
- recommendations.test.ts: pin the full TOGGLEABLE_COLUMNS contract
  (include on_demand_monthly and assert length === 9) so future column
  additions can't quietly drift past the test.

Addresses CodeRabbit feedback on PR #326.
Add `aria-haspopup="dialog"` to the "Columns ▾" button at mount time, and
toggle `aria-expanded` on the trigger as the popover opens/closes. Stash
the trigger in VisibilityPopoverState so close-from-any-path (toggle,
outside click, Escape, navigation tear-down) flips the attribute back.

`aria-pressed` is intentionally kept for the orthogonal "any columns
currently hidden?" indicator — different semantics from open/closed.

Addresses CodeRabbit a11y nitpick on PR #326.
@cristim cristim force-pushed the feat/recs-column-visibility branch from be348de to b665e1b Compare May 11, 2026 21:12
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 11, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 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 f33ae7c into feat/multicloud-web-frontend May 11, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p2 Backlog-worthy severity/low Minor 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