Show recent events on dashboard

This commit is contained in:
Nick Mowen 2023-12-16 07:29:07 -07:00
parent 9f00dccdba
commit 182d0f13e8
4 changed files with 227 additions and 3 deletions

View File

@ -0,0 +1,83 @@
import { useApiHost } from "@/api";
import { Card } from "../ui/card";
import { Event as FrigateEvent } from "@/types/event";
import { LuClock, LuStar } from "react-icons/lu";
import { useCallback } from "react";
import TimeAgo from "../dynamic/TimeAgo";
import { HiOutlineVideoCamera } from "react-icons/hi";
import { MdOutlineLocationOn } from "react-icons/md";
import axios from "axios";
type MiniEventCardProps = {
event: FrigateEvent;
onUpdate?: () => void;
};
export default function MiniEventCard({ event, onUpdate }: MiniEventCardProps) {
const baseUrl = useApiHost();
const onSave = useCallback(
async (e: Event) => {
e.stopPropagation();
let response;
if (!event.retain_indefinitely) {
response = await axios.post(`events/${event.id}/retain`);
} else {
response = await axios.delete(`events/${event.id}/retain`);
}
if (response.status === 200 && onUpdate) {
onUpdate();
}
},
[event]
);
return (
<Card className="mr-2 min-w-[260px] max-w-[320px]">
<div className="flex">
<div
className="relative rounded-l min-w-[125px] h-[125px] bg-contain bg-no-repeat bg-center"
style={{
backgroundImage: `url(${baseUrl}api/events/${event.id}/thumbnail.jpg)`,
}}
>
<LuStar
className="h-6 w-6 text-yellow-300 absolute top-1 right-1 cursor-pointer"
onClick={(e: Event) => onSave(e)}
fill={event.retain_indefinitely ? "currentColor" : "none"}
/>
{event.end_time ? null : (
<div className="bg-slate-300 dark:bg-slate-700 absolute bottom-0 text-center w-full uppercase text-sm rounded-bl">
In progress
</div>
)}
</div>
<div className="p-1 flex flex-col justify-between">
<div className="capitalize text-lg font-bold">
{event.label.replaceAll("_", " ")}
{event.sub_label
? `: ${event.sub_label.replaceAll("_", " ")}`
: null}
</div>
<div>
<div className="text-sm flex">
<LuClock className="h-4 w-4 mr-2 inline" />
<div className="hidden sm:inline">
<TimeAgo time={event.start_time * 1000} dense />
</div>
</div>
<div className="capitalize text-sm flex align-center mt-1 whitespace-nowrap">
<HiOutlineVideoCamera className="h-4 w-4 mr-2 inline" />
{event.camera.replaceAll("_", " ")}
</div>
{event.zones.length ? (
<div className="capitalize text-sm flex align-center">
<MdOutlineLocationOn className="w-4 h-4 mr-2 inline" />
{event.zones.join(", ").replaceAll("_", " ")}
</div>
) : null}
</div>
</div>
</div>
</Card>
);
}

View File

