forked from Github/frigate
Improved search input (#13815)
* create input with tags component * tweaks * only show filters pane when there are actual filters * special case for similarity searches * similarity search tweaks * populate suggestions values * scrollbar on outer div * clean up * separate custom hook * use command component * tooltips * regex tweaks * saved searches with confirmation dialogs * better date handling * fix filters * filter capitalization * filter instructions * replace underscore in filter type * alert dialog button color * toaster on success
This commit is contained in:
@@ -3,7 +3,6 @@ import SearchFilterGroup from "@/components/filter/SearchFilterGroup";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import Chip from "@/components/indicators/Chip";
|
||||
import SearchDetailDialog from "@/components/overlay/detail/SearchDetailDialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -12,16 +11,17 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { SearchFilter, SearchResult } from "@/types/search";
|
||||
import { SearchFilter, SearchResult, SearchSource } from "@/types/search";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { isMobileOnly } from "react-device-detect";
|
||||
import { LuImage, LuSearchX, LuText, LuXCircle } from "react-icons/lu";
|
||||
import { LuImage, LuSearchX, LuText } from "react-icons/lu";
|
||||
import useSWR from "swr";
|
||||
import ExploreView from "../explore/ExploreView";
|
||||
import useKeyboardListener, {
|
||||
KeyModifiers,
|
||||
} from "@/hooks/use-keyboard-listener";
|
||||
import scrollIntoView from "scroll-into-view-if-needed";
|
||||
import InputWithTags from "@/components/input/InputWithTags";
|
||||
|
||||
type SearchViewProps = {
|
||||
search: string;
|
||||
@@ -31,6 +31,7 @@ type SearchViewProps = {
|
||||
isLoading: boolean;
|
||||
setSearch: (search: string) => void;
|
||||
setSimilaritySearch: (search: SearchResult) => void;
|
||||
setSearchFilter: (filter: SearchFilter) => void;
|
||||
onUpdateFilter: (filter: SearchFilter) => void;
|
||||
onOpenSearch: (item: SearchResult) => void;
|
||||
loadMore: () => void;
|
||||
@@ -44,6 +45,7 @@ export default function SearchView({
|
||||
isLoading,
|
||||
setSearch,
|
||||
setSimilaritySearch,
|
||||
setSearchFilter,
|
||||
onUpdateFilter,
|
||||
loadMore,
|
||||
hasMore,
|
||||
@@ -52,6 +54,69 @@ export default function SearchView({
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
// suggestions values
|
||||
|
||||
const allLabels = useMemo<string[]>(() => {
|
||||
if (!config) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const labels = new Set<string>();
|
||||
const cameras = searchFilter?.cameras || Object.keys(config.cameras);
|
||||
|
||||
cameras.forEach((camera) => {
|
||||
if (camera == "birdseye") {
|
||||
return;
|
||||
}
|
||||
const cameraConfig = config.cameras[camera];
|
||||
cameraConfig.objects.track.forEach((label) => {
|
||||
labels.add(label);
|
||||
});
|
||||
|
||||
if (cameraConfig.audio.enabled_in_config) {
|
||||
cameraConfig.audio.listen.forEach((label) => {
|
||||
labels.add(label);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return [...labels].sort();
|
||||
}, [config, searchFilter]);
|
||||
|
||||
const { data: allSubLabels } = useSWR("sub_labels");
|
||||
|
||||
const allZones = useMemo<string[]>(() => {
|
||||
if (!config) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const zones = new Set<string>();
|
||||
const cameras = searchFilter?.cameras || Object.keys(config.cameras);
|
||||
|
||||
cameras.forEach((camera) => {
|
||||
if (camera == "birdseye") {
|
||||
return;
|
||||
}
|
||||
const cameraConfig = config.cameras[camera];
|
||||
Object.entries(cameraConfig.zones).map(([name, _]) => {
|
||||
zones.add(name);
|
||||
});
|
||||
});
|
||||
|
||||
return [...zones].sort();
|
||||
}, [config, searchFilter]);
|
||||
|
||||
const suggestionsValues = useMemo(
|
||||
() => ({
|
||||
cameras: Object.keys(config?.cameras || {}),
|
||||
labels: Object.values(allLabels || {}),
|
||||
zones: Object.values(allZones || {}),
|
||||
sub_labels: allSubLabels,
|
||||
search_type: ["thumbnail", "description"] as SearchSource[],
|
||||
}),
|
||||
[config, allLabels, allZones, allSubLabels],
|
||||
);
|
||||
|
||||
// remove duplicate event ids
|
||||
|
||||
const uniqueResults = useMemo(() => {
|
||||
@@ -192,7 +257,7 @@ export default function SearchView({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col items-start space-y-2 pl-2 pr-2 md:mb-2 md:pl-3 lg:h-10 lg:flex-row lg:items-center lg:space-y-0",
|
||||
"flex flex-col items-start space-y-2 pl-2 pr-2 md:mb-2 md:pl-3 lg:relative lg:h-10 lg:flex-row lg:items-center lg:space-y-0",
|
||||
config?.semantic_search?.enabled
|
||||
? "justify-between"
|
||||
: "justify-center",
|
||||
@@ -200,24 +265,14 @@ export default function SearchView({
|
||||
)}
|
||||
>
|
||||
{config?.semantic_search?.enabled && (
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-full",
|
||||
hasExistingSearch ? "lg:mr-3 lg:w-1/3" : "lg:ml-[25%] lg:w-1/2",
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
className="text-md w-full bg-muted pr-10"
|
||||
placeholder={"Search for a tracked object..."}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
<div className={cn("z-[41] w-full lg:absolute lg:top-0 lg:w-1/3")}>
|
||||
<InputWithTags
|
||||
filters={searchFilter ?? {}}
|
||||
setFilters={setSearchFilter}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
allSuggestions={suggestionsValues}
|
||||
/>
|
||||
{search && (
|
||||
<LuXCircle
|
||||
className="absolute right-2 top-1/2 h-5 w-5 -translate-y-1/2 cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={() => setSearch("")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user