Remove paging (#10310)

* Remove paging and use reviewed as an internal filter

* Fix new reviews not going away
This commit is contained in:
Nicolas Mowen 2024-03-07 07:33:36 -07:00 committed by GitHub
parent 8bfc0c98dc
commit b2931bcaa9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 78 additions and 165 deletions

View File

@ -26,7 +26,7 @@ def review():
cameras = request.args.get("cameras", "all")
labels = request.args.get("labels", "all")
reviewed = request.args.get("reviewed", type=int, default=0)
limit = request.args.get("limit", 100)
limit = request.args.get("limit", type=int, default=None)
severity = request.args.get("severity", None)
before = request.args.get("before", type=float, default=datetime.now().timestamp())

View File

@ -8,19 +8,22 @@ type NewReviewDataProps = {
className: string;
contentRef: MutableRefObject<HTMLDivElement | null>;
severity: ReviewSeverity;
hasUpdate: boolean;
setHasUpdate: (update: boolean) => void;
pullLatestData: () => void;
};
export default function NewReviewData({
className,
contentRef,
severity,
hasUpdate,
setHasUpdate,
pullLatestData,
}: NewReviewDataProps) {
const { payload: review } = useFrigateReviews();
const startCheckTs = useMemo(() => Date.now() / 1000, []);
const [reviewTs, setReviewTs] = useState(startCheckTs);
const [hasUpdate, setHasUpdate] = useState(false);
useEffect(() => {
if (!review) {
@ -36,7 +39,7 @@ export default function NewReviewData({
if (reviewTs > startCheckTs) {
setHasUpdate(true);
}
}, [startCheckTs, reviewTs]);
}, [startCheckTs, reviewTs, setHasUpdate]);
return (
<div className={className}>

View File

@ -19,9 +19,6 @@ import axios from "axios";
import { useCallback, useMemo, useState } from "react";
import { isMobile } from "react-device-detect";
import useSWR from "swr";
import useSWRInfinite from "swr/infinite";
const API_LIMIT = 100;
export default function Events() {
const { data: config } = useSWR<FrigateConfig>("config");
@ -41,7 +38,6 @@ export default function Events() {
useApiFilter<ReviewFilter>();
const onUpdateFilter = useCallback((newFilter: ReviewFilter) => {
setSize(1);
setReviewFilter(newFilter);
// we don't want this updating
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -69,53 +65,25 @@ export default function Events() {
return axios.get(path, { params }).then((res) => res.data);
}, []);
const getKey = useCallback(
(index: number, prevData: ReviewSegment[]) => {
if (index > 0) {
const lastDate = prevData[prevData.length - 1].start_time;
reviewSearchParams;
const pagedParams = {
cameras: reviewSearchParams["cameras"],
labels: reviewSearchParams["labels"],
reviewed: reviewSearchParams["showReviewed"],
before: lastDate,
after: reviewSearchParams["after"] || last24Hours.after,
limit: API_LIMIT,
};
return ["review", pagedParams];
}
const getKey = useCallback(() => {
const params = {
cameras: reviewSearchParams["cameras"],
labels: reviewSearchParams["labels"],
reviewed: reviewSearchParams["showReviewed"],
limit: API_LIMIT,
reviewed: 1,
before: reviewSearchParams["before"] || last24Hours.before,
after: reviewSearchParams["after"] || last24Hours.after,
};
return ["review", params];
},
[reviewSearchParams, last24Hours],
);
}, [reviewSearchParams, last24Hours]);
const {
data: reviewPages,
mutate: updateSegments,
size,
setSize,
isValidating,
} = useSWRInfinite<ReviewSegment[]>(getKey, reviewSegmentFetcher, {
const { data: reviews, mutate: updateSegments } = useSWR<ReviewSegment[]>(
getKey,
reviewSegmentFetcher,
{
revalidateOnFocus: false,
persistSize: true,
});
const isDone = useMemo(
() => (reviewPages?.at(-1)?.length ?? 0) < API_LIMIT,
[reviewPages],
},
);
const onLoadNextPage = useCallback(() => setSize(size + 1), [size, setSize]);
// review summary
const { data: reviewSummary, mutate: updateSummary } = useSWR<ReviewSummary>([
@ -136,24 +104,20 @@ export default function Events() {
// preview videos
const previewTimes = useMemo(() => {
if (
!reviewPages ||
reviewPages.length == 0 ||
reviewPages.at(-1)?.length == 0
) {
if (!reviews || reviews.length == 0) {
return undefined;
}
const startDate = new Date();
startDate.setMinutes(0, 0, 0);
const endDate = new Date(reviewPages.at(-1)?.at(-1)?.end_time || 0);
const endDate = new Date(reviews.at(-1)?.end_time || 0);
endDate.setHours(0, 0, 0, 0);
return {
start: startDate.getTime() / 1000,
end: endDate.getTime() / 1000,
};
}, [reviewPages]);
}, [reviews]);
const { data: allPreviews } = useSWR<Preview[]>(
previewTimes
? `preview/all/start/${previewTimes.start}/end/${previewTimes.end}`
@ -169,28 +133,21 @@ export default function Events() {
if (resp.status == 200) {
updateSegments(
(data: ReviewSegment[][] | undefined) => {
(data: ReviewSegment[] | undefined) => {
if (!data) {
return data;
}
const newData: ReviewSegment[][] = [];
data.forEach((page) => {
const reviewIndex = page.findIndex(
(item) => item.id == review.id,
);
const reviewIndex = data.findIndex((item) => item.id == review.id);
if (reviewIndex == -1) {
newData.push([...page]);
} else {
newData.push([
...page.slice(0, reviewIndex),
{ ...page[reviewIndex], has_been_reviewed: true },
...page.slice(reviewIndex + 1),
]);
return data;
}
});
const newData = [
...data.slice(0, reviewIndex),
{ ...data[reviewIndex], has_been_reviewed: true },
...data.slice(reviewIndex + 1),
];
return newData;
},
@ -252,7 +209,7 @@ export default function Events() {
return undefined;
}
if (!reviewPages) {
if (!reviews) {
return undefined;
}
@ -262,8 +219,6 @@ export default function Events() {
const allCameras = reviewFilter?.cameras ?? Object.keys(config.cameras);
const allReviews = reviewPages.flat();
if (selectedReviewId.startsWith("motion")) {
const motionData = selectedReviewId.split(",");
// format is motion,camera,start_time
@ -272,15 +227,13 @@ export default function Events() {
severity: "significant_motion" as ReviewSeverity,
start_time: parseFloat(motionData[2]),
allCameras: allCameras,
cameraSegments: allReviews.filter((seg) =>
cameraSegments: reviews.filter((seg) =>
allCameras.includes(seg.camera),
),
};
}
const selectedReview = allReviews.find(
(item) => item.id == selectedReviewId,
);
const selectedReview = reviews.find((item) => item.id == selectedReviewId);
if (!selectedReview) {
return undefined;
@ -296,14 +249,12 @@ export default function Events() {
severity: selectedReview.severity,
start_time: selectedReview.start_time,
allCameras: allCameras,
cameraSegments: allReviews.filter((seg) =>
allCameras.includes(seg.camera),
),
cameraSegments: reviews.filter((seg) => allCameras.includes(seg.camera)),
};
// previews will not update after item is selected
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedReviewId, reviewPages]);
}, [selectedReviewId, reviews]);
if (!timezone) {
return <ActivityIndicator />;
@ -335,16 +286,13 @@ export default function Events() {
} else {
return (
<EventView
reviewPages={reviewPages}
reviews={reviews}
reviewSummary={reviewSummary}
relevantPreviews={allPreviews}
timeRange={selectedTimeRange}
reachedEnd={isDone}
isValidating={isValidating}
filter={reviewFilter}
severity={severity ?? "alert"}
setSeverity={setSeverity}
loadNextPage={onLoadNextPage}
markItemAsReviewed={markItemAsReviewed}
onOpenReview={setSelectedReviewId}
pullLatestData={reloadData}

View File

@ -38,32 +38,26 @@ import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline";
import { Button } from "@/components/ui/button";
type EventViewProps = {
reviewPages?: ReviewSegment[][];
reviews?: ReviewSegment[];
reviewSummary?: ReviewSummary;
relevantPreviews?: Preview[];
timeRange: { before: number; after: number };
reachedEnd: boolean;
isValidating: boolean;
filter?: ReviewFilter;
severity: ReviewSeverity;
setSeverity: (severity: ReviewSeverity) => void;
loadNextPage: () => void;
markItemAsReviewed: (review: ReviewSegment) => void;
onOpenReview: (reviewId: string) => void;
pullLatestData: () => void;
updateFilter: (filter: ReviewFilter) => void;
};
export default function EventView({
reviewPages,
reviews,
reviewSummary,
relevantPreviews,
timeRange,
reachedEnd,
isValidating,
filter,
severity,
setSeverity,
loadNextPage,
markItemAsReviewed,
onOpenReview,
pullLatestData,
@ -115,8 +109,7 @@ export default function EventView({
const detections: ReviewSegment[] = [];
const motion: ReviewSegment[] = [];
reviewPages?.forEach((page) => {
page.forEach((segment) => {
reviews?.forEach((segment) => {
all.push(segment);
switch (segment.severity) {
@ -131,7 +124,6 @@ export default function EventView({
break;
}
});
});
return {
all: all,
@ -139,7 +131,7 @@ export default function EventView({
detection: detections,
significant_motion: motion,
};
}, [reviewPages]);
}, [reviews]);
const currentItems = useMemo(() => {
const current = reviewItems[severity];
@ -151,8 +143,6 @@ export default function EventView({
return current;
}, [reviewItems, severity]);
const pagingObserver = useRef<IntersectionObserver | null>(null);
// review interaction
const [selectedReviews, setSelectedReviews] = useState<string[]>([]);
@ -267,14 +257,11 @@ export default function EventView({
currentItems={currentItems}
reviewItems={reviewItems}
relevantPreviews={relevantPreviews}
pagingObserver={pagingObserver}
selectedReviews={selectedReviews}
itemsToReview={reviewCounts[severity]}
severity={severity}
filter={filter}
isValidating={isValidating}
reachedEnd={reachedEnd}
timeRange={timeRange}
loadNextPage={loadNextPage}
markItemAsReviewed={markItemAsReviewed}
onSelectReview={onSelectReview}
pullLatestData={pullLatestData}
@ -306,14 +293,10 @@ type DetectionReviewProps = {
};
itemsToReview?: number;
relevantPreviews?: Preview[];
pagingObserver: MutableRefObject<IntersectionObserver | null>;
selectedReviews: string[];
severity: ReviewSeverity;
filter?: ReviewFilter;
isValidating: boolean;
reachedEnd: boolean;
timeRange: { before: number; after: number };
loadNextPage: () => void;
markItemAsReviewed: (review: ReviewSegment) => void;
onSelectReview: (id: string, ctrl: boolean) => void;
pullLatestData: () => void;
@ -324,14 +307,10 @@ function DetectionReview({
reviewItems,
itemsToReview,
relevantPreviews,
pagingObserver,
selectedReviews,
severity,
filter,
isValidating,
reachedEnd,
timeRange,
loadNextPage,
markItemAsReviewed,
onSelectReview,
pullLatestData,
@ -344,23 +323,7 @@ function DetectionReview({
// review interaction
const lastReviewRef = useCallback(
(node: HTMLElement | null) => {
if (isValidating) return;
if (pagingObserver.current) pagingObserver.current.disconnect();
try {
pagingObserver.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !reachedEnd) {
loadNextPage();
}
});
if (node) pagingObserver.current.observe(node);
} catch (e) {
// no op
}
},
[isValidating, pagingObserver, reachedEnd, loadNextPage],
);
const [hasUpdate, setHasUpdate] = useState(false);
const markAllReviewed = useCallback(async () => {
if (!currentItems) {
@ -370,6 +333,7 @@ function DetectionReview({
await axios.post(`reviews/viewed`, {
ids: currentItems?.map((seg) => seg.id),
});
setHasUpdate(false);
pullLatestData();
}, [currentItems, pullLatestData]);
@ -463,11 +427,13 @@ function DetectionReview({
className="absolute w-full z-30"
contentRef={contentRef}
severity={severity}
hasUpdate={hasUpdate}
setHasUpdate={setHasUpdate}
pullLatestData={pullLatestData}
/>
)}
{(itemsToReview == 0 || (currentItems == null && !isValidating)) && (
{itemsToReview == 0 && (
<div className="size-full flex flex-col justify-center items-center">
<LuFolderCheck className="size-16" />
There are no {severity.replace(/_/g, " ")} items to review
@ -478,15 +444,18 @@ function DetectionReview({
className="w-full m-2 grid sm:grid-cols-2 md:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4"
ref={contentRef}
>
{currentItems ? (
currentItems.map((value, segIdx) => {
const lastRow = segIdx == currentItems.length - 1;
{currentItems &&
currentItems.map((value) => {
if (value.has_been_reviewed && filter?.showReviewed != 1) {
return;
}
const selected = selectedReviews.includes(value.id);
return (
<div
key={value.id}
ref={lastRow ? lastReviewRef : minimapRef}
ref={minimapRef}
data-start={value.start_time}
data-segment-start={
alignStartDateToTimeline(value.start_time) - segmentDuration
@ -505,13 +474,9 @@ function DetectionReview({
</div>
</div>
);
})
) : itemsToReview != 0 ? (
<div ref={lastReviewRef} />
) : null}
{currentItems && (
})}
{(itemsToReview ?? 0) > 0 && (
<div className="col-span-full flex justify-center items-center">
{reachedEnd ? (
<Button
className="text-white"
variant="select"
@ -519,9 +484,6 @@ function DetectionReview({
>
Mark these items as reviewed
</Button>
) : (
<ActivityIndicator />
)}
</div>
)}
</div>