@ -0,0 +1,83 @@
import { FunctionComponent, useEffect, useMemo, useState } from "react";
interface IProp {
/** The time to calculate time-ago from */
time: number;
/** OPTIONAL: overwrite current time */
currentTime?: Date;
/** OPTIONAL: boolean that determines whether to show the time-ago text in dense format */
dense?: boolean;
/** OPTIONAL: set custom refresh interval in milliseconds, default 1000 (1 sec) */
refreshInterval?: number;
}
type TimeUnit = {
unit: string;
full: string;
value: number;
};
const timeAgo = ({ time, currentTime = new Date(), dense = false }: IProp): string => {
if (typeof time !== 'number' || time < 0) return 'Invalid Time Provided';
const pastTime: Date = new Date(time);
const elapsedTime: number = currentTime.getTime() - pastTime.getTime();
const timeUnits: TimeUnit[] = [
{ unit: 'yr', full: 'year', value: 31536000 },
{ unit: 'mo', full: 'month', value: 0 },
{ unit: 'd', full: 'day', value: 86400 },
{ unit: 'h', full: 'hour', value: 3600 },
{ unit: 'm', full: 'minute', value: 60 },
{ unit: 's', full: 'second', value: 1 },
];
const elapsed: number = elapsedTime / 1000;
if (elapsed < 10) {
return 'just now';
}
for (let i = 0; i < timeUnits.length; i++) {
// if months
if (i === 1) {
// Get the month and year for the time provided
const pastMonth = pastTime.getUTCMonth();
const pastYear = pastTime.getUTCFullYear();
// get current month and year
const currentMonth = currentTime.getUTCMonth();
const currentYear = currentTime.getUTCFullYear();
let monthDiff = (currentYear - pastYear) * 12 + (currentMonth - pastMonth);
// check if the time provided is the previous month but not exceeded 1 month ago.
if (currentTime.getUTCDate() < pastTime.getUTCDate()) {
monthDiff--;
}
if (monthDiff > 0) {
const unitAmount = monthDiff;
return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`;
}
} else if (elapsed >= timeUnits[i].value) {
const unitAmount: number = Math.floor(elapsed / timeUnits[i].value);
return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`;
}
}
return 'Invalid Time';
};
const TimeAgo: FunctionComponent<IProp> = ({ refreshInterval = 1000, ...rest }): JSX.Element => {
const [currentTime, setCurrentTime] = useState<Date>(new Date());
useEffect(() => {
const intervalId: NodeJS.Timeout = setInterval(() => {
setCurrentTime(new Date());
}, refreshInterval);
return () => clearInterval(intervalId);
}, [refreshInterval]);
const timeAgoValue = useMemo(() => timeAgo({ currentTime, ...rest }), [currentTime, rest]);
return <span>{timeAgoValue}</span>;
};
export default TimeAgo;

View File

@ -17,10 +17,23 @@ import { AiOutlinePicture } from "react-icons/ai";
import { FaWalking } from "react-icons/fa"; import { FaWalking } from "react-icons/fa";
import { LuEar } from "react-icons/lu"; import { LuEar } from "react-icons/lu";
import { TbMovie } from "react-icons/tb"; import { TbMovie } from "react-icons/tb";
import MiniEventCard from "@/components/card/MiniEventCard";
import { Event } from "@/types/event";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
export function Dashboard() { export function Dashboard() {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const recentTimestamp = useMemo(() => {
const now = new Date();
now.setMinutes(now.getMinutes() - 30);
return now.getTime() / 1000;
}, []);
const { data: events, mutate: updateEvents } = useSWR<Event[]>([
"events",
{ limit: 10, after: recentTimestamp },
]);
const sortedCameras = useMemo(() => { const sortedCameras = useMemo(() => {
if (!config) { if (!config) {
return []; return [];
@ -39,7 +52,27 @@ export function Dashboard() {
{config && ( {config && (
<div> <div>
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4"> {events && events.length > 0 && (
<>
<Heading as="h4">Recent Events</Heading>
<ScrollArea>
<div className="flex">
{events.map((event) => {
return (
<MiniEventCard
key={event.id}
event={event}
onUpdate={() => updateEvents()}
/>
);
})}
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</>
)}
<Heading as="h4">Cameras</Heading>
<div className="mt-2 grid gap-2 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
{sortedCameras.map((camera) => { {sortedCameras.map((camera) => {
return <Camera key={camera.name} camera={camera} />; return <Camera key={camera.name} camera={camera} />;
})} })}
@ -73,9 +106,9 @@ function Camera({ camera }: { camera: CameraConfig }) {
<CameraImage camera={camera.name} fitAspect={16 / 9} /> <CameraImage camera={camera.name} fitAspect={16 / 9} />
</AspectRatio> </AspectRatio>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<Heading className="capitalize p-2" as="h4"> <div className="text-lg capitalize p-2">
{camera.name.replaceAll("_", " ")} {camera.name.replaceAll("_", " ")}
</Heading> </div>
<div> <div>
<Button <Button
variant="ghost" variant="ghost"

View File

@ -0,0 +1,25 @@
export interface Event {
id: string;
label: string;
sub_label?: string;
camera: string;
start_time: number;
end_time?: number;
false_positive: boolean;
zones: string[];
thumbnail: string;
has_clip: boolean;
has_snapshot: boolean;
retain_indefinitely: boolean;
plus_id?: string;
model_hash?: string;
data: {
top_score: number;
score: number;
region: number[];
box: number[];
area: number;
ratio: number;
type: "object" | "audio" | "manual";
}
}