diff --git a/web/package-lock.json b/web/package-lock.json
index 34b159fa1..5b279adc3 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -15,6 +15,7 @@
"@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
@@ -1392,6 +1393,37 @@
}
}
},
+ "node_modules/@radix-ui/react-hover-card": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.0.7.tgz",
+ "integrity": "sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/primitive": "1.0.1",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-context": "1.0.1",
+ "@radix-ui/react-dismissable-layer": "1.0.5",
+ "@radix-ui/react-popper": "1.1.3",
+ "@radix-ui/react-portal": "1.0.4",
+ "@radix-ui/react-presence": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-use-controllable-state": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-id": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
diff --git a/web/package.json b/web/package.json
index ebdb6dceb..8eae592c4 100644
--- a/web/package.json
+++ b/web/package.json
@@ -20,6 +20,7 @@
"@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx
index 8efb7f972..5eede4575 100644
--- a/web/src/components/player/LivePlayer.tsx
+++ b/web/src/components/player/LivePlayer.tsx
@@ -127,8 +127,8 @@ export default function LivePlayer({
diff --git a/web/src/components/timeline/EventReviewTimeline.tsx b/web/src/components/timeline/EventReviewTimeline.tsx
index 428a9ec37..6637dda3d 100644
--- a/web/src/components/timeline/EventReviewTimeline.tsx
+++ b/web/src/components/timeline/EventReviewTimeline.tsx
@@ -10,7 +10,6 @@ import {
import EventSegment from "./EventSegment";
import { useEventUtils } from "@/hooks/use-event-utils";
import { ReviewSegment, ReviewSeverity } from "@/types/review";
-import { TooltipProvider } from "../ui/tooltip";
export type EventReviewTimelineProps = {
segmentDuration: number;
@@ -56,14 +55,18 @@ export function EventReviewTimeline({
[timelineEnd, timelineStart]
);
- const { alignDateToTimeline } = useEventUtils(events, segmentDuration);
+ const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils(
+ events,
+ segmentDuration
+ );
const { handleMouseDown, handleMouseUp, handleMouseMove } =
useDraggableHandler({
contentRef,
timelineRef,
scrollTimeRef,
- alignDateToTimeline,
+ alignStartDateToTimeline,
+ alignEndDateToTimeline,
segmentDuration,
showHandlebar,
timelineDuration,
@@ -96,7 +99,7 @@ export function EventReviewTimeline({
// Generate segments for the timeline
const generateSegments = useCallback(() => {
const segmentCount = timelineDuration / segmentDuration;
- const segmentAlignedTime = alignDateToTimeline(timelineStart);
+ const segmentAlignedTime = alignStartDateToTimeline(timelineStart);
return Array.from({ length: segmentCount }, (_, index) => {
const segmentTime = segmentAlignedTime - index * segmentDuration;
@@ -172,7 +175,7 @@ export function EventReviewTimeline({
timelineHeight / (timelineDuration / segmentDuration);
// Calculate the segment index corresponding to the target time
- const alignedHandlebarTime = alignDateToTimeline(handlebarTime);
+ const alignedHandlebarTime = alignStartDateToTimeline(handlebarTime);
const segmentIndex = Math.ceil(
(timelineStart - alignedHandlebarTime) / segmentDuration
);
@@ -213,44 +216,39 @@ export function EventReviewTimeline({
]);
return (
-
-
-
{segments}
- {showHandlebar && (
-
-
+
+
{segments}
+ {showHandlebar && (
+
+
+
-
+ ref={currentTimeRef}
+ className="text-white text-xs z-10"
+ >
+
- )}
-
-
+
+ )}
+
);
}
diff --git a/web/src/components/timeline/EventSegment.tsx b/web/src/components/timeline/EventSegment.tsx
index d3b249688..78d54d578 100644
--- a/web/src/components/timeline/EventSegment.tsx
+++ b/web/src/components/timeline/EventSegment.tsx
@@ -9,8 +9,13 @@ import React, {
useMemo,
useRef,
} from "react";
-import { Tooltip, TooltipContent } from "../ui/tooltip";
-import { TooltipTrigger } from "@radix-ui/react-tooltip";
+import { isDesktop } from "react-device-detect";
+import {
+ HoverCard,
+ HoverCardContent,
+ HoverCardTrigger,
+} from "../ui/hover-card";
+import { HoverCardPortal } from "@radix-ui/react-hover-card";
type EventSegmentProps = {
events: ReviewSegment[];
@@ -33,8 +38,6 @@ type MinimapSegmentProps = {
};
type TickSegmentProps = {
- isFirstSegmentInMinimap: boolean;
- isLastSegmentInMinimap: boolean;
timestamp: Date;
timestampSpread: number;
};
@@ -58,25 +61,23 @@ function MinimapBounds({
<>
{isFirstSegmentInMinimap && (
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
- month: "short",
- day: "2-digit",
+ ...(isDesktop && { month: "short", day: "2-digit" }),
})}
)}
{isLastSegmentInMinimap && (
-
+
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
- month: "short",
- day: "2-digit",
+ ...(isDesktop && { month: "short", day: "2-digit" }),
})}
)}
@@ -84,15 +85,10 @@ function MinimapBounds({
);
}
-function Tick({
- isFirstSegmentInMinimap,
- isLastSegmentInMinimap,
- timestamp,
- timestampSpread,
-}: TickSegmentProps) {
+function Tick({ timestamp, timestampSpread }: TickSegmentProps) {
return (
-
- {!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
+
);
}
@@ -114,7 +110,7 @@ function Timestamp({
segmentKey,
}: TimestampSegmentProps) {
return (
-
+
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
getSeverity(segmentTime, displaySeverityType),
@@ -177,7 +176,7 @@ export function EventSegment({
const startTimestamp = useMemo(() => {
const eventStart = getEventStart(segmentTime);
if (eventStart) {
- return alignDateToTimeline(eventStart);
+ return alignStartDateToTimeline(eventStart);
}
}, [getEventStart, segmentTime]);
@@ -191,23 +190,26 @@ export function EventSegment({
const segmentKey = useMemo(() => segmentTime, [segmentTime]);
const alignedMinimapStartTime = useMemo(
- () => alignDateToTimeline(minimapStartTime ?? 0),
- [minimapStartTime, alignDateToTimeline]
+ () => alignStartDateToTimeline(minimapStartTime ?? 0),
+ [minimapStartTime, alignStartDateToTimeline]
);
const alignedMinimapEndTime = useMemo(
- () => alignDateToTimeline(minimapEndTime ?? 0),
- [minimapEndTime, alignDateToTimeline]
+ () => alignEndDateToTimeline(minimapEndTime ?? 0),
+ [minimapEndTime, alignEndDateToTimeline]
);
const isInMinimapRange = useMemo(() => {
return (
showMinimap &&
- minimapStartTime &&
- minimapEndTime &&
- segmentTime > minimapStartTime &&
- segmentTime < minimapEndTime
+ segmentTime >= alignedMinimapStartTime &&
+ segmentTime < alignedMinimapEndTime
);
- }, [showMinimap, minimapStartTime, minimapEndTime, segmentTime]);
+ }, [
+ showMinimap,
+ alignedMinimapStartTime,
+ alignedMinimapEndTime,
+ segmentTime,
+ ]);
const isFirstSegmentInMinimap = useMemo(() => {
return showMinimap && segmentTime === alignedMinimapStartTime;
@@ -236,11 +238,17 @@ export function EventSegment({
}
}, [showMinimap, isFirstSegmentInMinimap, events, segmentDuration]);
- const segmentClasses = `flex flex-row ${
- showMinimap ? (isInMinimapRange ? "bg-muted" : "bg-background") : ""
+ const segmentClasses = `h-2 relative w-[55px] md:w-[100px] ${
+ showMinimap
+ ? isInMinimapRange
+ ? "bg-card"
+ : isLastSegmentInMinimap
+ ? ""
+ : "opacity-70"
+ : ""
} ${
isFirstSegmentInMinimap || isLastSegmentInMinimap
- ? "relative h-2 border-b border-gray-500"
+ ? "relative h-2 border-b-2 border-gray-500"
: ""
}`;
@@ -280,7 +288,11 @@ export function EventSegment({
}, [startTimestamp]);
return (
-
+
-
+
(
{severityValue === displaySeverityType && (
-
+
-
+
-
-
-
-
+
+
+
+
+
+
-
+
)}
{severityValue !== displaySeverityType && (
-
+
,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+))
+HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
+
+export { HoverCard, HoverCardTrigger, HoverCardContent }
diff --git a/web/src/hooks/use-event-utils.ts b/web/src/hooks/use-event-utils.ts
index b8483d8e6..ac4300c5d 100644
--- a/web/src/hooks/use-event-utils.ts
+++ b/web/src/hooks/use-event-utils.ts
@@ -42,7 +42,7 @@ export const useEventUtils = (
[segmentDuration]
);
- const alignDateToTimeline = useCallback(
+ const alignEndDateToTimeline = useCallback(
(time: number): number => {
const remainder = time % segmentDuration;
const adjustment = remainder !== 0 ? segmentDuration - remainder : 0;
@@ -51,11 +51,21 @@ export const useEventUtils = (
[segmentDuration]
);
+ const alignStartDateToTimeline = useCallback(
+ (time: number): number => {
+ const remainder = time % segmentDuration;
+ const adjustment = remainder === 0 ? 0 : -(remainder);
+ return time + adjustment;
+ },
+ [segmentDuration]
+ );
+
return {
isStartOfEvent,
isEndOfEvent,
getSegmentStart,
getSegmentEnd,
- alignDateToTimeline,
+ alignEndDateToTimeline,
+ alignStartDateToTimeline,
};
};
diff --git a/web/src/hooks/use-handle-dragging.ts b/web/src/hooks/use-handle-dragging.ts
index cf887095f..9f70374b2 100644
--- a/web/src/hooks/use-handle-dragging.ts
+++ b/web/src/hooks/use-handle-dragging.ts
@@ -4,7 +4,8 @@ interface DragHandlerProps {
contentRef: React.RefObject;
timelineRef: React.RefObject;
scrollTimeRef: React.RefObject;
- alignDateToTimeline: (time: number) => number;
+ alignStartDateToTimeline: (time: number) => number;
+ alignEndDateToTimeline: (time: number) => number;
segmentDuration: number;
showHandlebar: boolean;
timelineDuration: number;
@@ -20,7 +21,7 @@ function useDraggableHandler({
contentRef,
timelineRef,
scrollTimeRef,
- alignDateToTimeline,
+ alignStartDateToTimeline,
segmentDuration,
showHandlebar,
timelineDuration,
@@ -94,7 +95,7 @@ function useDraggableHandler({
);
const segmentIndex = Math.floor(newHandlePosition / segmentHeight);
- const segmentStartTime = alignDateToTimeline(
+ const segmentStartTime = alignStartDateToTimeline(
timelineStart - segmentIndex * segmentDuration
);
diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx
index dbc20e84e..d6057a184 100644
--- a/web/src/views/events/EventView.tsx
+++ b/web/src/views/events/EventView.tsx
@@ -82,7 +82,7 @@ export default function EventView({
};
}, [reviewPages]);
- const { alignDateToTimeline } = useEventUtils(
+ const { alignStartDateToTimeline } = useEventUtils(
reviewItems.all,
segmentDuration
);
@@ -270,7 +270,8 @@ export default function EventView({
ref={lastRow ? lastReviewRef : minimapRef}
data-start={value.start_time}
data-segment-start={
- alignDateToTimeline(value.start_time) - segmentDuration
+ alignStartDateToTimeline(value.start_time) -
+ segmentDuration
}
className="outline outline-offset-1 outline-0 rounded-lg shadow-none transition-all duration-500 my-1 md:my-0"
>
@@ -291,7 +292,7 @@ export default function EventView({
)}
-