Add filters to plus page and fix layout (#10320)

This commit is contained in:
Nicolas Mowen 2024-03-07 17:32:26 -07:00 committed by GitHub
parent 90a40d2509
commit 507c6afa2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,4 +1,5 @@
import { baseUrl } from "@/api/baseUrl"; import { baseUrl } from "@/api/baseUrl";
import FilterCheckBox from "@/components/filter/FilterCheckBox";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@ -8,15 +9,37 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Event } from "@/types/event"; import { Event } from "@/types/event";
import { FrigateConfig } from "@/types/frigateConfig";
import axios from "axios"; import axios from "axios";
import { useCallback, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { FaList, FaVideo } from "react-icons/fa";
import useSWR from "swr"; import useSWR from "swr";
export default function SubmitPlus() { export default function SubmitPlus() {
// filters
const [selectedCameras, setSelectedCameras] = useState<string[]>();
const [selectedLabels, setSelectedLabels] = useState<string[]>();
// data
const { data: events, mutate: refresh } = useSWR<Event[]>([ const { data: events, mutate: refresh } = useSWR<Event[]>([
"events", "events",
{ limit: 100, in_progress: 0, is_submitted: 0 }, {
limit: 100,
in_progress: 0,
is_submitted: 0,
cameras: selectedCameras ? selectedCameras.join(",") : null,
labels: selectedLabels ? selectedLabels.join(",") : null,
},
]); ]);
const [upload, setUpload] = useState<Event>(); const [upload, setUpload] = useState<Event>();
@ -54,7 +77,15 @@ export default function SubmitPlus() {
); );
return ( return (
<div className="size-full p-2 grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2 overflow-auto"> <div className="size-full flex flex-col">
<PlusFilterGroup
selectedCameras={selectedCameras}
setSelectedCameras={setSelectedCameras}
selectedLabels={selectedLabels}
setSelectedLabels={setSelectedLabels}
/>
<div className="size-full flex flex-1 flex-wrap content-start gap-2 md:gap-4 overflow-y-auto no-scrollbar">
<div className="w-full p-2 grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2">
<Dialog <Dialog
open={upload != undefined} open={upload != undefined}
onOpenChange={(open) => (!open ? setUpload(undefined) : null)} onOpenChange={(open) => (!open ? setUpload(undefined) : null)}
@ -63,8 +94,9 @@ export default function SubmitPlus() {
<DialogHeader> <DialogHeader>
<DialogTitle>Submit To Frigate+</DialogTitle> <DialogTitle>Submit To Frigate+</DialogTitle>
<DialogDescription> <DialogDescription>
Objects in locations you want to avoid are not false positives. Objects in locations you want to avoid are not false
Submitting them as false positives will confuse the model. positives. Submitting them as false positives will confuse the
model.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<img <img
@ -80,7 +112,10 @@ export default function SubmitPlus() {
> >
This is a {upload?.label} This is a {upload?.label}
</Button> </Button>
<Button variant="destructive" onClick={() => onSubmitToPlus(true)}> <Button
variant="destructive"
onClick={() => onSubmitToPlus(true)}
>
This is not a {upload?.label} This is not a {upload?.label}
</Button> </Button>
</DialogFooter> </DialogFooter>
@ -90,16 +125,222 @@ export default function SubmitPlus() {
{events?.map((event) => { {events?.map((event) => {
return ( return (
<div <div
className="size-full rounded-2xl flex justify-center items-center bg-black cursor-pointer" className="w-full rounded-2xl aspect-video flex justify-center items-center bg-black cursor-pointer"
onClick={() => setUpload(event)} onClick={() => setUpload(event)}
> >
<img <img
className="aspect-video h-full object-contain rounded-2xl" className="aspect-video h-full object-contain rounded-2xl"
src={`${baseUrl}api/events/${event.id}/snapshot.jpg`} src={`${baseUrl}api/events/${event.id}/snapshot.jpg`}
loading="lazy"
/> />
</div> </div>
); );
})} })}
</div> </div>
</div>
</div>
);
}
const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
type PlusFilterGroupProps = {
selectedCameras: string[] | undefined;
setSelectedCameras: (cameras: string[] | undefined) => void;
selectedLabels: string[] | undefined;
setSelectedLabels: (cameras: string[] | undefined) => void;
};
function PlusFilterGroup({
selectedCameras,
setSelectedCameras,
selectedLabels,
setSelectedLabels,
}: PlusFilterGroupProps) {
const { data: config } = useSWR<FrigateConfig>("config");
const allCameras = useMemo(() => {
if (!config) {
return [];
}
return Object.keys(config.cameras);
}, [config]);
const allLabels = useMemo<string[]>(() => {
if (!config) {
return [];
}
const labels = new Set<string>();
const cameras = selectedCameras || Object.keys(config.cameras);
cameras.forEach((camera) => {
const cameraConfig = config.cameras[camera];
cameraConfig.objects.track.forEach((label) => {
if (!ATTRIBUTES.includes(label)) {
labels.add(label);
}
});
});
return [...labels].sort();
}, [config, selectedCameras]);
const [open, setOpen] = useState<"none" | "camera" | "label">("none");
const [currentCameras, setCurrentCameras] = useState<string[] | undefined>(
undefined,
);
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
undefined,
);
return (
<div className="w-full h-16 flex justify-start gap-2 items-center">
<DropdownMenu
open={open == "camera"}
onOpenChange={(open) => {
if (!open) {
setCurrentCameras(selectedCameras);
}
setOpen(open ? "camera" : "none");
}}
>
<DropdownMenuTrigger asChild>
<Button size="sm" className="mx-1 capitalize" variant="secondary">
<FaVideo className="md:mr-[10px] text-muted-foreground" />
<div className="hidden md:block">
{selectedCameras == undefined
? "All Cameras"
: `${selectedCameras.length} Cameras`}
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel className="flex justify-center">
Filter Cameras
</DropdownMenuLabel>
<DropdownMenuSeparator />
<FilterCheckBox
isChecked={currentCameras == undefined}
label="All Cameras"
onCheckedChange={(isChecked) => {
if (isChecked) {
setCurrentCameras(undefined);
}
}}
/>
<DropdownMenuSeparator />
{allCameras.map((item) => (
<FilterCheckBox
key={item}
isChecked={currentCameras?.includes(item) ?? false}
label={item.replaceAll("_", " ")}
onCheckedChange={(isChecked) => {
if (isChecked) {
const updatedCameras = currentCameras
? [...currentCameras]
: [];
updatedCameras.push(item);
setCurrentCameras(updatedCameras);
} else {
const updatedCameras = currentCameras
? [...currentCameras]
: [];
// can not deselect the last item
if (updatedCameras.length > 1) {
updatedCameras.splice(updatedCameras.indexOf(item), 1);
setCurrentCameras(updatedCameras);
}
}
}}
/>
))}
<DropdownMenuSeparator />
<div className="flex justify-center items-center">
<Button
variant="select"
onClick={() => {
setSelectedCameras(currentCameras);
setOpen("none");
}}
>
Apply
</Button>
</div>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
open={open == "label"}
onOpenChange={(open) => {
if (!open) {
setCurrentLabels(selectedLabels);
}
setOpen(open ? "label" : "none");
}}
>
<DropdownMenuTrigger asChild>
<Button size="sm" className="mx-1 capitalize" variant="secondary">
<FaList className="md:mr-[10px] text-muted-foreground" />
<div className="hidden md:block">
{selectedLabels == undefined
? "All Labels"
: `${selectedLabels.length} Labels`}
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel className="flex justify-center">
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 />
<div className="flex justify-center items-center">
<Button
variant="select"
onClick={() => {
setSelectedLabels(currentLabels);
setOpen("none");
}}
>
Apply
</Button>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
); );
} }