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
}, [config]); 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),
);
})
.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,14 +221,16 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
); );
})} })}
<Button {!isCustomRole && (
className="bg-secondary text-muted-foreground" <Button
aria-label={t("group.add")} className="bg-secondary text-muted-foreground"
size="xs" aria-label={t("group.add")}
onClick={() => setAddGroup(true)} size="xs"
> onClick={() => setAddGroup(true)}
<LuPlus className="size-4 text-primary" /> >
</Button> <LuPlus className="size-4 text-primary" />
</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,28 +390,30 @@ 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>
<div {!isCustomRole && (
className={cn( <div
"absolute",
isDesktop && "right-6 top-10",
isMobile && "absolute right-0 top-4",
)}
>
<Button
size="sm"
className={cn( className={cn(
isDesktop && "absolute",
"size-6 rounded-md bg-secondary-foreground p-1 text-background", isDesktop && "right-6 top-10",
isMobile && "text-secondary-foreground", isMobile && "absolute right-0 top-4",
)} )}
aria-label={t("group.add")}
onClick={() => {
setEditState("add");
}}
> >
<LuPlus /> <Button
</Button> size="sm"
</div> className={cn(
isDesktop &&
"size-6 rounded-md bg-secondary-foreground p-1 text-background",
isMobile && "text-secondary-foreground",
)}
aria-label={t("group.add")}
onClick={() => {
setEditState("add");
}}
>
<LuPlus />
</Button>
</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>