diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index d7583b4..a6e019c 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -14,6 +14,7 @@ export default function App() { const [buckets, setBuckets] = useState([]) const [range, setRange] = useState(null) const [filter, setFilter] = useState('') + const [filterError, setFilterError] = useState(null) useEffect(() => { let cancelled = false @@ -22,16 +23,19 @@ export default function App() { try { const [nextStats, nextBuckets, nextLogs] = await Promise.all([ fetchStats(), - fetchTimeline({ startMs: range?.startMs, endMs: range?.endMs }), - fetchLogs({ startMs: range?.startMs, endMs: range?.endMs, limit: 1000 }), + fetchTimeline({ startMs: range?.startMs, endMs: range?.endMs, filter: filter || undefined }), + fetchLogs({ startMs: range?.startMs, endMs: range?.endMs, limit: 1000, filter: filter || undefined }), ]) if (!cancelled) { setStats(nextStats) setBuckets(nextBuckets) setLogs(nextLogs) + setFilterError(null) + } + } catch (err) { + if (!cancelled) { + setFilterError(err instanceof Error ? err.message : String(err)) } - } catch { - // engine not reachable yet; retry on next tick } } @@ -41,17 +45,15 @@ export default function App() { cancelled = true clearInterval(id) } - }, [range]) - - const visibleLogs = filter ? logs.filter((l) => l.text.includes(filter)) : logs + }, [range, filter]) return (
- + - +
diff --git a/webapp/src/components/FilterBar.tsx b/webapp/src/components/FilterBar.tsx index 920a403..43c266d 100644 --- a/webapp/src/components/FilterBar.tsx +++ b/webapp/src/components/FilterBar.tsx @@ -1,32 +1,46 @@ import { useState } from 'react' /** - * Placeholder for the syntax-aware pest-parsed filter language described in the - * README. For now this only does a plain substring match, applied client-side. + * Input for the syntax-aware pest-parsed filter language described in the + * README (regex + metric comparisons combined with `and`/`or`). The raw + * filter string is sent to the server (`GET /api/logs` / `GET /api/timeline` + * `filter` param) and evaluated there; this component just manages the + * draft text and surfaces any parse error the server reports back. */ -export function FilterBar({ value, onChange }: { value: string; onChange: (value: string) => void }) { +export function FilterBar({ + value, + onChange, + error, +}: { + value: string + onChange: (value: string) => void + error?: string | null +}) { const [draft, setDraft] = useState(value) return (
{ e.preventDefault() onChange(draft) }} > - setDraft(e.target.value)} - /> - +
+ setDraft(e.target.value)} + /> + +
+ {error &&

{error}

}
) } diff --git a/webapp/src/lib/api.ts b/webapp/src/lib/api.ts index bc44d95..eddaf5f 100644 --- a/webapp/src/lib/api.ts +++ b/webapp/src/lib/api.ts @@ -46,7 +46,16 @@ export interface Stats { async function getJson(path: string): Promise { const res = await fetch(path) if (!res.ok) { - throw new Error(`${path} failed: ${res.status}`) + let message = `${path} failed: ${res.status}` + try { + const body: unknown = await res.json() + if (body && typeof body === 'object' && 'error' in body && typeof (body as { error: unknown }).error === 'string') { + message = (body as { error: string }).error + } + } catch { + // body wasn't JSON (or had no `error` field) — fall back to the generic message + } + throw new Error(message) } return res.json() as Promise } @@ -56,12 +65,14 @@ export function fetchLogs(params: { startMs?: number endMs?: number limit?: number + filter?: string }): Promise { const search = new URLSearchParams() if (params.sinceSequence !== undefined) search.set('since_sequence', String(params.sinceSequence)) if (params.startMs !== undefined) search.set('start_ms', String(params.startMs)) if (params.endMs !== undefined) search.set('end_ms', String(params.endMs)) if (params.limit !== undefined) search.set('limit', String(params.limit)) + if (params.filter) search.set('filter', params.filter) return getJson(`/api/logs?${search.toString()}`) } @@ -69,11 +80,13 @@ export function fetchTimeline(params: { startMs?: number endMs?: number bucketMs?: number + filter?: string }): Promise { const search = new URLSearchParams() if (params.startMs !== undefined) search.set('start_ms', String(params.startMs)) if (params.endMs !== undefined) search.set('end_ms', String(params.endMs)) if (params.bucketMs !== undefined) search.set('bucket_ms', String(params.bucketMs)) + if (params.filter) search.set('filter', params.filter) return getJson(`/api/timeline?${search.toString()}`) }