From dd1a0a234de963ac7c48f6c4c83d5e349398f4d6 Mon Sep 17 00:00:00 2001
From: Hugh Kaznowski
Date: Sat, 5 Sep 2026 16:43:53 +0100
Subject: [PATCH] Add Params declaration UI (ticket 034)
New Params panel (Log viewer / Params / Metrics / Dashboards) declares a
bare param via POST /api/params, mirroring Metrics' declare-then-list
pattern including inline 400 error surfacing. Lifted the params list from
LogPreview's own effect into App.tsx so declaring a param there updates
the Columns picker immediately, without a page reload.
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
---
tickets/034-params-declaration-ui.md | 41 ----------
webapp/src/App.tsx | 28 ++++++-
webapp/src/components/Docs.tsx | 6 +-
webapp/src/components/LogPreview.tsx | 25 +-----
webapp/src/components/Params.tsx | 111 +++++++++++++++++++++++++++
webapp/src/lib/api.ts | 4 +
6 files changed, 147 insertions(+), 68 deletions(-)
delete mode 100644 tickets/034-params-declaration-ui.md
create mode 100644 webapp/src/components/Params.tsx
diff --git a/tickets/034-params-declaration-ui.md b/tickets/034-params-declaration-ui.md
deleted file mode 100644
index 5be790b..0000000
--- a/tickets/034-params-declaration-ui.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# 034 — Params declaration UI
-
-## Summary
-
-`POST /api/params` (ticket 004) already lets a param be declared — name,
-`message_match_regex`, `capture_group`, `kind` — and `GET /api/params`
-(`fetchParams`, `webapp/src/lib/api.ts:245`) already feeds LogPreview's
-"Columns" picker (`webapp/src/components/LogPreview.tsx:63`) so a declared
-param can be shown as a log table column. But nothing in the webapp can
-*create* one — the only way to declare a param today is a raw `POST
-/api/params` call. Add a UI for it, following the same declare-then-list
-pattern `Metrics` already uses for `POST /api/metrics`
-(`webapp/src/components/Metrics.tsx`).
-
-## Scope
-
-- A "Params" section in the primary UI (alongside Log viewer / Metrics /
- Dashboards in `App.tsx`), listing currently-declared params (name, regex,
- capture group, kind) via `fetchParams`.
-- A declare form: name, `message_match_regex`, `capture_group`, kind
- (string/number/timestamp — mirror `MetricPrimitiveKind`), submitting via a
- new `declareParam` call to `POST /api/params`.
-- Surface `400` validation errors from the backend (bad regex, unknown
- capture group) inline on the form, matching how `Metrics`' declare form
- reports its own validation errors.
-- After a successful declare, refresh the params list so it (and
- LogPreview's Columns picker) immediately reflects the new param without a
- page reload.
-- Contextual "?" help icon on the section, matching [021](021-docs-contextual-help.md)'s pattern.
-
-## Acceptance criteria
-
-- A param can be declared from the UI and immediately appears in LogPreview's
- Columns picker without a page reload.
-- An invalid regex or unknown capture group shows the backend's error message
- on the form instead of failing silently.
-- The declared-params list matches `GET /api/params` after a page reload.
-
-## Depends on
-
-- [021 — Docs contextual help](021-docs-contextual-help.md)
diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx
index ddbc43c..98e14b9 100644
--- a/webapp/src/App.tsx
+++ b/webapp/src/App.tsx
@@ -3,10 +3,20 @@ import { NavMenu } from './components/NavMenu'
import { FilterBar } from './components/FilterBar'
import { LogTimeline, computePresetRange, type TimeRange, type TimeRangePresetId } from './components/LogTimeline'
import { LogPreview } from './components/LogPreview'
+import { Params } from './components/Params'
import { Metrics } from './components/Metrics'
import { Dashboards } from './components/Dashboards'
import { Alerts } from './components/Alerts'
-import { fetchLogs, fetchStats, fetchTimeline, type LogEntry, type Stats, type TimelineBucket } from './lib/api'
+import {
+ fetchLogs,
+ fetchParamDeclarations,
+ fetchStats,
+ fetchTimeline,
+ type LogEntry,
+ type ParamDeclaration,
+ type Stats,
+ type TimelineBucket,
+} from './lib/api'
import { DocsNavProvider } from './lib/docsNav'
const POLL_MS = 2000
@@ -14,6 +24,7 @@ const POLL_MS = 2000
export default function App() {
const [stats, setStats] = useState(null)
const [logs, setLogs] = useState([])
+ const [params, setParams] = useState([])
const [buckets, setBuckets] = useState([])
const [range, setRange] = useState(null)
const [activePreset, setActivePreset] = useState('all')
@@ -30,6 +41,18 @@ export default function App() {
rangeRef.current = range
}, [range])
+ async function loadParams() {
+ try {
+ setParams(await fetchParamDeclarations())
+ } catch {
+ // engine not reachable yet; leave the previous list in place
+ }
+ }
+
+ useEffect(() => {
+ loadParams()
+ }, [])
+
function selectPreset(id: TimeRangePresetId) {
setActivePreset(id)
setRange(computePresetRange(id))
@@ -108,7 +131,8 @@ export default function App() {
activePreset={activePreset}
onSelectPreset={selectPreset}
/>
-
+
+
diff --git a/webapp/src/components/Docs.tsx b/webapp/src/components/Docs.tsx
index 870367c..54c36cf 100644
--- a/webapp/src/components/Docs.tsx
+++ b/webapp/src/components/Docs.tsx
@@ -116,9 +116,9 @@ const SECTIONS: DocSection[] = [
the log table and as a comparable value in the filter bar.
- Declaring a param independent of a metric is supported by the API (POST /api/params), used
- today via the Metrics panel's declaration form and read back by the log preview's column picker; there
- isn't yet a standalone "declare a bare param" form in the UI.
+ The Params panel declares a bare param directly (POST /api/params) — the same request the
+ Metrics panel's declaration form makes per param matcher. Declaring one there immediately updates the log
+ preview's column picker, with no page reload needed.
),
diff --git a/webapp/src/components/LogPreview.tsx b/webapp/src/components/LogPreview.tsx
index 1948f80..5178b47 100644
--- a/webapp/src/components/LogPreview.tsx
+++ b/webapp/src/components/LogPreview.tsx
@@ -1,9 +1,5 @@
import { useEffect, useRef, useState } from 'react'
-import {
- fetchParamDeclarations,
- metricPrimitiveToDisplayString,
- type LogEntry,
-} from '../lib/api'
+import { metricPrimitiveToDisplayString, type LogEntry, type ParamDeclaration } from '../lib/api'
import { HelpIcon } from './HelpIcon'
/// localStorage key for LogPreview's selected param columns. Namespaced
@@ -89,25 +85,10 @@ function ColumnPicker({
)
}
-export function LogPreview({ logs }: { logs: LogEntry[] }) {
- const [paramNames, setParamNames] = useState([])
+export function LogPreview({ logs, params }: { logs: LogEntry[]; params: ParamDeclaration[] }) {
+ const paramNames = params.map((d) => d.name)
const [visibleParamColumns, setVisibleParamColumns] = useState(loadVisibleParamColumns)
- useEffect(() => {
- let cancelled = false
- fetchParamDeclarations()
- .then((declarations) => {
- if (!cancelled) setParamNames(declarations.map((d) => d.name))
- })
- .catch(() => {
- // params API not reachable yet; column picker just stays empty until
- // the next successful fetch.
- })
- return () => {
- cancelled = true
- }
- }, [])
-
const visible = new Set(visibleParamColumns)
function toggleColumn(name: string) {
diff --git a/webapp/src/components/Params.tsx b/webapp/src/components/Params.tsx
new file mode 100644
index 0000000..af48a2c
--- /dev/null
+++ b/webapp/src/components/Params.tsx
@@ -0,0 +1,111 @@
+import { useState } from 'react'
+import { ApiError, declareParam, type ParamDeclaration } from '../lib/api'
+import { HelpIcon } from './HelpIcon'
+
+const EMPTY_KIND: ParamDeclaration['kind'] = 'string'
+
+export function Params({ params, onDeclared }: { params: ParamDeclaration[]; onDeclared: () => void }) {
+ const [name, setName] = useState('')
+ const [regex, setRegex] = useState('')
+ const [captureGroup, setCaptureGroup] = useState('')
+ const [kind, setKind] = useState(EMPTY_KIND)
+ const [submitError, setSubmitError] = useState(null)
+ const [submitting, setSubmitting] = useState(false)
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault()
+ setSubmitError(null)
+ setSubmitting(true)
+ try {
+ await declareParam({ name, message_match_regex: regex, capture_group: captureGroup, kind })
+ setName('')
+ setRegex('')
+ setCaptureGroup('')
+ setKind(EMPTY_KIND)
+ onDeclared()
+ } catch (err) {
+ setSubmitError(err instanceof ApiError ? err.message : 'Failed to declare param')
+ } finally {
+ setSubmitting(false)
+ }
+ }
+
+ return (
+
+
+ Params
+
+
+
+
+
+
+
Declared params
+
+ {params.length === 0 && No params declared yet.}
+ {params.map((p) => (
+
+ {p.name} ({p.kind}, group {p.capture_group})
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/webapp/src/lib/api.ts b/webapp/src/lib/api.ts
index 3e8020b..de1f172 100644
--- a/webapp/src/lib/api.ts
+++ b/webapp/src/lib/api.ts
@@ -273,6 +273,10 @@ export function fetchParamDeclarations(): Promise {
return getJson('/api/params')
}
+export function declareParam(request: ParamDeclaration): Promise {
+ return postJson('/api/params', request)
+}
+
export function fetchMetricDeclarations(): Promise {
return getJson('/api/metrics')
}