Skip to content

Commit 9da2d32

Browse files
authored
feat: Improve search and services dashboard filters (#1445)
Closes HDX-2977 Closes HDX-2602 # Summary This PR makes a few changes to improve the filter experience for users with large numbers of facet values. ## On the search page: 1. The limit for facet values upon clicking Load More is now 10k, up from 200. This limit is applied when Load More is explicitly clicked for a single filter key, the default limit on page load remains the same. 2. Because 10k values is too many to display without serious render lag (and 10k values is more than anyone wants to scroll through) we now impose a limit of 50 values displayed with a message encouraging users to search for values if some might be hiding. 3. When a user searches for a filter value, we now automatically load more, as presumably the value they're searching for is not already being displayed in the list 4. Filter values are sorted alphabetically when searching ### Bug Fix Previously, when a user selected `Load More` for a filter and then switched to a different source, all values from `Load more` would be displayed as values for the second source. This has been fixed, and the Loaded More values are cleared when switching sources. ### Demo https://github.com/user-attachments/assets/381a6366-25d9-401c-9310-fede75e9a793 ## On the services dashboard 1. ServiceNames are now queried from the selected time range, to avoid poor performance or timeouts on large data volumes 2. ServiceNames are now sorted alphabetically in the dropdown 3. We now show up to 10k service names, to match the search page filter value limit ## Future improvements Ideally, when a user searches for a filter value, we'd dispatch a new query searching for potentially matching values. This would ensure that users could find values outside of the new 10k value limit.
1 parent edfcea6 commit 9da2d32

File tree

3 files changed

+110
-52
lines changed

3 files changed

+110
-52
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
feat: Improve filter search

packages/app/src/ServicesDashboardPage.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,12 @@ function getScopedFilters({
110110
function ServiceSelectControlled({
111111
sourceId,
112112
onCreate,
113+
dateRange,
113114
...props
114115
}: {
115116
sourceId?: string;
116117
size?: string;
118+
dateRange: [Date, Date];
117119
onCreate?: () => void;
118120
} & UseControllerProps<any>) {
119121
const { data: source } = useSource({ id: sourceId });
@@ -134,7 +136,8 @@ function ServiceSelectControlled({
134136
],
135137
where: `${expressions?.service} IS NOT NULL`,
136138
whereLanguage: 'sql' as const,
137-
limit: { limit: 200 },
139+
limit: { limit: 10000 },
140+
dateRange,
138141
};
139142

140143
const { data, isLoading, isError } = useQueriedChartConfig(queriedConfig, {
@@ -145,7 +148,12 @@ function ServiceSelectControlled({
145148

146149
const values = useMemo(() => {
147150
const services =
148-
data?.data?.map((d: any) => d.service).filter(Boolean) || [];
151+
data?.data
152+
?.map((d: any) => d.service)
153+
.filter(Boolean)
154+
.sort((a, b) =>
155+
a.localeCompare(b, undefined, { sensitivity: 'base' }),
156+
) || [];
149157
return [
150158
{
151159
value: '',
@@ -165,6 +173,7 @@ function ServiceSelectControlled({
165173
placeholder="All Services"
166174
maxDropdownHeight={280}
167175
onCreate={onCreate}
176+
nothingFoundMessage={isLoading ? 'Loading more...' : 'No matches found'}
168177
/>
169178
);
170179
}
@@ -1360,6 +1369,7 @@ function ServicesDashboardPage() {
13601369
sourceId={sourceId}
13611370
control={control}
13621371
name="service"
1372+
dateRange={searchedTimeRange}
13631373
/>
13641374
<WhereLanguageControlled
13651375
name="whereLanguage"

packages/app/src/components/DBSearchPageFilters.tsx

Lines changed: 93 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,20 @@ import { groupFacetsByBaseName } from './DBSearchPageFilters/utils';
5050
import resizeStyles from '../../styles/ResizablePanel.module.scss';
5151
import classes from '../../styles/SearchPage.module.scss';
5252

53+
/* The initial number of values per filter to load */
54+
const INITIAL_LOAD_LIMIT = 20;
55+
56+
/* The maximum number of values per filter to load when "Load More" is clicked */
57+
const LOAD_MORE_LOAD_LIMIT = 10000;
58+
59+
/* The initial number of values per filter to render */
60+
const INITIAL_MAX_VALUES_DISPLAYED = 10;
61+
62+
/* The maximum number of values per filter to render at once after loading more */
63+
const SHOW_MORE_MAX_VALUES_DISPLAYED = 50;
64+
5365
// This function will clean json string attributes specifically. It will turn a string like
54-
// 'toString(ResourceAttributes.`hdx`.`sdk`.`version`)' into 'ResourceAttributes.hdx.sdk.verion'.
66+
// 'toString(ResourceAttributes.`hdx`.`sdk`.`version`)' into 'ResourceAttributes.hdx.sdk.version'.
5567
export function cleanedFacetName(key: string): string {
5668
if (key.startsWith('toString')) {
5769
return key
@@ -314,8 +326,6 @@ export type FilterGroupProps = {
314326
distributionKey?: string; // Optional key to use for distribution queries, defaults to name
315327
};
316328

317-
const MAX_FILTER_GROUP_ITEMS = 10;
318-
319329
export const FilterGroup = ({
320330
name,
321331
options,
@@ -376,6 +386,17 @@ export const FilterGroup = ({
376386
}
377387
}, [isDefaultExpanded]);
378388

389+
const handleSetSearch = useCallback(
390+
(value: string) => {
391+
setSearch(value);
392+
393+
if (value && !hasLoadedMore) {
394+
onLoadMore(name);
395+
}
396+
},
397+
[hasLoadedMore, name, onLoadMore],
398+
);
399+
379400
const {
380401
data: distributionData,
381402
isFetching: isFetchingDistribution,
@@ -403,11 +424,12 @@ export const FilterGroup = ({
403424
}
404425
}, [distributionError]);
405426

406-
const totalFiltersSize =
427+
const totalAppliedFiltersSize =
407428
selectedValues.included.size +
408429
selectedValues.excluded.size +
409430
(hasRange ? 1 : 0);
410431

432+
// Loaded options + any selected options that aren't in the loaded list
411433
const augmentedOptions = useMemo(() => {
412434
const selectedSet = new Set([
413435
...selectedValues.included,
@@ -421,18 +443,28 @@ export const FilterGroup = ({
421443
];
422444
}, [options, selectedValues]);
423445

424-
const displayedOptions = useMemo(() => {
446+
const displayedItemLimit = shouldShowMore
447+
? SHOW_MORE_MAX_VALUES_DISPLAYED
448+
: INITIAL_MAX_VALUES_DISPLAYED;
449+
450+
// Options matching search, sorted appropriately
451+
const sortedMatchingOptions = useMemo(() => {
452+
// When searching, sort alphabetically
425453
if (search) {
426-
return augmentedOptions.filter(option => {
427-
return (
428-
option.value &&
429-
option.value.toLowerCase().includes(search.toLowerCase())
454+
return augmentedOptions
455+
.filter(option => {
456+
return (
457+
option.value &&
458+
option.value.toLowerCase().includes(search.toLowerCase())
459+
);
460+
})
461+
.toSorted((a, b) =>
462+
a.value.localeCompare(b.value, undefined, { numeric: true }),
430463
);
431-
});
432464
}
433465

434-
// General Sorting of List
435-
augmentedOptions.sort((a, b) => {
466+
// When not searching, sort by pinned, selected, distribution, then alphabetically
467+
return augmentedOptions.toSorted((a, b) => {
436468
const aPinned = isPinned(a.value);
437469
const aIncluded = selectedValues.included.has(a.value);
438470
const aExcluded = selectedValues.excluded.has(a.value);
@@ -462,24 +494,22 @@ export const FilterGroup = ({
462494
// Finally sort alphabetically/numerically
463495
return a.value.localeCompare(b.value, undefined, { numeric: true });
464496
});
465-
466-
// If expanded or small list, return everything
467-
if (shouldShowMore || augmentedOptions.length <= MAX_FILTER_GROUP_ITEMS) {
468-
return augmentedOptions;
469-
}
470-
// Return the subset of items
471-
const pageSize = Math.max(MAX_FILTER_GROUP_ITEMS, totalFiltersSize);
472-
return augmentedOptions.slice(0, pageSize);
473497
}, [
474498
search,
475-
shouldShowMore,
476-
isPinned,
477499
augmentedOptions,
478-
selectedValues,
479-
totalFiltersSize,
500+
isPinned,
501+
selectedValues.included,
502+
selectedValues.excluded,
480503
distributionData,
481504
]);
482505

506+
// The subset of options to be displayed
507+
const displayedOptions = useMemo(() => {
508+
return sortedMatchingOptions.length <= displayedItemLimit
509+
? sortedMatchingOptions
510+
: sortedMatchingOptions.slice(0, displayedItemLimit);
511+
}, [sortedMatchingOptions, displayedItemLimit]);
512+
483513
// Simple highlight animation when checkbox is checked
484514
const handleChange = useCallback(
485515
(value: string) => {
@@ -501,10 +531,14 @@ export const FilterGroup = ({
501531
},
502532
[onChange, selectedValues],
503533
);
534+
535+
const isLimitingDisplayedItems =
536+
sortedMatchingOptions.length > displayedOptions.length;
537+
504538
const showShowMoreButton =
505539
!search &&
506-
augmentedOptions.length > MAX_FILTER_GROUP_ITEMS &&
507-
totalFiltersSize < augmentedOptions.length;
540+
augmentedOptions.length > INITIAL_MAX_VALUES_DISPLAYED &&
541+
totalAppliedFiltersSize < augmentedOptions.length;
508542

509543
return (
510544
<Accordion
@@ -581,7 +615,7 @@ export const FilterGroup = ({
581615
)}
582616
</>
583617
)}
584-
{totalFiltersSize > 0 && (
618+
{totalAppliedFiltersSize > 0 && (
585619
<TextButton
586620
label="Clear"
587621
onClick={() => {
@@ -616,7 +650,7 @@ export const FilterGroup = ({
616650
value={search}
617651
data-testid={`filter-search-${name}`}
618652
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
619-
setSearch(event.currentTarget.value)
653+
handleSetSearch(event.currentTarget.value)
620654
}
621655
rightSectionWidth={20}
622656
rightSection={<IconSearch size={12} stroke={2} />}
@@ -669,6 +703,19 @@ export const FilterGroup = ({
669703
</Text>
670704
</Group>
671705
) : null}
706+
{isLimitingDisplayedItems && (shouldShowMore || search) && (
707+
<Text size="xxs" ms={28} fs="italic">
708+
Search to see more
709+
</Text>
710+
)}
711+
{loadMoreLoading && (
712+
<Group m={6} gap="xs">
713+
<Loader size={12} color="gray" />
714+
<Text c="dimmed" size="xs">
715+
Loading more...
716+
</Text>
717+
</Group>
718+
)}
672719
{showShowMoreButton && (
673720
<div className="d-flex m-1">
674721
<TextButton
@@ -696,26 +743,18 @@ export const FilterGroup = ({
696743
{onLoadMore &&
697744
!showShowMoreButton &&
698745
!shouldShowMore &&
699-
!hasLoadedMore && (
746+
!hasLoadedMore &&
747+
!loadMoreLoading && (
700748
<div className="d-flex m-1">
701-
{loadMoreLoading ? (
702-
<Group m={6} gap="xs">
703-
<Loader size={12} color="gray" />
704-
<Text c="dimmed" size="xs">
705-
Loading more...
706-
</Text>
707-
</Group>
708-
) : (
709-
<TextButton
710-
display={hasLoadedMore ? 'none' : undefined}
711-
label={
712-
<>
713-
<span className="bi-chevron-right" /> Load more
714-
</>
715-
}
716-
onClick={() => onLoadMore(name)}
717-
/>
718-
)}
749+
<TextButton
750+
display={hasLoadedMore ? 'none' : undefined}
751+
label={
752+
<>
753+
<span className="bi-chevron-right" /> Load more
754+
</>
755+
}
756+
onClick={() => onLoadMore(name)}
757+
/>
719758
</div>
720759
)}
721760
</Stack>
@@ -845,16 +884,20 @@ const DBSearchPageFiltersComponent = ({
845884
}
846885
}, [chartConfig.dateRange, isLive]);
847886

887+
// Clear extra facets (from "load more") when switching sources
888+
useEffect(() => {
889+
setExtraFacets({});
890+
}, [sourceId]);
891+
848892
const showRefreshButton = isLive && dateRange !== chartConfig.dateRange;
849893

850-
const keyLimit = 20;
851894
const {
852895
data: facets,
853896
isLoading: isFacetsLoading,
854897
isFetching: isFacetsFetching,
855898
} = useGetKeyValues({
856899
chartConfig: { ...chartConfig, dateRange },
857-
limit: keyLimit,
900+
limit: INITIAL_LOAD_LIMIT,
858901
keys: keysToFetch,
859902
});
860903

@@ -894,7 +937,7 @@ const DBSearchPageFiltersComponent = ({
894937
dateRange,
895938
},
896939
keys: [key],
897-
limit: 200,
940+
limit: LOAD_MORE_LOAD_LIMIT,
898941
disableRowLimit: true,
899942
});
900943
const newValues = newKeyVals[0].value;

0 commit comments

Comments
 (0)