agent_docs/code_style.md
Rama main · réplica de lectura del repositorio original.
# Code Style & Best Practices
> **Note**: Pre-commit hooks handle formatting automatically. Focus on implementation patterns.
## TypeScript
- Avoid `any` - use proper typing
- Avoid `as` casts, use `satisfies` or type inference whenever possible
- Use Zod schemas for runtime validation
- Define clear interfaces for data structures
- Implement proper error boundaries
- Define and import reusable named types instead of repeating verbose types
## Code Organization
- **Single Responsibility**: One clear purpose per component/function
- **File Size**: Max 300 lines - refactor when approaching limit
- **DRY**: Grep for an existing implementation before adding a type, schema,
helper, or component - see "Before you add a type, schema, or helper" below
(REQUIRED)
- **In-Context Learning**: Explore similar files before implementing
## Before you add a type, schema, or helper (REQUIRED)
**Grep for an existing implementation before you define a new type, Zod schema,
helper function, or React component.** Search for the _operation_ — splitting a
list, bucketing a timestamp, validating a URL, formatting a duration — not for
the name you were about to give it. Names differ; operations don't.
```bash
# Two or three words for the operation, as alternates. Runs in ~20ms.
grep -rniE "export (const|function|type|interface|enum) [a-zA-Z]*(bucket|interval|granular)" \
packages/common-utils/src packages/app/src packages/api/src \
--include='*.ts' --include='*.tsx'
```
Search order: the file you are editing, then `packages/common-utils/src/`, then
the rest of your package. `common-utils` has **no root barrel** — no
`src/index.ts`, and its `package.json` declares no entry point, which is why
every import is a deep `@hyperdx/common-utils/dist/...` path. `src/types.ts`
alone is ~2,850 lines and 280+ exports. You cannot discover what exists by
reading an index; grep is the index.
### Where shared code already lives
Nine modules carry ~90% of all cross-package imports:
| Import path | Holds |
|-------------|-------|
| `.../dist/types` | Every shared Zod schema and its inferred type: sources, alerts, dashboards, tiles, webhooks, chart configs, `SQLInterval`, `MetricsDataType`, `StacktraceFrame` |
| `.../dist/core/utils` | Time-bucket and granularity math (`toStartOfInterval`, `timeBucketByGranularity`, `convertGranularityToSeconds`), string splitting (`splitAndTrimCSV`, `splitAndTrimWithBracket`, `escapeSqlString`), `hashCode`, `parseJSON`, `formatDate` |
| `.../dist/core/metadata` | ClickHouse table/column introspection — `Metadata`, `getMetadata`, `parseKeyPath`, `unquoteIdentifier` |
| `.../dist/clickhouse` (`/node`, `/browser`) | Query client, `ChSql`/`chSql`/`concatChSql`, CH↔JS type conversion |
| `.../dist/core/renderChartConfig` | `ChartConfig` → SQL |
| `.../dist/filters` | Filter state, serialization, and validation (`FilterState`, `filtersToQuery`, `parseQuery`) |
| `.../dist/guards` | Chart-config and source type guards (`isBuilderChartConfig`, `isRawSqlChartConfig`, `isHeatmapCompatibleSource`) |
| `.../dist/validation` | `isValidUrl`, `isValidSlackUrl`, password validators |
| `.../dist/variables` | Dashboard variable resolution and template expansion |
Package-local homes, searched second: `packages/app/src/utils.ts`,
`packages/app/src/types.ts`, `packages/app/src/ChartUtils.tsx`,
`packages/api/src/utils/`.
### What to do once you find it
| Situation | Do this |
|-----------|---------|
| Canonical version exists and fits | Import it. Delete the copy you were writing. |
| Exists, but you want a local name | Alias-shim it — `packages/app/src/types.ts:9`, `export type NumberFormat = _NumberFormat;`. Never retype the definition. |
| Exists, but the shape is _almost_ right | Derive from it (`Pick`, `Omit`, `.extend()`, `Alert & { … }`) or parameterize the canonical one. One source of truth. |
| Shared logic between two components | Factor it into a sibling module — `packages/app/src/components/sourceSelectUtils.tsx:51` (`useFilteredSortedSourceItems`, shared by `SourceSelect` and `SourceMultiSelect`). |
| Two genuinely must coexist | Doc-comment _why_, and name the twin. `packages/common-utils/src/types.ts:571-582` does this for the Mongoose `IWebhook` (`packages/api/src/models/webhook.ts:17`) vs the JSON-serialized `WebhookSchema`. |
Not acceptable: a second definition with no alias, no doc comment explaining the
twin, and no test pinning the two together.
### Marking a whole-file port
When an entire file has to be copied across packages — `packages/cli` does this
for the components listed under "Web Frontend Alignment" in
[`packages/cli/AGENTS.md`](../packages/cli/AGENTS.md) — head it with an
`@source` tag naming the origin, one tag per origin file:
```ts
/**
* Source helper functions.
*
* @source packages/app/src/source.ts
*/
```
`scripts/ci/ratchet.mjs` counts these so the total stays visible in CI. That
count is advisory, not a gate: annotating a port never fails the build, and
stripping the tag to go green hides the copy instead of removing it. Lower it by
moving the shared code into `common-utils`, then run `yarn ratchet:update`.
## React Patterns
- Functional components with hooks (not class components)
- Write small, focused components
- Extract reusable logic into custom hooks
- Define TypeScript interfaces for props
- Use proper keys for lists, memoization for expensive computations
## Mantine UI Components
The project uses Mantine UI with **custom variants** defined in `packages/app/src/theme/mantineTheme.ts`.
- Prefer Mantine components over custom-styled elements
- Prefer individual Mantine style props (eg. `m='xs'`) over raw styles (eg. `style={{ margin: '4px' }}`)
### Button & ActionIcon Variants (REQUIRED)
**ONLY use these variants for Button and ActionIcon components:**
| Variant | Use Case | Example |
|---------|----------|---------|
| `variant="primary"` | Primary actions (Submit, Save, Create, Run) | `<Button variant="primary">Save</Button>` |
| `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"` | Transparent background with hover highlight; for toolbar/utility controls that shouldn't draw attention until hovered (collapse toggles, close buttons, auxiliary actions) | `<Button variant="subtle">Filter</Button>` |
### Correct Usage
```tsx
<Button variant="primary">Save</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="danger">Delete</Button>
<Button variant="subtle">Filter</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)
```tsx
<Button variant="light" color="green">Save</Button>
<Button variant="light" color="gray">Cancel</Button>
<Button variant="light" color="red">Delete</Button>
<Button variant="outline" color="green">Save</Button>
<Button variant="outline" color="gray">Cancel</Button>
<Button variant="outline" color="red">Delete</Button>
<Button variant="filled" color="gray">Cancel</Button>
<Button variant="default">Cancel</Button>
<ActionIcon variant="light" color="red">...</ActionIcon>
<ActionIcon variant="filled" color="gray">...</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**: 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
**If a Button only contains an icon (no text), use ActionIcon instead:**
```tsx
// ❌ WRONG - Button with only an icon
<Button variant="secondary" px="xs">
<IconRefresh size={18} />
</Button>
// ✅ CORRECT - Use ActionIcon for icon-only buttons
<ActionIcon variant="secondary" size="input-sm">
<IconRefresh size={18} />
</ActionIcon>
```
This pattern cannot be enforced by ESLint and requires manual code review.
### Semantic component variants (Alert / Text / danger controls)
We ship **semantic variants** for `Alert`, `Text`, `Button`, and `ActionIcon` so callouts and status text are token-driven in both light and dark. **Prefer these over raw Mantine palette colors** (`color="yellow"`, `color="red"`, `c="green"`, etc.).
The variant → token mapping is centralized in `packages/app/src/theme/semanticVariants.ts` (the single source of truth, consumed by `theme/mantineTheme.ts`). See the Storybook stories `Components/Alert` (interactive `Playground`) and `Design tokens/Semantic variants` for the full visual matrix.
**`Alert`** — `info` | `success` | `warning` | `danger`. Renders a tinted `-subtle` background with the title, icon, **and body text** in the semantic color token:
```tsx
// ✅ token-driven, works in light and dark, meets WCAG AA
<Alert variant="warning" title="Heads up">This may take a while.</Alert>
<Alert variant="danger" title="Failed">Could not save the alert.</Alert>
// ❌ hardcoded Mantine palette — not theme-aware, inconsistent contrast
<Alert color="yellow" title="Heads up">...</Alert>
<Alert color="red" title="Failed">...</Alert>
```
**`Text`** — `danger` | `warning` | `success` for inline status/validation text:
```tsx
<Text variant="danger">Required field</Text>
<Text variant="success">Connection verified</Text>
// ❌ don't reach for raw palette colors for semantic status text
<Text c="red.5">Required field</Text>
```
**`Button` / `ActionIcon` `variant="danger"`** is a **soft** control: a tinted `--color-bg-danger-subtle` background (with hover) and semantic foreground, not a solid red fill. `warning`/`success` are intentionally **not** exposed as control variants — use them on `Text` and `Alert` only.
**Note**: Existing `<Alert color="...">` call sites are untouched; the semantic variants are opt-in. Prefer the variant for any **new** callout, and migrate nearby `color="..."` alerts when you touch them.
### Confirmation dialogs: use `useConfirm` (REQUIRED)
**Use `useConfirm` (`@/useConfirm`) for any "are you sure?" step. Do not
hand-roll a `<Modal>` with Cancel/Confirm buttons.** The provider is already
mounted app-wide in `pages/_app.tsx`, so there is no setup at the call site.
```tsx
const confirm = useConfirm();
const handleDelete = async () => {
if (
await confirm(
<>
Deleting {name} is <b>not reversible</b>.
</>,
'Delete',
{ variant: 'danger' },
)
) {
await deleteThing.mutateAsync({ id });
}
};
```
- The message is a `ReactNode`, so it can carry emphasis and multiple sentences.
- Pass `{ variant: 'danger' }` for destructive actions; the confirm label
defaults to `Confirm`.
- It resolves **exactly once**, so a double click on Confirm during the modal's
exit transition cannot fire the action twice. A hand-rolled modal has to guard
that itself.
- Test ids are shared and already exist: `confirm-modal`,
`confirm-confirm-button`, `confirm-cancel-button`. **Do not invent per-flow
confirm/cancel test ids** — E2E page objects key off the shared ones.
**Known limits.** It passes no `title` to the Modal and renders the body at
`size="sm" opacity={0.7}`, and CSS opacity applies to the whole subtree so a
nested `<Text>` cannot opt back out. If a flow genuinely needs a heading or
full-contrast body, **extend `useConfirm`** (an optional prop, applied to all
call sites) rather than forking a one-off modal.
**In component tests**, mock it — `ConfirmProvider` pulls in `next/router`,
which is not available in jsdom:
```tsx
jest.mock('@/useConfirm', () => ({ useConfirm: jest.fn() }));
```
Assert on the arguments (and render the message `ReactNode` if you need to check
the copy). Exercise the real dialog in E2E instead.
### EmptyState Component (REQUIRED)
**Use `EmptyState` (`@/components/EmptyState`) for all empty/no-data states.** Do not create ad-hoc inline empty states.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `icon` | `ReactNode` | — | Icon in the theme circle (hidden if not provided) |
| `title` | `string` | — | Heading text (headline style — no trailing period) |
| `description` | `ReactNode` | — | Subtext below the title |
| `children` | `ReactNode` | — | Actions (buttons, links) below description |
| `variant` | `"default" \| "card"` | `"default"` | `"card"` wraps in a bordered Paper |
```tsx
// ❌ BAD - ad-hoc inline empty states
<div className="text-center my-4 fs-8">No data</div>
<Text ta="center" c="dimmed">Nothing here</Text>
// ✅ GOOD - use the EmptyState component
<EmptyState
icon={<IconBell size={32} />}
title="No alerts created yet"
description="Create alerts from dashboard charts or saved searches."
variant="card"
/>
```
**Title copy**: Treat `title` as a short headline (like `Title` in the UI). Do **not** end it with a period. Use `description` for full sentences, which should use normal punctuation including a trailing period when appropriate. Match listing pages (e.g. dashboards and saved searches use parallel phrasing such as “No matching … yet” / “No … yet” without dots).
### Object names: use `InlineNameInput` (REQUIRED)
**Name or rename a dashboard, saved search, tile, or any other user-created
object with `InlineNameInput` (`@/components/InlineNameInput/InlineNameInput`).**
Do not build a pencil-button edit mode, a bordered name `TextInput` in a
header, or a "Name" field inside a save modal. See
[naming_objects.md](naming_objects.md) for which variant to use.
### Code snippets (REQUIRED)
**Do not render raw `<pre>`, ad-hoc `Paper` + monospace text, or unstyled `<code>`.** Use the existing Mantine/product components so snippets match Terraform export, onboarding, and Storybook guidelines.
| Kind | Component | When |
|------|-----------|------|
| **Inline code** | Mantine `<Code>` | Short tokens in prose (`Session`, a column name, a flag). |
| **Fenced / multi-line / copyable** | `CopySnippet` (`@/components/ClickStackOnboarding/CopySnippet`) | Install commands, HCL, JSX examples, any block the user might copy. Wraps `<Code block>` plus a Copy button. |
```tsx
// ✅ inline — Mantine Code
Create a source with <Code>Session</Code> type.
// ✅ fenced / copyable — CopySnippet (Code + Copy)
<CopySnippet
label="Import block"
snippet={`import { clickstack_dashboard } from "clickhouse/clickstack"`}
/>
// Label is optional when the surrounding heading already names the snippet
<CopySnippet snippet={USAGE} />
// ❌ BAD — raw pre / Paper chrome
<pre>{snippet}</pre>
<Paper bg="var(--color-bg-code)"><Text component="pre" ff="monospace">{snippet}</Text></Paper>
```
**SQL query previews** (rendered ClickHouse SQL with highlighting) still use `SQLPreview` / `ChartSQLPreview` — those are editors, not snippet chrome.
Storybook: `Components/Code` (live `InlineCode`, `FencedBlocks`, and `Usage`). Guidelines markdown maps fenced blocks → `CopySnippet` and inline `` `code` `` → `<Code>` automatically. Follow the same split in app UI.
### Chart cards (ChartCard)
**Use `ChartCard` (`@/components/charts/ChartCard`) to wrap a chart in a card.**
It is a bordered surface with the same header treatment as a custom dashboard
tile (a full-bleed divider under the title). It replaces the older `ChartBox`;
don't hand-roll a bordered `<div>`/`<Paper>` around a chart.
`ChartCard` renders the card **chrome only**. The header divider is drawn only
when a descendant renders a `ChartContainer` with a `title` (or `toolbarItems`) —
`ChartCard` supplies the `ChartContainerCardHeaderProvider` that switches that
header into card mode — so put a chart that renders a `ChartContainer` inside it
(`DBTimeChart`, `DBTableChart`, `DBHeatmapChart`, `DBListBarChart`, …). Content
with its own heading (e.g. a bespoke table card) should still route that heading
through a titled `ChartContainer` rather than a bare `Text`, so it gets the same
card header — divider and top padding included — instead of sitting flush against
the top border. The tile-level controls (fullscreen, line/bar display switcher,
kebab menu) belong to dashboard tiles and are intentionally **not** part of
`ChartCard`.
| Prop | Type | Description |
|------|------|-------------|
| `children` | `ReactNode` | The chart, usually a `DB*Chart` (or a titled `ChartContainer`) |
| `style` | `CSSProperties` | Sizing/overflow override — pass a fixed `height`, or `flex: 1; height: 100%` to fill a flex row (`paddingInline` is pinned to keep the divider aligned) |
| `data-testid` | `string` | Test hook |
```tsx
// ✅ GOOD — shared card chrome, consistent with dashboard tiles
<ChartCard style={{ height: 350 }}>
<DBTimeChart title="Request Latency" config={config} />
</ChartCard>
// ❌ BAD — hand-rolled card that drifts from the dashboard look
<Box style={{ border: '1px solid var(--color-border)', borderRadius: 4 }}>
<DBTimeChart title="Request Latency" config={config} />
</Box>
```
**Give it a height.** `ChartCard` is `width: 100%` and fills its parent, so the
parent (or a `style={{ height }}`) must define the height. For equal-width
side-by-side charts (e.g. the RED row) use
`style={{ flex: 1, minWidth: 0, minHeight: 0, height: '100%' }}` inside a
`Flex`. See the `Charts/ChartCard` Storybook stories for the variants.
## UI text: use sentence case
All user-facing text uses **sentence case** — capitalize only the **first word**
and any proper nouns/acronyms. Do **not** use Title Case (capitalizing every
significant word).
This applies to every string a user reads: field labels, buttons, tab/menu
items, headings, section titles, modal titles, placeholders, tooltips, table
column headers, empty states, and toast/notification copy.
| Title Case (avoid) | Sentence case (use) |
|--------------------|---------------------|
| `Data Source` | `Data source` |
| `Chart Name` | `Chart name` |
| `Add Series` | `Add series` |
| `Count of Events` | `Count of events` |
| `Save Changes` | `Save changes` |
| `Delete Dashboard` | `Delete dashboard` |
**Keep the original casing of proper nouns, product names, and acronyms**
anywhere in the string — sentence case only changes the words around them:
HyperDX, ClickHouse, ClickStack, OpenTelemetry/OTel, Lucene, SQL, PromQL,
MongoDB, Kubernetes, JSON, CSV, URL, ID, API, MCP, CPU, P95. For example:
`Search your events w/ Lucene`, `Edit SQL`, `Copy as cURL`, `View in ClickHouse`.
Only sentence-case **static UI chrome**. Never rewrite dynamic/user data (column
names, tag values, log/trace content, source or dashboard names a user typed) —
render those verbatim.
## Semantic design tokens (prefer over raw Mantine colors)
The UI is built with **Mantine components**, but **colors and surfaces** should follow the **semantic CSS custom properties** in our themes (`--color-*`, etc.), not ad-hoc Mantine palette values. Those tokens are defined in `packages/app/src/theme/_tokens.scss`, align with a **Click UI**–style system, and are shared by every deployment. They are the path toward a shared design system even while Mantine remains the component layer.
- **Do**: Use Mantine for layout, components, and spacing; use **semantic tokens** for themed backgrounds, text colors, borders, and states (e.g. `style={{ color: 'var(--color-text-muted)' }}` or `style={{ border: '1px solid var(--color-border)' }}`).
- **Do not**: Rely on raw Mantine color props for app chrome and content when a semantic token exists — e.g. `c="gray.5"`, `bg="dark.7"`, or arbitrary `color="blue.4"` for surfaces that should match the rest of the product.
- **Reference**: `packages/app/src/theme/semanticColorsGrouped.ts` (token names), `packages/app/src/theme/_tokens.scss`, and Storybook (`SemanticColors` and related theme stories) for a visual map.
Mantine theme overrides in `packages/app/src/theme/**` may map Mantine’s scale to our palette; that does not replace using **`var(--color-...)`** in new styling where you need explicit color control.
## Click UI (incremental adoption)
`@clickhouse/click-ui` is a dependency of `packages/app`. `ThemeWrapper` mounts
`ClickUIProvider` inside `MantineProvider` with `persistTheme={false}`, so it
follows the app color mode and never writes its own theme to localStorage.
Storybook and `renderWithMantine` in tests get the provider for free.
- **Do**: Prefer a click-ui component over a new Mantine one when click-ui has
an equivalent for a simple primitive (`Link`, `Text`, `Title`, `Container`,
`Badge`, `Separator`, `Spacer`, `Icon`). Import from `@clickhouse/click-ui`.
- **Do not**: Mix both libraries inside one small component, and do not
replace working Mantine code outside the change you were asked to make.
- **`Container` is not a `Box`**: click-ui has no plain-wrapper equivalent of
Mantine `Box`. `Container` is a flex layout, closer to `Group` or `Stack`. It
defaults to `display="flex"`, `orientation="horizontal"`, and
`alignItems="center"`, so a direct swap puts the children in one row. It also
has no `m`/`p`/`w`-style props, only `gap` and `padding` tokens. To replace a
`Box` that only carries attributes such as `id` or `data-testid`, use a plain
HTML element. To replace a `Box` that uses Mantine style props, keep the
`Box`, or set `Container` props to match the layout and check it visually.
- **Styling**: click-ui styles live in the `clickui` CSS cascade layer, so any
unlayered app or Mantine rule wins over them. Override `--click-*` variables
outside the layer if a token must change.
- **First use**: `packages/app/src/components/ContactSupportText.tsx` and the
`Click UI/Link` story in `packages/app/src/stories/ClickUI.stories.tsx`.
**Chart and visualization colors are a separate, more specific contract** — they have their own categorical, semantic, and heatmap palettes wired through `packages/app/src/utils.ts` helpers. Don't hard-code series colors or reuse `--color-text-*` for charts. See [`data_viz_colors.md`](./data_viz_colors.md) before touching anything that renders data.
## Refactoring
- Edit files directly - don't create `component-v2.tsx` copies
- Look for duplicate code across the affected area
- Verify all callers and integrations after changes
- Refactor to improve clarity or reduce complexity, not just to change
## File Naming and Organization
- Clear, descriptive names following package conventions
- Avoid "temp", "refactored", "improved" in permanent filenames
- Put related components in a single directory