Add Grafana-style time range presets: All, 5m, 15m, 1h, 24h (ticket 024)
LogTimeline gets a preset button row (All + four relative windows)
next to the existing drag-select interaction. App.tsx lifts the
active preset alongside `range`; a relative preset re-resolves to a
fresh {startMs, endMs} on the same 2s cadence the app already polls
on, so it visibly rolls forward rather than freezing at selection
time. Dragging on the timeline (or picking a different preset)
overrides back to a fixed range, matching today's drag behavior.
The rolling re-resolution reads/writes `range` via a ref inside the
poll loop instead of depending on `range` directly, since a naive
`setRange` inside an effect keyed on `range` would restart the effect
(and its interval) every tick — turning "poll every 2s" into "poll as
fast as React can re-render." Verified live: exactly 3 /api/stats
calls over ~6.2s with a rolling preset active, and console/network
free of errors.
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
363018793f
commit
b89fda39a0
+57
-6
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { NavMenu } from './components/NavMenu'
|
||||
import { FilterBar } from './components/FilterBar'
|
||||
import { LogTimeline, type TimeRange } from './components/LogTimeline'
|
||||
import { LogTimeline, computePresetRange, type TimeRange, type TimeRangePresetId } from './components/LogTimeline'
|
||||
import { LogPreview } from './components/LogPreview'
|
||||
import { Metrics } from './components/Metrics'
|
||||
import { Dashboards } from './components/Dashboards'
|
||||
@@ -15,18 +15,57 @@ export default function App() {
|
||||
const [logs, setLogs] = useState<LogEntry[]>([])
|
||||
const [buckets, setBuckets] = useState<TimelineBucket[]>([])
|
||||
const [range, setRange] = useState<TimeRange | null>(null)
|
||||
const [activePreset, setActivePreset] = useState<TimeRangePresetId | null>('all')
|
||||
const [filter, setFilter] = useState('')
|
||||
const [filterError, setFilterError] = useState<string | null>(null)
|
||||
|
||||
// Read from inside the poll loop below without making it a dependency —
|
||||
// a rolling preset's own re-resolution (ticket 024) updates `range`, and
|
||||
// depending on `range` directly would restart the effect (and its
|
||||
// interval) every tick, turning "poll every POLL_MS" into "poll as fast as
|
||||
// React can re-render."
|
||||
const rangeRef = useRef(range)
|
||||
useEffect(() => {
|
||||
rangeRef.current = range
|
||||
}, [range])
|
||||
|
||||
function selectPreset(id: TimeRangePresetId) {
|
||||
setActivePreset(id)
|
||||
setRange(computePresetRange(id))
|
||||
}
|
||||
|
||||
// A drag on LogTimeline always yields a fixed (non-rolling) range,
|
||||
// overriding whatever preset was active — same as picking a preset
|
||||
// overrides a previous drag.
|
||||
function handleManualRangeSelect(next: TimeRange | null) {
|
||||
if (next === null) {
|
||||
selectPreset('all')
|
||||
} else {
|
||||
setActivePreset(null)
|
||||
setRange(next)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
async function poll() {
|
||||
// Rolling presets "move with time": re-resolve on this same polling
|
||||
// cadence rather than freezing the window at selection time.
|
||||
const effectiveRange =
|
||||
activePreset && activePreset !== 'all' ? computePresetRange(activePreset) : rangeRef.current
|
||||
if (activePreset && activePreset !== 'all') {
|
||||
setRange((current) =>
|
||||
current && effectiveRange && current.startMs === effectiveRange.startMs && current.endMs === effectiveRange.endMs
|
||||
? current
|
||||
: effectiveRange,
|
||||
)
|
||||
}
|
||||
try {
|
||||
const [nextStats, nextBuckets, nextLogs] = await Promise.all([
|
||||
fetchStats(),
|
||||
fetchTimeline({ startMs: range?.startMs, endMs: range?.endMs, filter: filter || undefined }),
|
||||
fetchLogs({ startMs: range?.startMs, endMs: range?.endMs, limit: 1000, filter: filter || undefined }),
|
||||
fetchTimeline({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, filter: filter || undefined }),
|
||||
fetchLogs({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, limit: 1000, filter: filter || undefined }),
|
||||
])
|
||||
if (!cancelled) {
|
||||
setStats(nextStats)
|
||||
@@ -47,7 +86,13 @@ export default function App() {
|
||||
cancelled = true
|
||||
clearInterval(id)
|
||||
}
|
||||
}, [range, filter])
|
||||
// Intentionally excludes `range`: a manual drag (which does change
|
||||
// `activePreset` to null) still gets an immediate poll; only a second
|
||||
// drag made while already in fixed-range mode waits for the next
|
||||
// interval tick, which is an acceptable trade-off for not restarting
|
||||
// this effect (and its interval) every rolling-preset tick.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [filter, activePreset])
|
||||
|
||||
return (
|
||||
<DocsNavProvider>
|
||||
@@ -55,7 +100,13 @@ export default function App() {
|
||||
<NavMenu stats={stats} />
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<FilterBar value={filter} onChange={setFilter} error={filterError} />
|
||||
<LogTimeline buckets={buckets} range={range} onSelectRange={setRange} />
|
||||
<LogTimeline
|
||||
buckets={buckets}
|
||||
range={range}
|
||||
onSelectRange={handleManualRangeSelect}
|
||||
activePreset={activePreset}
|
||||
onSelectPreset={selectPreset}
|
||||
/>
|
||||
<LogPreview logs={logs} />
|
||||
<Metrics range={range} />
|
||||
<Dashboards range={range} />
|
||||
|
||||
Reference in New Issue
Block a user