Skip to content

Comments

Rework filters experience#423

Merged
Blaumaus merged 8 commits intomainfrom
improvement/rework-filters
Nov 30, 2025
Merged

Rework filters experience#423
Blaumaus merged 8 commits intomainfrom
improvement/rework-filters

Conversation

@Blaumaus
Copy link
Member

@Blaumaus Blaumaus commented Nov 30, 2025

#420

Community Edition support

  • Your feature is implemented for the Swetrix Community Edition
  • This PR only updates the Cloud (Enterprise) Edition code (e.g. Paddle webhooks, blog, payouts, etc.)

Database migrations

  • Clickhouse / MySQL migrations added for this PR
  • No table schemas changed in this PR

Documentation

  • You have updated the documentation according to your PR
  • This PR did not change any publicly documented endpoints

Summary by CodeRabbit

  • New Features

    • Redesigned multi-row filter editor with per-row operators and values
    • Added new filter columns, re-ordered filter panels, and support for combined browser/OS + version entries
    • New backend support to fetch browser/OS version values for autocomplete
  • Style

    • Modal gains a new "medium" size
    • Button styling adjusted for cleaner light/dark hover behavior
  • Localization

    • Added filter-related translations and a global "Search" label
  • Other

    • Added two new valid filter keys (te, co)

✏️ Tip: You can customize this high-level summary in your review settings.

@Blaumaus Blaumaus self-assigned this Nov 30, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 30, 2025

Walkthrough

Adds a multi-row filter editor, new contains-operator encoding, expanded filter keys and panel order, backend endpoints/services to fetch browser/OS version options, a medium modal size, translations, and minor UI styling tweaks.

Changes

