Skip to content

feat(search): edit a filter pill's value in place#2457

Closed
alex-fedotyev wants to merge 6 commits into
alex/hdx-4326-editable-filter-pillsfrom
alex/hdx-4326-pill-value-edit
Closed

feat(search): edit a filter pill's value in place#2457
alex-fedotyev wants to merge 6 commits into
alex/hdx-4326-editable-filter-pillsfrom
alex/hdx-4326-pill-value-edit

Conversation

@alex-fedotyev

Copy link
Copy Markdown
Contributor

Add a value picker to the filter-pill action menu so a filter can be switched to a different value of the same field without removing and re-adding it.

Builds on #2455 (the pill action menu) and is stacked on that branch, so the diff here is just the value-editing piece. This finishes the second half of HDX-4326.

Summary

  • The pill action menu from feat(search): editable filter pills #2455 gains a searchable value Select above the copy and polarity actions. Picking a different value swaps the filter in place.
  • Values come from the shared useGetKeyValues hook, fetched only while the menu is open (enabled: opened) and scoped to the pill's field.
  • A new replaceFilterValue helper on useSearchPageFilterState does the swap in one state update, so the search query runs once. Two setFilterValue calls would each emit onFilterChange, triggering two query runs for one action.
  • Polarity is preserved: changing the value of an excluded pill keeps it excluded.
  • DBSearchPage passes the active source's filtersChartConfig to the pills for the value query.

Why

Together with #2455 this completes "editable filters" (HDX-4326): from a pill you can now flip include and exclude, copy, remove, and change the value, without rebuilding the filter from the sidebar.

The value swap lives in a dedicated hook helper rather than two setFilterValue calls because each call emits onFilterChange, which would run the query twice for one user action.

Test plan

  • make ci-lint (eslint + tsc) on @hyperdx/app
  • make ci-unit: searchFilters.test.ts (new) covers replaceFilterValue swapping an included value, swapping an excluded value, and emitting onFilterChange exactly once; ActiveFilterPills.test.tsx covers the value picker populating from useGetKeyValues and selecting a value calling replaceFilterValue with the right args and polarity (28 tests total)
  • Playwright: drove the menu (opened the value picker, confirmed it shows the current value and is searchable, in both light and dark themes)
  • States: the picker shows a "Loading values" message while the fetch is in flight and a "No values" empty message when there are none; the no-filters case still renders nothing
  • Layout: the menu is an overlay popover and the pill row keeps its existing wrap at narrow viewport widths

Note

This PR also converts six pre-existing em-dashes in searchFilters.tsx comments to commas. They are unrelated to the feature, but our prose lint flags them on any change to the file.

alex-fedotyev and others added 5 commits June 12, 2026 03:37
Make the active filter pills under the search bar editable in place.
Clicking an included or excluded pill opens a small action menu to copy
the value or flip the filter polarity (include vs exclude), reusing the
EventTag popover pattern and setFilterValue's existing include/exclude
actions. The one-click remove (x) on the pill is unchanged, and range
and unapplied pills keep their remove-only behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add the changeset for the editable filter pills change, and replace the
en-dash in the range pill separator with a hyphen to satisfy the
no-en-dash lint on the file.
Add a value picker to the filter-pill action menu so a user can switch a
filter to a different value of the same field without removing and
re-adding it. The picker is a searchable Select fed by useGetKeyValues
(fetched only while the menu is open), and a new replaceFilterValue
helper on useSearchPageFilterState swaps the value in one state update so
the query runs once. Polarity is preserved (an excluded pill stays
excluded). DBSearchPage passes the active source's chart config to the
pills for the value query.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
prose-lint flags these on any change to the file; converting to commas.
@changeset-bot

changeset-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 84bf4b4

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 Patch
@hyperdx/api Patch
@hyperdx/otel-collector Patch

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

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Jun 12, 2026 7:29pm
hyperdx-storybook Ready Ready Preview, Comment Jun 12, 2026 7:29pm

Request Review

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds an inline value picker to the filter-pill action menu, letting users swap a filter's value without removing and re-adding it. It introduces replaceFilterValue on useSearchPageFilterState to perform the swap as a single state update (one onFilterChange emission) and threads BuilderChartConfigWithDateRange down to each pill so useGetKeyValues can list all field values scoped only to the current time range.

  • searchFilters.tsx: New replaceFilterValue helper uses immer to atomically swap oldValuenewValue in the target (included/excluded) set and calls updateFilterQuery once.
  • ActiveFilterPills.tsx: FilterPill gains a Mantine Select (searchable, 220 px, 50-value cap) that fetches values with where/filters stripped so the picker isn't scoped to the current query; the polarity-preserving handleReplaceValue callback wires it to the new hook helper.
  • Tests: searchFilters.test.ts (new) and ActiveFilterPills.test.tsx cover the swap logic, polarity preservation, single-emission guarantee, and picker population.

