Fix responsive breakpoints and flex layout for sidebar menu

This commit is contained in:
Tola Leng
2026-01-13 01:28:57 +07:00
parent 6ab3be6fe4
commit 4f367b92da
8 changed files with 241 additions and 334 deletions
+94 -104
View File
@@ -1,71 +1,59 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { AuthUser } from "@/services/authService"; import { AuthUser } from "@/services/authService";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText, Github, Twitter, MessageSquare, Bell, User, Settings, LogOut } from "lucide-react"; import {
Moon, PanelLeft, PanelLeftClose, Sun, Globe, FileText,
Github, Twitter, MessageSquare, Bell, User, Settings,
LogOut, Menu, X
} from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext"; import { useLanguage } from "@/contexts/LanguageContext";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu"; import {
DropdownMenu, DropdownMenuContent, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuSeparator
} from "@/components/ui/dropdown-menu";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useSystemSettings } from "@/hooks/useSystemSettings"; import { useSystemSettings } from "@/hooks/useSystemSettings";
import { useSidebar } from "@/contexts/SidebarContext";
import { cn } from "@/lib/utils";
interface HeaderProps { interface HeaderProps {
currentUser: AuthUser | null; currentUser: AuthUser | null;
onLogout: () => void; onLogout: () => void;
sidebarCollapsed: boolean;
toggleSidebar: () => void;
} }
export const Header = ({ export const Header = ({
currentUser, currentUser,
onLogout, onLogout,
sidebarCollapsed,
toggleSidebar
}: HeaderProps) => { }: HeaderProps) => {
const { theme, toggleTheme } = useTheme(); const { theme, toggleTheme } = useTheme();
const { language, setLanguage, t } = useLanguage(); const { language, setLanguage, t } = useLanguage();
const { sidebarCollapsed, toggleSidebar, isMobileOpen, toggleMobileMenu } = useSidebar();
const [greeting, setGreeting] = useState<string>(""); const [greeting, setGreeting] = useState<string>("");
const { systemName } = useSystemSettings(); const { systemName } = useSystemSettings();
const navigate = useNavigate(); const navigate = useNavigate();
// Set greeting based on time of day
useEffect(() => { useEffect(() => {
const updateGreeting = () => { const updateGreeting = () => {
const hour = new Date().getHours(); const hour = new Date().getHours();
if (hour >= 5 && hour < 12) { if (hour >= 5 && hour < 12) setGreeting(t("goodMorning"));
setGreeting(t("goodMorning")); else if (hour >= 12 && hour < 18) setGreeting(t("goodAfternoon"));
} else if (hour >= 12 && hour < 18) { else setGreeting(t("goodEvening"));
setGreeting(t("goodAfternoon"));
} else {
setGreeting(t("goodEvening"));
}
}; };
updateGreeting(); updateGreeting();
}, [language, t]); }, [language, t]);
// Log avatar data for debugging const avatarUrl = currentUser?.avatar || '';
useEffect(() => {
if (currentUser) {
} const openExternalLink = (url: string) => {
}, [currentUser]); window.open(url, '_blank');
};
let avatarUrl = '';
if (currentUser?.avatar) {
if (currentUser.avatar.startsWith('/upload/profile/')) {
avatarUrl = currentUser.avatar;
} else {
avatarUrl = currentUser.avatar;
}
console.log("Final avatar URL:", avatarUrl);
}
return ( return (
<header className="relative bg-background border-b border-border px-6 flex justify-between items-center py-[12px] overflow-hidden"> <header className="relative bg-background border-b border-border px-4 lg:px-6 flex justify-between items-center h-16 shrink-0 z-30 overflow-hidden">
{/* Grid Pattern Overlay - Similar to StatusCards */} {/* Grid Pattern Overlay */}
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0 pointer-events-none opacity-20">
<div <div
className="w-full h-full" className="w-full h-full"
style={{ style={{
@@ -78,19 +66,74 @@ export const Header = ({
</div> </div>
</div> </div>
{/* Header Content */} <div className="flex items-center gap-2 lg:gap-4 z-10">
<div className="flex items-center gap-4 z-10"> {/* Mobile Toggle */}
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="mr-2"> <Button variant="ghost" size="icon" onClick={toggleMobileMenu} className="lg:hidden h-9 w-9">
{isMobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</Button>
{/* Desktop Toggle */}
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="hidden lg:flex h-9 w-9">
{sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />} {sidebarCollapsed ? <PanelLeft className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
</Button> </Button>
<div className="flex items-center space-x-2"> <div className="flex items-center">
<h1 className="text-lg font-medium">{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋 </h1> <h1 className="text-sm lg:text-lg font-medium truncate max-w-[150px] lg:max-w-none">
{greeting}, {currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} 👋
</h1>
</div> </div>
</div> </div>
<div className="flex items-center space-x-4 z-10"> <div className="flex items-center space-x-1 lg:space-x-3 z-10">
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border" onClick={toggleTheme}> {/* External Links - Hidden on small screens */}
<div className="hidden md:flex items-center space-x-1 mr-2">
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => openExternalLink('https://docs.checkcle.io')}
title={t("documentation")}
>
<FileText className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => openExternalLink('https://github.com/operacle/checkcle')}
title="GitHub"
>
<Github className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => openExternalLink('https://x.com/checkcle_oss')}
title="X (Twitter)"
>
<Twitter className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => openExternalLink('https://discord.gg/xs9gbubGwX')}
title="Discord"
>
<MessageSquare className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
title={t("notifications")}
>
<Bell className="w-4 h-4" />
</Button>
</div>
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border hidden sm:flex" onClick={toggleTheme}>
<span className="sr-only">Toggle theme</span> <span className="sr-only">Toggle theme</span>
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />} {theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
</Button> </Button>
@@ -102,7 +145,7 @@ export const Header = ({
<Globe className="w-4 h-4" /> <Globe className="w-4 h-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem onClick={() => setLanguage("en")} className={language === "en" ? "bg-accent" : ""}> <DropdownMenuItem onClick={() => setLanguage("en")} className={language === "en" ? "bg-accent" : ""}>
{t("english")} {t("english")}
</DropdownMenuItem> </DropdownMenuItem>
@@ -110,7 +153,7 @@ export const Header = ({
{t("khmer")} {t("khmer")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("de")} className={language === "de" ? "bg-accent" : ""}> <DropdownMenuItem onClick={() => setLanguage("de")} className={language === "de" ? "bg-accent" : ""}>
{t("Deutsch")} {t("german")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("ko")} className={language === "ko" ? "bg-accent" : ""}> <DropdownMenuItem onClick={() => setLanguage("ko")} className={language === "ko" ? "bg-accent" : ""}>
{t("korean")} {t("korean")}
@@ -124,74 +167,21 @@ export const Header = ({
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
{/* Documentation */} <div className="h-8 w-px bg-border mx-1 hidden sm:block" />
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://docs.checkcle.io", "_blank")}
>
<span className="sr-only">{t("documentation")}</span>
<FileText className="w-4 h-4" />
</Button>
{/* GitHub */}
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://github.com/operacle/checkcle", "_blank")}
>
<span className="sr-only">GitHub</span>
<Github className="w-4 h-4" />
</Button>
{/* X (Twitter) */}
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://x.com/checkcle_oss", "_blank")}
>
<span className="sr-only">X (Twitter)</span>
<Twitter className="w-4 h-4" />
</Button>
{/* Discord */}
<Button
variant="outline"
size="icon"
className="rounded-full w-8 h-8 border-border"
onClick={() => window.open("https://discord.gg/xs9gbubGwX", "_blank")}
>
<span className="sr-only">Discord</span>
<MessageSquare className="w-4 h-4" />
</Button>
{/* Notifications */}
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
<span className="sr-only">{t("notifications")}</span>
<Bell className="w-4 h-4" />
</Button>
{/* User Profile Dropdown */}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Avatar className="h-8 w-8 cursor-pointer ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground"> <Avatar className="h-8 w-8 cursor-pointer border hover:ring-2 hover:ring-primary/20 transition-all">
{avatarUrl ? <AvatarImage src={avatarUrl} alt={currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} /> : <AvatarFallback className="bg-primary/20 text-primary"> {avatarUrl ? <AvatarImage src={avatarUrl} alt="User" /> : <AvatarFallback className="bg-primary/20 text-primary">{currentUser?.name?.[0] || 'U'}</AvatarFallback>}
{currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'}
</AvatarFallback>}
</Avatar> </Avatar>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56"> <DropdownMenuContent align="end" className="w-56">
<div className="flex items-center gap-3 p-2"> <div className="flex items-center gap-3 p-2">
<Avatar className="h-10 w-10"> <Avatar className="h-10 w-10">
{avatarUrl ? <AvatarImage src={avatarUrl} alt={currentUser?.name || currentUser?.email?.split('@')[0] || 'User'} /> : <AvatarFallback className="bg-primary/20 text-primary"> {avatarUrl ? <AvatarImage src={avatarUrl} alt="User" /> : <AvatarFallback className="bg-primary/20 text-primary">{currentUser?.name?.[0] || 'U'}</AvatarFallback>}
{currentUser?.name?.[0] || currentUser?.email?.[0] || 'U'}
</AvatarFallback>}
</Avatar> </Avatar>
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-0.5">
<span className="font-medium">{currentUser?.name || 'User'}</span> <span className="text-sm font-medium truncate">{currentUser?.name || 'User'}</span>
<span className="text-xs text-muted-foreground truncate">{currentUser?.email}</span> <span className="text-xs text-muted-foreground truncate">{currentUser?.email}</span>
</div> </div>
</div> </div>
@@ -214,4 +204,4 @@ export const Header = ({
</div> </div>
</header> </header>
); );
}; };
@@ -1,28 +1,44 @@
import React from "react"; import React from "react";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { SidebarHeader } from "./sidebar/SidebarHeader"; import { SidebarHeader } from "./sidebar/SidebarHeader";
import { MainNavigation } from "./sidebar/MainNavigation"; import { MainNavigation } from "./sidebar/MainNavigation";
import { SettingsPanel } from "./sidebar/SettingsPanel"; import { SettingsPanel } from "./sidebar/SettingsPanel";
import { useSidebar } from "@/contexts/SidebarContext";
import { cn } from "@/lib/utils";
interface SidebarProps { export const Sidebar = () => {
collapsed: boolean;
}
export const Sidebar = ({ collapsed }: SidebarProps) => {
const { theme } = useTheme(); const { theme } = useTheme();
const { sidebarCollapsed, isMobileOpen, toggleMobileMenu } = useSidebar();
return ( return (
<div <>
className={` {/* Mobile Overlay */}
${theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border'} {isMobileOpen && (
border-r flex flex-col h-full <div
${collapsed ? 'w-16' : 'w-64'} className="fixed inset-0 bg-black/60 z-40 lg:hidden backdrop-blur-sm transition-opacity animate-in fade-in"
`} onClick={toggleMobileMenu}
> />
<SidebarHeader collapsed={collapsed} /> )}
<MainNavigation collapsed={collapsed} />
<SettingsPanel collapsed={collapsed} /> {/* Sidebar Container */}
</div> <aside
className={cn(
"fixed inset-y-0 left-0 z-50 lg:static lg:block transition-all duration-300 ease-in-out border-r flex flex-col h-full",
theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border',
// Mobile state
isMobileOpen ? "translate-x-0 w-64" : "-translate-x-full lg:translate-x-0",
// Desktop state (collapsed/expanded)
!isMobileOpen && sidebarCollapsed ? "lg:w-16" : "lg:w-64"
)}
>
<SidebarHeader collapsed={!isMobileOpen && sidebarCollapsed} />
<div className="flex-1 overflow-y-auto overflow-x-hidden custom-scrollbar">
<MainNavigation collapsed={!isMobileOpen && sidebarCollapsed} />
</div>
<SettingsPanel collapsed={!isMobileOpen && sidebarCollapsed} />
</aside>
</>
); );
}; };
@@ -3,6 +3,8 @@ import { useLocation, useNavigate } from "react-router-dom";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { useLanguage } from "@/contexts/LanguageContext"; import { useLanguage } from "@/contexts/LanguageContext";
import { LucideIcon } from "lucide-react"; import { LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
interface MenuItemProps { interface MenuItemProps {
id: string; id: string;
path: string | null; path: string | null;
@@ -12,8 +14,8 @@ interface MenuItemProps {
hasNavigation: boolean; hasNavigation: boolean;
collapsed: boolean; collapsed: boolean;
} }
export const MenuItem: React.FC<MenuItemProps> = ({ export const MenuItem: React.FC<MenuItemProps> = ({
id,
path, path,
icon: Icon, icon: Icon,
translationKey, translationKey,
@@ -21,36 +23,44 @@ export const MenuItem: React.FC<MenuItemProps> = ({
hasNavigation, hasNavigation,
collapsed collapsed
}) => { }) => {
const { const { theme } = useTheme();
theme const { t } = useLanguage();
} = useTheme();
const {
t
} = useLanguage();
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const isActive = path && location.pathname === path;
const handleClick = (e: React.MouseEvent) => { const handleClick = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation();
if (hasNavigation && path) { if (hasNavigation && path) {
navigate(path, { navigate(path);
replace: false
});
} }
}; };
const isActive = path && location.pathname === path;
const mainIconSize = "h-5 w-5"; return (
return <div className={` <div
${collapsed ? 'p-3' : 'py-3 px-3'} className={cn(
mb-1 rounded-lg "group relative flex items-center mb-1 rounded-lg cursor-pointer transition-all duration-200",
${isActive ? theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`} collapsed ? "p-3 justify-center" : "p-2.5 px-4",
flex items-center gap-3 isActive
${collapsed ? 'justify-center' : ''} ? (theme === 'dark' ? 'bg-zinc-800 text-white' : 'bg-primary/10 text-primary')
cursor-pointer transition-colors : (theme === 'dark' ? 'text-zinc-400 hover:bg-zinc-800/50 hover:text-white' : 'text-zinc-600 hover:bg-zinc-100')
`} onClick={handleClick}> )}
<Icon className={`${mainIconSize} flex-shrink-0 ${theme === 'dark' ? 'text-gray-400' : 'text-muted-foreground'}`} /> onClick={handleClick}
{!collapsed && <span className="text-sidebar-foreground font-inter text-[14px] font-medium tracking-tight leading-tight whitespace-nowrap"> title={collapsed ? t(translationKey) : ""}
>
<Icon className={cn("h-5 w-5 shrink-0 transition-colors", color, isActive ? "opacity-100" : "opacity-80 group-hover:opacity-100")} />
{!collapsed && (
<span className="ml-3 font-medium tracking-wide text-sm truncate">
{t(translationKey)} {t(translationKey)}
</span>} </span>
</div>; )}
{/* Small Indicator for active state */}
{isActive && !collapsed && (
<div className="absolute left-0 w-1 h-6 bg-primary rounded-r-full" />
)}
</div>
);
}; };
@@ -1,4 +1,3 @@
import React from "react"; import React from "react";
import { Sidebar } from "@/components/dashboard/Sidebar"; import { Sidebar } from "@/components/dashboard/Sidebar";
import { Header } from "@/components/dashboard/Header"; import { Header } from "@/components/dashboard/Header";
@@ -7,11 +6,9 @@ import { ServiceNotFound } from "@/components/services/ServiceNotFound";
import { Service } from "@/types/service.types"; import { Service } from "@/types/service.types";
interface ServiceDetailWrapperProps { interface ServiceDetailWrapperProps {
children: React.ReactNode; children?: React.ReactNode;
isLoading: boolean; isLoading: boolean;
service: Service | null; service: Service | null;
sidebarCollapsed: boolean;
toggleSidebar: () => void;
currentUser: any; currentUser: any;
handleLogout: () => void; handleLogout: () => void;
} }
@@ -20,20 +17,16 @@ export const ServiceDetailWrapper = ({
children, children,
isLoading, isLoading,
service, service,
sidebarCollapsed,
toggleSidebar,
currentUser, currentUser,
handleLogout handleLogout
}: ServiceDetailWrapperProps) => { }: ServiceDetailWrapperProps) => {
return ( return (
<div className="flex h-screen w-full overflow-hidden bg-background text-foreground"> <div className="flex h-screen w-full overflow-hidden bg-background text-foreground">
<Sidebar collapsed={sidebarCollapsed} /> <Sidebar />
<div className="flex flex-col flex-1 min-w-0"> <div className="flex flex-col flex-1 min-w-0">
<Header <Header
currentUser={currentUser} currentUser={currentUser}
onLogout={handleLogout} onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
/> />
{isLoading ? ( {isLoading ? (
@@ -48,4 +41,4 @@ export const ServiceDetailWrapper = ({
</div> </div>
</div> </div>
); );
}; };
@@ -1,78 +1,34 @@
import { useState, useEffect, useCallback } from "react"; import React, { useState, useCallback } from "react";
import { useParams, useNavigate } from "react-router-dom"; import { useParams, useNavigate } from "react-router-dom";
import { DateRangeOption } from "../DateRangeFilter"; import { DateRangeOption } from "../DateRangeFilter";
import { authService } from "@/services/authService"; import { authService } from "@/services/authService";
import { ServiceDetailContent } from "../ServiceDetailContent"; import { ServiceDetailContent } from "../ServiceDetailContent";
import { ServiceDetailWrapper } from "./ServiceDetailWrapper"; import { ServiceDetailWrapper } from "./ServiceDetailWrapper";
import { useServiceData, useRealTimeUpdates } from "./hooks"; import { useServiceData, useRealTimeUpdates } from "./hooks";
import { toast } from "@/components/ui/use-toast";
export const ServiceDetailContainer = () => { export const ServiceDetailContainer = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
// Set default to 24h
const [startDate, setStartDate] = useState<Date>(() => { const [startDate, setStartDate] = useState<Date>(() => {
const date = new Date(); const date = new Date();
date.setHours(date.getHours() - 24); // Go back 24 hours date.setHours(date.getHours() - 24);
return date; return date;
}); });
const [endDate, setEndDate] = useState<Date>(() => { const [endDate, setEndDate] = useState<Date>(() => {
const date = new Date(); const date = new Date();
date.setMinutes(date.getMinutes() + 5); // Add 5 minutes buffer to future date.setMinutes(date.getMinutes() + 5);
return date; return date;
}); });
const [selectedRange, setSelectedRange] = useState<DateRangeOption>('24h'); const [selectedRange, setSelectedRange] = useState<DateRangeOption>('24h');
// State for sidebar collapse functionality (shared with Dashboard)
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
// Check if there's a saved preference in localStorage
const saved = localStorage.getItem("sidebarCollapsed");
return saved ? JSON.parse(saved) : window.innerWidth < 768;
});
// Toggle sidebar and save preference
const toggleSidebar = useCallback(() => {
setSidebarCollapsed(prev => {
const newState = !prev;
localStorage.setItem("sidebarCollapsed", JSON.stringify(newState));
return newState;
});
}, []);
// Get current user for header
const currentUser = authService.getCurrentUser(); const currentUser = authService.getCurrentUser();
useEffect(() => {
// Verify user is authenticated
if (!authService.isAuthenticated()) {
toast({
variant: "destructive",
title: "Authentication required",
description: "Please log in to view service details",
});
navigate("/login");
}
// Auto-collapse sidebar on small screens
const handleResize = () => {
if (window.innerWidth < 768 && !sidebarCollapsed) {
setSidebarCollapsed(true);
localStorage.setItem("sidebarCollapsed", JSON.stringify(true));
}
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [navigate, sidebarCollapsed]);
// Handler for logout (same as Dashboard)
const handleLogout = () => { const handleLogout = () => {
authService.logout(); authService.logout();
navigate("/login"); navigate("/login");
}; };
// Use our custom hooks
const { const {
service, service,
uptimeData, uptimeData,
@@ -85,7 +41,6 @@ export const ServiceDetailContainer = () => {
handleRegionalAgentChange handleRegionalAgentChange
} = useServiceData(id, startDate, endDate); } = useServiceData(id, startDate, endDate);
// Set up real-time updates
useRealTimeUpdates({ useRealTimeUpdates({
serviceId: id, serviceId: id,
startDate, startDate,
@@ -94,18 +49,12 @@ export const ServiceDetailContainer = () => {
setUptimeData setUptimeData
}); });
// Handle date range filter changes
const handleDateRangeChange = useCallback((start: Date, end: Date, option: DateRangeOption) => { const handleDateRangeChange = useCallback((start: Date, end: Date, option: DateRangeOption) => {
console.log(`ServiceDetailContainer: Date range changed: ${start.toISOString()} to ${end.toISOString()}, option: ${option}`);
// Update state which will trigger the useEffect in useServiceData
setStartDate(start); setStartDate(start);
setEndDate(end); setEndDate(end);
setSelectedRange(option); setSelectedRange(option);
// Also explicitly fetch data with the new range to ensure immediate update
if (id) { if (id) {
console.log(`ServiceDetailContainer: Explicitly fetching data for service ${id} with new range`);
fetchUptimeData(id, start, end, option, selectedRegionalAgent); fetchUptimeData(id, start, end, option, selectedRegionalAgent);
} }
}, [id, fetchUptimeData, selectedRegionalAgent]); }, [id, fetchUptimeData, selectedRegionalAgent]);
@@ -114,12 +63,10 @@ export const ServiceDetailContainer = () => {
<ServiceDetailWrapper <ServiceDetailWrapper
isLoading={isLoading} isLoading={isLoading}
service={service} service={service}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
currentUser={currentUser} currentUser={currentUser}
handleLogout={handleLogout} handleLogout={handleLogout}
> >
{service && ( {service ? (
<ServiceDetailContent <ServiceDetailContent
service={service} service={service}
uptimeData={uptimeData} uptimeData={uptimeData}
@@ -129,7 +76,7 @@ export const ServiceDetailContainer = () => {
selectedRegionalAgent={selectedRegionalAgent} selectedRegionalAgent={selectedRegionalAgent}
onRegionalAgentChange={handleRegionalAgentChange} onRegionalAgentChange={handleRegionalAgentChange}
/> />
)} ) : null}
</ServiceDetailWrapper> </ServiceDetailWrapper>
); );
}; };
+46 -7
View File
@@ -1,25 +1,64 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import React, { createContext, useContext, useState } from 'react';
interface SidebarContextType { interface SidebarContextType {
sidebarCollapsed: boolean; sidebarCollapsed: boolean;
setSidebarCollapsed: (collapsed: boolean) => void; setSidebarCollapsed: (collapsed: boolean) => void;
toggleSidebar: () => void; toggleSidebar: () => void;
isMobileOpen: boolean;
setIsMobileOpen: (open: boolean) => void;
toggleMobileMenu: () => void;
} }
const SidebarContext = createContext<SidebarContextType | undefined>(undefined); const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const SidebarProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false); // Initialize sidebar collapsed state from localStorage with window check for safety
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem("sidebarCollapsed");
return saved ? JSON.parse(saved) : false;
}
return false;
});
// Fix: added isMobileOpen state for responsive mobile menu
const [isMobileOpen, setIsMobileOpen] = useState(false);
const toggleSidebar = () => { const toggleSidebar = () => {
setSidebarCollapsed(prev => !prev); setSidebarCollapsed(prev => {
const newState = !prev;
if (typeof window !== 'undefined') {
localStorage.setItem("sidebarCollapsed", JSON.stringify(newState));
}
return newState;
});
}; };
// Fix: added toggleMobileMenu function to manage mobile drawer state
const toggleMobileMenu = () => {
setIsMobileOpen(prev => !prev);
};
// Fix: added resize listener to close mobile menu when switching to desktop view
useEffect(() => {
if (typeof window === 'undefined') return;
const handleResize = () => {
if (window.innerWidth >= 1024) {
setIsMobileOpen(false);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const value = { const value = {
sidebarCollapsed, sidebarCollapsed,
setSidebarCollapsed, setSidebarCollapsed,
toggleSidebar toggleSidebar,
isMobileOpen,
setIsMobileOpen,
toggleMobileMenu
}; };
return ( return (
@@ -35,4 +74,4 @@ export const useSidebar = () => {
throw new Error('useSidebar must be used within a SidebarProvider'); throw new Error('useSidebar must be used within a SidebarProvider');
} }
return context; return context;
}; };
+5 -39
View File
@@ -1,4 +1,3 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Header } from "@/components/dashboard/Header"; import { Header } from "@/components/dashboard/Header";
@@ -11,72 +10,39 @@ import { LoadingState } from "@/components/services/LoadingState";
import { useSidebar } from "@/contexts/SidebarContext"; import { useSidebar } from "@/contexts/SidebarContext";
const Dashboard = () => { const Dashboard = () => {
// Use shared sidebar state
const { sidebarCollapsed, toggleSidebar } = useSidebar();
// Get current user
const currentUser = authService.getCurrentUser(); const currentUser = authService.getCurrentUser();
const navigate = useNavigate(); const navigate = useNavigate();
// For debugging user data
useEffect(() => {
// console.log("Current user data:", currentUser);
}, [currentUser]);
// Handle logout
const handleLogout = () => { const handleLogout = () => {
authService.logout(); authService.logout();
navigate("/login"); navigate("/login");
}; };
// Fetch all services with 1-minute polling for real-time updates
const { data: services = [], isLoading, error } = useQuery({ const { data: services = [], isLoading, error } = useQuery({
queryKey: ['services'], queryKey: ['services'],
queryFn: serviceService.getServices, queryFn: serviceService.getServices,
refetchInterval: 60000, // 1 minute as requested refetchInterval: 60000,
staleTime: 30000, // Data is fresh for 30 seconds
gcTime: 120000, // Keep in cache for 2 minutes
refetchOnWindowFocus: true, // Refetch when window gains focus
refetchOnMount: true, // Refetch on mount
refetchOnReconnect: true, // Refetch on reconnect
retry: 2,
retryDelay: 3000,
}); });
// Start monitoring all active services when the dashboard loads - only once
useEffect(() => { useEffect(() => {
let hasStarted = false;
const startActiveServices = async () => { const startActiveServices = async () => {
if (hasStarted) return;
hasStarted = true;
await serviceService.startAllActiveServices(); await serviceService.startAllActiveServices();
// console.log("Active services monitoring started");
}; };
// Only start once and add a delay to prevent immediate execution
const timeoutId = setTimeout(startActiveServices, 2000); const timeoutId = setTimeout(startActiveServices, 2000);
return () => clearTimeout(timeoutId);
return () => { }, []);
clearTimeout(timeoutId);
};
}, []); // Remove services dependency to prevent re-runs
// Show the loading state while fetching data
if (isLoading) { if (isLoading) {
return <LoadingState />; return <LoadingState />;
} }
return ( return (
<div className="flex h-screen overflow-hidden bg-background text-foreground"> <div className="flex h-screen overflow-hidden bg-background text-foreground">
<Sidebar collapsed={sidebarCollapsed} /> <Sidebar />
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1 min-w-0">
<Header <Header
currentUser={currentUser} currentUser={currentUser}
onLogout={handleLogout} onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
/> />
<DashboardContent <DashboardContent
services={services} services={services}
+20 -74
View File
@@ -1,4 +1,3 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
@@ -10,16 +9,13 @@ import { ServerTable } from "@/components/servers/ServerTable";
import { AddServerAgentDialog } from "@/components/servers/AddServerAgentDialog"; import { AddServerAgentDialog } from "@/components/servers/AddServerAgentDialog";
import { serverService } from "@/services/serverService"; import { serverService } from "@/services/serverService";
import { Server, ServerStats } from "@/types/server.types"; import { Server, ServerStats } from "@/types/server.types";
import { useSidebar } from "@/contexts/SidebarContext";
import { authService } from "@/services/authService"; import { authService } from "@/services/authService";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
const InstanceMonitoring = () => { const InstanceMonitoring = () => {
const { theme } = useTheme();
const { t } = useLanguage(); const { t } = useLanguage();
const { sidebarCollapsed, toggleSidebar } = useSidebar();
const navigate = useNavigate(); const navigate = useNavigate();
const [stats, setStats] = useState<ServerStats>({ const [stats, setStats] = useState<ServerStats>({
@@ -29,13 +25,13 @@ const InstanceMonitoring = () => {
warning: 0 warning: 0
}); });
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser()); const [currentUser] = useState(authService.getCurrentUser());
const [addDialogOpen, setAddDialogOpen] = useState(false); const [addDialogOpen, setAddDialogOpen] = useState(false);
const { data: servers = [], isLoading, error, refetch } = useQuery({ const { data: servers = [], isLoading, error, refetch } = useQuery<Server[]>({
queryKey: ['servers'], queryKey: ['servers'],
queryFn: serverService.getServers, queryFn: serverService.getServers,
refetchInterval: 30000 // Refetch every 30 seconds refetchInterval: 30000
}); });
useEffect(() => { useEffect(() => {
@@ -44,88 +40,38 @@ const InstanceMonitoring = () => {
} }
}, [servers]); }, [servers]);
const handleRefresh = () => {
refetch();
};
const handleLogout = () => { const handleLogout = () => {
authService.logout(); authService.logout();
navigate('/login'); navigate('/login');
}; };
const handleAgentAdded = () => {
refetch();
};
if (error) {
return (
<div className="flex h-screen overflow-hidden bg-background text-foreground">
<Sidebar collapsed={sidebarCollapsed} />
<div className="flex flex-col flex-1 min-w-0">
<Header
currentUser={currentUser}
onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
/>
<main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6">
<div className="text-center max-w-md w-full">
<h2 className="text-xl sm:text-2xl font-bold mb-4">{t('errorLoadingServers')}</h2>
<p className="text-muted-foreground mb-4 text-sm sm:text-base">
{t('unableToFetchServerData')}
</p>
<button
onClick={handleRefresh}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 text-sm sm:text-base"
>
{t('retry')}
</button>
</div>
</main>
</div>
</div>
);
}
return ( return (
<div className="flex h-screen overflow-hidden bg-background text-foreground"> <div className="flex h-screen overflow-hidden bg-background text-foreground">
<Sidebar collapsed={sidebarCollapsed} /> <Sidebar />
<div className="flex flex-col flex-1 min-w-0"> <div className="flex flex-col flex-1 min-w-0">
<Header <Header
currentUser={currentUser} currentUser={currentUser}
onLogout={handleLogout} onLogout={handleLogout}
sidebarCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
/> />
<main className="flex-1 overflow-auto"> <main className="flex-1 overflow-auto">
<div className="p-4 sm:p-6 lg:p-8 space-y-6"> <div className="p-4 lg:p-8 space-y-6">
{/* Header Section */} <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div className="min-w-0 flex-1">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <h1 className="text-2xl font-bold text-foreground">
<div className="min-w-0 flex-1"> {t('instanceMonitoring')}
<h1 className="text-2xl lg:text-2xl font-bold text-foreground"> </h1>
{t('instanceMonitoring')} <p className="text-muted-foreground mt-1 text-sm">
</h1> {t('describeMonitorInstance')}
<p className="text-muted-foreground mt-1 text-xs sm:text-sm"> </p>
{t('describeMonitorInstance')}
</p>
</div>
<Button onClick={() => setAddDialogOpen(true)} className="flex-shrink-0">
<Plus className="mr-2 h-4 w-4" />
{t('addServerAgent')}
</Button>
</div> </div>
<Button onClick={() => setAddDialogOpen(true)} className="flex-shrink-0">
<Plus className="mr-2 h-4 w-4" />
{t('addServerAgent')}
</Button>
</div> </div>
{/* Stats Cards Section */} <ServerStatsCards stats={stats} />
<div> <ServerTable servers={servers} isLoading={isLoading} onRefresh={refetch} />
<ServerStatsCards stats={stats} />
</div>
{/* Server Table Section */}
<div>
<ServerTable servers={servers} isLoading={isLoading} onRefresh={handleRefresh} />
</div>
</div> </div>
</main> </main>
</div> </div>
@@ -133,7 +79,7 @@ const InstanceMonitoring = () => {
<AddServerAgentDialog <AddServerAgentDialog
open={addDialogOpen} open={addDialogOpen}
onOpenChange={setAddDialogOpen} onOpenChange={setAddDialogOpen}
onAgentAdded={handleAgentAdded} onAgentAdded={refetch}
/> />
</div> </div>
); );