Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/top-align-growing-query-toolbars.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@hyperdx/app': patch
---

fix: keep toolbar controls level with the first line of a wrapping query field

Date pickers, Run, and nearby row actions stay top-aligned as a SQL or Lucene field grows. Sessions opts into PageHeader block padding so a taller search bar still has room.
2 changes: 1 addition & 1 deletion .claude/skills/page-layout/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,5 @@ import { PageLayout } from '@/components/PageLayout';
| List page | `AlertsPage.tsx` | `PageHeader` + `title` + `Container` (no inputs in header) |
| Tool page with inputs + hierarchy | `KubernetesDashboardPage.tsx`, `ClickhousePage.tsx` | `PageLayout` **without** `title`; **`breadcrumbs`** + `leading` + `actions` in one sticky header |
| Tool page (top-level) | `DBServiceMapPage.tsx` | `PageLayout` **without** `title`; `leading` / `actions` only; no duplicate breadcrumb unless you add a real hierarchy |
| Custom toolbar | `SessionsPage.tsx` | `PageLayout` + `header` = custom `PageHeader` `children` (single-row inputs, no `title`) |
| Custom toolbar | `SessionsPage.tsx` | `PageLayout` + `header` = custom `PageHeader` `children` with `growing` (single-row inputs, no `title`) |
| Custom title | `TeamPage.tsx` | `PageHeader` with `children` only |
3 changes: 2 additions & 1 deletion agent_docs/page_layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ Both live next to `withAppNav` in `packages/app/src/layout.tsx`, which wraps pag
| `leading` | Left cluster: source picker, badges, or other controls. When inputs are present, **do not** pair with `title` for the same page name. |
| `actions` | Right-aligned cluster: time range, Run/Save, sampling, refresh. |
| `children` | Full custom header when slots are not enough. Do not combine with `title` / `leading` / `actions` / `breadcrumbs` unless you use the breadcrumbs-only branch. |
| `growing` | Opt in to block padding that only shows once the toolbar outgrows `min-height`. Sessions uses this for its multi-line search; title-only pages omit it. |

