From e292d639a7c1e7baae157701ccb31fb2de85588d Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:52:32 -0500 Subject: [PATCH] add filterable scroll list to more filters pane for identifiers --- frigate/api/event.py | 54 +++---- .../overlay/dialog/SearchFilterDialog.tsx | 133 +++++++++++++++++- 2 files changed, 152 insertions(+), 35 deletions(-) diff --git a/frigate/api/event.py b/frigate/api/event.py index f6f497061..e9cf2fea4 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -163,7 +163,6 @@ def events(params: EventsQueryParams = Depends()): # use matching so joined identifiers are included # for example an identifier 'ABC123' would get events # with identifiers 'ABC123' and 'ABC123, XYZ789' - # also supports regex with slashes before and after the pattern identifier_clauses = [] filtered_identifiers = identifier.split(",") @@ -172,22 +171,16 @@ def events(params: EventsQueryParams = Depends()): identifier_clauses.append((Event.data["identifier"].is_null())) for identifier in filtered_identifiers: - if identifier.startswith("r:"): # Regex pattern - pattern = identifier[2:] # Strip the "r:" prefix - identifier_clauses.append( - (Event.data["identifier"].cast("text").regexp(pattern)) - ) - print(pattern) - else: # Regular exact matching plus list inclusion - identifier_clauses.append( - (Event.data["identifier"].cast("text") == identifier) - ) - identifier_clauses.append( - (Event.data["identifier"].cast("text") % f"*{identifier},*") - ) - identifier_clauses.append( - (Event.data["identifier"].cast("text") % f"*, {identifier}*") - ) + # Exact matching plus list inclusion + identifier_clauses.append( + (Event.data["identifier"].cast("text") == identifier) + ) + identifier_clauses.append( + (Event.data["identifier"].cast("text") % f"*{identifier},*") + ) + identifier_clauses.append( + (Event.data["identifier"].cast("text") % f"*, {identifier}*") + ) identifier_clause = reduce(operator.or_, identifier_clauses) clauses.append((identifier_clause)) @@ -507,7 +500,6 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) # use matching so joined identifiers are included # for example an identifier 'ABC123' would get events # with identifiers 'ABC123' and 'ABC123, XYZ789' - # also supports regex with slashes before and after the pattern identifier_clauses = [] filtered_identifiers = identifier.split(",") @@ -516,22 +508,16 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) identifier_clauses.append((Event.data["identifier"].is_null())) for identifier in filtered_identifiers: - if identifier.startswith("r:"): # Regex pattern - pattern = identifier[2:] # Strip the "r:" prefix - identifier_clauses.append( - (Event.data["identifier"].cast("text").regexp(pattern)) - ) - print(pattern) - else: # Regular exact matching plus list inclusion - identifier_clauses.append( - (Event.data["identifier"].cast("text") == identifier) - ) - identifier_clauses.append( - (Event.data["identifier"].cast("text") % f"*{identifier},*") - ) - identifier_clauses.append( - (Event.data["identifier"].cast("text") % f"*, {identifier}*") - ) + # Exact matching plus list inclusion + identifier_clauses.append( + (Event.data["identifier"].cast("text") == identifier) + ) + identifier_clauses.append( + (Event.data["identifier"].cast("text") % f"*{identifier},*") + ) + identifier_clauses.append( + (Event.data["identifier"].cast("text") % f"*, {identifier}*") + ) identifier_clause = reduce(operator.or_, identifier_clauses) event_filters.append((identifier_clause)) diff --git a/web/src/components/overlay/dialog/SearchFilterDialog.tsx b/web/src/components/overlay/dialog/SearchFilterDialog.tsx index 23deee531..59b30f82f 100644 --- a/web/src/components/overlay/dialog/SearchFilterDialog.tsx +++ b/web/src/components/overlay/dialog/SearchFilterDialog.tsx @@ -33,6 +33,14 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { + Command, + CommandEmpty, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { LuCheck } from "react-icons/lu"; type SearchFilterDialogProps = { config?: FrigateConfig; @@ -77,7 +85,8 @@ export default function SearchFilterDialog({ (currentFilter.max_score ?? 1) < 1 || (currentFilter.max_speed ?? 150) < 150 || (currentFilter.zones?.length ?? 0) > 0 || - (currentFilter.sub_labels?.length ?? 0) > 0), + (currentFilter.sub_labels?.length ?? 0) > 0 || + (currentFilter.identifier?.length ?? 0) > 0), [currentFilter], ); @@ -119,6 +128,12 @@ export default function SearchFilterDialog({ setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels }) } /> + + setCurrentFilter({ ...currentFilter, identifier: identifiers }) + } + /> @@ -830,3 +846,118 @@ export function SnapshotClipFilterContent({ ); } + +type IdentifierFilterContentProps = { + identifiers: string[] | undefined; + setIdentifiers: (identifiers: string[] | undefined) => void; +}; + +export function IdentifierFilterContent({ + identifiers, + setIdentifiers, +}: IdentifierFilterContentProps) { + const { data: allIdentifiers, error } = useSWR("identifiers", { + revalidateOnFocus: false, + }); + + const [selectedIdentifiers, setSelectedIdentifiers] = useState( + identifiers || [], + ); + const [inputValue, setInputValue] = useState(""); + + useEffect(() => { + if (identifiers) { + setSelectedIdentifiers(identifiers); + } else { + setSelectedIdentifiers([]); + } + }, [identifiers]); + + const handleSelect = (identifier: string) => { + const newSelected = selectedIdentifiers.includes(identifier) + ? selectedIdentifiers.filter((id) => id !== identifier) // Deselect + : [...selectedIdentifiers, identifier]; // Select + + setSelectedIdentifiers(newSelected); + if (newSelected.length === 0) { + setIdentifiers(undefined); // Clear filter if no identifiers selected + } else { + setIdentifiers(newSelected); + } + }; + + if (!allIdentifiers || allIdentifiers.length === 0) { + return null; + } + + const filteredIdentifiers = + allIdentifiers?.filter((id) => + id.toLowerCase().includes(inputValue.toLowerCase()), + ) || []; + + return ( +
+ +
Identifiers
+ {error ? ( +

Failed to load identifiers

+ ) : !allIdentifiers ? ( +

Loading identifiers...

+ ) : ( + <> + + + + {filteredIdentifiers.length === 0 && inputValue && ( + No identifiers found. + )} + {filteredIdentifiers.map((identifier) => ( + handleSelect(identifier)} + className="cursor-pointer" + > + + {identifier} + + ))} + + + {selectedIdentifiers.length > 0 && ( +
+ {selectedIdentifiers.map((id) => ( + + {id} + + + ))} +
+ )} + + )} +

+ Select one or more identifiers from the list. +

+
+ ); +}