mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-01-21 00:06:44 +01:00
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:
parent
7c63cb5bca
commit
27e71eb142
@ -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,24 +586,26 @@ 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
|
||||||
<span
|
.filter(() => filterType !== "query")
|
||||||
key={`${filterType}-${index}`}
|
.map((value, index) => (
|
||||||
className="inline-flex items-center whitespace-nowrap rounded-full bg-green-100 px-2 py-0.5 text-sm capitalize text-green-800"
|
<span
|
||||||
>
|
key={`${filterType}-${index}`}
|
||||||
{filterType.replaceAll("_", " ")}:{" "}
|
className="inline-flex items-center whitespace-nowrap rounded-full bg-green-100 px-2 py-0.5 text-sm capitalize text-green-800"
|
||||||
{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" />
|
{filterType.replaceAll("_", " ")}:{" "}
|
||||||
</button>
|
{value.replaceAll("_", " ")}
|
||||||
</span>
|
<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
|
<span
|
||||||
key={filterType}
|
key={filterType}
|
||||||
|
@ -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]);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,109 +173,19 @@ export default function Explore() {
|
|||||||
}
|
}
|
||||||
}, [isReachingEnd, isLoadingMore, setSize, size, searchResults, searchQuery]);
|
}, [isReachingEnd, isLoadingMore, setSize, size, searchResults, searchQuery]);
|
||||||
|
|
||||||
// previews
|
return (
|
||||||
|
<SearchView
|
||||||
const previewTimeRange = useMemo<TimeRange>(() => {
|
search={search}
|
||||||
if (!searchResults) {
|
searchTerm={searchTerm}
|
||||||
return { after: 0, before: 0 };
|
searchFilter={searchFilter}
|
||||||
}
|
searchResults={searchResults}
|
||||||
|
isLoading={(isLoadingInitialData || isLoadingMore) ?? true}
|
||||||
return {
|
setSearch={setSearch}
|
||||||
after: Math.min(...searchResults.map((res) => res.start_time)),
|
setSimilaritySearch={(search) => setSearch(`similarity:${search.id}`)}
|
||||||
before: Math.max(
|
setSearchFilter={setSearchFilter}
|
||||||
...searchResults.map((res) => res.end_time ?? Date.now() / 1000),
|
onUpdateFilter={setSearchFilter}
|
||||||
),
|
loadMore={loadMore}
|
||||||
};
|
hasMore={!isReachingEnd}
|
||||||
}, [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 (
|
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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[];
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user