Cohort / File(s) Summary
Filter Constants
web/app/lib/constants/index.ts
Reordered and expanded FILTERS_PANELS_ORDER (now includes host, rg, ct, brv, osv, so, me, ca, te, co, etc.) and updated ERRORS_FILTERS_PANELS_ORDER with additional keys and new ordering.
Filter validation
web/app/pages/Project/View/utils/filters.tsx
Added new valid filter keys: te and co.
Filter UI (multi-row)
web/app/pages/Project/View/components/SearchFilters.tsx
Replaced single-input filters with a multi-row editor: per-row column/operator/value, operators include is/isNot/contains/notContains, ValueInput with autocomplete/virtualization, prefetch/caching of values, special handling for brv/osv (combined name
API client
web/app/api/index.ts
Added getVersionFilters(pid, type, column, password?) wrapper to call new backend endpoint for version options.
Modal UI
web/app/ui/Modal.tsx
Added 'medium' size to ModalProps and CSS mapping for medium dialog width.
Translations
web/public/locales/en.json
Added common.search and project translation keys: addFilter, selectColumn, selectValues, clearAllFilters, filterSearchOrType, filterUseValue, filterAddCustomValue.
Button styling
web/app/pages/Project/View/components/RefreshStatsButton.tsx
Removed default light/dark background classes; button is transparent by default with hover background/border changes.
Backend controller (cloud)
backend/apps/cloud/src/analytics/analytics.controller.ts
Added GET /analytics/filters/versions endpoint using GetVersionFiltersDto, guarded by Auth and permission checks, delegating to service.
Backend service (cloud)
backend/apps/cloud/src/analytics/analytics.service.ts
Added getVersionFilters(pid, type, column) to query distinct name/version pairs; updated generateParams signature to accept an extra paramsData parameter.
Backend DTO (cloud)
backend/apps/cloud/src/analytics/dto/get-version-filters.dto.ts
Added GetVersionFiltersDto with validated pid, type ('traffic'
Backend controller (community)
backend/apps/community/src/analytics/analytics.controller.ts
Added GET /filters/versions endpoint using GetVersionFiltersDto, delegating to service after access checks.
Backend service (community)
backend/apps/community/src/analytics/analytics.service.ts
Added getVersionFilters(pid, type, column) to retrieve grouped browser/OS version pairs from ClickHouse; new public method.
Backend DTO (community)
backend/apps/community/src/analytics/dto/get-version-filters.dto.ts
Added GetVersionFiltersDto (same fields/validations as cloud DTO).
Active filters keying
web/app/pages/Project/View/components/AddAViewModal.tsx
Filter list item key now includes isExclusive and isContains to ensure uniqueness for similar filters with different flags.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Browser UI
    participant Frontend as Web App
    participant API as Frontend API wrapper
    participant Controller as Analytics Controller
    participant Service as Analytics Service
    participant DB as ClickHouse

    Client->>Frontend: Open SearchFilters modal / request versions for column
    Frontend->>API: getVersionFilters(pid, type, column, password?)
    API->>Controller: GET /analytics/filters/versions (validated DTO)
    Controller->>Service: analyticsService.getVersionFilters(pid, type, column)
    Service->>DB: SELECT DISTINCT name, version FROM <table> WHERE pid = $pid AND column IS NOT NULL AND version IS NOT NULL
    DB-->>Service: rows {name, version}
    Service-->>Controller: [{name, version}, ...]
    Controller-->>API: 200 OK with version list
    API-->>Frontend: version list (frontend builds "Name|||Version" entries)
    Frontend-->>Client: show combined version suggestions
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Inspect SearchFilters.tsx for state complexity, virtualization, and contains-operator URL encoding.
  • Verify brv/osv combined-value handling (creation, parsing, submission) and that it aligns with backend shape.
  • Review new backend queries for SQL correctness, parameterization, and null checks.
  • Confirm updated generateParams signature in cloud service doesn't break callers.

Possibly related PRs

Poem

🐰 I hopped through rows of filters, neat and spry,
Paired browsers with versions, under a medium sky,
Autocomplete nibbles carrots of suggestion light,
Click, filter, bloom — the dashboard feels just right,
Tiny paws, big hops — filters done, goodnight! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Rework filters experience' directly corresponds to the main changes in this PR, which comprehensively redesign the filters UI and experience across multiple components.
Description check ✅ Passed The PR description addresses all required template sections with appropriate checkboxes marked, including community edition support, database migrations, and documentation statements.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch improvement/rework-filters

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@Blaumaus Blaumaus marked this pull request as ready for review November 30, 2025 00:02
Copy link

@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: 4

🧹 Nitpick comments (4)
web/app/ui/VirtualizedSelect.tsx (2)

166-169: Index passed to extractors refers to filteredItems, not original items.

When search filtering is active, virtualItem.index is the index within filteredItems, not the original items array. If extractors depend on the original item index, this could cause inconsistencies.

If this is intentional (extractors typically use the item value, not index), consider documenting this behavior in the interface comments. Otherwise, you may need to track original indices.


122-136: Consider adding aria-label to the search input for accessibility.

The search input has a placeholder but lacks an explicit aria-label, which would improve screen reader experience.

 <input
   type='text'
   className='w-full rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-hidden dark:border-gray-700 dark:bg-slate-700 dark:text-gray-50'
   placeholder={t('common.search') || 'Search...'}
+  aria-label={t('common.search') || 'Search'}
   value={query}
   onChange={(e) => setQuery(e.target.value)}
web/app/pages/Project/View/components/SearchFilters.tsx (2)

168-177: Country/OS/browser/device icon logic is solid; minor duplication with existing deviceIconMapping helper

The per‑column icon handling in getIconForItem is well‑structured and handles:

  • Countries → Flag + i18n name
  • Browsers/OS → logo maps with dark theme fallback
  • Devices → local deviceIconMapping with a question‑mark fallback

Functionally this looks good.

There is a small duplication with deviceIconMapping already defined in ViewProject.helpers.tsx (same keys, different size classes). If you want to reduce drift, consider centralizing the mapping and passing a className or size prop from here instead of redefining it.

Also applies to: 179-275


460-485: Filter values prefetching is robust; small risk of double‑fetch and some avoidable effect churn

The caching + loading‑set approach is solid overall:

  • filterValuesCache[column] and loadingColumns prevent most duplicate fetches.
  • Pre‑fetch on modal open plus targeted fetch on column change is a good UX optimization.
  • State updates use functional setters, which avoids stale closure issues.

Two minor points you may want to tighten:

  1. Potential double fetch for the same column
    Because React state updates are async, two quick successive calls to fetchFilterValues(column) (e.g. from the prefetch effect and a row’s updateFilterRow) can both see loadingColumns.has(column) === false and both start a request. Not critical, but you can harden this by tracking an “in‑flight” set in a ref or by narrowing call sites (e.g., skipping prefetch for the just‑changed column in updateFilterRow).

  2. Effect dependency noise on panelOptions and caches
    panelOptions is recomputed on every render and then used directly as a dependency, which forces the prefetch effect to re‑run each render (even though the showModal guard prevents extra work). Wrapping it in useMemo would make the effect dependencies cleaner:

  • const panelOptions = type === 'traffic' ? FILTERS_PANELS_ORDER : ERRORS_FILTERS_PANELS_ORDER
  • const panelOptions = useMemo(
  • () => (type === 'traffic' ? FILTERS_PANELS_ORDER : ERRORS_FILTERS_PANELS_ORDER),
  • [type],
  • )
    
    

These are non‑blocking, but worth considering.

Also applies to: 487-498, 524-538

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45cd8b4 and 66df5a5.

📒 Files selected for processing (6)
  • web/app/lib/constants/index.ts (1 hunks)
  • web/app/pages/Project/View/components/SearchFilters.tsx (4 hunks)
  • web/app/pages/Project/View/utils/filters.tsx (1 hunks)
  • web/app/ui/Modal.tsx (2 hunks)
  • web/app/ui/VirtualizedSelect.tsx (1 hunks)
  • web/public/locales/en.json (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
web/app/pages/Project/View/components/SearchFilters.tsx (3)
web/app/pages/Project/View/ViewProject.helpers.tsx (1)
  • deviceIconMapping (1646-1654)
web/app/lib/constants/index.ts (5)
  • BROWSER_LOGO_MAP (929-976)
  • OS_LOGO_MAP (978-1004)
  • OS_LOGO_MAP_DARK (1006-1011)
  • FILTERS_PANELS_ORDER (253-273)
  • ERRORS_FILTERS_PANELS_ORDER (274-274)
web/app/api/index.ts (2)
  • getErrorsFilters (1422-1432)
  • getFilters (1410-1420)
🪛 GitHub Actions: Pull Request Validation Workflow
web/app/ui/VirtualizedSelect.tsx

[error] 1-1: Unused files detected by knip: 'app/ui/VirtualizedSelect.tsx'. Process exited with code 1.

🔇 Additional comments (9)
web/app/ui/Modal.tsx (1)

28-28: LGTM!

The addition of the 'medium' size option is consistent with the existing size patterns. The CSS classes 'w-[90vw] max-w-2xl md:w-full' appropriately position it between 'regular' (max-w-lg) and 'large' (max-w-5xl).

Also applies to: 69-69

web/app/pages/Project/View/utils/filters.tsx (1)

37-38: LGTM!

The addition of 'te' (UTM Term) and 'co' (UTM Content) to validFilters is consistent with the updated FILTERS_PANELS_ORDER in constants and the mapping translations in en.json.

web/public/locales/en.json (1)

144-144: LGTM!

The new translation keys are well-structured and use correct interpolation syntax ({{value}}). They align properly with the enhanced filter UI components.

Also applies to: 943-949

web/app/lib/constants/index.ts (1)

253-274: The omission of 'refn' from FILTERS_PANELS_ORDER is intentional and correct.

'refn' is a valid filter used internally in the system (backend special handling in analytics services, conditional filtering logic in ViewProject.tsx), but it is not exposed as a user-selectable filter panel option. The UI uses 'ref' instead in the traffic sources tabs, and 'refn' is only applied programmatically when transitioning between grouped and filtered views. This is a deliberate design choice, not an oversight.

web/app/pages/Project/View/components/SearchFilters.tsx (5)

87-137: URL map key now depends on isContains — behavior looks correct but needs consistency check with other filter builders

You now include isContains in both the dedupe map key and the URL key prefix (~ vs ^) which is necessary to distinguish “contains” vs “not contains” filters:

  • Map key: ${f.isExclusive ? '!' : ''}${f.isContains ? '~' : ''}${f.column}=${f.filter}
  • URL key: plain / ! / ~ / ^ depending on isExclusive/isContains.

This is logically consistent and should avoid collisions between is, isNot, contains, and notContains filters, and the preservation loop correctly strips all !/~/^ prefixes before isFilterValid checks.

Please double‑check that:

  • FilterType always sets isContains (including in places where filters are parsed from URL/search params), and
  • Any other code that constructs filter URL params uses the same prefix scheme.

285-291: Virtualized dropdown implementation looks correct and efficient

The useVirtualizer setup and rendering are wired properly:

  • count derives from filteredItems.length.
  • getScrollElement targets the scrollable container via listRef.
  • Each option uses virtualItem.start and virtualItem.size for absolute positioning and height.
  • getLabelForItem/getIconForItem are reused for display.

This should scale well for large lists without layout glitches.

Also applies to: 387-427


552-580: Filter row → FilterType conversion and isContains mapping look correct

The onSubmit pipeline:

  • Filters out incomplete rows (row.column && row.value).
  • Uses operatorToFilter to derive { isExclusive, isContains } from the operator.
  • Normalizes country values to alpha‑2 codes when possible.
  • Calls getFiltersUrlParams with override=true, replacing existing URL filters atomically.

This is coherent with the new operator model and seems to produce the expected FilterType objects for downstream use.


580-589: Submit enablement and Clear‑all logic are good, but consider reusing hasValidFilters after ValueInput fix

hasValidFilters correctly disables submit unless at least one row has both a column and a (non‑empty) value, and the Clear‑all button is only shown when it’s meaningful.

Once you sync ValueInput changes to row.value, these controls will accurately track what the user sees. No change required beyond the ValueInput fix, just calling out that the wiring is otherwise sound.

Also applies to: 745-765


767-809: Applied filters summary is well‑designed and leverages the new operator semantics

The applied‑filters chips:

  • Localize column names via tnMapping/project.mapping.
  • Show country names for cc via countries.getName.
  • Compute operator labels directly from isExclusive/isContains.
  • Use different color accents for each operator combination.

This provides a clear, compact summary of active filters and correctly reflects the expanded operator set.

Copy link

@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 (3)
web/app/pages/Project/View/components/SearchFilters.tsx (3)

330-333: ValueInput does not sync user edits back to row.value, causing stale filter values to be applied

handleInputChange only updates the local inputValue state; it never calls onChange. As a result, row.value remains whatever was last committed (via option click / Enter / "Use this value" button). This creates UX bugs:

  1. If a user clears an existing value and applies filters without hitting Enter or selecting a suggestion, the UI shows an empty field but the old filter still gets submitted.
  2. submitDisabled can remain true because row.value is non‑empty even though the visible input is empty, or vice versa.

Apply this diff to keep parent state in sync:

 const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
   const newValue = e.target.value
   setInputValue(newValue)
+  onChange(newValue)
 }

789-812: Fix ListboxOption className prop to use active instead of focus

Headless UI's ListboxOption render prop exposes active (and selected, disabled), not focus. Using focus means the active background style never applies.

Apply this diff:

 <ListboxOption
   key={option}
   value={option}
-  className={({ focus }) =>
+  className={({ active }) =>
     cx('relative cursor-pointer py-2 pr-4 pl-3 select-none', {
-      'bg-gray-100 dark:bg-slate-700': focus,
-      'text-gray-700 dark:text-gray-50': !focus,
+      'bg-gray-100 dark:bg-slate-700': active,
+      'text-gray-700 dark:text-gray-50': !active,
     })
   }
 >

847-869: Fix ListboxOption className prop to use active instead of focus

Same issue as the column select: Headless UI's ListboxOption render prop exposes active, not focus. The hover/active styling won't work correctly.

Apply this diff:

 <ListboxOption
   key={op.value}
   value={op.value}
-  className={({ focus }) =>
+  className={({ active }) =>
     cx('relative cursor-pointer py-2 pr-4 pl-8 select-none', {
-      'bg-gray-100 dark:bg-slate-700': focus,
-      'text-gray-700 dark:text-gray-50': !focus,
+      'bg-gray-100 dark:bg-slate-700': active,
+      'text-gray-700 dark:text-gray-50': !active,
     })
   }
 >
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 66df5a5 and e6be8c7.

📒 Files selected for processing (2)
  • web/app/pages/Project/View/components/RefreshStatsButton.tsx (1 hunks)
  • web/app/pages/Project/View/components/SearchFilters.tsx (4 hunks)
🔇 Additional comments (1)
web/app/pages/Project/View/components/RefreshStatsButton.tsx (1)

75-80: Button hover/dark-mode styling change looks consistent

The updated class list keeps focus/disabled behavior intact while making the button visually quieter by default and only emphasizing it on hover (including dark mode via hover:dark:border-slate-700/80 and dark:hover:bg-slate-800). This aligns with the rest of the UI; no issues from a code perspective.

Copy link

@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 (2)
web/app/pages/Project/View/components/SearchFilters.tsx (2)

330-333: User input is not persisted until explicitly committed—verify this is the intended UX.

handleInputChange only updates local inputValue state without calling onChange. The value is only committed when the user:

  1. Selects an item from the dropdown
  2. Presses Enter
  3. Clicks "Use this value" button

This means if a user types a value and clicks elsewhere (including the Submit button), their typed text is lost. Since hasValidFilters (line 717) checks row.value, the Submit button may be disabled even though text appears in the input.

If this is intentional (requiring explicit commitment), consider adding visual feedback (e.g., a subtle "Press Enter to confirm" hint). Otherwise, sync on blur:

+ const handleBlur = () => {
+   if (inputValue.trim() && inputValue !== displayValue) {
+     // Commit the typed value
+     if (column === 'cc') {
+       const alpha2 = countries.getAlpha2Code(inputValue, language)
+       onChange(alpha2 || inputValue.trim())
+     } else {
+       onChange(inputValue.trim())
+     }
+   }
+   setIsOpen(false)
+ }

768-776: ListboxOption render prop should use active instead of focus.

Headless UI's ListboxOption exposes { active, selected, disabled } in its render function, not focus. Using focus means it's always undefined, so the hover/keyboard-focus styling never applies.

- className={({ focus }) =>
-   cx('relative cursor-pointer py-2 pr-4 pl-3 select-none', {
-     'bg-gray-100 dark:bg-slate-700': focus,
-     'text-gray-700 dark:text-gray-50': !focus,
-   })
- }
+ className={({ active }) =>
+   cx('relative cursor-pointer py-2 pr-4 pl-3 select-none', {
+     'bg-gray-100 dark:bg-slate-700': active,
+     'text-gray-700 dark:text-gray-50': !active,
+   })
+ }

Same fix needed at lines 829-834.

🧹 Nitpick comments (6)
backend/apps/cloud/src/analytics/analytics.service.ts (1)

2007-2033: Version filters query is consistent and safely scoped

The new getVersionFilters helper correctly scopes by pid, restricts table/column to a small union, and returns a simple { name, version } shape that matches the UI usage. You might optionally add an ORDER BY ${column}, ${safeVersionCol} for deterministic ordering, but the current implementation is sound.

backend/apps/community/src/analytics/dto/get-version-filters.dto.ts (1)

5-37: Community DTO mirrors cloud DTO, avoiding drift

This community GetVersionFiltersDto is identical in structure and validation to the cloud one, which is good for keeping behavior aligned across editions. If duplication grows, consider extracting a shared DTO base, but it's fine as-is.

web/app/pages/Project/View/components/SearchFilters.tsx (4)

526-563: Dependency array may cause stale closure or unnecessary re-creations.

fetchFilterValues includes filterValuesCache and loadingColumns in dependencies. Since these are state values that change when the function itself modifies them, this could cause subtle issues:

  1. The function gets recreated on every state update
  2. The early-return check filterValuesCache[column] uses the closure value, which should be fine

Consider using refs for the early-return checks if performance becomes an issue, but functionally this should work correctly.

Minor: The error case silently sets an empty array. Consider logging or showing a toast so users know why no suggestions appear:

      } catch (error) {
        console.error('Failed to fetch filter values:', error)
        setFilterValuesCache((prev) => ({ ...prev, [column]: [] }))
+       // Optional: Show user-facing error
      }

565-575: Pre-fetching all filter columns at modal open could cause burst of API calls.

When the modal opens, this effect iterates through all panelOptions (up to 20 columns) and triggers a fetch for each. This could result in:

  • 20+ concurrent API requests
  • Backend load spikes
  • Potential rate limiting

Consider:

  1. Lazy loading: fetch only when user selects a column
  2. Batching: create a single endpoint that returns all filter options
  3. Progressive loading: prioritize commonly-used filters first

For now, the UX benefit of instant suggestions may outweigh the cost, but monitor backend performance.


577-585: Effect dependency could be simplified.

Including filterRows in the dependency array means this effect runs on every row change, even though it only acts when filterRows is empty. This is functionally correct but slightly wasteful.

A minor optimization:

  useEffect(() => {
    if (showModal) {
      if (_isEmpty(filterRows)) {
        setFilterRows([{ id: crypto.randomUUID(), column: '', operator: 'is', value: '' }])
      }
    }
- }, [showModal, filterRows])
+ }, [showModal]) // Only run when modal visibility changes

However, removing filterRows would violate the exhaustive-deps rule, so this is fine as-is.


496-500: Consider edge case: version strings containing the separator.

If VERSION_SEPARATOR (|||) ever appears in a browser/OS name or version (unlikely but possible with custom user agents), split will produce more than 2 parts, and only the first two are captured.

For robustness:

 const parseVersionValue = (value: string): { parent: string; version: string } | null => {
   if (!value.includes(VERSION_SEPARATOR)) return null
-  const [parent, version] = value.split(VERSION_SEPARATOR)
+  const idx = value.indexOf(VERSION_SEPARATOR)
+  const parent = value.slice(0, idx)
+  const version = value.slice(idx + VERSION_SEPARATOR.length)
   return { parent, version }
 }

This ensures only the first separator is used as the split point.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e6be8c7 and 9819e39.

📒 Files selected for processing (8)
  • backend/apps/cloud/src/analytics/analytics.controller.ts (2 hunks)
  • backend/apps/cloud/src/analytics/analytics.service.ts (1 hunks)
  • backend/apps/cloud/src/analytics/dto/get-version-filters.dto.ts (1 hunks)
  • backend/apps/community/src/analytics/analytics.controller.ts (2 hunks)
  • backend/apps/community/src/analytics/analytics.service.ts (1 hunks)
  • backend/apps/community/src/analytics/dto/get-version-filters.dto.ts (1 hunks)
  • web/app/api/index.ts (1 hunks)
  • web/app/pages/Project/View/components/SearchFilters.tsx (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
backend/apps/community/src/analytics/dto/get-version-filters.dto.ts (1)
backend/apps/cloud/src/analytics/dto/get-version-filters.dto.ts (1)
  • GetVersionFiltersDto (5-38)
backend/apps/community/src/analytics/analytics.controller.ts (4)
backend/apps/cloud/src/auth/decorators/auth.decorator.ts (1)
  • Auth (10-17)
backend/apps/community/src/auth/decorators/auth.decorator.ts (1)
  • Auth (9-16)
backend/apps/cloud/src/analytics/dto/get-version-filters.dto.ts (1)
  • GetVersionFiltersDto (5-38)
backend/apps/community/src/analytics/dto/get-version-filters.dto.ts (1)
  • GetVersionFiltersDto (5-38)
web/app/api/index.ts (4)
backend/apps/cloud/src/analytics/analytics.controller.ts (1)
  • getVersionFilters (531-547)
backend/apps/cloud/src/analytics/analytics.service.ts (1)
  • getVersionFilters (2007-2033)
backend/apps/community/src/analytics/analytics.controller.ts (1)
  • getVersionFilters (1406-1420)
backend/apps/community/src/analytics/analytics.service.ts (1)
  • getVersionFilters (490-516)
backend/apps/cloud/src/analytics/dto/get-version-filters.dto.ts (1)
backend/apps/community/src/analytics/dto/get-version-filters.dto.ts (1)
  • GetVersionFiltersDto (5-38)
backend/apps/cloud/src/analytics/analytics.controller.ts (2)
backend/apps/cloud/src/auth/decorators/auth.decorator.ts (1)
  • Auth (10-17)
backend/apps/cloud/src/analytics/dto/get-version-filters.dto.ts (1)
  • GetVersionFiltersDto (5-38)
web/app/pages/Project/View/components/SearchFilters.tsx (4)
web/app/pages/Project/View/ViewProject.helpers.tsx (1)
  • deviceIconMapping (1646-1654)
web/app/lib/constants/index.ts (5)
  • BROWSER_LOGO_MAP (929-976)
  • OS_LOGO_MAP (978-1004)
  • OS_LOGO_MAP_DARK (1006-1011)
  • FILTERS_PANELS_ORDER (253-273)
  • ERRORS_FILTERS_PANELS_ORDER (274-274)
web/app/providers/ThemeProvider.tsx (1)
  • useTheme (29-37)
web/app/api/index.ts (3)
  • getVersionFilters (1434-1444)
  • getErrorsFilters (1422-1432)
  • getFilters (1410-1420)
🔇 Additional comments (12)
backend/apps/cloud/src/analytics/dto/get-version-filters.dto.ts (1)

5-37: DTO definition for version filters is clear and aligns with usage

The GetVersionFiltersDto shape (pid/type/column) and validators match the controller and service expectations, and mirror the community edition DTO, which keeps both stacks consistent. No changes needed.

web/app/api/index.ts (1)

1434-1444: Frontend wrapper for version filters matches backend contract

getVersionFilters uses the same URL pattern and password header style as getFilters/getErrorsFilters, and the return type { name, version }[] matches the service implementation. Throwing the raw error is consistent with neighboring helpers here.

backend/apps/cloud/src/analytics/analytics.controller.ts (2)

68-68: Import wiring for GetVersionFiltersDto is correct

The DTO import path is consistent with other analytics DTO imports and matches the file location.


529-547: Cloud version-filters endpoint is correctly guarded and delegated

GET log/filters/versions reuses the existing access model (@Auth, checkProjectAccess, checkBillingAccess) and simply delegates to analyticsService.getVersionFilters, which keeps controller logic thin and consistent with other filter endpoints.

backend/apps/community/src/analytics/analytics.controller.ts (2)

44-44: DTO import for version filters is consistent with other DTOs

Importing GetVersionFiltersDto alongside GetFiltersDto keeps the controller’s DTO usage organized and clear.


1404-1420: Community version-filters endpoint matches cloud behavior minus billing

The community GET log/filters/versions endpoint mirrors the cloud one (minus billing checks, as expected here), using the DTO for validation and delegating straight to analyticsService.getVersionFilters. Route path and auth usage are consistent with existing filters endpoints.

backend/apps/community/src/analytics/analytics.service.ts (1)

490-516: LGTM! Clean implementation of the version filters endpoint.

The method correctly:

  • Sanitizes table and column names by mapping to known safe values (no SQL injection risk)
  • Uses parameterized queries for the pid value
  • Applies NULL checks on both columns
  • Returns a properly typed result
web/app/pages/Project/View/components/SearchFilters.tsx (5)

62-85: Good organization of filter category icons.

The icon mapping is well-structured and covers all filter panel types. Using ReactNode as the value type allows flexibility for different icon components.


139-166: Well-structured operator definitions.

The operatorToFilter function cleanly maps the UI operator values to the internal filter representation. The exhaustive switch covers all cases.


640-664: Browser/OS version filtering creates paired filters—this is intentional but worth documenting.

When a user selects "Chrome 120.5" from the browser version dropdown, the code correctly splits this into two filters:

  1. br=Chrome (browser name)
  2. brv=120.5 (version number)

This ensures precise filtering (Chrome version 120.5, not Firefox version 120.5). Good design choice.


717-717: Clear validation logic for submit button.

The hasValidFilters check correctly ensures at least one filter row has both a column and value selected before enabling submission.


129-134: I'll help you verify the prefix logic for filter keys. Let me search for the URL parsing logic that handles these prefixes to ensure they're consistently interpreted.
<function_calls>

#!/bin/bash

Search for logic that parses URL params and reconstructs filters

Looking for where ^ prefix is handled

First, search for parseUrlParams or similar parsing functions

rg -n "parseUrlParams|parseFilter|fromUrl" --type ts --type tsx -C 3

Also search for handling of ^ prefix specifically

rg -n "^|startsWith.*'\^'" --type ts --type tsx -C 3


</function_calls>

Looking for the file to understand the full context of filter handling:
<function_calls>

#!/bin/bash

Get more context from the SearchFilters.tsx file

First check file size

wc -l web/app/pages/Project/View/components/SearchFilters.tsx

Look for all function definitions and filter-related logic

rg -n "function|const.=.(|export" web/app/pages/Project/View/components/SearchFilters.tsx | head -30


</function_calls>

<function_calls>

#!/bin/bash

Read the relevant sections of SearchFilters.tsx

Show lines around the encoding logic and find any parsing logic

sed -n '1,50p' web/app/pages/Project/View/components/SearchFilters.tsx
echo "---"
sed -n '87,140p' web/app/pages/Project/View/components/SearchFilters.tsx


</function_calls>

<function_calls>

#!/bin/bash

Continue reading the file to find parsing logic

sed -n '140,250p' web/app/pages/Project/View/components/SearchFilters.tsx


</function_calls>

<function_calls>

#!/bin/bash

Search in the entire file for where filters are reconstructed from URL

Look for logic that reads these prefixes back

rg -n "isExclusive|isContains" web/app/pages/Project/View/components/SearchFilters.tsx -C 2


</function_calls>

<function_calls>

#!/bin/bash

Search more broadly for filter reconstruction or parsing from URL

Look in the entire codebase for functions that handle URLSearchParams with these prefixes

rg -n "URLSearchParams|searchParams" web/app/pages/Project/View/components/SearchFilters.tsx -C 5 | head -50


</function_calls>

<function_calls>

#!/bin/bash

Let's search for the complementary function - if there's getFiltersUrlParams,

there should be a function to parse URL params back to filters

rg -n "getFiltersUrlParams|parseFiltersFromUrl|filtersFromUrl" --type ts --type tsx -C 3


</function_calls>

<function_calls>

#!/bin/bash

Search for where filters are read from URL (setSearchParams or similar)

and look for the decoding logic

rg -n "setSearchParams|new URLSearchParams|location.search" --type ts --type tsx web/app/pages/Project/View/components/SearchFilters.tsx -C 5


</function_calls>

<function_calls>

#!/bin/bash

Let's read the entire SearchFilters.tsx file to understand the full context

cat -n web/app/pages/Project/View/components/SearchFilters.tsx


</function_calls>

<function_calls>

#!/bin/bash

Search for where the URL parameters are being interpreted

Look for any place where '!' or '~' or '^' prefixes are stripped/parsed

rg -n "startsWith.['"]!" --type ts --type tsx -C 3
rg -n "startsWith.
['"]~" --type ts --type tsx -C 3
rg -n "substring|slice(" web/app/pages/Project/View/components/SearchFilters.tsx -C 2 | head -40


</function_calls>

<function_calls>

#!/bin/bash

Search for the actual parsing/reconstruction of filters from URL

Look for the useEffect or initialization that reads from URL

rg -n "useEffect|useSearchParams|location|searchParams" web/app/pages/Project/View/components/SearchFilters.tsx -C 5


</function_calls>

<function_calls>

#!/bin/bash

Let's check if there's a utility file that handles filter parsing

Search for files that might contain the parsing logic

fd -name ".ts" -o -name ".tsx" web/app/pages/Project/View/ | xargs rg -l "getFiltersUrlParams"


</function_calls>

<function_calls>

#!/bin/bash

Search for all files that reference getFiltersUrlParams or similar functions

rg -r "import.*getFiltersUrlParams|export.*getFiltersUrlParams" --type ts --type tsx


</function_calls>

<function_calls>

#!/bin/bash

Let's look at the entire SearchFilters.tsx more carefully

and focus on finding all the pieces of the puzzle

wc -l web/app/pages/Project/View/components/SearchFilters.tsx


</function_calls>

<function_calls>

#!/bin/bash

Get the full file with line numbers

head -n 500 web/app/pages/Project/View/components/SearchFilters.tsx | cat -n


</function_calls>

<function_calls>

#!/bin/bash

Look for the section where filters are being applied or read

Search for the part that handles the URL parameters in reverse

rg -n "Array.from|getAll|forEach" web/app/pages/Project/View/components/SearchFilters.tsx -C 3


</function_calls>

<function_calls>

#!/bin/bash

Let me search for where the prefixes are actually parsed from keys

This should show us where ! ~ or ^ are stripped from the filter key names

rg -n 'replace.*[!~^]|substring|startsWith|slice' web/app/pages/Project/View/components/SearchFilters.tsx -C 2


</function_calls>

<function_calls>

#!/bin/bash

Try a different approach - look at lines 87-200 in detail to understand the full flow

sed -n '87,200p' web/app/pages/Project/View/components/SearchFilters.tsx | cat -n


</function_calls>

<function_calls>

#!/bin/bash

Check if there's a reverse parsing function (from URL back to filters)

Search across the codebase for functions that might reconstruct filters

rg -n "setFilters|filters.*=" web/app/pages/Project/View/components/SearchFilters.tsx -C 3 | head -50


</function_calls>

<function_calls>

#!/bin/bash

Let's search for where the URL is initially parsed when the component loads

Look for useEffect with dependency on searchParams or location

ast-grep --pattern 'useEffect(() => {
$$$
}, [$_])'


