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 + +
+
+
+
+ + setName(e.target.value)} + placeholder="path" + required + /> +
+
+ + setRegex(e.target.value)} + placeholder={String.raw`request path=(?P\S+)`} + required + /> +
+
+
+ + setCaptureGroup(e.target.value)} + placeholder="path" + required + /> +
+ +
+ {submitError &&
{submitError}
} + +
+ +
+
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') }