diff --git a/frigate/http.py b/frigate/http.py index 04de7371a..8b09f073d 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -2396,6 +2396,8 @@ def vod_event(id): @bp.route("/review") def review(): cameras = request.args.get("cameras", "all") + labels = request.args.get("labels", "all") + reviewed = request.args.get("reviewed", default=False) limit = request.args.get("limit", 100) severity = request.args.get("severity", None) @@ -2410,6 +2412,23 @@ def review(): camera_list = cameras.split(",") clauses.append((ReviewSegment.camera << camera_list)) + if labels != "all": + # use matching so segments with multiple labels + # still match on a search where any label matches + label_clauses = [] + filtered_labels = labels.split(",") + + for label in filtered_labels: + label_clauses.append( + (ReviewSegment.data["objects"].cast("text") % f'*"{label}"*') + ) + + label_clause = reduce(operator.or_, label_clauses) + clauses.append((label_clause)) + + if not reviewed: + clauses.append((ReviewSegment.has_been_reviewed == False)) + if severity: clauses.append((ReviewSegment.severity == severity)) diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx index 252ce3d38..3216dd409 100644 --- a/web/src/components/filter/ReviewFilterGroup.tsx +++ b/web/src/components/filter/ReviewFilterGroup.tsx @@ -8,8 +8,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuLabel, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../ui/dropdown-menu"; @@ -17,6 +15,8 @@ import { Calendar } from "../ui/calendar"; import { ReviewFilter } from "@/types/review"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; +const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"]; + type ReviewFilterGroupProps = { filter?: ReviewFilter; onUpdateFilter: (filter: ReviewFilter) => void; @@ -28,9 +28,25 @@ export default function ReviewFilterGroup({ }: ReviewFilterGroupProps) { const { data: config } = useSWR("config"); - const { data: allLabels } = useSWR(["labels"], { - revalidateOnFocus: false, - }); + const allLabels = useMemo(() => { + if (!config) { + return []; + } + + const labels = new Set(); + const cameras = filter?.cameras || Object.keys(config.cameras); + + cameras.forEach((camera) => { + config.cameras[camera].objects.track.forEach((label) => { + if (!ATTRIBUTES.includes(label)) { + labels.add(label); + } + }); + }); + + return [...labels]; + }, [config, filter]); + const filterValues = useMemo( () => ({ cameras: Object.keys(config?.cameras || {}), @@ -49,10 +65,17 @@ export default function ReviewFilterGroup({ }} /> - + { + onUpdateFilter({ ...filter, labels: newLabels }); + }} + showReviewed={filter?.showReviewed || false} + setShowReviewed={(reviewed) => + onUpdateFilter({ ...filter, showReviewed: reviewed }) + } + /> ); @@ -285,6 +308,7 @@ function CalendarFilterButton({ before, after }: CalendarFilterButtonProps) { future.setFullYear(tomorrow.getFullYear() + 10); return { from: tomorrow, to: future }; }, []); + // @ts-ignore const dateRange = useMemo(() => { return before == undefined || after == undefined ? undefined @@ -311,6 +335,118 @@ function CalendarFilterButton({ before, after }: CalendarFilterButtonProps) { ); } +type GeneralFilterButtonProps = { + allLabels: string[]; + selectedLabels: string[] | undefined; + updateLabelFilter: (labels: string[] | undefined) => void; + showReviewed: boolean; + setShowReviewed: (reviewed: boolean) => void; +}; +function GeneralFilterButton({ + allLabels, + selectedLabels, + updateLabelFilter, + showReviewed, + setShowReviewed, +}: GeneralFilterButtonProps) { + return ( + + + + + +
+ + setShowReviewed(isChecked)} + /> +
+
+
+ ); +} + +type LabelFilterButtonProps = { + allLabels: string[]; + selectedLabels: string[] | undefined; + updateLabelFilter: (labels: string[] | undefined) => void; +}; +function LabelsFilterButton({ + allLabels, + selectedLabels, + updateLabelFilter, +}: LabelFilterButtonProps) { + const [currentLabels, setCurrentLabels] = useState( + selectedLabels + ); + + return ( + { + if (!open) { + updateLabelFilter(currentLabels); + } + }} + > + + + + + Filter Labels + + { + if (isChecked) { + setCurrentLabels(undefined); + } + }} + /> + + {allLabels.map((item) => ( + { + if (isChecked) { + const updatedLabels = currentLabels ? [...currentLabels] : []; + + updatedLabels.push(item); + setCurrentLabels(updatedLabels); + } else { + const updatedLabels = currentLabels ? [...currentLabels] : []; + + // can not deselect the last item + if (updatedLabels.length > 1) { + updatedLabels.splice(updatedLabels.indexOf(item), 1); + setCurrentLabels(updatedLabels); + } + } + }} + /> + ))} + + + + ); +} + type FilterCheckBoxProps = { label: string; isChecked: boolean; diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index d358203fe..dcc549386 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -34,11 +34,14 @@ export default function Events() { const getKey = useCallback( (index: number, prevData: ReviewSegment[]) => { + console.log("The params are " + JSON.stringify(reviewSearchParams)) if (index > 0) { const lastDate = prevData[prevData.length - 1].start_time; reviewSearchParams; const pagedParams = { cameras: reviewSearchParams["cameras"], + labels: reviewSearchParams["labels"], + reviewed: reviewSearchParams["showReviewed"], before: lastDate, after: reviewSearchParams["after"] || timeRange.after, limit: API_LIMIT, @@ -48,6 +51,8 @@ export default function Events() { const params = { cameras: reviewSearchParams["cameras"], + labels: reviewSearchParams["labels"], + reviewed: reviewSearchParams["showReviewed"], limit: API_LIMIT, before: reviewSearchParams["before"] || timeRange.before, after: reviewSearchParams["after"] || timeRange.after,