Abstract filters to separate components

This commit is contained in:
Nicolas Mowen 2024-06-21 16:28:45 -06:00
parent f3b858364d
commit bc6b2d5659
6 changed files with 759 additions and 253 deletions

View File

@ -0,0 +1,79 @@
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
import { ReviewSummary } from "@/types/review";
import { Button } from "../ui/button";
import { FaCalendarAlt } from "react-icons/fa";
import ReviewActivityCalendar from "../overlay/ReviewActivityCalendar";
import { DropdownMenuSeparator } from "../ui/dropdown-menu";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import { isMobile } from "react-device-detect";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
type CalendarFilterButtonProps = {
reviewSummary?: ReviewSummary;
day?: Date;
defaultText: string;
updateSelectedDay: (day?: Date) => void;
};
export default function CalendarFilterButton({
reviewSummary,
day,
defaultText,
updateSelectedDay,
}: CalendarFilterButtonProps) {
const selectedDate = useFormattedTimestamp(
day == undefined ? 0 : day?.getTime() / 1000 + 1,
"%b %-d",
);
const trigger = (
<Button
className="flex items-center gap-2"
variant={day == undefined ? "default" : "select"}
size="sm"
>
<FaCalendarAlt
className={`${day == undefined ? "text-secondary-foreground" : "text-selected-foreground"}`}
/>
<div
className={`hidden md:block ${day == undefined ? "text-primary" : "text-selected-foreground"}`}
>
{day == undefined ? defaultText : selectedDate}
</div>
</Button>
);
const content = (
<>
<ReviewActivityCalendar
reviewSummary={reviewSummary}
selectedDay={day}
onSelect={updateSelectedDay}
/>
<DropdownMenuSeparator />
<div className="flex items-center justify-center p-2">
<Button
onClick={() => {
updateSelectedDay(undefined);
}}
>
Reset
</Button>
</div>
</>
);
if (isMobile) {
return (
<Drawer>
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent>{content}</DrawerContent>
</Drawer>
);
}
return (
<Popover>
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent>{content}</PopoverContent>
</Popover>
);
}

View File

