import { useCallback, useMemo, useState } from "react"; import useSWR from "swr"; import useSWRInfinite from "swr/infinite"; import { FrigateConfig } from "@/types/frigateConfig"; import Heading from "@/components/ui/heading"; import ActivityIndicator from "@/components/ui/activity-indicator"; import axios from "axios"; import { getHourlyTimelineData } from "@/utils/historyUtil"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import HistoryFilterPopover from "@/components/filter/HistoryFilterPopover"; import useApiFilter from "@/hooks/use-api-filter"; import HistoryCardView from "@/views/history/HistoryCardView"; import HistoryTimelineView from "@/views/history/HistoryTimelineView"; import { Button } from "@/components/ui/button"; import { IoMdArrowBack } from "react-icons/io"; import useOverlayState from "@/hooks/use-overlay-state"; import { useNavigate } from "react-router-dom"; import { Dialog, DialogContent } from "@/components/ui/dialog"; const API_LIMIT = 200; function History() { const { data: config } = useSWR("config"); const timezone = useMemo( () => config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, [config] ); const [historyFilter, setHistoryFilter, historySearchParams] = useApiFilter(); const timelineFetcher = useCallback((key: any) => { const [path, params] = Array.isArray(key) ? key : [key, undefined]; return axios.get(path, { params }).then((res) => res.data); }, []); const getKey = useCallback( (index: number, prevData: HourlyTimeline) => { if (index > 0) { const lastDate = prevData.end; const pagedParams = historySearchParams == undefined ? { before: lastDate, timezone, limit: API_LIMIT } : { ...historySearchParams, before: lastDate, timezone, limit: API_LIMIT, }; return ["timeline/hourly", pagedParams]; } const params = historySearchParams == undefined ? { timezone, limit: API_LIMIT } : { ...historySearchParams, timezone, limit: API_LIMIT }; return ["timeline/hourly", params]; }, [historySearchParams] ); const { data: timelinePages, mutate: updateHistory, size, setSize, isValidating, } = useSWRInfinite(getKey, timelineFetcher); const { data: allPreviews } = useSWR( timelinePages ? `preview/all/start/${timelinePages?.at(0) ?.start}/end/${timelinePages?.at(-1)?.end}` : null, { revalidateOnFocus: false } ); const navigate = useNavigate(); const [playback, setPlayback] = useState(); const [viewingPlayback, setViewingPlayback] = useOverlayState("timeline"); const setPlaybackState = useCallback( (playback: TimelinePlayback | undefined) => { if (playback == undefined) { setPlayback(undefined); navigate(-1); } else { setPlayback(playback); setViewingPlayback(true); } }, [navigate] ); const isMobile = useMemo(() => { return window.innerWidth < 768; }, [playback]); const timelineCards: CardsData | never[] = useMemo(() => { if (!timelinePages) { return []; } return getHourlyTimelineData( timelinePages, historyFilter?.detailLevel ?? "normal" ); }, [historyFilter, timelinePages]); const isDone = (timelinePages?.[timelinePages.length - 1]?.count ?? 0) < API_LIMIT; const [itemsToDelete, setItemsToDelete] = useState(null); const onDelete = useCallback( async (timeline: Card) => { if (timeline.entries.length > 1) { const uniqueEvents = new Set( timeline.entries.map((entry) => entry.source_id) ); setItemsToDelete(new Array(...uniqueEvents)); } else { const response = await axios.delete( `events/${timeline.entries[0].source_id}` ); if (response.status === 200) { updateHistory(); } } }, [updateHistory] ); const onDeleteMulti = useCallback(async () => { if (!itemsToDelete) { return; } const responses = itemsToDelete.map(async (id) => { return axios.delete(`events/${id}`); }); if ((await responses[0]).status == 200) { updateHistory(); setItemsToDelete(null); } }, [itemsToDelete, updateHistory]); if (!config || !timelineCards || timelineCards.length == 0) { return ; } return ( <>
{viewingPlayback && ( )} History
{!playback && ( setHistoryFilter(filter)} /> )}
setItemsToDelete(null)} > {`Delete ${itemsToDelete?.length} events?`} This will delete all events associated with these objects. setItemsToDelete(null)}> Cancel onDeleteMulti()} > Delete { setSize(size + 1); }} onDelete={onDelete} onItemSelected={(item) => setPlaybackState(item)} /> setPlaybackState(undefined)} /> ); } type TimelineViewerProps = { playback: TimelinePlayback | undefined; isMobile: boolean; onClose: () => void; }; function TimelineViewer({ playback, isMobile, onClose }: TimelineViewerProps) { if (isMobile) { return playback != undefined ? (
) : null; } return ( onClose()}> {playback && ( )} ); } export default History;