Skip to content
Open
7 changes: 7 additions & 0 deletions .changeset/search-filters-by-name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@hyperdx/app': patch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: This feels more like a minor than a patch

---

feat: search filters by name in the search page sidebar

Teams with a wide schema had no way to reach a filter that wasn't in the sidebar's default list, which shows low-cardinality columns and map sub-fields only. The new search box filters what is already rendered as you type, and runs a second query for matching fields the browse list never loaded, so a high-cardinality column is reachable by name.
108 changes: 93 additions & 15 deletions packages/app/src/components/DBSearchPageFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
Tooltip,
UnstyledButton,
} from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import {
IconArrowBarToLeft,
Expand Down Expand Up @@ -65,6 +66,7 @@ import {
import { useSource } from '@/source';
import { useLocalStorage } from '@/utils';

import { FilterKeySearch } from './DBSearchPageFilters/FilterKeySearch';
import { FilterSettingsPanel } from './DBSearchPageFilters/FilterSettingsPopover';
import { useFetchFacets } from './DBSearchPageFilters/hooks';
import { NestedFilterGroup } from './DBSearchPageFilters/NestedFilterGroup';
Expand All @@ -73,6 +75,7 @@ import {
PinShareMenu,
} from './DBSearchPageFilters/PinShareMenu';
import { SharedFiltersSection } from './DBSearchPageFilters/SharedFilters';
import { useGroupExpansion } from './DBSearchPageFilters/useGroupExpansion';
import {
getFilterStateEntry,
groupFacetsByBaseName,
Expand All @@ -88,6 +91,9 @@ const INITIAL_MAX_VALUES_DISPLAYED = 10;
/* The maximum number of values per filter to render at once after loading more */
const SHOW_MORE_MAX_VALUES_DISPLAYED = 50;

/* How long typing in the filter-name search settles before it queries */
const SEARCH_DEBOUNCE_MS = 300;

// This function will clean json string attributes specifically. It will turn a string like
// 'toString(ResourceAttributes.`hdx`.`sdk`.`version`)' into 'ResourceAttributes.hdx.sdk.version'.
export function cleanedFacetName(key: string): string {
Expand Down Expand Up @@ -400,6 +406,12 @@ export type FilterGroupProps = {
loadMoreLoading: boolean;
hasLoadedMore: boolean;
isDefaultExpanded?: boolean;
/**
* Holds the group open for as long as it is true, without touching the
* user's own expand/collapse state — so a filter-name search can reveal
* matches and the group snaps back when the search is cleared.
*/
isForceExpanded?: boolean;
showFilterCounts?: boolean;
'data-testid'?: string;
chartConfig: BuilderChartConfigWithDateRange;
Expand Down Expand Up @@ -898,14 +910,18 @@ export const FilterGroup = ({
loadMoreLoading,
hasLoadedMore,
isDefaultExpanded,
isForceExpanded,
showFilterCounts,
'data-testid': dataTestId,
chartConfig,
isLive,
distributionKey,
onRangeChange,
}: FilterGroupProps) => {
const [isExpanded, setExpanded] = useState(isDefaultExpanded ?? false);
const [expanded, setExpanded, expandBrowse] = useGroupExpansion(
isDefaultExpanded ?? false,
isForceExpanded,
);
const [showDistributions, setShowDistributions] = useState(false);
const [isFetchingDistribution, setIsFetchingDistribution] = useState(false);

Expand All @@ -923,17 +939,17 @@ export const FilterGroup = ({
}
return !prev;
});
}, []);
}, [setExpanded]);

const onDistributionError = useCallback(() => {
setShowDistributions(false);
}, []);

useEffect(() => {
if (isDefaultExpanded) {
setExpanded(true);
expandBrowse();
}
}, [isDefaultExpanded]);
}, [isDefaultExpanded, expandBrowse]);

