mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-12 11:27:34 +03:00
add debug replay to explore and review menus
This commit is contained in:
parent
68d8fc3987
commit
126e708c4c
@ -216,6 +216,10 @@
|
|||||||
},
|
},
|
||||||
"hideObjectDetails": {
|
"hideObjectDetails": {
|
||||||
"label": "Hide object path"
|
"label": "Hide object path"
|
||||||
|
},
|
||||||
|
"debugReplay": {
|
||||||
|
"label": "Debug replay",
|
||||||
|
"aria": "View this tracked object in the debug replay view"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { useState, ReactNode } from "react";
|
import { useState, ReactNode, useCallback } from "react";
|
||||||
import { SearchResult } from "@/types/search";
|
import { SearchResult } from "@/types/search";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { baseUrl } from "@/api/baseUrl";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { FiMoreVertical } from "react-icons/fi";
|
import { FiMoreVertical } from "react-icons/fi";
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
@ -32,6 +32,7 @@ import useSWR from "swr";
|
|||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import BlurredIconButton from "../button/BlurredIconButton";
|
import BlurredIconButton from "../button/BlurredIconButton";
|
||||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
type SearchResultActionsProps = {
|
type SearchResultActionsProps = {
|
||||||
searchResult: SearchResult;
|
searchResult: SearchResult;
|
||||||
@ -52,8 +53,10 @@ export default function SearchResultActions({
|
|||||||
isContextMenu = false,
|
isContextMenu = false,
|
||||||
children,
|
children,
|
||||||
}: SearchResultActionsProps) {
|
}: SearchResultActionsProps) {
|
||||||
const { t } = useTranslation(["views/explore"]);
|
const { t } = useTranslation(["views/explore", "views/replay", "common"]);
|
||||||
const isAdmin = useIsAdmin();
|
const isAdmin = useIsAdmin();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isStarting, setIsStarting] = useState(false);
|
||||||
|
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
@ -84,6 +87,59 @@ export default function SearchResultActions({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDebugReplay = useCallback(
|
||||||
|
(event: SearchResult) => {
|
||||||
|
setIsStarting(true);
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post("debug_replay/start", {
|
||||||
|
camera: event.camera,
|
||||||
|
start_time: event.start_time,
|
||||||
|
end_time: event.end_time,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
toast.success(t("dialog.toast.success", { ns: "views/replay" }), {
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
navigate("/replay");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMessage =
|
||||||
|
error.response?.data?.message ||
|
||||||
|
error.response?.data?.detail ||
|
||||||
|
"Unknown error";
|
||||||
|
|
||||||
|
if (error.response?.status === 409) {
|
||||||
|
toast.error(
|
||||||
|
t("dialog.toast.alreadyActive", { ns: "views/replay" }),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
closeButton: true,
|
||||||
|
dismissible: false,
|
||||||
|
action: (
|
||||||
|
<a href="/replay" target="_blank" rel="noopener noreferrer">
|
||||||
|
<Button>
|
||||||
|
{t("dialog.toast.goToReplay", { ns: "views/replay" })}
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.error(t("dialog.toast.error", { error: errorMessage }), {
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsStarting(false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[navigate, t],
|
||||||
|
);
|
||||||
|
|
||||||
const MenuItem = isContextMenu ? ContextMenuItem : DropdownMenuItem;
|
const MenuItem = isContextMenu ? ContextMenuItem : DropdownMenuItem;
|
||||||
|
|
||||||
const menuItems = (
|
const menuItems = (
|
||||||
@ -149,6 +205,20 @@ export default function SearchResultActions({
|
|||||||
<span>{t("itemMenu.addTrigger.label")}</span>
|
<span>{t("itemMenu.addTrigger.label")}</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
{searchResult.has_clip && (
|
||||||
|
<MenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
aria-label={t("itemMenu.debugReplay.aria")}
|
||||||
|
disabled={isStarting}
|
||||||
|
onSelect={() => {
|
||||||
|
handleDebugReplay(searchResult);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isStarting
|
||||||
|
? t("dialog.starting", { ns: "views/replay" })
|
||||||
|
: t("itemMenu.debugReplay.label")}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
aria-label={t("itemMenu.deleteTrackedObject.label")}
|
aria-label={t("itemMenu.deleteTrackedObject.label")}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
} from "../ui/dropdown-menu";
|
} from "../ui/dropdown-menu";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FaEllipsisVertical } from "react-icons/fa6";
|
import { FaFilm } from "react-icons/fa6";
|
||||||
|
|
||||||
type ActionsDropdownProps = {
|
type ActionsDropdownProps = {
|
||||||
onDebugReplayClick: () => void;
|
onDebugReplayClick: () => void;
|
||||||
@ -27,7 +27,7 @@ export default function ActionsDropdown({
|
|||||||
aria-label={t("menu.actions", { ns: "common" })}
|
aria-label={t("menu.actions", { ns: "common" })}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<FaEllipsisVertical className="size-5 text-secondary-foreground" />
|
<FaFilm className="size-5 text-secondary-foreground" />
|
||||||
<div className="text-primary">
|
<div className="text-primary">
|
||||||
{t("menu.actions", { ns: "common" })}
|
{t("menu.actions", { ns: "common" })}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,8 +12,11 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Event } from "@/types/event";
|
import { Event } from "@/types/event";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||||
|
import axios from "axios";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
type EventMenuProps = {
|
type EventMenuProps = {
|
||||||
event: Event;
|
event: Event;
|
||||||
@ -34,9 +37,10 @@ export default function EventMenu({
|
|||||||
}: EventMenuProps) {
|
}: EventMenuProps) {
|
||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation("views/explore");
|
const { t } = useTranslation(["views/explore", "views/replay"]);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const isAdmin = useIsAdmin();
|
const isAdmin = useIsAdmin();
|
||||||
|
const [isStarting, setIsStarting] = useState(false);
|
||||||
|
|
||||||
const handleObjectSelect = () => {
|
const handleObjectSelect = () => {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
@ -46,6 +50,59 @@ export default function EventMenu({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDebugReplay = useCallback(
|
||||||
|
(event: Event) => {
|
||||||
|
setIsStarting(true);
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post("debug_replay/start", {
|
||||||
|
camera: event.camera,
|
||||||
|
start_time: event.start_time,
|
||||||
|
end_time: event.end_time,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
toast.success(t("dialog.toast.success", { ns: "views/replay" }), {
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
navigate("/replay");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMessage =
|
||||||
|
error.response?.data?.message ||
|
||||||
|
error.response?.data?.detail ||
|
||||||
|
"Unknown error";
|
||||||
|
|
||||||
|
if (error.response?.status === 409) {
|
||||||
|
toast.error(
|
||||||
|
t("dialog.toast.alreadyActive", { ns: "views/replay" }),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
closeButton: true,
|
||||||
|
dismissible: false,
|
||||||
|
action: (
|
||||||
|
<a href="/replay" target="_blank" rel="noopener noreferrer">
|
||||||
|
<Button>
|
||||||
|
{t("dialog.toast.goToReplay", { ns: "views/replay" })}
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.error(t("dialog.toast.error", { error: errorMessage }), {
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsStarting(false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[navigate, t],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span tabIndex={0} className="sr-only" />
|
<span tabIndex={0} className="sr-only" />
|
||||||
@ -117,6 +174,19 @@ export default function EventMenu({
|
|||||||
{t("itemMenu.findSimilar.label")}
|
{t("itemMenu.findSimilar.label")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
{event.has_clip && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
disabled={isStarting}
|
||||||
|
onSelect={() => {
|
||||||
|
handleDebugReplay(event);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isStarting
|
||||||
|
? t("dialog.starting", { ns: "views/replay" })
|
||||||
|
: t("itemMenu.debugReplay.label")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenuPortal>
|
</DropdownMenuPortal>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user