diff --git a/apps/webapp/app/components/primitives/Timeline.tsx b/apps/webapp/app/components/primitives/Timeline.tsx index dfb3253750..b56c9392a2 100644 --- a/apps/webapp/app/components/primitives/Timeline.tsx +++ b/apps/webapp/app/components/primitives/Timeline.tsx @@ -6,6 +6,7 @@ import { createContext, useCallback, useContext, + useEffect, useRef, useState, } from "react"; @@ -16,12 +17,21 @@ interface MousePosition { y: number; } const MousePositionContext = createContext(undefined); -export function MousePositionProvider({ children }: { children: ReactNode }) { +export function MousePositionProvider({ + children, + recalculateTrigger, +}: { + children: ReactNode; + recalculateTrigger?: unknown; +}) { const ref = useRef(null); const [position, setPosition] = useState(undefined); + const lastMouseCoordsRef = useRef<{ clientX: number; clientY: number } | null>(null); const handleMouseMove = useCallback( (e: React.MouseEvent) => { + lastMouseCoordsRef.current = { clientX: e.clientX, clientY: e.clientY }; + if (!ref.current) { setPosition(undefined); return; @@ -41,11 +51,41 @@ export function MousePositionProvider({ children }: { children: ReactNode }) { [ref.current] ); + // Recalculate position when trigger changes (e.g., panel opens/closes) + // Use requestAnimationFrame to wait for the DOM layout to complete + useEffect(() => { + if (!lastMouseCoordsRef.current) { + return; + } + + const rafId = requestAnimationFrame(() => { + if (!ref.current || !lastMouseCoordsRef.current) { + return; + } + + const { top, left, width, height } = ref.current.getBoundingClientRect(); + const x = (lastMouseCoordsRef.current.clientX - left) / width; + const y = (lastMouseCoordsRef.current.clientY - top) / height; + + if (x < 0 || x > 1 || y < 0 || y > 1) { + setPosition(undefined); + return; + } + + setPosition({ x, y }); + }); + + return () => cancelAnimationFrame(rafId); + }, [recalculateTrigger]); + return (
setPosition(undefined)} + onMouseLeave={() => { + lastMouseCoordsRef.current = null; + setPosition(undefined); + }} onMouseMove={handleMouseMove} style={{ width: "100%", height: "100%" }} > @@ -83,6 +123,8 @@ export type RootProps = { maxWidth: number; children?: ReactNode; className?: string; + /** When this value changes, recalculate the mouse position (useful when panels resize) */ + recalculateTrigger?: unknown; }; /** The main element that determines the dimensions for all sub-elements */ @@ -94,6 +136,7 @@ export function Root({ maxWidth, children, className, + recalculateTrigger, }: RootProps) { const pixelWidth = calculatePixelWidth(minWidth, maxWidth, scale); @@ -106,7 +149,9 @@ export function Root({ width: `${pixelWidth}px`, }} > - {children} + + {children} +
); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index 1ffd128b30..f2fe7cdfcd 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -920,6 +920,7 @@ function TasksTreeView({ treeScrollRef={treeScrollRef} virtualizer={virtualizer} toggleNodeSelection={toggleNodeSelection} + selectedId={selectedId} /> @@ -973,7 +974,7 @@ function TasksTreeView({ type TimelineViewProps = Pick< TasksTreeViewProps, - "totalDuration" | "rootSpanStatus" | "events" | "rootStartedAt" | "queuedDuration" + "totalDuration" | "rootSpanStatus" | "events" | "rootStartedAt" | "queuedDuration" | "selectedId" > & { scale: number; parentRef: React.RefObject; @@ -1004,6 +1005,7 @@ function TimelineView({ showDurations, treeScrollRef, queuedDuration, + selectedId, }: TimelineViewProps) { const timelineContainerRef = useRef(null); const initialTimelineDimensions = useInitialDimensions(timelineContainerRef); @@ -1042,6 +1044,7 @@ function TimelineView({ className="h-full overflow-hidden" minWidth={minTimelineWidth} maxWidth={maxTimelineWidth} + recalculateTrigger={selectedId} > {/* Follows the cursor */}