const totalAppliedFiltersSize =
selectedValues.included.size +
Expand All @@ -951,7 +967,7 @@ export const FilterGroup = ({
variant="unstyled"
chevronPosition="left"
classNames={{ chevron: classes.chevron }}
value={isExpanded ? displayName : null}
value={expanded ? displayName : null}
onChange={v => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 major — Force-expand makes every group's collapse control a no-op during a search, and silently flips the stored state

expanded = isExpanded || !!isForceExpanded (line 921) leaves the Accordion permanently open while isSearching, so onChange at line 968 only writes setExpanded(false) — the panel does not close and the chevron does not rotate. Search service, every match expands, and none of them can be closed. Then clearing the search collapses any group the user clicked, including ones expanded before the search. Same in NestedFilterGroup.tsx:101/124. Let the click win: keep a local override set by onChange while isForceExpanded is true (reset when the force goes away), e.g. const expanded = isForceExpanded ? !forcedCollapse : isExpanded.

Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond

setExpanded(v === displayName);
}}
Expand Down Expand Up @@ -1024,7 +1040,7 @@ export const FilterGroup = ({
onRangeChange={onRangeChange}
/>
) : (
isExpanded && (
expanded && (
<FilterGroupBody
name={name}
options={options}
Expand Down Expand Up @@ -1170,10 +1186,16 @@ const DBSearchPageFiltersComponent = ({
);

const [showMoreFields, setShowMoreFields] = useState(false);
const [filterSearch, setFilterSearch] = useState('');
const [debouncedFilterSearch] = useDebouncedValue(
filterSearch.trim(),
SEARCH_DEBOUNCE_MS,
);
const {
data: fetchFacetsData,
isLoading: isFacetsLoading,
isFetching: isFacetsFetching,
isSearchFetching,
error,
loadMoreFacetsForKey,
loadMoreLoadingKeys,
Expand All @@ -1185,6 +1207,7 @@ const DBSearchPageFiltersComponent = ({
mode: showAllValues ? 'all' : 'exact',
filterState,
showMoreFields,
searchQuery: debouncedFilterSearch,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Clearing the search box leaves search-only facets in the browse list for 300 ms

isSearching flips to false the instant the input is emptied, so matchesFilterSearch stops filtering, but searchQuery is still the debounced old term for up to SEARCH_DEBOUNCE_MS, so escapedSearchKeys is non-empty and the previous page's facets are still unioned into shownFacets. The user sees high-cardinality fields (e.g. TraceId) sitting in the browse list and then vanishing. Bypass the debounce on empty: searchQuery: filterSearch.trim() ? debouncedFilterSearch : ''.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

});
const facets = fetchFacetsData.keyValues;

Expand Down Expand Up @@ -1342,6 +1365,34 @@ const DBSearchPageFiltersComponent = ({
sharedFilterKeys,
]);

// Applied against the raw input, not the debounced value: filtering what is
// already on screen costs nothing, so it should keep up with typing even
// while the query behind it is still settling.
const isSearching = filterSearch.trim().length > 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — A one-character search force-expands nearly every filter group for no benefit

isSearching is length > 0, but the backing query needs MIN_SEARCH_LENGTH = 2 (hooks.ts:42). Typing a single common letter (e) matches almost every column, so renderFacetList(visibleFacets, { isForceExpanded: true }) expands ~all groups at once — each rendering up to INITIAL_MAX_VALUES_DISPLAYED checkboxes into a non-virtualized Stack — while no search query has even run. Gate isForceExpanded on the same MIN_SEARCH_LENGTH (export it from hooks.ts so the two stay in sync), and/or only force-expand when the match count is small.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

// The debounce has not fired yet, so no query is in flight for what is
// currently typed. Without this the sidebar answers "No matching filters"
// for 300ms before it has asked anything.
const isSearchPending =
isSearching && filterSearch.trim() !== debouncedFilterSearch;
const isSearchBusy = isSearchPending || isSearchFetching;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Spinner and "Searching filters…" shown for inputs that will never be queried

isSearchBusy is true for 300 ms after any keystroke, but hooks.ts:42 refuses to query anything shorter than MIN_SEARCH_LENGTH, so a single character always shows a spinner (and "Searching filters…" when nothing matches locally) for a query that is never sent — and the component has no way to know why. Export the minimum from hooks.ts, gate isSearchPending on filterSearch.trim().length >= MIN_SEARCH_LENGTH, and say "Type at least 2 characters" below it.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

const matchesFilterSearch = useCallback(
(key: string) => {
const needle = filterSearch.trim().toLowerCase();
if (!needle) return true;
return cleanedFacetName(key).toLowerCase().includes(needle);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Search matches the raw bracket key, not the LogAttributes > service.version path the sidebar shows

Map sub-fields render as a nested group (LogAttributes) with child service.version (parseMapFieldName in DBSearchPageFilters/utils.ts). But the client filter matches cleanedFacetName(key), which leaves LogAttributes['service.version'] unchanged, and the server haystack in hooks.ts is also the bracket form. So typing logattributes.service.version or attributes.service, which follows the path the user sees, returns 'No matching filters' on both sides. Meanwhile dot-form keys that come from filterState/URL (LogAttributes.time) do match that input, so matching is inconsistent. Fix: normalize both sides the same way, e.g. parseKeyPath(cleanClickHouseExpression(key)).join('.'), and use one shared helper in matchesFilterSearch and in the haystack in hooks.ts so the two matchers can't drift.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The instant filter and the search query match different strings for JSON sub-fields

The hook strips backticks before matching, but matchesFilterSearch doesn't, so a JSON key stays as Body.`user`.`id` . Typing user.id seems like it would fetch the field and then hide it, and the sidebar shows "No matching filters". JSON facets already in the list would miss the same way.

IMO one helper in DBSearchPageFilters/utils.ts would fix it. Both files already import from there, so no cycle:

export const filterSearchHaystack = (key: string) =>
  cleanedFacetName(key).toLowerCase().replaceAll('`', '');

},
[filterSearch],
);

const visibleFacets = useMemo(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — No test covers the client-side name filter or the search empty and loading states

matchesFilterSearch, visibleFacets and visibleSharedFacets, the 'Searching filters…' / 'No matching filters' switch, and passing isForceExpanded into renderFacetList are new logic in the component, but only useFetchFacets and useGroupExpansion are tested, and in isolation. Add a component test that types into filter-key-search and checks three things: facets filter immediately, a group containing a match is expanded, and clearing the search brings back the full list with the previous expansion state.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

() => shownFacets.filter(facet => matchesFilterSearch(facet.key)),
[shownFacets, matchesFilterSearch],
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
);
const visibleSharedFacets = useMemo(
() => sharedFacets.filter(facet => matchesFilterSearch(facet.key)),
[sharedFacets, matchesFilterSearch],
);

// Check if shared facets have active selections
const showSharedClearButton = useMemo(
() =>
Expand Down Expand Up @@ -1421,10 +1472,17 @@ const DBSearchPageFiltersComponent = ({
const renderFacetList = useCallback(
(
facets: { key: string; value: (string | boolean)[] }[],
options?: { keyPrefix?: string; isDefaultExpanded?: boolean },
options?: {
keyPrefix?: string;
isDefaultExpanded?: boolean;
isForceExpanded?: boolean;
},
) => {
const { keyPrefix = '', isDefaultExpanded: forceExpanded } =
options ?? {};
const {
keyPrefix = '',
isDefaultExpanded: forceExpanded,
isForceExpanded,
} = options ?? {};
const { grouped, nonGrouped } = groupFacetsByBaseName(facets);

const makeValuePins = (key: string): ValuePinHandlers => ({
Expand Down Expand Up @@ -1516,6 +1574,7 @@ const DBSearchPageFiltersComponent = ({
);
})
}
isForceExpanded={isForceExpanded}
chartConfig={chartConfig}
isLive={isLive}
/>
Expand Down Expand Up @@ -1571,6 +1630,7 @@ const DBSearchPageFiltersComponent = ({
entry.range != null)))
);
})()}
isForceExpanded={isForceExpanded}
chartConfig={chartConfig}
isLive={isLive}
onRangeChange={range => setFilterRange(facet.key, range)}
Expand Down Expand Up @@ -1690,23 +1750,23 @@ const DBSearchPageFiltersComponent = ({

{isSharedFiltersVisible && (
<SharedFiltersSection
hasSharedFacets={sharedFacets.length > 0}
hasSharedFacets={visibleSharedFacets.length > 0}
opened={isSharedFiltersExpanded}
onToggle={() =>
setSharedFiltersExpanded(!isSharedFiltersExpanded)
}
showClearButton={showSharedClearButton}
onClearSelections={clearSharedSelections}
>
{renderFacetList(sharedFacets, {
{renderFacetList(visibleSharedFacets, {
keyPrefix: 'shared-',
isDefaultExpanded: true,
})}
</SharedFiltersSection>
)}

{/* Divider between shared and regular filters */}
{isSharedFiltersVisible && sharedFacets.length > 0 && (
{isSharedFiltersVisible && visibleSharedFacets.length > 0 && (
<Divider color="dark.4" />
)}

Expand Down Expand Up @@ -1827,17 +1887,35 @@ const DBSearchPageFiltersComponent = ({
/>
)}

<FilterKeySearch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Search input is nested inside the collapsible "Filters" section but also filters Shared Filters above it

FilterKeySearch renders inside <Collapse expanded={isFiltersExpanded}> (line 1822), yet visibleSharedFacets drives the Shared Filters section at lines 1750–1766, which sits outside that collapse. isFiltersExpanded is persisted in localStorage, so: type a search, collapse Filters — the Shared Filters section disappears (hasSharedFacets={visibleSharedFacets.length > 0}) with no visible control to clear the search. Either move FilterKeySearch above the Shared Filters section so it visibly owns both, or scope it to the regular Filters list only and leave sharedFacets unfiltered.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Search box is inside the Filters collapse but also filters the Shared Filters section outside it

FilterKeySearch is rendered inside <Collapse expanded={isFiltersExpanded}> (line 1825), while visibleSharedFacets drives SharedFiltersSection above it (lines 1751-1771). SharedFiltersSection returns null when hasSharedFacets is false (SharedFilters.tsx:43-45), so with a non-matching query typed and the Filters section then collapsed, the whole Shared Filters block disappears with the input that caused it hidden and no way to see or reset the query. Either clear filterSearch when isFiltersExpanded goes false, or move FilterKeySearch above the Filters header so it stays visible for as long as it affects anything outside the collapse.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

value={filterSearch}
onChange={setFilterSearch}
isFetching={isSearchBusy}
/>

{isFacetsLoading ? (
<Flex align="center" justify="center">
<Loader size="xs" color="gray" />
</Flex>
) : (
shownFacets.length === 0 && (
visibleFacets.length === 0 &&
(isSearching ? (
<Text size="xxs">
{isSearchBusy
? 'Searching filters…'
: 'No matching filters'}
</Text>
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
) : (
<Text size="xxs">No filters available</Text>
)
))
)}
{/* Show facets even when loading to ensure pinned filters are visible while loading */}
{renderFacetList(shownFacets)}
{renderFacetList(visibleFacets, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — No test covers any of the new UI behaviour

Seven tests cover the hook, but isForceExpanded, FilterKeySearch, and the Searching filters… / No matching filters branches have none — packages/app/src/components/__tests__/DBSearchPageFilters.test.tsx already has FilterGroup and NestedFilterGroup suites with expand/collapse assertions (lines 641-724) to extend. A test that renders FilterGroup with isForceExpanded and clicks filter-group-control would have caught finding 1.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

// A match buried in a collapsed group is a match the user
// cannot see, which defeats searching for it. Transient, so
// clearing the search restores the browse view as it was.
isForceExpanded: isSearching,
})}

<Button
variant="secondary"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { CloseButton, Loader, TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';

/**
* Search box for the filter sidebar, matching on filter (column) names rather
* than their values. Matches already on screen filter instantly; `isFetching`
* covers the query that goes looking for fields the browse list never loaded.
*/
export function FilterKeySearch({
value,
onChange,
isFetching,
}: {
value: string;
onChange: (value: string) => void;
isFetching: boolean;
}) {
return (
<TextInput
size="xs"
placeholder="Search filter column/properties"
aria-label="Search filter column/properties"
data-testid="filter-key-search"
value={value}
onChange={event => onChange(event.currentTarget.value)}
onKeyDown={event => {
if (event.key === 'Escape' && value) {
// Stop it bubbling to whatever else listens for Escape on the page;
// clearing the search is the more specific intent here.
event.stopPropagation();
onChange('');
}
}}
leftSection={<IconSearch size={12} stroke={2} />}
leftSectionWidth={24}
rightSection={

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — The clear button disappears while the search query is in flight

rightSection renders the Loader instead of the CloseButton whenever isFetching, so during a slow facet aggregation over 50 keys the ✕ is gone exactly when a user who mistyped wants it. Keep the CloseButton mounted whenever value is non-empty and put the spinner elsewhere (e.g. swap the leftSection icon for the Loader).

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Clear button is swapped out for the loader on every keystroke, so the search can't be cleared while busy

isFetching ? <Loader/> : value ? <CloseButton/> : null and the caller passes isSearchBusy, which is true from the first keystroke through the 300ms debounce and for the whole query. On a wide schema the 50-key value query takes seconds, and the X is absent for all of it; while typing continuously it never appears. Render the CloseButton whenever value is non-empty and put the Loader in leftSection (or use rightSectionWidth to show both) so clearing is always available.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Clear button is swapped out for the loader on every keystroke, so it is never clickable while typing

rightSection renders the Loader when isFetching and the CloseButton only otherwise, and the caller passes isSearchBusy = isSearchPending || isSearchFetching (DBSearchPageFilters.tsx:1377). isSearchPending is true from the first keystroke until 300ms after the last one, so the ✕ is absent for the entire time the user is typing and again for the duration of the query — it only appears once everything is idle, which is precisely when the user is least likely to want it. Render the CloseButton whenever value is non-empty and put the Loader in the left section (replacing or beside IconSearch) instead of alternating the two in the same slot.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

isFetching ? (
<Loader size={12} color="gray" />
) : value ? (
<CloseButton
size="xs"
aria-label="Clear filter search"
onClick={() => onChange('')}
/>
) : null
}
/>
);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useMemo, useRef, useState } from 'react';
import { useMemo, useRef } from 'react';
import { FilterState } from '@hyperdx/common-utils/dist/filters';
import { Accordion, Group, Text, Tooltip, UnstyledButton } from '@mantine/core';
import { useVirtualizer } from '@tanstack/react-virtual';

import { FilterGroup } from '@/components/DBSearchPageFilters';

import { useGroupExpansion } from './useGroupExpansion';

import classes from '@styles/SearchPage.module.scss';

type NestedFilterGroupProps = {
Expand Down Expand Up @@ -38,6 +40,12 @@ type NestedFilterGroupProps = {
loadMoreLoading: Record<string, boolean>;
hasLoadedMore: Record<string, boolean>;
isDefaultExpanded?: boolean;
/**
* Holds the group open for as long as it is true, without touching the
* user's own expand/collapse state — so a filter-name search can reveal
* matching children and the group snaps back when the search is cleared.
*/
isForceExpanded?: boolean;
'data-testid'?: string;
chartConfig: any; // Using any to avoid importing ChartConfigWithDateRange
isLive?: boolean;
Expand Down Expand Up @@ -69,6 +77,7 @@ export const NestedFilterGroup = ({
loadMoreLoading,
hasLoadedMore,
isDefaultExpanded,
isForceExpanded,
'data-testid': dataTestId,
chartConfig,
isLive,
Expand All @@ -88,26 +97,32 @@ export const NestedFilterGroup = ({
);

const hasSelections = totalFiltersSize > 0;
const [isExpanded, setExpanded] = useState(
const [expanded, setExpanded] = useGroupExpansion(
isDefaultExpanded ?? hasSelections,
isForceExpanded,
);

const scrollContainerRef = useRef<HTMLDivElement>(null);

const rowVirtualizer = useVirtualizer({
count: isExpanded ? childFilters.length : 0,
count: expanded ? childFilters.length : 0,
getScrollElement: () => scrollContainerRef.current,
estimateSize: () => VIRTUAL_ITEM_HEIGHT_ESTIMATE,
overscan: 10,
getItemKey: index => childFilters[index]?.key ?? index,
// Collapsing the whole Filters section measures every row at 0 while it is
// hidden; that zero is cached, so the total size stays 0 and the group
// reopens empty. Fall back to the estimate rather than cache a zero.
measureElement: el =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — The virtualizer zero-height fix and the whole search UI have no component-level test

useGroupExpansion.test.ts exercises the hook in isolation and hooks.test.tsx mocks useGetKeyValues, so nothing covers what the PR actually ships: the measureElement fallback, the isForceExpanded prop on FilterGroup/NestedFilterGroup, visibleFacets/visibleSharedFacets filtering, or the three-way Searching filters… / No matching filters / No filters available branch. The virtualizer fix is the cheapest to pin and the most likely to silently regress: jsdom reports every getBoundingClientRect().height as 0, which is exactly the condition the fallback guards, so an expanded NestedFilterGroup in __tests__/DBSearchPageFilters.test.tsx (which already renders one at line 668) asserting its child rows are still present after measurement would fail without the change. Add that case plus one isForceExpanded render assertion alongside the existing NestedFilterGroup tests.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

el.getBoundingClientRect().height || VIRTUAL_ITEM_HEIGHT_ESTIMATE,
});

return (
<Accordion
variant="unstyled"
chevronPosition="left"
classNames={{ chevron: classes.chevron }}
value={isExpanded ? name : null}
value={expanded ? name : null}
onChange={v => setExpanded(v === name)}
>
<Accordion.Item value={name} data-testid={dataTestId}>
Expand Down
Loading
Loading