Redesign query/search: syntax highlighting, autocomplete, richer results

QueryEditor.svelte wraps CodeMirror 6, not a hand-rolled
textarea-plus-overlay highlighter -- autocomplete needs real
cursor-aware popup positioning a plain textarea can't give. language.ts
is a StreamLanguage tokenizer for the pipe grammar; its token() function
must return real @lezer/highlight tag names looked up by string
('controlKeyword', 'operatorKeyword', 'name.function' for tag+modifier
pairs) -- a custom Tag.define() looks plausible but silently highlights
nothing. completions.ts is context-aware: stage keywords after `|`,
stats functions after `stats`, field names elsewhere.

A two-way-binding race between the editor's updateListener and an
external-sync $effect could drop characters on rapid/bulk input --
fixed with a lastEmitted guard so the sync effect only reacts to
genuinely external value changes, not its own echoes.

ResultsTable gets sortable columns (a real <button> in the <th>, so
sorting is keyboard-operable for free), resizable columns
(pointer-drag, deliberately mouse-only -- the resize handle stays out
of the tab order, same as most apps treat column resize), and
expandable rows. The row-expand affordance was originally a bare `<tr
onclick>` with no keyboard equivalent at all; fixed with
tabindex/role="button"/aria-expanded and an Enter/Space handler.

AddToDashboardModal lets a query built on the Search page become a
saved panel without hand-copying the query string.
This commit is contained in:
2026-08-16 12:36:01 -07:00
parent 45e0865a0c
commit 0e37ca6669
7 changed files with 702 additions and 62 deletions
+24 -30
View File
@@ -4,6 +4,8 @@
// deliberately just the input+run affordance, not results/history,
// which differ per consumer.
import type { Language } from '$lib/api';
import { Button } from '$lib/components/ui';
import QueryEditor from '$lib/query-editor/QueryEditor.svelte';
let {
query = $bindable(''),
@@ -28,20 +30,10 @@
}
let detected = $derived(detectedLanguage(query));
let effectiveLanguage = $derived(language === '' ? detected : language);
function onKeydown(e: KeyboardEvent) {
// Cmd/Ctrl+Enter runs the query -- textarea's own Enter key needs
// to stay newline-for-pipe-stage-formatting, so this isn't a bare
// Enter binding.
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
e.preventDefault();
onRun();
}
}
</script>
<div class="query-bar">
<textarea bind:value={query} onkeydown={onKeydown} rows="4" spellcheck="false" {placeholder}></textarea>
<QueryEditor bind:value={query} {onRun} {placeholder} />
<div class="controls">
<label>
Language:
@@ -54,40 +46,42 @@
<span class="detected-badge" class:sql={effectiveLanguage === 'sql'}>
{effectiveLanguage === 'sql' ? 'SQL' : 'pipe syntax'}
</span>
<button onclick={onRun} disabled={loading || query.trim() === ''}>
<Button variant="primary" onclick={onRun} disabled={loading || query.trim() === ''}>
{loading ? 'Running…' : 'Run query'}
</button>
</Button>
<span class="hint">⌘/Ctrl+Enter to run</span>
</div>
</div>
<style>
.query-bar textarea {
width: 100%;
font-family: monospace;
font-size: 0.9rem;
box-sizing: border-box;
}
.controls {
margin-top: 0.5rem;
margin-top: var(--space-3);
display: flex;
align-items: center;
gap: 0.75rem;
gap: var(--space-3);
flex-wrap: wrap;
}
.controls select {
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
height: var(--control-height);
font-family: var(--font-ui);
}
.detected-badge {
font-size: 0.75rem;
padding: 0.15rem 0.5rem;
border-radius: 1rem;
background: #eef;
color: #224;
font-size: var(--text-xs);
padding: 0.15rem var(--space-2);
border-radius: var(--radius-full);
background: var(--color-sev-info-bg);
color: var(--color-sev-info);
}
.detected-badge.sql {
background: #fee;
color: #422;
background: var(--color-sev-warn-bg);
color: var(--color-sev-warn);
}
.hint {
font-size: 0.8rem;
color: #777;
font-size: var(--text-sm);
color: var(--color-text-muted);
}
</style>