Extract shared useRangeSelection hook from App.tsx's three page variants

StandaloneRanged, StandaloneLog, and FullApp each reimplemented the same
range/preset state machine (range/activePreset state, a rangeRef synced
via effect, selectPreset/handleManualRangeSelect, and rolling-preset
re-resolution at the top of the poll loop). Move that bookkeeping into
webapp/src/lib/useRangeSelection.ts; each component keeps only its own
polling logic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
This commit is contained in:
Hugh Kaznowski
2026-09-07 10:46:30 +01:00
co-authored by Claude Sonnet 5
parent 1014aeaf68
commit 8b2d512c94
2 changed files with 65 additions and 97 deletions
+17 -97
View File
@@ -8,7 +8,7 @@ import {
saveLogSortSettings,
type LogSortSettings,
} from './components/LogTimeline'
import { TIME_RANGE_PRESETS, computePresetRange, type TimeRange, type TimeRangePresetId } from './lib/timeRange'
import { TIME_RANGE_PRESETS, type TimeRange, type TimeRangePresetId } from './lib/timeRange'
import { LogPreview } from './components/LogPreview'
import { Params } from './components/Params'
import { Metrics } from './components/Metrics'
@@ -26,6 +26,7 @@ import {
type TimelineBucket,
} from './lib/api'
import { DocsNavProvider } from './lib/docsNav'
import { useRangeSelection } from './lib/useRangeSelection'
import { useSettingsPanel } from './lib/useSettingsPanel'
import { POLL_MS } from './lib/polling'
@@ -186,47 +187,18 @@ function StandaloneRanged({
initialFixedRange: TimeRange | null
}) {
const [buckets, setBuckets] = useState<TimelineBucket[]>([])
const [range, setRange] = useState<TimeRange | null>(
initialPresetId ? computePresetRange(initialPresetId) : initialFixedRange,
)
// No preset and no fixed range means the popout was opened with nothing carried over (e.g. a bare URL) — default to 'all', matching FullApp's own default.
const [activePreset, setActivePreset] = useState<TimeRangePresetId | null>(
initialPresetId ?? (initialFixedRange ? null : 'all'),
)
const rangeRef = useRef(range)
useEffect(() => {
rangeRef.current = range
}, [range])
function selectPreset(id: TimeRangePresetId) {
setActivePreset(id)
setRange(computePresetRange(id))
}
function handleManualRangeSelect(next: TimeRange | null) {
if (next === null) {
selectPreset('all')
} else {
setActivePreset(null)
setRange(next)
}
}
const { range, activePreset, selectPreset, handleManualRangeSelect, resolveEffectiveRange } = useRangeSelection({
preset: initialPresetId ?? (initialFixedRange ? null : 'all'),
range: initialFixedRange,
})
useEffect(() => {
let cancelled = false
async function poll() {
// Rolling presets re-resolve on this same polling cadence instead of freezing the window at selection time — this is what makes a carried-over preset independently slide rather than track the source tab.
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,
)
}
const effectiveRange = resolveEffectiveRange()
try {
const nextBuckets = await fetchTimeline({
startMs: effectiveRange?.startMs,
@@ -295,8 +267,10 @@ function StandaloneLog() {
const [logs, setLogs] = useState<LogEntry[]>([])
const [params, setParams] = useState<ParamDeclaration[]>([])
const [buckets, setBuckets] = useState<TimelineBucket[]>([])
const [range, setRange] = useState<TimeRange | null>(null)
const [activePreset, setActivePreset] = useState<TimeRangePresetId | null>('all')
const { range, activePreset, selectPreset, handleManualRangeSelect, resolveEffectiveRange } = useRangeSelection({
preset: 'all',
range: null,
})
const [filter, setFilter] = useState('')
const [filterError, setFilterError] = useState<string | null>(null)
const [sortSettings, setSortSettings] = useState<LogSortSettings>(loadLogSortSettings)
@@ -307,11 +281,6 @@ function StandaloneLog() {
saveLogSortSettings(next)
}
const rangeRef = useRef(range)
useEffect(() => {
rangeRef.current = range
}, [range])
useEffect(() => {
fetchParamDeclarations()
.then(setParams)
@@ -320,33 +289,11 @@ function StandaloneLog() {
})
}, [])
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,
)
}
const effectiveRange = resolveEffectiveRange()
try {
const [nextBuckets, nextLogs, nextParams] = await Promise.all([
fetchTimeline({
@@ -436,8 +383,10 @@ function FullApp() {
const [logs, setLogs] = useState<LogEntry[]>([])
const [params, setParams] = useState<ParamDeclaration[]>([])
const [buckets, setBuckets] = useState<TimelineBucket[]>([])
const [range, setRange] = useState<TimeRange | null>(null)
const [activePreset, setActivePreset] = useState<TimeRangePresetId | null>('all')
const { range, activePreset, selectPreset, handleManualRangeSelect, resolveEffectiveRange } = useRangeSelection({
preset: 'all',
range: null,
})
const [filter, setFilter] = useState('')
const [filterError, setFilterError] = useState<string | null>(null)
const [logViewerCollapsed, setLogViewerCollapsed] = useSectionCollapsed('log-viewer')
@@ -461,12 +410,6 @@ function FullApp() {
})
}
// 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())
@@ -479,35 +422,12 @@ function FullApp() {
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,
)
}
const effectiveRange = resolveEffectiveRange()
try {
const [nextStats, nextBuckets, nextLogs, nextParams] = await Promise.all([
fetchStats(),
+48
View File
@@ -0,0 +1,48 @@
import { useEffect, useRef, useState } from 'react'
import { computePresetRange, type TimeRange, type TimeRangePresetId } from './timeRange'
// `resolveEffectiveRange` is meant to be called once at the top of each variant's own poll
// callback: it re-resolves a rolling preset to "now" on every tick (instead of freezing the
// window at selection time) while reading a manually-selected fixed range via a ref, so the poll
// loop's own `useEffect` doesn't need `range` itself as a dependency and restart its interval on
// every tick.
export function useRangeSelection(initial: { preset: TimeRangePresetId | null; range: TimeRange | null }) {
const [range, setRange] = useState<TimeRange | null>(
initial.preset ? computePresetRange(initial.preset) : initial.range,
)
const [activePreset, setActivePreset] = useState<TimeRangePresetId | null>(initial.preset)
const rangeRef = useRef(range)
useEffect(() => {
rangeRef.current = range
}, [range])
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)
}
}
function resolveEffectiveRange(): TimeRange | null {
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,
)
}
return effectiveRange
}
return { range, activePreset, selectPreset, handleManualRangeSelect, resolveEffectiveRange }
}