conditionally display actions for admin role only

This commit is contained in:
Josh Hawkins 2025-12-09 14:37:38 -06:00
parent 9cdc10008d
commit 8d5349ca85
4 changed files with 72 additions and 55 deletions

View File

@ -19,6 +19,7 @@ import {
import useKeyboardListener from "@/hooks/use-keyboard-listener"; import useKeyboardListener from "@/hooks/use-keyboard-listener";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
import { useIsAdmin } from "@/hooks/use-is-admin";
type ReviewActionGroupProps = { type ReviewActionGroupProps = {
selectedReviews: ReviewSegment[]; selectedReviews: ReviewSegment[];
@ -33,6 +34,7 @@ export default function ReviewActionGroup({
pullLatestData, pullLatestData,
}: ReviewActionGroupProps) { }: ReviewActionGroupProps) {
const { t } = useTranslation(["components/dialog"]); const { t } = useTranslation(["components/dialog"]);
const isAdmin = useIsAdmin();
const onClearSelected = useCallback(() => { const onClearSelected = useCallback(() => {
setSelectedReviews([]); setSelectedReviews([]);
}, [setSelectedReviews]); }, [setSelectedReviews]);
@ -185,21 +187,23 @@ export default function ReviewActionGroup({
</div> </div>
)} )}
</Button> </Button>
<Button {isAdmin && (
className="flex items-center gap-2 p-2" <Button
aria-label={t("button.delete", { ns: "common" })} className="flex items-center gap-2 p-2"
size="sm" aria-label={t("button.delete", { ns: "common" })}
onClick={handleDelete} size="sm"
> onClick={handleDelete}
<HiTrash className="text-secondary-foreground" /> >
{isDesktop && ( <HiTrash className="text-secondary-foreground" />
<div className="text-primary"> {isDesktop && (
{bypassDialog <div className="text-primary">
? t("recording.button.deleteNow") {bypassDialog
: t("button.delete", { ns: "common" })} ? t("recording.button.deleteNow")
</div> : t("button.delete", { ns: "common" })}
)} </div>
</Button> )}
</Button>
)}
</div> </div>
</div> </div>
</> </>

View File

@ -16,6 +16,7 @@ import {
import useKeyboardListener from "@/hooks/use-keyboard-listener"; import useKeyboardListener from "@/hooks/use-keyboard-listener";
import { toast } from "sonner"; import { toast } from "sonner";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { useIsAdmin } from "@/hooks/use-is-admin";
type SearchActionGroupProps = { type SearchActionGroupProps = {
selectedObjects: string[]; selectedObjects: string[];
@ -28,6 +29,7 @@ export default function SearchActionGroup({
pullLatestData, pullLatestData,
}: SearchActionGroupProps) { }: SearchActionGroupProps) {
const { t } = useTranslation(["components/filter"]); const { t } = useTranslation(["components/filter"]);
const isAdmin = useIsAdmin();
const onClearSelected = useCallback(() => { const onClearSelected = useCallback(() => {
setSelectedObjects([]); setSelectedObjects([]);
}, [setSelectedObjects]); }, [setSelectedObjects]);
@ -123,23 +125,25 @@ export default function SearchActionGroup({
{t("button.unselect", { ns: "common" })} {t("button.unselect", { ns: "common" })}
</div> </div>
</div> </div>
<div className="flex items-center gap-1 md:gap-2"> {isAdmin && (
<Button <div className="flex items-center gap-1 md:gap-2">
className="flex items-center gap-2 p-2" <Button
aria-label={t("button.delete", { ns: "common" })} className="flex items-center gap-2 p-2"
size="sm" aria-label={t("button.delete", { ns: "common" })}
onClick={handleDelete} size="sm"
> onClick={handleDelete}
<HiTrash className="text-secondary-foreground" /> >
{isDesktop && ( <HiTrash className="text-secondary-foreground" />
<div className="text-primary"> {isDesktop && (
{bypassDialog <div className="text-primary">
? t("button.deleteNow", { ns: "common" }) {bypassDialog
: t("button.delete", { ns: "common" })} ? t("button.deleteNow", { ns: "common" })
</div> : t("button.delete", { ns: "common" })}
)} </div>
</Button> )}
</div> </Button>
</div>
)}
</div> </div>
</> </>
); );

View File

@ -31,6 +31,7 @@ import {
import useSWR from "swr"; 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";
type SearchResultActionsProps = { type SearchResultActionsProps = {
searchResult: SearchResult; searchResult: SearchResult;
@ -52,6 +53,7 @@ export default function SearchResultActions({
children, children,
}: SearchResultActionsProps) { }: SearchResultActionsProps) {
const { t } = useTranslation(["views/explore"]); const { t } = useTranslation(["views/explore"]);
const isAdmin = useIsAdmin();
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
@ -137,7 +139,8 @@ export default function SearchResultActions({
<span>{t("itemMenu.findSimilar.label")}</span> <span>{t("itemMenu.findSimilar.label")}</span>
</MenuItem> </MenuItem>
)} )}
{config?.semantic_search?.enabled && {isAdmin &&
config?.semantic_search?.enabled &&
searchResult.data.type == "object" && ( searchResult.data.type == "object" && (
<MenuItem <MenuItem
aria-label={t("itemMenu.addTrigger.aria")} aria-label={t("itemMenu.addTrigger.aria")}
@ -146,12 +149,14 @@ export default function SearchResultActions({
<span>{t("itemMenu.addTrigger.label")}</span> <span>{t("itemMenu.addTrigger.label")}</span>
</MenuItem> </MenuItem>
)} )}
<MenuItem {isAdmin && (
aria-label={t("itemMenu.deleteTrackedObject.label")} <MenuItem
onClick={() => setDeleteDialogOpen(true)} aria-label={t("itemMenu.deleteTrackedObject.label")}
> onClick={() => setDeleteDialogOpen(true)}
<span>{t("button.delete", { ns: "common" })}</span> >
</MenuItem> <span>{t("button.delete", { ns: "common" })}</span>
</MenuItem>
)}
</> </>
); );

View File

@ -15,6 +15,7 @@ import {
import { HiDotsHorizontal } from "react-icons/hi"; import { HiDotsHorizontal } from "react-icons/hi";
import { SearchResult } from "@/types/search"; import { SearchResult } from "@/types/search";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { useIsAdmin } from "@/hooks/use-is-admin";
type Props = { type Props = {
search: SearchResult | Event; search: SearchResult | Event;
@ -35,6 +36,7 @@ export default function DetailActionsMenu({
const { t } = useTranslation(["views/explore", "views/faceLibrary"]); const { t } = useTranslation(["views/explore", "views/faceLibrary"]);
const navigate = useNavigate(); const navigate = useNavigate();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const isAdmin = useIsAdmin();
const clipTimeRange = useMemo(() => { const clipTimeRange = useMemo(() => {
const startTime = (search.start_time ?? 0) - REVIEW_PADDING; const startTime = (search.start_time ?? 0) - REVIEW_PADDING;
@ -130,22 +132,24 @@ export default function DetailActionsMenu({
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{config?.semantic_search.enabled && search.data.type == "object" && ( {isAdmin &&
<DropdownMenuItem config?.semantic_search.enabled &&
onClick={() => { search.data.type == "object" && (
setIsOpen(false); <DropdownMenuItem
setTimeout(() => { onClick={() => {
navigate( setIsOpen(false);
`/settings?page=triggers&camera=${search.camera}&event_id=${search.id}`, setTimeout(() => {
); navigate(
}, 0); `/settings?page=triggers&camera=${search.camera}&event_id=${search.id}`,
}} );
> }, 0);
<div className="flex cursor-pointer items-center gap-2"> }}
<span>{t("itemMenu.addTrigger.label")}</span> >
</div> <div className="flex cursor-pointer items-center gap-2">
</DropdownMenuItem> <span>{t("itemMenu.addTrigger.label")}</span>
)} </div>
</DropdownMenuItem>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenuPortal> </DropdownMenuPortal>
</DropdownMenu> </DropdownMenu>