Improve display of recordings data (#16436)

* backend

* frontend

* add earliest recording available to storage metrics page
This commit is contained in:
Josh Hawkins
2025-02-09 16:02:36 -07:00
committed by GitHub
parent bc96db8612
commit d5b60237a2
11 changed files with 217 additions and 39 deletions
@@ -2,7 +2,7 @@ import {
useFormattedRange,
useFormattedTimestamp,
} from "@/hooks/use-date-utils";
import { ReviewSummary } from "@/types/review";
import { RecordingsSummary, ReviewSummary } from "@/types/review";
import { Button } from "../ui/button";
import { FaCalendarAlt } from "react-icons/fa";
import ReviewActivityCalendar from "../overlay/ReviewActivityCalendar";
@@ -17,11 +17,13 @@ import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
type CalendarFilterButtonProps = {
reviewSummary?: ReviewSummary;
recordingsSummary?: RecordingsSummary;
day?: Date;
updateSelectedDay: (day?: Date) => void;
};
export default function CalendarFilterButton({
reviewSummary,
recordingsSummary,
day,
updateSelectedDay,
}: CalendarFilterButtonProps) {
@@ -52,6 +54,7 @@ export default function CalendarFilterButton({
<>
<ReviewActivityCalendar
reviewSummary={reviewSummary}
recordingsSummary={recordingsSummary}
selectedDay={day}
onSelect={updateSelectedDay}
/>
@@ -3,7 +3,12 @@ import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import { useCallback, useEffect, useMemo, useState } from "react";
import { DropdownMenuSeparator } from "../ui/dropdown-menu";
import { ReviewFilter, ReviewSeverity, ReviewSummary } from "@/types/review";
import {
RecordingsSummary,
ReviewFilter,
ReviewSeverity,
ReviewSummary,
} from "@/types/review";
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
import { FaCheckCircle, FaFilter, FaRunning } from "react-icons/fa";
import { isDesktop, isMobile } from "react-device-detect";
@@ -39,6 +44,7 @@ type ReviewFilterGroupProps = {
filters?: ReviewFilters[];
currentSeverity?: ReviewSeverity;
reviewSummary?: ReviewSummary;
recordingsSummary?: RecordingsSummary;
filter?: ReviewFilter;
motionOnly: boolean;
filterList?: FilterList;
@@ -52,6 +58,7 @@ export default function ReviewFilterGroup({
filters = DEFAULT_REVIEW_FILTERS,
currentSeverity,
reviewSummary,
recordingsSummary,
filter,
motionOnly,
filterList,
@@ -192,6 +199,7 @@ export default function ReviewFilterGroup({
{isDesktop && filters.includes("date") && (
<CalendarFilterButton
reviewSummary={reviewSummary}
recordingsSummary={recordingsSummary}
day={
filter?.after == undefined
? undefined
@@ -225,6 +233,7 @@ export default function ReviewFilterGroup({
filter={filter}
currentSeverity={currentSeverity}
reviewSummary={reviewSummary}
recordingsSummary={recordingsSummary}
allLabels={allLabels}
allZones={allZones}
onUpdateFilter={onUpdateFilter}
@@ -7,7 +7,12 @@ import { ExportContent, ExportPreviewDialog } from "./ExportDialog";
import { ExportMode, GeneralFilter } from "@/types/filter";
import ReviewActivityCalendar from "./ReviewActivityCalendar";
import { SelectSeparator } from "../ui/select";
import { ReviewFilter, ReviewSeverity, ReviewSummary } from "@/types/review";
import {
RecordingsSummary,
ReviewFilter,
ReviewSeverity,
ReviewSummary,
} from "@/types/review";
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
import { GeneralFilterContent } from "../filter/ReviewFilterGroup";
import { toast } from "sonner";
@@ -36,6 +41,7 @@ type MobileReviewSettingsDrawerProps = {
mode: ExportMode;
showExportPreview: boolean;
reviewSummary?: ReviewSummary;
recordingsSummary?: RecordingsSummary;
allLabels: string[];
allZones: string[];
onUpdateFilter: (filter: ReviewFilter) => void;
@@ -54,6 +60,7 @@ export default function MobileReviewSettingsDrawer({
mode,
showExportPreview,
reviewSummary,
recordingsSummary,
allLabels,
allZones,
onUpdateFilter,
@@ -211,6 +218,7 @@ export default function MobileReviewSettingsDrawer({
<div className="flex w-full flex-row justify-center">
<ReviewActivityCalendar
reviewSummary={reviewSummary}
recordingsSummary={recordingsSummary}
selectedDay={
filter?.after == undefined
? undefined
@@ -1,4 +1,4 @@
import { ReviewSummary } from "@/types/review";
import { RecordingsSummary, ReviewSummary } from "@/types/review";
import { Calendar } from "../ui/calendar";
import { useMemo } from "react";
import { FaCircle } from "react-icons/fa";
@@ -6,16 +6,19 @@ import { getUTCOffset } from "@/utils/dateUtil";
import { type DayContentProps } from "react-day-picker";
import { LAST_24_HOURS_KEY } from "@/types/filter";
import { usePersistence } from "@/hooks/use-persistence";
import { cn } from "@/lib/utils";
type WeekStartsOnType = 0 | 1 | 2 | 3 | 4 | 5 | 6;
type ReviewActivityCalendarProps = {
reviewSummary?: ReviewSummary;
recordingsSummary?: RecordingsSummary;
selectedDay?: Date;
onSelect: (day?: Date) => void;
};
export default function ReviewActivityCalendar({
reviewSummary,
recordingsSummary,
selectedDay,
onSelect,
}: ReviewActivityCalendarProps) {
@@ -30,39 +33,56 @@ export default function ReviewActivityCalendar({
}, []);
const modifiers = useMemo(() => {
if (!reviewSummary) {
return { alerts: [], detections: [] };
const recordings: Date[] = [];
const alerts: Date[] = [];
const detections: Date[] = [];
// Handle recordings
if (recordingsSummary) {
Object.keys(recordingsSummary).forEach((date) => {
if (date === LAST_24_HOURS_KEY) {
return;
}
const parts = date.split("-");
const cal = new Date(date);
cal.setFullYear(
parseInt(parts[0]),
parseInt(parts[1]) - 1,
parseInt(parts[2]),
);
recordings.push(cal);
});
}
const unreviewedDetections: Date[] = [];
const unreviewedAlerts: Date[] = [];
// Handle reviews if present
if (reviewSummary) {
Object.entries(reviewSummary).forEach(([date, data]) => {
if (date === LAST_24_HOURS_KEY) {
return;
}
Object.entries(reviewSummary).forEach(([date, data]) => {
if (date == LAST_24_HOURS_KEY) {
return;
}
const parts = date.split("-");
const cal = new Date(date);
const parts = date.split("-");
const cal = new Date(date);
cal.setFullYear(
parseInt(parts[0]),
parseInt(parts[1]) - 1,
parseInt(parts[2]),
);
cal.setFullYear(
parseInt(parts[0]),
parseInt(parts[1]) - 1,
parseInt(parts[2]),
);
if (data.total_alert > data.reviewed_alert) {
alerts.push(cal);
} else if (data.total_detection > data.reviewed_detection) {
detections.push(cal);
}
});
}
if (data.total_alert > data.reviewed_alert) {
unreviewedAlerts.push(cal);
} else if (data.total_detection > data.reviewed_detection) {
unreviewedDetections.push(cal);
}
});
return {
alerts: unreviewedAlerts,
detections: unreviewedDetections,
};
}, [reviewSummary]);
return { alerts, detections, recordings };
}, [reviewSummary, recordingsSummary]);
return (
<Calendar
@@ -94,13 +114,30 @@ function ReviewActivityDay({ date, activeModifiers }: DayContentProps) {
}, [activeModifiers]);
return (
<div className="flex flex-col items-center justify-center gap-0.5">
{date.getDate()}
{dayActivity != "none" && (
<FaCircle
className={`size-2 ${dayActivity == "alert" ? "fill-severity_alert" : "fill-severity_detection"}`}
/>
)}
<div className={cn("flex flex-col items-center justify-center gap-0.5")}>
<span
className={cn(
"w-4",
activeModifiers["recordings"]
? "border-b border-primary/60 text-primary"
: "text-primary/40",
activeModifiers.selected && "border-white text-white",
)}
>
{date.getDate()}
</span>
<div className="mt-0.5 flex h-2 flex-row gap-0.5">
{dayActivity != "none" && (
<FaCircle
size={6}
className={cn(
dayActivity == "alert"
? "fill-severity_alert"
: "fill-severity_detection",
)}
/>
)}
</div>
</div>
);
}