Add api filter hook and use UI with filtering

This commit is contained in:
Nick Mowen 2023-12-20 10:48:26 -07:00
parent 62710bb346
commit 70ef2c0508
5 changed files with 153 additions and 30 deletions

View File

@ -1,26 +1,26 @@
import { LuFilter } from "react-icons/lu"; import { LuCheck, LuFilter, LuFocus } from "react-icons/lu";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import useSWR from "swr"; import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { useMemo } from "react"; import { useMemo, useState } from "react";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "../ui/dropdown-menu"; } from "../ui/dropdown-menu";
import { Calendar } from "../ui/calendar";
type HistoryFilterPopoverProps = { type HistoryFilterPopoverProps = {
filter: HistoryFilter; filter: HistoryFilter | undefined;
onUpdateFilter: (filter: HistoryFilter) => void; onUpdateFilter: (filter: HistoryFilter) => void;
}; };
export default function HistoryFilterPopover({ export default function HistoryFilterPopover({
filter, filter,
onUpdateFilter onUpdateFilter,
}: HistoryFilterPopoverProps) { }: HistoryFilterPopoverProps) {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const { data: allLabels } = useSWR<string[]>(["labels"], { const { data: allLabels } = useSWR<string[]>(["labels"], {
@ -39,6 +39,10 @@ export default function HistoryFilterPopover({
}), }),
[config, allLabels, allSubLabels] [config, allLabels, allSubLabels]
); );
const [selectedFilters, setSelectedFilters] = useState({
cameras: filter == undefined ? [] : filter.cameras,
labels: filter == undefined ? [] : filter.labels,
});
return ( return (
<Popover> <Popover>
@ -48,8 +52,8 @@ export default function HistoryFilterPopover({
Filter Filter
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-80"> <PopoverContent className="w-screen md:w-[340px]">
<div className="grid gap-2 grid-cols-2 md:grid-cols-3"> <div className="flex justify-around">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button className="capitalize" variant="outline"> <Button className="capitalize" variant="outline">
@ -60,15 +64,43 @@ export default function HistoryFilterPopover({
<DropdownMenuLabel>Filter Cameras</DropdownMenuLabel> <DropdownMenuLabel>Filter Cameras</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{filterValues.cameras.map((item) => ( {filterValues.cameras.map((item) => (
<DropdownMenuCheckboxItem <FilterCheckBox
className="capitalize"
key={item} key={item}
checked={ isChecked={
filter.cameras.length == 0 || filter.cameras.includes(item) selectedFilters.cameras.length == 0 ||
selectedFilters.cameras.includes(item)
} }
> label={item.replaceAll("_", " ")}
{item.replaceAll("_", " ")} onCheckedChange={(isChecked) => {
</DropdownMenuCheckboxItem> 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> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@ -82,20 +114,94 @@ export default function HistoryFilterPopover({
<DropdownMenuLabel>Filter Labels</DropdownMenuLabel> <DropdownMenuLabel>Filter Labels</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{filterValues.labels.map((item) => ( {filterValues.labels.map((item) => (
<DropdownMenuCheckboxItem <FilterCheckBox
className="capitalize"
key={item} key={item}
checked={ isChecked={
filter.labels == undefined || filter.labels.includes(item) selectedFilters.labels.length == 0 ||
selectedFilters.labels.includes(item)
} }
> label={item.replaceAll("_", " ")}
{item.replaceAll("_", " ")} onCheckedChange={(isChecked) => {
</DropdownMenuCheckboxItem> 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> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
<Calendar mode="range" />
<Button
onClick={() => {
onUpdateFilter(selectedFilters);
}}
>
Save
</Button>
</PopoverContent> </PopoverContent>
</Popover> </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>
);
}

View 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, {}];
}

View File

@ -20,6 +20,7 @@ import {
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import HistoryFilterPopover from "@/components/filter/HistoryFilterPopover"; import HistoryFilterPopover from "@/components/filter/HistoryFilterPopover";
import useApiFilter from "@/hooks/use-api-filter";
const API_LIMIT = 200; const API_LIMIT = 200;
@ -31,10 +32,8 @@ function History() {
[config] [config]
); );
const [searchFilter, setSearchFilter] = useState<HistoryFilter>({ const [historyFilter, setHistoryFilter, historySearchParams] =
cameras: [], useApiFilter<HistoryFilter>();
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];
@ -147,8 +146,8 @@ function History() {
<div className="flex justify-between"> <div className="flex justify-between">
<Heading as="h2">History</Heading> <Heading as="h2">History</Heading>
<HistoryFilterPopover <HistoryFilterPopover
filter={searchFilter} filter={historyFilter}
onUpdateFilter={(filter) => setSearchFilter(filter)} onUpdateFilter={(filter) => setHistoryFilter(filter)}
/> />
</div> </div>

1
web/src/types/filter.ts Normal file
View File

@ -0,0 +1 @@
type FilterType = { [searchKey: string]: any };

View File

@ -39,7 +39,7 @@ type HourlyTimeline = {
hours: { [key: string]: Timeline[] }; hours: { [key: string]: Timeline[] };
} }
type HistoryFilter = { interface HistoryFilter extends FilterType {
cameras: string[], cameras: string[],
labels: string[], labels: string[],
} }