Skip to content

Commit e1ecb62

Browse files
committed
feat: can save and enact filters
1 parent f6f217f commit e1ecb62

File tree

2 files changed

+184
-25
lines changed

2 files changed

+184
-25
lines changed

Diff for: packages/app/src/components/DBSearchPageFilters.tsx

+153-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import { useEffect, useMemo, useState } from 'react';
2-
import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types';
1+
import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
2+
import { parseAsJson, useQueryState } from 'nuqs';
3+
import objectHash from 'object-hash';
4+
import {
5+
ChartConfigWithDateRange,
6+
Filter,
7+
} from '@hyperdx/common-utils/dist/types';
38
import {
49
Box,
510
Button,
@@ -21,7 +26,7 @@ import { IconSearch } from '@tabler/icons-react';
2126
import { useAllFields, useGetKeyValues } from '@/hooks/useMetadata';
2227
import useResizable from '@/hooks/useResizable';
2328
import { useSearchPageFilterState } from '@/searchFilters';
24-
import { mergePath } from '@/utils';
29+
import { mergePath, useLocalStorage } from '@/utils';
2530

2631
import resizeStyles from '../../styles/ResizablePanel.module.scss';
2732
import classes from '../../styles/SearchPage.module.scss';
@@ -304,6 +309,149 @@ export const FilterGroup = ({
304309
);
305310
};
306311

312+
type SavedFilters = {
313+
[key: string]: Filter[];
314+
};
315+
316+
function SaveFilterInput() {
317+
const [savedFilters, setSavedFilters] = useLocalStorage<SavedFilters>(
318+
'hdx-saved-search-filters',
319+
{},
320+
);
321+
const [queryFilters] = useQueryState<Filter[]>(
322+
'filters',
323+
parseAsJson<Filter[]>(),
324+
);
325+
const [newFilterName, setNewFilterName] = useState('');
326+
const [showButton, setShowButton] = useState(true);
327+
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
328+
e.preventDefault();
329+
setNewFilterName(e.target.value);
330+
};
331+
const handleSubmit = (e: React.FormEvent) => {
332+
e.preventDefault();
333+
if (!queryFilters) return;
334+
const tmp = savedFilters;
335+
tmp[newFilterName] = queryFilters;
336+
setSavedFilters(tmp);
337+
};
338+
339+
return (
340+
<Flex pl="xs" py="xxs" mb="xs" className={classes.filterCheckbox}>
341+
{showButton ? (
342+
<UnstyledButton
343+
onClick={() => setShowButton(false)}
344+
className={classes.textButton}
345+
style={{ width: '100%' }}
346+
>
347+
<Text size="xs" c="gray.6" lh={1}>
348+
<b>+ Save Filter</b>
349+
</Text>
350+
</UnstyledButton>
351+
) : (
352+
<form onSubmit={handleSubmit}>
353+
<TextInput
354+
autoFocus
355+
onBlur={() => setShowButton(true)}
356+
placeholder="New Filter"
357+
onChange={handleChange}
358+
name="newFilterName"
359+
/>
360+
</form>
361+
)}
362+
</Flex>
363+
);
364+
}
365+
366+
export function SavedFilters() {
367+
const [queryFilters, setQueryFilters] = useQueryState(
368+
'filters',
369+
parseAsJson<Filter[]>(),
370+
);
371+
const [savedFilters, setSavedFilters] = useLocalStorage<SavedFilters>(
372+
'hdx-saved-search-filters',
373+
{},
374+
);
375+
const showSaveButton = useMemo(
376+
// true if no saved filter matches the current filters
377+
() =>
378+
queryFilters &&
379+
queryFilters.length > 0 &&
380+
!Object.entries(savedFilters).some(
381+
([_, filter]) =>
382+
objectHash.sha1(filter) === objectHash.sha1(queryFilters),
383+
),
384+
[queryFilters, savedFilters],
385+
);
386+
const removeFilter = useCallback(
387+
(label: string) => {
388+
const newFilters = structuredClone(savedFilters);
389+
delete newFilters[label];
390+
setSavedFilters(newFilters);
391+
},
392+
[savedFilters, setSavedFilters],
393+
);
394+
395+
const SavedFilterOption = ({
396+
label,
397+
filters,
398+
}: {
399+
label: string;
400+
filters: Filter[];
401+
}) => {
402+
const [isHovered, setIsHovered] = useState(false);
403+
const active = objectHash.sha1(filters) === objectHash.sha1(queryFilters);
404+
return (
405+
<Group
406+
key={label}
407+
justify="space-between"
408+
wrap="nowrap"
409+
onMouseOver={() => setIsHovered(true)}
410+
onMouseOut={() => setIsHovered(false)}
411+
className={classes.highlightRow}
412+
>
413+
<Text
414+
size="xs"
415+
c={active ? 'green' : 'gray.3'}
416+
w="100%"
417+
pl="xs"
418+
onClick={() => setQueryFilters(filters)}
419+
style={{ cursor: 'pointer', opacity: 0.8 }}
420+
>
421+
{label}
422+
</Text>
423+
{/* ONLY SHOW X IF HOVERING OVER THIS COMPONENT */}
424+
<UnstyledButton
425+
className={classes.highlightButton}
426+
style={{ visibility: isHovered ? 'inherit' : 'hidden' }}
427+
p="2px"
428+
onClick={() => removeFilter(label)}
429+
>
430+
<i className="bi bi-x"></i>
431+
</UnstyledButton>
432+
</Group>
433+
);
434+
};
435+
436+
return (
437+
<Stack gap={0}>
438+
{(Object.keys(savedFilters).length > 0 || showSaveButton) && (
439+
<Text size="xxs" c="dimmed" fw="bold">
440+
Saved Filters
441+
</Text>
442+
)}
443+
{Object.keys(savedFilters).length > 0 && (
444+
<Stack gap={0}>
445+
{Object.entries(savedFilters).map(([label, filters]) => (
446+
<SavedFilterOption key={label} label={label} filters={filters} />
447+
))}
448+
</Stack>
449+
)}
450+
{showSaveButton && <SaveFilterInput />}
451+
</Stack>
452+
);
453+
}
454+
307455
type FilterStateHook = ReturnType<typeof useSearchPageFilterState>;
308456

