import { useQuery } from "@tanstack/react-query"; import { Activity, BarChart3, ChevronLeft, ChevronRight, Clock, Github, Globe, Home, LogOut, Mail, Menu, MessageCircle, Package, Plus, RefreshCw, Server, Settings, Shield, Star, UserCircle, Users, Wrench, X, } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { Link, useLocation } from "react-router-dom"; import { useAuth } from "../contexts/AuthContext"; import { useUpdateNotification } from "../contexts/UpdateNotificationContext"; import { dashboardAPI, versionAPI } from "../utils/api"; import UpgradeNotificationIcon from "./UpgradeNotificationIcon"; const Layout = ({ children }) => { const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { // Load sidebar state from localStorage, default to false const saved = localStorage.getItem("sidebarCollapsed"); return saved ? JSON.parse(saved) : false; }); const [_userMenuOpen, setUserMenuOpen] = useState(false); const [githubStars, setGithubStars] = useState(null); const location = useLocation(); const { user, logout, canViewDashboard, canViewHosts, canManageHosts, canViewPackages, canViewUsers, canManageUsers, canViewReports, canExportData, canManageSettings, } = useAuth(); const { updateAvailable } = useUpdateNotification(); const userMenuRef = useRef(null); // Fetch dashboard stats for the "Last updated" info const { data: stats, refetch, isFetching, } = useQuery({ queryKey: ["dashboardStats"], queryFn: () => dashboardAPI.getStats().then((res) => res.data), staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes refetchOnWindowFocus: false, // Don't refetch when window regains focus }); // Fetch version info const { data: versionInfo } = useQuery({ queryKey: ["versionInfo"], queryFn: () => versionAPI.getCurrent().then((res) => res.data), staleTime: 300000, // Consider data stale after 5 minutes }); // Build navigation based on permissions const buildNavigation = () => { const nav = []; // Dashboard - only show if user can view dashboard if (canViewDashboard()) { nav.push({ name: "Dashboard", href: "/", icon: Home }); } // Inventory section - only show if user has any inventory permissions if (canViewHosts() || canViewPackages() || canViewReports()) { const inventoryItems = []; if (canViewHosts()) { inventoryItems.push({ name: "Hosts", href: "/hosts", icon: Server }); inventoryItems.push({ name: "Repos", href: "/repositories", icon: GitBranch, }); } if (canViewPackages()) { inventoryItems.push({ name: "Packages", href: "/packages", icon: Package, }); } if (canViewReports()) { inventoryItems.push( { name: "Services", href: "/services", icon: Activity, comingSoon: true, }, { name: "Docker", href: "/docker", icon: Container, comingSoon: true, }, { name: "Reporting", href: "/reporting", icon: BarChart3, comingSoon: true, }, ); } if (inventoryItems.length > 0) { nav.push({ section: "Inventory", items: inventoryItems, }); } } // PatchMon Users section - only show if user can view/manage users if (canViewUsers() || canManageUsers()) { const userItems = []; if (canViewUsers()) { userItems.push({ name: "Users", href: "/users", icon: Users }); } if (canManageSettings()) { userItems.push({ name: "Permissions", href: "/permissions", icon: Shield, }); } if (userItems.length > 0) { nav.push({ section: "PatchMon Users", items: userItems, }); } } // Settings section - only show if user has any settings permissions if (canManageSettings() || canViewReports() || canExportData()) { const settingsItems = []; if (canManageSettings()) { settingsItems.push({ name: "PatchMon Options", href: "/options", icon: Settings, }); settingsItems.push({ name: "Server Config", href: "/settings", icon: Wrench, showUpgradeIcon: updateAvailable, }); } if (canViewReports() || canExportData()) { settingsItems.push({ name: "Audit Log", href: "/audit-log", icon: FileText, comingSoon: true, }); } if (settingsItems.length > 0) { nav.push({ section: "Settings", items: settingsItems, }); } } return nav; }; const navigation = buildNavigation(); const isActive = (path) => location.pathname === path; // Get page title based on current route const getPageTitle = () => { const path = location.pathname; if (path === "/") return "Dashboard"; if (path === "/hosts") return "Hosts"; if (path === "/packages") return "Packages"; if (path === "/repositories" || path.startsWith("/repositories/")) return "Repositories"; if (path === "/services") return "Services"; if (path === "/docker") return "Docker"; if (path === "/users") return "Users"; if (path === "/permissions") return "Permissions"; if (path === "/settings") return "Settings"; if (path === "/options") return "PatchMon Options"; if (path === "/audit-log") return "Audit Log"; if (path === "/profile") return "My Profile"; if (path.startsWith("/hosts/")) return "Host Details"; if (path.startsWith("/packages/")) return "Package Details"; return "PatchMon"; }; const handleLogout = async () => { await logout(); setUserMenuOpen(false); }; const handleAddHost = () => { // Navigate to hosts page with add modal parameter window.location.href = "/hosts?action=add"; }; // Fetch GitHub stars count const fetchGitHubStars = useCallback(async () => { try { const response = await fetch( "https://api.github.com/repos/9technologygroup/patchmon.net", ); if (response.ok) { const data = await response.json(); setGithubStars(data.stargazers_count); } } catch (error) { console.error("Failed to fetch GitHub stars:", error); } }, []); // Short format for navigation area const formatRelativeTimeShort = (date) => { if (!date) return "Never"; const now = new Date(); const dateObj = new Date(date); // Check if date is valid if (Number.isNaN(dateObj.getTime())) return "Invalid date"; const diff = now - dateObj; const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) return `${days}d ago`; if (hours > 0) return `${hours}h ago`; if (minutes > 0) return `${minutes}m ago`; return `${seconds}s ago`; }; // Save sidebar collapsed state to localStorage useEffect(() => { localStorage.setItem("sidebarCollapsed", JSON.stringify(sidebarCollapsed)); }, [sidebarCollapsed]); // Close user menu when clicking outside useEffect(() => { const handleClickOutside = (event) => { if (userMenuRef.current && !userMenuRef.current.contains(event.target)) { setUserMenuOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []); // Fetch GitHub stars on component mount useEffect(() => { fetchGitHubStars(); }, [fetchGitHubStars]); return (