Review timeline bugfixes (#9962)

* fix severity logic

* timestamp line height

* use timestamp for end of timeline instead of duration
This commit is contained in:
Josh Hawkins 2024-02-21 11:58:41 -06:00 committed by GitHub
parent e608297c31
commit be4b570346
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 198 additions and 130 deletions

View File

@ -15,7 +15,7 @@ export type EventReviewTimelineProps = {
segmentDuration: number; segmentDuration: number;
timestampSpread: number; timestampSpread: number;
timelineStart: number; timelineStart: number;
timelineDuration?: number; timelineEnd: number;
showHandlebar?: boolean; showHandlebar?: boolean;
handlebarTime?: number; handlebarTime?: number;
showMinimap?: boolean; showMinimap?: boolean;
@ -30,7 +30,7 @@ export function EventReviewTimeline({
segmentDuration, segmentDuration,
timestampSpread, timestampSpread,
timelineStart, timelineStart,
timelineDuration = 24 * 60 * 60, timelineEnd,
showHandlebar = false, showHandlebar = false,
handlebarTime, handlebarTime,
showMinimap = false, showMinimap = false,
@ -46,6 +46,10 @@ export function EventReviewTimeline({
const timelineRef = useRef<HTMLDivElement>(null); const timelineRef = useRef<HTMLDivElement>(null);
const currentTimeRef = useRef<HTMLDivElement>(null); const currentTimeRef = useRef<HTMLDivElement>(null);
const observer = useRef<ResizeObserver | null>(null); const observer = useRef<ResizeObserver | null>(null);
const timelineDuration = useMemo(
() => timelineEnd - timelineStart,
[timelineEnd, timelineStart]
);
const { alignDateToTimeline } = useEventUtils(events, segmentDuration); const { alignDateToTimeline } = useEventUtils(events, segmentDuration);

View File

@ -1,7 +1,7 @@
import { useEventUtils } from "@/hooks/use-event-utils"; import { useEventUtils } from "@/hooks/use-event-utils";
import { useSegmentUtils } from "@/hooks/use-segment-utils"; import { useSegmentUtils } from "@/hooks/use-segment-utils";
import { ReviewSegment, ReviewSeverity } from "@/types/review"; import { ReviewSegment, ReviewSeverity } from "@/types/review";
import { useMemo } from "react"; import React, { useMemo } from "react";
type EventSegmentProps = { type EventSegmentProps = {
events: ReviewSegment[]; events: ReviewSegment[];
@ -45,7 +45,7 @@ function MinimapBounds({
return ( return (
<> <>
{isFirstSegmentInMinimap && ( {isFirstSegmentInMinimap && (
<div className="absolute inset-0 -bottom-5 w-full flex items-center justify-center text-xs text-primary font-medium z-20 text-center text-[9px]"> <div className="absolute inset-0 -bottom-5 w-full flex items-center justify-center text-xs text-primary font-medium z-20 text-center text-[8px]">
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], { {new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], {
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
@ -56,7 +56,7 @@ function MinimapBounds({
)} )}
{isLastSegmentInMinimap && ( {isLastSegmentInMinimap && (
<div className="absolute inset-0 -top-1 w-full flex items-center justify-center text-xs text-primary font-medium z-20 text-center text-[9px]"> <div className="absolute inset-0 -top-1 w-full flex items-center justify-center text-xs text-primary font-medium z-20 text-center text-[8px]">
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], { {new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], {
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
@ -76,14 +76,14 @@ function Tick({
timestampSpread, timestampSpread,
}: TickSegmentProps) { }: TickSegmentProps) {
return ( return (
<div className="w-5 h-2 flex justify-left items-end"> <div className="w-[12px] h-2 flex justify-left items-end">
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && ( {!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
<div <div
className={`h-0.5 ${ className={`h-0.5 ${
timestamp.getMinutes() % timestampSpread === 0 && timestamp.getMinutes() % timestampSpread === 0 &&
timestamp.getSeconds() === 0 timestamp.getSeconds() === 0
? "w-4 bg-gray-400" ? "w-[12px] bg-gray-400"
: "w-2 bg-gray-600" : "w-[8px] bg-gray-600"
}`} }`}
></div> ></div>
)} )}
@ -99,7 +99,7 @@ function Timestamp({
segmentKey, segmentKey,
}: TimestampSegmentProps) { }: TimestampSegmentProps) {
return ( return (
<div className="w-10 h-2 flex justify-left items-top z-10"> <div className="w-[36px] pl-[3px] leading-[9px] h-2 flex justify-left items-top z-10">
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && ( {!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
<div <div
key={`${segmentKey}_timestamp`} key={`${segmentKey}_timestamp`}
@ -137,7 +137,7 @@ export function EventSegment({
const { alignDateToTimeline } = useEventUtils(events, segmentDuration); const { alignDateToTimeline } = useEventUtils(events, segmentDuration);
const severity = useMemo( const severity = useMemo(
() => getSeverity(segmentTime), () => getSeverity(segmentTime, displaySeverityType),
[getSeverity, segmentTime] [getSeverity, segmentTime]
); );
const reviewed = useMemo( const reviewed = useMemo(
@ -195,13 +195,13 @@ export function EventSegment({
const severityColors: { [key: number]: string } = { const severityColors: { [key: number]: string } = {
1: reviewed 1: reviewed
? "from-severity_motion-dimmed/30 to-severity_motion/30" ? "from-severity_motion-dimmed/50 to-severity_motion/50"
: "from-severity_motion-dimmed to-severity_motion", : "from-severity_motion-dimmed to-severity_motion",
2: reviewed 2: reviewed
? "from-severity_detection-dimmed/30 to-severity_detection/30" ? "from-severity_detection-dimmed/50 to-severity_detection/50"
: "from-severity_detection-dimmed to-severity_detection", : "from-severity_detection-dimmed to-severity_detection",
3: reviewed 3: reviewed
? "from-severity_alert-dimmed/30 to-severity_alert/30" ? "from-severity_alert-dimmed/50 to-severity_alert/50"
: "from-severity_alert-dimmed to-severity_alert", : "from-severity_alert-dimmed to-severity_alert",
}; };
@ -229,35 +229,40 @@ export function EventSegment({
segmentKey={segmentKey} segmentKey={segmentKey}
/> />
{severity == displaySeverityType && ( {severity.map((severityValue, index) => (
<div className="mr-3 w-2 h-2 flex justify-left items-end"> <React.Fragment key={index}>
{severityValue === displaySeverityType && (
<div <div
key={`${segmentKey}_primary_data`} className="mr-3 w-[8px] h-2 flex justify-left items-end"
data-severity={severityValue}
>
<div
key={`${segmentKey}_${index}_primary_data`}
className={` className={`
w-full h-2 bg-gradient-to-r w-full h-2 bg-gradient-to-r
${roundBottom ? "rounded-bl-full rounded-br-full" : ""} ${roundBottom ? "rounded-bl-full rounded-br-full" : ""}
${roundTop ? "rounded-tl-full rounded-tr-full" : ""} ${roundTop ? "rounded-tl-full rounded-tr-full" : ""}
${severityColors[severity]} ${severityColors[severityValue]}
`} `}
></div> ></div>
</div> </div>
)} )}
{severity != displaySeverityType && ( {severityValue !== displaySeverityType && (
<div className="h-2 flex flex-grow justify-end items-end"> <div className="h-2 flex flex-grow justify-end items-end">
<div <div
key={`${segmentKey}_secondary_data`} key={`${segmentKey}_${index}_secondary_data`}
className={` className={`
w-1 h-2 bg-gradient-to-r w-1 h-2 bg-gradient-to-r
${roundBottom ? "rounded-bl-full rounded-br-full" : ""} ${roundBottom ? "rounded-bl-full rounded-br-full" : ""}
${roundTop ? "rounded-tl-full rounded-tr-full" : ""} ${roundTop ? "rounded-tl-full rounded-tr-full" : ""}
${severityColors[severity]} ${severityColors[severityValue]}
`} `}
></div> ></div>
</div> </div>
)} )}
</React.Fragment>
))}
</div> </div>
); );
} }

View File

@ -1,37 +1,61 @@
import { useCallback } from 'react'; import { useCallback } from "react";
import { ReviewSegment } from '@/types/review'; import { ReviewSegment } from "@/types/review";
export const useEventUtils = (events: ReviewSegment[], segmentDuration: number) => { export const useEventUtils = (
const isStartOfEvent = useCallback((time: number): boolean => { events: ReviewSegment[],
segmentDuration: number
) => {
const isStartOfEvent = useCallback(
(time: number): boolean => {
return events.some((event) => { return events.some((event) => {
const segmentStart = getSegmentStart(event.start_time); const segmentStart = getSegmentStart(event.start_time);
return time >= segmentStart && time < segmentStart + segmentDuration; return time >= segmentStart && time < segmentStart + segmentDuration;
}); });
}, [events, segmentDuration]); },
[events, segmentDuration]
);
const isEndOfEvent = useCallback((time: number): boolean => { const isEndOfEvent = useCallback(
(time: number): boolean => {
return events.some((event) => { return events.some((event) => {
if (typeof event.end_time === 'number') { if (typeof event.end_time === "number") {
const segmentEnd = getSegmentEnd(event.end_time); const segmentEnd = getSegmentEnd(event.end_time);
return time >= segmentEnd - segmentDuration && time < segmentEnd; return time >= segmentEnd - segmentDuration && time < segmentEnd;
} }
return false; // Return false if end_time is undefined return false;
}); });
}, [events, segmentDuration]); },
[events, segmentDuration]
);
const getSegmentStart = useCallback((time: number): number => { const getSegmentStart = useCallback(
return Math.floor(time / (segmentDuration)) * (segmentDuration); (time: number): number => {
}, [segmentDuration]); return Math.floor(time / segmentDuration) * segmentDuration;
},
[segmentDuration]
);
const getSegmentEnd = useCallback((time: number): number => { const getSegmentEnd = useCallback(
return Math.ceil(time / (segmentDuration)) * (segmentDuration); (time: number): number => {
}, [segmentDuration]); return Math.ceil(time / segmentDuration) * segmentDuration;
},
[segmentDuration]
);
const alignDateToTimeline = useCallback((time: number): number => { const alignDateToTimeline = useCallback(
const remainder = time % (segmentDuration); (time: number): number => {
const remainder = time % segmentDuration;
const adjustment = remainder !== 0 ? segmentDuration - remainder : 0; const adjustment = remainder !== 0 ? segmentDuration - remainder : 0;
return time + adjustment; return time + adjustment;
}, [segmentDuration]); },
[segmentDuration]
);
return { isStartOfEvent, isEndOfEvent, getSegmentStart, getSegmentEnd, alignDateToTimeline }; return {
isStartOfEvent,
isEndOfEvent,
getSegmentStart,
getSegmentEnd,
alignDateToTimeline,
};
}; };

View File

@ -50,7 +50,11 @@ function useDraggableHandler({
const handleMouseMove = useCallback( const handleMouseMove = useCallback(
(e: MouseEvent) => { (e: MouseEvent) => {
if (!contentRef.current || !timelineRef.current || !scrollTimeRef.current) { if (
!contentRef.current ||
!timelineRef.current ||
!scrollTimeRef.current
) {
return; return;
} }
@ -68,9 +72,7 @@ function useDraggableHandler({
const segmentHeight = const segmentHeight =
timelineHeight / (timelineDuration / segmentDuration); timelineHeight / (timelineDuration / segmentDuration);
const getCumulativeScrollTop = ( const getCumulativeScrollTop = (element: HTMLElement | null) => {
element: HTMLElement | null
) => {
let scrollTop = 0; let scrollTop = 0;
while (element) { while (element) {
scrollTop += element.scrollTop; scrollTop += element.scrollTop;

View File

@ -1,22 +1,30 @@
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from "react";
import { ReviewSegment } from '@/types/review'; import { ReviewSegment } from "@/types/review";
export const useSegmentUtils = ( export const useSegmentUtils = (
segmentDuration: number, segmentDuration: number,
events: ReviewSegment[], events: ReviewSegment[],
severityType: string, severityType: string
) => { ) => {
const getSegmentStart = useCallback((time: number): number => { const getSegmentStart = useCallback(
return Math.floor(time / (segmentDuration)) * (segmentDuration); (time: number): number => {
}, [segmentDuration]); return Math.floor(time / segmentDuration) * segmentDuration;
},
[segmentDuration]
);
const getSegmentEnd = useCallback((time: number | undefined): number => { const getSegmentEnd = useCallback(
(time: number | undefined): number => {
if (time) { if (time) {
return Math.ceil(time / (segmentDuration)) * (segmentDuration); return (
Math.floor(time / segmentDuration) * segmentDuration + segmentDuration
);
} else { } else {
return (Date.now()/1000)+(segmentDuration); return Date.now() / 1000 + segmentDuration;
} }
}, [segmentDuration]); },
[segmentDuration]
);
const mapSeverityToNumber = useCallback((severity: string): number => { const mapSeverityToNumber = useCallback((severity: string): number => {
switch (severity) { switch (severity) {
@ -36,20 +44,34 @@ export const useSegmentUtils = (
[severityType] [severityType]
); );
const getSeverity = useCallback((time: number): number => { const getSeverity = useCallback(
(time: number, displaySeverityType: number): number[] => {
const activeEvents = events?.filter((event) => { const activeEvents = events?.filter((event) => {
const segmentStart = getSegmentStart(event.start_time); const segmentStart = getSegmentStart(event.start_time);
const segmentEnd = getSegmentEnd(event.end_time); const segmentEnd = getSegmentEnd(event.end_time);
return time >= segmentStart && time < segmentEnd; return time >= segmentStart && time < segmentEnd;
}); });
if (activeEvents?.length === 0) return 0; // No event at this time
const severityValues = activeEvents?.map((event) => if (activeEvents?.length === 0) return [0];
const severityValues = activeEvents.map((event) =>
mapSeverityToNumber(event.severity) mapSeverityToNumber(event.severity)
); );
return Math.max(...severityValues); const highestSeverityValue = Math.max(...severityValues);
}, [events, getSegmentStart, getSegmentEnd, mapSeverityToNumber]);
const getReviewed = useCallback((time: number): boolean => { if (
severityValues.includes(displaySeverityType) &&
displaySeverityType !== highestSeverityValue
) {
return [displaySeverityType, highestSeverityValue];
} else {
return [highestSeverityValue];
}
},
[events, getSegmentStart, getSegmentEnd, mapSeverityToNumber]
);
const getReviewed = useCallback(
(time: number): boolean => {
return events.some((event) => { return events.some((event) => {
const segmentStart = getSegmentStart(event.start_time); const segmentStart = getSegmentStart(event.start_time);
const segmentEnd = getSegmentEnd(event.end_time); const segmentEnd = getSegmentEnd(event.end_time);
@ -57,54 +79,59 @@ export const useSegmentUtils = (
time >= segmentStart && time < segmentEnd && event.has_been_reviewed time >= segmentStart && time < segmentEnd && event.has_been_reviewed
); );
}); });
}, [events, getSegmentStart, getSegmentEnd]); },
[events, getSegmentStart, getSegmentEnd]
);
const shouldShowRoundedCorners = useCallback( const shouldShowRoundedCorners = useCallback(
(segmentTime: number): { roundTop: boolean, roundBottom: boolean } => { (segmentTime: number): { roundTop: boolean; roundBottom: boolean } => {
const prevSegmentTime = segmentTime - segmentDuration; const prevSegmentTime = segmentTime - segmentDuration;
const nextSegmentTime = segmentTime + segmentDuration; const nextSegmentTime = segmentTime + segmentDuration;
const severityEvents = events.filter(e => e.severity === severityType); const severityEvents = events.filter((e) => e.severity === severityType);
const otherEvents = events.filter(e => e.severity !== severityType); const otherEvents = events.filter((e) => e.severity !== severityType);
const hasPrevSeverityEvent = severityEvents.some(e => { const hasPrevSeverityEvent = severityEvents.some((e) => {
return ( return (
prevSegmentTime >= getSegmentStart(e.start_time) && prevSegmentTime >= getSegmentStart(e.start_time) &&
prevSegmentTime < getSegmentEnd(e.end_time) prevSegmentTime < getSegmentEnd(e.end_time)
); );
}); });
const hasNextSeverityEvent = severityEvents.some(e => { const hasNextSeverityEvent = severityEvents.some((e) => {
return ( return (
nextSegmentTime >= getSegmentStart(e.start_time) && nextSegmentTime >= getSegmentStart(e.start_time) &&
nextSegmentTime < getSegmentEnd(e.end_time) nextSegmentTime < getSegmentEnd(e.end_time)
); );
}); });
const hasPrevOtherEvent = otherEvents.some(e => { const hasPrevOtherEvent = otherEvents.some((e) => {
return ( return (
prevSegmentTime >= getSegmentStart(e.start_time) && prevSegmentTime >= getSegmentStart(e.start_time) &&
prevSegmentTime < getSegmentEnd(e.end_time) prevSegmentTime < getSegmentEnd(e.end_time)
); );
}); });
const hasNextOtherEvent = otherEvents.some(e => { const hasNextOtherEvent = otherEvents.some((e) => {
return ( return (
nextSegmentTime >= getSegmentStart(e.start_time) && nextSegmentTime >= getSegmentStart(e.start_time) &&
nextSegmentTime < getSegmentEnd(e.end_time) nextSegmentTime < getSegmentEnd(e.end_time)
); );
}); });
const hasOverlappingSeverityEvent = severityEvents.some(e => { const hasOverlappingSeverityEvent = severityEvents.some((e) => {
return segmentTime >= getSegmentStart(e.start_time) && return (
segmentTime >= getSegmentStart(e.start_time) &&
segmentTime < getSegmentEnd(e.end_time) segmentTime < getSegmentEnd(e.end_time)
);
}); });
const hasOverlappingOtherEvent = otherEvents.some(e => { const hasOverlappingOtherEvent = otherEvents.some((e) => {
return segmentTime >= getSegmentStart(e.start_time) && return (
segmentTime >= getSegmentStart(e.start_time) &&
segmentTime < getSegmentEnd(e.end_time) segmentTime < getSegmentEnd(e.end_time)
);
}); });
let roundTop = false; let roundTop = false;
@ -123,12 +150,18 @@ export const useSegmentUtils = (
return { return {
roundTop, roundTop,
roundBottom roundBottom,
}; };
}, },
[events, getSegmentStart, getSegmentEnd, segmentDuration, severityType] [events, getSegmentStart, getSegmentEnd, segmentDuration, severityType]
); );
return { getSegmentStart, getSegmentEnd, getSeverity, displaySeverityType, getReviewed, shouldShowRoundedCorners }; return {
getSegmentStart,
getSegmentEnd,
getSeverity,
displaySeverityType,
getReviewed,
shouldShowRoundedCorners,
};
}; };

View File

@ -188,7 +188,7 @@ function UIPlayground() {
segmentDuration={60} // seconds per segment segmentDuration={60} // seconds per segment
timestampSpread={15} // minutes between each major timestamp timestampSpread={15} // minutes between each major timestamp
timelineStart={Math.floor(Date.now() / 1000)} // start of the timeline - all times are numeric, not Date objects timelineStart={Math.floor(Date.now() / 1000)} // start of the timeline - all times are numeric, not Date objects
timelineDuration={24 * 60 * 60} // in minutes, defaults to 24 hours timelineEnd={Math.floor(Date.now() / 1000) + 2 * 60 * 60} // end of timeline - timestamp
showHandlebar // show / hide the handlebar showHandlebar // show / hide the handlebar
handlebarTime={Math.floor(Date.now() / 1000) - 27 * 60} // set the time of the handlebar handlebarTime={Math.floor(Date.now() / 1000) - 27 * 60} // set the time of the handlebar
showMinimap // show / hide the minimap showMinimap // show / hide the minimap