309457
export const DBSearchPageFilters = ({
@@ -443,6 +591,8 @@ export const DBSearchPageFilters = ({
443591
</Tabs.List>
444592
</Tabs>
445593

594+
<SavedFilters />
595+
446596
<Flex align="center" justify="space-between">
447597
<Flex className={isFacetsFetching ? 'effect-pulse' : ''}>
448598
<Text size="xxs" c="dimmed" fw="bold">

Diff for: packages/app/styles/SearchPage.module.scss

+31-22
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,37 @@
5656
}
5757
}
5858

59+
.highlightRow {
60+
&:hover {
61+
input {
62+
opacity: 1;
63+
}
64+
65+
background-color: $slate-950;
66+
67+
.filterActions {
68+
display: flex;
69+
}
70+
}
71+
}
72+
73+
.highlightButton {
74+
border-radius: 3px;
75+
76+
&:hover {
77+
background-color: $slate-800;
78+
color: $slate-200;
79+
}
80+
81+
&:active {
82+
background-color: $slate-700;
83+
color: $slate-100;
84+
}
85+
}
86+
5987
.filterCheckbox {
88+
@extend .highlightRow;
89+
6090
width: 100%;
6191
display: grid;
6292
grid-template-columns: 1fr 20px;
@@ -87,29 +117,8 @@
87117

88118
.textButton {
89119
padding: 2px 6px;
90-
border-radius: 3px;
91-
92-
&:hover {
93-
background-color: $slate-800;
94-
color: $slate-200;
95-
}
96-
97-
&:active {
98-
background-color: $slate-700;
99-
color: $slate-100;
100-
}
101-
}
102-
}
103120

104-
&:hover {
105-
input {
106-
opacity: 1;
107-
}
108-
109-
background-color: $slate-950;
110-
111-
.filterActions {
112-
display: flex;
121+
@extend .highlightButton;
113122
}
114123
}
115124
}

0 commit comments

Comments
 (0)