Anchor log viewer scroll to visible content and add 1m/2m timeline presets
The log table no longer jumps when a sliding window prunes rows from the front: scroll position now tracks the same visible row instead of holding scrollTop constant in pixels. Also adds 1m/2m to the timeline quick-range presets. 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
070e835fa4
commit
ae4e550c1f
@@ -82,9 +82,51 @@ function ColumnPicker({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keeps the scroll position anchored to the same visible row across a `logs` update (e.g. a sliding time window
|
||||||
|
/// pruning old rows from the front), instead of the browser's default of holding `scrollTop` constant in pixels —
|
||||||
|
/// which would otherwise scroll the view to different content whenever rows are removed above it.
|
||||||
|
function useScrollAnchor(containerRef: React.RefObject<HTMLDivElement | null>, deps: unknown[]) {
|
||||||
|
const pendingAnchor = useRef<{ key: string; offsetTop: number } | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
if (pendingAnchor.current) {
|
||||||
|
const { key, offsetTop } = pendingAnchor.current
|
||||||
|
const row = container.querySelector(`[data-row-key="${CSS.escape(key)}"]`)
|
||||||
|
if (row) {
|
||||||
|
const rowTop = row.getBoundingClientRect().top - container.getBoundingClientRect().top
|
||||||
|
container.scrollTop += rowTop - offsetTop
|
||||||
|
}
|
||||||
|
pendingAnchor.current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs right before the DOM mutates for the next update (React calls a layout effect's cleanup
|
||||||
|
// synchronously before re-running it), so this still sees the pre-update DOM — the same idea as
|
||||||
|
// `getSnapshotBeforeUpdate`, expressed with a plain effect.
|
||||||
|
return () => {
|
||||||
|
const containerRect = container.getBoundingClientRect()
|
||||||
|
for (const row of container.querySelectorAll('[data-row-key]')) {
|
||||||
|
const rect = row.getBoundingClientRect()
|
||||||
|
if (rect.bottom > containerRect.top) {
|
||||||
|
pendingAnchor.current = {
|
||||||
|
key: row.getAttribute('data-row-key')!,
|
||||||
|
offsetTop: rect.top - containerRect.top,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, deps)
|
||||||
|
}
|
||||||
|
|
||||||
export function LogPreview({ logs, params }: { logs: LogEntry[]; params: ParamDeclaration[] }) {
|
export function LogPreview({ logs, params }: { logs: LogEntry[]; params: ParamDeclaration[] }) {
|
||||||
const paramNames = params.map((d) => d.name)
|
const paramNames = params.map((d) => d.name)
|
||||||
const [visibleParamColumns, setVisibleParamColumns] = useState<string[]>(loadVisibleParamColumns)
|
const [visibleParamColumns, setVisibleParamColumns] = useState<string[]>(loadVisibleParamColumns)
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
useScrollAnchor(scrollContainerRef, [logs])
|
||||||
|
|
||||||
const visible = new Set(visibleParamColumns)
|
const visible = new Set(visibleParamColumns)
|
||||||
|
|
||||||
@@ -110,7 +152,7 @@ export function LogPreview({ logs, params }: { logs: LogEntry[]; params: ParamDe
|
|||||||
</div>
|
</div>
|
||||||
<ColumnPicker paramNames={paramNames} visible={visible} onToggle={toggleColumn} />
|
<ColumnPicker paramNames={paramNames} visible={visible} onToggle={toggleColumn} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto">
|
<div ref={scrollContainerRef} className="flex-1 overflow-auto">
|
||||||
<table className="w-full border-collapse text-left text-xs">
|
<table className="w-full border-collapse text-left text-xs">
|
||||||
<thead className="sticky top-0 bg-surface-0 text-text-subtle">
|
<thead className="sticky top-0 bg-surface-0 text-text-subtle">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -126,7 +168,7 @@ export function LogPreview({ logs, params }: { logs: LogEntry[]; params: ParamDe
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{logs.map((log) => (
|
{logs.map((log) => (
|
||||||
<tr key={log.sequence} className="border-t border-border-faint hover:bg-surface-1/60">
|
<tr key={log.sequence} data-row-key={log.sequence} className="border-t border-border-faint hover:bg-surface-1/60">
|
||||||
<td className="whitespace-nowrap px-3 py-1 text-text-subtle">{log.sequence}</td>
|
<td className="whitespace-nowrap px-3 py-1 text-text-subtle">{log.sequence}</td>
|
||||||
<td className="whitespace-nowrap px-3 py-1 text-text-subtle">
|
<td className="whitespace-nowrap px-3 py-1 text-text-subtle">
|
||||||
{new Date(log.timestamp_ms).toLocaleTimeString()}
|
{new Date(log.timestamp_ms).toLocaleTimeString()}
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ export interface TimeRange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// `'all'` clears the range (the implicit default); the rest resolve to a window ending "now" at the moment they're (re-)computed — see `computePresetRange`.
|
// `'all'` clears the range (the implicit default); the rest resolve to a window ending "now" at the moment they're (re-)computed — see `computePresetRange`.
|
||||||
export type TimeRangePresetId = 'all' | '5m' | '15m' | '1h' | '24h'
|
export type TimeRangePresetId = 'all' | '1m' | '2m' | '5m' | '15m' | '1h' | '24h'
|
||||||
|
|
||||||
export const TIME_RANGE_PRESETS: { id: TimeRangePresetId; label: string; ms: number | null }[] = [
|
export const TIME_RANGE_PRESETS: { id: TimeRangePresetId; label: string; ms: number | null }[] = [
|
||||||
{ id: 'all', label: 'All', ms: null },
|
{ id: 'all', label: 'All', ms: null },
|
||||||
|
{ id: '1m', label: '1m', ms: 60 * 1000 },
|
||||||
|
{ id: '2m', label: '2m', ms: 2 * 60 * 1000 },
|
||||||
{ id: '5m', label: '5m', ms: 5 * 60 * 1000 },
|
{ id: '5m', label: '5m', ms: 5 * 60 * 1000 },
|
||||||
{ id: '15m', label: '15m', ms: 15 * 60 * 1000 },
|
{ id: '15m', label: '15m', ms: 15 * 60 * 1000 },
|
||||||
{ id: '1h', label: '1h', ms: 60 * 60 * 1000 },
|
{ id: '1h', label: '1h', ms: 60 * 60 * 1000 },
|
||||||
|
|||||||
Reference in New Issue
Block a user