Header styling is defined in `PageHeader.module.scss`: sticky top, bottom border, horizontal padding `var(--mantine-spacing-sm)` (same as Search `px="sm"`). Single-row headers keep `min-height: 60px`; stacked header (breadcrumbs + toolbar) grows with content.
Header styling is defined in `PageHeader.module.scss`: sticky top, bottom border, horizontal padding `var(--mantine-spacing-sm)` (same as Search `px="sm"`). Single-row headers keep `min-height: 60px`; stacked header (breadcrumbs + toolbar) grows with content. Pass `growing` when the toolbar can outgrow that min-height (Sessions' multi-line search) so block padding appears only once the row is taller than the bar.

## PageLayout API

Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/DBSearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2394,7 +2394,7 @@ export function DBSearchPage() {
<Flex
gap="sm"
style={{ flex: '0 1 500px', minWidth: 0 }}
align="center"
align="flex-start"
>
<TimePicker
data-testid="time-picker"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,13 @@ function ServicesDashboardPage() {
}}
>
<Group gap="xs">
<Group justify="space-between" gap="xs" wrap="nowrap" flex={1}>
<Group
justify="space-between"
gap="xs"
wrap="nowrap"
flex={1}
align="flex-start"
>
<SourceSelectControlled
control={control}
name="source"
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/SessionsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -434,12 +434,13 @@ function SessionsPage() {
<PageLayout
data-testid="sessions-page"
header={
<PageHeader>
<PageHeader growing>
<Group
justify="space-between"
gap="xs"
wrap="nowrap"
w="100%"
align="flex-start"
className={styles.toolbar}
>
<SourceSelectControlled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '@tabler/icons-react';

import { ChartEditorFormState } from '@/components/ChartEditor/types';
import { EDITOR_INPUT_HEIGHTS } from '@/components/editorInputHeights';
import { SQLInlineEditorControlled } from '@/components/SQLEditor/SQLInlineEditor';
import { TimePicker } from '@/components/TimePicker';
import { IS_LOCAL_MODE } from '@/config';
Expand Down Expand Up @@ -154,9 +155,11 @@ export function ChartActionBar({
</Button>
)}
</Flex>
<Flex gap="sm" mb="sm" align="center" justify="end">
<Flex gap="sm" mb="sm" align="flex-start" justify="end">
{filtersToggle != null && tabQueriesData(activeTab) && (
<DashboardFiltersToggle {...filtersToggle} />
<Flex h={`${EDITOR_INPUT_HEIGHTS.sm}px`} align="center">
<DashboardFiltersToggle {...filtersToggle} />
</Flex>
)}
{(activeTab === 'table' ||
activeTab === 'pie' ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
isFormulaDisplayType,
isFormulaSourceKind,
} from '@/components/ChartEditor/utils';
import { EDITOR_INPUT_HEIGHTS } from '@/components/editorInputHeights';
import MVOptimizationIndicator from '@/components/MaterializedViews/MVOptimizationIndicator';
import SearchWhereInput from '@/components/SearchInput/SearchWhereInput';
import SourceSchemaPreview, {
Expand Down Expand Up @@ -325,13 +326,14 @@ export function ChartEditorControls({
<>
<Divider mt="md" mb="sm" />
<div
className="gap-2 align-items-center"
className="gap-2"
style={{
display: 'grid',
gridTemplateColumns: 'auto minmax(0, 1fr)',
alignItems: 'start',
}}
>
<div>
<Flex h={`${EDITOR_INPUT_HEIGHTS.sm}px`} align="center">
<Text
me="sm"
size="sm"
Expand All @@ -341,7 +343,7 @@ export function ChartEditorControls({
>
Group By
</Text>
</div>
</Flex>
<div>
<SQLInlineEditorControlled
{...groupByConnectionProps}
Expand All @@ -355,7 +357,7 @@ export function ChartEditorControls({
</div>
{displayType === DisplayType.Table && (
<>
<div>
<Flex h={`${EDITOR_INPUT_HEIGHTS.sm}px`} align="center">
<Text
me="sm"
size="sm"
Expand All @@ -365,7 +367,7 @@ export function ChartEditorControls({
>
Having
</Text>
</div>
</Flex>
<div>
<SQLInlineEditorControlled
tableConnection={tableConnection}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
SavedChartConfigWithSelectArray,
} from '@/components/ChartEditor/types';
import { isFormulaSourceKind } from '@/components/ChartEditor/utils';
import { EDITOR_INPUT_HEIGHTS } from '@/components/editorInputHeights';
import { CheckBoxControlled } from '@/components/InputControlled';
import { MetricAttributeHelperPanel } from '@/components/MetricAttributeHelperPanel';
import {
Expand Down Expand Up @@ -428,15 +429,18 @@ export function ChartSeriesEditor({
)}
{(showWhere || showGroupBy || showHaving) && (
<div
className="flex-grow-1 gap-2 align-items-center"
className="flex-grow-1 gap-2"
style={{
display: 'grid',
gridTemplateColumns: 'auto 1fr auto 1fr',
alignItems: 'start',
}}
>
{showWhere && (
<>
<Text size="sm">Where</Text>
<Flex h={`${EDITOR_INPUT_HEIGHTS.sm}px`} align="center">
<Text size="sm">Where</Text>
</Flex>
<div
style={{
gridColumn:
Expand All @@ -460,9 +464,11 @@ export function ChartSeriesEditor({
)}
{showGroupBy && (
<>
<Text size="sm" style={{ whiteSpace: 'nowrap' }}>
Group By
</Text>
<Flex h={`${EDITOR_INPUT_HEIGHTS.sm}px`} align="center">
<Text size="sm" style={{ whiteSpace: 'nowrap' }}>
Group By
</Text>
</Flex>
<div
style={{
minWidth: 200,
Expand All @@ -484,9 +490,11 @@ export function ChartSeriesEditor({
</div>
{showHaving && (
<>
<Text size="sm" style={{ whiteSpace: 'nowrap' }}>
Having
</Text>
<Flex h={`${EDITOR_INPUT_HEIGHTS.sm}px`} align="center">
<Text size="sm" style={{ whiteSpace: 'nowrap' }}>
Having
</Text>
</Flex>
<div style={{ minWidth: 300, maxWidth: '100%' }}>
<SQLInlineEditorControlled
tableConnection={tableConnection}
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/DBTracePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ export default function DBTracePanel({
{!traceId && parentSourceId != null && (
<Stack gap="xs" mb="sm">
<Text size="xs">Trace ID Expression</Text>
<Flex align="center">
<Flex align="flex-start">
<SQLInlineEditorControlled
tableConnection={tcFromSource(parentSourceData)}
name="traceIdExpression"
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/KubernetesFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ export const KubernetesFilters: React.FC<KubernetesFiltersProps> = ({
};

return (
<Group mt="md" mb="xs" wrap="wrap" gap="xxs">
<Group mt="md" mb="xs" wrap="wrap" gap="xxs" align="flex-start">
{K8S_FILTER_FIELDS.map(({ field, placeholder, dataTestId }) => (
<FilterSelect
key={field}
Expand Down
10 changes: 9 additions & 1 deletion packages/app/src/components/PageHeader.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
justify-content: space-between;
line-height: 1;

/* Match Search page toolbar: DBSearchPage uses Mantine `px="sm"` */
/* Match Search page toolbar: DBSearchPage uses Mantine `px="sm"`. */
padding: 0 var(--mantine-spacing-sm);
position: sticky;
top: 0;
Expand All @@ -22,6 +22,14 @@
z-index: 2;
}

/* A toolbar that can grow past `min-height` (Sessions' multi-line search).
Block padding is absorbed at rest and only shows once the row outgrows the
header — otherwise the query sits flush against the edges. Other pages keep
the zero block padding above so a title-only bar does not shift. */
.header.growing:not(.notSticky) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — The growing prop and its :not(.notSticky) guard can be replaced by one unconditional line

.header is box-sizing: border-box with min-height: 60px, so padding-block: var(--mantine-spacing-xs) (20px total) is absorbed for any content ≤40px tall — which covers every current non-stacked header: title-only pages, TeamPage.tsx:274 (xs input, 30px), DashboardKioskHeader.tsx:14, DBServiceMapPage.tsx:343 (Text/Slider/TimePicker, 36px). Applying it unconditionally to .header lets .notSticky's later padding-block-end: 0 win naturally, drops the growing prop, the :not(.notSticky) specificity workaround, and the doc edits in agent_docs/page_layout.md and .claude/skills/page-layout/SKILL.md. It also removes a latent trap: as written, growing + stickyRow loses the top padding too, not just the bottom.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

padding-block: var(--mantine-spacing-xs);
}

/* Applied automatically when a `stickyRow` is provided: the chrome scrolls
away with the page and the `stickyRow` below becomes the only pinned bar.
The chrome's bottom border and bottom padding are also dropped — the
Expand Down
37 changes: 17 additions & 20 deletions packages/app/src/components/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ export type PageHeaderProps = {
* When omitted, the header behaves as a single sticky block.
*/
stickyRow?: React.ReactNode;
/**
* Add block padding that only shows once the toolbar outgrows `min-height`.
* Use for a header that can grow (Sessions' multi-line search); omit on
* title-only pages so they keep a flush 60px bar.
*/
growing?: boolean;
Comment on lines +64 to +69

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Required changeset is missing

This user-facing @hyperdx/app layout change does not include a changeset. The repository guide requires a changeset for behavior changes to published packages, so this requirement must be satisfied before merging by adding a patch changeset.

Context Used: CLAUDE.md (source)

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a patch changeset.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — New growing prop is unreachable via PageLayout and renders identically to the default on every current header

PageLayoutProps picks only 'title' | 'leading' | 'actions' | 'breadcrumbs' | 'children' (packages/app/src/components/PageLayout.tsx:27-30), so the structured-slot pages — the only ones that could ever need it (DBServiceMapPage, ClickhousePage, KubernetesDashboardPage) — cannot opt in without switching to the header escape hatch; add 'growing' to that Pick. Separately, the stated rationale in PageHeader.module.scss:25-28 and the prop doc ("omit on title-only pages so they keep a flush 60px bar") does not hold: box-sizing: border-box is global (packages/app/styles/globals.css:12) and .header has min-height: 60px, so padding-block: 10px leaves a 39px content box and every current header's at-rest content is ≤36px (a sm input/button) — title-only pages would not shift. If the opt-in isn't wanted, put padding-block on .header and delete the flag; that also removes a latent conflict, since .header.growing (two classes) outranks .notSticky { padding-block-end: 0 } (one class), so a future header passing both growing and stickyRow would keep the bottom padding .notSticky exists to drop.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sessions already uses the header escape hatch, which is the only page that needs growing. Structured-slot pages stay flush; forwarding through PageLayout can wait until one of them actually grows.

};

export function PageHeader({
Expand All @@ -72,6 +78,7 @@ export function PageHeader({
className,
'data-testid': testId,
stickyRow,
growing = false,
}: PageHeaderProps) {
const hasToolbar = title != null || leading != null || actions != null;
const hasBreadcrumbs = breadcrumbs != null;
Expand Down Expand Up @@ -101,6 +108,12 @@ export function PageHeader({
// than the chrome `<header>`, which lets it stay pinned for the full
// length of the page rather than only the height of the header.
const chromeStickyClass = hasStickyRow ? styles.notSticky : undefined;
const chromeClassName = classNames(
styles.header,
growing && styles.growing,
chromeStickyClass,
className,
);

const toolbarInner = (
<>
Expand All @@ -118,44 +131,28 @@ export function PageHeader({
// it. A page with only a `stickyRow` (no breadcrumbs, no title)
// skips the empty chrome entirely.
children != null ? (
<header
className={classNames(styles.header, chromeStickyClass, className)}
data-testid={testId}
>
<header className={chromeClassName} data-testid={testId}>
{children}
</header>
) : null
) : hasBreadcrumbs && hasToolbar ? (
<header
className={classNames(
styles.header,
styles.headerStacked,
chromeStickyClass,
className,
)}
className={classNames(chromeClassName, styles.headerStacked)}
data-testid={testId}
>
<div className={styles.breadcrumbsRow}>{breadcrumbs}</div>
<div className={styles.toolbarRow}>{toolbarInner}</div>
</header>
) : hasBreadcrumbs && !hasToolbar ? (
<header
className={classNames(
styles.header,
styles.headerStacked,
chromeStickyClass,
className,
)}
className={classNames(chromeClassName, styles.headerStacked)}
data-testid={testId}
>
<div className={styles.breadcrumbsRow}>{breadcrumbs}</div>
{children}
</header>
) : (
<header
className={classNames(styles.header, chromeStickyClass, className)}
data-testid={testId}
>
<header className={chromeClassName} data-testid={testId}>
{toolbarInner}
</header>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
IconTrash,
} from '@tabler/icons-react';

import { EDITOR_INPUT_HEIGHTS } from '@/components/editorInputHeights';
import { ErrorCollapse } from '@/components/Error/ErrorCollapse';
import { InputControlled } from '@/components/InputControlled';
import { SQLInlineEditorControlled } from '@/components/SQLEditor/SQLInlineEditor';
Expand Down Expand Up @@ -83,34 +84,38 @@ function HighlightedAttributeRow({
</div>
</Grid.Col>
<Grid.Col span={2} ps="xs">
<Flex align="center" gap="sm">
<Text c="gray">AS</Text>
<Flex align="flex-start" gap="sm">
<Flex h={`${EDITOR_INPUT_HEIGHTS.sm}px`} align="center">
<Text c="gray">AS</Text>
</Flex>
<SQLInlineEditorControlled
control={control}
name={`${name}.${index}.alias`}
placeholder="Optional Alias"
disableKeywordAutocomplete
/>
<Tooltip label="Validate expression">
<Flex h={`${EDITOR_INPUT_HEIGHTS.sm}px`} align="center" gap="sm">
<Tooltip label="Validate expression">
<ActionIcon
size="xs"
variant="subtle"
color="gray"
loading={isExplainLoading}
disabled={!expressionInput || isExplainLoading}
onClick={validateNow}
>
<IconCheck size={16} />
</ActionIcon>
</Tooltip>
<ActionIcon
size="xs"
variant="subtle"
color="gray"
loading={isExplainLoading}
disabled={!expressionInput || isExplainLoading}
onClick={validateNow}
onClick={() => removeHighlightedAttribute(index)}
>
<IconCheck size={16} />
<IconTrash size={16} />
</ActionIcon>
</Tooltip>
<ActionIcon
size="xs"
variant="subtle"
color="gray"
onClick={() => removeHighlightedAttribute(index)}
>
<IconTrash size={16} />
</ActionIcon>
</Flex>
</Flex>
</Grid.Col>

Expand Down
Loading
Loading