From daadbfe913c59625592dc9be4f70ecb5a7128156 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 24 May 2026 15:07:46 -0500 Subject: [PATCH] remove obsolete camera edit form --- .../components/settings/CameraEditForm.tsx | 755 ------------------ .../views/settings/CameraManagementView.tsx | 278 +++---- 2 files changed, 113 insertions(+), 920 deletions(-) delete mode 100644 web/src/components/settings/CameraEditForm.tsx diff --git a/web/src/components/settings/CameraEditForm.tsx b/web/src/components/settings/CameraEditForm.tsx deleted file mode 100644 index efae88aac5..0000000000 --- a/web/src/components/settings/CameraEditForm.tsx +++ /dev/null @@ -1,755 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Switch } from "@/components/ui/switch"; -import { Card, CardContent } from "@/components/ui/card"; -import Heading from "@/components/ui/heading"; -import { Separator } from "@/components/ui/separator"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm, useFieldArray } from "react-hook-form"; -import { z } from "zod"; -import axios from "axios"; -import { toast } from "sonner"; -import { useTranslation } from "react-i18next"; -import { useState, useMemo, useEffect } from "react"; -import { LuTrash2, LuPlus } from "react-icons/lu"; -import ActivityIndicator from "@/components/indicators/activity-indicator"; -import { FrigateConfig } from "@/types/frigateConfig"; -import useSWR from "swr"; -import { processCameraName } from "@/utils/cameraUtil"; -import { Label } from "@/components/ui/label"; -import { ConfigSetBody } from "@/types/cameraWizard"; -import { Toaster } from "../ui/sonner"; - -const RoleEnum = z.enum(["audio", "detect", "record"]); -type Role = z.infer; - -type CameraEditFormProps = { - cameraName?: string; - onSave?: () => void; - onCancel?: () => void; -}; - -export default function CameraEditForm({ - cameraName, - onSave, - onCancel, -}: CameraEditFormProps) { - const { t } = useTranslation(["views/settings"]); - const { data: config, mutate: mutateConfig } = - useSWR("config"); - const { data: rawPaths, mutate: mutateRawPaths } = useSWR<{ - cameras: Record< - string, - { ffmpeg: { inputs: { path: string; roles: string[] }[] } } - >; - go2rtc: { streams: Record }; - }>(cameraName ? "config/raw_paths" : null); - const [isLoading, setIsLoading] = useState(false); - - const formSchema = useMemo( - () => - z.object({ - cameraName: z - .string() - .min(1, { message: t("cameraManagement.cameraConfig.nameRequired") }), - enabled: z.boolean(), - ffmpeg: z.object({ - inputs: z - .array( - z.object({ - path: z.string().min(1, { - message: t( - "cameraManagement.cameraConfig.ffmpeg.pathRequired", - ), - }), - roles: z.array(RoleEnum).min(1, { - message: t( - "cameraManagement.cameraConfig.ffmpeg.rolesRequired", - ), - }), - }), - ) - .min(1, { - message: t("cameraManagement.cameraConfig.ffmpeg.inputsRequired"), - }) - .refine( - (inputs) => { - const roleOccurrences = new Map(); - inputs.forEach((input) => { - input.roles.forEach((role) => { - roleOccurrences.set( - role, - (roleOccurrences.get(role) || 0) + 1, - ); - }); - }); - return Array.from(roleOccurrences.values()).every( - (count) => count <= 1, - ); - }, - { - message: t("cameraManagement.cameraConfig.ffmpeg.rolesUnique"), - path: ["inputs"], - }, - ), - }), - go2rtcStreams: z.record(z.string(), z.array(z.string())).optional(), - }), - [t], - ); - - type FormValues = z.infer; - - const cameraInfo = useMemo(() => { - if (!cameraName || !config?.cameras[cameraName]) { - return { - friendly_name: undefined, - name: cameraName || "", - roles: new Set(), - go2rtcStreams: {}, - }; - } - - const camera = config.cameras[cameraName]; - const roles = new Set(); - - camera.ffmpeg?.inputs?.forEach((input) => { - input.roles.forEach((role) => roles.add(role as Role)); - }); - - // Load existing go2rtc streams - const go2rtcStreams = config.go2rtc?.streams || {}; - - return { - friendly_name: camera?.friendly_name || cameraName, - name: cameraName, - roles, - go2rtcStreams, - }; - }, [cameraName, config]); - - const defaultValues: FormValues = { - cameraName: cameraInfo?.friendly_name || cameraName || "", - enabled: true, - ffmpeg: { - inputs: [ - { - path: "", - roles: cameraInfo.roles.has("detect") ? [] : ["detect"], - }, - ], - }, - go2rtcStreams: {}, - }; - - // Load existing camera config if editing - if (cameraName && config?.cameras[cameraName]) { - const camera = config.cameras[cameraName]; - defaultValues.enabled = camera.enabled ?? true; - - // Use raw paths from the admin endpoint if available, otherwise fall back to masked paths - const rawCameraData = rawPaths?.cameras?.[cameraName]; - defaultValues.ffmpeg.inputs = rawCameraData?.ffmpeg?.inputs?.length - ? rawCameraData.ffmpeg.inputs.map((input) => ({ - path: input.path, - roles: input.roles as Role[], - })) - : camera.ffmpeg?.inputs?.length - ? camera.ffmpeg.inputs.map((input) => ({ - path: input.path, - roles: input.roles as Role[], - })) - : defaultValues.ffmpeg.inputs; - - const go2rtcStreams = - rawPaths?.go2rtc?.streams || config.go2rtc?.streams || {}; - const cameraStreams: Record = {}; - - // get candidate stream names for this camera. could be the camera's own name, - // any restream names referenced by this camera, or any keys under live --> streams - const validNames = new Set(); - validNames.add(cameraName); - - // deduce go2rtc stream names from rtsp restream inputs - camera.ffmpeg?.inputs?.forEach((input) => { - // exclude any query strings or trailing slashes from the stream name - const restreamMatch = input.path.match( - /^rtsp:\/\/127\.0\.0\.1:8554\/([^?#/]+)(?:[?#].*)?$/, - ); - if (restreamMatch) { - const streamName = restreamMatch[1]; - validNames.add(streamName); - } - }); - - // Include live --> streams keys - const liveStreams = camera?.live?.streams; - if (liveStreams) { - Object.keys(liveStreams).forEach((key) => { - validNames.add(key); - }); - } - - // Map only go2rtc entries that match the collected names - Object.entries(go2rtcStreams).forEach(([name, urls]) => { - if (validNames.has(name)) { - cameraStreams[name] = Array.isArray(urls) ? urls : [urls]; - } - }); - - defaultValues.go2rtcStreams = cameraStreams; - } - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues, - mode: "onChange", - }); - - // Update form values when rawPaths loads - useEffect(() => { - if ( - cameraName && - config?.cameras[cameraName] && - rawPaths?.cameras?.[cameraName] - ) { - const camera = config.cameras[cameraName]; - const rawCameraData = rawPaths.cameras[cameraName]; - - // Update ffmpeg inputs with raw paths - if (rawCameraData.ffmpeg?.inputs?.length) { - form.setValue( - "ffmpeg.inputs", - rawCameraData.ffmpeg.inputs.map((input) => ({ - path: input.path, - roles: input.roles as Role[], - })), - ); - } - - // Update go2rtc streams with raw URLs - if (rawPaths.go2rtc?.streams) { - const validNames = new Set(); - validNames.add(cameraName); - - camera.ffmpeg?.inputs?.forEach((input) => { - const restreamMatch = input.path.match( - /^rtsp:\/\/127\.0\.0\.1:8554\/([^?#/]+)(?:[?#].*)?$/, - ); - if (restreamMatch) { - validNames.add(restreamMatch[1]); - } - }); - - const liveStreams = camera?.live?.streams; - if (liveStreams) { - Object.keys(liveStreams).forEach((key) => validNames.add(key)); - } - - const cameraStreams: Record = {}; - Object.entries(rawPaths.go2rtc.streams).forEach(([name, urls]) => { - if (validNames.has(name)) { - cameraStreams[name] = Array.isArray(urls) ? urls : [urls]; - } - }); - - if (Object.keys(cameraStreams).length > 0) { - form.setValue("go2rtcStreams", cameraStreams); - } - } - } - }, [cameraName, config, rawPaths, form]); - - const { fields, append, remove } = useFieldArray({ - control: form.control, - name: "ffmpeg.inputs", - }); - - // Watch ffmpeg.inputs to track used roles - const watchedInputs = form.watch("ffmpeg.inputs"); - - // Watch go2rtc streams - const watchedGo2rtcStreams = form.watch("go2rtcStreams") || {}; - - const saveCameraConfig = (values: FormValues) => { - setIsLoading(true); - const { finalCameraName, friendlyName } = processCameraName( - values.cameraName, - ); - - const configData: ConfigSetBody["config_data"] = { - cameras: { - [finalCameraName]: { - enabled: values.enabled, - ...(friendlyName && { friendly_name: friendlyName }), - ffmpeg: { - inputs: values.ffmpeg.inputs.map((input) => ({ - path: input.path, - roles: input.roles, - })), - }, - }, - }, - }; - - // Add go2rtc streams if provided - if (values.go2rtcStreams && Object.keys(values.go2rtcStreams).length > 0) { - configData.go2rtc = { - streams: values.go2rtcStreams, - }; - } - - const requestBody: ConfigSetBody = { - requires_restart: 1, - config_data: configData, - }; - - // Add update_topic for new cameras - if (!cameraName) { - requestBody.update_topic = `config/cameras/${finalCameraName}/add`; - } - - axios - .put("config/set", requestBody) - .then((res) => { - if (res.status === 200) { - // Update running go2rtc instance if streams were configured - if ( - values.go2rtcStreams && - Object.keys(values.go2rtcStreams).length > 0 - ) { - const updatePromises = Object.entries(values.go2rtcStreams).map( - ([streamName, urls]) => - axios.put( - `go2rtc/streams/${streamName}?src=${encodeURIComponent(urls[0])}`, - ), - ); - - Promise.allSettled(updatePromises).then(() => { - toast.success( - t("cameraManagement.cameraConfig.toast.success", { - cameraName: values.cameraName, - }), - { position: "top-center" }, - ); - mutateConfig(); - mutateRawPaths(); - if (onSave) onSave(); - }); - } else { - toast.success( - t("cameraManagement.cameraConfig.toast.success", { - cameraName: values.cameraName, - }), - { position: "top-center" }, - ); - mutateConfig(); - mutateRawPaths(); - if (onSave) onSave(); - } - } else { - throw new Error(res.statusText); - } - }) - .catch((error) => { - const errorMessage = - error.response?.data?.message || - error.response?.data?.detail || - "Unknown error"; - toast.error( - t("toast.save.error.title", { errorMessage, ns: "common" }), - { position: "top-center" }, - ); - }) - .finally(() => { - setIsLoading(false); - }); - }; - - const onSubmit = (values: FormValues) => { - if ( - cameraName && - values.cameraName !== cameraName && - values.cameraName !== cameraInfo?.friendly_name - ) { - // If camera name changed, delete old camera config - const deleteRequestBody = { - requires_restart: 1, - config_data: { - cameras: { - [cameraName]: null, - }, - }, - update_topic: `config/cameras/${cameraName}/remove`, - }; - - axios - .put("config/set", deleteRequestBody) - .then(() => saveCameraConfig(values)) - .catch((error) => { - const errorMessage = - error.response?.data?.message || - error.response?.data?.detail || - "Unknown error"; - toast.error( - t("toast.save.error.title", { errorMessage, ns: "common" }), - { position: "top-center" }, - ); - }) - .finally(() => { - setIsLoading(false); - }); - } else { - saveCameraConfig(values); - } - }; - - // Determine available roles for new streams - const getAvailableRoles = (): Role[] => { - const used = new Set(); - watchedInputs.forEach((input) => { - input.roles.forEach((role) => used.add(role)); - }); - return used.has("detect") ? [] : ["detect"]; - }; - - const getUsedRolesExcludingIndex = (excludeIndex: number) => { - const roles = new Set(); - watchedInputs.forEach((input, idx) => { - if (idx !== excludeIndex) { - input.roles.forEach((role) => roles.add(role)); - } - }); - return roles; - }; - - return ( -
- - - {cameraName - ? t("cameraManagement.cameraConfig.edit") - : t("cameraManagement.cameraConfig.add")} - -
- {t("cameraManagement.cameraConfig.description")} -
- - -
- - ( - - {t("cameraManagement.cameraConfig.name")} - - - - - - )} - /> - - ( - - - - - - {t("cameraManagement.cameraConfig.enabled")} - - - - )} - /> - -
- - {fields.map((field, index) => ( - - -
-

- {t("cameraWizard.step3.streamTitle", { - number: index + 1, - })} -

- -
- - ( - - - {t("cameraManagement.cameraConfig.ffmpeg.path")} - - - - - - - )} - /> - -
- -
-
- {(["detect", "record", "audio"] as const).map( - (role) => { - const isUsedElsewhere = - getUsedRolesExcludingIndex(index).has(role); - const isChecked = - watchedInputs[index]?.roles?.includes(role) || - false; - return ( -
- - {role} - - { - const currentRoles = - watchedInputs[index]?.roles || []; - const updatedRoles = checked - ? [...currentRoles, role] - : currentRoles.filter((r) => r !== role); - form.setValue( - `ffmpeg.inputs.${index}.roles`, - updatedRoles, - ); - }} - disabled={!isChecked && isUsedElsewhere} - /> -
- ); - }, - )} -
-
-
-
-
- ))} - - {form.formState.errors.ffmpeg?.inputs?.root && - form.formState.errors.ffmpeg.inputs.root.message} - - -
- - {/* go2rtc Streams Section */} - {Object.keys(watchedGo2rtcStreams).length > 0 && ( -
- - {Object.entries(watchedGo2rtcStreams).map( - ([streamName, urls]) => ( - - -
-

{streamName}

- -
- -
- - {(Array.isArray(urls) ? urls : [urls]).map( - (url, urlIndex) => ( -
- { - const updatedStreams = { - ...watchedGo2rtcStreams, - }; - const currentUrls = Array.isArray( - updatedStreams[streamName], - ) - ? updatedStreams[streamName] - : [updatedStreams[streamName]]; - currentUrls[urlIndex] = e.target.value; - updatedStreams[streamName] = currentUrls; - form.setValue( - "go2rtcStreams", - updatedStreams, - ); - }} - placeholder="rtsp://username:password@host:port/path" - /> - {(Array.isArray(urls) ? urls : [urls]).length > - 1 && ( - - )} -
- ), - )} - -
-
-
- ), - )} - -
- )} - -
- - -
- - -
- ); -} diff --git a/web/src/views/settings/CameraManagementView.tsx b/web/src/views/settings/CameraManagementView.tsx index def5902c6e..f18b8f94b4 100644 --- a/web/src/views/settings/CameraManagementView.tsx +++ b/web/src/views/settings/CameraManagementView.tsx @@ -11,7 +11,6 @@ import { Button } from "@/components/ui/button"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { useTranslation } from "react-i18next"; -import CameraEditForm from "@/components/settings/CameraEditForm"; import CameraWizardDialog from "@/components/settings/CameraWizardDialog"; import DeleteCameraDialog from "@/components/overlay/dialog/DeleteCameraDialog"; import { @@ -24,10 +23,8 @@ import { LuTrash2, } from "react-icons/lu"; import { Reorder, useDragControls } from "framer-motion"; -import { IoMdArrowRoundBack } from "react-icons/io"; import { Link } from "react-router-dom"; import { useDocDomain } from "@/hooks/use-doc-domain"; -import { isDesktop } from "react-device-detect"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import { Trans } from "react-i18next"; import { useEnabledState, useRestart } from "@/api/ws"; @@ -78,12 +75,10 @@ const REORDER_SAVED_INDICATOR_MS = 1500; type ReorderSaveStatus = "idle" | "saving" | "saved"; type CameraManagementViewProps = { - setUnsavedChanges: React.Dispatch>; profileState?: ProfileState; }; export default function CameraManagementView({ - setUnsavedChanges, profileState, }: CameraManagementViewProps) { const { t } = useTranslation(["views/settings", "common"]); @@ -91,12 +86,6 @@ export default function CameraManagementView({ const { data: config, mutate: updateConfig } = useSWR("config"); - const [viewMode, setViewMode] = useState<"settings" | "add" | "edit">( - "settings", - ); // Control view state - const [editCameraName, setEditCameraName] = useState( - undefined, - ); // Track camera being edited const [showWizard, setShowWizard] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); @@ -226,14 +215,6 @@ export default function CameraManagementView({ document.title = t("documentTitle.cameraManagement"); }, [t]); - // Handle back navigation from add/edit form - const handleBack = useCallback(() => { - setViewMode("settings"); - setEditCameraName(undefined); - setUnsavedChanges(false); - updateConfig(); - }, [updateConfig, setUnsavedChanges]); - return ( <>
- {viewMode === "settings" ? ( - <> - - {t("cameraManagement.title")} - -

- {t("cameraManagement.description")} -

+ + {t("cameraManagement.title")} + +

+ {t("cameraManagement.description")} +

-
-
- - {enabledCameras.length + disabledCameras.length > 0 && ( - - )} -
- - {(enabledCameras.length > 0 || disabledCameras.length > 0) && ( - - cameraManagement.streams.title - - } - > -
-
- -

- - cameraManagement.streams.description - -

-
-
-
- {orderedCameras.length > 0 && ( - - {orderedCameras.map((camera) => ( - - ))} - - )} - {orderedCameras.length > 0 && - disabledCameras.length > 0 && ( -
- )} - {disabledCameras.length > 0 && ( -
-

- {t( - "cameraManagement.streams.disabledSubheading", - )} -

- {disabledCameras.map((camera) => ( - - ))} -
- )} -
- -
-

- - cameraManagement.streams.description - -

-
- - )} - - {profileState && - profileState.allProfileNames.length > 0 && - enabledCameras.length > 0 && ( - - )} - - {config?.lpr?.enabled && allCameras.length > 0 && ( - - )} -
- - ) : ( - <> -
+
+
+ + {enabledCameras.length + disabledCameras.length > 0 && ( -
-
- + + {(enabledCameras.length > 0 || disabledCameras.length > 0) && ( + + cameraManagement.streams.title + + } + > +
+
+ +

+ + cameraManagement.streams.description + +

+
+
+
+ {orderedCameras.length > 0 && ( + + {orderedCameras.map((camera) => ( + + ))} + + )} + {orderedCameras.length > 0 && + disabledCameras.length > 0 && ( +
+ )} + {disabledCameras.length > 0 && ( +
+

+ {t("cameraManagement.streams.disabledSubheading")} +

+ {disabledCameras.map((camera) => ( + + ))} +
+ )} +
+ +
+

+ + cameraManagement.streams.description + +

+
+ + )} + + {profileState && + profileState.allProfileNames.length > 0 && + enabledCameras.length > 0 && ( + -
- - )} + )} + + {config?.lpr?.enabled && allCameras.length > 0 && ( + + )} +