timeline scrubber and revamp for all event handlers

This commit is contained in:
Josh Hawkins 2023-12-20 17:07:09 -06:00
parent 9167130896
commit 3c498fc087
4 changed files with 227 additions and 85 deletions

View File

@ -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[]>([
"timeline",
{
source_id: eventID,
},
]);
return (
<>
{eventTimeline && (
<>
<ActivityScrubber
items={timelineEventsToScrubberItems(eventTimeline)}
options={generateScrubberOptions(eventTimeline)}
/>
</>
)}
</>
);
}
export default TimelineScrubber;

View File

@ -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<TimelineEventHandler, EventHandler>
>;
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<HTMLDivElement>(null);
const timelineRef = useRef<Timeline | null>(null);
...eventHandlers
}: ActivityScrubberProps) {
const containerRef = useRef<HTMLDivElement>(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 <div ref={container}></div>;
// 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 <div ref={containerRef} />;
}
export default ActivityScrubber;

View File

@ -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];

View File

@ -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: `<div class="flex"><img class="" src="${apiHost}api/events/${event.id}/thumbnail.jpg" /><span>${event.label}</span></div>`,
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<FrigateConfig>("config");
const [timeline, setTimeline] = useState<string | undefined>(undefined);
const onSelect = useCallback(({ items }: { items: string[] }) => {
setTimeline(items[0]);
}, []);
const recentTimestamp = useMemo(() => {
const now = new Date();
@ -93,13 +94,23 @@ function UIPlayground() {
<>
<ActivityScrubber
items={eventsToScrubberItems(events)}
onSelect={onSelect}
selectHandler={onSelect}
/>
</>
)}
</div>
)}
{config && (
<div>
{timeline && (
<>
<TimelineScrubber eventID={timeline} />
</>
)}
</div>
)}
<Heading as="h4" className="my-5">
Color scheme
</Heading>