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 { 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
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,
|
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
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[] };
|
hours: { [key: string]: Timeline[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
type HistoryFilter = {
|
interface HistoryFilter extends FilterType {
|
||||||
cameras: string[],
|
cameras: string[],
|
||||||
labels: string[],
|
labels: string[],
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user