Skip to content

[HDX-5307] Keep multi-line query fields visible when unfocused - #3150

Merged
kodiakhq[bot] merged 3 commits into
mainfrom
elizabet/hdx-5307-always-visible-query-fields
Sep 18, 2026
Merged

kodiakhq[bot] merged 3 commits into
mainfrom
elizabet/hdx-5307-always-visible-query-fields

Conversation

@elizabetdev

Copy link
Copy Markdown
Member

Summary

  • Fixes HDX-5307: SQL, Lucene, and PromQL query fields stay fully visible when unfocused, growing the layout instead of collapsing or floating an overlay.
  • Shared EDITOR_INPUT_HEIGHTS keeps the language switch flush with the input. allowMultiline={false} stays one row (Lucene scrolls horizontally).
  • Stacked follow-ups: toolbar alignment, search-bar chrome. Split from [HDX-5307] Keep multi-line query fields visible when unfocused #3132.

Test plan

  • On Search, type a two-line SQL and Lucene query, then click away: both lines stay visible and the bar grows the layout.
  • SELECT and ORDER BY stay expanded after blur.
  • On a source or chart form SQL field, a wrapped value stays visible after blur.
  • allowMultiline={false} (trace spans filter) stays one row after blur.
  • make dev-e2e FILE=multiline

Grow SQL, Lucene, and PromQL fields in layout instead of collapsing to one line or floating an overlay. Shared editor heights keep the language switch flush with the input.

Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f0f46bb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Patch
@hyperdx/api Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hyperdx-oss Ready Ready Preview Sep 18, 2026 4:11pm UTC
hyperdx-storybook Ready Ready Preview Sep 18, 2026 4:11pm UTC

Request Review

@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Sep 18, 2026
@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 404 production lines changed (Tier 2 max: < 250)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 14
  • Production lines changed: 404 (+ 853 in test files, excluded from tier calculation)
  • Branch: elizabet/hdx-5307-always-visible-query-fields
  • Author: elizabetdev

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@greptile-apps

greptile-apps Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The implementation appears safe to merge, with a non-blocking gap remaining in SELECT and ORDER BY regression coverage.

Fix All in Claude CodeFindings

  1. P1 Removed prop breaks type-check ▶
  2. P2 Clause locators target wrong editors ▶

Summary

Keeps multiline SQL, Lucene, and PromQL query fields expanded after blur while preserving single-line behavior where multiline input is disabled.

  • Moves the query-language switch outside the language-specific editors and shares editor height constants.
  • Preserves validation and focus border states across the joined search control.
  • Adds reusable Playwright helpers and regression coverage for multiline growth, blur, and focus behavior.

Reviews (3) · Last reviewed commit: "Merge branch 'main' into elizabet/hdx-53..."

placeholder?: string;
onLanguageChange?: (language: 'sql' | 'lucene') => void;
language?: 'sql' | 'lucene';
onSubmit?: () => void;

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.

P1 Removed prop breaks type-check

Removing language from SQLInlineEditorProps leaves the unchanged QueryExpressionFilterEditForm caller passing language="sql" to SQLInlineEditorControlled. Because the controlled component derives its accepted props from this interface, TypeScript reports the extra property and the app type-check cannot pass until the caller is updated.

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

@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

E2E Test Results

✅ All tests passed • 364 passed • 1 skipped • 1302s

Status Count
✅ Passed 364
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found.

This PR replaces the focus-time position: absolute overlay for the SQL, Lucene, and PromQL query fields with in-flow growth, centralizes editor heights in editorInputHeights.ts, recolors borders on focus instead of resizing them, adds an allowMultiline single-row mode, and factors the validation-state ladder into variableValidationState(). Prop flow, CSS cascade order (error/warning border correctly outranks focus on both the input and the language switch), and caller updates were verified directly. The type-check regression flagged in a prior review is resolved: no remaining caller passes the removed language/onLanguageChange props to the affected editors, and the language= props in ChartSeriesEditor.tsx target unrelated components (MetricExplorerModal, MetricAttributeHelperPanel).

🟡 P2 -- recommended

  • packages/app/tests/e2e/features/shared/input-focus.spec.ts:45 -- The new spec asserts focus recolors both seam halves but never asserts that an error or warning border wins over focus, which is the interaction the new data-validation-state cascade rules in SearchWhereInput.module.scss and SQLInlineEditor.module.scss were added to guarantee.
    • Fix: Add a case that puts the WHERE input into an error/warning state, focuses it, and asserts the border color stays the danger/warning color rather than the focus color.
