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
13 changes: 13 additions & 0 deletions .changeset/multiline-search-bar-always-visible.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@hyperdx/app': patch
---

fix: keep a multi-line query visible when the field is not focused

Multi-line SQL fields collapsed to a single line whenever they lost focus, so everything past the first line was hidden until you clicked back into them. Focusing one expanded it into a floating overlay that covered the content underneath — and in a container sized to its content, that overlay left the layout flow and shrank the field to a sliver one character wide.

Any field that allows multiple lines now simply sizes to its content, focused or not, growing the layout rather than floating over it, up to 150px before it scrolls. That covers the search WHERE, SELECT and ORDER BY, the chart editor's SQL fields, and PromQL. Lucene search fields similarly grow up to four lines. Fields with `allowMultiline={false}` remain single-line in both SQL and Lucene. The focus overlay is gone rather than made optional, so there is no longer a separate expand-on-focus state to reason about.

The language switch beside the search bar stretches to match, with no divider between it and the input. A Lucene bar at the default size also no longer overhangs that switch. The Lucene input reserved a 38px row while drawing a 36px box inside it, so the switch — sized to the 36px the SQL editor uses — stood 2px proud of the input. Both languages now take their height from one shared table, so the seam is flush and the bar does not change height when you switch. Only the default size was affected; the compact bar was already consistent at 30px.

