import { useEffect, useRef, useState, type ReactNode } from 'react' import { NavMenu } from './components/NavMenu' import { FilterBar } from './components/FilterBar' import { LogTimeline, computeBucketMs, 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 { CollapseToggle, PopOutButton, type PopOutSectionId, useSectionCollapsed } from './components/CollapsibleSection' 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 /// Reads the routing params `PopOutButton` writes: `section` picks which primary section to render full-page, and `startMs`/`endMs` carry over the range for sections with no `LogTimeline` of their own to pick one. function parsePopOutQuery(): { section: PopOutSectionId | null; range: TimeRange | null } { const query = new URLSearchParams(window.location.search) const section = query.get('section') as PopOutSectionId | null const startMs = Number(query.get('startMs')) const endMs = Number(query.get('endMs')) const range = query.has('startMs') && query.has('endMs') && Number.isFinite(startMs) && Number.isFinite(endMs) ? { startMs, endMs } : null return { section, range } } function StandalonePage({ children }: { children: ReactNode }) { return (
{children}
) } /// Popped out on its own, Params has no lifted-state sibling to share `params`/`loadParams` with, so it owns a minimal fetch here instead. function StandaloneParams() { const [params, setParams] = useState([]) async function loadParams() { try { setParams(await fetchParamDeclarations()) } catch { // engine not reachable yet; leave the previous list in place } } useEffect(() => { loadParams() }, []) return ( ) } /// Metrics/Dashboards own their own data fetching — they only need the range carried over via query params. function StandaloneRanged({ section, range }: { section: 'metrics' | 'dashboards'; range: TimeRange | null }) { return {section === 'metrics' ? : } } /// The log viewer has its own `LogTimeline`, so it needs its own copy of App's log/timeline polling state instead of a carried-over range. function StandaloneLog() { const [logs, setLogs] = useState([]) const [params, setParams] = useState([]) const [buckets, setBuckets] = useState([]) const [range, setRange] = useState(null) const [activePreset, setActivePreset] = useState('all') const [filter, setFilter] = useState('') const [filterError, setFilterError] = useState(null) const rangeRef = useRef(range) useEffect(() => { rangeRef.current = range }, [range]) useEffect(() => { fetchParamDeclarations() .then(setParams) .catch(() => { // engine not reachable yet; leave the previous list in place }) }, []) function selectPreset(id: TimeRangePresetId) { setActivePreset(id) setRange(computePresetRange(id)) } function handleManualRangeSelect(next: TimeRange | null) { if (next === null) { selectPreset('all') } else { setActivePreset(null) setRange(next) } } useEffect(() => { let cancelled = false async function poll() { 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 [nextBuckets, nextLogs] = await Promise.all([ fetchTimeline({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, // Only meaningful for a fixed window — 'all' has no span to derive a resolution from, so it keeps the server's default bucketing. bucketMs: effectiveRange ? computeBucketMs(effectiveRange.endMs - effectiveRange.startMs) : undefined, filter: filter || undefined, }), fetchLogs({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, limit: 1000, filter: filter || undefined }), ]) if (!cancelled) { setBuckets(nextBuckets) setLogs(nextLogs) setFilterError(null) } } catch (err) { if (!cancelled) { setFilterError(err instanceof Error ? err.message : String(err)) } } } poll() const id = setInterval(poll, POLL_MS) return () => { cancelled = true clearInterval(id) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [filter, activePreset]) return (
) } export default function App() { const { section, range: standaloneRange } = parsePopOutQuery() if (section === 'log') return if (section === 'params') return if (section === 'metrics' || section === 'dashboards') return return } function FullApp() { 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') const [filter, setFilter] = useState('') const [filterError, setFilterError] = useState(null) const [logViewerCollapsed, setLogViewerCollapsed] = useSectionCollapsed('log-viewer') // Read via ref inside the poll loop instead of depending on `range` directly, which would restart the interval every rolling-preset tick. const rangeRef = useRef(range) useEffect(() => { 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)) } // A drag always yields a fixed (non-rolling) range, overriding any active preset. function handleManualRangeSelect(next: TimeRange | null) { if (next === null) { selectPreset('all') } else { setActivePreset(null) setRange(next) } } useEffect(() => { let cancelled = false async function poll() { // Rolling presets re-resolve on this same polling cadence instead of 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: effectiveRange?.startMs, endMs: effectiveRange?.endMs, // Only meaningful for a fixed window — 'all' has no span to derive a resolution from, so it keeps the server's default bucketing. bucketMs: effectiveRange ? computeBucketMs(effectiveRange.endMs - effectiveRange.startMs) : undefined, filter: filter || undefined, }), fetchLogs({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.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)) } } } poll() const id = setInterval(poll, POLL_MS) return () => { cancelled = true clearInterval(id) } // Intentionally excludes `range` to avoid restarting this interval on every rolling-preset tick. // eslint-disable-next-line react-hooks/exhaustive-deps }, [filter, activePreset]) return (
setLogViewerCollapsed(!logViewerCollapsed)} /> } /> {/* min-h-[...] keeps this from being squeezed to 0 by the sections below it in the shared scroll container — flex-1 alone lets a flex item shrink past its content, which let this section's absolutely-unclipped content spill over Params et al instead of the column just scrolling. */}
) }