Webapp
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import type { TimelineBucket } from '../lib/api'
|
||||
|
||||
export interface TimeRange {
|
||||
startMs: number
|
||||
endMs: number
|
||||
}
|
||||
|
||||
export function LogTimeline({
|
||||
buckets,
|
||||
range,
|
||||
onSelectRange,
|
||||
}: {
|
||||
buckets: TimelineBucket[]
|
||||
range: TimeRange | null
|
||||
onSelectRange: (range: TimeRange | null) => void
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [dragStartX, setDragStartX] = useState<number | null>(null)
|
||||
const [dragCurrentX, setDragCurrentX] = useState<number | null>(null)
|
||||
|
||||
const maxCount = Math.max(1, ...buckets.map((b) => b.count))
|
||||
const domainStart = range?.startMs ?? buckets[0]?.start_ms ?? 0
|
||||
const domainEnd = range?.endMs ?? Date.now()
|
||||
const domainSpan = Math.max(1, domainEnd - domainStart)
|
||||
|
||||
function xToMs(x: number) {
|
||||
const rect = containerRef.current?.getBoundingClientRect()
|
||||
if (!rect) return domainStart
|
||||
const ratio = Math.min(1, Math.max(0, (x - rect.left) / rect.width))
|
||||
return domainStart + ratio * domainSpan
|
||||
}
|
||||
|
||||
function handleMouseDown(e: React.MouseEvent) {
|
||||
setDragStartX(e.clientX)
|
||||
setDragCurrentX(e.clientX)
|
||||
}
|
||||
function handleMouseMove(e: React.MouseEvent) {
|
||||
if (dragStartX !== null) setDragCurrentX(e.clientX)
|
||||
}
|
||||
function handleMouseUp() {
|
||||
if (dragStartX !== null && dragCurrentX !== null && Math.abs(dragCurrentX - dragStartX) > 3) {
|
||||
const a = xToMs(Math.min(dragStartX, dragCurrentX))
|
||||
const b = xToMs(Math.max(dragStartX, dragCurrentX))
|
||||
onSelectRange({ startMs: a, endMs: b })
|
||||
}
|
||||
setDragStartX(null)
|
||||
setDragCurrentX(null)
|
||||
}
|
||||
|
||||
const dragging = dragStartX !== null && dragCurrentX !== null
|
||||
const selRect = dragging
|
||||
? {
|
||||
left: Math.min(dragStartX!, dragCurrentX!),
|
||||
width: Math.abs(dragCurrentX! - dragStartX!),
|
||||
}
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="border-b border-neutral-800 p-3">
|
||||
<div className="mb-1 flex items-center justify-between text-xs text-neutral-500">
|
||||
<span>Timeline</span>
|
||||
{range && (
|
||||
<button className="underline hover:text-neutral-300" onClick={() => onSelectRange(null)}>
|
||||
Clear selection
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative h-24 w-full cursor-crosshair select-none rounded bg-neutral-900"
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={() => {
|
||||
setDragStartX(null)
|
||||
setDragCurrentX(null)
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full w-full items-end gap-px overflow-hidden px-1">
|
||||
{buckets.map((b) => (
|
||||
<div
|
||||
key={b.start_ms}
|
||||
className="min-w-[2px] flex-1 rounded-t bg-sky-500/70"
|
||||
style={{ height: `${(b.count / maxCount) * 100}%` }}
|
||||
title={`${new Date(b.start_ms).toLocaleTimeString()} — ${b.count}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{selRect && (
|
||||
<div
|
||||
className="pointer-events-none absolute top-0 h-full border border-sky-400 bg-sky-400/20"
|
||||
style={{ left: selRect.left - (containerRef.current?.getBoundingClientRect().left ?? 0), width: selRect.width }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user