Skip to content

Commit 171c50f

Browse files
committed
Added event for recalculating vertical line hover effect on timeline span
1 parent dcd73f1 commit 171c50f

File tree

2 files changed

+55
-71
lines changed
  • apps/webapp/app
    • components/primitives
    • routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam

2 files changed

+55
-71
lines changed

apps/webapp/app/components/primitives/Timeline.tsx

Lines changed: 51 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
Component,
23
ComponentPropsWithoutRef,
34
Fragment,
45
ReactNode,
@@ -16,91 +17,66 @@ interface MousePosition {
1617
y: number;
1718
}
1819
const MousePositionContext = createContext<MousePosition | undefined>(undefined);
19-
export function MousePositionProvider({ children }: { children: ReactNode }) {
20+
export function MousePositionProvider({
21+
children,
22+
recalculateTrigger,
23+
}: {
24+
children: ReactNode;
25+
recalculateTrigger?: unknown;
26+
}) {
2027
const ref = useRef<HTMLDivElement>(null);
2128
const [position, setPosition] = useState<MousePosition | undefined>(undefined);
22-
const lastClient = useRef<{ clientX: number; clientY: number } | null>(null);
23-
const rafId = useRef<number | null>(null);
29+
const lastMouseCoordsRef = useRef<{ clientX: number; clientY: number } | null>(null);
2430

25-
const computeFromClient = useCallback((clientX: number, clientY: number) => {
26-
if (!ref.current) {
27-
setPosition(undefined);
28-
return;
29-
}
31+
const handleMouseMove = useCallback(
32+
(e: React.MouseEvent) => {
33+
lastMouseCoordsRef.current = { clientX: e.clientX, clientY: e.clientY };
3034

31-
const { top, left, width, height } = ref.current.getBoundingClientRect();
32-
const x = (clientX - left) / width;
33-
const y = (clientY - top) / height;
35+
if (!ref.current) {
36+
setPosition(undefined);
37+
return;
38+
}
3439

35-
if (x < 0 || x > 1 || y < 0 || y > 1) {
36-
setPosition(undefined);
37-
return;
38-
}
40+
const { top, left, width, height } = ref.current.getBoundingClientRect();
41+
const x = (e.clientX - left) / width;
42+
const y = (e.clientY - top) / height;
3943

40-
setPosition({ x, y });
41-
}, []);
44+
if (x < 0 || x > 1 || y < 0 || y > 1) {
45+
setPosition(undefined);
46+
return;
47+
}
4248

43-
const handleMouseMove = useCallback(
44-
(e: React.MouseEvent) => {
45-
lastClient.current = { clientX: e.clientX, clientY: e.clientY };
46-
computeFromClient(e.clientX, e.clientY);
49+
setPosition({ x, y });
4750
},
48-
[computeFromClient]
51+
[ref.current]
4952
);
5053

51-
// Recalculate the relative position when the container resizes or the window/ancestors scroll.
54+
// Recalculate position when trigger changes (e.g., panel opens/closes)
55+
// Use requestAnimationFrame to wait for the DOM layout to complete
5256
useEffect(() => {
53-
if (!ref.current) return;
54-
55-
const ro = new ResizeObserver(() => {
56-
const lc = lastClient.current;
57-
if (lc) computeFromClient(lc.clientX, lc.clientY);
58-
});
59-
ro.observe(ref.current);
60-
61-
const onRecalc = () => {
62-
const lc = lastClient.current;
63-
if (lc) computeFromClient(lc.clientX, lc.clientY);
64-
};
57+
if (!lastMouseCoordsRef.current) {
58+
return;
59+
}
6560

66-
window.addEventListener("resize", onRecalc);
67-
// Use capture to catch scroll on any ancestor that impacts bounding rect
68-
window.addEventListener("scroll", onRecalc, true);
61+
const rafId = requestAnimationFrame(() => {
62+
if (!ref.current || !lastMouseCoordsRef.current) {
63+
return;
64+
}
6965

70-
return () => {
71-
ro.disconnect();
72-
window.removeEventListener("resize", onRecalc);
73-
window.removeEventListener("scroll", onRecalc, true);
74-
};
75-
}, [computeFromClient]);
66+
const { top, left, width, height } = ref.current.getBoundingClientRect();
67+
const x = (lastMouseCoordsRef.current.clientX - left) / width;
68+
const y = (lastMouseCoordsRef.current.clientY - top) / height;
7669

77-
useEffect(() => {
78-
if (position === undefined || !lastClient.current || !ref.current) return;
79-
80-
const isAnimating = () => {
81-
if (!ref.current) return false;
82-
const styles = window.getComputedStyle(ref.current);
83-
return styles.transition !== "none" || styles.animation !== "none";
84-
};
85-
86-
const tick = () => {
87-
const lc = lastClient.current;
88-
if (lc) {
89-
computeFromClient(lc.clientX, lc.clientY);
90-
if (isAnimating()) {
91-
rafId.current = requestAnimationFrame(tick);
92-
} else {
93-
rafId.current = null;
94-
}
70+
if (x < 0 || x > 1 || y < 0 || y > 1) {
71+
setPosition(undefined);
72+
return;
9573
}
96-
};
9774

98-
rafId.current = requestAnimationFrame(tick);
99-
return () => {
100-
if (rafId.current !== null) cancelAnimationFrame(rafId.current);
101-
rafId.current = null;
102-
};
103-
}, [position, computeFromClient]);
75+
setPosition({ x, y });
76+
});
77+
78+
return () => cancelAnimationFrame(rafId);
79+
}, [recalculateTrigger]);
10480

10581
return (
10682
<div
@@ -144,6 +120,8 @@ export type RootProps = {
144120
maxWidth: number;
145121
children?: ReactNode;
146122
className?: string;
123+
/** When this value changes, recalculate the mouse position (useful when panels resize) */
124+
recalculateTrigger?: unknown;
147125
};
148126

149127
/** The main element that determines the dimensions for all sub-elements */
@@ -155,6 +133,7 @@ export function Root({
155133
maxWidth,
156134
children,
157135
className,
136+
recalculateTrigger,
158137
}: RootProps) {
159138
const pixelWidth = calculatePixelWidth(minWidth, maxWidth, scale);
160139

@@ -167,7 +146,9 @@ export function Root({
167146
width: `${pixelWidth}px`,
168147
}}
169148
>
170-
<MousePositionProvider>{children}</MousePositionProvider>
149+
<MousePositionProvider recalculateTrigger={recalculateTrigger}>
150+
{children}
151+
</MousePositionProvider>
171152
</div>
172153
</TimelineContext.Provider>
173154
);

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,7 @@ function TasksTreeView({
920920
treeScrollRef={treeScrollRef}
921921
virtualizer={virtualizer}
922922
toggleNodeSelection={toggleNodeSelection}
923+
selectedId={selectedId}
923924
/>
924925
</ResizablePanel>
925926
</ResizablePanelGroup>
@@ -973,7 +974,7 @@ function TasksTreeView({
973974

974975
type TimelineViewProps = Pick<
975976
TasksTreeViewProps,
976-
"totalDuration" | "rootSpanStatus" | "events" | "rootStartedAt" | "queuedDuration"
977+
"totalDuration" | "rootSpanStatus" | "events" | "rootStartedAt" | "queuedDuration" | "selectedId"
977978
> & {
978979
scale: number;
979980
parentRef: React.RefObject<HTMLDivElement>;
@@ -1004,6 +1005,7 @@ function TimelineView({
10041005
showDurations,
10051006
treeScrollRef,
10061007
queuedDuration,
1008+
selectedId,
10071009
}: TimelineViewProps) {
10081010
const timelineContainerRef = useRef<HTMLDivElement>(null);
10091011
const initialTimelineDimensions = useInitialDimensions(timelineContainerRef);
@@ -1042,6 +1044,7 @@ function TimelineView({
10421044
className="h-full overflow-hidden"
10431045
minWidth={minTimelineWidth}
10441046
maxWidth={maxTimelineWidth}
1047+
recalculateTrigger={selectedId}
10451048
>
10461049
{/* Follows the cursor */}
10471050
<CurrentTimeIndicator

0 commit comments

Comments
 (0)