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({ )}
-
+