mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-06 05:27:44 +03:00
add filterable scroll list to more filters pane for identifiers
This commit is contained in:
parent
b11149937c
commit
e292d639a7
@ -163,7 +163,6 @@ def events(params: EventsQueryParams = Depends()):
|
|||||||
# use matching so joined identifiers are included
|
# use matching so joined identifiers are included
|
||||||
# for example an identifier 'ABC123' would get events
|
# for example an identifier 'ABC123' would get events
|
||||||
# with identifiers 'ABC123' and 'ABC123, XYZ789'
|
# with identifiers 'ABC123' and 'ABC123, XYZ789'
|
||||||
# also supports regex with slashes before and after the pattern
|
|
||||||
identifier_clauses = []
|
identifier_clauses = []
|
||||||
filtered_identifiers = identifier.split(",")
|
filtered_identifiers = identifier.split(",")
|
||||||
|
|
||||||
@ -172,22 +171,16 @@ def events(params: EventsQueryParams = Depends()):
|
|||||||
identifier_clauses.append((Event.data["identifier"].is_null()))
|
identifier_clauses.append((Event.data["identifier"].is_null()))
|
||||||
|
|
||||||
for identifier in filtered_identifiers:
|
for identifier in filtered_identifiers:
|
||||||
if identifier.startswith("r:"): # Regex pattern
|
# Exact matching plus list inclusion
|
||||||
pattern = identifier[2:] # Strip the "r:" prefix
|
identifier_clauses.append(
|
||||||
identifier_clauses.append(
|
(Event.data["identifier"].cast("text") == identifier)
|
||||||
(Event.data["identifier"].cast("text").regexp(pattern))
|
)
|
||||||
)
|
identifier_clauses.append(
|
||||||
print(pattern)
|
(Event.data["identifier"].cast("text") % f"*{identifier},*")
|
||||||
else: # Regular exact matching plus list inclusion
|
)
|
||||||
identifier_clauses.append(
|
identifier_clauses.append(
|
||||||
(Event.data["identifier"].cast("text") == identifier)
|
(Event.data["identifier"].cast("text") % f"*, {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)
|
identifier_clause = reduce(operator.or_, identifier_clauses)
|
||||||
clauses.append((identifier_clause))
|
clauses.append((identifier_clause))
|
||||||
@ -507,7 +500,6 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
|
|||||||
# use matching so joined identifiers are included
|
# use matching so joined identifiers are included
|
||||||
# for example an identifier 'ABC123' would get events
|
# for example an identifier 'ABC123' would get events
|
||||||
# with identifiers 'ABC123' and 'ABC123, XYZ789'
|
# with identifiers 'ABC123' and 'ABC123, XYZ789'
|
||||||
# also supports regex with slashes before and after the pattern
|
|
||||||
identifier_clauses = []
|
identifier_clauses = []
|
||||||
filtered_identifiers = identifier.split(",")
|
filtered_identifiers = identifier.split(",")
|
||||||
|
|
||||||
@ -516,22 +508,16 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
|
|||||||
identifier_clauses.append((Event.data["identifier"].is_null()))
|
identifier_clauses.append((Event.data["identifier"].is_null()))
|
||||||
|
|
||||||
for identifier in filtered_identifiers:
|
for identifier in filtered_identifiers:
|
||||||
if identifier.startswith("r:"): # Regex pattern
|
# Exact matching plus list inclusion
|
||||||
pattern = identifier[2:] # Strip the "r:" prefix
|
identifier_clauses.append(
|
||||||
identifier_clauses.append(
|
(Event.data["identifier"].cast("text") == identifier)
|
||||||
(Event.data["identifier"].cast("text").regexp(pattern))
|
)
|
||||||
)
|
identifier_clauses.append(
|
||||||
print(pattern)
|
(Event.data["identifier"].cast("text") % f"*{identifier},*")
|
||||||
else: # Regular exact matching plus list inclusion
|
)
|
||||||
identifier_clauses.append(
|
identifier_clauses.append(
|
||||||
(Event.data["identifier"].cast("text") == identifier)
|
(Event.data["identifier"].cast("text") % f"*, {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)
|
identifier_clause = reduce(operator.or_, identifier_clauses)
|
||||||
event_filters.append((identifier_clause))
|
event_filters.append((identifier_clause))
|
||||||
|
|||||||
@ -33,6 +33,14 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import { LuCheck } from "react-icons/lu";
|
||||||
|
|
||||||
type SearchFilterDialogProps = {
|
type SearchFilterDialogProps = {
|
||||||
config?: FrigateConfig;
|
config?: FrigateConfig;
|
||||||
@ -77,7 +85,8 @@ export default function SearchFilterDialog({
|
|||||||
(currentFilter.max_score ?? 1) < 1 ||
|
(currentFilter.max_score ?? 1) < 1 ||
|
||||||
(currentFilter.max_speed ?? 150) < 150 ||
|
(currentFilter.max_speed ?? 150) < 150 ||
|
||||||
(currentFilter.zones?.length ?? 0) > 0 ||
|
(currentFilter.zones?.length ?? 0) > 0 ||
|
||||||
(currentFilter.sub_labels?.length ?? 0) > 0),
|
(currentFilter.sub_labels?.length ?? 0) > 0 ||
|
||||||
|
(currentFilter.identifier?.length ?? 0) > 0),
|
||||||
[currentFilter],
|
[currentFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -119,6 +128,12 @@ export default function SearchFilterDialog({
|
|||||||
setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels })
|
setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<IdentifierFilterContent
|
||||||
|
identifiers={currentFilter.identifier}
|
||||||
|
setIdentifiers={(identifiers) =>
|
||||||
|
setCurrentFilter({ ...currentFilter, identifier: identifiers })
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ScoreFilterContent
|
<ScoreFilterContent
|
||||||
minScore={currentFilter.min_score}
|
minScore={currentFilter.min_score}
|
||||||
maxScore={currentFilter.max_score}
|
maxScore={currentFilter.max_score}
|
||||||
@ -192,6 +207,7 @@ export default function SearchFilterDialog({
|
|||||||
max_speed: undefined,
|
max_speed: undefined,
|
||||||
has_snapshot: undefined,
|
has_snapshot: undefined,
|
||||||
has_clip: undefined,
|
has_clip: undefined,
|
||||||
|
identifier: undefined,
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -830,3 +846,118 @@ export function SnapshotClipFilterContent({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IdentifierFilterContentProps = {
|
||||||
|
identifiers: string[] | undefined;
|
||||||
|
setIdentifiers: (identifiers: string[] | undefined) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function IdentifierFilterContent({
|
||||||
|
identifiers,
|
||||||
|
setIdentifiers,
|
||||||
|
}: IdentifierFilterContentProps) {
|
||||||
|
const { data: allIdentifiers, error } = useSWR<string[]>("identifiers", {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedIdentifiers, setSelectedIdentifiers] = useState<string[]>(
|
||||||
|
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 (
|
||||||
|
<div className="overflow-x-hidden">
|
||||||
|
<DropdownMenuSeparator className="mb-3" />
|
||||||
|
<div className="mb-3 text-lg">Identifiers</div>
|
||||||
|
{error ? (
|
||||||
|
<p className="text-sm text-red-500">Failed to load identifiers</p>
|
||||||
|
) : !allIdentifiers ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Loading identifiers...</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Command className="border border-input bg-background">
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Type to search identifiers..."
|
||||||
|
value={inputValue}
|
||||||
|
onValueChange={setInputValue}
|
||||||
|
/>
|
||||||
|
<CommandList className="max-h-[200px] overflow-auto">
|
||||||
|
{filteredIdentifiers.length === 0 && inputValue && (
|
||||||
|
<CommandEmpty>No identifiers found.</CommandEmpty>
|
||||||
|
)}
|
||||||
|
{filteredIdentifiers.map((identifier) => (
|
||||||
|
<CommandItem
|
||||||
|
key={identifier}
|
||||||
|
value={identifier}
|
||||||
|
onSelect={() => handleSelect(identifier)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<LuCheck
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
selectedIdentifiers.includes(identifier)
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{identifier}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
{selectedIdentifiers.length > 0 && (
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{selectedIdentifiers.map((id) => (
|
||||||
|
<span
|
||||||
|
key={id}
|
||||||
|
className="inline-flex items-center rounded bg-selected px-2 py-1 text-sm text-white"
|
||||||
|
>
|
||||||
|
{id}
|
||||||
|
<button
|
||||||
|
onClick={() => handleSelect(id)}
|
||||||
|
className="ml-1 text-white hover:text-gray-200"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
Select one or more identifiers from the list.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user