feat: Add collapsible filter sidebar toggle to search page (#1975)

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Elizabet Oliveira 2026-03-25 15:44:56 +00:00 committed by GitHub
parent 8b629385f2
commit a6a83d59d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 130 additions and 46 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
feat: Add collapsible filter sidebar toggle to search page

View file

@ -37,13 +37,25 @@ The project uses Mantine UI with **custom variants** defined in `packages/app/sr
| `variant="secondary"` | Secondary actions (Cancel, Clear, auxiliary actions) | `<Button variant="secondary">Cancel</Button>` |
| `variant="danger"` | Destructive actions (Delete, Remove, Rotate API Key) | `<Button variant="danger">Delete</Button>` |
| `variant="link"` | Link-style actions with no background or border (View Details, navigation-style CTAs) | `<Button variant="link">View Details</Button>` |
| `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>` |
### Correct Usage
```tsx
<Button variant="primary">Save</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="danger">Delete</Button>
<Button variant="link">View Details</Button>
<ActionIcon variant="primary">...</ActionIcon>
<ActionIcon variant="secondary">...</ActionIcon>
<ActionIcon variant="danger">...</ActionIcon>
<ActionIcon variant="link">...</ActionIcon>
<ActionIcon variant="subtle">...</ActionIcon>
```
### DO NOT USE (Forbidden Patterns)
The following patterns are **NOT ALLOWED** for Button and ActionIcon:
```tsx
// ❌ WRONG - Don't use these
<Button variant="light" color="green">Save</Button>
<Button variant="light" color="gray">Cancel</Button>
<Button variant="light" color="red">Delete</Button>
@ -54,20 +66,12 @@ The following patterns are **NOT ALLOWED** for Button and ActionIcon:
<Button variant="default">Cancel</Button>
<ActionIcon variant="light" color="red">...</ActionIcon>
<ActionIcon variant="filled" color="gray">...</ActionIcon>
// ✅ CORRECT - Use custom variants
<Button variant="primary">Save</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="danger">Delete</Button>
<Button variant="link">View Details</Button>
<ActionIcon variant="primary">...</ActionIcon>
<ActionIcon variant="secondary">...</ActionIcon>
<ActionIcon variant="danger">...</ActionIcon>
<ActionIcon variant="link">...</ActionIcon>
```
**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").
**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.
**Note**: `variant="filled"` is still valid for **form inputs** (Select, TextInput, etc.), just not for Button/ActionIcon.
### Icon-Only Buttons → ActionIcon

View file

@ -64,6 +64,7 @@ import {
import { notifications } from '@mantine/notifications';
import {
IconBolt,
IconLayoutSidebarLeftExpand,
IconPlayerPlay,
IconPlus,
IconTags,
@ -171,6 +172,8 @@ const SearchConfigSchema = z.object({
type SearchConfigFromSchema = z.infer<typeof SearchConfigSchema>;
const QUERY_KEY_PREFIX = 'search';
// Helper function to get the default source id
export function getDefaultSourceId(
sources: { id: string }[] | undefined,
@ -261,6 +264,46 @@ function SearchSubmitButton({
);
}
function ExpandFiltersButton({ onExpand }: { onExpand: () => void }) {
return (
<Tooltip label="Show filters" position="bottom">
<ActionIcon
variant="subtle"
size="xs"
onClick={onExpand}
aria-label="Show filters"
>
<IconLayoutSidebarLeftExpand size={14} />
</ActionIcon>
</Tooltip>
);
}
function SearchResultsCountGroup({
isFilterSidebarCollapsed,
onExpandFilters,
histogramTimeChartConfig,
enableParallelQueries,
}: {
isFilterSidebarCollapsed: boolean;
onExpandFilters: () => void;
histogramTimeChartConfig: BuilderChartConfigWithDateRange;
enableParallelQueries?: boolean;
}) {
return (
<Group gap={4} align="center">
{isFilterSidebarCollapsed && (
<ExpandFiltersButton onExpand={onExpandFilters} />
)}
<SearchTotalCountChart
config={histogramTimeChartConfig}
queryKeyPrefix={QUERY_KEY_PREFIX}
enableParallelQueries={enableParallelQueries}
/>
</Group>
);
}
function SearchNumRows({
config,
enabled,
@ -278,7 +321,7 @@ function SearchNumRows({
const numRows = data?.[0]?.rows;
return (
<Text size="xs" mb={4}>
<Text size="xs">
{isLoading
? 'Scanned Rows ...'
: error || !numRows
@ -812,6 +855,9 @@ function DBSearchPage() {
}
}, [analysisMode, setIsLive]);
const [isFilterSidebarCollapsed, setIsFilterSidebarCollapsed] =
useLocalStorage<boolean>('isFilterSidebarCollapsed', false);
const [denoiseResults, _setDenoiseResults] = useQueryState(
'denoise',
parseAsBoolean.withDefault(false),
@ -1174,8 +1220,6 @@ function DBSearchPage() {
const [newSourceModalOpened, setNewSourceModalOpened] = useState(false);
const QUERY_KEY_PREFIX = 'search';
const isAnyQueryFetching =
useIsFetching({
queryKey: [QUERY_KEY_PREFIX],
@ -1761,33 +1805,43 @@ function DBSearchPage() {
height: '100%',
}}
>
<ErrorBoundary message="Unable to render search filters">
<DBSearchPageFilters
denoiseResults={denoiseResults}
setDenoiseResults={setDenoiseResults}
isLive={isLive}
analysisMode={analysisMode}
setAnalysisMode={setAnalysisMode}
chartConfig={filtersChartConfig}
sourceId={inputSourceObj?.id}
showDelta={
!!(searchedSource?.kind === SourceKind.Trace
? searchedSource.durationExpression
: undefined)
}
onColumnToggle={toggleColumn}
displayedColumns={displayedColumns}
{...searchFilters}
/>
</ErrorBoundary>
{!isFilterSidebarCollapsed && (
<ErrorBoundary message="Unable to render search filters">
<DBSearchPageFilters
denoiseResults={denoiseResults}
setDenoiseResults={setDenoiseResults}
isLive={isLive}
analysisMode={analysisMode}
setAnalysisMode={setAnalysisMode}
chartConfig={filtersChartConfig}
sourceId={inputSourceObj?.id}
showDelta={
!!(searchedSource?.kind === SourceKind.Trace
? searchedSource.durationExpression
: undefined)
}
onColumnToggle={toggleColumn}
displayedColumns={displayedColumns}
onCollapse={() => setIsFilterSidebarCollapsed(true)}
{...searchFilters}
/>
</ErrorBoundary>
)}
{analysisMode === 'pattern' &&
histogramTimeChartConfig != null && (
<Flex direction="column" w="100%" gap="0px" mih="0" miw={0}>
<Box className={searchPageStyles.searchStatsContainer}>
<Group justify="space-between" style={{ width: '100%' }}>
<SearchTotalCountChart
config={histogramTimeChartConfig}
queryKeyPrefix={QUERY_KEY_PREFIX}
<Group
justify="space-between"
align="center"
style={{ width: '100%' }}
>
<SearchResultsCountGroup
isFilterSidebarCollapsed={isFilterSidebarCollapsed}
onExpandFilters={() =>
setIsFilterSidebarCollapsed(false)
}
histogramTimeChartConfig={histogramTimeChartConfig}
/>
<SearchNumRows
config={{
@ -1855,11 +1909,15 @@ function DBSearchPage() {
<Box className={searchPageStyles.searchStatsContainer}>
<Group
justify="space-between"
align="center"
style={{ width: '100%' }}
>
<SearchTotalCountChart
config={histogramTimeChartConfig}
queryKeyPrefix={QUERY_KEY_PREFIX}
<SearchResultsCountGroup
isFilterSidebarCollapsed={isFilterSidebarCollapsed}
onExpandFilters={() =>
setIsFilterSidebarCollapsed(false)
}
histogramTimeChartConfig={histogramTimeChartConfig}
enableParallelQueries
/>
<Group gap="sm" align="center">

View file

@ -36,6 +36,7 @@ import {
IconChevronRight,
IconChevronUp,
IconFilterOff,
IconLayoutSidebarLeftCollapse,
IconMinus,
IconPin,
IconPinFilled,
@ -875,6 +876,7 @@ const DBSearchPageFiltersComponent = ({
setFilterRange,
onColumnToggle,
displayedColumns,
onCollapse,
}: {
analysisMode: 'results' | 'delta' | 'pattern';
setAnalysisMode: (mode: 'results' | 'delta' | 'pattern') => void;
@ -887,6 +889,7 @@ const DBSearchPageFiltersComponent = ({
setFilterRange: (key: string, range: { min: number; max: number }) => void;
onColumnToggle?: (column: string) => void;
displayedColumns?: string[];
onCollapse?: () => void;
} & FilterStateHook) => {
const setFilterValue = useCallback(
(
@ -1203,9 +1206,23 @@ const DBSearchPageFiltersComponent = ({
}}
>
<Stack gap="sm" p="xs">
<Text size="xxs" c="dimmed" fw="bold">
Analysis Mode
</Text>
<Flex align="center" justify="space-between">
<Text size="xxs" c="dimmed" fw="bold">
Analysis Mode
</Text>
{onCollapse && (
<Tooltip label="Hide filters" position="bottom">
<ActionIcon
variant="subtle"
size="xs"
onClick={onCollapse}
aria-label="Hide filters"
>
<IconLayoutSidebarLeftCollapse size={14} />
</ActionIcon>
</Tooltip>
)}
</Flex>
<Tabs
value={analysisMode}
onChange={value =>

View file

@ -103,7 +103,7 @@ export default function SearchTotalCountChart({
);
return (
<Text size="xs" mb={4}>
<Text size="xs" lh="normal">
{isLoading ? (
<span className="effect-pulse">&middot;&middot;&middot; Results</span>
) : totalCount !== null && !isError ? (