Add filter popover

This commit is contained in:
Nick Mowen 2023-12-18 15:43:17 -07:00
parent 229f98a78d
commit 62710bb346
5 changed files with 139 additions and 10 deletions

View File

@ -614,20 +614,30 @@ def timeline():
@bp.route("/timeline/hourly") @bp.route("/timeline/hourly")
def hourly_timeline(): def hourly_timeline():
"""Get hourly summary for timeline.""" """Get hourly summary for timeline."""
camera = request.args.get("camera", "all") cameras = request.args.get("cameras", "all")
labels = request.args.get("labels", "all")
before = request.args.get("before", type=float) before = request.args.get("before", type=float)
after = request.args.get("after", type=float)
limit = request.args.get("limit", 200) limit = request.args.get("limit", 200)
tz_name = request.args.get("timezone", default="utc", type=str) tz_name = request.args.get("timezone", default="utc", type=str)
_, minute_modifier, _ = get_tz_modifiers(tz_name) _, minute_modifier, _ = get_tz_modifiers(tz_name)
clauses = [] clauses = []
if camera != "all": if cameras != "all":
clauses.append((Timeline.camera == camera)) camera_list = cameras.split(",")
clauses.append((Timeline.camera << camera_list))
if labels != "all":
label_list = labels.split(",")
clauses.append((Timeline.data["label"] << label_list))
if before: if before:
clauses.append((Timeline.timestamp < before)) clauses.append((Timeline.timestamp < before))
if after:
clauses.append((Timeline.timestamp > after))
if len(clauses) == 0: if len(clauses) == 0:
clauses.append((True)) clauses.append((True))

View File

@ -0,0 +1,101 @@
import { LuFilter } from "react-icons/lu";
import { Button } from "../ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import { useMemo } from "react";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
type HistoryFilterPopoverProps = {
filter: HistoryFilter;
onUpdateFilter: (filter: HistoryFilter) => void;
};
export default function HistoryFilterPopover({
filter,
onUpdateFilter
}: HistoryFilterPopoverProps) {
const { data: config } = useSWR<FrigateConfig>("config");
const { data: allLabels } = useSWR<string[]>(["labels"], {
revalidateOnFocus: false,
});
const { data: allSubLabels } = useSWR<string[]>(
["sub_labels", { split_joined: 1 }],
{
revalidateOnFocus: false,
}
);
const filterValues = useMemo(
() => ({
cameras: Object.keys(config?.cameras || {}),
labels: Object.values(allLabels || {}),
}),
[config, allLabels, allSubLabels]
);
return (
<Popover>
<PopoverTrigger asChild>
<Button>
<LuFilter className="mx-1" />
Filter
</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="grid gap-2 grid-cols-2 md:grid-cols-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="capitalize" variant="outline">
{""?.replaceAll("_", " ") || "All Cameras"}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Filter Cameras</DropdownMenuLabel>
<DropdownMenuSeparator />
{filterValues.cameras.map((item) => (
<DropdownMenuCheckboxItem
className="capitalize"
key={item}
checked={
filter.cameras.length == 0 || filter.cameras.includes(item)
}
>
{item.replaceAll("_", " ")}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="capitalize" variant="outline">
{""?.replaceAll("_", " ") || "All Labels"}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Filter Labels</DropdownMenuLabel>
<DropdownMenuSeparator />
{filterValues.labels.map((item) => (
<DropdownMenuCheckboxItem
className="capitalize"
key={item}
checked={
filter.labels == undefined || filter.labels.includes(item)
}
>
{item.replaceAll("_", " ")}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</PopoverContent>
</Popover>
);
}

View File

@ -19,6 +19,7 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import HistoryFilterPopover from "@/components/filter/HistoryFilterPopover";
const API_LIMIT = 200; const API_LIMIT = 200;
@ -29,6 +30,12 @@ function History() {
config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
[config] [config]
); );
const [searchFilter, setSearchFilter] = useState<HistoryFilter>({
cameras: [],
labels: [],
});
const timelineFetcher = useCallback((key: any) => { const timelineFetcher = useCallback((key: any) => {
const [path, params] = Array.isArray(key) ? key : [key, undefined]; const [path, params] = Array.isArray(key) ? key : [key, undefined];
return axios.get(path, { params }).then((res) => res.data); return axios.get(path, { params }).then((res) => res.data);
@ -137,7 +144,13 @@ function History() {
return ( return (
<> <>
<Heading as="h2">Review</Heading> <div className="flex justify-between">
<Heading as="h2">History</Heading>
<HistoryFilterPopover
filter={searchFilter}
onUpdateFilter={(filter) => setSearchFilter(filter)}
/>
</div>
<AlertDialog <AlertDialog
open={itemsToDelete != null} open={itemsToDelete != null}

View File

@ -38,3 +38,8 @@ type HourlyTimeline = {
count: number, count: number,
hours: { [key: string]: Timeline[] }; hours: { [key: string]: Timeline[] };
} }
type HistoryFilter = {
cameras: string[],
labels: string[],
}

View File

@ -12,24 +12,24 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:5000', target: 'http://192.168.50.106:5000',
ws: true, ws: true,
}, },
'/vod': { '/vod': {
target: 'http://localhost:5000' target: 'http://192.168.50.106:5000'
}, },
'/clips': { '/clips': {
target: 'http://localhost:5000' target: 'http://192.168.50.106:5000'
}, },
'/exports': { '/exports': {
target: 'http://localhost:5000' target: 'http://192.168.50.106:5000'
}, },
'/ws': { '/ws': {
target: 'ws://localhost:5000', target: 'ws://192.168.50.106:5000',
ws: true, ws: true,
}, },
'/live': { '/live': {
target: 'ws://localhost:5000', target: 'ws://192.168.50.106:5000',
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
}, },