import { useQuery } from "@tanstack/react-query"; import { Activity, BarChart3, BookOpen, ChevronLeft, ChevronRight, Clock, Container, GitBranch, Github, Globe, Home, LogOut, Mail, Menu, Package, Plus, RefreshCw, Route, Server, Settings, Star, UserCircle, X, } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { Link, useLocation, useNavigate } from "react-router-dom"; import { useAuth } from "../contexts/AuthContext"; import { useUpdateNotification } from "../contexts/UpdateNotificationContext"; import { dashboardAPI, versionAPI } from "../utils/api"; import DiscordIcon from "./DiscordIcon"; import GlobalSearch from "./GlobalSearch"; import Logo from "./Logo"; 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 navigate = useNavigate(); 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, }); } } return nav; }; // Build settings navigation separately (for bottom placement) const buildSettingsNavigation = () => { const settingsNav = []; // Settings section - consolidated all settings into one page if ( canManageSettings() || canViewUsers() || canManageUsers() || canViewReports() || canExportData() ) { const settingsItems = []; settingsItems.push({ name: "Settings", href: "/settings/users", icon: Settings, showUpgradeIcon: updateAvailable, }); settingsNav.push({ section: "Settings", items: settingsItems, }); } return settingsNav; }; const navigation = buildNavigation(); const settingsNavigation = buildSettingsNavigation(); 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 === "/settings/profile") return "My Profile"; if (path.startsWith("/hosts/")) return "Host Details"; if (path.startsWith("/packages/")) return "Package Details"; if (path.startsWith("/settings/")) return "Settings"; return "PatchMon"; }; const handleLogout = async () => { await logout(); setUserMenuOpen(false); }; const handleAddHost = () => { // Navigate to hosts page with add modal parameter navigate("/hosts?action=add"); }; // Fetch GitHub stars count const fetchGitHubStars = useCallback(async () => { // Skip if already fetched recently const lastFetch = localStorage.getItem("githubStarsFetchTime"); const now = Date.now(); if (lastFetch && now - parseInt(lastFetch, 15) < 600000) { // 15 minute cache return; } 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); localStorage.setItem("githubStarsFetchTime", now.toString()); } } 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 (