Skip to content

Commit bdca9bd

Browse files
authored
[HDX-3966] Improve TUI error message rendering and add SQL preview (#2095)
## Summary Improves error message rendering in the CLI/TUI with visible highlighting, structured error display matching the web frontend patterns, and adds a SQL preview feature. **Linear:** https://linear.app/hyperdx/issue/HDX-3966 ## Changes ### Error Display - **`ErrorDisplay` component** — Reusable error/warning display with bordered boxes, color-coded severity (red/yellow), icons, and responsive rendering based on terminal height (compact < 20 rows, medium 20-35, full > 35) - **`parseError` utility** — Parses ClickHouse `DB::Exception` strings and HTML error responses into clean messages; accepts `Error | ClickHouseQueryError | string` - **`useSqlSuggestions`** — Copied from `packages/app/src/hooks/useSqlSuggestions.tsx` to `packages/cli/src/shared/` for detecting common ClickHouse query mistakes (e.g. double quotes) ### Error Object Preservation - **`useEventData` / `useTraceData`** — Error state changed from `string | null` to `Error | null`, preserving `ClickHouseQueryError` instances with their `.query` property through the entire chain - **Row detail errors** — Added `expandedRowError` state (replaces `__fetch_error` string embedding in row data) and `selectedRowError` for trace span details - **Pagination errors** — Now surfaced instead of silently swallowed ### SQL Preview (Shift-D) - Press `D` to view the generated ClickHouse SQL for the current context - Context-aware: shows table query, expanded row SELECT *, or trace spans query depending on which view is active - Uses `parameterizedQueryToSql` from common-utils to resolve query parameters - Scrollable with Ctrl+D/U for long queries - Renders as an overlay (display=none) to preserve component state — no re-fetch when toggling ### Other Fixes - Follow mode (`f` key) disabled in event detail panel (was incorrectly toggleable) - All error display sites updated: TableView, DetailPanel, TraceWaterfall, Footer, App, LoginForm ## Files Created | File | Purpose | |------|---------| | `packages/cli/src/components/ErrorDisplay.tsx` | Reusable error/warning display component | | `packages/cli/src/utils/parseError.ts` | Error message parser (ClickHouse, HTML) | | `packages/cli/src/shared/useSqlSuggestions.ts` | SQL suggestion engines (copied from app) | ## Files Modified | File | Change | |------|--------| | `useEventData.ts` | Error objects, expandedRowError, lastChSql/lastExpandedChSql as state | | `useTraceData.ts` | Error objects, selectedRowError, eager traceQuery via useMemo | | `EventViewer.tsx` | showSql overlay, activeChSql, traceChSql state | | `useKeybindings.ts` | D keybinding, showSql handling, f key guard | | `SubComponents.tsx` | SqlPreviewScreen, Footer with ErrorDisplay, HelpScreen update | | `TableView.tsx` | Error object + searchQuery props | | `DetailPanel.tsx` | expandedRowError prop, onTraceChSqlChange | | `TraceWaterfall.tsx` | onChSqlChange callback, selectedRowError display | | `types.ts` (TraceWaterfall) | onChSqlChange prop | | `App.tsx` | ErrorDisplay | | `LoginForm.tsx` | ErrorDisplay | ## Testing - `tsc --noEmit` — passes - `yarn build` — passes - `npx lint-staged` — passes - Manual testing of all error display paths and SQL preview across tabs ## Demo <img width="1169" height="322" alt="image" src="https://github.com/user-attachments/assets/b0580f0a-c226-4297-9937-f263afd39f6a" /> <img width="1165" height="250" alt="image" src="https://github.com/user-attachments/assets/d7139a91-0c2e-4b60-b637-3e900447d3fa" />
1 parent 0daa529 commit bdca9bd

15 files changed

Lines changed: 830 additions & 137 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@hyperdx/cli": patch
3+
---
4+
5+
Improve error message rendering with visible highlighting and add SQL preview
6+
7+
- Add ErrorDisplay component with bordered boxes, color-coded severity, and responsive terminal height adaptation
8+
- Preserve ClickHouseQueryError objects through the error chain to show sent query context
9+
- Surface previously silent errors: pagination failures, row detail fetch errors, trace span detail errors
10+
- Add Shift-D keybinding to view generated ClickHouse SQL (context-aware across all tabs)
11+
- Copy useSqlSuggestions from app package to detect common query mistakes
12+
- Disable follow mode toggle in event detail panel

packages/cli/src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
type SourceResponse,
1010
type SavedSearchResponse,
1111
} from '@/api/client';
12+
import ErrorDisplay from '@/components/ErrorDisplay';
1213
import LoginForm from '@/components/LoginForm';
1314
import SourcePicker from '@/components/SourcePicker';
1415
import EventViewer from '@/components/EventViewer';
@@ -124,7 +125,7 @@ export default function App({ apiUrl, query, sourceName, follow }: AppProps) {
124125
if (error) {
125126
return (
126127
<Box paddingX={1}>
127-
<Text color="red">Error: {error}</Text>
128+
<ErrorDisplay error={error} severity="error" />
128129
</Box>
129130
);
130131
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* Reusable error/warning display component for the TUI.
3+
*
4+
* Renders errors and warnings with clear visual highlighting
5+
* so they are immediately noticeable and distinguishable from
6+
* normal output.
7+
*
8+
* Responsive to terminal height:
9+
* - Small (< 20 rows): compact single-line rendering
10+
* - Medium (20–35 rows): bordered box with message only
11+
* - Large (> 35 rows): full display with query context + suggestions
12+
*
13+
* Mirrors the error rendering patterns from the web frontend:
14+
* @source packages/app/src/DBSearchPage.tsx (queryError + ClickHouseQueryError rendering)
15+
* @source packages/app/src/components/DBTableChart.tsx (error message + sent query)
16+
*/
17+
18+
import React from 'react';
19+
import { Box, Text, useStdout } from 'ink';
20+
21+
import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse';
22+
23+
import { useSqlSuggestions, type Suggestion } from '@/shared/useSqlSuggestions';
24+
import { parseError, type ErrorSeverity } from '@/utils/parseError';
25+
26+
/** Terminal height breakpoints for responsive rendering */
27+
const COMPACT_THRESHOLD = 20;
28+
const FULL_THRESHOLD = 35;
29+
30+
interface ErrorDisplayProps {
31+
/** The error — accepts a string, Error, or ClickHouseQueryError */
32+
error: string | Error | ClickHouseQueryError;
33+
/** Severity level — defaults to 'error' */
34+
severity?: ErrorSeverity;
35+
/** Optional additional context shown below the error message */
36+
detail?: string;
37+
/**
38+
* The user's search query — when provided alongside a ClickHouseQueryError,
39+
* SQL suggestions (e.g. double-quote correction) will be shown.
40+
* @source packages/app/src/DBSearchPage.tsx (whereSuggestions)
41+
*/
42+
searchQuery?: string;
43+
/** Force compact (single-line) mode regardless of terminal size */
44+
compact?: boolean;
45+
}
46+
47+
const SEVERITY_CONFIG = {
48+
error: {
49+
icon: '✖',
50+
label: 'Error',
51+
color: 'red' as const,
52+
borderColor: 'red' as const,
53+
},
54+
warning: {
55+
icon: '⚠',
56+
label: 'Warning',
57+
color: 'yellow' as const,
58+
borderColor: 'yellow' as const,
59+
},
60+
};
61+
62+
/**
63+
* Renders a visually prominent error or warning message.
64+
*
65+
* Large terminal (> 35 rows):
66+
* ╭─────────────────────────────────────────────────╮
67+
* │ ✖ Error │
68+
* │ Syntax error: failed at position 5 ... │
69+
* │ │
70+
* │ Sent Query: │
71+
* │ SELECT * FROM default.logs WHERE "name" = 'foo' │
72+
* │ │
73+
* │ 💡 ClickHouse does not support double quotes ... │
74+
* ╰─────────────────────────────────────────────────╯
75+
*
76+
* Medium terminal (20–35 rows):
77+
* ╭─────────────────────────────────────────────────╮
78+
* │ ✖ Error │
79+
* │ Syntax error: failed at position 5 ... │
80+
* ╰─────────────────────────────────────────────────╯
81+
*
82+
* Small terminal (< 20 rows) or compact=true:
83+
* ✖ Error: Syntax error: failed at position 5
84+
*/
85+
export default function ErrorDisplay({
86+
error,
87+
severity = 'error',
88+
detail,
89+
searchQuery,
90+
compact = false,
91+
}: ErrorDisplayProps) {
92+
const { stdout } = useStdout();
93+
const termHeight = stdout?.rows ?? 24;
94+
95+
const config = SEVERITY_CONFIG[severity];
96+
const parsed = parseError(error, severity);
97+
98+
// SQL suggestions — only when we have a search query and there's an error
99+
const suggestions = useSqlSuggestions({
100+
input: searchQuery ?? '',
101+
enabled: !!searchQuery && severity === 'error',
102+
});
103+
104+
// Responsive: force compact when terminal is very small
105+
const useCompact = compact || termHeight < COMPACT_THRESHOLD;
106+
// Only show query context and suggestions in large terminals
107+
const showFullDetails = !useCompact && termHeight >= FULL_THRESHOLD;
108+
109+
if (useCompact) {
110+
return (
111+
<Box>
112+
<Text color={config.color} bold>
113+
{config.icon} {config.label}:{' '}
114+
</Text>
115+
<Text color={config.color} wrap="truncate">
116+
{parsed.message}
117+
</Text>
118+
</Box>
119+
);
120+
}
121+
122+
return (
123+
<Box
124+
flexDirection="column"
125+
borderStyle="round"
126+
borderColor={config.borderColor}
127+
paddingX={1}
128+
>
129+
<Text color={config.color} bold>
130+
{config.icon} {config.label}
131+
</Text>
132+
<Text color={config.color}>{parsed.message}</Text>
133+
134+
{/* Original query — only in large terminals */}
135+
{showFullDetails && parsed.query && (
136+
<Box flexDirection="column" marginTop={1}>
137+
<Text dimColor bold>
138+
Sent Query:
139+
</Text>
140+
<Text dimColor wrap="wrap">
141+
{parsed.query}
142+
</Text>
143+
</Box>
144+
)}
145+
146+
{/* Additional context — only in large terminals */}
147+
{showFullDetails && detail && (
148+
<Box marginTop={1}>
149+
<Text dimColor wrap="wrap">
150+
{detail}
151+
</Text>
152+
</Box>
153+
)}
154+
155+
{/* SQL suggestions — only in large terminals */}
156+
{showFullDetails && suggestions && suggestions.length > 0 && (
157+
<Box flexDirection="column" marginTop={1}>
158+
{suggestions.map((s: Suggestion, i: number) => (
159+
<Text key={i} color="cyan">
160+
💡 {s.userMessage('where')}
161+
</Text>
162+
))}
163+
</Box>
164+
)}
165+
</Box>
166+
);
167+
}

packages/cli/src/components/EventViewer/DetailPanel.tsx

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Spinner from 'ink-spinner';
44

55
import type { SourceResponse, ProxyClickhouseClient } from '@/api/client';
66
import ColumnValues from '@/components/ColumnValues';
7+
import ErrorDisplay from '@/components/ErrorDisplay';
78
import RowOverview from '@/components/RowOverview';
89
import TraceWaterfall from '@/components/TraceWaterfall';
910

@@ -20,6 +21,7 @@ type DetailPanelProps = {
2021
detailTab: DetailTab;
2122
expandedRowData: Record<string, unknown> | null;
2223
expandedRowLoading: boolean;
24+
expandedRowError: Error | null;
2325
expandedTraceId: string | null;
2426
expandedSpanId: string | null;
2527
traceSelectedIndex: number | null;
@@ -40,6 +42,10 @@ type DetailPanelProps = {
4042
};
4143
scrollOffset: number;
4244
expandedRow: number;
45+
/** Callback when the trace tab's SQL changes */
46+
onTraceChSqlChange?: (
47+
chSql: { sql: string; params: Record<string, unknown> } | null,
48+
) => void;
4349
};
4450

4551
export function DetailPanel({
@@ -49,6 +55,7 @@ export function DetailPanel({
4955
detailTab,
5056
expandedRowData,
5157
expandedRowLoading,
58+
expandedRowError,
5259
expandedTraceId,
5360
expandedSpanId,
5461
traceSelectedIndex,
@@ -66,6 +73,7 @@ export function DetailPanel({
6673
expandedFormattedRow,
6774
scrollOffset,
6875
expandedRow,
76+
onTraceChSqlChange,
6977
}: DetailPanelProps) {
7078
const hasTrace =
7179
source.kind === 'trace' || (source.kind === 'log' && source.traceSourceId);
@@ -136,14 +144,25 @@ export function DetailPanel({
136144
<Spinner type="dots" /> Loading…
137145
</Text>
138146
) : expandedRowData ? (
139-
<RowOverview
140-
source={source}
141-
rowData={expandedRowData}
142-
searchQuery={detailSearchQuery}
143-
wrapLines={wrapLines}
144-
maxRows={fullDetailMaxRows}
145-
scrollOffset={columnValuesScrollOffset}
146-
/>
147+
<>
148+
{expandedRowError && (
149+
<Box marginBottom={1}>
150+
<ErrorDisplay
151+
error={expandedRowError}
152+
severity="warning"
153+
detail="Showing partial row data — full row fetch failed."
154+
/>
155+
</Box>
156+
)}
157+
<RowOverview
158+
source={source}
159+
rowData={expandedRowData}
160+
searchQuery={detailSearchQuery}
161+
wrapLines={wrapLines}
162+
maxRows={fullDetailMaxRows}
163+
scrollOffset={columnValuesScrollOffset}
164+
/>
165+
</>
147166
) : null}
148167
</Box>
149168
)}
@@ -201,6 +220,7 @@ export function DetailPanel({
201220
wrapLines={wrapLines}
202221
detailScrollOffset={traceDetailScrollOffset}
203222
detailMaxRows={detailMaxRows}
223+
onChSqlChange={onTraceChSqlChange}
204224
/>
205225
);
206226
})()}
@@ -217,13 +237,24 @@ export function DetailPanel({
217237
<Spinner type="dots" /> Loading all fields…
218238
</Text>
219239
) : expandedRowData ? (
220-
<ColumnValues
221-
data={expandedRowData}
222-
searchQuery={detailSearchQuery}
223-
wrapLines={wrapLines}
224-
maxRows={fullDetailMaxRows}
225-
scrollOffset={columnValuesScrollOffset}
226-
/>
240+
<>
241+
{expandedRowError && (
242+
<Box marginBottom={1}>
243+
<ErrorDisplay
244+
error={expandedRowError}
245+
severity="warning"
246+
detail="Showing partial row data — full row fetch failed."
247+
/>
248+
</Box>
249+
)}
250+
<ColumnValues
251+
data={expandedRowData}
252+
searchQuery={detailSearchQuery}
253+
wrapLines={wrapLines}
254+
maxRows={fullDetailMaxRows}
255+
scrollOffset={columnValuesScrollOffset}
256+
/>
257+
</>
227258
) : null}
228259
</Box>
229260
)}

0 commit comments

Comments
 (0)