mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-26 21:59:02 +03:00
Improve display of recordings data (#16436)
* backend * frontend * add earliest recording available to storage metrics page
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user