Reviewed buttons (#10271)

* mark items as reviewed when they are opened

* Update api to use json and add button to mark all as reviewed

* fix api so last24 hours has its own review summary

* fix sidebar spacing

* formatting

* Bug fixes

* Make motion activity respect filters
This commit is contained in:
Nicolas Mowen
2024-03-05 17:39:37 -07:00
committed by GitHub
parent b5edcd2fae
commit 68ed18d3f4
8 changed files with 219 additions and 75 deletions

View File

@@ -22,7 +22,7 @@ export default function ReviewActionGroup({
const onMarkAsReviewed = useCallback(async () => {
const idList = selectedReviews.join(",");
await axios.post(`reviews/${idList}/viewed`);
await axios.post(`reviews/viewed`, { ids: idList });
setSelectedReviews([]);
pullLatestData();
}, [selectedReviews, setSelectedReviews, pullLatestData]);

View File

@@ -13,20 +13,23 @@ function Sidebar() {
<span tabIndex={0} className="sr-only" />
<div className="w-full flex flex-col gap-0 items-center">
<Logo className="w-8 h-8 mb-6" />
{navbarLinks.map((item) => (
<div key={item.id}>
<NavItem
className={`mx-[10px] ${item.id == 1 ? "mb-2" : "mb-4"}`}
Icon={item.icon}
title={item.title}
url={item.url}
dev={item.dev}
/>
{item.id == 1 && item.url == location.pathname && (
<CameraGroupSelector className="mb-4" />
)}
</div>
))}
{navbarLinks.map((item) => {
const showCameraGroups =
item.id == 1 && item.url == location.pathname;
return (
<div key={item.id}>
<NavItem
className={`mx-[10px] ${showCameraGroups ? "mb-2" : "mb-4"}`}
Icon={item.icon}
title={item.title}
url={item.url}
dev={item.dev}
/>
{showCameraGroups && <CameraGroupSelector className="mb-4" />}
</div>
);
})}
</div>
<SettingsNavItems className="hidden md:flex flex-col items-center mb-8" />
</aside>

View File

@@ -29,7 +29,7 @@ type DynamicVideoPlayerProps = {
timeRange: { start: number; end: number };
cameraPreviews: Preview[];
previewOnly?: boolean;
onControllerReady?: (controller: DynamicVideoController) => void;
onControllerReady: (controller: DynamicVideoController) => void;
onClick?: () => void;
};
export default function DynamicVideoPlayer({
@@ -86,14 +86,17 @@ export default function DynamicVideoPlayer({
}, [camera, config, previewOnly]);
useEffect(() => {
if (!controller) {
if (!playerRef.current && !previewRef.current) {
return;
}
if (onControllerReady) {
if (controller) {
onControllerReady(controller);
}
}, [controller, onControllerReady]);
// we only want to fire once when players are ready
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [playerRef, previewRef]);
const [hasRecordingAtTime, setHasRecordingAtTime] = useState(true);
@@ -277,10 +280,6 @@ export default function DynamicVideoPlayer({
player.on("ended", () =>
controller.fireClipChangeEvent("forward"),
);
if (onControllerReady) {
onControllerReady(controller);
}
}}
onDispose={() => {
playerRef.current = undefined;

View File

@@ -10,6 +10,7 @@ const buttonVariants = cva(
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
select: "bg-select text-white hover:bg-select/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:

View File

@@ -115,9 +115,7 @@ export default function Events() {
// review summary
const { data: reviewSummary, mutate: updateSummary } = useSWR<
ReviewSummary[]
>([
const { data: reviewSummary, mutate: updateSummary } = useSWR<ReviewSummary>([
"review/summary",
{
timezone: timezone,
@@ -164,7 +162,7 @@ export default function Events() {
const markItemAsReviewed = useCallback(
async (review: ReviewSegment) => {
const resp = await axios.post(`review/${review.id}/viewed`);
const resp = await axios.post(`reviews/viewed`, { ids: [review.id] });
if (resp.status == 200) {
updateSegments(
@@ -197,23 +195,30 @@ export default function Events() {
);
updateSummary(
(data: ReviewSummary[] | undefined) => {
(data: ReviewSummary | undefined) => {
if (!data) {
return data;
}
const day = new Date(review.start_time * 1000);
const key = `${day.getFullYear()}-${("0" + (day.getMonth() + 1)).slice(-2)}-${("0" + day.getDate()).slice(-2)}`;
const index = data.findIndex((summary) => summary.day == key);
const today = new Date();
today.setHours(0, 0, 0, 0);
if (index == -1) {
let key;
if (day.getTime() > today.getTime()) {
key = "last24Hours";
} else {
key = `${day.getFullYear()}-${("0" + (day.getMonth() + 1)).slice(-2)}-${("0" + day.getDate()).slice(-2)}`;
}
if (!Object.keys(data).includes(key)) {
return data;
}
const item = data[index];
return [
...data.slice(0, index),
{
const item = data[key];
return {
...data,
[key]: {
...item,
reviewed_alert:
review.severity == "alert"
@@ -228,8 +233,7 @@ export default function Events() {
? item.reviewed_motion + 1
: item.reviewed_motion,
},
...data.slice(index + 1),
];
};
},
{ revalidate: false, populateCache: true },
);
@@ -279,6 +283,11 @@ export default function Events() {
return undefined;
}
// mark item as reviewed since it has been opened
if (!selectedReview?.has_been_reviewed) {
markItemAsReviewed(selectedReview);
}
return {
camera: selectedReview.camera,
severity: selectedReview.severity,

View File

@@ -28,7 +28,7 @@ export type ReviewFilter = {
showReviewed?: 0 | 1;
};
export type ReviewSummary = {
type ReviewSummaryDay = {
day: string;
reviewed_alert: number;
reviewed_detection: number;
@@ -38,6 +38,10 @@ export type ReviewSummary = {
total_motion: number;
};
export type ReviewSummary = {
[day: string]: ReviewSummaryDay;
};
export type MotionData = {
start_time: number;
motion: number;

View File

@@ -35,10 +35,11 @@ import { LuFolderCheck } from "react-icons/lu";
import { MdCircle } from "react-icons/md";
import useSWR from "swr";
import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline";
import { Button } from "@/components/ui/button";
type EventViewProps = {
reviewPages?: ReviewSegment[][];
reviewSummary?: ReviewSummary[];
reviewSummary?: ReviewSummary;
relevantPreviews?: Preview[];
timeRange: { before: number; after: number };
reachedEnd: boolean;
@@ -74,17 +75,17 @@ export default function EventView({
// review counts
const reviewCounts = useMemo(() => {
if (!reviewSummary || reviewSummary.length == 0) {
if (!reviewSummary) {
return { alert: 0, detection: 0, significant_motion: 0 };
}
let summary;
if (filter?.before == undefined) {
summary = reviewSummary[0];
summary = reviewSummary["last24Hours"];
} else {
const day = new Date(filter.before * 1000);
const key = `${day.getFullYear()}-${("0" + (day.getMonth() + 1)).slice(-2)}-${("0" + day.getDate()).slice(-2)}`;
summary = reviewSummary.find((check) => check.day == key);
summary = reviewSummary[key];
}
if (!summary) {
@@ -211,9 +212,11 @@ export default function EventView({
<ToggleGroup
className="*:px-3 *:py-4 *:rounded-2xl"
type="single"
defaultValue="alert"
size="sm"
onValueChange={(value: ReviewSeverity) => setSeverity(value)}
value={severity}
onValueChange={(value: ReviewSeverity) =>
value ? setSeverity(value) : null
} // don't allow the severity to be unselected
>
<ToggleGroupItem
className={`${severity == "alert" ? "" : "text-gray-500"}`}
@@ -241,9 +244,7 @@ export default function EventView({
aria-label="Select motion"
>
<MdCircle className="size-2 md:mr-[10px] text-severity_motion" />
<div className="hidden md:block">
Motion {reviewCounts.significant_motion}
</div>
<div className="hidden md:block">Motion</div>
</ToggleGroupItem>
</ToggleGroup>
@@ -303,6 +304,7 @@ type DetectionReviewProps = {
detection: ReviewSegment[];
significant_motion: ReviewSegment[];
};
itemsToReview?: number;
relevantPreviews?: Preview[];
pagingObserver: MutableRefObject<IntersectionObserver | null>;
selectedReviews: string[];
@@ -320,6 +322,7 @@ function DetectionReview({
contentRef,
currentItems,
reviewItems,
itemsToReview,
relevantPreviews,
pagingObserver,
selectedReviews,
@@ -359,6 +362,17 @@ function DetectionReview({
[isValidating, pagingObserver, reachedEnd, loadNextPage],
);
const markAllReviewed = useCallback(async () => {
if (!currentItems) {
return;
}
await axios.post(`reviews/viewed`, {
ids: currentItems?.map((seg) => seg.id),
});
pullLatestData();
}, [currentItems, pullLatestData]);
// timeline interaction
const { alignStartDateToTimeline } = useEventUtils(
@@ -453,7 +467,7 @@ function DetectionReview({
/>
)}
{!isValidating && currentItems == null && (
{(itemsToReview == 0 || (currentItems == null && !isValidating)) && (
<div className="size-full flex flex-col justify-center items-center">
<LuFolderCheck className="size-16" />
There are no {severity.replace(/_/g, " ")} items to review
@@ -489,13 +503,27 @@ function DetectionReview({
onClick={onSelectReview}
/>
</div>
{lastRow && !reachedEnd && <ActivityIndicator />}
</div>
);
})
) : severity != "alert" ? (
) : itemsToReview != 0 ? (
<div ref={lastReviewRef} />
) : null}
{currentItems && (
<div className="col-span-full flex justify-center items-center">
{reachedEnd ? (
<Button
className="text-white"
variant="select"
onClick={markAllReviewed}
>
Mark all items as reviewed
</Button>
) : (
<ActivityIndicator />
)}
</div>
)}
</div>
</div>
<div className="w-[55px] md:w-[100px] mt-2 overflow-y-auto no-scrollbar">
@@ -574,6 +602,7 @@ function MotionReview({
before: timeRange.before,
after: timeRange.after,
scale: segmentDuration / 2,
cameras: filter?.cameras?.join(",") ?? null,
},
]);