From 9c94640ffbdc69d3663a9d210a3421fbb20ff8e2 Mon Sep 17 00:00:00 2001
From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Date: Sun, 26 Apr 2026 17:00:47 -0500
Subject: [PATCH] allow changing camera friendly_name from camera management
pane
---
web/public/locales/en/views/settings.json | 8 +-
.../overlay/dialog/TextEntryDialog.tsx | 23 +++-
.../views/settings/CameraManagementView.tsx | 113 +++++++++++++++++-
3 files changed, 138 insertions(+), 6 deletions(-)
diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json
index 5a50e4daf..7dfd0ea98 100644
--- a/web/public/locales/en/views/settings.json
+++ b/web/public/locales/en/views/settings.json
@@ -457,7 +457,13 @@
"enableDesc": "Temporarily disable an enabled camera until Frigate restarts. Disabling a camera completely stops Frigate's processing of this camera's streams. Detection, recording, and debugging will be unavailable.
Note: This does not disable go2rtc restreams.",
"disableLabel": "Disabled cameras",
"disableDesc": "Enable a camera that is currently not visible in the UI and disabled in the configuration. A restart of Frigate is required after enabling.",
- "enableSuccess": "Enabled {{cameraName}} in configuration. Restart Frigate to apply the changes."
+ "enableSuccess": "Enabled {{cameraName}} in configuration. Restart Frigate to apply the changes.",
+ "friendlyName": {
+ "edit": "Edit camera display name",
+ "title": "Edit Display Name",
+ "description": "Set the friendly name shown for this camera throughout the Frigate UI. Leave blank to use the camera ID.",
+ "rename": "Rename"
+ }
},
"cameraConfig": {
"add": "Add Camera",
diff --git a/web/src/components/overlay/dialog/TextEntryDialog.tsx b/web/src/components/overlay/dialog/TextEntryDialog.tsx
index 4ee0876fc..38fcee657 100644
--- a/web/src/components/overlay/dialog/TextEntryDialog.tsx
+++ b/web/src/components/overlay/dialog/TextEntryDialog.tsx
@@ -1,3 +1,4 @@
+import ActivityIndicator from "@/components/indicators/activity-indicator";
import TextEntry from "@/components/input/TextEntry";
import { Button } from "@/components/ui/button";
import {
@@ -19,7 +20,9 @@ type TextEntryDialogProps = {
setOpen: (open: boolean) => void;
onSave: (text: string) => void;
defaultValue?: string;
+ placeholder?: string;
allowEmpty?: boolean;
+ isSaving?: boolean;
regexPattern?: RegExp;
regexErrorMessage?: string;
forbiddenPattern?: RegExp;
@@ -33,7 +36,9 @@ export default function TextEntryDialog({
setOpen,
onSave,
defaultValue = "",
+ placeholder,
allowEmpty = false,
+ isSaving = false,
regexPattern,
regexErrorMessage,
forbiddenPattern,
@@ -50,6 +55,7 @@ export default function TextEntryDialog({
-
diff --git a/web/src/views/settings/CameraManagementView.tsx b/web/src/views/settings/CameraManagementView.tsx
index 86e9b7a31..472aee923 100644
--- a/web/src/views/settings/CameraManagementView.tsx
+++ b/web/src/views/settings/CameraManagementView.tsx
@@ -14,7 +14,7 @@ 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 { LuPlus, LuTrash2 } from "react-icons/lu";
+import { LuPencil, LuPlus, LuTrash2 } from "react-icons/lu";
import { IoMdArrowRoundBack } from "react-icons/io";
import { isDesktop } from "react-device-detect";
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
@@ -26,6 +26,12 @@ import axios from "axios";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import RestartDialog from "@/components/overlay/dialog/RestartDialog";
import RestartRequiredIndicator from "@/components/indicators/RestartRequiredIndicator";
+import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
import type { ProfileState } from "@/types/profile";
import { getProfileColor } from "@/utils/profileColors";
import { cn } from "@/lib/utils";
@@ -161,7 +167,13 @@ export default function CameraManagementView({
key={camera}
className="flex flex-row items-center justify-between"
>
-
+
+
+
+
))}
@@ -297,6 +309,103 @@ function CameraEnableSwitch({ cameraName }: CameraEnableSwitchProps) {
);
}
+type CameraFriendlyNameEditorProps = {
+ cameraName: string;
+ onConfigChanged: () => Promise;
+};
+
+function CameraFriendlyNameEditor({
+ cameraName,
+ onConfigChanged,
+}: CameraFriendlyNameEditorProps) {
+ const { t } = useTranslation(["views/settings", "common"]);
+ const { data: config } = useSWR("config");
+ const [open, setOpen] = useState(false);
+ const [isSaving, setIsSaving] = useState(false);
+
+ const currentFriendlyName = config?.cameras?.[cameraName]?.friendly_name;
+
+ const onSave = useCallback(
+ async (text: string) => {
+ if (isSaving) return;
+ setIsSaving(true);
+
+ try {
+ await axios.put("config/set", {
+ requires_restart: 0,
+ config_data: {
+ cameras: {
+ [cameraName]: {
+ friendly_name: text.trim() || null,
+ },
+ },
+ },
+ });
+
+ await onConfigChanged();
+ setOpen(false);
+
+ toast.success(t("toast.save.success", { ns: "common" }), {
+ position: "top-center",
+ });
+ } catch (error) {
+ const errorMessage =
+ axios.isAxiosError(error) &&
+ (error.response?.data?.message || error.response?.data?.detail)
+ ? error.response?.data?.message || error.response?.data?.detail
+ : t("toast.save.error.noMessage", { ns: "common" });
+
+ toast.error(
+ t("toast.save.error.title", { errorMessage, ns: "common" }),
+ { position: "top-center" },
+ );
+ } finally {
+ setIsSaving(false);
+ }
+ },
+ [cameraName, isSaving, onConfigChanged, t],
+ );
+
+ const renameLabel = t("cameraManagement.streams.friendlyName.rename", {
+ ns: "views/settings",
+ });
+
+ return (
+ <>
+
+
+ setOpen(true)}
+ disabled={isSaving}
+ >
+
+
+
+ {renameLabel}
+
+
+ >
+ );
+}
+
type CameraConfigEnableSwitchProps = {
cameraName: string;
setRestartDialogOpen: React.Dispatch>;