diff --git a/web/src/components/overlay/CreateRoleDialog.tsx b/web/src/components/overlay/CreateRoleDialog.tsx new file mode 100644 index 000000000..4e53c006d --- /dev/null +++ b/web/src/components/overlay/CreateRoleDialog.tsx @@ -0,0 +1,228 @@ +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { Switch } from "@/components/ui/switch"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import { useEffect, useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useTranslation } from "react-i18next"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { CameraNameLabel } from "../camera/CameraNameLabel"; + +type CreateRoleOverlayProps = { + show: boolean; + config: FrigateConfig; + onCreate: (role: string, cameras: string[]) => void; + onCancel: () => void; +}; + +export default function CreateRoleDialog({ + show, + config, + onCreate, + onCancel, +}: CreateRoleOverlayProps) { + const { t } = useTranslation(["views/settings"]); + const [isLoading, setIsLoading] = useState(false); + + const cameras = Object.keys(config.cameras || {}); + + const existingRoles = Object.keys(config.auth?.roles || {}); + + const formSchema = z.object({ + role: z + .string() + .min(1, t("roles.dialog.form.roleIsRequired")) + .regex(/^[A-Za-z0-9._]+$/, { + message: t("roles.dialog.form.role.roleOnlyInclude"), + }) + .refine((role) => !existingRoles.includes(role), { + message: t("roles.dialog.form.role.roleExists"), + }), + cameras: z + .array(z.string()) + .min(1, t("roles.dialog.form.cameras.required")), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + mode: "onChange", + defaultValues: { + role: "", + cameras: [], + }, + }); + + const onSubmit = async (values: z.infer) => { + setIsLoading(true); + try { + await onCreate(values.role, values.cameras); + form.reset(); + } catch (error) { + // Error handled in parent + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (!show) { + form.reset({ + role: "", + cameras: [], + }); + } + }, [show, form]); + + const handleCancel = () => { + form.reset({ + role: "", + cameras: [], + }); + onCancel(); + }; + + return ( + + + + {t("roles.dialog.createRole.title")} + + {t("roles.dialog.createRole.desc")} + + + +
+ + ( + + + {t("roles.dialog.form.role.title")} + + + + + + {t("roles.dialog.form.role.desc")} + + + + )} + /> + +
+ {t("roles.dialog.form.cameras.title")} + + {t("roles.dialog.form.cameras.desc")} + +
+ {cameras.map((camera) => ( + { + return ( + +
+ + + +
+ + { + return checked + ? field.onChange([ + ...(field.value as string[]), + camera, + ]) + : field.onChange( + (field.value as string[])?.filter( + (value: string) => value !== camera, + ) || [], + ); + }} + /> + +
+ ); + }} + /> + ))} +
+ +
+ + +
+
+ + +
+
+
+ + +
+
+ ); +} diff --git a/web/src/components/overlay/EditRoleCamerasDialog.tsx b/web/src/components/overlay/EditRoleCamerasDialog.tsx new file mode 100644 index 000000000..7aee546df --- /dev/null +++ b/web/src/components/overlay/EditRoleCamerasDialog.tsx @@ -0,0 +1,195 @@ +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { Switch } from "@/components/ui/switch"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { Trans, useTranslation } from "react-i18next"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { CameraNameLabel } from "@/components/camera/CameraNameLabel"; + +type EditRoleCamerasOverlayProps = { + show: boolean; + config: FrigateConfig; + role: string; + currentCameras: string[]; + onSave: (cameras: string[]) => void; + onCancel: () => void; +}; + +export default function EditRoleCamerasDialog({ + show, + config, + role, + currentCameras, + onSave, + onCancel, +}: EditRoleCamerasOverlayProps) { + const { t } = useTranslation(["views/settings"]); + const [isLoading, setIsLoading] = useState(false); + + const cameras = Object.keys(config.cameras || {}); + + const formSchema = z.object({ + cameras: z + .array(z.string()) + .min(1, t("roles.dialog.form.cameras.required")), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + mode: "onChange", + defaultValues: { + cameras: currentCameras, + }, + }); + + const onSubmit = async (values: z.infer) => { + setIsLoading(true); + try { + await onSave(values.cameras); + form.reset(); + } catch (error) { + // Error handled in parent + } finally { + setIsLoading(false); + } + }; + + const handleCancel = () => { + form.reset({ + cameras: currentCameras, + }); + onCancel(); + }; + + return ( + + + + + {t("roles.dialog.editCameras.title", { role })} + + + }} + > + roles.dialog.editCameras.desc + + + + +
+ +
+ {t("roles.dialog.form.cameras.title")} + + {t("roles.dialog.form.cameras.desc")} + +
+ {cameras.map((camera) => ( + { + return ( + +
+ + + +
+ + { + return checked + ? field.onChange([ + ...(field.value as string[]), + camera, + ]) + : field.onChange( + (field.value as string[])?.filter( + (value: string) => value !== camera, + ) || [], + ); + }} + /> + +
+ ); + }} + /> + ))} +
+ +
+ + +
+
+ + +
+
+
+
+ +
+
+ ); +}