发布

  • [OPIK-5087] [FE] feat: filter chip framework for Logs Traces/Spans + Threads (#6854)

    frostbyte_neo 发布于 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,
      and isApplied predicate.
    • 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 padDecimalsString util);
      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 inline String(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

    下载附件