Add ability to filter Explore by Frigate+ submission status (#14909)

* backend

* add is_submitted to query params

* add submitted filter to dialog

* allow is_submitted filter selection with input
This commit is contained in:
Josh Hawkins 2024-11-10 16:57:11 -06:00 committed by GitHub
parent c1bfc1df67
commit 0829517b72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 135 additions and 13 deletions

View File

@ -47,6 +47,7 @@ class EventsSearchQueryParams(BaseModel):
time_range: Optional[str] = DEFAULT_TIME_RANGE time_range: Optional[str] = DEFAULT_TIME_RANGE
has_clip: Optional[bool] = None has_clip: Optional[bool] = None
has_snapshot: Optional[bool] = None has_snapshot: Optional[bool] = None
is_submitted: Optional[bool] = None
timezone: Optional[str] = "utc" timezone: Optional[str] = "utc"
min_score: Optional[float] = None min_score: Optional[float] = None
max_score: Optional[float] = None max_score: Optional[float] = None

View File

@ -360,6 +360,7 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
time_range = params.time_range time_range = params.time_range
has_clip = params.has_clip has_clip = params.has_clip
has_snapshot = params.has_snapshot has_snapshot = params.has_snapshot
is_submitted = params.is_submitted
# for similarity search # for similarity search
event_id = params.event_id event_id = params.event_id
@ -441,6 +442,12 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
if has_snapshot is not None: if has_snapshot is not None:
event_filters.append((Event.has_snapshot == has_snapshot)) event_filters.append((Event.has_snapshot == has_snapshot))
if is_submitted is not None:
if is_submitted == 0:
event_filters.append((Event.plus_id.is_null()))
elif is_submitted > 0:
event_filters.append((Event.plus_id != ""))
if min_score is not None and max_score is not None: if min_score is not None and max_score is not None:
event_filters.append((Event.data["score"].between(min_score, max_score))) event_filters.append((Event.data["score"].between(min_score, max_score)))
else: else:

View File

@ -194,6 +194,11 @@ export default function InputWithTags({
if (newFilters[filterType] === filterValue) { if (newFilters[filterType] === filterValue) {
delete newFilters[filterType]; delete newFilters[filterType];
} }
} else if (filterType === "has_snapshot") {
if (newFilters[filterType] === filterValue) {
delete newFilters[filterType];
delete newFilters["is_submitted"];
}
} else { } else {
delete newFilters[filterType]; delete newFilters[filterType];
} }
@ -307,6 +312,10 @@ export default function InputWithTags({
if (!newFilters.has_snapshot) newFilters.has_snapshot = undefined; if (!newFilters.has_snapshot) newFilters.has_snapshot = undefined;
newFilters.has_snapshot = value == "yes" ? 1 : 0; newFilters.has_snapshot = value == "yes" ? 1 : 0;
break; break;
case "is_submitted":
if (!newFilters.is_submitted) newFilters.is_submitted = undefined;
newFilters.is_submitted = value == "yes" ? 1 : 0;
break;
case "has_clip": case "has_clip":
if (!newFilters.has_clip) newFilters.has_clip = undefined; if (!newFilters.has_clip) newFilters.has_clip = undefined;
newFilters.has_clip = value == "yes" ? 1 : 0; newFilters.has_clip = value == "yes" ? 1 : 0;
@ -356,7 +365,11 @@ export default function InputWithTags({
}`; }`;
} else if (filterType === "min_score" || filterType === "max_score") { } else if (filterType === "min_score" || filterType === "max_score") {
return Math.round(Number(filterValues) * 100).toString() + "%"; return Math.round(Number(filterValues) * 100).toString() + "%";
} else if (filterType === "has_clip" || filterType === "has_snapshot") { } else if (
filterType === "has_clip" ||
filterType === "has_snapshot" ||
filterType === "is_submitted"
) {
return filterValues ? "Yes" : "No"; return filterValues ? "Yes" : "No";
} else { } else {
return filterValues as string; return filterValues as string;
@ -774,7 +787,9 @@ export default function InputWithTags({
> >
{filterType === "event_id" {filterType === "event_id"
? "Tracked Object ID" ? "Tracked Object ID"
: filterType.replaceAll("_", " ")} : filterType === "is_submitted"
? "Submitted to Frigate+"
: filterType.replaceAll("_", " ")}
: {formatFilterValues(filterType, filterValues)} : {formatFilterValues(filterType, filterValues)}
<button <button
onClick={() => onClick={() =>

View File

@ -119,6 +119,7 @@ export default function SearchFilterDialog({
} }
/> />
<SnapshotClipFilterContent <SnapshotClipFilterContent
config={config}
hasSnapshot={ hasSnapshot={
currentFilter.has_snapshot !== undefined currentFilter.has_snapshot !== undefined
? currentFilter.has_snapshot === 1 ? currentFilter.has_snapshot === 1
@ -129,12 +130,19 @@ export default function SearchFilterDialog({
? currentFilter.has_clip === 1 ? currentFilter.has_clip === 1
: undefined : undefined
} }
setSnapshotClip={(snapshot, clip) => submittedToFrigatePlus={
currentFilter.is_submitted !== undefined
? currentFilter.is_submitted === 1
: undefined
}
setSnapshotClip={(snapshot, clip, submitted) =>
setCurrentFilter({ setCurrentFilter({
...currentFilter, ...currentFilter,
has_snapshot: has_snapshot:
snapshot !== undefined ? (snapshot ? 1 : 0) : undefined, snapshot !== undefined ? (snapshot ? 1 : 0) : undefined,
has_clip: clip !== undefined ? (clip ? 1 : 0) : undefined, has_clip: clip !== undefined ? (clip ? 1 : 0) : undefined,
is_submitted:
submitted !== undefined ? (submitted ? 1 : 0) : undefined,
}) })
} }
/> />
@ -508,17 +516,22 @@ export function ScoreFilterContent({
} }
type SnapshotClipContentProps = { type SnapshotClipContentProps = {
config?: FrigateConfig;
hasSnapshot: boolean | undefined; hasSnapshot: boolean | undefined;
hasClip: boolean | undefined; hasClip: boolean | undefined;
submittedToFrigatePlus: boolean | undefined;
setSnapshotClip: ( setSnapshotClip: (
snapshot: boolean | undefined, snapshot: boolean | undefined,
clip: boolean | undefined, clip: boolean | undefined,
submittedToFrigate: boolean | undefined,
) => void; ) => void;
}; };
function SnapshotClipFilterContent({ export function SnapshotClipFilterContent({
config,
hasSnapshot, hasSnapshot,
hasClip, hasClip,
submittedToFrigatePlus,
setSnapshotClip, setSnapshotClip,
}: SnapshotClipContentProps) { }: SnapshotClipContentProps) {
const [isSnapshotFilterActive, setIsSnapshotFilterActive] = useState( const [isSnapshotFilterActive, setIsSnapshotFilterActive] = useState(
@ -527,6 +540,11 @@ function SnapshotClipFilterContent({
const [isClipFilterActive, setIsClipFilterActive] = useState( const [isClipFilterActive, setIsClipFilterActive] = useState(
hasClip !== undefined, hasClip !== undefined,
); );
const [isFrigatePlusFilterActive, setIsFrigatePlusFilterActive] = useState(
submittedToFrigatePlus !== undefined &&
isSnapshotFilterActive &&
hasSnapshot === true,
);
useEffect(() => { useEffect(() => {
setIsSnapshotFilterActive(hasSnapshot !== undefined); setIsSnapshotFilterActive(hasSnapshot !== undefined);
@ -536,6 +554,14 @@ function SnapshotClipFilterContent({
setIsClipFilterActive(hasClip !== undefined); setIsClipFilterActive(hasClip !== undefined);
}, [hasClip]); }, [hasClip]);
useEffect(() => {
setIsFrigatePlusFilterActive(
submittedToFrigatePlus !== undefined &&
isSnapshotFilterActive &&
hasSnapshot === true,
);
}, [submittedToFrigatePlus, isSnapshotFilterActive, hasSnapshot]);
return ( return (
<div className="overflow-x-hidden"> <div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" /> <DropdownMenuSeparator className="mb-3" />
@ -551,9 +577,9 @@ function SnapshotClipFilterContent({
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
setIsSnapshotFilterActive(checked as boolean); setIsSnapshotFilterActive(checked as boolean);
if (checked) { if (checked) {
setSnapshotClip(true, hasClip); setSnapshotClip(true, hasClip, submittedToFrigatePlus);
} else { } else {
setSnapshotClip(undefined, hasClip); setSnapshotClip(undefined, hasClip, undefined);
} }
}} }}
/> />
@ -570,8 +596,10 @@ function SnapshotClipFilterContent({
hasSnapshot === undefined ? undefined : hasSnapshot ? "yes" : "no" hasSnapshot === undefined ? undefined : hasSnapshot ? "yes" : "no"
} }
onValueChange={(value) => { onValueChange={(value) => {
if (value === "yes") setSnapshotClip(true, hasClip); if (value === "yes")
else if (value === "no") setSnapshotClip(false, hasClip); setSnapshotClip(true, hasClip, submittedToFrigatePlus);
else if (value === "no")
setSnapshotClip(false, hasClip, undefined);
}} }}
disabled={!isSnapshotFilterActive} disabled={!isSnapshotFilterActive}
> >
@ -592,6 +620,66 @@ function SnapshotClipFilterContent({
</ToggleGroup> </ToggleGroup>
</div> </div>
{config?.plus?.enabled && (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox
id="plus-filter"
className="size-5 text-white accent-white data-[state=checked]:bg-selected data-[state=checked]:text-white"
checked={isFrigatePlusFilterActive}
disabled={!isSnapshotFilterActive || hasSnapshot !== true}
onCheckedChange={(checked) => {
setIsFrigatePlusFilterActive(checked as boolean);
if (checked) {
setSnapshotClip(hasSnapshot, hasClip, true);
} else {
setSnapshotClip(hasSnapshot, hasClip, undefined);
}
}}
/>
<Label
htmlFor="plus-filter"
className="cursor-pointer text-sm font-medium leading-none"
>
Submitted to Frigate+
</Label>
</div>
<ToggleGroup
type="single"
value={
submittedToFrigatePlus === undefined
? undefined
: submittedToFrigatePlus
? "yes"
: "no"
}
onValueChange={(value) => {
if (value === "yes")
setSnapshotClip(hasSnapshot, hasClip, true);
else if (value === "no")
setSnapshotClip(hasSnapshot, hasClip, false);
else setSnapshotClip(hasSnapshot, hasClip, undefined);
}}
disabled={!isFrigatePlusFilterActive}
>
<ToggleGroupItem
value="yes"
aria-label="Yes"
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
Yes
</ToggleGroupItem>
<ToggleGroupItem
value="no"
aria-label="No"
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
>
No
</ToggleGroupItem>
</ToggleGroup>
</div>
)}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Checkbox <Checkbox
@ -601,9 +689,13 @@ function SnapshotClipFilterContent({
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
setIsClipFilterActive(checked as boolean); setIsClipFilterActive(checked as boolean);
if (checked) { if (checked) {
setSnapshotClip(hasSnapshot, true); setSnapshotClip(hasSnapshot, true, submittedToFrigatePlus);
} else { } else {
setSnapshotClip(hasSnapshot, undefined); setSnapshotClip(
hasSnapshot,
undefined,
submittedToFrigatePlus,
);
} }
}} }}
/> />
@ -618,8 +710,10 @@ function SnapshotClipFilterContent({
type="single" type="single"
value={hasClip === undefined ? undefined : hasClip ? "yes" : "no"} value={hasClip === undefined ? undefined : hasClip ? "yes" : "no"}
onValueChange={(value) => { onValueChange={(value) => {
if (value === "yes") setSnapshotClip(hasSnapshot, true); if (value === "yes")
else if (value === "no") setSnapshotClip(hasSnapshot, false); setSnapshotClip(hasSnapshot, true, submittedToFrigatePlus);
else if (value === "no")
setSnapshotClip(hasSnapshot, false, submittedToFrigatePlus);
}} }}
disabled={!isClipFilterActive} disabled={!isClipFilterActive}
> >

View File

@ -113,6 +113,7 @@ export default function Explore() {
min_score: searchSearchParams["min_score"], min_score: searchSearchParams["min_score"],
max_score: searchSearchParams["max_score"], max_score: searchSearchParams["max_score"],
has_snapshot: searchSearchParams["has_snapshot"], has_snapshot: searchSearchParams["has_snapshot"],
is_submitted: searchSearchParams["is_submitted"],
has_clip: searchSearchParams["has_clip"], has_clip: searchSearchParams["has_clip"],
event_id: searchSearchParams["event_id"], event_id: searchSearchParams["event_id"],
limit: limit:
@ -144,6 +145,7 @@ export default function Explore() {
min_score: searchSearchParams["min_score"], min_score: searchSearchParams["min_score"],
max_score: searchSearchParams["max_score"], max_score: searchSearchParams["max_score"],
has_snapshot: searchSearchParams["has_snapshot"], has_snapshot: searchSearchParams["has_snapshot"],
is_submitted: searchSearchParams["is_submitted"],
has_clip: searchSearchParams["has_clip"], has_clip: searchSearchParams["has_clip"],
event_id: searchSearchParams["event_id"], event_id: searchSearchParams["event_id"],
timezone, timezone,

View File

@ -61,6 +61,7 @@ export type SearchFilter = {
max_score?: number; max_score?: number;
has_snapshot?: number; has_snapshot?: number;
has_clip?: number; has_clip?: number;
is_submitted?: number;
time_range?: string; time_range?: string;
search_type?: SearchSource[]; search_type?: SearchSource[];
event_id?: string; event_id?: string;

View File

@ -159,8 +159,10 @@ export default function SearchView({
max_score: ["100"], max_score: ["100"],
has_clip: ["yes", "no"], has_clip: ["yes", "no"],
has_snapshot: ["yes", "no"], has_snapshot: ["yes", "no"],
...(config?.plus?.enabled &&
searchFilter?.has_snapshot && { is_submitted: ["yes", "no"] }),
}), }),
[config, allLabels, allZones, allSubLabels], [config, allLabels, allZones, allSubLabels, searchFilter],
); );
// remove duplicate event ids // remove duplicate event ids