🔵 P3 nitpicks (2)
  • packages/app/src/components/SQLEditor/SQLInlineEditor.module.scss:24 -- Several border-color rules now rely on !important to defeat Mantine's inline Paper border, a fragile coupling to a third-party inline style that repeats across three stylesheets.
    • Fix: Consider centralizing the border override (e.g. a shared mixin or a single wrapper selector) so the !important escape hatch lives in one documented place.
  • packages/app/src/components/SearchInput/AutocompleteInput.tsx:200 -- When rightAdornment toggles from present to absent (the variable-warning icon clearing), the measuring div unmounts so rightSectionWidth retains its last non-zero value, leaving reserved right padding until the next remeasure.
    • Fix: Reset rightSectionWidth to 0 when rightAdornment is null so the textarea reclaims the space.

Reviewers (2): previous-comments, orchestrator diff analysis (correctness, maintainability, and CSS-cascade tracing performed directly against the diff).

Testing gaps:

  • Error/warning-border-wins-over-focus interaction is implemented but unasserted.
  • expectStaysOneRow and the border helpers flagged in prior review are now genuinely exercised (verified).

Coverage note: The correctness, kieran-typescript, julik-frontend-races, maintainability, and testing reviewer sub-agents were still running when output was requested; the findings above come from the completed previous-comments reviewer and direct orchestrator analysis of the full production diff, CSS specificity, prop flow, and the e2e/unit test files. No P0/P1 defects were found in that analysis.

