Skip to content

feat: render active filters as inline chips in search where input#2388

Open
MikeShi42 wants to merge 3 commits into
mainfrom
mikeshi/filter-chips-in-where-input
Open

feat: render active filters as inline chips in search where input#2388
MikeShi42 wants to merge 3 commits into
mainfrom
mikeshi/filter-chips-in-where-input

Conversation

@MikeShi42
Copy link
Copy Markdown
Contributor

Summary

Replaces the separate ActiveFilterPills row below the search input with inline filter chips rendered directly inside SearchWhereInput. The chips slot is wired through both SQLInlineEditor (CodeMirror) and AutocompleteInput (Lucene), and Backspace at cursor position 0 removes the last chip — making filter state feel native to the input rather than parked beside it.

What changed

  • New InlineFilterChips component + filterPillUtils helpers (flattenFilters, removePill) that turn a FilterState into a list of chips and route removal back through the existing filter-state hook.
  • Chip slot wiring: SQLInlineEditor, AutocompleteInput, and SearchInputV2 all gain optional filterChips and onRemoveLastChip props.
  • SearchWhereInput accepts a new searchFilters?: FilterStateHook prop, flattens it into chips, and forwards both the chip nodes and the last-chip removal callback to whichever underlying editor is active.
  • Removed ActiveFilterPills (and its test) — its responsibility is now subsumed by SearchWhereInput.
  • DBSearchPage drops the <ActiveFilterPills .../> row and passes its searchFilters hook to SearchWhereInput instead.

Tests

  • Unit: InlineFilterChips.test.tsx (rendering, group container, no artificial limit), filterPillUtils.test.ts (flatten + remove semantics).
  • E2E: tests/e2e/features/search/filter-chips.spec.ts + a FilterChipsComponent page object.

