mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-07 11:45:24 +03:00
Add api filter hook and use UI with filtering
This commit is contained in:
parent
62710bb346
commit
70ef2c0508
@ -1,26 +1,26 @@
|
||||
import { LuFilter } from "react-icons/lu";
|
||||
import { LuCheck, LuFilter, LuFocus } 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 { useMemo, useState } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import { Calendar } from "../ui/calendar";
|
||||
|
||||
type HistoryFilterPopoverProps = {
|
||||
filter: HistoryFilter;
|
||||
onUpdateFilter: (filter: HistoryFilter) => void;
|
||||
filter: HistoryFilter | undefined;
|
||||
onUpdateFilter: (filter: HistoryFilter) => void;
|
||||
};
|
||||
|
||||
export default function HistoryFilterPopover({
|
||||
filter,
|
||||
onUpdateFilter
|
||||
filter,
|
||||
onUpdateFilter,
|
||||
}: HistoryFilterPopoverProps) {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const { data: allLabels } = useSWR<string[]>(["labels"], {
|
||||
@ -39,6 +39,10 @@ export default function HistoryFilterPopover({
|
||||
}),
|
||||
[config, allLabels, allSubLabels]
|
||||
);
|
||||
const [selectedFilters, setSelectedFilters] = useState({
|
||||
cameras: filter == undefined ? [] : filter.cameras,
|
||||
labels: filter == undefined ? [] : filter.labels,
|
||||
});
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
@ -48,8 +52,8 @@ export default function HistoryFilterPopover({
|
||||
Filter
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80">
|
||||
<div className="grid gap-2 grid-cols-2 md:grid-cols-3">
|
||||
<PopoverContent className="w-screen md:w-[340px]">
|
||||
<div className="flex justify-around">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="capitalize" variant="outline">
|
||||
@ -60,15 +64,43 @@ export default function HistoryFilterPopover({
|
||||
<DropdownMenuLabel>Filter Cameras</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{filterValues.cameras.map((item) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
className="capitalize"
|
||||
<FilterCheckBox
|
||||
key={item}
|
||||
checked={
|
||||
filter.cameras.length == 0 || filter.cameras.includes(item)
|
||||
isChecked={
|
||||
selectedFilters.cameras.length == 0 ||
|
||||
selectedFilters.cameras.includes(item)
|
||||
}
|
||||
>
|
||||
{item.replaceAll("_", " ")}
|
||||
</DropdownMenuCheckboxItem>
|
||||
label={item.replaceAll("_", " ")}
|
||||
onCheckedChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
const selectedCameras = [...selectedFilters.cameras];
|
||||
selectedCameras.push(item);
|
||||
setSelectedFilters({
|
||||
...selectedFilters,
|
||||
cameras: selectedCameras,
|
||||
});
|
||||
} else {
|
||||
const selectedCameraList =
|
||||
selectedFilters.cameras.length == 0
|
||||
? [...filterValues.cameras]
|
||||
: [...selectedFilters.cameras];
|
||||
selectedCameraList.splice(
|
||||
selectedCameraList.indexOf(item),
|
||||
1
|
||||
);
|
||||
setSelectedFilters({
|
||||
...selectedFilters,
|
||||
cameras: selectedCameraList,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onSingleSelect={() => {
|
||||
setSelectedFilters({
|
||||
...selectedFilters,
|
||||
cameras: [item],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@ -82,20 +114,94 @@ export default function HistoryFilterPopover({
|
||||
<DropdownMenuLabel>Filter Labels</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{filterValues.labels.map((item) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
className="capitalize"
|
||||
<FilterCheckBox
|
||||
key={item}
|
||||
checked={
|
||||
filter.labels == undefined || filter.labels.includes(item)
|
||||
isChecked={
|
||||
selectedFilters.labels.length == 0 ||
|
||||
selectedFilters.labels.includes(item)
|
||||
}
|
||||
>
|
||||
{item.replaceAll("_", " ")}
|
||||
</DropdownMenuCheckboxItem>
|
||||
label={item.replaceAll("_", " ")}
|
||||
onCheckedChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
const selectedLabels = [...selectedFilters.labels];
|
||||
selectedLabels.push(item);
|
||||
setSelectedFilters({
|
||||
...selectedFilters,
|
||||
labels: selectedLabels,
|
||||
});
|
||||
} else {
|
||||
const selectedLabelList =
|
||||
selectedFilters.labels.length == 0
|
||||
? [...filterValues.labels]
|
||||
: selectedFilters.labels;
|
||||
selectedLabelList.splice(
|
||||
selectedLabelList.indexOf(item),
|
||||
1
|
||||
);
|
||||
setSelectedFilters({
|
||||
...selectedFilters,
|
||||
labels: selectedLabelList,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onSingleSelect={() => {
|
||||
setSelectedFilters({
|
||||
...selectedFilters,
|
||||
labels: [item],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<Calendar mode="range" />
|
||||
<Button
|
||||
onClick={() => {
|
||||
onUpdateFilter(selectedFilters);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
type FilterCheckBoxProps = {
|
||||
label: string;
|
||||
isChecked: boolean;
|
||||
onCheckedChange: (isChecked: boolean) => void;
|
||||
onSingleSelect: () => void;
|
||||
};
|
||||
|
||||
function FilterCheckBox({
|
||||
label,
|
||||
isChecked,
|
||||
onCheckedChange,
|
||||
onSingleSelect,
|
||||
}: FilterCheckBoxProps) {
|
||||
return (
|
||||
<div
|
||||
className="capitalize flex justify-between items-center cursor-pointer"
|
||||
onClick={(_) => onCheckedChange(!isChecked)}
|
||||
>
|
||||
{isChecked ? (
|
||||
<LuCheck className="w-8 h-8" />
|
||||
) : (
|
||||
<div className="w-8 h-8" />
|
||||
)}
|
||||
<div className="ml-1 w-full flex justify-start">{label}</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSingleSelect();
|
||||
}}
|
||||
>
|
||||
<LuFocus className="text-primary w-6 h-6" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
17
web/src/hooks/use-api-filter.ts
Normal file
17
web/src/hooks/use-api-filter.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { useState } from "react";
|
||||
|
||||
type useApiFilterReturn<F extends FilterType> = [
|
||||
filter: F | undefined,
|
||||
setFilter: (filter: F) => void,
|
||||
searchParams: {
|
||||
[key: string]: any;
|
||||
},
|
||||
];
|
||||
|
||||
export default function useApiFilter<
|
||||
F extends FilterType,
|
||||
>(): useApiFilterReturn<F> {
|
||||
const [filter, setFilter] = useState<F>();
|
||||
|
||||
return [filter, setFilter, {}];
|
||||
}
|
||||
@ -20,6 +20,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import HistoryFilterPopover from "@/components/filter/HistoryFilterPopover";
|
||||
import useApiFilter from "@/hooks/use-api-filter";
|
||||
|
||||
const API_LIMIT = 200;
|
||||
|
||||
@ -31,10 +32,8 @@ function History() {
|
||||
[config]
|
||||
);
|
||||
|
||||
const [searchFilter, setSearchFilter] = useState<HistoryFilter>({
|
||||
cameras: [],
|
||||
labels: [],
|
||||
});
|
||||
const [historyFilter, setHistoryFilter, historySearchParams] =
|
||||
useApiFilter<HistoryFilter>();
|
||||
|
||||
const timelineFetcher = useCallback((key: any) => {
|
||||
const [path, params] = Array.isArray(key) ? key : [key, undefined];
|
||||
@ -147,8 +146,8 @@ function History() {
|
||||
<div className="flex justify-between">
|
||||
<Heading as="h2">History</Heading>
|
||||
<HistoryFilterPopover
|
||||
filter={searchFilter}
|
||||
onUpdateFilter={(filter) => setSearchFilter(filter)}
|
||||
filter={historyFilter}
|
||||
onUpdateFilter={(filter) => setHistoryFilter(filter)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
1
web/src/types/filter.ts
Normal file
1
web/src/types/filter.ts
Normal file
@ -0,0 +1 @@
|
||||
type FilterType = { [searchKey: string]: any };
|
||||
@ -39,7 +39,7 @@ type HourlyTimeline = {
|
||||
hours: { [key: string]: Timeline[] };
|
||||
}
|
||||
|
||||
type HistoryFilter = {
|
||||
interface HistoryFilter extends FilterType {
|
||||
cameras: string[],
|
||||
labels: string[],
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user