* Border colors of the two halves of the WHERE control. They sit flush
* against each other, so a focus state on only one reads as half-focused.
*/
async getWhereBorderColors(): Promise<{

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 — Border-color test helpers have no callers, and the focus/validation recolor they were written for is untested

SearchPage.focusWhereInput (331), getWhereBorderColors (339), getSelectBorderColor (353), getOrderByBorderColor (359), WhereInputComponent.borderColors() and WhereInputComponent.focus() are referenced nowhere in packages/app/tests/e2e — readSeam's borderColor half exists only to feed them, and expectSeamFlush uses only the heights. Meanwhile the changeset's headline claim ("Focus recolors the whole control, including the language switch, without overriding an error or warning border") is exactly what the new .root:has(.inputWrapper :focus) .languageSwitch / [data-validation-state] rules in SearchWhereInput.module.scss:55-65 implement, and nothing asserts it. Either add a spec that focuses the WHERE input and asserts languageSwitch and input border colors match (and that an error/warning state wins over focus), or delete the helpers.

await page.keyboard.press('ControlOrMeta+A');
await page.keyboard.type(WRAPPING_SINGLE_LINE_QUERY);

expect(await settledHeight(visibleBox)).toBeLessThanOrEqual(oneRowHeight + 1);

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 — expectStaysOneRow cannot fail in SQL mode — it measures a box with a hard max-height

For SQL, WhereInputComponent.field('SQL') sets visibleBox to borderedBox(this.sqlEditor) — the Paper, which SQLInlineEditor.tsx:356 now gives styles.clamped (max-height: var(--editor-base-height), overflow: hidden, SQLInlineEditor.module.scss:27-30). So settledHeight(visibleBox) <= oneRowHeight + 1 holds by construction even if a regression made the content wrap to two lines and clipped the second. Also assert the growth box stays one row (e.g. .cm-line count === 1 / growthBox height unchanged), which is the property allowMultiline={false} is actually about.

@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

PR Review

If you are a coding agent acting for the author, read this first.
Nothing below blocks merge automatically; a maintainer decides. What they will expect:

  • 🔴 critical or 🟠 major in code this PR adds or changes: fix before asking for review.
  • 🔵 minor in code this PR changes: your call. Fix if small, otherwise reply.
  • Anything about surrounding code, or asking you to widen the change (hoist a helper,
    dedupe with another file, fix other call sites): never fix here, whatever the
    severity. Reply with one sentence; if it is critical, say so plainly so a human sees it.

One commit per review round. After two rounds, stop and ask a maintainer to review
scope rather than addressing more automated findings. Full rule: AGENTS.md.

3 finding(s): 🔴 0 critical · 🟠 1 major · 🔵 2 minor

1 posted as inline comment(s) on the changed lines. 2 listed below.

Findings outside the changed lines

  • 🟠 packages/app/src/components/SQLEditor/SQLInlineEditor.tsx:307 — Shift+Enter still inserts a newline in a single-line SQL editor, and .clamped then hides it → The Shift-Enter binding is only registered when allowMultiline, but its run returns false — which in CodeMirror means "not handled", so the default line-break insertion happens either way (the old comment // Allow default behavior (insert new line) says exactly this). Omitting the binding for allowMultiline={false} therefore blocks nothing: in the trace waterfall's spans/logs filters (DBTraceWaterfallChart.tsx:1292,1331) or PatternColumnSelector.tsx:53, switch to SQL, type a, press Shift+Enter, type b — the doc becomes a\nb but .paper.clamped (max-height + overflow: hidden, SQLInlineEditor.module.scss:27-30) now hides the second line permanently, which is the exact bug this PR removes everywhere else. The Lucene half got an explicit guard (AutocompleteInput.tsx:290); SQL needs the mirror image: register { key: 'Shift-Enter', run: () => true } when !allowMultiline instead of registering nothing.
1 minor
  • 🔵 packages/app/src/components/SearchInput/SearchWhereInput.module.scss:75 — / hotkey hint drifts to the vertical middle of a grown query field → .shortcutHint is top: 50% inside .inputWrapper, which used to be pinned to one row (the Paper was max-height-clamped when unfocused) and now stretches with the query. On the search page (DBSearchPage.tsx:2355, enableHotkey), a three-line WHERE puts the / Kbd badge halfway down the box, floating over line 2 instead of sitting beside the first row. Anchor it to the first row: top: calc(var(--editor-base-height) / 2).

Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides.

…ders

The dashboard filter expression editor still passed the removed `language` prop, which failed the app type-check. Removing the mirror dead switch in AutocompleteInput/SearchInputV2 too, folding the validation-state ladder into variableValidation, and moving the focus-border spec here so the recolor it asserts ships with its test.

Co-authored-by: Cursor <cursoragent@cursor.com>
@elizabetdev

Copy link
Copy Markdown
Member Author

Addressed the review in 483d5d2.

P1 (build break) — QueryExpressionFilterEditForm still passed language="sql" to SQLInlineEditorControlled. Dropped; the editor is always SQL. npx tsc --noEmit in packages/app is clean.

P2 (unused border helpers) — input-focus.spec.ts belonged in this PR, not the chrome one: the focus recolor is implemented here. Moved it, so focusWhereInput / getWhereBorderColors / getSelectBorderColor / getOrderByBorderColor now have a caller that asserts both halves of the WHERE control take the focus color. The error-wins-over-focus half of the rule is still enforced only by the :not(.error, .warning).focused selector — no E2E page has dashboard variables in scope for a WHERE input, so there is nothing to drive a validation border from.

P3 / minor — also in this commit:

  • expectStaysOneRow now asserts the content stays one row (.cm-line count for CodeMirror, growth-box height for the textarea), not just the clamped bordered box. Good catch: that assertion could not fail in SQL mode.
  • The errors > 0 ? 'error' : warnings > 0 ? 'warning' ladder is now variableValidationState(issues, hasError?) in variableValidation.tsx, used by SQLInlineEditor, SearchInputV2, and PromQLEditor. AutocompleteInputValidationState is gone in favour of the shared VariableValidationState.
  • Dropped the mirror dead InputLanguageSwitch from AutocompleteInput / SearchInputV2 (plus language="lucene" at the KubernetesFilters call site), simplified the rightSection gate to rightAdornment != null, and trimmed the useLayoutEffect deps.
  • clauseField now builds on getSELECTEditor() / getOrderByEditor(); the incorrect comment about .cm-content ordering is gone.

Not doing: a Lucene validationState='error' unit case and the SQLInlineEditor error-prop case (P3). Both paths now run through one shared helper, which is what the duplicated ternaries were risking.

Stacked PRs #3151 and #3152 were rebased on this. 3905 app unit tests pass.

Comment on lines 349 to 350
getSELECTEditor() {
return this.page.locator('.cm-content').first();

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 Clause locators target wrong editors

In SQL mode, the WHERE field adds a CodeMirror editor before SELECT and ORDER BY. These global .cm-content indices therefore make getSELECTEditor() return WHERE and getOrderByEditor() return SELECT. The new focus and multiline tests exercise the wrong fields and can pass without covering ORDER BY, weakening this regression coverage.

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

autosize
minRows={1}
maxRows={isSearchInputFocused ? 4 : 1}
maxRows={allowMultiline ? maxVisibleRows : 1}

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.

🟠 major — An empty field now grows to fit its wrapped placeholder, so a narrow search box renders 2+ rows tall before the user types anything

react-textarea-autosize measures node.value || node.placeholder, so with maxRows no longer clamped to 1 while unfocused, an empty input sizes to the placeholder's wrapped height. KubernetesFilters.tsx:338 gives the Lucene input minWidth: 200 with the placeholder "Search your events w/ Lucene ex. column:foo", which wraps at that width — the empty k8s filter bar is now double height. The same applies to SQL: SQLInlineEditor has EditorView.lineWrapping on whenever allowMultiline (line 282) and the old .paper:not(.expanded) { max-height; overflow: hidden } clamp is gone, so DBTracePanel.tsx:404 ("Log Trace ID Column (ex. trace_id)") and DBSearchPage.tsx:2282 (SELECT, placeholder = the full default select list) grow while still empty. The PR's own helper documents the mechanism (tests/e2e/utils/multiline-input.ts: "the placeholder wraps at narrow widths and the content then drives the height"), which is why onSingleLine types a character first — so no test covers the empty state. Gate growth on there being content: maxRows={allowMultiline && value ? maxVisibleRows : 1}, and for SQL re-apply styles.clamped when the value is empty.

height: calc(var(--editor-base-height) - var(--editor-border-inset, 2px));
}

.shortcutHint {

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 / hotkey hint floats to the vertical middle of a multi-line query instead of staying on the first row

.shortcutHint is position: absolute; top: 50%; transform: translateY(-50%) inside .inputWrapper, which used to be one row tall whenever the hint was visible (the expanded editor was an absolute overlay). Now that the input grows in flow, the 22px Kbd centres on the whole box: on the search page (DBSearchPage.tsx:2362, enableHotkey, size="xs" → 30px base) a two-line WHERE puts it straddling both lines at the right edge, on top of the query text. Pin it to the first row the same way variableValidation.module.scss does — replace the centring with top: 0; height: var(--editor-base-height); display: flex; align-items: center (drop the transform).

field(language: QueryLanguage): MultilineField {
if (language === 'Lucene') {
const input = this.luceneInput;
return { focusTarget: input, growthBox: input, visibleBox: input };

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 — For Lucene, visibleBox is the textarea rather than the bordered box, so the growth assertion never checks the element this PR resized

MultilineField.visibleBox is documented as "the bordered box the user sees, which must not clip the content", and expectExpandsAndStaysExpanded asserts it grows separately from growthBox. For Lucene both are the same textarea, making that assertion a duplicate of the growthBox one; the Mantine .mantine-InputWrapper-root — the box whose reserved-38px/drawn-36px mismatch this PR fixes, and the only thing that could clip the second line — is never measured after it grows (expectSeamFlush only runs at one line, via onSingleLine). Point visibleBox at the wrapper, e.g. input.locator('xpath=ancestor::div[contains(@class, "mantine-InputWrapper-root")][1]'), mirroring borderedBox on the SQL side.


/* Single-line Lucene: one visual row, like SQL without line wrapping. A long
query scrolls horizontally instead of wrapping under the clip. */
.root[data-single-line='true'] .textarea textarea {

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 — overflow: auto hidden on a 28px single-line textarea lets a classic horizontal scrollbar eat the text

With white-space: pre and overflow-x: auto, a query wider than the box shows a real horizontal scrollbar on Chrome/Linux and Windows (~15px). The height is pinned by min-height: calc(var(--editor-base-height) - 2px) !important — 28px at the xs size used by the trace spans/logs filters (DBTraceWaterfallChart.tsx:1292) — so the scrollbar leaves ~13px for a ~19px text row, and overflow-y: hidden clips the rest. The new expectStaysOneRow check only compares heights, so it passes either way. Use overflow: hidden instead: a textarea still scrolls horizontally to follow the caret without reserving scrollbar space.

@pulpdrew pulpdrew left a comment •

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.

LGTM

nit: On the chart editor, the group by vertically aligns centered with the multi-line input, but the select row is top aligned Looks fixed in the next PR

field(language: QueryLanguage): MultilineField {
if (language === 'Lucene') {
const input = this.luceneInput;
return { focusTarget: input, growthBox: input, visibleBox: input };

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 — Lucene growth check compares the textarea against itself, so the anti-clipping assertion is vacuous

For Lucene, field() returns the same locator as both growthBox and visibleBox, but the bordered box the user sees is the Mantine InputWrapper root — .textarea carries the border and textarea { border: none !important } (AutocompleteInput.module.scss:16,23). So expectExpandsAndStaysExpanded's "The visible box has to actually grow, not clip the second line" check (utils/multiline-input.ts:143-146) compares the textarea to itself and would still pass if the wrapper clamped to one row — only the SQL arm of the test actually guards against clipping. Use the textarea's bordered wrapper (e.g. this.row.locator('.mantine-InputWrapper-root').first()) as visibleBox.

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

@kodiakhq
kodiakhq Bot merged commit 4db7397 into main Sep 18, 2026
27 of 28 checks passed
kodiakhq Bot pushed a commit that referenced this pull request Sep 21, 2026
## Summary
- Drop the redundant `WHERE` label and the `/` keycap overlay from the search bar.
- `/` and `s` still focus the input.
- Re-opened against `main` after #3152 merged into the stacked base branch instead of `main` (#3150 had already landed).

## Test plan
- [ ] Empty SQL bar shows the placeholder and no `WHERE` label.
- [ ] `/` and `s` still focus the input with no keycap overlay.

This branch was successfully deployed

2 active deployments
Preview – hyperdx-oss — f0f46bbd Deployed Sep 18, 2026 by vercel[bot]
Preview – hyperdx-storybook — f0f46bbd Deployed Sep 18, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants