From 3f0ebb35778ae5943ab53ea0bfd85ea9b4f8b749 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 2 Jun 2026 15:11:42 -0600 Subject: [PATCH] Add ability to hide cameras from review UI (#23387) * Add field to control if cameras show in review * i18n * Add config to UI --- frigate/config/camera/ui.py | 5 ++ web/public/locales/en/config/cameras.json | 4 + web/public/locales/en/config/global.json | 4 + web/public/locales/en/views/settings.json | 8 +- .../components/filter/ReviewFilterGroup.tsx | 12 +-- web/src/pages/Events.tsx | 24 +++-- web/src/types/frigateConfig.ts | 1 + web/src/views/recording/RecordingView.tsx | 9 +- .../views/settings/CameraManagementView.tsx | 89 ++++++++++++++++++- 9 files changed, 136 insertions(+), 20 deletions(-) diff --git a/frigate/config/camera/ui.py b/frigate/config/camera/ui.py index 5e903b2543..7db1c537fa 100644 --- a/frigate/config/camera/ui.py +++ b/frigate/config/camera/ui.py @@ -16,3 +16,8 @@ class CameraUiConfig(FrigateBaseModel): title="Show in UI", description="Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again.", ) + review: bool = Field( + default=True, + title="Show in review", + description="Toggle whether this camera is visible in review (the review page and its camera filter, motion review, and the history view).", + ) diff --git a/web/public/locales/en/config/cameras.json b/web/public/locales/en/config/cameras.json index fda78c6abc..98e625abf7 100644 --- a/web/public/locales/en/config/cameras.json +++ b/web/public/locales/en/config/cameras.json @@ -862,6 +862,10 @@ "dashboard": { "label": "Show in UI", "description": "Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again." + }, + "review": { + "label": "Show in review", + "description": "Toggle whether this camera is visible in review (the review page and its camera filter, motion review, and the history view)." } }, "webui_url": { diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json index c901aba0b6..119d384e43 100644 --- a/web/public/locales/en/config/global.json +++ b/web/public/locales/en/config/global.json @@ -1546,6 +1546,10 @@ "dashboard": { "label": "Show in UI", "description": "Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again." + }, + "review": { + "label": "Show in review", + "description": "Toggle whether this camera is visible in review (the review page and its camera filter, motion review, and the history view)." } }, "onvif": { diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 6d52ae154b..0c46aec581 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -492,12 +492,16 @@ "details": { "edit": "Edit camera details", "title": "Edit Camera Details", - "description": "Update the display name and external URL used for this camera throughout the Frigate UI.", + "description": "Update the display name, external URL, and visibility used for this camera throughout the Frigate UI.", "friendlyNameLabel": "Display Name", "friendlyNameHelp": "Friendly name shown for this camera throughout the Frigate UI. Leave blank to use the camera ID.", "webuiUrlLabel": "Camera Web UI URL", "webuiUrlHelp": "URL to visit the camera's web UI directly from the Debug view. Leave blank to disable the link.", - "webuiUrlInvalid": "Must be a valid URL (e.g., https://example.com)." + "webuiUrlInvalid": "Must be a valid URL (e.g., https://example.com).", + "dashboardLabel": "Show on Live dashboard", + "dashboardHelp": "Show this camera on the Live dashboard.", + "reviewLabel": "Show in Review", + "reviewHelp": "Show this camera in Review, including the camera filter, motion review, and the history view." } }, "cameraConfig": { diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx index 76274ec3f2..3c75dde630 100644 --- a/web/src/components/filter/ReviewFilterGroup.tsx +++ b/web/src/components/filter/ReviewFilterGroup.tsx @@ -144,11 +144,13 @@ export default function ReviewFilterGroup({ const filterValues = useMemo( () => ({ - cameras: allowedCameras.sort( - (a, b) => - (config?.cameras[a]?.ui?.order ?? 0) - - (config?.cameras[b]?.ui?.order ?? 0), - ), + cameras: allowedCameras + .filter((cam) => config?.cameras[cam]?.ui?.review !== false) + .sort( + (a, b) => + (config?.cameras[a]?.ui?.order ?? 0) - + (config?.cameras[b]?.ui?.order ?? 0), + ), labels: Object.values(allLabels || {}), zones: Object.values(allZones || {}), }), diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index 393f895b0e..a1aa4c79ef 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -70,13 +70,15 @@ export default function Events() { undefined, ); - const motionSearchCameras = useMemo(() => { + const reviewCameras = useMemo(() => { if (!config?.cameras) { return [] as string[]; } - return Object.keys(config.cameras).filter((cam) => - allowedCameras.includes(cam), + return Object.keys(config.cameras).filter( + (cam) => + allowedCameras.includes(cam) && + config.cameras[cam]?.ui?.review !== false, ); }, [allowedCameras, config?.cameras]); @@ -85,12 +87,12 @@ export default function Events() { return null; } - if (motionSearchCameras.includes(motionSearchCamera)) { + if (reviewCameras.includes(motionSearchCamera)) { return motionSearchCamera; } - return motionSearchCameras[0] ?? null; - }, [motionSearchCamera, motionSearchCameras]); + return reviewCameras[0] ?? null; + }, [motionSearchCamera, reviewCameras]); const motionSearchTimeRange = useMemo(() => { if (motionSearchDay) { @@ -357,6 +359,10 @@ export default function Events() { const motion: ReviewSegment[] = []; reviews?.forEach((segment) => { + if (config?.cameras[segment.camera]?.ui?.review === false) { + return; + } + all.push(segment); switch (segment.severity) { @@ -378,7 +384,7 @@ export default function Events() { detection: detections, significant_motion: motion, }; - }, [reviews]); + }, [reviews, config?.cameras]); // update review items in place when a review segment ends const reviewUpdate = useFrigateReviews(); @@ -635,7 +641,7 @@ export default function Events() { } setStartTime(recording.startTime); - const allCameras = reviewFilter?.cameras ?? allowedCameras; + const allCameras = reviewFilter?.cameras ?? reviewCameras; return { camera: recording.camera, @@ -680,7 +686,7 @@ export default function Events() { ) : ( allCameras.filter((camera) => allowedCameras.includes(camera)), - [allCameras, allowedCameras], + () => + allCameras.filter( + (camera) => + allowedCameras.includes(camera) && + config?.cameras[camera]?.ui?.review !== false, + ), + [allCameras, allowedCameras, config?.cameras], ); const [mainCamera, setMainCamera] = useState(startCamera); diff --git a/web/src/views/settings/CameraManagementView.tsx b/web/src/views/settings/CameraManagementView.tsx index db8d5fc3b8..a51f3374ac 100644 --- a/web/src/views/settings/CameraManagementView.tsx +++ b/web/src/views/settings/CameraManagementView.tsx @@ -75,6 +75,7 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; +import { Switch } from "@/components/ui/switch"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; @@ -704,6 +705,8 @@ type CameraDetailsEditorProps = { type CameraDetailsFormValues = { friendlyName: string; webuiUrl: string; + dashboard: boolean; + review: boolean; }; function CameraDetailsEditor({ @@ -717,11 +720,15 @@ function CameraDetailsEditor({ const currentFriendlyName = config?.cameras?.[cameraName]?.friendly_name; const currentWebuiUrl = config?.cameras?.[cameraName]?.webui_url; + const currentDashboard = config?.cameras?.[cameraName]?.ui?.dashboard ?? true; + const currentReview = config?.cameras?.[cameraName]?.ui?.review ?? true; const formSchema = useMemo( () => z.object({ friendlyName: z.string(), + dashboard: z.boolean(), + review: z.boolean(), webuiUrl: z.string().refine( (val) => { const trimmed = val.trim(); @@ -748,6 +755,8 @@ function CameraDetailsEditor({ defaultValues: { friendlyName: currentFriendlyName ?? "", webuiUrl: currentWebuiUrl ?? "", + dashboard: currentDashboard, + review: currentReview, }, }); @@ -757,9 +766,18 @@ function CameraDetailsEditor({ form.reset({ friendlyName: currentFriendlyName ?? "", webuiUrl: currentWebuiUrl ?? "", + dashboard: currentDashboard, + review: currentReview, }); } - }, [open, currentFriendlyName, currentWebuiUrl, form]); + }, [ + open, + currentFriendlyName, + currentWebuiUrl, + currentDashboard, + currentReview, + form, + ]); const onSubmit = useCallback( async (values: CameraDetailsFormValues) => { @@ -768,7 +786,7 @@ function CameraDetailsEditor({ // only send fields the user actually changed const newFriendly = values.friendlyName.trim() || null; const newWebui = values.webuiUrl.trim() || null; - const cameraUpdate: Record = {}; + const cameraUpdate: Record = {}; if (newFriendly !== (currentFriendlyName ?? null)) { cameraUpdate.friendly_name = newFriendly; } @@ -776,6 +794,17 @@ function CameraDetailsEditor({ cameraUpdate.webui_url = newWebui; } + const uiUpdate: Record = {}; + if (values.dashboard !== currentDashboard) { + uiUpdate.dashboard = values.dashboard; + } + if (values.review !== currentReview) { + uiUpdate.review = values.review; + } + if (Object.keys(uiUpdate).length > 0) { + cameraUpdate.ui = uiUpdate; + } + if (Object.keys(cameraUpdate).length === 0) { setOpen(false); return; @@ -818,6 +847,8 @@ function CameraDetailsEditor({ cameraName, currentFriendlyName, currentWebuiUrl, + currentDashboard, + currentReview, isSaving, onConfigChanged, t, @@ -914,6 +945,60 @@ function CameraDetailsEditor({ )} /> + ( + +
+ + {t("cameraManagement.streams.details.dashboardLabel", { + ns: "views/settings", + })} + +

+ {t("cameraManagement.streams.details.dashboardHelp", { + ns: "views/settings", + })} +

+
+ + + +
+ )} + /> + ( + +
+ + {t("cameraManagement.streams.details.reviewLabel", { + ns: "views/settings", + })} + +

+ {t("cameraManagement.streams.details.reviewHelp", { + ns: "views/settings", + })} +

+
+ + + +
+ )} + />