Skip to content

Commit

Permalink
Improve navigation (#13833)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
NickM-27 authored Sep 19, 2024
1 parent 7c63cb5 commit 27e71eb
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 142 deletions.
38 changes: 21 additions & 17 deletions web/src/components/input/InputWithTags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ export default function InputWithTags({
let timestamp = 0;

switch (type) {
case "query":
break;
case "before":
case "after":
timestamp = convertLocalDateToTimestamp(value);
Expand Down Expand Up @@ -584,24 +586,26 @@ export default function InputWithTags({
)}
{Object.entries(filters).map(([filterType, filterValues]) =>
Array.isArray(filterValues) ? (
filterValues.map((value, index) => (
<span
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"
>
{filterType.replaceAll("_", " ")}:{" "}
{value.replaceAll("_", " ")}
<button
onClick={() =>
removeFilter(filterType as FilterType, value)
}
className="ml-1 focus:outline-none"
aria-label={`Remove ${filterType}:${value.replaceAll("_", " ")} filter`}
filterValues
.filter(() => filterType !== "query")
.map((value, index) => (
<span
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"
>
<LuX className="h-3 w-3" />
</button>
</span>
))
{filterType.replaceAll("_", " ")}:{" "}
{value.replaceAll("_", " ")}
<button
onClick={() =>
removeFilter(filterType as FilterType, value)
}
className="ml-1 focus:outline-none"
aria-label={`Remove ${filterType}:${value.replaceAll("_", " ")} filter`}
>
<LuX className="h-3 w-3" />
</button>
</span>
))
) : (
<span
key={filterType}
Expand Down
2 changes: 1 addition & 1 deletion web/src/hooks/use-overlay-state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export function useSearchEffect(
const remove = callback(param[1]);

if (remove) {
setSearchParams();
setSearchParams(undefined, { replace: true });
}
}, [param, callback, setSearchParams]);
}
13 changes: 12 additions & 1 deletion web/src/pages/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
ReviewSummary,
SegmentedReviewData,
} from "@/types/review";
import {
getBeginningOfDayTimestamp,
getEndOfDayTimestamp,
} from "@/utils/dateUtil";
import EventView from "@/views/events/EventView";
import { RecordingView } from "@/views/recording/RecordingView";
import axios from "axios";
Expand Down Expand Up @@ -43,10 +47,17 @@ export default function Events() {
.get(`review/${reviewId}`)
.then((resp) => {
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(
{
camera: resp.data.camera,
startTime: resp.data.start_time - REVIEW_PADDING,
startTime,
severity: resp.data.severity,
},
true,
Expand Down
159 changes: 37 additions & 122 deletions web/src/pages/Explore.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,24 @@
import { useApiFilterArgs } from "@/hooks/use-api-filter";
import { useCameraPreviews } from "@/hooks/use-camera-previews";
import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
import { FrigateConfig } from "@/types/frigateConfig";
import { RecordingStartingPoint } from "@/types/record";
import { useSearchEffect } from "@/hooks/use-overlay-state";
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 { useCallback, useEffect, useMemo, useState } from "react";
import useSWR from "swr";
import useSWRInfinite from "swr/infinite";

const API_LIMIT = 25;

export default function Explore() {
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});

// search field handler

const [search, setSearch] = useState("");
const [searchTerm, setSearchTerm] = useState("");

const [recording, setRecording] =
useOverlayState<RecordingStartingPoint>("recording");
const [searchFilter, setSearchFilter, searchSearchParams] =
useApiFilterArgs<SearchFilter>();

const searchTerm = useMemo(
() => searchSearchParams?.["query"] || "",
[searchSearchParams],
);

// search filter

Expand All @@ -36,11 +30,13 @@ export default function Explore() {
return searchTerm.split(":")[1];
}, [searchTerm]);

const [searchFilter, setSearchFilter, searchSearchParams] =
useApiFilterArgs<SearchFilter>();

// search api

useSearchEffect("query", (query) => {
setSearch(query);
return false;
});

useSearchEffect("similarity_search_id", (similarityId) => {
setSearch(`similarity:${similarityId}`);
// @ts-expect-error we want to clear this
Expand All @@ -49,7 +45,16 @@ export default function Explore() {
});

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]);

const searchQuery: SearchQuery = useMemo(() => {
Expand Down Expand Up @@ -168,109 +173,19 @@ export default function Explore() {
}
}, [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],
return (
<SearchView
search={search}
searchTerm={searchTerm}
searchFilter={searchFilter}
searchResults={searchResults}
isLoading={(isLoadingInitialData || isLoadingMore) ?? true}
setSearch={setSearch}
setSimilaritySearch={(search) => setSearch(`similarity:${search.id}`)}
setSearchFilter={setSearchFilter}
onUpdateFilter={setSearchFilter}
loadMore={loadMore}
hasMore={!isReachingEnd}
/>
);

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 (
<SearchView
search={search}
searchTerm={searchTerm}
searchFilter={searchFilter}
searchResults={searchResults}
isLoading={(isLoadingInitialData || isLoadingMore) ?? true}
setSearch={setSearch}
setSimilaritySearch={(search) => setSearch(`similarity:${search.id}`)}
setSearchFilter={setSearchFilter}
onUpdateFilter={setSearchFilter}
onOpenSearch={onOpenSearch}
loadMore={loadMore}
hasMore={!isReachingEnd}
/>
);
}
}
1 change: 1 addition & 0 deletions web/src/types/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type SearchResult = {
};

export type SearchFilter = {
query?: string;
cameras?: string[];
labels?: string[];
subLabels?: string[];
Expand Down
5 changes: 5 additions & 0 deletions web/src/utils/dateUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ export function endOfHourOrCurrentTime(timestamp: number) {
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) {
date.setHours(23, 59, 59, 999);
return date.getTime() / 1000;
Expand Down
1 change: 0 additions & 1 deletion web/src/views/search/SearchView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ type SearchViewProps = {
setSimilaritySearch: (search: SearchResult) => void;
setSearchFilter: (filter: SearchFilter) => void;
onUpdateFilter: (filter: SearchFilter) => void;
onOpenSearch: (item: SearchResult) => void;
loadMore: () => void;
hasMore: boolean;
};
Expand Down

0 comments on commit 27e71eb

Please sign in to comment.