</function_calls>

<function_calls>

#!/bin/bash

Search for any function that processes the URL search params

and converts them back to filter objects

rg -n "filter.*=|setFilter" web/app/pages/Project/View/components/SearchFilters.tsx | head -30


</function_calls>

<function_calls>

#!/bin/bash

Let me look at the getFiltersUrlParams implementation more carefully

Search for where it's defined

rg -n "function getFiltersUrlParams|const getFiltersUrlParams|export.*getFiltersUrlParams" --type ts --type tsx -A 50 | head -80


</function_calls>

<function_calls>

#!/bin/bash

Look at lines 100-140 more carefully to understand what's happening

sed -n '100,140p' web/app/pages/Project/View/components/SearchFilters.tsx | cat -n


</function_calls>

<function_calls>

#!/bin/bash

Now let's search for the mirror/complementary logic that parses these parameters

Search in the entire web/app directory for parsing logic

rg -n "startsWith.*'!'" web/app/ --type ts --type tsx -C 3


</function_calls>

<function_calls>

#!/bin/bash

Search for where the filter key prefixes are interpreted

Look for conditions checking for these special characters

rg -n "[0].*[=!~^]|\.substring(0|charAt(0" web/app/pages/Project/View/components/ --type ts --type tsx -C 3


</function_calls>

