Add support for selecting zones

This commit is contained in:
Nicolas Mowen 2024-06-11 07:47:03 -06:00
parent 8ecae9dd98
commit b3b4f049c9
4 changed files with 131 additions and 10 deletions

View File

@ -30,6 +30,7 @@ import MobileReviewSettingsDrawer, {
} 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";
const REVIEW_FILTERS = [ const REVIEW_FILTERS = [
"cameras", "cameras",
@ -53,7 +54,7 @@ type ReviewFilterGroupProps = {
reviewSummary?: ReviewSummary; reviewSummary?: ReviewSummary;
filter?: ReviewFilter; filter?: ReviewFilter;
motionOnly: boolean; motionOnly: boolean;
filterLabels?: string[]; filterList?: FilterList;
onUpdateFilter: (filter: ReviewFilter) => void; onUpdateFilter: (filter: ReviewFilter) => void;
setMotionOnly: React.Dispatch<React.SetStateAction<boolean>>; setMotionOnly: React.Dispatch<React.SetStateAction<boolean>>;
}; };
@ -64,15 +65,15 @@ export default function ReviewFilterGroup({
reviewSummary, reviewSummary,
filter, filter,
motionOnly, motionOnly,
filterLabels, filterList,
onUpdateFilter, onUpdateFilter,
setMotionOnly, setMotionOnly,
}: ReviewFilterGroupProps) { }: ReviewFilterGroupProps) {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const allLabels = useMemo<string[]>(() => { const allLabels = useMemo<string[]>(() => {
if (filterLabels) { if (filterList?.labels) {
return filterLabels; return filterList.labels;
} }
if (!config) { if (!config) {
@ -99,14 +100,43 @@ export default function ReviewFilterGroup({
}); });
return [...labels].sort(); return [...labels].sort();
}, [config, filterLabels, filter]); }, [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( const filterValues = useMemo(
() => ({ () => ({
cameras: Object.keys(config?.cameras || {}), cameras: Object.keys(config?.cameras || {}),
labels: Object.values(allLabels || {}), labels: Object.values(allLabels || {}),
zones: Object.values(allZones || {}),
}), }),
[config, allLabels], [config, allLabels, allZones],
); );
const groups = useMemo(() => { const groups = useMemo(() => {
@ -189,12 +219,17 @@ export default function ReviewFilterGroup({
selectedLabels={filter?.labels} selectedLabels={filter?.labels}
currentSeverity={currentSeverity} currentSeverity={currentSeverity}
showAll={filter?.showAll == true} showAll={filter?.showAll == true}
allZones={filterValues.zones}
selectedZones={filter?.zones}
setShowAll={(showAll) => { setShowAll={(showAll) => {
onUpdateFilter({ ...filter, showAll }); onUpdateFilter({ ...filter, showAll });
}} }}
updateLabelFilter={(newLabels) => { updateLabelFilter={(newLabels) => {
onUpdateFilter({ ...filter, labels: newLabels }); onUpdateFilter({ ...filter, labels: newLabels });
}} }}
updateZoneFilter={(newZones) =>
onUpdateFilter({ ...filter, zones: newZones })
}
/> />
)} )}
{isMobile && mobileSettingsFeatures.length > 0 && ( {isMobile && mobileSettingsFeatures.length > 0 && (
@ -495,21 +530,30 @@ type GeneralFilterButtonProps = {
selectedLabels: string[] | undefined; selectedLabels: string[] | undefined;
currentSeverity?: ReviewSeverity; currentSeverity?: ReviewSeverity;
showAll: boolean; showAll: boolean;
allZones: string[];
selectedZones?: string[];
setShowAll: (showAll: boolean) => void; setShowAll: (showAll: boolean) => void;
updateLabelFilter: (labels: string[] | undefined) => void; updateLabelFilter: (labels: string[] | undefined) => void;
updateZoneFilter: (zones: string[] | undefined) => void;
}; };
function GeneralFilterButton({ function GeneralFilterButton({
allLabels, allLabels,
selectedLabels, selectedLabels,
currentSeverity, currentSeverity,
showAll, showAll,
allZones,
selectedZones,
setShowAll, setShowAll,
updateLabelFilter, updateLabelFilter,
updateZoneFilter,
}: GeneralFilterButtonProps) { }: GeneralFilterButtonProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>( const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
selectedLabels, selectedLabels,
); );
const [currentZones, setCurrentZones] = useState<string[] | undefined>(
selectedZones,
);
const trigger = ( const trigger = (
<Button <Button
@ -534,6 +578,11 @@ function GeneralFilterButton({
currentLabels={currentLabels} currentLabels={currentLabels}
currentSeverity={currentSeverity} currentSeverity={currentSeverity}
showAll={showAll} showAll={showAll}
allZones={allZones}
selectedZones={selectedZones}
currentZones={currentZones}
setCurrentZones={setCurrentZones}
updateZoneFilter={updateZoneFilter}
setShowAll={setShowAll} setShowAll={setShowAll}
updateLabelFilter={updateLabelFilter} updateLabelFilter={updateLabelFilter}
setCurrentLabels={setCurrentLabels} setCurrentLabels={setCurrentLabels}
@ -584,9 +633,14 @@ type GeneralFilterContentProps = {
currentLabels: string[] | undefined; currentLabels: string[] | undefined;
currentSeverity?: ReviewSeverity; currentSeverity?: ReviewSeverity;
showAll?: boolean; showAll?: boolean;
allZones?: string[];
selectedZones?: string[];
currentZones?: string[];
setShowAll?: (showAll: boolean) => void; setShowAll?: (showAll: boolean) => void;
updateLabelFilter: (labels: string[] | undefined) => void; updateLabelFilter: (labels: string[] | undefined) => void;
setCurrentLabels: (labels: string[] | undefined) => void; setCurrentLabels: (labels: string[] | undefined) => void;
updateZoneFilter?: (zones: string[] | undefined) => void;
setCurrentZones?: (zones: string[] | undefined) => void;
onClose: () => void; onClose: () => void;
}; };
export function GeneralFilterContent({ export function GeneralFilterContent({
@ -595,9 +649,14 @@ export function GeneralFilterContent({
currentLabels, currentLabels,
currentSeverity, currentSeverity,
showAll, showAll,
allZones,
selectedZones,
currentZones,
setShowAll, setShowAll,
updateLabelFilter, updateLabelFilter,
setCurrentLabels, setCurrentLabels,
updateZoneFilter,
setCurrentZones,
onClose, onClose,
}: GeneralFilterContentProps) { }: GeneralFilterContentProps) {
return ( return (
@ -640,7 +699,6 @@ export function GeneralFilterContent({
}} }}
/> />
</div> </div>
<DropdownMenuSeparator />
<div className="my-2.5 flex flex-col gap-2.5"> <div className="my-2.5 flex flex-col gap-2.5">
{allLabels.map((item) => ( {allLabels.map((item) => (
<FilterSwitch <FilterSwitch
@ -666,6 +724,53 @@ export function GeneralFilterContent({
))} ))}
</div> </div>
</div> </div>
{allZones && setCurrentZones && (
<>
<DropdownMenuSeparator />
<div className="my-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>
</>
)}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<div className="flex items-center justify-evenly p-2"> <div className="flex items-center justify-evenly p-2">
<Button <Button
@ -675,6 +780,10 @@ export function GeneralFilterContent({
updateLabelFilter(currentLabels); updateLabelFilter(currentLabels);
} }
if (updateZoneFilter && selectedZones != currentZones) {
updateZoneFilter(currentZones);
}
onClose(); onClose();
}} }}
> >

View File

@ -3,3 +3,8 @@
export type FilterType = { [searchKey: string]: any }; export type FilterType = { [searchKey: string]: any };
export type ExportMode = "select" | "timeline" | "none"; export type ExportMode = "select" | "timeline" | "none";
export type FilterList = {
labels?: string[];
zones?: string[];
};

View File

@ -32,6 +32,7 @@ export type SegmentedReviewData =
export type ReviewFilter = { export type ReviewFilter = {
cameras?: string[]; cameras?: string[];
labels?: string[]; labels?: string[];
zones?: string[];
before?: number; before?: number;
after?: number; after?: number;
showReviewed?: 0 | 1; showReviewed?: 0 | 1;

View File

@ -49,6 +49,7 @@ import scrollIntoView from "scroll-into-view-if-needed";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import { toast } from "sonner"; import { toast } from "sonner";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { FilterList } from "@/types/filter";
type EventViewProps = { type EventViewProps = {
reviewItems?: SegmentedReviewData; reviewItems?: SegmentedReviewData;
@ -203,8 +204,9 @@ export default function EventView({
// review filter info // review filter info
const reviewLabels = useMemo(() => { const reviewFilterList = useMemo<FilterList>(() => {
const uniqueLabels = new Set<string>(); const uniqueLabels = new Set<string>();
const uniqueZones = new Set<string>();
reviewItems?.all?.forEach((rev) => { reviewItems?.all?.forEach((rev) => {
rev.data.objects.forEach((obj) => rev.data.objects.forEach((obj) =>
@ -213,7 +215,11 @@ export default function EventView({
rev.data.audio.forEach((aud) => uniqueLabels.add(aud)); rev.data.audio.forEach((aud) => uniqueLabels.add(aud));
}); });
return [...uniqueLabels]; reviewItems?.all?.forEach((rev) => {
rev.data.zones.forEach((zone) => uniqueZones.add(zone));
});
return { labels: [...uniqueLabels], zones: [...uniqueZones] };
}, [reviewItems]); }, [reviewItems]);
if (!config) { if (!config) {
@ -282,7 +288,7 @@ export default function EventView({
reviewSummary={reviewSummary} reviewSummary={reviewSummary}
filter={filter} filter={filter}
motionOnly={motionOnly} motionOnly={motionOnly}
filterLabels={reviewLabels} filterList={reviewFilterList}
onUpdateFilter={updateFilter} onUpdateFilter={updateFilter}
setMotionOnly={setMotionOnly} setMotionOnly={setMotionOnly}
/> />