Allow filtering on detail level

This commit is contained in:
Nick Mowen 2023-12-20 13:25:57 -07:00
parent dac8c1f975
commit 734a488db3
3 changed files with 118 additions and 67 deletions

View File

@ -1,4 +1,4 @@
import { LuCheck, LuFilter, LuFocus } from "react-icons/lu"; import { LuCheck, LuFilter } 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";
@ -8,6 +8,8 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "../ui/dropdown-menu"; } from "../ui/dropdown-menu";
@ -50,10 +52,11 @@ export default function HistoryFilterPopover({
[config, allLabels, allSubLabels] [config, allLabels, allSubLabels]
); );
const [selectedFilters, setSelectedFilters] = useState({ const [selectedFilters, setSelectedFilters] = useState({
cameras: filter == undefined ? [] : filter.cameras, cameras: filter == undefined ? ["all"] : filter.cameras,
labels: filter == undefined ? [] : filter.labels, labels: filter == undefined ? ["all"] : filter.labels,
before: filter?.before, before: filter?.before,
after: filter?.after, after: filter?.after,
detailLevel: filter?.detailLevel ?? "normal",
}); });
const dateRange = useMemo(() => { const dateRange = useMemo(() => {
return selectedFilters?.before == undefined || return selectedFilters?.before == undefined ||
@ -65,6 +68,14 @@ export default function HistoryFilterPopover({
}; };
}, [selectedFilters]); }, [selectedFilters]);
const allItems = useMemo(() => {
return {
cameras:
JSON.stringify(selectedFilters.cameras) == JSON.stringify(["all"]),
labels: JSON.stringify(selectedFilters.labels) == JSON.stringify(["all"]),
};
}, [selectedFilters]);
return ( return (
<Popover open={open} onOpenChange={(open) => setOpen(open)}> <Popover open={open} onOpenChange={(open) => setOpen(open)}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@ -78,7 +89,7 @@ export default function HistoryFilterPopover({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button className="capitalize" variant="outline"> <Button className="capitalize" variant="outline">
{selectedFilters.cameras.length == 0 {allItems.cameras
? "All Cameras" ? "All Cameras"
: `${selectedFilters.cameras.length} Cameras`} : `${selectedFilters.cameras.length} Cameras`}
</Button> </Button>
@ -86,27 +97,39 @@ export default function HistoryFilterPopover({
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuLabel>Filter Cameras</DropdownMenuLabel> <DropdownMenuLabel>Filter Cameras</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<FilterCheckBox
isChecked={allItems.cameras}
label="All Cameras"
onCheckedChange={(isChecked) => {
if (isChecked) {
setSelectedFilters({
...selectedFilters,
cameras: ["all"],
});
}
}}
/>
<DropdownMenuSeparator />
{filterValues.cameras.map((item) => ( {filterValues.cameras.map((item) => (
<FilterCheckBox <FilterCheckBox
key={item} key={item}
isChecked={ isChecked={selectedFilters.cameras.includes(item)}
selectedFilters.cameras.length == 0 ||
selectedFilters.cameras.includes(item)
}
label={item.replaceAll("_", " ")} label={item.replaceAll("_", " ")}
onCheckedChange={(isChecked) => { onCheckedChange={(isChecked) => {
if (isChecked) { if (isChecked) {
const selectedCameras = [...selectedFilters.cameras]; const selectedCameras = allItems.cameras
? []
: [...selectedFilters.cameras];
selectedCameras.push(item); selectedCameras.push(item);
setSelectedFilters({ setSelectedFilters({
...selectedFilters, ...selectedFilters,
cameras: selectedCameras, cameras: selectedCameras,
}); });
} else { } else {
const selectedCameraList = const selectedCameraList = [...selectedFilters.cameras];
selectedFilters.cameras.length == 0
? [...filterValues.cameras] // can not deselect the last item
: [...selectedFilters.cameras]; if (selectedCameraList.length > 1) {
selectedCameraList.splice( selectedCameraList.splice(
selectedCameraList.indexOf(item), selectedCameraList.indexOf(item),
1 1
@ -116,12 +139,7 @@ export default function HistoryFilterPopover({
cameras: selectedCameraList, cameras: selectedCameraList,
}); });
} }
}} }
onSingleSelect={() => {
setSelectedFilters({
...selectedFilters,
cameras: [item],
});
}} }}
/> />
))} ))}
@ -130,7 +148,7 @@ export default function HistoryFilterPopover({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button className="capitalize" variant="outline"> <Button className="capitalize" variant="outline">
{selectedFilters.labels.length == 0 {allItems.labels
? "All Labels" ? "All Labels"
: `${selectedFilters.labels.length} Labels`} : `${selectedFilters.labels.length} Labels`}
</Button> </Button>
@ -138,6 +156,19 @@ export default function HistoryFilterPopover({
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuLabel>Filter Labels</DropdownMenuLabel> <DropdownMenuLabel>Filter Labels</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<FilterCheckBox
isChecked={allItems.labels}
label="All Labels"
onCheckedChange={(isChecked) => {
if (isChecked) {
setSelectedFilters({
...selectedFilters,
labels: ["all"],
});
}
}}
/>
<DropdownMenuSeparator />
{filterValues.labels.map((item) => ( {filterValues.labels.map((item) => (
<FilterCheckBox <FilterCheckBox
key={item} key={item}
@ -148,17 +179,19 @@ export default function HistoryFilterPopover({
label={item.replaceAll("_", " ")} label={item.replaceAll("_", " ")}
onCheckedChange={(isChecked) => { onCheckedChange={(isChecked) => {
if (isChecked) { if (isChecked) {
const selectedLabels = [...selectedFilters.labels]; const selectedLabels = allItems.labels
? []
: [...selectedFilters.labels];
selectedLabels.push(item); selectedLabels.push(item);
setSelectedFilters({ setSelectedFilters({
...selectedFilters, ...selectedFilters,
labels: selectedLabels, labels: selectedLabels,
}); });
} else { } else {
const selectedLabelList = const selectedLabelList = [...selectedFilters.labels];
selectedFilters.labels.length == 0
? [...filterValues.labels] // can not deselect the last item
: selectedFilters.labels; if (selectedLabelList.length > 1) {
selectedLabelList.splice( selectedLabelList.splice(
selectedLabelList.indexOf(item), selectedLabelList.indexOf(item),
1 1
@ -168,17 +201,43 @@ export default function HistoryFilterPopover({
labels: selectedLabelList, labels: selectedLabelList,
}); });
} }
}} }
onSingleSelect={() => {
setSelectedFilters({
...selectedFilters,
labels: [item],
});
}} }}
/> />
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="capitalize" variant="outline">
{selectedFilters.detailLevel}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>
Detail Level
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={selectedFilters.detailLevel}
onValueChange={(value) => {
setSelectedFilters({
...selectedFilters,
// @ts-ignore we know that value is one of the detailLevel
detailLevel: value,
});
}}
>
<DropdownMenuRadioItem value="normal">
Normal
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="extra">
Extra
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="full">Full</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
<Calendar <Calendar
mode="range" mode="range"
@ -222,36 +281,25 @@ type FilterCheckBoxProps = {
label: string; label: string;
isChecked: boolean; isChecked: boolean;
onCheckedChange: (isChecked: boolean) => void; onCheckedChange: (isChecked: boolean) => void;
onSingleSelect: () => void;
}; };
function FilterCheckBox({ function FilterCheckBox({
label, label,
isChecked, isChecked,
onCheckedChange, onCheckedChange,
onSingleSelect,
}: FilterCheckBoxProps) { }: FilterCheckBoxProps) {
return ( return (
<div <Button
className="capitalize flex justify-between items-center cursor-pointer" className="capitalize flex justify-between items-center cursor-pointer w-full"
variant="ghost"
onClick={(_) => onCheckedChange(!isChecked)} onClick={(_) => onCheckedChange(!isChecked)}
> >
{isChecked ? ( {isChecked ? (
<LuCheck className="w-8 h-8" /> <LuCheck className="w-6 h-6" />
) : ( ) : (
<div className="w-8 h-8" /> <div className="w-6 h-6" />
)} )}
<div className="ml-1 w-full flex justify-start">{label}</div> <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> </Button>
</div>
); );
} }

View File

@ -80,7 +80,6 @@ function History() {
{ revalidateOnFocus: false } { revalidateOnFocus: false }
); );
const [detailLevel, _] = useState<"normal" | "extra" | "full">("normal");
const [playback, setPlayback] = useState<Card | undefined>(); const [playback, setPlayback] = useState<Card | undefined>();
const shouldAutoPlay = useMemo(() => { const shouldAutoPlay = useMemo(() => {
@ -92,8 +91,11 @@ function History() {
return []; return [];
} }
return getHourlyTimelineData(timelinePages, detailLevel); return getHourlyTimelineData(
}, [detailLevel, timelinePages]); timelinePages,
historyFilter?.detailLevel ?? "normal"
);
}, [historyFilter, timelinePages]);
const isDone = const isDone =
(timelinePages?.[timelinePages.length - 1]?.count ?? 0) < API_LIMIT; (timelinePages?.[timelinePages.length - 1]?.count ?? 0) < API_LIMIT;

View File

@ -44,4 +44,5 @@ interface HistoryFilter extends FilterType {
labels: string[]; labels: string[];
before: number | undefined; before: number | undefined;
after: number | undefined; after: number | undefined;
detailLevel: "normal" | "extra" | "full";
} }