mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
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:
parent
8b629385f2
commit
a6a83d59d4
5 changed files with 130 additions and 46 deletions
5
.changeset/collapsible-filter-sidebar.md
Normal file
5
.changeset/collapsible-filter-sidebar.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
feat: Add collapsible filter sidebar toggle to search page
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export default function SearchTotalCountChart({
|
|||
);
|
||||
|
||||
return (
|
||||
<Text size="xs" mb={4}>
|
||||
<Text size="xs" lh="normal">
|
||||
{isLoading ? (
|
||||
<span className="effect-pulse">··· Results</span>
|
||||
) : totalCount !== null && !isError ? (
|
||||
|
|
|
|||
Loading…
Reference in a new issue