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,52 +77,270 @@ 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">
<Dialog <PlusFilterGroup
open={upload != undefined} selectedCameras={selectedCameras}
onOpenChange={(open) => (!open ? setUpload(undefined) : null)} setSelectedCameras={setSelectedCameras}
> selectedLabels={selectedLabels}
<DialogContent className="md:max-w-4xl"> setSelectedLabels={setSelectedLabels}
<DialogHeader> />
<DialogTitle>Submit To Frigate+</DialogTitle> <div className="size-full flex flex-1 flex-wrap content-start gap-2 md:gap-4 overflow-y-auto no-scrollbar">
<DialogDescription> <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">
Objects in locations you want to avoid are not false positives. <Dialog
Submitting them as false positives will confuse the model. open={upload != undefined}
</DialogDescription> onOpenChange={(open) => (!open ? setUpload(undefined) : null)}
</DialogHeader>
<img
className="flex-grow-0"
src={`${baseUrl}api/events/${upload?.id}/snapshot.jpg`}
alt={`${upload?.label}`}
/>
<DialogFooter>
<Button>Cancel</Button>
<Button
className="bg-success"
onClick={() => onSubmitToPlus(false)}
>
This is a {upload?.label}
</Button>
<Button variant="destructive" onClick={() => onSubmitToPlus(true)}>
This is not a {upload?.label}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{events?.map((event) => {
return (
<div
className="size-full rounded-2xl flex justify-center items-center bg-black cursor-pointer"
onClick={() => setUpload(event)}
> >
<img <DialogContent className="md:max-w-4xl">
className="aspect-video h-full object-contain rounded-2xl" <DialogHeader>
src={`${baseUrl}api/events/${event.id}/snapshot.jpg`} <DialogTitle>Submit To Frigate+</DialogTitle>
/> <DialogDescription>
</div> Objects in locations you want to avoid are not false
); positives. Submitting them as false positives will confuse the
})} model.
</DialogDescription>
</DialogHeader>
<img
className="flex-grow-0"
src={`${baseUrl}api/events/${upload?.id}/snapshot.jpg`}
alt={`${upload?.label}`}
/>
<DialogFooter>
<Button>Cancel</Button>
<Button
className="bg-success"
onClick={() => onSubmitToPlus(false)}
>
This is a {upload?.label}
</Button>
<Button
variant="destructive"
onClick={() => onSubmitToPlus(true)}
>
This is not a {upload?.label}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{events?.map((event) => {
return (
<div
className="w-full rounded-2xl aspect-video flex justify-center items-center bg-black cursor-pointer"
onClick={() => setUpload(event)}
>
<img
className="aspect-video h-full object-contain rounded-2xl"
src={`${baseUrl}api/events/${event.id}/snapshot.jpg`}
loading="lazy"
/>
</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> </div>
); );
} }