-
[OPIK-5087] [FE] feat: filter chip framework for Logs Traces/Spans + Threads (#6854)
发布于
2026-06-01 07:28:05 +00:00 - [OPIK-5087] [FE] feat: filter chip framework for Logs Traces/Spans + Threads
Introduces the new chip-based filter framework in
apps/opik-frontend/src/shared/filter-chips/and wires it into the
v2 Logs Traces/Spans and Threads tabs.Highlights:
- Six chip kinds: single-select, pseudo-search, boolean, numeric, time,
query-builder. Each ships its own popover, value model,toFilters,
andisAppliedpredicate. FilterChipBar+FilterManagerPopover(Pinned / All filters) wired
to URL state with localStorage mirroring per project; "Clear all" and
per-chip clear.- Visual states (default / hover / open / applied) aligned with Figma,
including dark-mode bg-background fixes and "+ Add filter" trigger. - TimeChip pill renders date-only when time is midnight; between mode
uses YYYY/MM/DD - YYYY/MM/DD with no time. Popover keeps date+time
inputs in all modes per Figma popover spec. - NumericChip pads decimals on blur (shared
padDecimalsStringutil);
between mode validates min ≤ max with inline error. - QueryBuilder pills smart-trim path-aware keys and values, multi-row
tooltip joined with AND. First-key auto-focus suppressed when opening
the popover with existing rows. - Traces/Spans tag cells become click-to-filter, adding to the Tags
chip directly from the row.
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
-
- self review fixes
-
refactor(filter-chips): debounce QueryBuilder TextCell + NumericCell
Both cells were calling onChange on every keystroke, which writes to
the URL via chipsToFilters → setRawFilters per character. Switch to
DebounceInput so URL writes (and downstream queryKey hashes) settle on
a 300ms pause; blur flushes immediately.To preserve the bare-input look the cells rely on, extend Input with a
variant="unstyled" + dimension="none" pair that emits no classes —
DebounceInput inherits via InputProps. Existing default/ghost/sm
consumers unchanged. The styled base shared between default and ghost
is factored into a STYLED_BASE constant to avoid duplication.Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
- refactor(filter-chips): move trimValue + toNumber to lib/helpers
Both helpers were chip-scoped (PseudoSearchChip.logic / NumericChip.logic)
but pure generics. Move them to shared/filter-chips/lib/helpers.ts next
to the existing sanitizeFilters/chipsToFilters lib files so the pseudo-
search popover can drop its inlineString(raw ?? "").trim()and reuse
the same helper as the chip's from-filters logic.Addresses baz-reviewer feedback on PR #6854 (handleChange duplicating
trimValue).Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
- fix(filter-chips): guard toNumber against non-string filter values
Filter["value"] is typed as string|number, but rawFilters comes from
URL JsonParam — JSON.parse can hand back null/false/objects. The string
branch called raw.trim() unconditionally and crashed on those. Add a
typeof string guard so malformed values return null and flow into the
sanitizer's invalid_value bucket instead.Addresses baz-reviewer feedback on PR #6854.
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
- test(filter-chips): cover useFilterChips hook end-to-end
25 cases across 10 groups: initial state, URL deep-link → values via
sanitizeFilters, the chipsPinned = pinnedIds ∪ appliedIds union,
applyValue (URL write + auto-pin), clearValue (URL only, no pin
change), clearAll, pinChip (idempotent), unpinChip (clears URL too),
onChange firing semantics, and the sanitize console.info side effect.useQueryParam and useLocalStorageState are mocked at module level so
the tests cover the hook's wiring without spinning up providers.Addresses baz-reviewer feedback on PR #6854 (no unit tests for the
pinned/unpinned partitioning logic).Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
-
refactor(traces-spans): rename isProjectId to hasProjectId in usePathsOptions
-
fix(filter-chips): NumericChip clears applied filter on empty input
-
refactor(filter-chips): extract FilterChipPopover shell
-
feat(filter-chips): product feedback iteration for OPIK-5087
Bundle of UX + structural polish on the v2 Logs chip framework based on
product walkthrough:Pinning / manager popover
- Newly pinned/applied chips are appended to the right (pin-order),
no longer follow declaration order - Search input added to FilterManagerPopover with sticky header and
scrollable body; refactored to use DropdownMenu + DropdownMenuItem
primitives (ColumnsButton / ProjectSelector pattern) - "Pinned filters" / "Available filters" labels per Figma; 32px items
Chip visuals
- Inline X clear button shown on chip hover (CircleX, "Clear" tooltip);
lives in ChipShell as a sibling button when applied + onClear given - BooleanChip adopts the new X pattern, drops its inline CircleX
- TimeChip summary uses operator symbols (=, <, >) matching NumericChip
TimeChip popover rebuild
- Single source of truth in Slot (date, dateText, dateTouched, time,
timeText, timeTouched, period); DropdownMenuItem-style controls - DebounceInput kept; commit always mirrors typed text into slot
(TimeRow pattern) so DebounceInput.value stays in sync - Validation gates the warning border behind first blur; URL writes
only on semantic delta - parseDateInput accepts dash/dot separators + YYMMDD (21st century)
- Calendar pick auto-fills a per-mode default time so date-only picks
apply naturally
SingleSelect / dropdowns
- SingleSelectChip switched from Popover to DropdownMenu (project-
selector pattern) — keyboard nav for free; selected styling via
DropdownMenuItem size=sm + selected prop (blue highlight, no check) - Sticky search + scrollable list inside the dropdown
Defaults / operators
- Numeric chips default to "At least" (NUMERIC_DEFAULT_MODE constant)
- Time chips default to "After" (TIME_DEFAULT_MODE constant)
- Metadata query-builder chip defaults to "contains" operator
Buttons / link styling
- "+ Add metadata" and "Clear (N)" in QueryFilterShell switched to
ghost variant with text-foreground / hover:text-primary; Clear is
disabled when nothing to clear - PopoverClearFooter (Numeric / Time / PseudoSearch) aligned to the
same convention and accepts a disabled prop
QueryBuilder row polish
- FilterRow gets rounded-[4px] container per Figma
- Cells use position:relative + focus:z-10 so the focused cell's
border lands on top of -ml-px overlapping borders - Autocomplete's cmdk wrapper uses display:contents so the
inner input becomes a real layout-child of FilterRow
-
UX feedback second round
-
feat(filter-chips): focus-driven calendar popover on time date input
- focusing the date input opens the calendar popover
- clicking the calendar icon focuses the input, selects existing text, and opens the calendar
- typing an editing key closes the calendar
- picking a date applies it and returns focus to the input
- Slot A auto-focuses on chip open so the calendar opens automatically
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
- fix(filter-chips): time date input keyboard, paste, and icon a11y
- skip modifier-key shortcuts (Cmd+A, Ctrl+C, etc.) from closing the calendar
- close the calendar on paste (Cmd+V or context-menu paste)
- restore aria-haspopup/aria-expanded on the calendar icon button
- flatten handleKeyDown's Enter branch
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
- feat(filter-chips): OPIK-5087 product feedback iteration
- analytics tracking for filter chip events (apply / clear / pin / unpin / dialog open + close)
- chip definitions composed via an order array + byId record (replaces the splice-by-id pattern in TracesSpansTab / ThreadsTab)
- query builder shows a focused row on open; the search step is dropped and the local Autocomplete / QueryFilterShell layers are folded into AutocompleteCell / popover content
- numeric chip focuses its input on open and on operator change (first-empty in between mode)
- time chip focuses the time input after a date pick and the first-empty date input after a mode switch
- filter manager defers opening the chip popover until the manager is fully closed (no focus race)
- chip autocompletes scoped to the logs source
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
- fix(filter-chips): make chip popovers modal to prevent cell-tooltip dismiss
Hovering text in a DataTable TextCell opens a Radix Popover (the table's
cell-tooltip via DataTableTooltipContext). On open, Radix's default
onOpenAutoFocus moved focus into that tooltip — the chip popover saw
focus-outside and dismissed itself. SingleSelectChip was immune because
it uses DropdownMenu, which is modal by default and traps focus.Marking the chip Popover as modal traps focus inside the chip while it's
open, so the cell-tooltip's focus grab is blocked and the chip popover
stays open. Tab cycles within the popover and the rest of the page
becomes aria-inert until close, both standard modal-popover behaviors
acceptable for an inline filter editor.Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
- fix(filter-chips): post-review fixes (boolean pin tracking, autocomplete empty-blur, trace/span dynamic chip dedup)
- useFilterChips.applyValue now fires analytics.trackPinned when it
implicitly adds an id to pinnedIds. Previously, boolean-filter
selections (and any apply path that also pins) went via this branch
without firing FILTER_PINNED — leaving pinnedDuringOpenRef false, so
FILTER_DIALOG_CLOSED_WITHOUT_SELECTION mis-fired when the manager
dropdown closed. - AutocompleteCell.commit no longer early-returns on an empty trimmed
draft. Clearing the field and blurring now calls onChange("") so the
parent row's key/value clears too; previously the input went visually
empty while the applied filter stayed stale. - TracesSpansTab: extract buildSharedDynamicChips helper so the shared
set (tags, error_type, feedback_scores, metadata, custom, guardrails)
is declared once. The trace useMemo spreads it and adds
span_feedback_scores; the span useMemo prepends the type chip.
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
- fix(filter-chips): boolean manager-select pins explicitly; no double FILTER_PINNED
Previous fix added an analytics.trackPinned() inside useFilterChips.applyValue,
which caused a double-fire of FILTER_PINNED in useTagsChipActions.addTag —
that path calls applyValue() then pinChip() back-to-back, and because React
hadn't re-rendered between the two calls, the !pinnedIds.includes(id) guard
in applyValue still passed and trackPinned fired before pinChip fired it
again.Revert applyValue to its single-responsibility shape (no trackPinned). Move
the missing-event fix to the only path that actually needed it: route the
boolean branch in FilterManagerPopover.handleSelectUnpinned through
onPinChip before onApplyValue, mirroring the non-boolean path. trackPinned
now fires once for any manager-driven selection (boolean or otherwise),
pinnedDuringOpenRef gets set, and FILTER_DIALOG_CLOSED_WITHOUT_SELECTION
no longer mis-fires when the user picks a boolean filter.Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
- style(filter-chips): UX iteration — All-filters trigger, manager dropdown polish, query-builder Figma alignment
FilterChipBar trigger:
- Rename "Add filter" → "All filters", swap Plus → ListFilter icon,
bump right padding to 8px (className="pr-2").
FilterManagerPopover:
- Drop the redundant "All filters" section heading (the trigger label
already conveys this); unpinned items still render below the separator. - Pinned-row icon transitions from text-light-slate to text-foreground
on row hover (alongside the existing Pin → PinOff swap) for a more
prominent unpin affordance.
QueryBuilder operator labels:
- Sentence-case operator labels for keyless chips (Tags, Error type) via
lodash/upperFirst since the operator starts the query line: "Contains",
"Does not contain", "Equals". Keyed chips (metadata, feedback_scores,
custom filter) keep lowercase mid-row labels.
QueryBuilder autocomplete dropdown — align with Figma:
- Add "Recently used" CommandGroup heading; matched items render below.
- Item rows: comet-body-s-accented (Inter Medium 14px / 20px), h-8 px-4
flex items-center, foreground color, primary-foreground bg on select,
highlight match in primary-active. - No-match row: Inter Regular 14px / 20px, text-light-slate (#94A3B8),
h-auto min-h-8 py-1.5 (allow wrap on narrow popovers), left-aligned,
plain hyphen, copy "No match in recent {itemNoun} - type your {itemNoun}
to search all". - Override CommandItem's base data-[selected=true]:text-accent-foreground
on the no-match row so the color stays light-slate when cmdk
auto-highlights it. - No-match Enter routes through pick(draft): commits the typed key and
advances focus to the value input via the existing onPick callback.
QueryBuilder row removal:
- Drop the disableRemove={rows.length === 1} guard. Clicking X on the
only row now clears the whole filter — commit([]) → applied is empty
→ onClear() → chip-level handleClear closes the popover and clears
the URL filter.
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
Co-authored-by: Claude Opus 4.7 noreply@anthropic.com
下载附件