- {renderMenuItemLabel(
- item.key as SettingsType,
+ (() => {
+ const hasActiveItem = filteredItems.some(
+ (item) => pageToggle === item.key,
+ );
+ const renderedExpanded = !collapsedGroups.has(
+ group.label,
+ );
+ return (
+
+ toggleGroupCollapsed(group.label)
+ }
+ >
+
+
+ {t("menu." + group.label)}
+
-
-
- ))}
-
- >
+ />
+
+
+
+
+ {filteredItems.map((item) => (
+
+ {
+ if (
+ !isAdmin &&
+ !ALLOWED_VIEWS_FOR_VIEWER.includes(
+ item.key as SettingsType,
+ )
+ ) {
+ setPageToggle("uiSettings");
+ } else {
+ setPageToggle(
+ item.key as SettingsType,
+ );
+ }
+ }}
+ >
+
+ {renderMenuItemLabel(
+ item.key as SettingsType,
+ )}
+
+
+
+ ))}
+
+
+
+ );
+ })()
)}
);
diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx
index ba9cdf0328..86ab910374 100644
--- a/web/src/views/events/EventView.tsx
+++ b/web/src/views/events/EventView.tsx
@@ -66,6 +66,7 @@ import SummaryTimeline from "@/components/timeline/SummaryTimeline";
import { RecordingStartingPoint } from "@/types/record";
import VideoControls from "@/components/player/VideoControls";
import { TimeRange } from "@/types/timeline";
+import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
import {
useCameraMotionNextTimestamp,
useCameraMotionOnlyRanges,
@@ -1008,27 +1009,29 @@ function MotionReview({
const { t } = useTranslation(["views/events", "common"]);
const segmentDuration = 30;
const { data: config } = useSWR
("config");
+ const allowedCameras = useAllowedCameras();
const reviewCameras = useMemo(() => {
if (!config) {
return [];
}
- let cameras;
- if (!filter || !filter.cameras) {
- cameras = Object.values(config.cameras).filter(
- (cam) => !isReplayCamera(cam.name),
- );
- } else {
- const filteredCams = filter.cameras;
-
- cameras = Object.values(config.cameras).filter(
- (cam) => filteredCams.includes(cam.name) && !isReplayCamera(cam.name),
- );
- }
+ const selectedCams = filter?.cameras;
+ const cameras = Object.values(config.cameras).filter((cam) => {
+ if (isReplayCamera(cam.name)) {
+ return false;
+ }
+ if (!allowedCameras.includes(cam.name)) {
+ return false;
+ }
+ if (selectedCams && !selectedCams.includes(cam.name)) {
+ return false;
+ }
+ return true;
+ });
return cameras.sort((a, b) => a.ui.order - b.ui.order);
- }, [config, filter]);
+ }, [config, filter, allowedCameras]);
const videoPlayersRef = useRef<{ [camera: string]: PreviewController }>({});
diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx
index 716ffed041..629bf30b87 100644
--- a/web/src/views/live/LiveDashboardView.tsx
+++ b/web/src/views/live/LiveDashboardView.tsx
@@ -13,6 +13,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
+import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
import { useUserPersistence } from "@/hooks/use-user-persistence";
import {
AllGroupsStreamingSettings,
@@ -90,6 +91,7 @@ export default function LiveDashboardView({
// recent events
const eventUpdate = useFrigateReviews();
+ const allowedCameras = useAllowedCameras();
const alertCameras = useMemo(() => {
if (!config) {
@@ -98,14 +100,16 @@ export default function LiveDashboardView({
if (cameraGroup == "default") {
return Object.values(config.cameras)
- .filter((cam) => cam.ui.dashboard)
+ .filter((cam) => cam.ui.dashboard && allowedCameras.includes(cam.name))
.map((cam) => cam.name)
.join(",");
}
if (includeBirdseye && cameras.length == 0) {
return Object.values(config.cameras)
- .filter((cam) => cam.birdseye.enabled)
+ .filter(
+ (cam) => cam.birdseye.enabled && allowedCameras.includes(cam.name),
+ )
.map((cam) => cam.name)
.join(",");
}
@@ -114,7 +118,7 @@ export default function LiveDashboardView({
.map((cam) => cam.name)
.filter((cam) => config.camera_groups[cameraGroup]?.cameras.includes(cam))
.join(",");
- }, [cameras, cameraGroup, config, includeBirdseye]);
+ }, [cameras, cameraGroup, config, includeBirdseye, allowedCameras]);
const { data: allEvents, mutate: updateEvents } = useSWR([
"review",
diff --git a/web/src/views/recording/RecordingView.tsx b/web/src/views/recording/RecordingView.tsx
index 7f382dee1a..f19a86a17a 100644
--- a/web/src/views/recording/RecordingView.tsx
+++ b/web/src/views/recording/RecordingView.tsx
@@ -44,6 +44,7 @@ import {
import { IoMdArrowRoundBack } from "react-icons/io";
import { useLocation, useNavigate } from "react-router-dom";
import { Toaster } from "@/components/ui/sonner";
+import { useIsAdmin } from "@/hooks/use-is-admin";
import useSWR from "swr";
import { TimeRange, TimelineType } from "@/types/timeline";
import MobileCameraDrawer from "@/components/overlay/MobileCameraDrawer";
@@ -109,6 +110,7 @@ export function RecordingView({
}: RecordingViewProps) {
const { t } = useTranslation(["views/events", "components/dialog"]);
const { data: config } = useSWR("config");
+ const isAdmin = useIsAdmin();
const navigate = useNavigate();
const location = useLocation();
const contentRef = useRef(null);
@@ -723,13 +725,17 @@ export function RecordingView({
setCustomShareTimestamp(initialTimestamp);
setShareTimestampOpen(true);
}}
- onDebugReplayClick={() => {
- setDebugReplayRange({
- after: timeRange.before - 60,
- before: timeRange.before,
- });
- setDebugReplayMode("select");
- }}
+ onDebugReplayClick={
+ isAdmin
+ ? () => {
+ setDebugReplayRange({
+ after: timeRange.before - 60,
+ before: timeRange.before,
+ });
+ setDebugReplayMode("select");
+ }
+ : undefined
+ }
onExportClick={() => {
const now = new Date(timeRange.before * 1000);
now.setHours(now.getHours() - 1);