From c628d3e6b43dae4d263bf6d4b8589c74be6926ac Mon Sep 17 00:00:00 2001 From: Elizabet Oliveira Date: Fri, 15 May 2026 12:55:08 +0100 Subject: [PATCH 1/5] Add shared PageHeader and PageLayout primitives. Introduces a sticky page shell with optional breadcrumbs, title, and toolbar slots plus agent documentation and the page-layout skill. Co-authored-by: Cursor --- .claude/skills/page-layout/SKILL.md | 44 ++++++ agent_docs/README.md | 1 + agent_docs/page_layout.md | 131 ++++++++++++++++++ .../app/src/components/PageHeader.module.scss | 55 +++++++- packages/app/src/components/PageHeader.tsx | 87 +++++++++++- .../app/src/components/PageLayout.module.scss | 22 +++ packages/app/src/components/PageLayout.tsx | 75 ++++++++++ 7 files changed, 411 insertions(+), 4 deletions(-) create mode 100644 .claude/skills/page-layout/SKILL.md create mode 100644 agent_docs/page_layout.md create mode 100644 packages/app/src/components/PageLayout.module.scss create mode 100644 packages/app/src/components/PageLayout.tsx diff --git a/.claude/skills/page-layout/SKILL.md b/.claude/skills/page-layout/SKILL.md new file mode 100644 index 0000000000..7d2cadccd2 --- /dev/null +++ b/.claude/skills/page-layout/SKILL.md @@ -0,0 +1,44 @@ +--- +name: page-layout +description: Add or update app page headers and layout using PageHeader and PageLayout. Use when creating a new page, fixing inconsistent headers, or migrating Service Map / Kubernetes / dashboard pages to the shared layout. +--- + +# Page layout and headers + +Read **`agent_docs/page_layout.md`** before changing any page shell. It is the source of truth for API, examples, and migration steps. + +## Quick rules + +1. **Default for new pages**: `PageLayout` with `title`, optional `leading` / `actions`, and `content` — only when the sticky bar is **text-only** (no inputs in that bar). +2. **Sticky bar contains inputs** (source pickers, SQL/search, sliders, time range, Run, etc.): **do not** use `PageHeader` / `PageLayout` **`title`**. The sticky row is for controls only. Put **where you are** in the **`breadcrumbs` prop** (renders **inside the sticky `PageHeader`**, above the toolbar) when the page lives under a hierarchy (e.g. `Dashboards` → `Kubernetes`); otherwise you may omit `breadcrumbs` and rely on ``, the sidebar active item, and the empty state copy. +3. **Never** duplicate location: do not set `title="Kubernetes Dashboard"` **and** breadcrumbs that repeat the same page name. +4. **Never** use `<Text size="xl">` as the page title in the body. +5. **Global controls** (time range, Run, Save, sampling) go in `actions`, right-aligned. **Context** pickers go in `leading` (no `title` when those slots are used for inputs). **Breadcrumbs** use the `breadcrumbs` prop so they stay in the sticky header, not in `content`. +6. **Full-height tools** (maps, large charts): `fillViewport` on `PageLayout`. +7. **Search / Chart Explorer**: keep bespoke toolbars unless the task is explicitly to redesign them. + +## Workflow + +1. Read `agent_docs/page_layout.md`. +2. Open a similar page already on `PageHeader` / `PageLayout` (e.g. `AlertsPage.tsx`, `DBServiceMapPage.tsx`, `KubernetesDashboardPage.tsx`). +3. Implement using `@/components/PageLayout` or `@/components/PageHeader`. +4. Preserve or add `data-testid` on the page root for E2E tests. +5. Run `yarn lint:fix` in the repo root when done. +6. If the page has E2E coverage, run the relevant spec under `packages/app/tests/e2e/`. + +## Imports + +```tsx +import { PageHeader } from '@/components/PageHeader'; +import { PageLayout } from '@/components/PageLayout'; +``` + +## Reference implementations + +| Page | File | Pattern | +|------|------|---------| +| 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 title | `TeamPage.tsx` | `PageHeader` with `children` only | diff --git a/agent_docs/README.md b/agent_docs/README.md index b962eec57b..babb3ec7a8 100644 --- a/agent_docs/README.md +++ b/agent_docs/README.md @@ -16,6 +16,7 @@ Instead of stuffing all instructions into `CLAUDE.md` (which goes into every con - **`tech_stack.md`** - Technology choices, UI component patterns, library usage - **`development.md`** - Development workflows, testing strategy, common tasks, debugging - **`code_style.md`** - Code patterns and best practices (read only when actively coding) +- **`page_layout.md`** - PageHeader, PageLayout, and consistent page chrome (titles, actions, tool pages) - **`data_viz_colors.md`** - Chart, heatmap, and semantic status colors. Read before adding or changing any color in a chart, sparkline, heatmap, legend, or status pill. ## Usage Pattern diff --git a/agent_docs/page_layout.md b/agent_docs/page_layout.md new file mode 100644 index 0000000000..04f422f45d --- /dev/null +++ b/agent_docs/page_layout.md @@ -0,0 +1,131 @@ +# Page layout and headers + +HyperDX app pages share a sticky top header bar and a scrollable content region below it. Use the shared layout primitives so titles, controls, and spacing stay consistent across Search, list pages, and tool pages (Service Map, Kubernetes, Chart Explorer). + +## Components + +| Component | Path | Use when | +|-----------|------|----------| +| `PageHeader` | `packages/app/src/components/PageHeader.tsx` | You only need the header bar (page already has its own content wrapper). | +| `PageLayout` | `packages/app/src/components/PageLayout.tsx` | You want header + flex content column in one wrapper (recommended for new pages). | + +Both live next to `withAppNav` in `packages/app/src/layout.tsx`, which wraps pages with the left nav and scroll container. + +## PageHeader API + +```tsx +<PageHeader title="Alerts" /> + +{/* Sticky bar has inputs: no `title` — pass breadcrumbs into the header */} +<PageHeader + breadcrumbs={<Breadcrumbs>...</Breadcrumbs>} + leading={<SourceSelectControlled ... />} + actions={<TimePicker ... />} +/> + +<PageHeader> + {/* Custom title row, e.g. editable team name on Team settings */} +</PageHeader> +``` + +| Prop | Purpose | +|------|---------| +| `title` | Plain page title (`<h1>`). **Use only when the sticky bar has no inputs** (list/settings pages). If the bar has pickers, search, sliders, or Run, omit `title` and use **`breadcrumbs`** (or rely on nav + document title only). | +| `breadcrumbs` | Location trail **inside the sticky header**, rendered above the toolbar when `leading` / `actions` are set. Use Mantine `Breadcrumbs` (e.g. `Dashboards` → current page). Do not duplicate the same text as `title`. | +| `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. | + +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. + +## PageLayout API + +```tsx +<PageLayout + data-testid="service-map-page" + breadcrumbs={<Breadcrumbs>...</Breadcrumbs>} + leading={sourceSelect} + actions={headerActions} + fillViewport + content={<ServiceMap ... />} +/> +``` + +| Prop | Purpose | +|------|---------| +| `title`, `leading`, `actions`, `breadcrumbs`, `children` | Forwarded to `PageHeader` (same rules as above). | +| `header` | Replace the default `PageHeader` entirely. | +| `content` | Main page body below the header (required). | +| `fillViewport` | Set `height: 100vh` on the page root for full-height canvases (maps, charts). | +| `contentClassName` | Extra class on the content region (e.g. padding). | + +## When to use which pattern + +### List / settings pages (Alerts, Dashboards, Saved Searches, Team) + +- Use `PageHeader` at the top of the page. +- Put filters, tables, and tabs in a `Container` below the header (see `AlertsPage.tsx`, `DashboardsListPage.tsx`). +- Simple title-only pages: `<PageHeader title="Dashboards" />` — **only** when the header row has **no** inputs. + +### Tool / canvas pages (Service Map, Kubernetes, Clickhouse, Chart Explorer) + +- Prefer `PageLayout` with `fillViewport` when the main UI should fill remaining height. +- Put **global** controls in `actions` (time picker, sampling, Run)—not inside the canvas. +- Put **context** controls in `leading` (source picker, environment). +- **If the sticky header row includes any inputs** (selectors, sliders, search, time range, Run): **omit `title`**. Express location with the **`breadcrumbs` prop** on `PageLayout` / `PageHeader` so the trail stays **inside the sticky header** above the toolbar (see `KubernetesDashboardPage.tsx`). Do **not** repeat the same label as both `title` and the last breadcrumb. Top-level tools (e.g. Service Map) may omit `breadcrumbs` if `<Head><title>` and the sidebar active state are enough. + +### Search and Chart Explorer (complex query chrome) + +These pages use bespoke multi-row toolbars instead of a single title row. That is intentional until those flows are redesigned. Do not force a `title` on them; if you add a header row, use `PageHeader` `children` or a dedicated toolbar component, not ad-hoc `Text size="xl"` in the body. + +### Client Sessions (query + list) + +Sessions keeps a **single-row** query toolbar (source, where filter, time range, Run). Put the full toolbar in a custom `PageHeader` via `PageLayout` `header` — do not split controls across `title` / `leading` / `actions` / `content`. + +## Do / don't + +```tsx +// ❌ Ad-hoc title in page body +<Group justify="space-between"> + <Text size="xl">Service Map</Text> + <TimePicker ... /> +</Group> + +// ✅ Dashboard tool page: breadcrumbs in header, inputs in same sticky block (no `title`) +<PageLayout + breadcrumbs={<Breadcrumbs>...</Breadcrumbs>} + leading={<SourceSelect ... />} + actions={<TimePicker ... />} + content={<>...</>} +/> + +// ❌ Title + breadcrumbs repeating the same page name +<PageLayout title="Kubernetes Dashboard" breadcrumbs={<Breadcrumbs>… Kubernetes</Breadcrumbs>} /> + +// ❌ Duplicate padding wrapper around the whole page including title +<Box p="sm"> + <Text size="xl">Alerts</Text> + ... +</Box> + +// ✅ Header outside padded content +<PageHeader title="Alerts" /> +<Container py="lg">...</Container> +``` + +## Migrating an existing page + +1. Replace `Text size="xl"` title + `Group justify="space-between"` with `PageLayout` or `PageHeader` slots. +2. Move time picker and primary actions to `actions`. +3. Move source pickers and badges to `leading`. +4. If the sticky bar has inputs, **do not** set `title`; pass **`breadcrumbs`** on `PageLayout` when the route has a hierarchy (breadcrumbs render inside the sticky `PageHeader`, not in `content`). +5. Keep `data-testid` on `PageLayout` / page root for E2E tests. +6. Run affected Playwright tests (`packages/app/tests/e2e/`). + +## Pages using PageHeader / PageLayout today + +- Alerts, Dashboards, Saved Searches — `PageHeader` with `title` +- Team — custom `PageHeader` `children` (editable name) +- Service Map — `PageLayout` with `fillViewport`, `leading` / `actions` only (no `title`; top-level tool) +- Kubernetes Dashboard, Clickhouse Dashboard — `PageLayout` with `breadcrumbs`, `leading`, `actions`, `padded` (no `title`) +- Client Sessions — `PageLayout` with custom `PageHeader` containing the full single-row query toolbar diff --git a/packages/app/src/components/PageHeader.module.scss b/packages/app/src/components/PageHeader.module.scss index 7bcaadbbcf..08c01dff7d 100644 --- a/packages/app/src/components/PageHeader.module.scss +++ b/packages/app/src/components/PageHeader.module.scss @@ -5,11 +5,62 @@ color: var(--color-text-default); display: flex; font-weight: 500; - height: 60px; + gap: 16px; + min-height: 60px; justify-content: space-between; line-height: 1; - padding: 0 32px; + /* Match Search page toolbar: DBSearchPage uses Mantine `px="sm"` */ + padding: 0 var(--mantine-spacing-sm); position: sticky; top: 0; z-index: 100; } + +/* Breadcrumbs row + toolbar row inside one sticky header */ +.headerStacked { + align-items: stretch; + flex-direction: column; + gap: var(--mantine-spacing-xs); + justify-content: flex-start; + min-height: auto; + padding-block: var(--mantine-spacing-xs); +} + +.breadcrumbsRow { + flex-shrink: 0; + font-weight: 400; + line-height: 1.2; +} + +.toolbarRow { + align-items: center; + display: flex; + flex: 1; + gap: 16px; + justify-content: space-between; + min-height: 44px; + min-width: 0; +} + +.start { + align-items: center; + display: flex; + flex: 1; + gap: 12px; + min-width: 0; +} + +.title { + font-size: inherit; + font-weight: inherit; + line-height: inherit; + margin: 0; + white-space: nowrap; +} + +.actions { + align-items: center; + display: flex; + flex-shrink: 0; + gap: 12px; +} diff --git a/packages/app/src/components/PageHeader.tsx b/packages/app/src/components/PageHeader.tsx index 61ee10efbe..61b7a31cad 100644 --- a/packages/app/src/components/PageHeader.tsx +++ b/packages/app/src/components/PageHeader.tsx @@ -1,5 +1,88 @@ +import classNames from 'classnames'; + import styles from './PageHeader.module.scss'; -export const PageHeader = ({ children }: { children: React.ReactNode }) => { - return <div className={styles.header}> {children}</div>; +export type PageHeaderProps = { + /** Plain-text page title for standard list and tool pages. */ + title?: string; + /** Content after the title (source picker, badge, edit control). */ + leading?: React.ReactNode; + /** Right-aligned controls (time picker, Run/Save, sampling). */ + actions?: React.ReactNode; + /** + * Location trail inside the sticky header (above the toolbar when `leading` / `actions` exist). + * Use with dashboard-style routes; omit `title` when the toolbar has inputs. + */ + breadcrumbs?: React.ReactNode; + /** Custom header when structured slots are not enough (e.g. editable team name). */ + children?: React.ReactNode; + className?: string; + 'data-testid'?: string; }; + +export function PageHeader({ + title, + leading, + actions, + breadcrumbs, + children, + className, + 'data-testid': testId, +}: PageHeaderProps) { + const hasToolbar = title != null || leading != null || actions != null; + const hasBreadcrumbs = breadcrumbs != null; + + if (!hasToolbar && !hasBreadcrumbs) { + return ( + <header + className={classNames(styles.header, className)} + data-testid={testId} + > + {children} + </header> + ); + } + + const toolbarInner = ( + <> + <div className={styles.start}> + {title != null && <h1 className={styles.title}>{title}</h1>} + {leading} + </div> + {actions != null && <div className={styles.actions}>{actions}</div>} + </> + ); + + if (hasBreadcrumbs && hasToolbar) { + return ( + <header + className={classNames(styles.header, styles.headerStacked, className)} + data-testid={testId} + > + <div className={styles.breadcrumbsRow}>{breadcrumbs}</div> + <div className={styles.toolbarRow}>{toolbarInner}</div> + </header> + ); + } + + if (hasBreadcrumbs && !hasToolbar) { + return ( + <header + className={classNames(styles.header, styles.headerStacked, className)} + data-testid={testId} + > + <div className={styles.breadcrumbsRow}>{breadcrumbs}</div> + {children} + </header> + ); + } + + return ( + <header + className={classNames(styles.header, className)} + data-testid={testId} + > + {toolbarInner} + </header> + ); +} diff --git a/packages/app/src/components/PageLayout.module.scss b/packages/app/src/components/PageLayout.module.scss new file mode 100644 index 0000000000..ef6ee44f1d --- /dev/null +++ b/packages/app/src/components/PageLayout.module.scss @@ -0,0 +1,22 @@ +.root { + background-color: var(--color-bg-body); + display: flex; + flex-direction: column; + min-height: 100%; +} + +.fillViewport { + height: 100vh; +} + +.content { + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + min-width: 0; +} + +.padded { + padding: var(--mantine-spacing-sm); +} diff --git a/packages/app/src/components/PageLayout.tsx b/packages/app/src/components/PageLayout.tsx new file mode 100644 index 0000000000..c4398434f0 --- /dev/null +++ b/packages/app/src/components/PageLayout.tsx @@ -0,0 +1,75 @@ +import classNames from 'classnames'; + +import { PageHeader, type PageHeaderProps } from './PageHeader'; + +import styles from './PageLayout.module.scss'; + +export type PageLayoutProps = Pick< + PageHeaderProps, + 'title' | 'leading' | 'actions' | 'breadcrumbs' | 'children' +> & { + /** Page content below the header. */ + content: React.ReactNode; + /** Custom header when `title` / `leading` / `actions` are not enough. */ + header?: React.ReactNode; + className?: string; + contentClassName?: string; + 'data-testid'?: string; + fillViewport?: boolean; + /** Apply standard page body padding (`var(--mantine-spacing-sm)`). */ + padded?: boolean; +}; + +export function PageLayout({ + title, + leading, + actions, + breadcrumbs, + children: headerChildren, + header, + content, + className, + contentClassName, + 'data-testid': testId, + fillViewport = false, + padded = false, +}: PageLayoutProps) { + const headerNode = + header ?? + (title != null || + leading != null || + actions != null || + breadcrumbs != null || + headerChildren ? ( + <PageHeader + title={title} + leading={leading} + actions={actions} + breadcrumbs={breadcrumbs} + > + {headerChildren} + </PageHeader> + ) : null); + + return ( + <div + className={classNames( + styles.root, + fillViewport && styles.fillViewport, + className, + )} + data-testid={testId} + > + {headerNode} + <div + className={classNames( + styles.content, + padded && styles.padded, + contentClassName, + )} + > + {content} + </div> + </div> + ); +} From 8c1ba73630db53236f14fb7f05cb6289760cbe79 Mon Sep 17 00:00:00 2001 From: Elizabet Oliveira <elizabet.oliveira@clickhouse.com> Date: Fri, 15 May 2026 12:55:53 +0100 Subject: [PATCH 2/5] Migrate Sessions page to PageLayout. Keeps the single-row query toolbar inside a custom PageHeader while using the shared layout wrapper for scroll and padding. Co-authored-by: Cursor <cursoragent@cursor.com> --- packages/app/src/SessionsPage.tsx | 167 ++++++++++--------- packages/app/styles/SessionsPage.module.scss | 11 ++ 2 files changed, 101 insertions(+), 77 deletions(-) diff --git a/packages/app/src/SessionsPage.tsx b/packages/app/src/SessionsPage.tsx index 25661456a6..b948dd6d77 100644 --- a/packages/app/src/SessionsPage.tsx +++ b/packages/app/src/SessionsPage.tsx @@ -16,7 +16,6 @@ import { } from '@hyperdx/common-utils/dist/types'; import { Anchor, - Box, Button, Code, Flex, @@ -32,6 +31,8 @@ import { import { useVirtualizer } from '@tanstack/react-virtual'; import EmptyState from '@/components/EmptyState'; +import { PageHeader } from '@/components/PageHeader'; +import { PageLayout } from '@/components/PageLayout'; import { SourceSelectControlled } from '@/components/SourceSelect'; import { TimePicker } from '@/components/TimePicker'; import { parseTimeQuery, useNewTimeQuery } from '@/timeQuery'; @@ -352,11 +353,7 @@ export default function SessionsPage() { const targetSession = sessions.find(s => s.sessionId === selectedSession?.id); return ( - <div - className="SessionsPage" - data-testid="sessions-page" - style={{ display: 'flex', flexDirection: 'column', height: '100%' }} - > + <> <Head> <title>Client Sessions - {brandName} @@ -382,80 +379,96 @@ export default function SessionsPage() { } /> )} - -
{ - e.preventDefault(); - onSubmit(); - return false; - }} - > - - - - - { - onSearch(range); - }} - /> - - - -
- - {isSessionsLoading || isSessionSourceLoading ? ( - - - {isSessionSourceLoading ? 'Loading...' : 'Searching sessions...'} - - ) : ( - <> - {!sessions.length ? ( - { + e.preventDefault(); + onSubmit(); + return false; + }} + > + + - - - ) : ( -
- { - setSelectedSession(session); + + + { + onSearch(range); }} - sessions={sessions} - isSessionLoading={isSessionsLoading} /> -
- )} - - )} -
- + + + + } + padded + content={ + <> + {isSessionsLoading || isSessionSourceLoading ? ( + + + {isSessionSourceLoading + ? 'Loading...' + : 'Searching sessions...'} + + ) : ( + <> + {!sessions.length ? ( + + + + ) : ( +
+ { + setSelectedSession(session); + }} + sessions={sessions} + isSessionLoading={isSessionsLoading} + /> +
+ )} + + )} + + } + /> + + ); } diff --git a/packages/app/styles/SessionsPage.module.scss b/packages/app/styles/SessionsPage.module.scss index 27b37010bf..828142f848 100644 --- a/packages/app/styles/SessionsPage.module.scss +++ b/packages/app/styles/SessionsPage.module.scss @@ -1,3 +1,14 @@ +.pageForm { + display: flex; + flex-direction: column; + height: 100%; +} + +.toolbar { + flex: 1; + min-width: 0; +} + .sessionCard { &:hover { .emailText { From ef5526ec910e44fefd3c1d59a85bd90a52eee500 Mon Sep 17 00:00:00 2001 From: Elizabet Oliveira Date: Fri, 15 May 2026 13:25:27 +0100 Subject: [PATCH 3/5] fix: address CI stylelint and Knip for page shell Use ignoreFiles for PageLayout until consumer PRs land, document cleanup in agent_docs, add a TODO on the component, and fix SCSS comment spacing. Co-authored-by: Cursor --- agent_docs/page_layout.md | 4 ++++ knip.json | 1 + packages/app/src/components/PageHeader.module.scss | 1 + packages/app/src/components/PageLayout.tsx | 3 +++ 4 files changed, 9 insertions(+) diff --git a/agent_docs/page_layout.md b/agent_docs/page_layout.md index 04f422f45d..8cbf023ee8 100644 --- a/agent_docs/page_layout.md +++ b/agent_docs/page_layout.md @@ -129,3 +129,7 @@ Sessions keeps a **single-row** query toolbar (source, where filter, time range, - Service Map — `PageLayout` with `fillViewport`, `leading` / `actions` only (no `title`; top-level tool) - Kubernetes Dashboard, Clickhouse Dashboard — `PageLayout` with `breadcrumbs`, `leading`, `actions`, `padded` (no `title`) - Client Sessions — `PageLayout` with custom `PageHeader` containing the full single-row query toolbar + +## Knip (`PageLayout` temporary ignore) + +`PageLayout.tsx` is listed in **`knip.json`** under `workspaces["packages/app"].ignoreFiles` so the layout shell can merge before every consumer route imports it (Knip only treats `pages/`, `scripts/`, and e2e as entry roots by default). **Delete that `ignoreFiles` entry** when `yarn knip` passes without it—typically after the dashboard and tool-page migrations that import `@/components/PageLayout` are on `main`. diff --git a/knip.json b/knip.json index 7868b33e02..3936bb136c 100644 --- a/knip.json +++ b/knip.json @@ -5,6 +5,7 @@ "entry": ["pages/**/*.{ts,tsx}", "scripts/*.js", "tests/e2e/**/*.ts"], "project": ["src/**/*.{ts,tsx}", "pages/**/*.{ts,tsx}"], "ignore": [".storybook/public/**"], + "ignoreFiles": ["src/components/PageLayout.tsx"], "ignoreDependencies": [ "eslint-config-next", "@chromatic-com/storybook", diff --git a/packages/app/src/components/PageHeader.module.scss b/packages/app/src/components/PageHeader.module.scss index 08c01dff7d..47964b9227 100644 --- a/packages/app/src/components/PageHeader.module.scss +++ b/packages/app/src/components/PageHeader.module.scss @@ -9,6 +9,7 @@ min-height: 60px; justify-content: space-between; line-height: 1; + /* Match Search page toolbar: DBSearchPage uses Mantine `px="sm"` */ padding: 0 var(--mantine-spacing-sm); position: sticky; diff --git a/packages/app/src/components/PageLayout.tsx b/packages/app/src/components/PageLayout.tsx index c4398434f0..cb47205f8d 100644 --- a/packages/app/src/components/PageLayout.tsx +++ b/packages/app/src/components/PageLayout.tsx @@ -1,3 +1,6 @@ +// TODO: Remove `src/components/PageLayout.tsx` from `knip.json` → `workspaces["packages/app"].ignoreFiles` +// once enough app pages import `PageLayout` that Knip no longer reports it as unused. + import classNames from 'classnames'; import { PageHeader, type PageHeaderProps } from './PageHeader'; From 036bd803d629254d901fcb6c479e2e268631c71d Mon Sep 17 00:00:00 2001 From: Elizabet Oliveira Date: Fri, 15 May 2026 15:56:15 +0100 Subject: [PATCH 4/5] chore: drop Knip ignoreFiles for PageLayout now Sessions imports it Remove temporary ignore, PageLayout TODO, and refresh page_layout.md Knip note. Co-authored-by: Cursor --- agent_docs/page_layout.md | 4 ++-- knip.json | 1 - packages/app/src/components/PageLayout.tsx | 3 --- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/agent_docs/page_layout.md b/agent_docs/page_layout.md index 8cbf023ee8..18d7274bb2 100644 --- a/agent_docs/page_layout.md +++ b/agent_docs/page_layout.md @@ -130,6 +130,6 @@ Sessions keeps a **single-row** query toolbar (source, where filter, time range, - Kubernetes Dashboard, Clickhouse Dashboard — `PageLayout` with `breadcrumbs`, `leading`, `actions`, `padded` (no `title`) - Client Sessions — `PageLayout` with custom `PageHeader` containing the full single-row query toolbar -## Knip (`PageLayout` temporary ignore) +## Knip -`PageLayout.tsx` is listed in **`knip.json`** under `workspaces["packages/app"].ignoreFiles` so the layout shell can merge before every consumer route imports it (Knip only treats `pages/`, `scripts/`, and e2e as entry roots by default). **Delete that `ignoreFiles` entry** when `yarn knip` passes without it—typically after the dashboard and tool-page migrations that import `@/components/PageLayout` are on `main`. +`packages/app` Knip entry roots are `pages/`, `scripts/`, and e2e tests. After you add or move a `PageLayout` import, run `yarn knip` from the repo root (or `yarn knip` in `packages/app` if configured) so new consumers stay wired. diff --git a/knip.json b/knip.json index 3936bb136c..7868b33e02 100644 --- a/knip.json +++ b/knip.json @@ -5,7 +5,6 @@ "entry": ["pages/**/*.{ts,tsx}", "scripts/*.js", "tests/e2e/**/*.ts"], "project": ["src/**/*.{ts,tsx}", "pages/**/*.{ts,tsx}"], "ignore": [".storybook/public/**"], - "ignoreFiles": ["src/components/PageLayout.tsx"], "ignoreDependencies": [ "eslint-config-next", "@chromatic-com/storybook", diff --git a/packages/app/src/components/PageLayout.tsx b/packages/app/src/components/PageLayout.tsx index cb47205f8d..c4398434f0 100644 --- a/packages/app/src/components/PageLayout.tsx +++ b/packages/app/src/components/PageLayout.tsx @@ -1,6 +1,3 @@ -// TODO: Remove `src/components/PageLayout.tsx` from `knip.json` → `workspaces["packages/app"].ignoreFiles` -// once enough app pages import `PageLayout` that Knip no longer reports it as unused. - import classNames from 'classnames'; import { PageHeader, type PageHeaderProps } from './PageHeader'; From 5f147e168c2e144e2bb623bac86abf3a473a1ed7 Mon Sep 17 00:00:00 2001 From: Elizabet Oliveira Date: Fri, 15 May 2026 18:14:22 +0100 Subject: [PATCH 5/5] fix(sessions): remove extra margin above session list Drop `mt-4` on the list wrapper so content sits tighter below the toolbar. Co-authored-by: Cursor --- packages/app/src/SessionsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/SessionsPage.tsx b/packages/app/src/SessionsPage.tsx index b948dd6d77..70b4490fdc 100644 --- a/packages/app/src/SessionsPage.tsx +++ b/packages/app/src/SessionsPage.tsx @@ -452,7 +452,7 @@ export default function SessionsPage() { ) : ( -
+
{ setSelectedSession(session);