<function_calls>

#!/bin/bash

Check if there's a separate utility file for filter handling

fd -name ".ts" -o -name ".tsx" web/app/ | xargs rg -l "isExclusive.*isContains|filter.*prefix|parseFilter"


</function_calls>

<function_calls>

#!/bin/bash

Let me look for any comment or documentation about the prefix scheme

rg -n "exclusive|contains|prefix|mapKey" web/app/pages/Project/View/components/SearchFilters.tsx -C 2


</function_calls>

<function_calls>

#!/bin/bash

Search for the complete getFiltersUrlParams function

sed -n '87,137p' web/app/pages/Project/View/components/SearchFilters.tsx


</function_calls>

<function_calls>

#!/bin/bash

Search for where these URL params are actually read and used

Look for the initialization or effects that consume the URL params

rg -n "getFiltersUrlParams|" web/app/ --type ts --type tsx -B 5 -A 10


</function_calls>

<function_calls>

#!/bin/bash

Let me search in a much simpler way - look at what functions call getFiltersUrlParams

rg -n "getFiltersUrlParams" web/app/ --type ts --type tsx


</function_calls>

Based on the review comment structure and my search attempts, let me look directly at the file content to understand the full context:
<function_calls>

#!/bin/bash

Just show me the entire SearchFilters.tsx file with line numbers

