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
has_clip: Optional[bool] = None
has_snapshot: Optional[bool] = None
is_submitted: Optional[bool] = None
timezone: Optional[str] = "utc"
min_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
has_clip = params.has_clip
has_snapshot = params.has_snapshot
is_submitted = params.is_submitted
# for similarity search
event_id = params.event_id
@ -441,6 +442,12 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
if has_snapshot is not None:
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:
event_filters.append((Event.data["score"].between(min_score, max_score)))
else:

View File

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

View File

@ -119,6 +119,7 @@ export default function SearchFilterDialog({
}
/>
<SnapshotClipFilterContent
config={config}
hasSnapshot={
currentFilter.has_snapshot !== undefined
? currentFilter.has_snapshot === 1
@ -129,12 +130,19 @@ export default function SearchFilterDialog({
? currentFilter.has_clip === 1
: undefined
}
setSnapshotClip={(snapshot, clip) =>
submittedToFrigatePlus={
currentFilter.is_submitted !== undefined
? currentFilter.is_submitted === 1
: undefined
}
setSnapshotClip={(snapshot, clip, submitted) =>
setCurrentFilter({
...currentFilter,
has_snapshot:
snapshot !== undefined ? (snapshot ? 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 = {
config?: FrigateConfig;
hasSnapshot: boolean | undefined;
hasClip: boolean | undefined;
submittedToFrigatePlus: boolean | undefined;
setSnapshotClip: (
snapshot: boolean | undefined,
clip: boolean | undefined,
submittedToFrigate: boolean | undefined,
) => void;
};
function SnapshotClipFilterContent({
export function SnapshotClipFilterContent({
config,
hasSnapshot,
hasClip,
submittedToFrigatePlus,
setSnapshotClip,
}: SnapshotClipContentProps) {
const [isSnapshotFilterActive, setIsSnapshotFilterActive] = useState(
@ -527,6 +540,11 @@ function SnapshotClipFilterContent({
const [isClipFilterActive, setIsClipFilterActive] = useState(
hasClip !== undefined,
);
const [isFrigatePlusFilterActive, setIsFrigatePlusFilterActive] = useState(
submittedToFrigatePlus !== undefined &&
isSnapshotFilterActive &&
hasSnapshot === true,
);
useEffect(() => {
setIsSnapshotFilterActive(hasSnapshot !== undefined);
@ -536,6 +554,14 @@ function SnapshotClipFilterContent({
setIsClipFilterActive(hasClip !== undefined);
}, [hasClip]);
useEffect(() => {
setIsFrigatePlusFilterActive(
submittedToFrigatePlus !== undefined &&
isSnapshotFilterActive &&
hasSnapshot === true,
);
}, [submittedToFrigatePlus, isSnapshotFilterActive, hasSnapshot]);
return (
<div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" />
@ -551,9 +577,9 @@ function SnapshotClipFilterContent({
onCheckedChange={(checked) => {
setIsSnapshotFilterActive(checked as boolean);
if (checked) {
setSnapshotClip(true, hasClip);
setSnapshotClip(true, hasClip, submittedToFrigatePlus);
} else {
setSnapshotClip(undefined, hasClip);
setSnapshotClip(undefined, hasClip, undefined);
}
}}
/>
@ -570,8 +596,10 @@ function SnapshotClipFilterContent({
hasSnapshot === undefined ? undefined : hasSnapshot ? "yes" : "no"
}
onValueChange={(value) => {
if (value === "yes") setSnapshotClip(true, hasClip);
else if (value === "no") setSnapshotClip(false, hasClip);
if (value === "yes")
setSnapshotClip(true, hasClip, submittedToFrigatePlus);
else if (value === "no")
setSnapshotClip(false, hasClip, undefined);
}}
disabled={!isSnapshotFilterActive}
>
@ -592,6 +620,66 @@ function SnapshotClipFilterContent({
</ToggleGroup>
</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 space-x-2">
<Checkbox
@ -601,9 +689,13 @@ function SnapshotClipFilterContent({
onCheckedChange={(checked) => {
setIsClipFilterActive(checked as boolean);
if (checked) {
setSnapshotClip(hasSnapshot, true);
setSnapshotClip(hasSnapshot, true, submittedToFrigatePlus);
} else {
setSnapshotClip(hasSnapshot, undefined);
setSnapshotClip(
hasSnapshot,
undefined,
submittedToFrigatePlus,
);
}
}}
/>
@ -618,8 +710,10 @@ function SnapshotClipFilterContent({
type="single"
value={hasClip === undefined ? undefined : hasClip ? "yes" : "no"}
onValueChange={(value) => {
if (value === "yes") setSnapshotClip(hasSnapshot, true);
else if (value === "no") setSnapshotClip(hasSnapshot, false);
if (value === "yes")
setSnapshotClip(hasSnapshot, true, submittedToFrigatePlus);
else if (value === "no")
setSnapshotClip(hasSnapshot, false, submittedToFrigatePlus);
}}
disabled={!isClipFilterActive}
>

View File

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

View File

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

View File

@ -159,8 +159,10 @@ export default function SearchView({
max_score: ["100"],
has_clip: ["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