@ -0,0 +1,177 @@
import { Button } from "../ui/button";
import { CameraGroupConfig } from "@/types/frigateConfig";
import { useState } from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { isMobile } from "react-device-detect";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import FilterSwitch from "./FilterSwitch";
import { FaVideo } from "react-icons/fa";
type CameraFilterButtonProps = {
allCameras: string[];
groups: [string, CameraGroupConfig][];
selectedCameras: string[] | undefined;
updateCameraFilter: (cameras: string[] | undefined) => void;
};
export function CamerasFilterButton({
allCameras,
groups,
selectedCameras,
updateCameraFilter,
}: CameraFilterButtonProps) {
const [open, setOpen] = useState(false);
const [currentCameras, setCurrentCameras] = useState<string[] | undefined>(
selectedCameras,
);
const trigger = (
<Button
className="flex items-center gap-2 capitalize"
variant={selectedCameras?.length == undefined ? "default" : "select"}
size="sm"
>
<FaVideo
className={`${(selectedCameras?.length ?? 0) >= 1 ? "text-selected-foreground" : "text-secondary-foreground"}`}
/>
<div
className={`hidden md:block ${selectedCameras?.length ? "text-selected-foreground" : "text-primary"}`}
>
{selectedCameras == undefined
? "All Cameras"
: `${selectedCameras.includes("birdseye") ? selectedCameras.length - 1 : selectedCameras.length} Camera${selectedCameras.length !== 1 ? "s" : ""}`}
</div>
</Button>
);
const content = (
<>
{isMobile && (
<>
<DropdownMenuLabel className="flex justify-center">
Cameras
</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
)}
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden p-4">
<FilterSwitch
isChecked={currentCameras == undefined}
label="All Cameras"
onCheckedChange={(isChecked) => {
if (isChecked) {
setCurrentCameras(undefined);
}
}}
/>
{groups.length > 0 && (
<>
<DropdownMenuSeparator className="mt-2" />
{groups.map(([name, conf]) => {
return (
<div
key={name}
className="w-full cursor-pointer rounded-lg px-2 py-1.5 text-sm capitalize text-primary hover:bg-muted"
onClick={() => setCurrentCameras([...conf.cameras])}
>
{name}
</div>
);
})}
</>
)}
<DropdownMenuSeparator className="my-2" />
<div className="flex flex-col gap-2.5">
{allCameras.map((item) => (
<FilterSwitch
key={item}
isChecked={currentCameras?.includes(item) ?? false}
label={item.replaceAll("_", " ")}
onCheckedChange={(isChecked) => {
if (isChecked) {
const updatedCameras = currentCameras
? [...currentCameras]
: [];
updatedCameras.push(item);
setCurrentCameras(updatedCameras);
} else {
const updatedCameras = currentCameras
? [...currentCameras]
: [];
// can not deselect the last item
if (updatedCameras.length > 1) {
updatedCameras.splice(updatedCameras.indexOf(item), 1);
setCurrentCameras(updatedCameras);
}
}
}}
/>
))}
</div>
</div>
<DropdownMenuSeparator className="my-2" />
<div className="flex items-center justify-evenly p-2">
<Button
variant="select"
onClick={() => {
updateCameraFilter(currentCameras);
setOpen(false);
}}
>
Apply
</Button>
<Button
onClick={() => {
setCurrentCameras(undefined);
updateCameraFilter(undefined);
}}
>
Reset
</Button>
</div>
</>
);
if (isMobile) {
return (
<Drawer
open={open}
onOpenChange={(open) => {
if (!open) {
setCurrentCameras(selectedCameras);
}
setOpen(open);
}}
>
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent className="max-h-[75dvh] overflow-hidden">
{content}
</DrawerContent>
</Drawer>
);
}
return (
<DropdownMenu
modal={false}
open={open}
onOpenChange={(open) => {
if (!open) {
setCurrentCameras(selectedCameras);
}
setOpen(open);
}}
>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent>{content}</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -1,36 +1,24 @@
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 { CameraGroupConfig, FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { import { DropdownMenuSeparator } from "../ui/dropdown-menu";
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { ReviewFilter, ReviewSeverity, ReviewSummary } from "@/types/review"; import { ReviewFilter, ReviewSeverity, ReviewSummary } from "@/types/review";
import { getEndOfDayTimestamp } from "@/utils/dateUtil"; import { getEndOfDayTimestamp } from "@/utils/dateUtil";
import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import { FaCheckCircle, FaFilter, FaRunning } from "react-icons/fa";
import {
FaCalendarAlt,
FaCheckCircle,
FaFilter,
FaRunning,
FaVideo,
} from "react-icons/fa";
import { isDesktop, isMobile } from "react-device-detect"; import { isDesktop, isMobile } from "react-device-detect";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import { Switch } from "../ui/switch"; import { Switch } from "../ui/switch";
import { Label } from "../ui/label"; import { Label } from "../ui/label";
import ReviewActivityCalendar from "../overlay/ReviewActivityCalendar";
import MobileReviewSettingsDrawer, { import MobileReviewSettingsDrawer, {
DrawerFeatures, DrawerFeatures,
} from "../overlay/MobileReviewSettingsDrawer"; } from "../overlay/MobileReviewSettingsDrawer";
import useOptimisticState from "@/hooks/use-optimistic-state"; import useOptimisticState from "@/hooks/use-optimistic-state";
import FilterSwitch from "./FilterSwitch"; import FilterSwitch from "./FilterSwitch";
import { FilterList } from "@/types/filter"; import { FilterList } from "@/types/filter";
import CalendarFilterButton from "./CalendarFilterButton";
import { CamerasFilterButton } from "./CamerasFilterButton";
const REVIEW_FILTERS = [ const REVIEW_FILTERS = [
"cameras", "cameras",
@ -204,6 +192,7 @@ export default function ReviewFilterGroup({
? undefined ? undefined
: new Date(filter.after * 1000) : new Date(filter.after * 1000)
} }
defaultText="Last 24 Hours"
updateSelectedDay={onUpdateSelectedDay} updateSelectedDay={onUpdateSelectedDay}
/> />
)} )}
@ -254,169 +243,6 @@ export default function ReviewFilterGroup({
); );
} }
type CameraFilterButtonProps = {
allCameras: string[];
groups: [string, CameraGroupConfig][];
selectedCameras: string[] | undefined;
updateCameraFilter: (cameras: string[] | undefined) => void;
};
export function CamerasFilterButton({
allCameras,
groups,
selectedCameras,
updateCameraFilter,
}: CameraFilterButtonProps) {
const [open, setOpen] = useState(false);
const [currentCameras, setCurrentCameras] = useState<string[] | undefined>(
selectedCameras,
);
const trigger = (
<Button
className="flex items-center gap-2 capitalize"
variant={selectedCameras?.length == undefined ? "default" : "select"}
size="sm"
>
<FaVideo
className={`${(selectedCameras?.length ?? 0) >= 1 ? "text-selected-foreground" : "text-secondary-foreground"}`}
/>
<div
className={`hidden md:block ${selectedCameras?.length ? "text-selected-foreground" : "text-primary"}`}
>
{selectedCameras == undefined
? "All Cameras"
: `${selectedCameras.includes("birdseye") ? selectedCameras.length - 1 : selectedCameras.length} Camera${selectedCameras.length !== 1 ? "s" : ""}`}
</div>
</Button>
);
const content = (
<>
{isMobile && (
<>
<DropdownMenuLabel className="flex justify-center">
Cameras
</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
)}
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden p-4">
<FilterSwitch
isChecked={currentCameras == undefined}
label="All Cameras"
onCheckedChange={(isChecked) => {
if (isChecked) {
setCurrentCameras(undefined);
}
}}
/>
{groups.length > 0 && (
<>
<DropdownMenuSeparator className="mt-2" />
{groups.map(([name, conf]) => {
return (
<div
key={name}
className="w-full cursor-pointer rounded-lg px-2 py-1.5 text-sm capitalize text-primary hover:bg-muted"
onClick={() => setCurrentCameras([...conf.cameras])}
>
{name}
</div>
);
})}
</>
)}
<DropdownMenuSeparator className="my-2" />
<div className="flex flex-col gap-2.5">
{allCameras.map((item) => (
<FilterSwitch
key={item}
isChecked={currentCameras?.includes(item) ?? false}
label={item.replaceAll("_", " ")}
onCheckedChange={(isChecked) => {
if (isChecked) {
const updatedCameras = currentCameras
? [...currentCameras]
: [];
updatedCameras.push(item);
setCurrentCameras(updatedCameras);
} else {
const updatedCameras = currentCameras
? [...currentCameras]
: [];
// can not deselect the last item
if (updatedCameras.length > 1) {
updatedCameras.splice(updatedCameras.indexOf(item), 1);
setCurrentCameras(updatedCameras);
}
}
}}
/>
))}
</div>
</div>
<DropdownMenuSeparator className="my-2" />
<div className="flex items-center justify-evenly p-2">
<Button
variant="select"
onClick={() => {
updateCameraFilter(currentCameras);
setOpen(false);
}}
>
Apply
</Button>
<Button
onClick={() => {
setCurrentCameras(undefined);
updateCameraFilter(undefined);
}}
>
Reset
</Button>
</div>
</>
);
if (isMobile) {
return (
<Drawer
open={open}
onOpenChange={(open) => {
if (!open) {
setCurrentCameras(selectedCameras);
}
setOpen(open);
}}
>
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent className="max-h-[75dvh] overflow-hidden">
{content}
</DrawerContent>
</Drawer>
);
}
return (
<DropdownMenu
modal={false}
open={open}
onOpenChange={(open) => {
if (!open) {
setCurrentCameras(selectedCameras);
}
setOpen(open);
}}
>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent>{content}</DropdownMenuContent>
</DropdownMenu>
);
}
type ShowReviewedFilterProps = { type ShowReviewedFilterProps = {
showReviewed?: 0 | 1; showReviewed?: 0 | 1;
setShowReviewed: (reviewed?: 0 | 1) => void; setShowReviewed: (reviewed?: 0 | 1) => void;
@ -458,74 +284,6 @@ function ShowReviewFilter({
); );
} }
type CalendarFilterButtonProps = {
reviewSummary?: ReviewSummary;
day?: Date;
updateSelectedDay: (day?: Date) => void;
};
function CalendarFilterButton({
reviewSummary,
day,
updateSelectedDay,
}: CalendarFilterButtonProps) {
const selectedDate = useFormattedTimestamp(
day == undefined ? 0 : day?.getTime() / 1000 + 1,
"%b %-d",
);
const trigger = (
<Button
className="flex items-center gap-2"
variant={day == undefined ? "default" : "select"}
size="sm"
>
<FaCalendarAlt
className={`${day == undefined ? "text-secondary-foreground" : "text-selected-foreground"}`}
/>
<div
className={`hidden md:block ${day == undefined ? "text-primary" : "text-selected-foreground"}`}
>
{day == undefined ? "Last 24 Hours" : selectedDate}
</div>
</Button>
);
const content = (
<>
<ReviewActivityCalendar
reviewSummary={reviewSummary}
selectedDay={day}
onSelect={updateSelectedDay}
/>
<DropdownMenuSeparator />
<div className="flex items-center justify-center p-2">
<Button
onClick={() => {
updateSelectedDay(undefined);
}}
>
Reset
</Button>
</div>
</>
);
if (isMobile) {
return (
<Drawer>
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent>{content}</DrawerContent>
</Drawer>
);
}
return (
<Popover>
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent>{content}</PopoverContent>
</Popover>
);
}
type GeneralFilterButtonProps = { type GeneralFilterButtonProps = {
allLabels: string[]; allLabels: string[];
selectedLabels: string[] | undefined; selectedLabels: string[] | undefined;

View File

@ -0,0 +1,491 @@
import { Button } from "../ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import { useCallback, useMemo, useState } from "react";
import { DropdownMenuSeparator } from "../ui/dropdown-menu";
import { ReviewFilter, ReviewSeverity } from "@/types/review";
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
import { FaFilter } from "react-icons/fa";
import { isDesktop, isMobile } from "react-device-detect";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import { Switch } from "../ui/switch";
import { Label } from "../ui/label";
import MobileReviewSettingsDrawer, {
DrawerFeatures,
} from "../overlay/MobileReviewSettingsDrawer";
import FilterSwitch from "./FilterSwitch";
import { FilterList } from "@/types/filter";
import CalendarFilterButton from "./CalendarFilterButton";
import { CamerasFilterButton } from "./CamerasFilterButton";
const SEARCH_FILTERS = ["cameras", "date", "general"] as const;
type SearchFilters = (typeof SEARCH_FILTERS)[number];
const DEFAULT_REVIEW_FILTERS: SearchFilters[] = ["cameras", "date", "general"];
type SearchFilterGroupProps = {
filters?: SearchFilters[];
filter?: ReviewFilter;
filterList?: FilterList;
onUpdateFilter: (filter: ReviewFilter) => void;
setMotionOnly: React.Dispatch<React.SetStateAction<boolean>>;
};
export default function SearchFilterGroup({
filters = DEFAULT_REVIEW_FILTERS,
filter,
filterList,
onUpdateFilter,
}: SearchFilterGroupProps) {
const { data: config } = useSWR<FrigateConfig>("config");
const allLabels = useMemo<string[]>(() => {
if (filterList?.labels) {
return filterList.labels;
}
if (!config) {
return [];
}
const labels = new Set<string>();
const cameras = filter?.cameras || Object.keys(config.cameras);
cameras.forEach((camera) => {
if (camera == "birdseye") {
return;
}
const cameraConfig = config.cameras[camera];
cameraConfig.objects.track.forEach((label) => {
labels.add(label);
});
if (cameraConfig.audio.enabled_in_config) {
cameraConfig.audio.listen.forEach((label) => {
labels.add(label);
});
}
});
return [...labels].sort();
}, [config, filterList, filter]);
const allZones = useMemo<string[]>(() => {
if (filterList?.zones) {
return filterList.zones;
}
if (!config) {
return [];
}
const zones = new Set<string>();
const cameras = filter?.cameras || Object.keys(config.cameras);
cameras.forEach((camera) => {
if (camera == "birdseye") {
return;
}
const cameraConfig = config.cameras[camera];
cameraConfig.review.alerts.required_zones.forEach((zone) => {
zones.add(zone);
});
cameraConfig.review.detections.required_zones.forEach((zone) => {
zones.add(zone);
});
});
return [...zones].sort();
}, [config, filterList, filter]);
const filterValues = useMemo(
() => ({
cameras: Object.keys(config?.cameras || {}),
labels: Object.values(allLabels || {}),
zones: Object.values(allZones || {}),
}),
[config, allLabels, allZones],
);
const groups = useMemo(() => {
if (!config) {
return [];
}
return Object.entries(config.camera_groups).sort(
(a, b) => a[1].order - b[1].order,
);
}, [config]);
const mobileSettingsFeatures = useMemo<DrawerFeatures[]>(() => {
const features: DrawerFeatures[] = [];
if (filters.includes("date")) {
features.push("calendar");
}
if (filters.includes("general")) {
features.push("filter");
}
return features;
}, [filters]);
// handle updating filters
const onUpdateSelectedDay = useCallback(
(day?: Date) => {
onUpdateFilter({
...filter,
after: day == undefined ? undefined : day.getTime() / 1000,
before: day == undefined ? undefined : getEndOfDayTimestamp(day),
});
},
[filter, onUpdateFilter],
);
return (
<div className="flex justify-center gap-2">
{filters.includes("cameras") && (
<CamerasFilterButton
allCameras={filterValues.cameras}
groups={groups}
selectedCameras={filter?.cameras}
updateCameraFilter={(newCameras) => {
onUpdateFilter({ ...filter, cameras: newCameras });
}}
/>
)}
{isDesktop && filters.includes("date") && (
<CalendarFilterButton
day={
filter?.after == undefined
? undefined
: new Date(filter.after * 1000)
}
defaultText="All Dates"
updateSelectedDay={onUpdateSelectedDay}
/>
)}
{isDesktop && filters.includes("general") && (
<GeneralFilterButton
allLabels={filterValues.labels}
selectedLabels={filter?.labels}
showAll={filter?.showAll == true}
allZones={filterValues.zones}
selectedZones={filter?.zones}
setShowAll={(showAll) => {
onUpdateFilter({ ...filter, showAll });
}}
updateLabelFilter={(newLabels) => {
onUpdateFilter({ ...filter, labels: newLabels });
}}
updateZoneFilter={(newZones) =>
onUpdateFilter({ ...filter, zones: newZones })
}
/>
)}
{isMobile && mobileSettingsFeatures.length > 0 && (
<MobileReviewSettingsDrawer
features={mobileSettingsFeatures}
filter={filter}
allLabels={allLabels}
allZones={allZones}
onUpdateFilter={onUpdateFilter}
// not applicable as exports are not used
camera=""
latestTime={0}
currentTime={0}
mode="none"
setMode={() => {}}
setRange={() => {}}
/>
)}
</div>
);
}
type GeneralFilterButtonProps = {
allLabels: string[];
selectedLabels: string[] | undefined;
currentSeverity?: ReviewSeverity;
showAll: boolean;
allZones: string[];
selectedZones?: string[];
setShowAll: (showAll: boolean) => void;
updateLabelFilter: (labels: string[] | undefined) => void;
updateZoneFilter: (zones: string[] | undefined) => void;
};
function GeneralFilterButton({
allLabels,
selectedLabels,
currentSeverity,
showAll,
allZones,
selectedZones,
setShowAll,
updateLabelFilter,
updateZoneFilter,
}: GeneralFilterButtonProps) {
const [open, setOpen] = useState(false);
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
selectedLabels,
);
const [currentZones, setCurrentZones] = useState<string[] | undefined>(
selectedZones,
);
const trigger = (
<Button
size="sm"
variant={
selectedLabels?.length || selectedZones?.length ? "select" : "default"
}
className="flex items-center gap-2 capitalize"
>
<FaFilter
className={`${selectedLabels?.length || selectedZones?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
/>
<div
className={`hidden md:block ${selectedLabels?.length || selectedZones?.length ? "text-selected-foreground" : "text-primary"}`}
>
Filter
</div>
</Button>
);
const content = (
<GeneralFilterContent
allLabels={allLabels}
selectedLabels={selectedLabels}
currentLabels={currentLabels}
currentSeverity={currentSeverity}
showAll={showAll}
allZones={allZones}
selectedZones={selectedZones}
currentZones={currentZones}
setCurrentZones={setCurrentZones}
updateZoneFilter={updateZoneFilter}
setShowAll={setShowAll}
updateLabelFilter={updateLabelFilter}
setCurrentLabels={setCurrentLabels}
onClose={() => setOpen(false)}
/>
);
if (isMobile) {
return (
<Drawer
open={open}
onOpenChange={(open) => {
if (!open) {
setCurrentLabels(selectedLabels);
}
setOpen(open);
}}
>
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent className="max-h-[75dvh] overflow-hidden">
{content}
</DrawerContent>
</Drawer>
);
}
return (
<Popover
open={open}
onOpenChange={(open) => {
if (!open) {
setCurrentLabels(selectedLabels);
}
setOpen(open);
}}
>
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent>{content}</PopoverContent>
</Popover>
);
}
type GeneralFilterContentProps = {
allLabels: string[];
selectedLabels: string[] | undefined;
currentLabels: string[] | undefined;
currentSeverity?: ReviewSeverity;
showAll?: boolean;
allZones?: string[];
selectedZones?: string[];
currentZones?: string[];
setShowAll?: (showAll: boolean) => void;
updateLabelFilter: (labels: string[] | undefined) => void;
setCurrentLabels: (labels: string[] | undefined) => void;
updateZoneFilter?: (zones: string[] | undefined) => void;
setCurrentZones?: (zones: string[] | undefined) => void;
onClose: () => void;
};
export function GeneralFilterContent({
allLabels,
selectedLabels,
currentLabels,
currentSeverity,
showAll,
allZones,
selectedZones,
currentZones,
setShowAll,
updateLabelFilter,
setCurrentLabels,
updateZoneFilter,
setCurrentZones,
onClose,
}: GeneralFilterContentProps) {
return (
<>
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
{currentSeverity && setShowAll && (
<div className="my-2.5 flex flex-col gap-2.5">
<FilterSwitch
label="Alerts"
disabled={currentSeverity == "alert"}
isChecked={currentSeverity == "alert" ? true : showAll == true}
onCheckedChange={setShowAll}
/>
<FilterSwitch
label="Detections"
disabled={currentSeverity == "detection"}
isChecked={
currentSeverity == "detection" ? true : showAll == true
}
onCheckedChange={setShowAll}
/>
<DropdownMenuSeparator />
</div>
)}
<div className="mb-5 mt-2.5 flex items-center justify-between">
<Label
className="mx-2 cursor-pointer text-primary"
htmlFor="allLabels"
>
All Labels
</Label>
<Switch
className="ml-1"
id="allLabels"
checked={currentLabels == undefined}
onCheckedChange={(isChecked) => {
if (isChecked) {
setCurrentLabels(undefined);
}
}}
/>
</div>
<div className="my-2.5 flex flex-col gap-2.5">
{allLabels.map((item) => (
<FilterSwitch
label={item.replaceAll("_", " ")}
isChecked={currentLabels?.includes(item) ?? false}
onCheckedChange={(isChecked) => {
if (isChecked) {
const updatedLabels = currentLabels ? [...currentLabels] : [];
updatedLabels.push(item);
setCurrentLabels(updatedLabels);
} else {
const updatedLabels = currentLabels ? [...currentLabels] : [];
// can not deselect the last item
if (updatedLabels.length > 1) {
updatedLabels.splice(updatedLabels.indexOf(item), 1);
setCurrentLabels(updatedLabels);
}
}
}}
/>
))}
</div>
{allZones && setCurrentZones && (
<>
<DropdownMenuSeparator />
<div className="mb-5 mt-2.5 flex items-center justify-between">
<Label
className="mx-2 cursor-pointer text-primary"
htmlFor="allZones"
>
All Zones
</Label>
<Switch
className="ml-1"
id="allZones"
checked={currentZones == undefined}
onCheckedChange={(isChecked) => {
if (isChecked) {
setCurrentZones(undefined);
}
}}
/>
</div>
<div className="my-2.5 flex flex-col gap-2.5">
{allZones.map((item) => (
<FilterSwitch
label={item.replaceAll("_", " ")}
isChecked={currentZones?.includes(item) ?? false}
onCheckedChange={(isChecked) => {
if (isChecked) {
const updatedZones = currentZones
? [...currentZones]
: [];
updatedZones.push(item);
setCurrentZones(updatedZones);
} else {
const updatedZones = currentZones
? [...currentZones]
: [];
// can not deselect the last item
if (updatedZones.length > 1) {
updatedZones.splice(updatedZones.indexOf(item), 1);
setCurrentZones(updatedZones);
}
}
}}
/>
))}
</div>
</>
)}
</div>
<DropdownMenuSeparator />
<div className="flex items-center justify-evenly p-2">
<Button
variant="select"
onClick={() => {
if (selectedLabels != currentLabels) {
updateLabelFilter(currentLabels);
}
if (updateZoneFilter && selectedZones != currentZones) {
updateZoneFilter(currentZones);
}
onClose();
}}
>
Apply
</Button>
<Button
onClick={() => {
setCurrentLabels(undefined);
setCurrentZones?.(undefined);
updateLabelFilter(undefined);
}}
>
Reset
</Button>
</div>
</>
);
}

View File

@ -1,3 +1,4 @@
import SearchFilterGroup from "@/components/filter/SearchFilterGroup";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import { useState } from "react"; import { useState } from "react";
@ -16,7 +17,9 @@ export default function Search() {
placeholder="Search for a specific detection..." placeholder="Search for a specific detection..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
/> />
<SearchFilterGroup />
</div> </div>
<div className="no-scrollbar flex flex-1 flex-wrap content-start gap-2 overflow-y-auto md:gap-4"> <div className="no-scrollbar flex flex-1 flex-wrap content-start gap-2 overflow-y-auto md:gap-4">

View File

@ -1,8 +1,6 @@
import { baseUrl } from "@/api/baseUrl"; import { baseUrl } from "@/api/baseUrl";
import { import { CamerasFilterButton } from "@/components/filter/CamerasFilterButton";
CamerasFilterButton, import { GeneralFilterContent } from "@/components/filter/ReviewFilterGroup";
GeneralFilterContent,
} from "@/components/filter/ReviewFilterGroup";
import Chip from "@/components/indicators/Chip"; import Chip from "@/components/indicators/Chip";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";