cat web/app/pages/Project/View/components/SearchFilters.tsx


</function_calls>

Copy link

@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: 0

♻️ Duplicate comments (2)
web/app/pages/Project/View/components/SearchFilters.tsx (2)

191-375: ValueInput still allows stale row.value to be submitted when the visible input is cleared or edited

ValueInput keeps inputValue local and never calls onChange from handleInputChange, so row.value can remain non-empty while the user clears the field or types a different value. In that case, hasValidFilters and onSubmit still use the old row.value, and the user can apply filters that don't match what they see (e.g. clear the field → button remains enabled → old filter is submitted).

To keep the parent state as the single source of truth, wire typing directly into onChange:

 const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
   const newValue = e.target.value
   setInputValue(newValue)
+  // Keep parent row.value in sync with what the user sees
+  onChange(newValue)
+  // Optionally ensure suggestions reopen when typing
+  setIsOpen(true)
 }

This preserves your existing submit-time normalization (e.g. country code conversion in onSubmit) while ensuring that validation and applied filters always reflect the visible input.

Also applies to: 380-485


739-797: Headless UI ListboxOption should use active (not focus) for highlight state

Both Listbox option lists use className={({ focus }) => ...}, but Headless UI’s ListboxOption render-prop exposes { active, selected, disabled }, not focus. As a result, your "active" background/text styles never apply, and keyboard/mouse hover state isn’t visually indicated.

