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:
+55
-13
@@ -8,7 +8,10 @@
|
||||
|
||||
import ResultsTable from '$lib/ResultsTable.svelte';
|
||||
import QueryBar from '$lib/QueryBar.svelte';
|
||||
import { runQuery as apiRunQuery, type Language } from '$lib/api';
|
||||
import AddToDashboardModal from '$lib/components/AddToDashboardModal.svelte';
|
||||
import { Button } from '$lib/components/ui';
|
||||
import { runQuery as apiRunQuery, injectTimeRange, type Language } from '$lib/api';
|
||||
import { page } from '$app/state';
|
||||
|
||||
type HistoryEntry = { query: string; language: Language; at: number };
|
||||
|
||||
@@ -66,6 +69,25 @@
|
||||
hasRun = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Drill-down landing: a chart's "click to drill into query"
|
||||
// (see $lib/charts/drilldown.ts) navigates here with ?q=&earliest=&latest=.
|
||||
// Runs once per navigation, not on every reactive change, so editing
|
||||
// the query bar afterwards doesn't keep re-injecting the original
|
||||
// drill-down range.
|
||||
let consumedDrillDownParams = false;
|
||||
$effect(() => {
|
||||
const params = page.url.searchParams;
|
||||
const q = params.get('q');
|
||||
if (!q || consumedDrillDownParams) return;
|
||||
consumedDrillDownParams = true;
|
||||
const earliest = params.get('earliest');
|
||||
const latest = params.get('latest');
|
||||
query = earliest ? injectTimeRange(q, earliest, latest ?? 'now') : q;
|
||||
runQuery();
|
||||
});
|
||||
|
||||
let addToDashboardOpen = $state(false);
|
||||
</script>
|
||||
|
||||
<main>
|
||||
@@ -82,8 +104,18 @@
|
||||
<p class="error">Error: {error}</p>
|
||||
{/if}
|
||||
|
||||
{#if hasRun && !error && columns.length > 0}
|
||||
<div class="results-actions">
|
||||
<Button size="sm" variant="secondary" onclick={() => (addToDashboardOpen = true)}>
|
||||
+ Add as panel to dashboard
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ResultsTable {columns} {rows} {hasRun} />
|
||||
|
||||
<AddToDashboardModal bind:open={addToDashboardOpen} {query} {language} />
|
||||
|
||||
{#if history.length > 0}
|
||||
<details class="history">
|
||||
<summary>Query history ({history.length})</summary>
|
||||
@@ -120,40 +152,50 @@
|
||||
|
||||
<style>
|
||||
main {
|
||||
font-family: system-ui, sans-serif;
|
||||
max-width: 960px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
max-width: 64rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: var(--text-xl);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
p {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.error {
|
||||
color: #b00020;
|
||||
color: var(--color-danger);
|
||||
}
|
||||
.results-actions {
|
||||
margin: var(--space-3) 0;
|
||||
}
|
||||
.history ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0 0;
|
||||
margin: var(--space-2) 0 0;
|
||||
}
|
||||
.history-item {
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
padding: 0.25rem 0;
|
||||
padding: var(--space-1) 0;
|
||||
cursor: pointer;
|
||||
color: #06c;
|
||||
color: var(--color-accent);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.history-item:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.cheatsheet {
|
||||
margin-top: 1.5rem;
|
||||
font-size: 0.85rem;
|
||||
margin-top: var(--space-5);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.cheatsheet table {
|
||||
border-collapse: collapse;
|
||||
margin-top: 0.5rem;
|
||||
margin-top: var(--space-2);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.cheatsheet td {
|
||||
padding: 0.2rem 0.75rem 0.2rem 0;
|
||||
padding: var(--space-1) var(--space-4) var(--space-1) 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user