Resolve ui stuff

This commit is contained in:
Hugh Kaznowski
2026-09-05 18:00:56 +01:00
parent 00c21c8032
commit 070e835fa4
11 changed files with 350 additions and 261 deletions
+26 -8
View File
@@ -1,5 +1,6 @@
import { useRef, useState } from 'react'
import type { TimelineBucket } from '../lib/api'
import { sectionHeaderClassName, sectionHeaderLeftClassName } from './CollapsibleSection'
import { HelpIcon } from './HelpIcon'
export interface TimeRange {
@@ -26,6 +27,14 @@ export function computePresetRange(id: TimeRangePresetId): TimeRange | null {
return { startMs: now - preset.ms, endMs: now }
}
/// Buckets a fixed range into this many slots, so e.g. a rolling `5m` preset always requests a resolution fine enough to visibly slide instead of a handful of coarse, jumpy bars.
const TIMELINE_TARGET_BUCKETS = 60
/// Callers with an explicit (non-`all`) range pass this as `bucketMs` to `fetchTimeline` — `LogTimeline` derives the same value from its own `domainSpan` when laying out bars, so requested and rendered resolution always agree.
export function computeBucketMs(domainSpanMs: number): number {
return Math.max(1000, Math.round(domainSpanMs / TIMELINE_TARGET_BUCKETS))
}
function formatTimestamp(ms: number): string {
return new Date(ms).toLocaleTimeString()
}
@@ -52,6 +61,8 @@ export function LogTimeline({
const domainStart = range?.startMs ?? buckets[0]?.start_ms ?? 0
const domainEnd = range?.endMs ?? Date.now()
const domainSpan = Math.max(1, domainEnd - domainStart)
// Matches what the caller requested via `computeBucketMs` for a fixed range; with no range ('all') there's no such request, so fall back to the actual spacing between returned buckets.
const bucketMs = range ? computeBucketMs(domainSpan) : buckets.length >= 2 ? buckets[1].start_ms - buckets[0].start_ms : domainSpan
const firstBucket = buckets[0] ?? null
const lastBucket = buckets[buckets.length - 1] ?? null
@@ -103,13 +114,13 @@ export function LogTimeline({
const containerLeft = containerRef.current?.getBoundingClientRect().left ?? 0
return (
<div className="border-b border-border p-3">
<div className="mb-1 flex items-center justify-between text-xs text-text-subtle">
<span className="flex items-center gap-1.5">
<div className="border-b border-border">
<div className={sectionHeaderClassName}>
<span className={sectionHeaderLeftClassName}>
Timeline
<HelpIcon sectionId="timeline" label="Help: Timeline" />
</span>
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1.5 text-xs text-text-subtle">
<div className="flex items-center gap-1">
{TIME_RANGE_PRESETS.map((preset) => (
<button
@@ -134,9 +145,10 @@ export function LogTimeline({
)}
</div>
</div>
<div className="p-3">
<div
ref={containerRef}
className="relative h-24 w-full cursor-crosshair select-none rounded bg-surface-1"
className="relative h-24 w-full cursor-crosshair select-none overflow-hidden rounded bg-surface-1"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
@@ -146,12 +158,17 @@ export function LogTimeline({
setHoverX(null)
}}
>
<div className="flex h-full w-full items-end gap-px overflow-hidden px-1">
{/* Positioned by actual elapsed time within the domain (not evenly spread via flex), so a rolling preset like `5m` visibly slides — a bucket's left/width tracks its real place in the window instead of jumping to a fixed slot as buckets enter/leave it. */}
<div className="absolute inset-x-1 bottom-0 top-0">
{buckets.map((b) => (
<div
key={b.start_ms}
className="min-w-[2px] flex-1 rounded-t bg-accent/70"
style={{ height: `${(b.count / maxCount) * 100}%` }}
className="absolute bottom-0 min-w-[2px] rounded-t bg-accent/70"
style={{
left: `${((b.start_ms - domainStart) / domainSpan) * 100}%`,
width: `${(bucketMs / domainSpan) * 100}%`,
height: `${(b.count / maxCount) * 100}%`,
}}
/>
))}
</div>
@@ -188,6 +205,7 @@ export function LogTimeline({
</div>
)}
</div>
</div>
</div>
)
}