Update the render-prop destructuring and conditionals to use active:

- className={({ focus }) =>
-   cx('relative cursor-pointer py-2 pr-4 pl-3 select-none', {
-     'bg-gray-100 dark:bg-slate-700': focus,
-     'text-gray-700 dark:text-gray-50': !focus,
-   })
- }
+ className={({ active }) =>
+   cx('relative cursor-pointer py-2 pr-4 pl-3 select-none', {
+     'bg-gray-100 dark:bg-slate-700': active,
+     'text-gray-700 dark:text-gray-50': !active,
+   })
+}

…and similarly for the operator options (pl-8 variant).

Headless UI React ListboxOption (v2.x) render-prop API: which state props (`active`, `selected`, `disabled`, etc.) are exposed for styling options?

Also applies to: 821-851

🧹 Nitpick comments (2)
web/app/pages/Project/View/components/AddAViewModal.tsx (1)

397-446: Filter key now depends on operator flags; consider a more stable identity if Filter becomes stateful

Using key={${column}-${filter}-${isExclusive}-${isContains}} guarantees uniqueness but also forces React to remount the Filter whenever the operator flags change, which can reset any internal state Filter might hold (focus, animations, etc.). Since (column, filter) pairs are already unique in this UI, you could keep the key stable (e.g., ${column}-${filter} or a dedicated id field on the filter) and rely on props for operator changes.