Test plan

  • Open /search, apply a couple of sidebar filters (e.g. ServiceName, SeverityText) — verify chips appear inside the where input, not below it.
  • Click the × on a chip — filter is removed from the URL and the chip disappears.
  • Place cursor at position 0 of an empty input and press Backspace — last chip is removed.
  • Press Backspace at position 0 with text in the input — no chip removed, no text deleted.
  • Switch between SQL and Lucene modes — chips persist and behave identically in both.
  • Switch sources (logs ↔ traces) — chips that survive the source switch (per feat: preserve compatible filters when switching sources #2314) re-render correctly inside the input.
  • Long chip values wrap nicely; input still resizes on focus.

Notes

Replaces the separate ActiveFilterPills row below the search input with
inline filter chips rendered directly inside SearchWhereInput. The chips
slot is wired through both SQLInlineEditor (CodeMirror) and
AutocompleteInput (Lucene), and Backspace at cursor position 0 removes
the last chip.

- Add InlineFilterChips component and filterPillUtils helpers
- Add filterChips + onRemoveLastChip props to SQLInlineEditor,
  AutocompleteInput, and SearchInputV2
- Wire SearchWhereInput to flatten searchFilters into chips and handle
  last-chip removal
- Remove ActiveFilterPills component and its test
- Add unit tests for InlineFilterChips and filterPillUtils, plus an
  E2E suite (filter-chips.spec.ts)
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 30, 2026

🦋 Changeset detected

Latest commit: 95fa195

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Minor
@hyperdx/api Minor
@hyperdx/otel-collector Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hyperdx-oss Ignored Ignored Preview May 31, 2026 12:25am
hyperdx-storybook Ignored Ignored Preview May 31, 2026 12:25am

Request Review

@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label May 30, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 881 production lines changed (Tier 2 max: < 250)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 12
  • Production lines changed: 881 (+ 1282 in test files, excluded from tier calculation)
  • Branch: mikeshi/filter-chips-in-where-input
  • Author: MikeShi42

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

E2E Test Results

All tests passed • 218 passed • 3 skipped • 1331s

Status Count
✅ Passed 218
❌ Failed 0
⚠️ Flaky 3
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

Deep Review

🟡 P2 — recommended

  • packages/app/src/components/SearchInput/SearchWhereInput.tsx:212onRemoveLastChip reads pills from a useCallback closure and calls setFilterValue (which toggles via setFilters(prev => ...) at searchFilters.tsx:478-483); two Backspace keydowns that fire before React commits both see the same stale pills[pills.length-1] and the second toggle re-adds the chip that the first removed.

    • Fix: Gate onRemoveLastChip with a ref that flips synchronously on the first call and resets on the next render, so a second invocation in the same commit window is a no-op until pills has been re-derived.
    • adversarial, julik-frontend-races, kieran-typescript
  • packages/app/src/components/SearchInput/AutocompleteInput.tsx:233 — The Backspace handler does not check e.nativeEvent.isComposing, so a Backspace press during an active IME composition with the cursor at position 0 fires onRemoveLastChip and silently removes a chip instead of being absorbed by the IME.

    • Fix: Add !e.nativeEvent.isComposing to the condition before invoking onRemoveLastChip; CodeMirror 6 already guards its keymap during composition, so only the textarea path needs this.
    • julik-frontend-races, correctness, adversarial
  • packages/app/src/components/SearchInput/SearchInputV2.tsx:66onRemoveLastChip?: () => void narrows the boolean return that both the sibling AutocompleteInput (AutocompleteInput.tsx:54) and the consumer at AutocompleteInput.tsx:239-242 (const removed = onRemoveLastChip(); if (removed) e.preventDefault();) depend on, so a future caller wired against this declared shape would silently break the preventDefault gate.

    • Fix: Change the prop declaration to onRemoveLastChip?: () => boolean | void to match SQLInlineEditor and AutocompleteInput.
    • kieran-typescript, api-contract, maintainability, adversarial
  • packages/app/src/components/SearchInput/InlineFilterChips.module.scss — The component relies on the input's collapsed max-height with overflow: hidden; chips beyond ~one row are silently clipped, replacing the deleted ActiveFilterPills MAX_VISIBLE_PILLS = 8 +N more affordance, so users with many filters have no in-input indication that additional filters are active until they focus the input.

    • Fix: Either render a trailing +N more counter chip when overflow occurs or expand the chip row on hover/focus the way the previous component did.
    • correctness, kieran-typescript
  • packages/app/src/components/SQLEditor/SQLInlineEditor.tsx:290 — The CodeMirror Backspace keymap branch (from === 0 && to === 0 && onRemoveLastChipRef.current()) is an entirely separate code path from the Lucene/textarea handler and is not covered by any unit or e2e test, so a regression where the keymap stops consuming the keystroke would not be caught.

    • Fix: Add an e2e case in filter-chips.spec.ts that switches to SQL mode after seeding chips and asserts that Backspace at position 0 removes the last chip.
    • testing, adversarial
  • packages/app/tests/e2e/features/search/filter-chips.spec.ts — The PR description references #2314 (source switching), but the e2e suite only covers SQL↔Lucene language switching; no test verifies that chips persist or prune correctly when the data source changes (logs↔traces), where the underlying retainFiltersByColumns path runs.

    • Fix: Add an e2e case that applies a chip, switches the source via the source picker, and asserts the expected chip state for both retained and pruned columns.
    • testing
🔵 P3 nitpicks (3)
  • packages/app/src/components/SearchInput/InlineFilterChips.tsx:51 — Keyboard-only users who Tab to the chip's ActionIcon and press Enter unmount the focused button, leaving focus on document.body and breaking the navigation flow inside the WHERE input.

    • Fix: After onRemove fires, programmatically focus the host textarea/CodeMirror editor (or the next chip's remove button).
    • julik-frontend-races, agent-native, adversarial
  • packages/app/src/components/SearchInput/InlineFilterChips.tsx:85 — The chip group container has no role="list" or aria-label, so assistive tech and agent drivers using the accessibility tree cannot discover that the region is a list of removable filters.

    • Fix: Add role="list" aria-label="Active filters" on the group <div> and role="listitem" on each chip span.
    • agent-native
  • packages/app/src/components/SearchInput/__tests__/InlineFilterChips.test.tsx:108 — The "prevents default on mouseDown" assertion fires mousedown on the X ActionIcon button, but the more important blur-prevention onMouseDown={e => e.preventDefault()} lives on the chip wrapper <span> (InlineFilterChips.tsx:32); removing that wrapper handler would not fail this unit test.

    • Fix: Add an assertion that dispatches mousedown on the data-testid="filter-chip" span and verifies preventDefault is called.
    • testing

Reviewers (10): correctness, testing, maintainability, project-standards, agent-native, learnings-researcher, kieran-typescript, julik-frontend-races, api-contract, adversarial.

Testing gaps:

  • No coverage for rapid (held) Backspace at position 0 — the toggle-back race only manifests under sub-frame key-repeat that fireEvent.keyDown does not simulate.
  • No coverage for IME-composition + Backspace at position 0.
  • No coverage for keyboard-only chip removal (Tab to X, Enter, assert focus restoration).
  • No coverage for chip values containing special characters (quotes, backslashes) that could break the Playwright [data-value="…"] selector in FilterChipsComponent.ts.
  • No coverage for the boolean-rawValue chip-removal roundtrip through coerceBooleanValue.

- SCSS: collapse `:not(.expanded):not([data-has-chips])` into a single
  complex `:not(.expanded, [data-has-chips])` to satisfy stylelint's
  `selector-not-notation` rule.
- Unit: remove the `@/components/ActiveFilterPills` jest.mock from
  DBSearchPage.directTrace.test.tsx since the component was deleted.
- E2E: the sidebar writes filters in Lucene form (`field:"value"`), not
  SQL `'value'`, so the URL-sync wait check in `applySidebarFilter`
  never matched. Switch to a value-substring check and bump the timeout
  to 10s for slower CI environments.
@elizabetdev
Copy link
Copy Markdown
Contributor

@MikeShi42 can you take a look at this PR? #2360 or HDX-4381

Previously, when a user switched the active source on the search page (e.g. Logs → Traces), any filters with fields not present in the new source were silently dropped. A transient yellow toast notified the user, but their selection and the context for switching back was lost.

The idea of the PR ##2360 was to keep those filters visible in the ActiveFilterPills bar using a muted, strikethrough, dashed-border style, along with a tooltip explaining why they aren’t applied. The filters are excluded from the generated query to keep it valid, and they automatically reapply if the user switches back to a compatible source.

Would be possible to achieve something similar in this PR? Do you think that proposal makes sense?

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

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants