mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-09 04:35:25 +03:00
Implement label and review filters
This commit is contained in:
parent
01ded3fcc5
commit
335f88f5e6
@ -2396,6 +2396,8 @@ def vod_event(id):
|
|||||||
@bp.route("/review")
|
@bp.route("/review")
|
||||||
def review():
|
def review():
|
||||||
cameras = request.args.get("cameras", "all")
|
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)
|
limit = request.args.get("limit", 100)
|
||||||
severity = request.args.get("severity", None)
|
severity = request.args.get("severity", None)
|
||||||
|
|
||||||
@ -2410,6 +2412,23 @@ def review():
|
|||||||
camera_list = cameras.split(",")
|
camera_list = cameras.split(",")
|
||||||
clauses.append((ReviewSegment.camera << camera_list))
|
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:
|
if severity:
|
||||||
clauses.append((ReviewSegment.severity == severity))
|
clauses.append((ReviewSegment.severity == severity))
|
||||||
|
|
||||||
|
|||||||
@ -8,8 +8,6 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "../ui/dropdown-menu";
|
} from "../ui/dropdown-menu";
|
||||||
@ -17,6 +15,8 @@ import { Calendar } from "../ui/calendar";
|
|||||||
import { ReviewFilter } from "@/types/review";
|
import { ReviewFilter } from "@/types/review";
|
||||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||||
|
|
||||||
|
const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
|
||||||
|
|
||||||
type ReviewFilterGroupProps = {
|
type ReviewFilterGroupProps = {
|
||||||
filter?: ReviewFilter;
|
filter?: ReviewFilter;
|
||||||
onUpdateFilter: (filter: ReviewFilter) => void;
|
onUpdateFilter: (filter: ReviewFilter) => void;
|
||||||
@ -28,9 +28,25 @@ export default function ReviewFilterGroup({
|
|||||||
}: ReviewFilterGroupProps) {
|
}: ReviewFilterGroupProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
const { data: allLabels } = useSWR<string[]>(["labels"], {
|
const allLabels = useMemo<string[]>(() => {
|
||||||
revalidateOnFocus: false,
|
if (!config) {
|
||||||
});
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = new Set<string>();
|
||||||
|
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(
|
const filterValues = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
cameras: Object.keys(config?.cameras || {}),
|
cameras: Object.keys(config?.cameras || {}),
|
||||||
@ -49,10 +65,17 @@ export default function ReviewFilterGroup({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<CalendarFilterButton before={filter?.before} after={filter?.after} />
|
<CalendarFilterButton before={filter?.before} after={filter?.after} />
|
||||||
<Button className="mx-1" variant="secondary">
|
<GeneralFilterButton
|
||||||
<LuFilter className=" mr-[10px]" />
|
allLabels={filterValues.labels}
|
||||||
Filter
|
selectedLabels={filter?.labels}
|
||||||
</Button>
|
updateLabelFilter={(newLabels) => {
|
||||||
|
onUpdateFilter({ ...filter, labels: newLabels });
|
||||||
|
}}
|
||||||
|
showReviewed={filter?.showReviewed || false}
|
||||||
|
setShowReviewed={(reviewed) =>
|
||||||
|
onUpdateFilter({ ...filter, showReviewed: reviewed })
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -285,6 +308,7 @@ function CalendarFilterButton({ before, after }: CalendarFilterButtonProps) {
|
|||||||
future.setFullYear(tomorrow.getFullYear() + 10);
|
future.setFullYear(tomorrow.getFullYear() + 10);
|
||||||
return { from: tomorrow, to: future };
|
return { from: tomorrow, to: future };
|
||||||
}, []);
|
}, []);
|
||||||
|
// @ts-ignore
|
||||||
const dateRange = useMemo(() => {
|
const dateRange = useMemo(() => {
|
||||||
return before == undefined || after == undefined
|
return before == undefined || after == undefined
|
||||||
? 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 (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button className="mx-1" variant="secondary">
|
||||||
|
<LuFilter className=" mr-[10px]" />
|
||||||
|
Filter
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent side="left" asChild>
|
||||||
|
<div className="w-80 flex">
|
||||||
|
<LabelsFilterButton
|
||||||
|
allLabels={allLabels}
|
||||||
|
selectedLabels={selectedLabels}
|
||||||
|
updateLabelFilter={updateLabelFilter}
|
||||||
|
/>
|
||||||
|
<FilterCheckBox
|
||||||
|
label="Show Reviewed"
|
||||||
|
isChecked={showReviewed}
|
||||||
|
onCheckedChange={(isChecked) => setShowReviewed(isChecked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type LabelFilterButtonProps = {
|
||||||
|
allLabels: string[];
|
||||||
|
selectedLabels: string[] | undefined;
|
||||||
|
updateLabelFilter: (labels: string[] | undefined) => void;
|
||||||
|
};
|
||||||
|
function LabelsFilterButton({
|
||||||
|
allLabels,
|
||||||
|
selectedLabels,
|
||||||
|
updateLabelFilter,
|
||||||
|
}: LabelFilterButtonProps) {
|
||||||
|
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
|
||||||
|
selectedLabels
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
updateLabelFilter(currentLabels);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button className="mx-1 capitalize" variant="secondary">
|
||||||
|
<LuVideo className=" mr-[10px]" />
|
||||||
|
{selectedLabels == undefined
|
||||||
|
? "All Labels"
|
||||||
|
: `${selectedLabels.length} Labels`}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuLabel>Filter Labels</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<FilterCheckBox
|
||||||
|
isChecked={currentLabels == undefined}
|
||||||
|
label="All Labels"
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
if (isChecked) {
|
||||||
|
setCurrentLabels(undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{allLabels.map((item) => (
|
||||||
|
<FilterCheckBox
|
||||||
|
key={item}
|
||||||
|
isChecked={currentLabels?.includes(item) ?? false}
|
||||||
|
label={item.replaceAll("_", " ")}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type FilterCheckBoxProps = {
|
type FilterCheckBoxProps = {
|
||||||
label: string;
|
label: string;
|
||||||
isChecked: boolean;
|
isChecked: boolean;
|
||||||
|
|||||||
@ -34,11 +34,14 @@ export default function Events() {
|
|||||||
|
|
||||||
const getKey = useCallback(
|
const getKey = useCallback(
|
||||||
(index: number, prevData: ReviewSegment[]) => {
|
(index: number, prevData: ReviewSegment[]) => {
|
||||||
|
console.log("The params are " + JSON.stringify(reviewSearchParams))
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
const lastDate = prevData[prevData.length - 1].start_time;
|
const lastDate = prevData[prevData.length - 1].start_time;
|
||||||
reviewSearchParams;
|
reviewSearchParams;
|
||||||
const pagedParams = {
|
const pagedParams = {
|
||||||
cameras: reviewSearchParams["cameras"],
|
cameras: reviewSearchParams["cameras"],
|
||||||
|
labels: reviewSearchParams["labels"],
|
||||||
|
reviewed: reviewSearchParams["showReviewed"],
|
||||||
before: lastDate,
|
before: lastDate,
|
||||||
after: reviewSearchParams["after"] || timeRange.after,
|
after: reviewSearchParams["after"] || timeRange.after,
|
||||||
limit: API_LIMIT,
|
limit: API_LIMIT,
|
||||||
@ -48,6 +51,8 @@ export default function Events() {
|
|||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
cameras: reviewSearchParams["cameras"],
|
cameras: reviewSearchParams["cameras"],
|
||||||
|
labels: reviewSearchParams["labels"],
|
||||||
|
reviewed: reviewSearchParams["showReviewed"],
|
||||||
limit: API_LIMIT,
|
limit: API_LIMIT,
|
||||||
before: reviewSearchParams["before"] || timeRange.before,
|
before: reviewSearchParams["before"] || timeRange.before,
|
||||||
after: reviewSearchParams["after"] || timeRange.after,
|
after: reviewSearchParams["after"] || timeRange.after,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user