import { useEffect, useRef, useState, type ReactNode } from 'react' 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 { 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 standalone-page routing params written by `PopOutButton` /// (ticket 036) — a `section` id selects one of the four primary sections to /// render full-page with no nav/sibling chrome, and `startMs`/`endMs` carry /// over the range that was active at pop-out time for sections that have 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}
) } /// Params has no lifted-state sibling to share `params`/`loadParams` with /// when popped out on its own, so it owns a minimal fetch of its own here /// rather than App's full `loadParams` machinery. 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 already own their own data fetching — they only need /// the range that was active at pop-out time, 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` to pick a range, so it doesn't /// need one carried over — it just needs its own copy of App's log/timeline /// polling state, since none of that is shared with any sibling here. 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, 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 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]) 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 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: effectiveRange?.startMs, endMs: effectiveRange?.endMs, 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`: 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 (
setLogViewerCollapsed(!logViewerCollapsed)} /> } />
) }