Improve navigation (#13833)

* Fix infinite loop

* Fix review page not opening to historical review items

* Use query arg for search and remove unused recording opening

* Retain query

* Clean up typing
This commit is contained in:
Nicolas Mowen 2024-09-19 10:01:57 -06:00 committed by GitHub
parent 7c63cb5bca
commit 27e71eb142
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 77 additions and 142 deletions

View File

@ -189,6 +189,8 @@ export default function InputWithTags({
let timestamp = 0; let timestamp = 0;
switch (type) { switch (type) {
case "query":
break;
case "before": case "before":
case "after": case "after":
timestamp = convertLocalDateToTimestamp(value); timestamp = convertLocalDateToTimestamp(value);
@ -584,7 +586,9 @@ export default function InputWithTags({
)} )}
{Object.entries(filters).map(([filterType, filterValues]) => {Object.entries(filters).map(([filterType, filterValues]) =>
Array.isArray(filterValues) ? ( Array.isArray(filterValues) ? (
filterValues.map((value, index) => ( filterValues
.filter(() => filterType !== "query")
.map((value, index) => (
<span <span
key={`${filterType}-${index}`} key={`${filterType}-${index}`}
className="inline-flex items-center whitespace-nowrap rounded-full bg-green-100 px-2 py-0.5 text-sm capitalize text-green-800" className="inline-flex items-center whitespace-nowrap rounded-full bg-green-100 px-2 py-0.5 text-sm capitalize text-green-800"

View File

@ -125,7 +125,7 @@ export function useSearchEffect(
const remove = callback(param[1]); const remove = callback(param[1]);
if (remove) { if (remove) {
setSearchParams(); setSearchParams(undefined, { replace: true });
} }
}, [param, callback, setSearchParams]); }, [param, callback, setSearchParams]);
} }

View File

@ -14,6 +14,10 @@ import {
ReviewSummary, ReviewSummary,
SegmentedReviewData, SegmentedReviewData,
} from "@/types/review"; } from "@/types/review";
import {
getBeginningOfDayTimestamp,
getEndOfDayTimestamp,
} from "@/utils/dateUtil";
import EventView from "@/views/events/EventView"; import EventView from "@/views/events/EventView";
import { RecordingView } from "@/views/recording/RecordingView"; import { RecordingView } from "@/views/recording/RecordingView";
import axios from "axios"; import axios from "axios";
@ -43,10 +47,17 @@ export default function Events() {
.get(`review/${reviewId}`) .get(`review/${reviewId}`)
.then((resp) => { .then((resp) => {
if (resp.status == 200 && resp.data) { if (resp.status == 200 && resp.data) {
const startTime = resp.data.start_time - REVIEW_PADDING;
const date = new Date(startTime * 1000);
setReviewFilter({
after: getBeginningOfDayTimestamp(date),
before: getEndOfDayTimestamp(date),
});
setRecording( setRecording(
{ {
camera: resp.data.camera, camera: resp.data.camera,
startTime: resp.data.start_time - REVIEW_PADDING, startTime,
severity: resp.data.severity, severity: resp.data.severity,
}, },
true, true,

View File

@ -1,30 +1,24 @@
import { useApiFilterArgs } from "@/hooks/use-api-filter"; import { useApiFilterArgs } from "@/hooks/use-api-filter";
import { useCameraPreviews } from "@/hooks/use-camera-previews"; import { useSearchEffect } from "@/hooks/use-overlay-state";
import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
import { FrigateConfig } from "@/types/frigateConfig";
import { RecordingStartingPoint } from "@/types/record";
import { SearchFilter, SearchQuery, SearchResult } from "@/types/search"; import { SearchFilter, SearchQuery, SearchResult } from "@/types/search";
import { TimeRange } from "@/types/timeline";
import { RecordingView } from "@/views/recording/RecordingView";
import SearchView from "@/views/search/SearchView"; import SearchView from "@/views/search/SearchView";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import useSWR from "swr";
import useSWRInfinite from "swr/infinite"; import useSWRInfinite from "swr/infinite";
const API_LIMIT = 25; const API_LIMIT = 25;
export default function Explore() { export default function Explore() {
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
// search field handler // search field handler
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [searchTerm, setSearchTerm] = useState("");
const [recording, setRecording] = const [searchFilter, setSearchFilter, searchSearchParams] =
useOverlayState<RecordingStartingPoint>("recording"); useApiFilterArgs<SearchFilter>();
const searchTerm = useMemo(
() => searchSearchParams?.["query"] || "",
[searchSearchParams],
);
// search filter // search filter
@ -36,11 +30,13 @@ export default function Explore() {
return searchTerm.split(":")[1]; return searchTerm.split(":")[1];
}, [searchTerm]); }, [searchTerm]);
const [searchFilter, setSearchFilter, searchSearchParams] =
useApiFilterArgs<SearchFilter>();
// search api // search api
useSearchEffect("query", (query) => {
setSearch(query);
return false;
});
useSearchEffect("similarity_search_id", (similarityId) => { useSearchEffect("similarity_search_id", (similarityId) => {
setSearch(`similarity:${similarityId}`); setSearch(`similarity:${similarityId}`);
// @ts-expect-error we want to clear this // @ts-expect-error we want to clear this
@ -49,7 +45,16 @@ export default function Explore() {
}); });
useEffect(() => { useEffect(() => {
setSearchTerm(search); if (!searchTerm && !search) {
return;
}
setSearchFilter({
...searchFilter,
query: search.length > 0 ? search : undefined,
});
// only update when search is updated
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [search]); }, [search]);
const searchQuery: SearchQuery = useMemo(() => { const searchQuery: SearchQuery = useMemo(() => {
@ -168,94 +173,6 @@ export default function Explore() {
} }
}, [isReachingEnd, isLoadingMore, setSize, size, searchResults, searchQuery]); }, [isReachingEnd, isLoadingMore, setSize, size, searchResults, searchQuery]);
// previews
const previewTimeRange = useMemo<TimeRange>(() => {
if (!searchResults) {
return { after: 0, before: 0 };
}
return {
after: Math.min(...searchResults.map((res) => res.start_time)),
before: Math.max(
...searchResults.map((res) => res.end_time ?? Date.now() / 1000),
),
};
}, [searchResults]);
const allPreviews = useCameraPreviews(previewTimeRange, {
autoRefresh: false,
fetchPreviews: searchResults != undefined,
});
// selection
const onOpenSearch = useCallback(
(item: SearchResult) => {
setRecording({
camera: item.camera,
startTime: item.start_time,
severity: "alert",
});
},
[setRecording],
);
const selectedReviewData = useMemo(() => {
if (!recording) {
return undefined;
}
if (!config) {
return undefined;
}
if (!searchResults) {
return undefined;
}
const allCameras = searchFilter?.cameras ?? Object.keys(config.cameras);
return {
camera: recording.camera,
start_time: recording.startTime,
allCameras: allCameras,
};
// previews will not update after item is selected
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [recording, searchResults]);
const selectedTimeRange = useMemo(() => {
if (!recording) {
return undefined;
}
const time = new Date(recording.startTime * 1000);
time.setUTCMinutes(0, 0, 0);
const start = time.getTime() / 1000;
time.setHours(time.getHours() + 2);
const end = time.getTime() / 1000;
return {
after: start,
before: end,
};
}, [recording]);
if (recording) {
if (selectedReviewData && selectedTimeRange) {
return (
<RecordingView
startCamera={selectedReviewData.camera}
startTime={selectedReviewData.start_time}
allCameras={selectedReviewData.allCameras}
allPreviews={allPreviews}
timeRange={selectedTimeRange}
updateFilter={setSearchFilter}
/>
);
}
} else {
return ( return (
<SearchView <SearchView
search={search} search={search}
@ -267,10 +184,8 @@ export default function Explore() {
setSimilaritySearch={(search) => setSearch(`similarity:${search.id}`)} setSimilaritySearch={(search) => setSearch(`similarity:${search.id}`)}
setSearchFilter={setSearchFilter} setSearchFilter={setSearchFilter}
onUpdateFilter={setSearchFilter} onUpdateFilter={setSearchFilter}
onOpenSearch={onOpenSearch}
loadMore={loadMore} loadMore={loadMore}
hasMore={!isReachingEnd} hasMore={!isReachingEnd}
/> />
); );
}
} }

View File

@ -29,6 +29,7 @@ export type SearchResult = {
}; };
export type SearchFilter = { export type SearchFilter = {
query?: string;
cameras?: string[]; cameras?: string[];
labels?: string[]; labels?: string[];
subLabels?: string[]; subLabels?: string[];

View File

@ -285,6 +285,11 @@ export function endOfHourOrCurrentTime(timestamp: number) {
return Math.min(timestamp, now.getTime() / 1000); return Math.min(timestamp, now.getTime() / 1000);
} }
export function getBeginningOfDayTimestamp(date: Date) {
date.setHours(0, 0, 0, 0);
return date.getTime() / 1000;
}
export function getEndOfDayTimestamp(date: Date) { export function getEndOfDayTimestamp(date: Date) {
date.setHours(23, 59, 59, 999); date.setHours(23, 59, 59, 999);
return date.getTime() / 1000; return date.getTime() / 1000;

View File

@ -33,7 +33,6 @@ type SearchViewProps = {
setSimilaritySearch: (search: SearchResult) => void; setSimilaritySearch: (search: SearchResult) => void;
setSearchFilter: (filter: SearchFilter) => void; setSearchFilter: (filter: SearchFilter) => void;
onUpdateFilter: (filter: SearchFilter) => void; onUpdateFilter: (filter: SearchFilter) => void;
onOpenSearch: (item: SearchResult) => void;
loadMore: () => void; loadMore: () => void;
hasMore: boolean; hasMore: boolean;
}; };