From 3c498fc087580d0885e84f525ba8f9b18c969353 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 20 Dec 2023 17:07:09 -0600 Subject: [PATCH] timeline scrubber and revamp for all event handlers --- .../playground/TimelineScrubber.tsx | 46 ++++ .../components/scrubber/ActivityScrubber.tsx | 206 +++++++++++++----- web/src/components/scrubber/scrubber.css | 13 +- web/src/pages/UIPlayground.tsx | 47 ++-- 4 files changed, 227 insertions(+), 85 deletions(-) create mode 100644 web/src/components/playground/TimelineScrubber.tsx diff --git a/web/src/components/playground/TimelineScrubber.tsx b/web/src/components/playground/TimelineScrubber.tsx new file mode 100644 index 000000000..44bd17eac --- /dev/null +++ b/web/src/components/playground/TimelineScrubber.tsx @@ -0,0 +1,46 @@ +import useSWR from "swr"; +import ActivityScrubber, { ScrubberItem } from "../scrubber/ActivityScrubber"; + +type TimelineScrubberProps = { + eventID: string; +}; + +function timelineEventsToScrubberItems(events: Timeline[]): ScrubberItem[] { + return events.map((event: Timeline, index: number) => ({ + id: index, + content: event.class_type, + start: event.timestamp * 1000, + type: "box", + })); +} + +function generateScrubberOptions(events: Timeline[]) { + const startTime = events[0].timestamp * 1000 - 10; + const endTime = events[events.length - 1].timestamp * 1000 + 10; + + return { start: startTime, end: endTime }; +} + +function TimelineScrubber({ eventID }: TimelineScrubberProps) { + const { data: eventTimeline } = useSWR([ + "timeline", + { + source_id: eventID, + }, + ]); + + return ( + <> + {eventTimeline && ( + <> + + + )} + + ); +} + +export default TimelineScrubber; diff --git a/web/src/components/scrubber/ActivityScrubber.tsx b/web/src/components/scrubber/ActivityScrubber.tsx index bf67ae8f5..31c9db6ed 100644 --- a/web/src/components/scrubber/ActivityScrubber.tsx +++ b/web/src/components/scrubber/ActivityScrubber.tsx @@ -1,39 +1,116 @@ -import { useRef, useEffect, useState } from "react"; -import { Timeline } from "vis-timeline"; -import { TimelineOptions } from "vis-timeline/standalone"; +import { useEffect, useRef, useState } from "react"; +import { + Timeline as VisTimeline, + TimelineGroup, + TimelineItem, + TimelineOptions, +} from "vis-timeline"; +import type { DataGroup, DataItem, TimelineEvents } from "vis-timeline/types"; import "./scrubber.css"; -export type ScrubberItem = { - id: string; - content: string; - start: Date; - end?: Date; - type?: "box" | "point"; +export type TimelineEventsWithMissing = + | TimelineEvents + | "dragover" + | "markerchange" + | "markerchanged"; + +export type TimelineEventHandler = + | "currentTimeTickHandler" + | "clickHandler" + | "contextmenuHandler" + | "doubleClickHandler" + | "dragoverHandler" + | "dropHandler" + | "mouseOverHandler" + | "mouseDownHandler" + | "mouseUpHandler" + | "mouseMoveHandler" + | "groupDraggedHandler" + | "changedHandler" + | "rangechangeHandler" + | "rangechangedHandler" + | "selectHandler" + | "itemoverHandler" + | "itemoutHandler" + | "timechangeHandler" + | "timechangedHandler" + | "markerchangeHandler" + | "markerchangedHandler"; + +type EventHandler = { + (properties: any): void; }; -export type ScrubberSelectProps = { - nodes: ScrubberItem[]; -}; +export type TimelineEventsHandlers = Partial< + Record +>; -type ScrubberChartProps = { - items: ScrubberItem[]; +export type ScrubberItem = TimelineItem; + +const domEvents: TimelineEventsWithMissing[] = [ + "currentTimeTick", + "click", + "contextmenu", + "doubleClick", + "dragover", + "drop", + "mouseOver", + "mouseDown", + "mouseUp", + "mouseMove", + "groupDragged", + "changed", + "rangechange", + "rangechanged", + "select", + "itemover", + "itemout", + "timechange", + "timechanged", + "markerchange", + "markerchanged", +]; + +type ActivityScrubberProps = { + items: TimelineItem[]; + groups?: TimelineGroup[]; options?: TimelineOptions; - onSelect?: (props: ScrubberSelectProps) => void; -}; +} & TimelineEventsHandlers; -export function ActivityScrubber({ +function ActivityScrubber({ items, + groups, options, - onSelect, -}: ScrubberChartProps) { - const container = useRef(null); - const timelineRef = useRef(null); + ...eventHandlers +}: ActivityScrubberProps) { + const containerRef = useRef(null); + const timelineRef = useRef<{ timeline: VisTimeline | null }>({ + timeline: null, + }); const [currentTime, setCurrentTime] = useState(Date.now()); + const defaultOptions: TimelineOptions = { + width: "100%", + maxHeight: "350px", + stack: true, + showMajorLabels: true, + showCurrentTime: false, + zoomMin: 10 * 1000, // 10 seconds + // start: new Date(currentTime - 60 * 1 * 60 * 1000), // 1 hour ago + end: currentTime, + max: currentTime, + format: { + minorLabels: { + minute: "h:mma", + hour: "ha", + }, + }, + }; + useEffect(() => { const intervalId = setInterval(() => { setCurrentTime(Date.now()); - }, 10000); + }, 60000); // Update every minute return () => { clearInterval(intervalId); @@ -41,54 +118,63 @@ export function ActivityScrubber({ }, []); useEffect(() => { - const defaultOptions: TimelineOptions = { - width: "100%", - stack: true, - showMajorLabels: true, - showCurrentTime: true, - zoomMin: 10 * 1000, // 10 seconds - end: currentTime, - max: currentTime, - format: { - minorLabels: { - minute: "h:mma", - hour: "ha", - }, - }, - }; + const divElement = containerRef.current; + if (!divElement) { + return; + } + + const timelineInstance = new VisTimeline( + divElement, + items as DataItem[], + groups as DataGroup[], + options + ); + + domEvents.forEach((event) => { + const eventHandler = eventHandlers[`${event}Handler`]; + if (typeof eventHandler === "function") { + timelineInstance.on(event, eventHandler); + } + }); + + timelineRef.current.timeline = timelineInstance; const timelineOptions: TimelineOptions = { ...defaultOptions, ...options, }; - if (!timelineRef.current) { - timelineRef.current = new Timeline( - container.current as HTMLDivElement, - items, - timelineOptions - ); + timelineInstance.setOptions(timelineOptions); - const updateRange = () => { - timelineRef.current?.setOptions(timelineOptions); - }; + return () => { + timelineInstance.destroy(); + }; + }, []); - timelineRef.current.on("rangechanged", updateRange); - if (onSelect) { - timelineRef.current.on("select", onSelect); - } - - return () => { - timelineRef.current?.off("rangechanged", updateRange); - }; - } else { - // Update existing timeline - timelineRef.current.setItems(items); - timelineRef.current.setOptions(timelineOptions); + useEffect(() => { + if (!timelineRef.current.timeline) { + return; } - }, [items, options, currentTime]); - return
; + // If the currentTime updates, adjust the scrubber's end date and max + // May not be applicable to all scrubbers, might want to just pass this in + // for any scrubbers that we want to dynamically move based on time + // const updatedTimeOptions: TimelineOptions = { + // end: currentTime, + // max: currentTime, + // }; + + const timelineOptions: TimelineOptions = { + ...defaultOptions, + // ...updatedTimeOptions, + ...options, + }; + + timelineRef.current.timeline.setOptions(timelineOptions); + if (items) timelineRef.current.timeline.setItems(items); + }, [items, groups, options, currentTime, eventHandlers]); + + return
; } export default ActivityScrubber; diff --git a/web/src/components/scrubber/scrubber.css b/web/src/components/scrubber/scrubber.css index 00536ee49..9a97ac8fa 100644 --- a/web/src/components/scrubber/scrubber.css +++ b/web/src/components/scrubber/scrubber.css @@ -14,10 +14,10 @@ @apply absolute invisible mx-0 px-0; } .vis-time-axis .vis-grid.vis-vertical { - @apply absolute border-l border-solid; + @apply absolute border-l border-dashed border-muted-foreground; } .vis-time-axis .vis-grid.vis-vertical-rtl { - @apply absolute border-r border-solid; + @apply absolute border-r border-dashed border-muted-foreground; } .vis-time-axis .vis-grid.vis-minor { @apply border-foreground; @@ -170,16 +170,16 @@ @apply min-h-0 w-auto; } .vis-item { - @apply bg-[#d5ddf6] border text-[#1a1a1a] inline-block absolute z-[1] border-[#97b0f8]; + @apply bg-accent border text-foreground inline-block absolute z-[1] border-border; } .vis-item.vis-selected { - @apply bg-[#fff785] z-[2] border-[#ffc200]; + @apply bg-muted-foreground z-[2] border-muted text-muted; } .vis-editable.vis-selected { @apply cursor-move; } .vis-item.vis-point.vis-selected { - @apply bg-[#fff785]; + @apply bg-muted-foreground; } .vis-item.vis-box { @apply text-center rounded-sm border-solid; @@ -209,8 +209,7 @@ @apply inline-block absolute; } .vis-item.vis-line { - @apply absolute w-0 p-0 border-l; - border-left-style: solid; + @apply absolute w-0 p-0 border-l border-solid text-muted; } .vis-item .vis-item-content { @apply box-border whitespace-nowrap p-[5px]; diff --git a/web/src/pages/UIPlayground.tsx b/web/src/pages/UIPlayground.tsx index 8fe890ae2..1359f77bf 100644 --- a/web/src/pages/UIPlayground.tsx +++ b/web/src/pages/UIPlayground.tsx @@ -1,14 +1,14 @@ -import { useMemo } from "react"; +import { useCallback, useMemo, useState } from "react"; import Heading from "@/components/ui/heading"; -import { - ScrubberSelectProps, - ActivityScrubber, +import ActivityScrubber, { ScrubberItem, } from "@/components/scrubber/ActivityScrubber"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { Event } from "@/types/event"; import ActivityIndicator from "@/components/ui/activity-indicator"; +import { useApiHost } from "@/api"; +import TimelineScrubber from "@/components/playground/TimelineScrubber"; // Color data const colors = [ @@ -45,24 +45,25 @@ function ColorSwatch({ name, value }: { name: string; value: string }) { ); } -function onSelect(props: ScrubberSelectProps) { - console.log(props); -} - function eventsToScrubberItems(events: Event[]): ScrubberItem[] { - return events.map((event) => { - return { - id: event.id, - content: event.label, - start: new Date(event.start_time * 1000), - end: event.end_time ? new Date(event.end_time * 1000) : undefined, - type: "box", - }; - }); + const apiHost = useApiHost(); + + return events.map((event: Event) => ({ + id: event.id, + content: `
${event.label}
`, + start: new Date(event.start_time * 1000), + end: event.end_time ? new Date(event.end_time * 1000) : undefined, + type: "box", + })); } function UIPlayground() { const { data: config } = useSWR("config"); + const [timeline, setTimeline] = useState(undefined); + + const onSelect = useCallback(({ items }: { items: string[] }) => { + setTimeline(items[0]); + }, []); const recentTimestamp = useMemo(() => { const now = new Date(); @@ -93,13 +94,23 @@ function UIPlayground() { <> )}
)} + {config && ( +
+ {timeline && ( + <> + + + )} +
+ )} + Color scheme