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:
co-authored by
Claude Sonnet 5
parent
1014aeaf68
commit
8b2d512c94
+17
-97
@@ -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(),
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user