Confidence Score: 5/5

Safe to merge; the value-picker feature is well-contained and the single-emission guarantee is correctly implemented and tested.

The new replaceFilterValue helper, the Select picker wiring, and the chartConfig threading are all implemented cleanly. The single-state-update guarantee (one query run per value swap) is correct, tests cover both polarities and the emission count, and the config stripping that prevents query-scoped value lists is verified. The chartConfig prop change is TypeScript-checked and the only non-mocked caller already supplies the value.

packages/app/src/searchFilters.tsx — the replaceFilterValue function was noted in a prior review thread as missing a cleanup of the opposite set when the new value already exists there; worth confirming that concern is addressed before shipping.

Important Files Changed

Filename Overview
packages/app/src/searchFilters.tsx Adds replaceFilterValue — atomically swaps a value in the included or excluded set; strips em-dashes from comments. The implementation does not clean the opposite set after adding newValue, which is a pre-existing concern flagged in the previous review thread.
packages/app/src/components/ActiveFilterPills.tsx Adds a Mantine Select value picker inside each editable pill's popover; fetches values only when the menu is open, strips where/filters from chartConfig to avoid query-scoping, and preserves polarity via the new handleReplaceValue callback. chartConfig is now a required prop.
packages/app/src/DBSearchPage.tsx Passes filtersChartConfig (always defined, with empty-string fallback) to ActiveFilterPills; a minimal one-line prop addition.
packages/app/src/searchFilters.test.ts New test file covering include-swap, exclude-swap, and single-emission guarantee for replaceFilterValue.
packages/app/src/components/tests/ActiveFilterPills.test.tsx Extended with four new tests: config stripping, picker population from useGetKeyValues, and polarity-preserving replace for both include and exclude cases.
.changeset/editable-filter-pills-value.md Changeset entry for the new value-picker feature; correctly classified as a patch bump.

Sequence Diagram

sequenceDiagram
    participant User
    participant FilterPill
    participant useGetKeyValues
    participant ActiveFilterPills
    participant replaceFilterValue
    participant updateFilterQuery

    User->>FilterPill: Click pill to open popover
    FilterPill->>useGetKeyValues: fetch field values with where and filters cleared
    useGetKeyValues-->>FilterPill: up to 50 values for the field
    FilterPill->>FilterPill: prepend current value to options list
    User->>FilterPill: Select a different value from picker
    FilterPill->>ActiveFilterPills: onReplaceValue with new value
    ActiveFilterPills->>replaceFilterValue: field, rawValue, newValue, polarity
    replaceFilterValue->>replaceFilterValue: immer delete oldValue then add newValue in target set
    replaceFilterValue->>updateFilterQuery: emit onFilterChange exactly once
    FilterPill->>FilterPill: close popover
Loading

Reviews (2): Last reviewed commit: "fix: show all field values when editing ..." | Re-trigger Greptile

Comment on lines +477 to +491
setFilters(prevFilters => {
const newFilters = produce(prevFilters, draft => {
if (!draft[property]) {
draft[property] = { included: new Set(), excluded: new Set() };
}
const set =
action === 'exclude'
? draft[property].excluded
: draft[property].included;
set.delete(oldValue);
set.add(newValue);
});
updateFilterQuery(newFilters);
return newFilters;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Cross-set contamination when new value already exists in the opposite set

replaceFilterValue only modifies the target set (included or excluded) but never removes newValue from the opposite set. If a user has status = 200 (included) and status != 404 (excluded), and then opens the include pill and picks 404, the result is included: {'404'} and excluded: {'404'} simultaneously. Both setFilterValue call paths (action === 'exclude' / default include) explicitly clean the other set before writing, but this new path does not.

The fix is to add a delete call on the opposite set after adding newValue:

const targetSet = action === 'exclude' ? draft[property].excluded : draft[property].included;
const oppositeSet = action === 'exclude' ? draft[property].included : draft[property].excluded;
targetSet.delete(oldValue);
targetSet.add(newValue);
oppositeSet.delete(newValue); // prevent value from existing in both sets

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

The value picker fetched its options with the search page's chart config,
which carries the active where clause and the filters (the pills
themselves). For an included pill the value query was scoped to rows
already matching that pill, so it only ever returned the pill's own value
and the picker had nothing to switch to. Clear where and filters before
the value query so it lists all of the field's values in the time range,
matching the sidebar facet list's default behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@alex-fedotyev

Copy link
Copy Markdown
Contributor Author

Folded into #2455, which now carries the full feature (the pill action menu plus in-place value editing) as a single PR. Closing here.

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