Wire FilterBar to server-side filter query language (ticket 016)
Send the raw filter string to GET /api/logs and GET /api/timeline instead of doing a naive client-side substring match, surface the API's 400 parse error inline under the FilterBar input, and drop the now-dead visibleLogs client-side filtering in App.tsx. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
57d23c62e9
commit
10753cc9f8
+11
-9
@@ -14,6 +14,7 @@ export default function App() {
|
|||||||
const [buckets, setBuckets] = useState<TimelineBucket[]>([])
|
const [buckets, setBuckets] = useState<TimelineBucket[]>([])
|
||||||
const [range, setRange] = useState<TimeRange | null>(null)
|
const [range, setRange] = useState<TimeRange | null>(null)
|
||||||
const [filter, setFilter] = useState('')
|
const [filter, setFilter] = useState('')
|
||||||
|
const [filterError, setFilterError] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
@@ -22,16 +23,19 @@ export default function App() {
|
|||||||
try {
|
try {
|
||||||
const [nextStats, nextBuckets, nextLogs] = await Promise.all([
|
const [nextStats, nextBuckets, nextLogs] = await Promise.all([
|
||||||
fetchStats(),
|
fetchStats(),
|
||||||
fetchTimeline({ startMs: range?.startMs, endMs: range?.endMs }),
|
fetchTimeline({ startMs: range?.startMs, endMs: range?.endMs, filter: filter || undefined }),
|
||||||
fetchLogs({ startMs: range?.startMs, endMs: range?.endMs, limit: 1000 }),
|
fetchLogs({ startMs: range?.startMs, endMs: range?.endMs, limit: 1000, filter: filter || undefined }),
|
||||||
])
|
])
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setStats(nextStats)
|
setStats(nextStats)
|
||||||
setBuckets(nextBuckets)
|
setBuckets(nextBuckets)
|
||||||
setLogs(nextLogs)
|
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
|
cancelled = true
|
||||||
clearInterval(id)
|
clearInterval(id)
|
||||||
}
|
}
|
||||||
}, [range])
|
}, [range, filter])
|
||||||
|
|
||||||
const visibleLogs = filter ? logs.filter((l) => l.text.includes(filter)) : logs
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen overflow-hidden">
|
<div className="flex h-screen w-screen overflow-hidden">
|
||||||
<NavMenu stats={stats} />
|
<NavMenu stats={stats} />
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
<FilterBar value={filter} onChange={setFilter} />
|
<FilterBar value={filter} onChange={setFilter} error={filterError} />
|
||||||
<LogTimeline buckets={buckets} range={range} onSelectRange={setRange} />
|
<LogTimeline buckets={buckets} range={range} onSelectRange={setRange} />
|
||||||
<LogPreview logs={visibleLogs} />
|
<LogPreview logs={logs} />
|
||||||
<ComingSoon title="Metrics" />
|
<ComingSoon title="Metrics" />
|
||||||
<ComingSoon title="Dashboards" />
|
<ComingSoon title="Dashboards" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,32 +1,46 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Placeholder for the syntax-aware pest-parsed filter language described in the
|
* Input for the syntax-aware pest-parsed filter language described in the
|
||||||
* README. For now this only does a plain substring match, applied client-side.
|
* 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)
|
const [draft, setDraft] = useState(value)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="flex items-center gap-2 border-b border-neutral-800 px-3 py-2"
|
className="flex flex-col gap-1 border-b border-neutral-800 px-3 py-2"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
onChange(draft)
|
onChange(draft)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<div className="flex items-center gap-2">
|
||||||
className="flex-1 rounded border border-neutral-700 bg-neutral-900 px-2 py-1 text-sm text-neutral-100 placeholder:text-neutral-500 focus:border-neutral-500 focus:outline-none"
|
<input
|
||||||
placeholder='regex("error") and sequence<5 or timestamp>12:00'
|
className="flex-1 rounded border border-neutral-700 bg-neutral-900 px-2 py-1 text-sm text-neutral-100 placeholder:text-neutral-500 focus:border-neutral-500 focus:outline-none"
|
||||||
value={draft}
|
placeholder='regex("error") and sequence<5 or timestamp>12:00'
|
||||||
onChange={(e) => setDraft(e.target.value)}
|
value={draft}
|
||||||
/>
|
onChange={(e) => setDraft(e.target.value)}
|
||||||
<button
|
/>
|
||||||
type="submit"
|
<button
|
||||||
className="rounded border border-neutral-700 px-3 py-1 text-sm text-neutral-300 hover:bg-neutral-800"
|
type="submit"
|
||||||
>
|
className="rounded border border-neutral-700 px-3 py-1 text-sm text-neutral-300 hover:bg-neutral-800"
|
||||||
Filter
|
>
|
||||||
</button>
|
Filter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{error && <p className="text-xs text-red-500">{error}</p>}
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -46,7 +46,16 @@ export interface Stats {
|
|||||||
async function getJson<T>(path: string): Promise<T> {
|
async function getJson<T>(path: string): Promise<T> {
|
||||||
const res = await fetch(path)
|
const res = await fetch(path)
|
||||||
if (!res.ok) {
|
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<T>
|
return res.json() as Promise<T>
|
||||||
}
|
}
|
||||||
@@ -56,12 +65,14 @@ export function fetchLogs(params: {
|
|||||||
startMs?: number
|
startMs?: number
|
||||||
endMs?: number
|
endMs?: number
|
||||||
limit?: number
|
limit?: number
|
||||||
|
filter?: string
|
||||||
}): Promise<LogEntry[]> {
|
}): Promise<LogEntry[]> {
|
||||||
const search = new URLSearchParams()
|
const search = new URLSearchParams()
|
||||||
if (params.sinceSequence !== undefined) search.set('since_sequence', String(params.sinceSequence))
|
if (params.sinceSequence !== undefined) search.set('since_sequence', String(params.sinceSequence))
|
||||||
if (params.startMs !== undefined) search.set('start_ms', String(params.startMs))
|
if (params.startMs !== undefined) search.set('start_ms', String(params.startMs))
|
||||||
if (params.endMs !== undefined) search.set('end_ms', String(params.endMs))
|
if (params.endMs !== undefined) search.set('end_ms', String(params.endMs))
|
||||||
if (params.limit !== undefined) search.set('limit', String(params.limit))
|
if (params.limit !== undefined) search.set('limit', String(params.limit))
|
||||||
|
if (params.filter) search.set('filter', params.filter)
|
||||||
return getJson(`/api/logs?${search.toString()}`)
|
return getJson(`/api/logs?${search.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,11 +80,13 @@ export function fetchTimeline(params: {
|
|||||||
startMs?: number
|
startMs?: number
|
||||||
endMs?: number
|
endMs?: number
|
||||||
bucketMs?: number
|
bucketMs?: number
|
||||||
|
filter?: string
|
||||||
}): Promise<TimelineBucket[]> {
|
}): Promise<TimelineBucket[]> {
|
||||||
const search = new URLSearchParams()
|
const search = new URLSearchParams()
|
||||||
if (params.startMs !== undefined) search.set('start_ms', String(params.startMs))
|
if (params.startMs !== undefined) search.set('start_ms', String(params.startMs))
|
||||||
if (params.endMs !== undefined) search.set('end_ms', String(params.endMs))
|
if (params.endMs !== undefined) search.set('end_ms', String(params.endMs))
|
||||||
if (params.bucketMs !== undefined) search.set('bucket_ms', String(params.bucketMs))
|
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()}`)
|
return getJson(`/api/timeline?${search.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user