Skip to content

Commit a45b3cf

Browse files
feat: Add collapsible filter sidebar toggle to search page (#1975)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent ea44d05 commit a45b3cf

5 files changed

Lines changed: 130 additions & 46 deletions

File tree

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: Add collapsible filter sidebar toggle to search page

agent_docs/code_style.md

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,25 @@ The project uses Mantine UI with **custom variants** defined in `packages/app/sr
3737
| `variant="secondary"` | Secondary actions (Cancel, Clear, auxiliary actions) | `<Button variant="secondary">Cancel</Button>` |
3838
| `variant="danger"` | Destructive actions (Delete, Remove, Rotate API Key) | `<Button variant="danger">Delete</Button>` |
3939
| `variant="link"` | Link-style actions with no background or border (View Details, navigation-style CTAs) | `<Button variant="link">View Details</Button>` |
40+
| `variant="subtle"` | **ActionIcon only.** Transparent background with hover highlight; for toolbar/utility icons that shouldn't draw attention until hovered (collapse toggles, close buttons, auxiliary controls) | `<ActionIcon variant="subtle">...</ActionIcon>` |
4041

41-
### DO NOT USE (Forbidden Patterns)
42+
### Correct Usage
4243

43-
The following patterns are **NOT ALLOWED** for Button and ActionIcon:
44+
```tsx
45+
<Button variant="primary">Save</Button>
46+
<Button variant="secondary">Cancel</Button>
47+
<Button variant="danger">Delete</Button>
48+
<Button variant="link">View Details</Button>
49+
<ActionIcon variant="primary">...</ActionIcon>
50+
<ActionIcon variant="secondary">...</ActionIcon>
51+
<ActionIcon variant="danger">...</ActionIcon>
52+
<ActionIcon variant="link">...</ActionIcon>
53+
<ActionIcon variant="subtle">...</ActionIcon>
54+
```
55+
56+
### DO NOT USE (Forbidden Patterns)
4457

4558
```tsx
46-
// ❌ WRONG - Don't use these
4759
<Button variant="light" color="green">Save</Button>
4860
<Button variant="light" color="gray">Cancel</Button>
4961
<Button variant="light" color="red">Delete</Button>
@@ -54,20 +66,12 @@ The following patterns are **NOT ALLOWED** for Button and ActionIcon:
5466
<Button variant="default">Cancel</Button>
5567
<ActionIcon variant="light" color="red">...</ActionIcon>
5668
<ActionIcon variant="filled" color="gray">...</ActionIcon>
57-
58-
// ✅ CORRECT - Use custom variants
59-
<Button variant="primary">Save</Button>
60-
<Button variant="secondary">Cancel</Button>
61-
<Button variant="danger">Delete</Button>
62-
<Button variant="link">View Details</Button>
63-
<ActionIcon variant="primary">...</ActionIcon>
64-
<ActionIcon variant="secondary">...</ActionIcon>
65-
<ActionIcon variant="danger">...</ActionIcon>
66-
<ActionIcon variant="link">...</ActionIcon>
6769
```
6870

6971
**Link variant details**: Renders with no background, no border, and muted text color. On hover, text brightens to full contrast. Use for link-style CTAs that should blend into surrounding content (e.g., "View Details", "View Full Trace").
7072

73+
**Subtle variant details (ActionIcon only)**: Transparent background with standard text color. On hover, a subtle background highlight appears (`--color-bg-hover`). This is the **default** ActionIcon variant. Use for toolbar icons, collapse toggles, close buttons, and utility controls that should stay unobtrusive but reveal interactivity on hover. Unlike `link`, `subtle` shows a hover background rather than changing text color.
74+
7175
**Note**: `variant="filled"` is still valid for **form inputs** (Select, TextInput, etc.), just not for Button/ActionIcon.
7276

7377
### Icon-Only Buttons → ActionIcon

packages/app/src/DBSearchPage.tsx

Lines changed: 87 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import {
6464
import { notifications } from '@mantine/notifications';
6565
import {
6666
IconBolt,
67+
IconLayoutSidebarLeftExpand,
6768
IconPlayerPlay,
6869
IconPlus,
6970
IconTags,
@@ -171,6 +172,8 @@ const SearchConfigSchema = z.object({
171172

172173
type SearchConfigFromSchema = z.infer<typeof SearchConfigSchema>;
173174

175+
const QUERY_KEY_PREFIX = 'search';
176+
174177
// Helper function to get the default source id
175178
export function getDefaultSourceId(
176179
sources: { id: string }[] | undefined,
@@ -261,6 +264,46 @@ function SearchSubmitButton({
261264
);
262265
}
263266

267+
function ExpandFiltersButton({ onExpand }: { onExpand: () => void }) {
268+
return (
269+
<Tooltip label="Show filters" position="bottom">
270+
<ActionIcon
271+
variant="subtle"
272+
size="xs"
273+
onClick={onExpand}
274+
aria-label="Show filters"
275+
>
276+
<IconLayoutSidebarLeftExpand size={14} />
277+
</ActionIcon>
278+
</Tooltip>
279+
);
280+
}
281+
282+
function SearchResultsCountGroup({
283+
isFilterSidebarCollapsed,
284+
onExpandFilters,
285+
histogramTimeChartConfig,
286+
enableParallelQueries,
287+
}: {
288+
isFilterSidebarCollapsed: boolean;
289+
onExpandFilters: () => void;
290+
histogramTimeChartConfig: BuilderChartConfigWithDateRange;
291+
enableParallelQueries?: boolean;
292+
}) {
293+
return (
294+
<Group gap={4} align="center">
295+
{isFilterSidebarCollapsed && (
296+
<ExpandFiltersButton onExpand={onExpandFilters} />
297+
)}
298+
<SearchTotalCountChart
299+
config={histogramTimeChartConfig}
300+
queryKeyPrefix={QUERY_KEY_PREFIX}
301+
enableParallelQueries={enableParallelQueries}
302+
/>
303+
</Group>
304+
);
305+
}
306+
264307
function SearchNumRows({
265308
config,
266309
enabled,
@@ -278,7 +321,7 @@ function SearchNumRows({
278321

279322
const numRows = data?.[0]?.rows;
280323
return (
281-
<Text size="xs" mb={4}>
324+
<Text size="xs">
282325
{isLoading
283326
? 'Scanned Rows ...'
284327
: error || !numRows
@@ -812,6 +855,9 @@ function DBSearchPage() {
812855
}
813856
}, [analysisMode, setIsLive]);
814857

858+
const [isFilterSidebarCollapsed, setIsFilterSidebarCollapsed] =
859+
useLocalStorage<boolean>('isFilterSidebarCollapsed', false);
860+
815861
const [denoiseResults, _setDenoiseResults] = useQueryState(
816862
'denoise',
817863
parseAsBoolean.withDefault(false),
@@ -1174,8 +1220,6 @@ function DBSearchPage() {
11741220

11751221
const [newSourceModalOpened, setNewSourceModalOpened] = useState(false);
11761222

1177-
const QUERY_KEY_PREFIX = 'search';
1178-
11791223
const isAnyQueryFetching =
11801224
useIsFetching({
11811225
queryKey: [QUERY_KEY_PREFIX],
@@ -1761,33 +1805,43 @@ function DBSearchPage() {
17611805
height: '100%',
17621806
}}
17631807
>
1764-
<ErrorBoundary message="Unable to render search filters">
1765-
<DBSearchPageFilters
1766-
denoiseResults={denoiseResults}
1767-
setDenoiseResults={setDenoiseResults}
1768-
isLive={isLive}
1769-
analysisMode={analysisMode}
1770-
setAnalysisMode={setAnalysisMode}
1771-
chartConfig={filtersChartConfig}
1772-
sourceId={inputSourceObj?.id}
1773-
showDelta={
1774-
!!(searchedSource?.kind === SourceKind.Trace
1775-
? searchedSource.durationExpression
1776-
: undefined)
1777-
}
1778-
onColumnToggle={toggleColumn}
1779-
displayedColumns={displayedColumns}
1780-
{...searchFilters}
1781-
/>
1782-
</ErrorBoundary>
1808+
{!isFilterSidebarCollapsed && (
1809+
<ErrorBoundary message="Unable to render search filters">
1810+
<DBSearchPageFilters
1811+
denoiseResults={denoiseResults}
1812+
setDenoiseResults={setDenoiseResults}
1813+
isLive={isLive}
1814+
analysisMode={analysisMode}
1815+
setAnalysisMode={setAnalysisMode}
1816+
chartConfig={filtersChartConfig}
1817+
sourceId={inputSourceObj?.id}
1818+
showDelta={
1819+
!!(searchedSource?.kind === SourceKind.Trace
1820+
? searchedSource.durationExpression
1821+
: undefined)
1822+
}
1823+
onColumnToggle={toggleColumn}
1824+
displayedColumns={displayedColumns}
1825+
onCollapse={() => setIsFilterSidebarCollapsed(true)}
1826+
{...searchFilters}
1827+
/>
1828+
</ErrorBoundary>
1829+
)}
17831830
{analysisMode === 'pattern' &&
17841831
histogramTimeChartConfig != null && (
17851832
<Flex direction="column" w="100%" gap="0px" mih="0" miw={0}>
17861833
<Box className={searchPageStyles.searchStatsContainer}>
1787-
<Group justify="space-between" style={{ width: '100%' }}>
1788-
<SearchTotalCountChart
1789-
config={histogramTimeChartConfig}
1790-
queryKeyPrefix={QUERY_KEY_PREFIX}
1834+
<Group
1835+
justify="space-between"
1836+
align="center"
1837+
style={{ width: '100%' }}
1838+
>
1839+
<SearchResultsCountGroup
1840+
isFilterSidebarCollapsed={isFilterSidebarCollapsed}
1841+
onExpandFilters={() =>
1842+
setIsFilterSidebarCollapsed(false)
1843+
}
1844+
histogramTimeChartConfig={histogramTimeChartConfig}
17911845
/>
17921846
<SearchNumRows
17931847
config={{
@@ -1855,11 +1909,15 @@ function DBSearchPage() {
18551909
<Box className={searchPageStyles.searchStatsContainer}>
18561910
<Group
18571911
justify="space-between"
1912+
align="center"
18581913
style={{ width: '100%' }}
18591914
>
1860-
<SearchTotalCountChart
1861-
config={histogramTimeChartConfig}
1862-
queryKeyPrefix={QUERY_KEY_PREFIX}
1915+
<SearchResultsCountGroup
1916+
isFilterSidebarCollapsed={isFilterSidebarCollapsed}
1917+
onExpandFilters={() =>
1918+
setIsFilterSidebarCollapsed(false)
1919+
}
1920+
histogramTimeChartConfig={histogramTimeChartConfig}
18631921
enableParallelQueries
18641922
/>
18651923
<Group gap="sm" align="center">

packages/app/src/components/DBSearchPageFilters.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
IconChevronRight,
3737
IconChevronUp,
3838
IconFilterOff,
39+
IconLayoutSidebarLeftCollapse,
3940
IconMinus,
4041
IconPin,
4142
IconPinFilled,
@@ -875,6 +876,7 @@ const DBSearchPageFiltersComponent = ({
875876
setFilterRange,
876877
onColumnToggle,
877878
displayedColumns,
879+
onCollapse,
878880
}: {
879881
analysisMode: 'results' | 'delta' | 'pattern';
880882
setAnalysisMode: (mode: 'results' | 'delta' | 'pattern') => void;
@@ -887,6 +889,7 @@ const DBSearchPageFiltersComponent = ({
887889
setFilterRange: (key: string, range: { min: number; max: number }) => void;
888890
onColumnToggle?: (column: string) => void;
889891
displayedColumns?: string[];
892+
onCollapse?: () => void;
890893
} & FilterStateHook) => {
891894
const setFilterValue = useCallback(
892895
(
@@ -1203,9 +1206,23 @@ const DBSearchPageFiltersComponent = ({
12031206
}}
12041207
>
12051208
<Stack gap="sm" p="xs">
1206-
<Text size="xxs" c="dimmed" fw="bold">
1207-
Analysis Mode
1208-
</Text>
1209+
<Flex align="center" justify="space-between">
1210+
<Text size="xxs" c="dimmed" fw="bold">
1211+
Analysis Mode
1212+
</Text>
1213+
{onCollapse && (
1214+
<Tooltip label="Hide filters" position="bottom">
1215+
<ActionIcon
1216+
variant="subtle"
1217+
size="xs"
1218+
onClick={onCollapse}
1219+
aria-label="Hide filters"
1220+
>
1221+
<IconLayoutSidebarLeftCollapse size={14} />
1222+
</ActionIcon>
1223+
</Tooltip>
1224+
)}
1225+
</Flex>
12091226
<Tabs
12101227
value={analysisMode}
12111228
onChange={value =>

packages/app/src/components/SearchTotalCountChart.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export default function SearchTotalCountChart({
103103
);
104104

105105
return (
106-
<Text size="xs" mb={4}>
106+
<Text size="xs" lh="normal">
107107
{isLoading ? (
108108
<span className="effect-pulse">&middot;&middot;&middot; Results</span>
109109
) : totalCount !== null && !isError ? (

0 commit comments

Comments
 (0)