camera group changes for custom viewer roles

- hide camera groups with no accessible cameras
- hide camera group edit button
This commit is contained in:
Josh Hawkins 2025-11-20 16:36:26 -06:00
parent 5d3f31175d
commit bb31dd18a4

View File

@ -87,6 +87,8 @@ type CameraGroupSelectorProps = {
export function CameraGroupSelector({ className }: CameraGroupSelectorProps) { export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
const { t } = useTranslation(["components/camera"]); const { t } = useTranslation(["components/camera"]);
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const allowedCameras = useAllowedCameras();
const isCustomRole = useIsCustomRole();
// tooltip // tooltip
@ -119,10 +121,22 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
return []; return [];
} }
return Object.entries(config.camera_groups).sort( const allGroups = Object.entries(config.camera_groups);
(a, b) => a[1].order - b[1].order,
// If custom role, filter out groups where user has no accessible cameras
if (isCustomRole) {
return allGroups
.filter(([, groupConfig]) => {
// Check if user has access to at least one camera in this group
return groupConfig.cameras.some((cameraName) =>
allowedCameras.includes(cameraName),
); );
}, [config]); })
.sort((a, b) => a[1].order - b[1].order);
}
return allGroups.sort((a, b) => a[1].order - b[1].order);
}, [config, allowedCameras, isCustomRole]);
// add group // add group
@ -139,6 +153,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
activeGroup={group} activeGroup={group}
setGroup={setGroup} setGroup={setGroup}
deleteGroup={deleteGroup} deleteGroup={deleteGroup}
isCustomRole={isCustomRole}
/> />
<Scroller className={`${isMobile ? "whitespace-nowrap" : ""}`}> <Scroller className={`${isMobile ? "whitespace-nowrap" : ""}`}>
<div <div
@ -206,6 +221,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
); );
})} })}
{!isCustomRole && (
<Button <Button
className="bg-secondary text-muted-foreground" className="bg-secondary text-muted-foreground"
aria-label={t("group.add")} aria-label={t("group.add")}
@ -214,6 +230,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
> >
<LuPlus className="size-4 text-primary" /> <LuPlus className="size-4 text-primary" />
</Button> </Button>
)}
{isMobile && <ScrollBar orientation="horizontal" className="h-0" />} {isMobile && <ScrollBar orientation="horizontal" className="h-0" />}
</div> </div>
</Scroller> </Scroller>
@ -228,6 +245,7 @@ type NewGroupDialogProps = {
activeGroup?: string; activeGroup?: string;
setGroup: (value: string | undefined, replace?: boolean | undefined) => void; setGroup: (value: string | undefined, replace?: boolean | undefined) => void;
deleteGroup: () => void; deleteGroup: () => void;
isCustomRole?: boolean;
}; };
function NewGroupDialog({ function NewGroupDialog({
open, open,
@ -236,6 +254,7 @@ function NewGroupDialog({
activeGroup, activeGroup,
setGroup, setGroup,
deleteGroup, deleteGroup,
isCustomRole,
}: NewGroupDialogProps) { }: NewGroupDialogProps) {
const { t } = useTranslation(["components/camera"]); const { t } = useTranslation(["components/camera"]);
const { mutate: updateConfig } = useSWR<FrigateConfig>("config"); const { mutate: updateConfig } = useSWR<FrigateConfig>("config");
@ -371,6 +390,7 @@ function NewGroupDialog({
> >
<Title>{t("group.label")}</Title> <Title>{t("group.label")}</Title>
<Description className="sr-only">{t("group.edit")}</Description> <Description className="sr-only">{t("group.edit")}</Description>
{!isCustomRole && (
<div <div
className={cn( className={cn(
"absolute", "absolute",
@ -393,6 +413,7 @@ function NewGroupDialog({
<LuPlus /> <LuPlus />
</Button> </Button>
</div> </div>
)}
</Header> </Header>
<div className="flex flex-col gap-4 md:gap-3"> <div className="flex flex-col gap-4 md:gap-3">
{currentGroups.map((group) => ( {currentGroups.map((group) => (
@ -401,6 +422,7 @@ function NewGroupDialog({
group={group} group={group}
onDeleteGroup={() => onDeleteGroup(group[0])} onDeleteGroup={() => onDeleteGroup(group[0])}
onEditGroup={() => onEditGroup(group)} onEditGroup={() => onEditGroup(group)}
isReadOnly={isCustomRole}
/> />
))} ))}
</div> </div>
@ -512,12 +534,14 @@ type CameraGroupRowProps = {
group: [string, CameraGroupConfig]; group: [string, CameraGroupConfig];
onDeleteGroup: () => void; onDeleteGroup: () => void;
onEditGroup: () => void; onEditGroup: () => void;
isReadOnly?: boolean;
}; };
export function CameraGroupRow({ export function CameraGroupRow({
group, group,
onDeleteGroup, onDeleteGroup,
onEditGroup, onEditGroup,
isReadOnly,
}: CameraGroupRowProps) { }: CameraGroupRowProps) {
const { t } = useTranslation(["components/camera"]); const { t } = useTranslation(["components/camera"]);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@ -564,7 +588,7 @@ export function CameraGroupRow({
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
{isMobile && ( {isMobile && !isReadOnly && (
<> <>
<DropdownMenu modal={!isDesktop}> <DropdownMenu modal={!isDesktop}>
<DropdownMenuTrigger> <DropdownMenuTrigger>
@ -589,7 +613,7 @@ export function CameraGroupRow({
</DropdownMenu> </DropdownMenu>
</> </>
)} )}
{!isMobile && ( {!isMobile && !isReadOnly && (
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>