mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-15 11:32:09 +03:00
split users and roles into separate tabs in settings
This commit is contained in:
parent
e888a08fd1
commit
56dc5773e1
@ -564,15 +564,15 @@
|
|||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"management": {
|
"management": {
|
||||||
"title": "Role Management",
|
"title": "Viewer Role Management",
|
||||||
"desc": "Manage roles and their camera access permissions for this Frigate instance."
|
"desc": "Manage custom viewer roles and their camera access permissions for this Frigate instance."
|
||||||
},
|
},
|
||||||
"addRole": "Add Role",
|
"addRole": "Add Role",
|
||||||
"table": {
|
"table": {
|
||||||
"role": "Role",
|
"role": "Role",
|
||||||
"cameras": "Cameras",
|
"cameras": "Cameras",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"noRoles": "No roles found.",
|
"noRoles": "No custom roles found.",
|
||||||
"editCameras": "Edit Cameras",
|
"editCameras": "Edit Cameras",
|
||||||
"deleteRole": "Delete Role"
|
"deleteRole": "Delete Role"
|
||||||
},
|
},
|
||||||
@ -581,7 +581,7 @@
|
|||||||
"createRole": "Role {{role}} created successfully",
|
"createRole": "Role {{role}} created successfully",
|
||||||
"updateCameras": "Cameras updated for role {{role}}",
|
"updateCameras": "Cameras updated for role {{role}}",
|
||||||
"deleteRole": "Role {{role}} deleted successfully",
|
"deleteRole": "Role {{role}} deleted successfully",
|
||||||
"userRolesUpdated": "{{count}} user(s) assigned to this role have been updated to 'viewer'."
|
"userRolesUpdated": "{{count}} user(s) assigned to this role have been updated to 'viewer', which has access to all cameras."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"createRoleFailed": "Failed to create role: {{errorMessage}}",
|
"createRoleFailed": "Failed to create role: {{errorMessage}}",
|
||||||
@ -601,7 +601,7 @@
|
|||||||
},
|
},
|
||||||
"deleteRole": {
|
"deleteRole": {
|
||||||
"title": "Delete Role",
|
"title": "Delete Role",
|
||||||
"desc": "This action cannot be undone. This will permanently delete the role and assign any users with this role to the 'viewer' role.",
|
"desc": "This action cannot be undone. This will permanently delete the role and assign any users with this role to the 'viewer' role, which will give viewer access to all cameras.",
|
||||||
"warn": "Are you sure you want to delete <strong>{{role}}</strong>?",
|
"warn": "Are you sure you want to delete <strong>{{role}}</strong>?",
|
||||||
"deleting": "Deleting..."
|
"deleting": "Deleting..."
|
||||||
},
|
},
|
||||||
|
|||||||
@ -33,7 +33,8 @@ import CameraSettingsView from "@/views/settings/CameraSettingsView";
|
|||||||
import ObjectSettingsView from "@/views/settings/ObjectSettingsView";
|
import ObjectSettingsView from "@/views/settings/ObjectSettingsView";
|
||||||
import MotionTunerView from "@/views/settings/MotionTunerView";
|
import MotionTunerView from "@/views/settings/MotionTunerView";
|
||||||
import MasksAndZonesView from "@/views/settings/MasksAndZonesView";
|
import MasksAndZonesView from "@/views/settings/MasksAndZonesView";
|
||||||
import AuthenticationView from "@/views/settings/AuthenticationView";
|
import UsersView from "@/views/settings/UsersView";
|
||||||
|
import RolesView from "@/views/settings/RolesView";
|
||||||
import NotificationView from "@/views/settings/NotificationsSettingsView";
|
import NotificationView from "@/views/settings/NotificationsSettingsView";
|
||||||
import EnrichmentsSettingsView from "@/views/settings/EnrichmentsSettingsView";
|
import EnrichmentsSettingsView from "@/views/settings/EnrichmentsSettingsView";
|
||||||
import UiSettingsView from "@/views/settings/UiSettingsView";
|
import UiSettingsView from "@/views/settings/UiSettingsView";
|
||||||
@ -57,6 +58,7 @@ const allSettingsViews = [
|
|||||||
"triggers",
|
"triggers",
|
||||||
"debug",
|
"debug",
|
||||||
"users",
|
"users",
|
||||||
|
"roles",
|
||||||
"notifications",
|
"notifications",
|
||||||
"frigateplus",
|
"frigateplus",
|
||||||
] as const;
|
] as const;
|
||||||
@ -288,7 +290,8 @@ export default function Settings() {
|
|||||||
setUnsavedChanges={setUnsavedChanges}
|
setUnsavedChanges={setUnsavedChanges}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{page == "users" && <AuthenticationView />}
|
{page == "users" && <UsersView />}
|
||||||
|
{page == "roles" && <RolesView />}
|
||||||
{page == "notifications" && (
|
{page == "notifications" && (
|
||||||
<NotificationView setUnsavedChanges={setUnsavedChanges} />
|
<NotificationView setUnsavedChanges={setUnsavedChanges} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -38,7 +38,13 @@ import DeleteRoleDialog from "@/components/overlay/DeleteRoleDialog";
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
|
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
|
||||||
|
|
||||||
export default function AuthenticationView() {
|
type AuthenticationViewProps = {
|
||||||
|
section?: "users" | "roles";
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AuthenticationView({
|
||||||
|
section,
|
||||||
|
}: AuthenticationViewProps) {
|
||||||
const { t } = useTranslation("views/settings");
|
const { t } = useTranslation("views/settings");
|
||||||
const { data: config, mutate: updateConfig } =
|
const { data: config, mutate: updateConfig } =
|
||||||
useSWR<FrigateConfig>("config");
|
useSWR<FrigateConfig>("config");
|
||||||
@ -377,10 +383,12 @@ export default function AuthenticationView() {
|
|||||||
|
|
||||||
const roles = useMemo(() => {
|
const roles = useMemo(() => {
|
||||||
return config?.auth?.roles
|
return config?.auth?.roles
|
||||||
? Object.entries(config.auth.roles).map(([name, data]) => ({
|
? Object.entries(config.auth.roles)
|
||||||
name,
|
.filter(([name]) => name !== "admin")
|
||||||
cameras: Array.isArray(data) ? data : [],
|
.map(([name, data]) => ({
|
||||||
}))
|
name,
|
||||||
|
cameras: Array.isArray(data) ? data : [],
|
||||||
|
}))
|
||||||
: [];
|
: [];
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
@ -396,319 +404,166 @@ export default function AuthenticationView() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
// Users section
|
||||||
<div className="flex size-full flex-col">
|
const UsersSection = (
|
||||||
<Toaster position="top-center" closeButton={true} />
|
<>
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
|
<div className="mb-5 flex flex-row items-center justify-between gap-2">
|
||||||
<div className="mb-5 flex flex-row items-center justify-between gap-2">
|
<div className="flex flex-col items-start">
|
||||||
<div className="flex flex-col items-start">
|
<Heading as="h3" className="my-2">
|
||||||
<Heading as="h3" className="my-2">
|
{t("users.management.title")}
|
||||||
{t("users.management.title")}
|
</Heading>
|
||||||
</Heading>
|
<p className="text-sm text-muted-foreground">
|
||||||
<p className="text-sm text-muted-foreground">
|
{t("users.management.desc")}
|
||||||
{t("users.management.desc")}
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
className="flex items-center gap-2 self-start sm:self-auto"
|
|
||||||
aria-label={t("users.addUser")}
|
|
||||||
variant="default"
|
|
||||||
onClick={() => setShowCreate(true)}
|
|
||||||
>
|
|
||||||
<LuPlus className="size-4" />
|
|
||||||
{t("users.addUser")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
<Button
|
||||||
<div className="scrollbar-container flex-1 overflow-hidden rounded-lg border border-border bg-background_alt">
|
className="flex items-center gap-2 self-start sm:self-auto"
|
||||||
<div className="h-full overflow-auto">
|
aria-label={t("users.addUser")}
|
||||||
<Table>
|
variant="default"
|
||||||
<TableHeader className="sticky top-0 bg-muted/50">
|
onClick={() => setShowCreate(true)}
|
||||||
|
>
|
||||||
|
<LuPlus className="size-4" />
|
||||||
|
{t("users.addUser")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="scrollbar-container flex-1 overflow-hidden rounded-lg border border-border bg-background_alt">
|
||||||
|
<div className="h-full overflow-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="sticky top-0 bg-muted/50">
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[250px]">
|
||||||
|
{t("users.table.username")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>{t("users.table.role")}</TableHead>
|
||||||
|
<TableHead className="text-right">
|
||||||
|
{t("users.table.actions")}
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{users.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[250px]">
|
<TableCell colSpan={3} className="h-24 text-center">
|
||||||
{t("users.table.username")}
|
{t("users.table.noUsers")}
|
||||||
</TableHead>
|
</TableCell>
|
||||||
<TableHead>{t("users.table.role")}</TableHead>
|
|
||||||
<TableHead className="text-right">
|
|
||||||
{t("users.table.actions")}
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
) : (
|
||||||
<TableBody>
|
users.map((user) => (
|
||||||
{users.length === 0 ? (
|
<TableRow key={user.username} className="group">
|
||||||
<TableRow>
|
<TableCell className="font-medium">
|
||||||
<TableCell colSpan={3} className="h-24 text-center">
|
<div className="flex items-center gap-2">
|
||||||
{t("users.table.noUsers")}
|
{user.username === "admin" ? (
|
||||||
|
<LuShield className="size-4 text-primary" />
|
||||||
|
) : (
|
||||||
|
<LuUserCog className="size-4 text-primary-variant" />
|
||||||
|
)}
|
||||||
|
{user.username}
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
<TableCell>
|
||||||
) : (
|
<Badge
|
||||||
users.map((user) => (
|
variant={
|
||||||
<TableRow key={user.username} className="group">
|
user.role === "admin" ? "default" : "outline"
|
||||||
<TableCell className="font-medium">
|
}
|
||||||
<div className="flex items-center gap-2">
|
className={
|
||||||
{user.username === "admin" ? (
|
user.role === "admin"
|
||||||
<LuShield className="size-4 text-primary" />
|
? "bg-primary/20 text-primary hover:bg-primary/30"
|
||||||
) : (
|
: ""
|
||||||
<LuUserCog className="size-4 text-primary-variant" />
|
}
|
||||||
)}
|
>
|
||||||
{user.username}
|
{t("role." + (user.role || "viewer"), {
|
||||||
</div>
|
ns: "common",
|
||||||
</TableCell>
|
})}
|
||||||
<TableCell>
|
</Badge>
|
||||||
<Badge
|
</TableCell>
|
||||||
variant={
|
<TableCell className="text-right">
|
||||||
user.role === "admin" ? "default" : "outline"
|
<TooltipProvider>
|
||||||
}
|
<div className="flex items-center justify-end gap-2">
|
||||||
className={
|
{user.username !== "admin" &&
|
||||||
user.role === "admin"
|
user.username !== "viewer" && (
|
||||||
? "bg-primary/20 text-primary hover:bg-primary/30"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("role." + (user.role || "viewer"), {
|
|
||||||
ns: "common",
|
|
||||||
})}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<TooltipProvider>
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
{user.username !== "admin" &&
|
|
||||||
user.username !== "viewer" && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="h-8 px-2"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedUser(user.username);
|
|
||||||
setSelectedUserRole(
|
|
||||||
user.role || "viewer",
|
|
||||||
);
|
|
||||||
setShowRoleChange(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LuUserCog className="size-3.5" />
|
|
||||||
<span className="ml-1.5 hidden sm:inline-block">
|
|
||||||
{t("role.title", { ns: "common" })}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{t("users.table.changeRole")}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="h-8 px-2"
|
|
||||||
onClick={() => {
|
|
||||||
setShowSetPassword(true);
|
|
||||||
setSelectedUser(user.username);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FaUserEdit className="size-3.5" />
|
|
||||||
<span className="ml-1.5 hidden sm:inline-block">
|
|
||||||
{t("users.table.password")}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{t("users.updatePassword")}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
{user.username !== "admin" && (
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="destructive"
|
variant="outline"
|
||||||
className="h-8 px-2"
|
className="h-8 px-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowDelete(true);
|
|
||||||
setSelectedUser(user.username);
|
setSelectedUser(user.username);
|
||||||
|
setSelectedUserRole(
|
||||||
|
user.role || "viewer",
|
||||||
|
);
|
||||||
|
setShowRoleChange(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HiTrash className="size-3.5" />
|
<LuUserCog className="size-3.5" />
|
||||||
<span className="ml-1.5 hidden sm:inline-block">
|
<span className="ml-1.5 hidden sm:inline-block">
|
||||||
{t("button.delete", { ns: "common" })}
|
{t("role.title", { ns: "common" })}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>{t("users.table.deleteUser")}</p>
|
<p>{t("users.table.changeRole")}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</TooltipProvider>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator className="my-6 flex bg-secondary" />
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="h-8 px-2"
|
||||||
|
onClick={() => {
|
||||||
|
setShowSetPassword(true);
|
||||||
|
setSelectedUser(user.username);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaUserEdit className="size-3.5" />
|
||||||
|
<span className="ml-1.5 hidden sm:inline-block">
|
||||||
|
{t("users.table.password")}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t("users.updatePassword")}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<div className="mb-5 flex flex-row items-center justify-between gap-2">
|
{user.username !== "admin" && (
|
||||||
<div className="flex flex-col items-start">
|
<Tooltip>
|
||||||
<Heading as="h3" className="my-2">
|
<TooltipTrigger asChild>
|
||||||
{t("roles.management.title")}
|
<Button
|
||||||
</Heading>
|
size="sm"
|
||||||
<p className="text-sm text-muted-foreground">
|
variant="destructive"
|
||||||
{t("roles.management.desc")}
|
className="h-8 px-2"
|
||||||
</p>
|
onClick={() => {
|
||||||
</div>
|
setShowDelete(true);
|
||||||
<Button
|
setSelectedUser(user.username);
|
||||||
className="flex items-center gap-2 self-start sm:self-auto"
|
}}
|
||||||
aria-label={t("roles.addRole")}
|
>
|
||||||
variant="default"
|
<HiTrash className="size-3.5" />
|
||||||
onClick={() => setShowCreateRole(true)}
|
<span className="ml-1.5 hidden sm:inline-block">
|
||||||
>
|
{t("button.delete", { ns: "common" })}
|
||||||
<LuPlus className="size-4" />
|
</span>
|
||||||
{t("roles.addRole")}
|
</Button>
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
</div>
|
<TooltipContent>
|
||||||
<div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
<p>{t("users.table.deleteUser")}</p>
|
||||||
<div className="scrollbar-container flex-1 overflow-hidden rounded-lg border border-border bg-background_alt">
|
</TooltipContent>
|
||||||
<div className="h-full overflow-auto">
|
</Tooltip>
|
||||||
<Table>
|
)}
|
||||||
<TableHeader className="sticky top-0 bg-muted/50">
|
</div>
|
||||||
<TableRow>
|
</TooltipProvider>
|
||||||
<TableHead className="w-[250px]">
|
|
||||||
{t("roles.table.role")}
|
|
||||||
</TableHead>
|
|
||||||
<TableHead>{t("roles.table.cameras")}</TableHead>
|
|
||||||
<TableHead className="text-right">
|
|
||||||
{t("roles.table.actions")}
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{roles.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={3} className="h-24 text-center">
|
|
||||||
{t("roles.table.noRoles")}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
))
|
||||||
roles.map((roleData) => (
|
)}
|
||||||
<TableRow key={roleData.name} className="group">
|
</TableBody>
|
||||||
<TableCell className="font-medium">
|
</Table>
|
||||||
{roleData.name}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{roleData.cameras.length === 0 ? (
|
|
||||||
<Badge
|
|
||||||
variant="default"
|
|
||||||
className="bg-primary/20 text-xs text-primary hover:bg-primary/30"
|
|
||||||
>
|
|
||||||
{t("menu.live.allCameras", { ns: "common" })}
|
|
||||||
</Badge>
|
|
||||||
) : roleData.cameras.length > 5 ? (
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
{roleData.cameras.length} cameras
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{roleData.cameras.map((camera) => (
|
|
||||||
<Badge
|
|
||||||
key={camera}
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
<CameraNameLabel
|
|
||||||
camera={camera}
|
|
||||||
className="text-xs smart-capitalize"
|
|
||||||
/>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<TooltipProvider>
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
{roleData.name !== "admin" &&
|
|
||||||
roleData.name !== "viewer" && (
|
|
||||||
<>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="h-8 px-2"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedRole(roleData.name);
|
|
||||||
setCurrentRoleCameras(
|
|
||||||
roleData.cameras,
|
|
||||||
);
|
|
||||||
setShowEditRole(true);
|
|
||||||
}}
|
|
||||||
disabled={roleData.name === "admin"}
|
|
||||||
>
|
|
||||||
<FaUserEdit className="size-3.5" />
|
|
||||||
<span className="ml-1.5 hidden sm:inline-block">
|
|
||||||
{t("roles.table.editCameras")}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{t("roles.table.editCameras")}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="destructive"
|
|
||||||
className="h-8 px-2"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedRoleForDelete(
|
|
||||||
roleData.name,
|
|
||||||
);
|
|
||||||
setShowDeleteRole(true);
|
|
||||||
}}
|
|
||||||
disabled={roleData.name === "admin"}
|
|
||||||
>
|
|
||||||
<HiTrash className="size-3.5" />
|
|
||||||
<span className="ml-1.5 hidden sm:inline-block">
|
|
||||||
{t("button.delete", {
|
|
||||||
ns: "common",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{t("roles.table.deleteRole")}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TooltipProvider>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SetPasswordDialog
|
<SetPasswordDialog
|
||||||
show={showSetPassword}
|
show={showSetPassword}
|
||||||
onCancel={() => setShowSetPassword(false)}
|
onCancel={() => setShowSetPassword(false)}
|
||||||
@ -735,6 +590,159 @@ export default function AuthenticationView() {
|
|||||||
onCancel={() => setShowRoleChange(false)}
|
onCancel={() => setShowRoleChange(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Roles section
|
||||||
|
const RolesSection = (
|
||||||
|
<>
|
||||||
|
<div className="mb-5 flex flex-row items-center justify-between gap-2">
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
|
<Heading as="h3" className="my-2">
|
||||||
|
{t("roles.management.title")}
|
||||||
|
</Heading>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("roles.management.desc")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="flex items-center gap-2 self-start sm:self-auto"
|
||||||
|
aria-label={t("roles.addRole")}
|
||||||
|
variant="default"
|
||||||
|
onClick={() => setShowCreateRole(true)}
|
||||||
|
>
|
||||||
|
<LuPlus className="size-4" />
|
||||||
|
{t("roles.addRole")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="scrollbar-container flex-1 overflow-hidden rounded-lg border border-border bg-background_alt">
|
||||||
|
<div className="h-full overflow-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="sticky top-0 bg-muted/50">
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[250px]">
|
||||||
|
{t("roles.table.role")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>{t("roles.table.cameras")}</TableHead>
|
||||||
|
<TableHead className="text-right">
|
||||||
|
{t("roles.table.actions")}
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{roles.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={3} className="h-24 text-center">
|
||||||
|
{t("roles.table.noRoles")}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
roles.map((roleData) => (
|
||||||
|
<TableRow key={roleData.name} className="group">
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{roleData.name}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{roleData.cameras.length === 0 ? (
|
||||||
|
<Badge
|
||||||
|
variant="default"
|
||||||
|
className="bg-primary/20 text-xs text-primary hover:bg-primary/30"
|
||||||
|
>
|
||||||
|
{t("menu.live.allCameras", { ns: "common" })}
|
||||||
|
</Badge>
|
||||||
|
) : roleData.cameras.length > 5 ? (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{roleData.cameras.length} cameras
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{roleData.cameras.map((camera) => (
|
||||||
|
<Badge
|
||||||
|
key={camera}
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<CameraNameLabel
|
||||||
|
camera={camera}
|
||||||
|
className="text-xs smart-capitalize"
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<TooltipProvider>
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
{roleData.name !== "admin" &&
|
||||||
|
roleData.name !== "viewer" && (
|
||||||
|
<>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="h-8 px-2"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedRole(roleData.name);
|
||||||
|
setCurrentRoleCameras(
|
||||||
|
roleData.cameras,
|
||||||
|
);
|
||||||
|
setShowEditRole(true);
|
||||||
|
}}
|
||||||
|
disabled={roleData.name === "admin"}
|
||||||
|
>
|
||||||
|
<FaUserEdit className="size-3.5" />
|
||||||
|
<span className="ml-1.5 hidden sm:inline-block">
|
||||||
|
{t("roles.table.editCameras")}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t("roles.table.editCameras")}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
className="h-8 px-2"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedRoleForDelete(
|
||||||
|
roleData.name,
|
||||||
|
);
|
||||||
|
setShowDeleteRole(true);
|
||||||
|
}}
|
||||||
|
disabled={roleData.name === "admin"}
|
||||||
|
>
|
||||||
|
<HiTrash className="size-3.5" />
|
||||||
|
<span className="ml-1.5 hidden sm:inline-block">
|
||||||
|
{t("button.delete", {
|
||||||
|
ns: "common",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t("roles.table.deleteRole")}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<CreateRoleDialog
|
<CreateRoleDialog
|
||||||
show={showCreateRole}
|
show={showCreateRole}
|
||||||
config={config}
|
config={config}
|
||||||
@ -772,6 +780,23 @@ export default function AuthenticationView() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full flex-col">
|
||||||
|
<Toaster position="top-center" closeButton={true} />
|
||||||
|
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
|
||||||
|
{section === "users" && UsersSection}
|
||||||
|
{section === "roles" && RolesSection}
|
||||||
|
{!section && (
|
||||||
|
<>
|
||||||
|
{UsersSection}
|
||||||
|
<Separator className="my-6 flex bg-secondary" />
|
||||||
|
{RolesSection}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
5
web/src/views/settings/RolesView.tsx
Normal file
5
web/src/views/settings/RolesView.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import AuthenticationView from "./AuthenticationView";
|
||||||
|
|
||||||
|
export default function RolesView() {
|
||||||
|
return <AuthenticationView section="roles" />;
|
||||||
|
}
|
||||||
5
web/src/views/settings/UsersView.tsx
Normal file
5
web/src/views/settings/UsersView.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import AuthenticationView from "./AuthenticationView";
|
||||||
|
|
||||||
|
export default function UsersView() {
|
||||||
|
return <AuthenticationView section="users" />;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user