web/app/pages/Project/View/components/SearchFilters.tsx (1)

526-575: Prefetching all filter columns on modal open may be unnecessarily heavy

useEffect prefetches values for every panelOptions column whenever the modal opens. For projects with many distinct values per column, this can trigger a large number of backend queries and increase initial modal latency, even if the user only interacts with 1–2 columns.

Consider lazily fetching values per column (on first selection) and optionally adding a background prefetch for the most commonly used panels, so initial open is cheaper while still benefiting from caching.

Also applies to: 587-617

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9819e39 and 8606b03.

📒 Files selected for processing (2)
  • web/app/pages/Project/View/components/AddAViewModal.tsx (1 hunks)
  • web/app/pages/Project/View/components/SearchFilters.tsx (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
web/app/pages/Project/View/components/SearchFilters.tsx (4)
web/app/pages/Project/View/ViewProject.helpers.tsx (1)
  • deviceIconMapping (1646-1654)
web/app/lib/constants/index.ts (5)
  • BROWSER_LOGO_MAP (929-976)
  • OS_LOGO_MAP (978-1004)
  • OS_LOGO_MAP_DARK (1006-1011)
  • FILTERS_PANELS_ORDER (253-273)
  • ERRORS_FILTERS_PANELS_ORDER (274-274)
web/app/providers/ThemeProvider.tsx (1)
  • useTheme (29-37)
web/app/api/index.ts (3)
  • getVersionFilters (1434-1444)
  • getErrorsFilters (1422-1432)
  • getFilters (1410-1420)
🔇 Additional comments (1)
web/app/pages/Project/View/components/SearchFilters.tsx (1)

87-137: URL filter encoding, operators, and version handling look consistent and correct

The combination of operatorToFilter, the updated map key in getFiltersUrlParams (including isExclusive and isContains), and the ^/~ prefixes yields a coherent mapping between UI operators and URL params. The brv/osv handling (splitting combined values into {br, brv} / {os, osv} pairs) also aligns with the version helpers and should serialize cleanly through getFiltersUrlParams.

Also applies to: 139-167, 489-500, 630-713

@Blaumaus Blaumaus merged commit 49f26e6 into main Nov 30, 2025
7 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Nov 30, 2025
6 tasks
@coderabbitai coderabbitai bot mentioned this pull request Dec 10, 2025
6 tasks
@coderabbitai coderabbitai bot mentioned this pull request Jan 31, 2026
6 tasks
@coderabbitai coderabbitai bot mentioned this pull request Feb 7, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant