Added Roles and Permissions. Some minor UI fixes
This commit is contained in:
@@ -3,46 +3,52 @@ import { NavLink, useLocation } from "react-router-dom";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
|
||||
const navItems = [
|
||||
{ to: "/", label: "Dashboard", roles: null },
|
||||
{ to: "/", label: "Dashboard", permission: null },
|
||||
{
|
||||
label: "Melodies",
|
||||
roles: ["superadmin", "melody_editor", "viewer"],
|
||||
permission: "melodies",
|
||||
children: [
|
||||
{ to: "/melodies", label: "Editor" },
|
||||
{ to: "/melodies/settings", label: "Settings" },
|
||||
],
|
||||
},
|
||||
{ to: "/devices", label: "Devices", roles: ["superadmin", "device_manager", "viewer"] },
|
||||
{ to: "/users", label: "Users", roles: ["superadmin", "user_manager", "viewer"] },
|
||||
{ to: "/devices", label: "Devices", permission: "devices" },
|
||||
{ to: "/users", label: "App Users", permission: "app_users" },
|
||||
{
|
||||
label: "MQTT",
|
||||
roles: ["superadmin", "device_manager", "viewer"],
|
||||
permission: "mqtt",
|
||||
children: [
|
||||
{ to: "/mqtt", label: "Dashboard" },
|
||||
{ to: "/mqtt/commands", label: "Commands" },
|
||||
{ to: "/mqtt/logs", label: "Logs" },
|
||||
],
|
||||
},
|
||||
{ to: "/equipment/notes", label: "Equipment Notes", roles: ["superadmin", "device_manager", "viewer"] },
|
||||
{ to: "/equipment/notes", label: "Equipment Notes", permission: "equipment" },
|
||||
];
|
||||
|
||||
const linkClass = (isActive) =>
|
||||
const linkClass = (isActive, locked) =>
|
||||
`block px-3 py-2 rounded-md text-sm transition-colors ${
|
||||
isActive
|
||||
? "bg-[var(--accent)] text-[var(--bg-primary)] font-medium"
|
||||
: "text-[var(--text-secondary)] hover:bg-[var(--bg-card-hover)] hover:text-[var(--text-heading)]"
|
||||
locked
|
||||
? "opacity-40 cursor-not-allowed"
|
||||
: isActive
|
||||
? "bg-[var(--accent)] text-[var(--bg-primary)] font-medium"
|
||||
: "text-[var(--text-secondary)] hover:bg-[var(--bg-card-hover)] hover:text-[var(--text-heading)]"
|
||||
}`;
|
||||
|
||||
export default function Sidebar() {
|
||||
const { hasRole } = useAuth();
|
||||
const { hasPermission, hasRole } = useAuth();
|
||||
const location = useLocation();
|
||||
|
||||
const visibleItems = navItems.filter(
|
||||
(item) => item.roles === null || hasRole(...item.roles)
|
||||
);
|
||||
const canViewSection = (permission) => {
|
||||
if (!permission) return true;
|
||||
return hasPermission(permission, "view");
|
||||
};
|
||||
|
||||
// Settings visible only to sysadmin and admin
|
||||
const canManageStaff = hasRole("sysadmin", "admin");
|
||||
|
||||
return (
|
||||
<aside className="w-56 min-h-screen p-4 border-r" style={{ backgroundColor: "var(--bg-sidebar)", borderColor: "var(--border-primary)" }}>
|
||||
<aside className="w-56 min-h-screen p-4 border-r flex flex-col" style={{ backgroundColor: "var(--bg-sidebar)", borderColor: "var(--border-primary)" }}>
|
||||
<div className="mb-8 px-2">
|
||||
<img
|
||||
src="/logo-dark.png"
|
||||
@@ -50,32 +56,57 @@ export default function Sidebar() {
|
||||
className="h-10 w-auto"
|
||||
/>
|
||||
</div>
|
||||
<nav className="space-y-1">
|
||||
{visibleItems.map((item) =>
|
||||
item.children ? (
|
||||
<nav className="space-y-1 flex-1">
|
||||
{navItems.map((item) => {
|
||||
const hasAccess = canViewSection(item.permission);
|
||||
return item.children ? (
|
||||
<CollapsibleGroup
|
||||
key={item.label}
|
||||
label={item.label}
|
||||
children={item.children}
|
||||
currentPath={location.pathname}
|
||||
locked={!hasAccess}
|
||||
/>
|
||||
) : (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
to={hasAccess ? item.to : "#"}
|
||||
end={item.to === "/"}
|
||||
className={({ isActive }) => linkClass(isActive)}
|
||||
className={({ isActive }) => linkClass(isActive && hasAccess, !hasAccess)}
|
||||
onClick={(e) => !hasAccess && e.preventDefault()}
|
||||
>
|
||||
{item.label}
|
||||
<span className="flex items-center gap-2">
|
||||
{item.label}
|
||||
{!hasAccess && (
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
</NavLink>
|
||||
)
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Settings section at the bottom */}
|
||||
{canManageStaff && (
|
||||
<div className="mt-4 pt-4 border-t" style={{ borderColor: "var(--border-primary)" }}>
|
||||
<nav className="space-y-1">
|
||||
<CollapsibleGroup
|
||||
label="Settings"
|
||||
children={[
|
||||
{ to: "/settings/staff", label: "Staff" },
|
||||
]}
|
||||
currentPath={location.pathname}
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsibleGroup({ label, children, currentPath }) {
|
||||
function CollapsibleGroup({ label, children, currentPath, locked = false }) {
|
||||
const isChildActive = children.some(
|
||||
(child) =>
|
||||
currentPath === child.to ||
|
||||
@@ -89,24 +120,35 @@ function CollapsibleGroup({ label, children, currentPath }) {
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(!shouldBeOpen)}
|
||||
onClick={() => !locked && setOpen(!shouldBeOpen)}
|
||||
className={`w-full flex items-center justify-between px-3 py-2 rounded-md text-sm transition-colors ${
|
||||
isChildActive
|
||||
? "text-[var(--text-heading)]"
|
||||
: "text-[var(--text-secondary)] hover:bg-[var(--bg-card-hover)] hover:text-[var(--text-heading)]"
|
||||
locked
|
||||
? "opacity-40 cursor-not-allowed"
|
||||
: isChildActive
|
||||
? "text-[var(--text-heading)]"
|
||||
: "text-[var(--text-secondary)] hover:bg-[var(--bg-card-hover)] hover:text-[var(--text-heading)]"
|
||||
}`}
|
||||
>
|
||||
<span>{label}</span>
|
||||
<svg
|
||||
className={`w-4 h-4 transition-transform ${shouldBeOpen ? "rotate-90" : ""}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<span className="flex items-center gap-2">
|
||||
{label}
|
||||
{locked && (
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
{!locked && (
|
||||
<svg
|
||||
className={`w-4 h-4 transition-transform ${shouldBeOpen ? "rotate-90" : ""}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
{shouldBeOpen && (
|
||||
{!locked && shouldBeOpen && (
|
||||
<div className="ml-3 mt-1 space-y-1">
|
||||
{children.map((child) => (
|
||||
<NavLink
|
||||
|
||||
Reference in New Issue
Block a user