Skip to content

refactor(frontend): extract parseNumericFilter to shared column-filter lib - #570

Merged
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
refactor/issue-166-column-filter-lib
May 28, 2026
Merged

cristim merged 2 commits into
feat/multicloud-web-frontendfrom
refactor/issue-166-column-filter-lib

Conversation

@cristim

@cristim cristim commented May 20, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Extracts parseNumericFilter + ParsedNumericFilter out of recommendations.ts into frontend/src/lib/column-filters.ts (closes refactor(frontend): extract column-filter primitives into a shared module — adopt on Plans / History / RI Exchange #166)
  • Adds a generic applyColumnFilters<TRow, TColumnId> in the lib so Plans / History / RI Exchange can adopt column-level filtering without copy-pasting the parser
  • recommendations.ts re-exports both the function and the type for backward compat -- no import-path churn for existing consumers
  • New __tests__/column-filters.test.ts exercises the lib directly (14 cases); all existing parseNumericFilter and applyColumnFilters tests in recommendations.test.ts continue passing via the re-exports

Scope note

Per the issue, this is a lighter-touch PR: extraction + Plans-ready lib. Wiring Plans / History / RI Exchange to the generic applyColumnFilters is tracked as follow-on work -- the lib contract is settled, so those PRs are unblocked.

Test plan

  • cd frontend && npx jest --no-coverage -- all 1908 tests pass
  • cd frontend && npx tsc --noEmit -- no type errors
  • New column-filters.test.ts exercises parseNumericFilter and generic applyColumnFilters directly from the lib
  • Existing recommendations filter tests continue to pass via re-exports

Summary by CodeRabbit

Release Notes

  • New Features

    • Added advanced numeric filtering with support for comparison operators (>, <, >=, <=), inclusive ranges, and comma-separated OR logic.
  • Refactor

    • Extracted column-filtering logic into a shared library for reuse across the application.
  • Tests

    • Added comprehensive test coverage for column filtering functionality and validation behavior.

Review Change Stack

…r lib (closes #166)

Move the numeric-filter parser out of recommendations.ts and into
frontend/src/lib/column-filters.ts so other tabs (Plans, History,
RI Exchange) can reuse it without copying the logic.

What changed:
- New lib/column-filters.ts exports parseNumericFilter, ParsedNumericFilter,
  ColumnFilterKind, and a generic applyColumnFilters<TRow, TColumnId> for
  any tab that wants column-level filtering.
- recommendations.ts drops the inline parseNumericFilter definition and
  imports from the lib; both the function and the type are re-exported so
  existing consumers that import from recommendations.ts require no changes.
- New __tests__/column-filters.test.ts exercises the lib directly (14 cases).
  The existing parseNumericFilter and applyColumnFilters suites in
  recommendations.test.ts continue to pass via the re-exports.

Adoption: Plans / History / RI Exchange can import parseNumericFilter (or
the generic applyColumnFilters) directly from lib/column-filters when they
add per-column numeric filters; no further extraction work is needed.
@coderabbitai

coderabbitai Bot commented May 20, 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: f5d86836-5d47-4b81-9bba-a6c1d1e23866

📥 Commits

Reviewing files that changed from the base of the PR and between bc7bf0f and 3aafba7.

📒 Files selected for processing (3)
  • frontend/src/__tests__/column-filters.test.ts
  • frontend/src/lib/column-filters.ts
  • frontend/src/recommendations.ts

📝 Walkthrough

Walkthrough

This PR extracts reusable column-filter primitives—numeric expression parser, filter application pipeline, and type contracts—into a new shared library. The existing Recommendations module is refactored to delegate to this library while preserving its numeric rounding behavior, and comprehensive tests validate both the library and backward compatibility.

Changes

Shared Column-Filter Library Extraction

Layer / File(s) Summary
Numeric filter parser and types
frontend/src/lib/column-filters.ts (lines 1–77), frontend/src/__tests__/column-filters.test.ts (lines 1–67)
parseNumericFilter converts textual expressions (comparison operators, inclusive ranges, exact matches, comma-separated OR terms, blank-as-match-all) into boolean predicates or errors. Tests validate operator correctness, range order-independence, OR semantics, and error reporting.
Filter application pipeline and contracts
frontend/src/lib/column-filters.ts (lines 79–127), frontend/src/__tests__/column-filters.test.ts (lines 69–133)
ColumnFilterKind types define categorical and numeric filter contracts. Generic applyColumnFilters AND-combines active column filters, delegating to categorical membership or numeric predicate checks; broken numeric expressions are skipped. Tests verify cloning, categorical narrowing, numeric predicate evaluation, multi-filter AND combination, and graceful failure handling.
Recommendations module migration
frontend/src/recommendations.ts (lines 25–29, 1313–1344)
Re-exports parseNumericFilter and ParsedNumericFilter from the shared library. Delegates applyColumnFilters to the shared version, wiring in this module's categorical/numeric cell extractors and numeric display-precision rounding to preserve filtering behavior against rounded values (issue #484).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • LeanerCloud/CUDly#160: Introduced the original column-filter primitives (parseNumericFilter, applyColumnFilters pipeline, and popover infrastructure) in recommendations.ts; this PR extracts those primitives into the shared module.
  • LeanerCloud/CUDly#491: Introduced the numeric filter display-precision and roundForDisplay semantics (issue #484) that this PR preserves when delegating to the shared library.
  • LeanerCloud/CUDly#322: Adds numeric filtering columns to recommendations.ts that plug into the same numeric filter pipeline being refactored here.

Poem

🐰 A filter so clever, extracted with care,
From recommendations it now floats in shared air,
Numeric ranges parsed, predicates flow,
Tests green and steady—the refactor can go!
In libraries nested, reuse takes its flight. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.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 'refactor(frontend): extract parseNumericFilter to shared column-filter lib' clearly summarizes the main change: extracting a numeric filter parser into a reusable library module.
Linked Issues check ✅ Passed The PR successfully extracts parseNumericFilter, ParsedNumericFilter, and applyColumnFilters into a shared frontend/src/lib/column-filters.ts module with comprehensive tests, meeting issue #166's stated coding requirements.
Out of Scope Changes check ✅ Passed All changes directly support the extraction objective: new shared library module, test coverage, and recommendations.ts re-exports for backward compatibility; no unrelated modifications present.

✏️ 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 refactor/issue-166-column-filter-lib

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

@cristim cristim added priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/m Days type/chore Maintenance / non-user-visible triaged Item has been triaged labels May 20, 2026
@cristim

cristim commented May 20, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 20, 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 commented May 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

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

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 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 commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

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

@cristim

cristim commented May 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@cristim

cristim commented May 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@cristim

cristim commented May 28, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

…shared lib

Closes the loop on #570: the column-filter lib was extracted (issue #166) but
recommendations.ts still contained a local 28-line applyColumnFilters that
duplicated the filter loop, only importing parseNumericFilter from the lib.

This commit removes the duplicate loop body and routes the exported
applyColumnFilters wrapper through applyColumnFiltersLib from
lib/column-filters.ts. The wrapper preserves the 2-argument public signature
(recs, filters) that tests and internal callers already use, while delegating
the generic filter pipeline to the shared implementation.

Numeric extractor wiring (issue #484): the wrapper captures getCostPeriod()
once at call time and passes an inline extractor
  (r, col) => roundForDisplay(numericCellValue(r, col), displayPrecision(col, period))
so numeric predicates still compare against the rounded display value, exactly
as before. The behavior is identical; the implementation now lives in one place.

All 309 recommendation tests and 1909 column-filter suite tests pass.
@cristim

cristim commented May 28, 2026

Copy link
Copy Markdown
Member Author

Wired recommendations.ts through the new generic applyColumnFilters in commit 3aafba7 - closes the loop on the extraction. The local 28-line filter loop is replaced by a 7-line wrapper that delegates to applyColumnFiltersLib with categorical/numeric extractors. roundForDisplay behavior is preserved via the numeric extractor. Plans/History/RI Exchange remain greenfield wiring tracked as follow-up. @coderabbitai review

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

@cristim Triggering a full review to pick up commit 3aafba786e437c43809ce251a8523fe7d09865d7 and verify the recommendations.ts wiring through applyColumnFilters.

✅ Actions performed

Full review triggered.

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Actionable comments posted: 0

@cristim
cristim merged commit aea9d5a into feat/multicloud-web-frontend May 28, 2026
5 checks passed
cristim added a commit that referenced this pull request Jun 1, 2026
…lters slice

Mirrors the recommendations slice shape (extracted in #570) so the RI Exchange
reshape-recommendations table can wire inline column filters via the shared
applyColumnFilters helper.

Slice is independent of the recommendations slice — column-id shape is
reshape-specific (source/target instance types, normalized units, utilization
%), and persistence stays out of scope on this iteration to match the
existing pattern.

Refs #166.
cristim added a commit that referenced this pull request Jun 1, 2026
Wires per-column filter popovers to the RI Exchange reshape-recommendations
table using the helpers extracted in merged #570 (parseNumericFilter +
applyColumnFilters). Categorical columns (Source RI, Source/Target instance
types, Reason) get a checkbox-list popover; numeric columns (Source/Target
count, Utilization %, Normalized used/purchased) get a free-text expression
popover that supports `>N`, `>=N`, `<N`, `<=N`, `N..M` ranges, exact match,
and comma-separated OR.

Numeric predicates compare against the display-rounded cell value so a user
typing the displayed figure (e.g. 95.0 for utilization) matches the cell
they see. Broken expressions are skipped (inline error in the popover)
rather than collapsing the table.

Filter state lives in the RI Exchange-specific slice on state.ts; no
cross-tab coupling.  No drive-by changes to the rest of the RI Exchange
page (convertible-RI table, exchange modal, automation settings, history).

Test-mocks for the riexchange + riexchange-permissions suites updated to
expose the new state getters/setters so the existing happy-path assertions
still pass.

Refs #166.
cristim added a commit that referenced this pull request Jun 1, 2026
Adds focused regression coverage for the RI Exchange filter wiring on top
of the shared lib (issue #166 follow-up to merged #570):

  * empty filter record returns a defensive clone of the input
  * numeric expression filter narrows by predicate
  * categorical set filter narrows by membership
  * multiple filters AND together across kinds
  * broken numeric expressions are skipped rather than treated as match-none
  * numeric predicates compare against the display-rounded cell value
    (utilization toFixed(1) regression guard)
  * clearing a column drops its narrowing

The popover / state-slice / button-rendering wiring is exercised by the
existing riexchange test suite; this file pins the pure-function
contract the lib + extractor + precision composition relies on.

Refs #166.
cristim added a commit that referenced this pull request Jun 3, 2026
…lters slice

Mirrors the recommendations slice shape (extracted in #570) so the RI Exchange
reshape-recommendations table can wire inline column filters via the shared
applyColumnFilters helper.

Slice is independent of the recommendations slice — column-id shape is
reshape-specific (source/target instance types, normalized units, utilization
%), and persistence stays out of scope on this iteration to match the
existing pattern.

Refs #166.
cristim added a commit that referenced this pull request Jun 3, 2026
Wires per-column filter popovers to the RI Exchange reshape-recommendations
table using the helpers extracted in merged #570 (parseNumericFilter +
applyColumnFilters). Categorical columns (Source RI, Source/Target instance
types, Reason) get a checkbox-list popover; numeric columns (Source/Target
count, Utilization %, Normalized used/purchased) get a free-text expression
popover that supports `>N`, `>=N`, `<N`, `<=N`, `N..M` ranges, exact match,
and comma-separated OR.

Numeric predicates compare against the display-rounded cell value so a user
typing the displayed figure (e.g. 95.0 for utilization) matches the cell
they see. Broken expressions are skipped (inline error in the popover)
rather than collapsing the table.

Filter state lives in the RI Exchange-specific slice on state.ts; no
cross-tab coupling.  No drive-by changes to the rest of the RI Exchange
page (convertible-RI table, exchange modal, automation settings, history).

Test-mocks for the riexchange + riexchange-permissions suites updated to
expose the new state getters/setters so the existing happy-path assertions
still pass.

Refs #166.
cristim added a commit that referenced this pull request Jun 3, 2026
Adds focused regression coverage for the RI Exchange filter wiring on top
of the shared lib (issue #166 follow-up to merged #570):

  * empty filter record returns a defensive clone of the input
  * numeric expression filter narrows by predicate
  * categorical set filter narrows by membership
  * multiple filters AND together across kinds
  * broken numeric expressions are skipped rather than treated as match-none
  * numeric predicates compare against the display-rounded cell value
    (utilization toFixed(1) regression guard)
  * clearing a column drops its narrowing

The popover / state-slice / button-rendering wiring is exercised by the
existing riexchange test suite; this file pins the pure-function
contract the lib + extractor + precision composition relies on.

Refs #166.
cristim added a commit that referenced this pull request Jun 3, 2026
Introduces a Plans-scoped per-column filter slice (PlansColumnId,
PlansColumnFilter, PlansColumnFilters) with get/set/clear accessors,
mirroring the existing Recommendations slice. Kept as a separate slice
so the Plans, History, and RI Exchange follow-ups to PR #570 can land
in parallel without contending on each other's state shape.

In-memory only — survives tab switches within the SPA, resets on full
reload (same lifecycle as recommendationsColumnFilters).

Refs #166.
cristim added a commit that referenced this pull request Jun 3, 2026
Wires the Planned Purchases table to the shared lib/column-filters
primitives extracted in PR #570. Each filterable column gets an inline
trigger button in its header; clicking opens a popover with either a
multi-select (categorical) or a numeric-expression input. Filters
AND together, persist in-memory across the SPA session, and are
applied at render time.

Columns wired:
- categorical: provider, service, resource_type, term, payment, status
- numeric: count, upfront_cost, estimated_savings

Numeric predicates compare against the rounded display value
(roundForDisplay + displayPrecisionForPlan) so the issue #484
exact-match contract is preserved here too.

Re-renders go through a cached lastLoadedPurchases module slice so
popover commits never re-fetch from the API. Popover lives on
document.body and is re-anchored after each table re-render.

Mock-state additions in plans*.test.ts and xss-purchase-status.test.ts
mirror the new accessors; legacy assertions continue to pass.

Refs #166. Sibling follow-ups land in parallel for History and RI
Exchange.
cristim added a commit that referenced this pull request Jun 3, 2026
…166) (#789)

* refactor(frontend/state): add RiExchangeColumnId + riExchangeColumnFilters slice

Mirrors the recommendations slice shape (extracted in #570) so the RI Exchange
reshape-recommendations table can wire inline column filters via the shared
applyColumnFilters helper.

Slice is independent of the recommendations slice — column-id shape is
reshape-specific (source/target instance types, normalized units, utilization
%), and persistence stays out of scope on this iteration to match the
existing pattern.

Refs #166.

* feat(frontend/riexchange): inline column filters via shared lib

Wires per-column filter popovers to the RI Exchange reshape-recommendations
table using the helpers extracted in merged #570 (parseNumericFilter +
applyColumnFilters). Categorical columns (Source RI, Source/Target instance
types, Reason) get a checkbox-list popover; numeric columns (Source/Target
count, Utilization %, Normalized used/purchased) get a free-text expression
popover that supports `>N`, `>=N`, `<N`, `<=N`, `N..M` ranges, exact match,
and comma-separated OR.

Numeric predicates compare against the display-rounded cell value so a user
typing the displayed figure (e.g. 95.0 for utilization) matches the cell
they see. Broken expressions are skipped (inline error in the popover)
rather than collapsing the table.

Filter state lives in the RI Exchange-specific slice on state.ts; no
cross-tab coupling.  No drive-by changes to the rest of the RI Exchange
page (convertible-RI table, exchange modal, automation settings, history).

Test-mocks for the riexchange + riexchange-permissions suites updated to
expose the new state getters/setters so the existing happy-path assertions
still pass.

Refs #166.

* test(frontend/riexchange): column-filter regression suite

Adds focused regression coverage for the RI Exchange filter wiring on top
of the shared lib (issue #166 follow-up to merged #570):

  * empty filter record returns a defensive clone of the input
  * numeric expression filter narrows by predicate
  * categorical set filter narrows by membership
  * multiple filters AND together across kinds
  * broken numeric expressions are skipped rather than treated as match-none
  * numeric predicates compare against the display-rounded cell value
    (utilization toFixed(1) regression guard)
  * clearing a column drops its narrowing

The popover / state-slice / button-rendering wiring is exercised by the
existing riexchange test suite; this file pins the pure-function
contract the lib + extractor + precision composition relies on.

Refs #166.
cristim added a commit that referenced this pull request Jun 3, 2026
#791)

* refactor(frontend/state): add PlansColumnId + plansColumnFilters slice

Introduces a Plans-scoped per-column filter slice (PlansColumnId,
PlansColumnFilter, PlansColumnFilters) with get/set/clear accessors,
mirroring the existing Recommendations slice. Kept as a separate slice
so the Plans, History, and RI Exchange follow-ups to PR #570 can land
in parallel without contending on each other's state shape.

In-memory only — survives tab switches within the SPA, resets on full
reload (same lifecycle as recommendationsColumnFilters).

Refs #166.

* feat(frontend/plans): inline column filters via shared lib (refs #166)

Wires the Planned Purchases table to the shared lib/column-filters
primitives extracted in PR #570. Each filterable column gets an inline
trigger button in its header; clicking opens a popover with either a
multi-select (categorical) or a numeric-expression input. Filters
AND together, persist in-memory across the SPA session, and are
applied at render time.

Columns wired:
- categorical: provider, service, resource_type, term, payment, status
- numeric: count, upfront_cost, estimated_savings

Numeric predicates compare against the rounded display value
(roundForDisplay + displayPrecisionForPlan) so the issue #484
exact-match contract is preserved here too.

Re-renders go through a cached lastLoadedPurchases module slice so
popover commits never re-fetch from the API. Popover lives on
document.body and is re-anchored after each table re-render.

Mock-state additions in plans*.test.ts and xss-purchase-status.test.ts
mirror the new accessors; legacy assertions continue to pass.

Refs #166. Sibling follow-ups land in parallel for History and RI
Exchange.

* test(frontend/plans): column-filter regression suite

Adds plans-column-filters.test.ts covering the Planned Purchases table
integration with lib/column-filters:

- every filterable column header carries a trigger button
- clicking opens a portal popover detached to document.body
- categorical set filter narrows rows (provider=aws)
- numeric expression filter narrows rows (count >= 2)
- stacked filters AND together
- invalid expressions surface the lib's inline error and apply no filter
- (All) tri-state restores the full row set after narrowing

The shared parseNumericFilter + applyColumnFilters primitives keep
their own coverage in column-filters.test.ts; these tests focus on
the Plans-specific wiring.

Refs #166.

* style(frontend/plans): use unicode escapes for filter-icon + em-dash

Matches the canonical recommendations.ts wiring exactly (⛛ filter
icon, — em-dash inside the aria-label). Keeps the rendered DOM
identical to the Opportunities tab so the two surfaces are
indistinguishable at the screen-reader and visual layers.

No behavioural change.
@cristim
cristim deleted the refactor/issue-166-column-filter-lib branch June 3, 2026 21:55
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/medium Moderate harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant