Design at autocomplete/autocomplete.md
Rewrites the query editor's autocomplete across every surface that has
one, and
follows the field-value path far enough to fix the bugs it was hiding.
53
commits, in six phases; each phase was written test-first, reviewed
adversarially, and checked in a browser before the next one started.
## What was wrong
The completion providers were wired inconsistently. Six surfaces bound a
resolver that was never defined, so their value completion could not
resolve at
all — SLO, dashboards and anomaly detection among them. Traces never
bound
`:suggestions`. Alerts never called `updateStreamKeywords`. PromQL ran a
second
editor runtime purely for its grammar, and asked
`/prometheus/api/v1/series`
— the heaviest endpoint available — for data the stream schema already
has.
## Phases
**1 — icons, staleness, quoting.** Correct `CompletionItemKind` glyphs,
stream
keywords refreshed when the stream changes, `spath` quoting.
**2 — the function catalog.** Custom functions get one tab stop per
argument,
org VRL functions stop appearing twice, column types read from
`dataType` as
well as `type`.
**3 — signature help, hover, async completion.** Providers return real
`CompletionList`s with `incomplete: true`, so a list that arrives late
is
re-queried rather than pinned.
**4 — numeric ranking.** Numeric columns sort first inside a numeric
aggregate.
**5 — values for streams nobody has searched.** The value cache only
ever held
what a search had already returned, so a stream you had not queried
offered
nothing. Values are now fetched on demand, bounded (10s timeout, 60s
cooldown,
one in-flight request per key), and shared between the editor and the
builder.
PromQL joins the same path: labels from the stream schema, values from
the
shared cache. `/series` now has zero production callers.
**6 — one way to fetch a value.** Item 22 only, in this PR.
## The dashboard filter 400
Clicking the ✕ beside "Select Field" in a filter's dropdown sets the
column to
`{}`, and the watcher on `condition.column` then asks for that column's
values.
`loadFilterItem` built `fields: [row.field]` out of `undefined`,
JSON.stringify
turned that into `null`, and the server answered
```
400 {"code":400,"message":"SerdeJsonError# invalid type: null, expected a string"}
```
`_values_stream` answers 400, not an empty result, for each way a piece
of the
payload can go missing — and every one is reachable: an unresolved
stream alias
(`.find(…)?.stream`), a stream not yet chosen (`""`), a range not yet
set. The
range fails differently: `meta.dateTime` starts as `{start_time: ""}`
and
`""?.toISOString()` does not short-circuit, so it throws and both
callers
surface "Something went wrong!" for a lookup the user never asked to
fail.
Guarded at the two callers, where the payload is built and where the
throw
happens. Not at `fetchFieldValues` — a guard there is too late to stop
the
throw and would duplicate validation the callers need anyway. The
trigger is
left alone: re-picking a field must still load its values.
## Notable removals
CodeMirror is gone — it was still bundled for the PromQL grammar alone.
Bundle 26.3 MB → 25.43 MB; the PromQL chunks 442.7 KB → 20.6 KB. The
PromQL
term catalog went from 7 entries to 113, generated from Prometheus's own
tables
and vendored with its MIT notice.
## Verification
- 76 files, +10.5k/−1.4k, ~9,100 tests passing across the touched areas.
- `vue-tsc` clean, `npm run build` exit 0.
- Every phase checked in a running browser against a live instance,
including
the 400 above: the ✕ now produces no request and no toast, adding a
filter and
re-picking a field each still fetch once, and the value list still
populates.
## Known, deliberately out of scope
- A filter still fetches twice (once on add, once on open) — that is
item 23,
the cache read, not this PR.
- `useMetricsExplorer.loadFilterItem` has the same unguarded
`toISOString()`
pattern on a different surface.
- `services/metrics.ts` and `services/search.ts` still carry dead
`get_promql_series` wrappers now that nothing calls `/series`.
## What
Adds a contextual **help side panel** to the alert **Additional
Settings** tab. Each control (Template Override, Additional Variables,
Row Template) gets a **"Learn more"** link that opens a right-side
`ODrawer` explaining the control and showing a live, honest preview of
the notification message.
## Why
The old one-line tooltips didn't tell users what was actually changing —
e.g. Template Override just listed templates without showing the current
vs. selected message. This panel teaches first-time users *what* each
control does, *why* they'd use it, and shows *what gets sent*.
## Highlights
- **`useTemplatePreview` composable** — display-only `{variable}`
substitution: live form values shown as "your data", mock runtime values
marked as samples, and context-sensitive tokens (`{rows}`, `{rows:N}`,
`{var:N}`, `{...rows}`) rendered as opaque "filled at notification time"
chips — never faked against the real server renderer.
- **Template Override** — shows each destination's current message vs. a
non-destructive template preview (Apply required to commit).
- **Additional Variables** — plain-language explanation, a before/after
"why use a variable", a worked example with rendered result, and a
collapsible built-in-variable reference (not an 18-chip wall). Removed a
phantom variable (`{alert_agg_value}`) the UI advertised but the backend
never substitutes; added the real `{alert_description}`.
- **Row Template** — explains how `row_template` + `{rows}` compose,
with a worked example.
- **Reordered** the Advanced tab: Additional Settings → Compare with
Past → Deduplication.
- **Editor scroll fix** — Monaco editors now release the mouse wheel to
the page once their own content has nothing left to scroll (was trapping
page scroll on hover). App-wide default via an opt-out prop on the
shared `CodeQueryEditor`.
## Implementation notes
- Styling uses the `.css` design-token layer (`--color-*`,
`--radius-*`), theme-aware (light/dark); reuses
`OBadge`/`ODrawer`/`OSelect` rather than hand-rolling. No `px` (except
1/2px borders), no inline styles, no hardcoded colors.
- Every section has an empty state — no blank preview boxes.
- i18n `{...}` braces escaped for vue-i18n so literal placeholders
render.
## Tests
- Unit: `useTemplatePreview` (token classification, live/mock/opaque,
context vars, stream fields stay opaque).
- Component: `AlertSettingsHelpDrawer` (per-topic content, current vs.
preview, collapsible built-ins, empty states, i18n-literal & phantom-var
regression guards).
- Wiring: `Advanced.spec.ts` (Learn-more triggers); editor:
`CodeQueryEditor.spec.ts` (scroll prop).
- E2E (Playwright): `alerts-help-drawer.spec.js` smoke — open drawer,
no-mutation-until-Apply, overlay close, collapsible built-ins. (Not
executed in this environment — no backend/creds.)
228 unit/component tests pass locally.
## Notes for reviewer
- Draft: please review the description and the UX copy.
- The Monaco scroll default change touches the shared `CodeQueryEditor`
(used app-wide); behavior is opt-out via
`:release-wheel-to-page="false"` if any full-page editor should own the
wheel.
## What changed
Replaced `VueJsonPretty` (`vue-json-pretty`) with `CodeQueryEditor.vue`
(Monaco-based) for JSON rendering in `LLMContentRenderer.vue`. Inlined
the `MessageItem` sub-component directly into the template with
`CodeQueryEditor` for JSON message content. Added `showLineNumbers` and
`stickyScroll` props to `CodeQueryEditor`. Removed the unused
`vue-json-pretty` dependency from `package.json`. Passed unique editor
IDs from `TraceDetailsSidebar` via a new `instanceId` prop to prevent
Monaco editor ID collisions.
## Why
`vue-json-pretty` formats the entire JSON tree on initial render,
causing lag on long JSON payloads. Monaco (via `CodeQueryEditor`)
renders only visible viewport lines, making performance O(visible)
instead of O(total).
## Approach
- Replaced all 7 `VueJsonPretty` instances in `LLMContentRenderer.vue`
with `<CodeQueryEditor>` in read-only mode (`:read-only="true"`,
`:show-line-numbers="false"`, `:sticky-scroll="false"`)
- Inlined the `MessageItem` sub-component as a `v-for` block with
`CodeQueryEditor` for JSON message content and the existing `v-html`
markdown renderer for non-JSON content
- Converted the component to `<script setup lang="ts">` with
`defineAsyncComponent` for `CodeQueryEditor`
- Added `showLineNumbers` and `stickyScroll` props (both default `true`)
to `CodeQueryEditor.vue` to support the JSON viewer use case
- Added a `watch` on `showLineNumbers` to dynamically toggle line
numbers in Monaco
- Passed `instanceId` from `TraceDetailsSidebar` down to
`LLMContentRenderer` to generate unique editor IDs like
`trace-id-msg-json-editor-0`
## Considered & rejected
- **Render function approach with synchronous import of
CodeQueryEditor** — rejected because it forced manual editor height
computation and module-level counters for editor IDs. Inlining
`MessageItem` as a template component was cleaner and consistent with
the other 5 template `CodeQueryEditor` locations.
- **Retaining `MessageItem` as a separate component with render
function** — rejected because the render function approach was
over-engineering for what is now a simple `v-for` with conditional
`CodeQueryEditor` / markdown branching.
## Risk
Medium. Monaco editor initialization has non-trivial overhead; rendering
multiple editors on one page (e.g., many messages in a conversation)
could impact initial render time. Reviewers should check that the
`instanceId` prop is correctly wired from `TraceDetailsSidebar` to avoid
duplicate Monaco editor IDs, and that the `tw:h-full` class application
on content-wrapper divs doesn't break non-JSON (plain text / markdown)
layouts.