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

172 lines
5.5 KiB
TypeScript
Raw Normal View History

import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { baseUrl } from "../../api/baseUrl";
import { cn } from "@/lib/utils";
2024-05-06 12:18:28 -05:00
import { TooltipPortal } from "@radix-ui/react-tooltip";
2024-04-16 14:55:24 -06:00
import { isDesktop } from "react-device-detect";
import { VscAccount } from "react-icons/vsc";
2024-05-18 11:36:13 -05:00
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
2025-03-17 07:26:01 -05:00
} from "@/components/ui/dropdown-menu";
import {
Drawer,
DrawerContent,
DrawerTrigger,
DrawerClose,
} from "@/components/ui/drawer";
2025-03-08 10:01:08 -06:00
import { LuLogOut, LuSquarePen } from "react-icons/lu";
2024-05-18 11:36:13 -05:00
import useSWR from "swr";
2025-03-08 10:01:08 -06:00
import { useState } from "react";
import axios from "axios";
import { toast } from "sonner";
import SetPasswordDialog from "../overlay/SetPasswordDialog";
import { useTranslation } from "react-i18next";
2024-04-29 09:59:27 -06:00
type AccountSettingsProps = {
className?: string;
};
2025-03-08 10:01:08 -06:00
2024-04-29 09:59:27 -06:00
export default function AccountSettings({ className }: AccountSettingsProps) {
2025-03-17 07:26:01 -05:00
const { t } = useTranslation(["views/settings", "common"]);
2024-05-18 11:36:13 -05:00
const { data: profile } = useSWR("profile");
const { data: config } = useSWR("config");
const logoutUrl = config?.proxy?.logout_url || `${baseUrl}api/logout`;
2024-05-18 11:36:13 -05:00
2025-03-08 10:01:08 -06:00
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
2025-12-08 10:02:28 -06:00
const [passwordError, setPasswordError] = useState<string | null>(null);
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
2025-03-08 10:01:08 -06:00
2024-05-18 11:36:13 -05:00
const Container = isDesktop ? DropdownMenu : Drawer;
const Trigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger;
const Content = isDesktop ? DropdownMenuContent : DrawerContent;
2025-03-17 07:26:01 -05:00
const MenuItem = isDesktop ? DropdownMenuItem : DrawerClose;
2024-05-18 11:36:13 -05:00
2025-12-08 10:02:28 -06:00
const handlePasswordSave = async (password: string, oldPassword?: string) => {
2025-03-08 10:01:08 -06:00
if (!profile?.username || profile.username === "anonymous") return;
2025-12-08 10:02:28 -06:00
setIsPasswordLoading(true);
2025-03-08 10:01:08 -06:00
axios
2025-12-08 10:02:28 -06:00
.put(`users/${profile.username}/password`, {
password,
old_password: oldPassword,
})
2025-03-08 10:01:08 -06:00
.then((response) => {
if (response.status === 200) {
setPasswordDialogOpen(false);
2025-12-08 10:02:28 -06:00
setPasswordError(null);
setIsPasswordLoading(false);
toast.success(t("users.toast.success.updatePassword"), {
2025-03-08 10:01:08 -06:00
position: "top-center",
});
}
})
.catch((error) => {
const errorMessage =
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
2025-12-08 10:02:28 -06:00
// Keep dialog open and show error
setPasswordError(errorMessage);
setIsPasswordLoading(false);
2025-03-08 10:01:08 -06:00
});
};
return (
2024-05-30 08:39:14 -05:00
<Container modal={!isDesktop}>
2025-03-17 07:26:01 -05:00
<Tooltip>
<Trigger asChild>
2024-05-18 14:19:32 -06:00
<TooltipTrigger asChild>
<div
className={cn(
"flex flex-col items-center justify-center",
isDesktop
? "cursor-pointer rounded-lg bg-secondary text-secondary-foreground hover:bg-muted"
: "text-secondary-foreground",
className,
)}
2024-05-18 11:36:13 -05:00
>
2024-05-18 14:19:32 -06:00
<VscAccount className="size-5 md:m-[6px]" />
</div>
</TooltipTrigger>
2025-03-17 07:26:01 -05:00
</Trigger>
<TooltipPortal>
<TooltipContent side="right" sideOffset={5}>
<p>{t("menu.user.account", { ns: "common" })}</p>
</TooltipContent>
</TooltipPortal>
</Tooltip>
2024-05-18 14:19:32 -06:00
<Content
2025-03-17 07:26:01 -05:00
className={cn(
isDesktop ? "mr-5 w-72" : "max-h-[75dvh] overflow-hidden p-4",
)}
2024-05-18 14:19:32 -06:00
>
<div className="scrollbar-container w-full flex-col overflow-y-auto overflow-x-hidden">
2025-03-17 07:26:01 -05:00
<DropdownMenuLabel className="flex flex-col gap-1.5">
<div>
{t("menu.user.current", {
ns: "common",
user:
profile?.username ||
t("menu.user.anonymous", { ns: "common" }),
})}{" "}
{t("role." + profile?.role) &&
`(${t("role." + profile?.role, { ns: "common" })})`}
</div>
2024-05-18 14:19:32 -06:00
</DropdownMenuLabel>
2025-03-17 07:26:01 -05:00
<DropdownMenuSeparator className={isDesktop ? "my-2" : "my-2"} />
2025-03-08 10:01:08 -06:00
{profile?.username && profile.username !== "anonymous" && (
<MenuItem
2025-03-17 07:26:01 -05:00
className={cn(
"flex w-full items-center gap-2",
isDesktop ? "cursor-pointer" : "p-2 text-sm",
)}
aria-label={t("menu.user.setPassword", { ns: "common" })}
2025-03-08 10:01:08 -06:00
onClick={() => setPasswordDialogOpen(true)}
>
<LuSquarePen className="mr-2 size-4" />
<span>{t("menu.user.setPassword", { ns: "common" })}</span>
2025-03-08 10:01:08 -06:00
</MenuItem>
)}
2025-03-17 07:26:01 -05:00
2024-05-18 14:19:32 -06:00
<MenuItem
2025-03-17 07:26:01 -05:00
className={cn(
"flex w-full items-center gap-2",
isDesktop ? "cursor-pointer" : "p-2 text-sm",
)}
asChild
aria-label={t("menu.user.logout", { ns: "common" })}
2024-05-18 14:19:32 -06:00
>
2025-03-17 07:26:01 -05:00
<a href={logoutUrl} className="flex items-center gap-2">
2024-05-18 14:19:32 -06:00
<LuLogOut className="mr-2 size-4" />
<span>{t("menu.user.logout", { ns: "common" })}</span>
2024-05-18 14:19:32 -06:00
</a>
</MenuItem>
</div>
</Content>
2025-03-08 10:01:08 -06:00
<SetPasswordDialog
show={passwordDialogOpen}
onSave={handlePasswordSave}
2025-12-08 10:02:28 -06:00
onCancel={() => {
setPasswordDialogOpen(false);
setPasswordError(null);
}}
initialError={passwordError}
2025-03-08 10:01:08 -06:00
username={profile?.username}
2025-12-08 10:02:28 -06:00
isLoading={isPasswordLoading}
2025-03-08 10:01:08 -06:00
/>
2024-05-18 14:19:32 -06:00
</Container>
);
}