Files
frigate/web/src/components/menu/SearchResultActions.tsx
T

288 lines
8.7 KiB
TypeScript
Raw Normal View History

2026-03-04 10:07:34 -06:00
import { useState, ReactNode, useCallback } from "react";
2024-10-22 09:01:01 -05:00
import { SearchResult } from "@/types/search";
2026-05-22 14:41:07 -05:00
import { REVIEW_PADDING } from "@/types/review";
2024-10-22 09:01:01 -05:00
import { FrigateConfig } from "@/types/frigateConfig";
import { baseUrl } from "@/api/baseUrl";
import { toast } from "sonner";
import axios from "axios";
2025-03-06 09:50:37 -07:00
import { FiMoreVertical } from "react-icons/fi";
2026-03-04 10:07:34 -06:00
import { Button, buttonVariants } from "@/components/ui/button";
2024-10-22 09:01:01 -05:00
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import useSWR from "swr";
import { Trans, useTranslation } from "react-i18next";
2025-10-27 07:44:34 -05:00
import BlurredIconButton from "../button/BlurredIconButton";
2025-12-11 08:23:34 -06:00
import { useIsAdmin } from "@/hooks/use-is-admin";
2026-03-04 10:07:34 -06:00
import { useNavigate } from "react-router-dom";
2024-10-22 09:01:01 -05:00
type SearchResultActionsProps = {
searchResult: SearchResult;
findSimilar: () => void;
refreshResults: () => void;
2025-10-26 13:12:20 -05:00
showTrackingDetails: () => void;
2025-07-07 09:03:57 -05:00
addTrigger: () => void;
2024-10-22 09:01:01 -05:00
isContextMenu?: boolean;
children?: ReactNode;
};
export default function SearchResultActions({
searchResult,
findSimilar,
refreshResults,
2025-10-26 13:12:20 -05:00
showTrackingDetails,
2025-07-07 09:03:57 -05:00
addTrigger,
2024-10-22 09:01:01 -05:00
isContextMenu = false,
children,
}: SearchResultActionsProps) {
2026-03-04 10:07:34 -06:00
const { t } = useTranslation(["views/explore", "views/replay", "common"]);
2025-12-11 08:23:34 -06:00
const isAdmin = useIsAdmin();
2026-03-04 10:07:34 -06:00
const navigate = useNavigate();
const [isStarting, setIsStarting] = useState(false);
2024-10-22 09:01:01 -05:00
const { data: config } = useSWR<FrigateConfig>("config");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const handleDelete = () => {
axios
.delete(`events/${searchResult.id}`)
.then((resp) => {
if (resp.status == 200) {
toast.success(t("searchResult.deleteTrackedObject.toast.success"), {
2024-10-22 09:01:01 -05:00
position: "top-center",
});
refreshResults();
}
})
2025-03-08 10:01:08 -06:00
.catch((error) => {
const errorMessage =
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(
t("searchResult.deleteTrackedObject.toast.error", { errorMessage }),
{
position: "top-center",
},
);
2024-10-22 09:01:01 -05:00
});
};
2026-03-04 10:07:34 -06:00
const handleDebugReplay = useCallback(
(event: SearchResult) => {
setIsStarting(true);
axios
.post("debug_replay/start", {
camera: event.camera,
2026-05-22 14:41:07 -05:00
start_time: (event.start_time ?? 0) - REVIEW_PADDING,
end_time: (event.end_time ?? Date.now() / 1000) + REVIEW_PADDING,
2026-03-04 10:07:34 -06:00
})
.then((response) => {
if (response.status === 202 || response.status === 200) {
2026-03-04 10:07:34 -06:00
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: (
2026-05-17 15:40:33 -05:00
<a
href={`${baseUrl}replay`}
target="_blank"
rel="noopener noreferrer"
>
2026-03-04 10:07:34 -06:00
<Button>
{t("dialog.toast.goToReplay", { ns: "views/replay" })}
</Button>
</a>
),
},
);
} else {
2026-05-24 07:48:52 -05:00
toast.error(
t("dialog.toast.error", {
ns: "views/replay",
error: errorMessage,
}),
{
position: "top-center",
},
);
2026-03-04 10:07:34 -06:00
}
})
.finally(() => {
setIsStarting(false);
});
},
[navigate, t],
);
2024-10-22 09:01:01 -05:00
const MenuItem = isContextMenu ? ContextMenuItem : DropdownMenuItem;
const menuItems = (
<>
{searchResult.has_clip && (
<MenuItem aria-label={t("itemMenu.downloadVideo.aria")}>
2024-10-22 09:01:01 -05:00
<a
className="flex items-center"
href={`${baseUrl}api/events/${searchResult.id}/clip.mp4`}
download={`${searchResult.camera}_${searchResult.label}.mp4`}
>
2025-03-16 12:13:34 -05:00
<span>{t("itemMenu.downloadVideo.label")}</span>
2024-10-22 09:01:01 -05:00
</a>
</MenuItem>
)}
{searchResult.has_snapshot && (
<MenuItem aria-label={t("itemMenu.downloadSnapshot.aria")}>
2024-10-22 09:01:01 -05:00
<a
className="flex items-center"
href={`${baseUrl}api/events/${searchResult.id}/snapshot.jpg?crop=0&bbox=1&timestamp=0`}
2024-10-22 09:01:01 -05:00
download={`${searchResult.camera}_${searchResult.label}.jpg`}
>
<span>{t("itemMenu.downloadSnapshot.label")}</span>
2024-10-22 09:01:01 -05:00
</a>
</MenuItem>
)}
{searchResult.has_snapshot && (
<MenuItem aria-label={t("itemMenu.downloadCleanSnapshot.aria")}>
<a
className="flex items-center"
href={`${baseUrl}api/events/${searchResult.id}/snapshot-clean.webp`}
download={`${searchResult.camera}_${searchResult.label}-clean.webp`}
>
<span>{t("itemMenu.downloadCleanSnapshot.label")}</span>
</a>
</MenuItem>
)}
2024-12-01 12:08:03 -06:00
{searchResult.data.type == "object" && (
<MenuItem
2025-10-26 13:12:20 -05:00
aria-label={t("itemMenu.viewTrackingDetails.aria")}
onClick={showTrackingDetails}
2024-12-01 12:08:03 -06:00
>
2025-10-26 13:12:20 -05:00
<span>{t("itemMenu.viewTrackingDetails.label")}</span>
2024-12-01 12:08:03 -06:00
</MenuItem>
)}
2025-11-07 07:53:27 -06:00
{config?.semantic_search?.enabled &&
searchResult.data.type == "object" && (
<MenuItem
2025-11-07 07:53:27 -06:00
aria-label={t("itemMenu.findSimilar.aria")}
onClick={findSimilar}
>
2025-11-07 07:53:27 -06:00
<span>{t("itemMenu.findSimilar.label")}</span>
2024-10-22 09:01:01 -05:00
</MenuItem>
)}
2025-12-11 08:23:34 -06:00
{isAdmin &&
config?.semantic_search?.enabled &&
2025-11-17 08:12:05 -06:00
searchResult.data.type == "object" && (
<MenuItem
aria-label={t("itemMenu.addTrigger.aria")}
onClick={addTrigger}
>
<span>{t("itemMenu.addTrigger.label")}</span>
</MenuItem>
)}
2026-05-24 07:48:52 -05:00
{isAdmin && searchResult.has_clip && (
2026-03-04 10:07:34 -06:00
<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>
)}
2025-12-11 08:23:34 -06:00
{isAdmin && (
<MenuItem
aria-label={t("itemMenu.deleteTrackedObject.label")}
onClick={() => setDeleteDialogOpen(true)}
>
<span>{t("button.delete", { ns: "common" })}</span>
</MenuItem>
)}
2024-10-22 09:01:01 -05:00
</>
);
return (
<>
<AlertDialog
open={deleteDialogOpen}
onOpenChange={() => setDeleteDialogOpen(!deleteDialogOpen)}
>
<AlertDialogContent>
<AlertDialogHeader>
2025-03-17 07:26:01 -05:00
<AlertDialogTitle>
{t("dialog.confirmDelete.title")}
</AlertDialogTitle>
2024-10-22 09:01:01 -05:00
</AlertDialogHeader>
<AlertDialogDescription>
<Trans ns="views/explore">dialog.confirmDelete.desc</Trans>
2024-10-22 09:01:01 -05:00
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>
{t("button.cancel", { ns: "common" })}
</AlertDialogCancel>
2024-10-22 09:01:01 -05:00
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
onClick={handleDelete}
>
{t("button.delete", { ns: "common" })}
2024-10-22 09:01:01 -05:00
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{isContextMenu ? (
2026-04-25 08:21:13 -05:00
<ContextMenu>
2024-10-22 09:01:01 -05:00
<ContextMenuTrigger>{children}</ContextMenuTrigger>
<ContextMenuContent>{menuItems}</ContextMenuContent>
</ContextMenu>
) : (
<>
2026-04-25 08:21:13 -05:00
<DropdownMenu>
2025-10-27 07:44:34 -05:00
<DropdownMenuTrigger asChild>
<BlurredIconButton aria-label={t("itemMenu.more.aria")}>
<FiMoreVertical className="size-5" />
</BlurredIconButton>
2024-10-22 09:01:01 -05:00
</DropdownMenuTrigger>
<DropdownMenuContent align="end">{menuItems}</DropdownMenuContent>
</DropdownMenu>
</>
)}
</>
);
}