Focus recolors the whole control, including the language switch, without overriding an error or warning border.
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ export const QueryExpressionFilterEditForm = ({
control={control}
name="expression"
placeholder="SQL column or expression"
language="sql"
enableHotkey
rules={{ required: true }}
parentRef={TOOLTIP_PORTAL_TARGET}
Expand Down
2 changes: 0 additions & 2 deletions packages/app/src/components/HeatmapSettingsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ export default function HeatmapSettingsDrawer({
size="xs"
tooltipText="Controls the Y axis range and scale — defines the metric plotted vertically."
placeholder="SQL expression"
language="sql"
onSubmit={form.handleSubmit(onSubmit)}
label="Value"
error={form.formState.errors.value?.message}
Expand All @@ -111,7 +110,6 @@ export default function HeatmapSettingsDrawer({
control={form.control}
name="count"
placeholder="SQL expression"
language="sql"
size="xs"
tooltipText="Controls the color intensity (Z axis) — shows how frequently or strongly each value occurs."
onSubmit={form.handleSubmit(onSubmit)}
Expand Down
1 change: 0 additions & 1 deletion packages/app/src/components/KubernetesFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,6 @@ export const KubernetesFilters: React.FC<KubernetesFiltersProps> = ({
sourceId={metricSource.id}
dateRange={dateRange}
placeholder="Search your events w/ Lucene ex. column:foo"
language="lucene"
name="searchQuery"
control={control}
size="xs"
Expand Down
25 changes: 8 additions & 17 deletions packages/app/src/components/PromQLEditor/PromQLEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import CodeMirror, {
tooltips,
} from '@uiw/react-codemirror';

import { EDITOR_INPUT_HEIGHTS } from '@/components/editorInputHeights';
import {
createCodeMirrorStyleTheme,
DEFAULT_CODE_MIRROR_BASIC_SETUP,
Expand All @@ -26,6 +27,7 @@ import { usePromqlVariableCompletions } from '@/components/SQLEditor/variableCom
import {
useVariableValidation,
VariableIssueIndicator,
variableValidationState,
} from '@/components/SQLEditor/variableValidation';

import { createVariableCompletionSource } from './variableCompletionSource';
Expand Down Expand Up @@ -197,36 +199,25 @@ export default function PromQLEditor({
}
}, []);

const isExpanded = isFocused;
const isVariableWarningOnly =
variableIssues.errors.length === 0 && variableIssues.warnings.length > 0;
const baseHeight = 36;
const validationState = variableValidationState(variableIssues);
const baseHeight = EDITOR_INPUT_HEIGHTS.sm;

return (
<div
className={styles.wrapper}
style={{ ['--editor-base-height' as string]: `${baseHeight}px` }}
data-expanded={isExpanded ? 'true' : undefined}
>
{isExpanded && <div className={styles.placeholder} aria-hidden="true" />}
<Paper
shadow="none"
className={cx(
styles.paper,
variableIssues.errors.length > 0 ? styles.error : undefined,
isVariableWarningOnly ? styles.warning : undefined,
isExpanded ? styles.expanded : undefined,
!isExpanded ? styles.collapseFade : undefined,
validationState === 'error' ? styles.error : undefined,
validationState === 'warning' ? styles.warning : undefined,
isFocused ? styles.focused : undefined,
)}
ps="4px"
>
<div
className={cx(
styles.cmWrapper,
!isExpanded ? styles.collapsed : undefined,
isExpanded ? 'cm-editor-multiline' : undefined,
)}
>
<div className={cx(styles.cmWrapper, 'cm-editor-multiline')}>
<CodeMirror
indentWithTab={false}
ref={ref}
Expand Down
60 changes: 14 additions & 46 deletions packages/app/src/components/SQLEditor/SQLInlineEditor.module.scss
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
.wrapper {
position: relative;
flex: 1 1 0%;
width: 100%;
min-width: 0;
}

.placeholder {
min-height: var(--editor-base-height, 36px);
width: 100%;
}

.paper {
background-color: var(--color-bg-field);
border: 1px solid var(--color-border);
Expand All @@ -18,29 +12,29 @@
min-height: var(--editor-base-height, 36px);
box-shadow: none;

/* The Paper theme gives every Paper an inline `border`, and an inline
declaration outranks any selector here, so every state colour below has to
force its way past it. */
&.error {
border-color: var(--color-bg-danger);
border-color: var(--color-bg-danger) !important;
}

&.warning {
border-color: var(--color-bg-warning);
border-color: var(--color-bg-warning) !important;
}

&.expanded {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 100;
box-shadow: var(--mb-shadow, var(--mantine-shadow-xs));
border-bottom: 2px solid var(--color-bg-brand) !important;
}

&:not(.expanded) {
position: relative;
/* Single-line editors only: hold one row and hide the rest. */
&.clamped {
max-height: var(--editor-base-height, 36px);
overflow: hidden;
}

/* An editor that grows with its content can't change border width on focus
without nudging everything below it, so recolor instead. Leave
error/warning borders alone so they stay visible while editing. */
&:not(.error, .warning).focused {
border-color: var(--color-bg-brand) !important;
}
}

.label {
Expand All @@ -65,30 +59,4 @@
padding-top: 2.4px;
font-size: var(--mantine-font-size-xs);
}

&.collapsed {
overflow: hidden;
}
}

.languageSwitchWrapper {
display: flex;
align-items: center;
min-height: 100%;
}

/* Fade at the bottom when allowMultiline and collapsed (single-line) so text doesn't appear hard-cut */
.collapseFade::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 12px;
background: linear-gradient(to bottom, transparent, var(--color-bg-field));
pointer-events: none;
}

.expanded {
/* Expanded state: no fade, full height */
}
44 changes: 13 additions & 31 deletions packages/app/src/components/SQLEditor/SQLInlineEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import CodeMirror, {
tooltips,
} from '@uiw/react-codemirror';

import InputLanguageSwitch from '@/components/SearchInput/InputLanguageSwitch';
import { EDITOR_INPUT_HEIGHTS } from '@/components/editorInputHeights';
import { useMultipleAllFields } from '@/hooks/useMetadata';
import { useSource } from '@/source';
import { useQueryHistory } from '@/utils';
Expand All @@ -46,6 +46,7 @@ import { useSqlVariableCompletions } from './variableCompletions';
import {
useVariableValidation,
VariableIssueIndicator,
variableValidationState,
} from './variableValidation';

import styles from './SQLInlineEditor.module.scss';
Expand All @@ -55,8 +56,6 @@ type SQLInlineEditorProps = {
value: string;
onChange: (value: string) => void;
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

error?: React.ReactNode;
size?: string;
Expand Down Expand Up @@ -87,8 +86,6 @@ export default function SQLInlineEditor({
filterField,
onChange,
placeholder,
onLanguageChange,
language,
onSubmit,
error,
value,
Expand Down Expand Up @@ -162,9 +159,8 @@ export default function SQLInlineEditor({
};
}, [queryHistory, onSelectSearchHistory]);

// Dashboard variables in scope, offered alongside the column identifiers, and
// checked for the references that won't expand. This editor's content is
// always SQL — `language` only drives the switch.
// Dashboard variables in scope, offered alongside the column identifiers,
// and checked for the references that won't expand.
const variableCompletions = useSqlVariableCompletions({
enabled: enableVariables,
});
Expand Down Expand Up @@ -337,29 +333,24 @@ export default function SQLInlineEditor({
}
}, []);

// Only apply expanded styling when multiline is enabled and focused
const isExpanded = allowMultiline && isFocused;

const isVariableWarningOnly =
variableIssues.errors.length === 0 && variableIssues.warnings.length > 0;
const baseHeight = size === 'xs' ? 30 : 36;
const validationState = variableValidationState(variableIssues, !!error);
const baseHeight =
size === 'xs' ? EDITOR_INPUT_HEIGHTS.xs : EDITOR_INPUT_HEIGHTS.sm;

return (
<div
className={styles.wrapper}
style={{ ['--editor-base-height' as string]: `${baseHeight}px` }}
data-expanded={isExpanded ? 'true' : undefined}
data-validation-state={validationState}
>
{/* When expanded, Paper is absolute; this keeps the wrapper width stable */}
{isExpanded && <div className={styles.placeholder} aria-hidden="true" />}
<Paper
shadow="none"
className={cx(
styles.paper,
error || variableIssues.errors.length > 0 ? styles.error : undefined,
isVariableWarningOnly ? styles.warning : undefined,
isExpanded ? styles.expanded : undefined,
allowMultiline && !isExpanded ? styles.collapseFade : undefined,
validationState === 'error' ? styles.error : undefined,
validationState === 'warning' ? styles.warning : undefined,
!allowMultiline ? styles.clamped : undefined,
isFocused ? styles.focused : undefined,
)}
ps="4px"
>
Expand All @@ -386,8 +377,7 @@ export default function SQLInlineEditor({
className={cx(
styles.cmWrapper,
size === 'xs' ? styles.sizeXs : undefined,
!isExpanded ? styles.collapsed : undefined,
isExpanded ? 'cm-editor-multiline' : undefined,
allowMultiline ? 'cm-editor-multiline' : undefined,
)}
>
<CodeMirror
Expand All @@ -410,14 +400,6 @@ export default function SQLInlineEditor({
/>
</div>
<VariableIssueIndicator issues={variableIssues} />
{onLanguageChange != null && language != null && (
<div className={styles.languageSwitchWrapper}>
<InputLanguageSwitch
language={language}
onLanguageChange={onLanguageChange}
/>
</div>
)}
</Paper>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
line-height: 1;
cursor: help;

/* In the SQL editor, centers the icon on the collapsed input's first line so
it stays put when the editor expands to multiple lines. Elsewhere (the
Lucene input's right section) it just fills its host. */
/* Keep the icon on the editor's first row as multiline content grows. */
min-height: var(--editor-base-height, 100%);
}
16 changes: 16 additions & 0 deletions packages/app/src/components/SQLEditor/variableValidation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ const VALIDATION_DEBOUNCE_MS = 500;
export const hasVariableIssues = (issues: VariableReferenceIssues) =>
issues.errors.length > 0 || issues.warnings.length > 0;

export type VariableValidationState = 'error' | 'warning';

/**
* Which state an input paints its border with. Errors win over warnings, and
* `hasError` folds in an error the caller already knows about from elsewhere
* (an invalid expression), which outranks a variable warning.
*/
export const variableValidationState = (
issues: VariableReferenceIssues,
hasError = false,
): VariableValidationState | undefined => {
if (hasError || issues.errors.length > 0) return 'error';
if (issues.warnings.length > 0) return 'warning';
return undefined;
};

/**
* Returns variable-reference issues in the given template. Returns nothing when no variables are in scope.
*/
Expand Down
Loading
Loading