Fix: Server Historical Performance chart improvement
Fix: Improve sidebar collapse/expand performance
This commit is contained in:
@@ -13,7 +13,13 @@ export const Sidebar = ({ collapsed }: SidebarProps) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className={`${collapsed ? 'w-16' : 'w-64'} ${theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border'} border-r flex flex-col transition-all duration-300 h-full`}>
|
||||
<div
|
||||
className={`
|
||||
${theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border'}
|
||||
border-r flex flex-col h-full
|
||||
${collapsed ? 'w-16' : 'w-64'}
|
||||
`}
|
||||
>
|
||||
<SidebarHeader collapsed={collapsed} />
|
||||
<MainNavigation collapsed={collapsed} />
|
||||
<SettingsPanel collapsed={collapsed} />
|
||||
|
||||
@@ -34,7 +34,6 @@ export const MenuItem: React.FC<MenuItemProps> = ({
|
||||
e.stopPropagation();
|
||||
|
||||
if (hasNavigation && path) {
|
||||
// Use navigate instead of window.location to prevent full page reload
|
||||
navigate(path, { replace: false });
|
||||
}
|
||||
};
|
||||
@@ -44,11 +43,22 @@ export const MenuItem: React.FC<MenuItemProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg ${isActive ? theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200 cursor-pointer`}
|
||||
className={`
|
||||
${collapsed ? 'p-3' : 'p-2 pl-3'}
|
||||
mb-1 rounded-lg
|
||||
${isActive ? (theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent') : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`}
|
||||
flex items-center
|
||||
${collapsed ? 'justify-center' : ''}
|
||||
cursor-pointer
|
||||
`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Icon className={`${mainIconSize} ${color}`} />
|
||||
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t(translationKey)}</span>}
|
||||
{!collapsed && (
|
||||
<span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">
|
||||
{t(translationKey)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -62,7 +62,7 @@ export const DockerContainersTable = ({ containers, isLoading, onRefresh }: Dock
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="min-w-full inline-block align-middle">
|
||||
<div className="overflow-hidden border border-border rounded-lg shadow-sm bg-card">
|
||||
<div className="overflow-hidden border border-border rounded-lg shadow-sm">
|
||||
<Table>
|
||||
<DockerTableHeader />
|
||||
<TableBody>
|
||||
|
||||
@@ -3,44 +3,51 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Container, Play, Square, AlertTriangle } from "lucide-react";
|
||||
import { DockerStats } from "@/types/docker.types";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
interface DockerStatsCardsProps {
|
||||
stats: DockerStats;
|
||||
}
|
||||
|
||||
export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const cards = [
|
||||
{
|
||||
title: "Total Containers",
|
||||
value: stats.total,
|
||||
icon: Container,
|
||||
color: "text-blue-600",
|
||||
bgColor: "bg-blue-50",
|
||||
borderColor: "border-blue-200",
|
||||
gradient: theme === 'dark'
|
||||
? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(59, 130, 246, 0.6) 100%)"
|
||||
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #3b82f6 100%)"
|
||||
},
|
||||
{
|
||||
title: "Running",
|
||||
value: stats.running,
|
||||
icon: Play,
|
||||
color: "text-green-600",
|
||||
bgColor: "bg-green-50",
|
||||
borderColor: "border-green-200",
|
||||
gradient: theme === 'dark'
|
||||
? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(16, 185, 129, 0.6) 100%)"
|
||||
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #10b981 100%)"
|
||||
},
|
||||
{
|
||||
title: "Stopped",
|
||||
value: stats.stopped,
|
||||
icon: Square,
|
||||
color: "text-gray-600",
|
||||
bgColor: "bg-gray-50",
|
||||
borderColor: "border-gray-200",
|
||||
gradient: theme === 'dark'
|
||||
? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(107, 114, 128, 0.6) 100%)"
|
||||
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #6b7280 100%)"
|
||||
},
|
||||
{
|
||||
title: "Warning",
|
||||
value: stats.warning,
|
||||
icon: AlertTriangle,
|
||||
color: "text-amber-600",
|
||||
bgColor: "bg-amber-50",
|
||||
borderColor: "border-amber-200",
|
||||
gradient: theme === 'dark'
|
||||
? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(245, 158, 11, 0.6) 100%)"
|
||||
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #f59e0b 100%)"
|
||||
},
|
||||
];
|
||||
|
||||
@@ -49,23 +56,39 @@ export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
|
||||
{cards.map((card) => {
|
||||
const IconComponent = card.icon;
|
||||
return (
|
||||
<Card key={card.title} className={`${card.borderColor} bg-card hover:shadow-md transition-shadow`}>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
<Card
|
||||
key={card.title}
|
||||
className="border-none rounded-xl shadow-lg overflow-hidden transition-all duration-300 hover:shadow-xl transform hover:-translate-y-1 relative"
|
||||
style={{ background: card.gradient }}
|
||||
>
|
||||
{/* Grid Pattern Overlay */}
|
||||
<div className="absolute inset-0 z-0 opacity-10">
|
||||
<div
|
||||
className="w-full h-full"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)`,
|
||||
backgroundSize: '20px 20px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 relative z-10">
|
||||
<CardTitle className="text-sm font-medium text-white/70">
|
||||
{card.title}
|
||||
</CardTitle>
|
||||
<div className={`${card.bgColor} ${card.color} p-2 rounded-md`}>
|
||||
<IconComponent className="h-4 w-4" />
|
||||
<div className="p-2.5 rounded-xl bg-white/20 backdrop-blur-sm shadow-sm transition-all duration-300 group-hover:scale-110">
|
||||
<IconComponent className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="relative z-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{card.value}
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${card.color} ${card.borderColor} text-xs`}
|
||||
className="text-xs font-mono font-bold px-2 py-1 rounded-md bg-white/20 backdrop-blur-sm text-white border border-white/30"
|
||||
>
|
||||
Containers
|
||||
</Badge>
|
||||
|
||||
@@ -0,0 +1,680 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { Server } from "@/types/server.types";
|
||||
import { RefreshCw, X } from "lucide-react";
|
||||
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
|
||||
import { templateService, NotificationTemplate } from "@/services/templateService";
|
||||
import { serverThresholdService, ServerThreshold } from "@/services/serverThresholdService";
|
||||
|
||||
interface EditServerDialogProps {
|
||||
server: Server | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onServerUpdated: () => void;
|
||||
}
|
||||
|
||||
interface ServerFormData {
|
||||
name: string;
|
||||
check_interval: number;
|
||||
retry_attempts: number;
|
||||
docker_monitoring: boolean;
|
||||
notification_enabled: boolean;
|
||||
notification_channels: string[]; // Changed to array for multiple selections
|
||||
threshold_id: string;
|
||||
template_id: string;
|
||||
}
|
||||
|
||||
interface ThresholdFormData {
|
||||
cpu_threshold: number;
|
||||
ram_threshold: number;
|
||||
disk_threshold: number;
|
||||
network_threshold: number;
|
||||
}
|
||||
|
||||
export const EditServerDialog: React.FC<EditServerDialogProps> = ({
|
||||
server,
|
||||
open,
|
||||
onOpenChange,
|
||||
onServerUpdated,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState<ServerFormData>({
|
||||
name: "",
|
||||
check_interval: 60,
|
||||
retry_attempts: 3,
|
||||
docker_monitoring: false,
|
||||
notification_enabled: false,
|
||||
notification_channels: [], // Changed to array
|
||||
threshold_id: "none",
|
||||
template_id: "none",
|
||||
});
|
||||
|
||||
const [thresholdFormData, setThresholdFormData] = useState<ThresholdFormData>({
|
||||
cpu_threshold: 80,
|
||||
ram_threshold: 80,
|
||||
disk_threshold: 80,
|
||||
network_threshold: 80,
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
|
||||
const [templates, setTemplates] = useState<NotificationTemplate[]>([]);
|
||||
const [thresholds, setThresholds] = useState<ServerThreshold[]>([]);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<NotificationTemplate | null>(null);
|
||||
const [selectedThreshold, setSelectedThreshold] = useState<ServerThreshold | null>(null);
|
||||
const [loadingAlertConfigs, setLoadingAlertConfigs] = useState(false);
|
||||
const [loadingTemplates, setLoadingTemplates] = useState(false);
|
||||
const [loadingThresholds, setLoadingThresholds] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
// Initialize form data when server changes
|
||||
useEffect(() => {
|
||||
if (server) {
|
||||
console.log("Setting form data for server:", server);
|
||||
// Parse comma-separated notification_id into array
|
||||
const notificationChannels = server.notification_id
|
||||
? server.notification_id.split(',').map(id => id.trim()).filter(id => id)
|
||||
: [];
|
||||
|
||||
setFormData({
|
||||
name: server.name || "",
|
||||
check_interval: server.check_interval || 60,
|
||||
retry_attempts: 3,
|
||||
docker_monitoring: server.docker === "true",
|
||||
notification_enabled: notificationChannels.length > 0,
|
||||
notification_channels: notificationChannels,
|
||||
threshold_id: server.threshold_id || "none",
|
||||
template_id: server.template_id || "none",
|
||||
});
|
||||
}
|
||||
}, [server]);
|
||||
|
||||
// Load data when dialog opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
loadAlertConfigurations();
|
||||
loadTemplates();
|
||||
loadThresholds();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Load existing threshold data when thresholds are loaded and we have a server with threshold_id
|
||||
useEffect(() => {
|
||||
if (server && server.threshold_id && thresholds.length > 0) {
|
||||
console.log("Loading existing threshold data for server:", server.threshold_id);
|
||||
const existingThreshold = thresholds.find(t => t.id === server.threshold_id);
|
||||
if (existingThreshold) {
|
||||
console.log("Found existing threshold:", existingThreshold);
|
||||
setSelectedThreshold(existingThreshold);
|
||||
// Handle the API response format with proper field names and type conversion
|
||||
setThresholdFormData({
|
||||
cpu_threshold: parseInt(String(existingThreshold.cpu_threshold)) || 80,
|
||||
ram_threshold: parseInt(String((existingThreshold as any).ram_threshold_message || existingThreshold.ram_threshold)) || 80,
|
||||
disk_threshold: parseInt(String(existingThreshold.disk_threshold)) || 80,
|
||||
network_threshold: parseInt(String(existingThreshold.network_threshold)) || 80,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [server, thresholds]);
|
||||
|
||||
// Update selected template when form data or templates change
|
||||
useEffect(() => {
|
||||
if (formData.template_id && formData.template_id !== "none" && templates.length > 0) {
|
||||
const template = templates.find(t => t.id === formData.template_id);
|
||||
setSelectedTemplate(template || null);
|
||||
} else {
|
||||
setSelectedTemplate(null);
|
||||
}
|
||||
}, [formData.template_id, templates]);
|
||||
|
||||
// Update selected threshold when threshold_id changes in form
|
||||
useEffect(() => {
|
||||
if (formData.threshold_id && formData.threshold_id !== "none" && thresholds.length > 0) {
|
||||
const threshold = thresholds.find(t => t.id === formData.threshold_id);
|
||||
setSelectedThreshold(threshold || null);
|
||||
if (threshold) {
|
||||
// Handle the API response format with proper field names and type conversion
|
||||
setThresholdFormData({
|
||||
cpu_threshold: parseInt(String(threshold.cpu_threshold)) || 80,
|
||||
ram_threshold: parseInt(String((threshold as any).ram_threshold_message || threshold.ram_threshold)) || 80,
|
||||
disk_threshold: parseInt(String(threshold.disk_threshold)) || 80,
|
||||
network_threshold: parseInt(String(threshold.network_threshold)) || 80,
|
||||
});
|
||||
}
|
||||
} else if (formData.threshold_id === "none") {
|
||||
setSelectedThreshold(null);
|
||||
setThresholdFormData({
|
||||
cpu_threshold: 80,
|
||||
ram_threshold: 80,
|
||||
disk_threshold: 80,
|
||||
network_threshold: 80,
|
||||
});
|
||||
}
|
||||
}, [formData.threshold_id, thresholds]);
|
||||
|
||||
const loadAlertConfigurations = async () => {
|
||||
try {
|
||||
setLoadingAlertConfigs(true);
|
||||
const configs = await alertConfigService.getAlertConfigurations();
|
||||
setAlertConfigs(configs);
|
||||
} catch (error) {
|
||||
console.error('Error loading alert configurations:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to load notification channels",
|
||||
});
|
||||
} finally {
|
||||
setLoadingAlertConfigs(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
setLoadingTemplates(true);
|
||||
const templateList = await templateService.getTemplates();
|
||||
setTemplates(templateList);
|
||||
} catch (error) {
|
||||
console.error('Error loading templates:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to load templates",
|
||||
});
|
||||
} finally {
|
||||
setLoadingTemplates(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadThresholds = async () => {
|
||||
try {
|
||||
setLoadingThresholds(true);
|
||||
const thresholdList = await serverThresholdService.getServerThresholds();
|
||||
setThresholds(thresholdList);
|
||||
} catch (error) {
|
||||
console.error('Error loading server thresholds:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to load server thresholds",
|
||||
});
|
||||
} finally {
|
||||
setLoadingThresholds(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleThresholdUpdate = async () => {
|
||||
if (!selectedThreshold) return;
|
||||
|
||||
try {
|
||||
// Use the correct field name for RAM threshold
|
||||
const updateData = {
|
||||
cpu_threshold: thresholdFormData.cpu_threshold,
|
||||
ram_threshold_message: thresholdFormData.ram_threshold, // Use the correct field name
|
||||
disk_threshold: thresholdFormData.disk_threshold,
|
||||
network_threshold: thresholdFormData.network_threshold,
|
||||
};
|
||||
|
||||
await serverThresholdService.updateServerThreshold(selectedThreshold.id, updateData);
|
||||
|
||||
// Update local state
|
||||
setSelectedThreshold({
|
||||
...selectedThreshold,
|
||||
...thresholdFormData,
|
||||
});
|
||||
|
||||
// Update thresholds list
|
||||
setThresholds(prev => prev.map(t =>
|
||||
t.id === selectedThreshold.id
|
||||
? { ...t, ...thresholdFormData }
|
||||
: t
|
||||
));
|
||||
|
||||
toast({
|
||||
title: "Threshold updated",
|
||||
description: "Server threshold values have been updated successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating threshold:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to update threshold values.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotificationChannelToggle = (channelId: string, checked: boolean) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
notification_channels: checked
|
||||
? [...prev.notification_channels, channelId]
|
||||
: prev.notification_channels.filter(id => id !== channelId)
|
||||
}));
|
||||
};
|
||||
|
||||
const removeNotificationChannel = (channelId: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
notification_channels: prev.notification_channels.filter(id => id !== channelId)
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!server || isSubmitting) return;
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Convert notification channels array to comma-separated string
|
||||
const notificationChannelsString = formData.notification_enabled
|
||||
? formData.notification_channels.join(',')
|
||||
: "";
|
||||
|
||||
const updateData = {
|
||||
name: formData.name,
|
||||
check_interval: formData.check_interval,
|
||||
docker: formData.docker_monitoring ? "true" : "false",
|
||||
notification_id: notificationChannelsString,
|
||||
threshold_id: formData.notification_enabled && formData.threshold_id !== "none" ? formData.threshold_id : "",
|
||||
template_id: formData.notification_enabled && formData.template_id !== "none" ? formData.template_id : "",
|
||||
updated: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await pb.collection('servers').update(server.id, updateData);
|
||||
|
||||
toast({
|
||||
title: "Server updated",
|
||||
description: `${formData.name} has been updated successfully.`,
|
||||
});
|
||||
|
||||
onServerUpdated();
|
||||
onOpenChange(false);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating server:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to update server. Please try again.",
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (server) {
|
||||
const notificationChannels = server.notification_id
|
||||
? server.notification_id.split(',').map(id => id.trim()).filter(id => id)
|
||||
: [];
|
||||
|
||||
setFormData({
|
||||
name: server.name || "",
|
||||
check_interval: server.check_interval || 60,
|
||||
retry_attempts: 3,
|
||||
docker_monitoring: server.docker === "true",
|
||||
notification_enabled: notificationChannels.length > 0,
|
||||
notification_channels: notificationChannels,
|
||||
threshold_id: server.threshold_id || "none",
|
||||
template_id: server.template_id || "none",
|
||||
});
|
||||
}
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Server Configuration</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="serverName">Server Name *</Label>
|
||||
<Input
|
||||
id="serverName"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="Enter server name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="checkInterval">Check Interval</Label>
|
||||
<Select
|
||||
value={formData.check_interval.toString()}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, check_interval: parseInt(value) }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select interval" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="30">30 seconds</SelectItem>
|
||||
<SelectItem value="60">1 minute</SelectItem>
|
||||
<SelectItem value="120">2 minutes</SelectItem>
|
||||
<SelectItem value="300">5 minutes</SelectItem>
|
||||
<SelectItem value="600">10 minutes</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="retryAttempts">Retry Attempts</Label>
|
||||
<Select
|
||||
value={formData.retry_attempts.toString()}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, retry_attempts: parseInt(value) }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select retry attempts" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">1 attempt</SelectItem>
|
||||
<SelectItem value="2">2 attempts</SelectItem>
|
||||
<SelectItem value="3">3 attempts</SelectItem>
|
||||
<SelectItem value="5">5 attempts</SelectItem>
|
||||
<SelectItem value="10">10 attempts</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dockerMonitoring">Docker Monitoring</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="dockerMonitoring"
|
||||
checked={formData.docker_monitoring}
|
||||
onCheckedChange={(checked) => setFormData(prev => ({
|
||||
...prev,
|
||||
docker_monitoring: checked
|
||||
}))}
|
||||
/>
|
||||
<Label htmlFor="dockerMonitoring" className="text-sm text-muted-foreground">
|
||||
{formData.docker_monitoring ? "Enabled" : "Disabled"}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notification Status Toggle */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="notificationEnabled"
|
||||
checked={formData.notification_enabled}
|
||||
onCheckedChange={(checked) => setFormData(prev => ({
|
||||
...prev,
|
||||
notification_enabled: checked,
|
||||
notification_channels: checked ? prev.notification_channels : [],
|
||||
threshold_id: checked ? prev.threshold_id : "none",
|
||||
template_id: checked ? prev.template_id : "none"
|
||||
}))}
|
||||
/>
|
||||
<Label htmlFor="notificationEnabled">Enable Notifications</Label>
|
||||
</div>
|
||||
|
||||
{/* Expanded Notification Settings */}
|
||||
{formData.notification_enabled && (
|
||||
<Card className="border-l-4 border-l-blue-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Notification Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Multiple Notification Channels Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>Notification Channels</Label>
|
||||
<div className="space-y-2 max-h-40 overflow-y-auto border rounded-md p-3">
|
||||
{loadingAlertConfigs ? (
|
||||
<div className="text-sm text-muted-foreground">Loading channels...</div>
|
||||
) : alertConfigs.length > 0 ? (
|
||||
alertConfigs.map((config) => (
|
||||
<div key={config.id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`channel-${config.id}`}
|
||||
checked={formData.notification_channels.includes(config.id || "")}
|
||||
onCheckedChange={(checked) =>
|
||||
handleNotificationChannelToggle(config.id || "", checked as boolean)
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`channel-${config.id}`}
|
||||
className="flex-1 text-sm cursor-pointer"
|
||||
>
|
||||
{config.notify_name} ({config.notification_type})
|
||||
</Label>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">No notification channels available</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Selected Channels Display */}
|
||||
{formData.notification_channels.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Selected Channels:</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{formData.notification_channels.map((channelId) => {
|
||||
const channel = alertConfigs.find(c => c.id === channelId);
|
||||
return (
|
||||
<div
|
||||
key={channelId}
|
||||
className="flex items-center gap-1 bg-secondary text-secondary-foreground px-2 py-1 rounded-md text-sm"
|
||||
>
|
||||
<span>{channel?.notify_name || channelId}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-4 w-4 p-0 hover:bg-destructive hover:text-destructive-foreground"
|
||||
onClick={() => removeNotificationChannel(channelId)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Server Set Threshold Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="thresholdId">Server Set Threshold</Label>
|
||||
<Select
|
||||
value={formData.threshold_id}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, threshold_id: value }))}
|
||||
disabled={loadingThresholds}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={loadingThresholds ? "Loading thresholds..." : "Select server threshold"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No threshold (use default)</SelectItem>
|
||||
{thresholds.map((threshold) => (
|
||||
<SelectItem key={threshold.id} value={threshold.id}>
|
||||
{threshold.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Editable Threshold Details */}
|
||||
{selectedThreshold && (
|
||||
<Card className="bg-muted/50">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-base">Threshold Details: {selectedThreshold.name}</CardTitle>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleThresholdUpdate}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
Update Thresholds
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">CPU Threshold (%)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={thresholdFormData.cpu_threshold}
|
||||
onChange={(e) => setThresholdFormData(prev => ({
|
||||
...prev,
|
||||
cpu_threshold: parseInt(e.target.value) || 0
|
||||
}))}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">RAM Threshold (%)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={thresholdFormData.ram_threshold}
|
||||
onChange={(e) => setThresholdFormData(prev => ({
|
||||
...prev,
|
||||
ram_threshold: parseInt(e.target.value) || 0
|
||||
}))}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Disk Threshold (%)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={thresholdFormData.disk_threshold}
|
||||
onChange={(e) => setThresholdFormData(prev => ({
|
||||
...prev,
|
||||
disk_threshold: parseInt(e.target.value) || 0
|
||||
}))}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Network Threshold (%)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={thresholdFormData.network_threshold}
|
||||
onChange={(e) => setThresholdFormData(prev => ({
|
||||
...prev,
|
||||
network_threshold: parseInt(e.target.value) || 0
|
||||
}))}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Server Template Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="templateId">Server Template</Label>
|
||||
<Select
|
||||
value={formData.template_id}
|
||||
onValueChange={(value) => setFormData(prev => ({ ...prev, template_id: value }))}
|
||||
disabled={loadingTemplates}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={loadingTemplates ? "Loading templates..." : "Select server template"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No template (use default)</SelectItem>
|
||||
{templates.map((template) => (
|
||||
<SelectItem key={template.id} value={template.id}>
|
||||
{template.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Template Details */}
|
||||
{selectedTemplate && (
|
||||
<Card className="bg-muted/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Template Details: {selectedTemplate.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-1 gap-3 text-sm">
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">RAM Threshold Message</Label>
|
||||
<p className="text-sm bg-background p-2 rounded border">
|
||||
{selectedTemplate.up_message || "No RAM threshold message defined"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">CPU Threshold Message</Label>
|
||||
<p className="text-sm bg-background p-2 rounded border">
|
||||
{selectedTemplate.down_message || "No CPU threshold message defined"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Disk Threshold Message</Label>
|
||||
<p className="text-sm bg-background p-2 rounded border">
|
||||
{selectedTemplate.incident_message || "No disk threshold message defined"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-muted-foreground">Network Threshold Message</Label>
|
||||
<p className="text-sm bg-background p-2 rounded border">
|
||||
{selectedTemplate.maintenance_message || "No network threshold message defined"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||||
Updating...
|
||||
</>
|
||||
) : (
|
||||
"Update Server"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -27,7 +27,7 @@ export const OneClickInstallTab: React.FC<OneClickInstallTabProps> = ({
|
||||
onDialogClose,
|
||||
}) => {
|
||||
const getOneClickInstallCommand = () => {
|
||||
const scriptUrl = "https://raw.githubusercontent.com/operacle/checke-server-agent/refs/heads/main/server-agent.sh";
|
||||
const scriptUrl = "https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/server-agent.sh";
|
||||
|
||||
return `curl -L -o server-agent.sh "${scriptUrl}"
|
||||
chmod +x server-agent.sh
|
||||
|
||||
@@ -24,7 +24,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
|
||||
isFetching
|
||||
} = useServerHistoryData(serverId);
|
||||
|
||||
//console.log('ServerHistoryCharts: Rendering with serverId:', serverId, 'timeRange:', timeRange);
|
||||
console.log('ServerHistoryCharts: Rendering with serverId:', serverId, 'timeRange:', timeRange);
|
||||
|
||||
// Memoize latest data calculation to prevent unnecessary recalculations
|
||||
const latestData = useMemo(() => {
|
||||
@@ -48,10 +48,10 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
|
||||
</div>
|
||||
|
||||
{/* Skeleton loading cards */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4 lg:gap-6">
|
||||
{[1, 2, 3, 4].map((index) => (
|
||||
<Card key={index} className="bg-gradient-to-br from-background to-muted/20">
|
||||
<CardContent className="p-6">
|
||||
<CardContent className="p-4 lg:p-6">
|
||||
<div className="animate-pulse">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -60,7 +60,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
|
||||
</div>
|
||||
<div className="w-16 h-4 bg-muted rounded"></div>
|
||||
</div>
|
||||
<div className="w-full h-80 bg-muted rounded"></div>
|
||||
<div className="w-full h-64 lg:h-80 bg-muted rounded"></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -71,7 +71,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
|
||||
}
|
||||
|
||||
if (error) {
|
||||
// console.error('ServerHistoryCharts: Error loading data:', error);
|
||||
console.error('ServerHistoryCharts: Error loading data:', error);
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -122,11 +122,11 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
// console.log('ServerHistoryCharts: Rendering charts with', chartData.length, 'data points for time range:', timeRange);
|
||||
console.log('ServerHistoryCharts: Rendering charts with', chartData.length, 'data points for time range:', timeRange);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
<h2 className="text-lg font-medium">Historical Performance</h2>
|
||||
@@ -143,12 +143,20 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
|
||||
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
|
||||
</div>
|
||||
|
||||
{/* Use CSS Grid for better performance than flexbox */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<CPUChart data={chartData} latestData={latestData} />
|
||||
<MemoryChart data={chartData} latestData={latestData} />
|
||||
<DiskChart data={chartData} latestData={latestData} />
|
||||
<NetworkChart data={chartData} latestData={latestData} />
|
||||
{/* Improved responsive grid layout */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4 lg:gap-6 auto-rows-fr">
|
||||
<div className="w-full min-w-0">
|
||||
<CPUChart data={chartData} latestData={latestData} />
|
||||
</div>
|
||||
<div className="w-full min-w-0">
|
||||
<MemoryChart data={chartData} latestData={latestData} />
|
||||
</div>
|
||||
<div className="w-full min-w-0">
|
||||
<DiskChart data={chartData} latestData={latestData} />
|
||||
</div>
|
||||
<div className="w-full min-w-0">
|
||||
<NetworkChart data={chartData} latestData={latestData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
@@ -13,9 +12,11 @@ import { RefreshCw, Search, Eye, Activity, MoreHorizontal, Pause, Play, Edit, Tr
|
||||
import { Server } from "@/types/server.types";
|
||||
import { ServerStatusBadge } from "./ServerStatusBadge";
|
||||
import { OSTypeIcon } from "./OSTypeIcon";
|
||||
import { EditServerDialog } from "./EditServerDialog";
|
||||
import { serverService } from "@/services/serverService";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
interface ServerTableProps {
|
||||
servers: Server[];
|
||||
@@ -24,8 +25,10 @@ interface ServerTableProps {
|
||||
}
|
||||
|
||||
export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => {
|
||||
const { theme } = useTheme();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [selectedServer, setSelectedServer] = useState<Server | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [pausingServers, setPausingServers] = useState<Set<string>>(new Set());
|
||||
@@ -104,9 +107,9 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (serverId: string) => {
|
||||
// TODO: Implement edit functionality
|
||||
console.log('Edit server:', serverId);
|
||||
const handleEdit = (server: Server) => {
|
||||
setSelectedServer(server);
|
||||
setEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = (server: Server) => {
|
||||
@@ -165,8 +168,8 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="flex-1 flex flex-col min-h-0">
|
||||
<CardHeader className="flex-shrink-0">
|
||||
<Card className="bg-transparent border-0 shadow-none">
|
||||
<CardHeader className="pb-4 px-0">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<CardTitle className="text-xl font-semibold">Servers</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -185,185 +188,191 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 flex flex-col min-h-0 p-0">
|
||||
<CardContent className="p-0">
|
||||
{filteredServers.length === 0 ? (
|
||||
<div className="flex-1 flex items-center justify-center p-8">
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<p className="text-muted-foreground">No servers found</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="flex-1 overflow-auto border-t">
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 bg-background z-10">
|
||||
<TableRow>
|
||||
<TableHead className="w-[150px] min-w-[120px]">Name</TableHead>
|
||||
<TableHead className="w-[100px] min-w-[80px]">Status</TableHead>
|
||||
<TableHead className="w-[100px] min-w-[80px]">OS</TableHead>
|
||||
<TableHead className="w-[120px] min-w-[100px]">IP Address</TableHead>
|
||||
<TableHead className="w-[140px] min-w-[120px]">CPU</TableHead>
|
||||
<TableHead className="w-[140px] min-w-[120px]">Memory</TableHead>
|
||||
<TableHead className="w-[140px] min-w-[120px]">Disk</TableHead>
|
||||
<TableHead className="w-[100px] min-w-[80px]">Uptime</TableHead>
|
||||
<TableHead className="w-[150px] min-w-[130px]">Last Checked</TableHead>
|
||||
<TableHead className="w-[80px] min-w-[60px] text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredServers.map((server) => {
|
||||
const cpuUsage = server.cpu_usage || 0;
|
||||
const memoryUsage = server.ram_total > 0 ? (server.ram_used / server.ram_total) * 100 : 0;
|
||||
const diskUsage = server.disk_total > 0 ? (server.disk_used / server.disk_total) * 100 : 0;
|
||||
const isPaused = server.status === "paused";
|
||||
const isProcessing = pausingServers.has(server.id);
|
||||
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} rounded-lg border border-border shadow-sm`}>
|
||||
<Table>
|
||||
<TableHeader className={`${theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50'}`}>
|
||||
<TableRow className={`${theme === 'dark' ? 'border-gray-700 hover:bg-gray-800' : 'border-gray-200 hover:bg-gray-100'}`}>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Name</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Status</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>OS</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>IP Address</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>CPU</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Memory</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Disk</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Uptime</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4`}>Last Checked</TableHead>
|
||||
<TableHead className={`${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'} font-medium text-base py-4 text-right`}>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredServers.map((server) => {
|
||||
const cpuUsage = server.cpu_usage || 0;
|
||||
const memoryUsage = server.ram_total > 0 ? (server.ram_used / server.ram_total) * 100 : 0;
|
||||
const diskUsage = server.disk_total > 0 ? (server.disk_used / server.disk_total) * 100 : 0;
|
||||
const isPaused = server.status === "paused";
|
||||
const isProcessing = pausingServers.has(server.id);
|
||||
|
||||
return (
|
||||
<TableRow key={server.id} className="hover:bg-muted/50">
|
||||
<TableCell className="font-medium">
|
||||
<div className="truncate max-w-[140px]" title={server.name}>
|
||||
{server.name}
|
||||
return (
|
||||
<TableRow key={server.id} className="hover:bg-muted/50">
|
||||
<TableCell className="font-medium">
|
||||
<div className="truncate" title={server.name}>
|
||||
{server.name}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ServerStatusBadge status={server.status} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<OSTypeIcon osType={server.os_type} />
|
||||
<span className="text-sm truncate" title={server.os_type}>
|
||||
{server.os_type}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-sm bg-muted px-1 py-0.5 rounded text-xs">
|
||||
{server.ip_address}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>{cpuUsage.toFixed(1)}%</span>
|
||||
<span className="text-muted-foreground text-xs">{server.cpu_cores} cores</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ServerStatusBadge status={server.status} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<OSTypeIcon osType={server.os_type} />
|
||||
<span className="text-sm truncate max-w-[60px]" title={server.os_type}>
|
||||
{server.os_type}
|
||||
</span>
|
||||
<Progress
|
||||
value={cpuUsage}
|
||||
className="h-2"
|
||||
indicatorClassName={
|
||||
cpuUsage > 90 ? "bg-red-500" :
|
||||
cpuUsage > 75 ? "bg-orange-500" :
|
||||
cpuUsage > 60 ? "bg-yellow-500" : "bg-green-500"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">{memoryUsage.toFixed(1)}%</span>
|
||||
<span className="text-xs">{serverService.formatBytes(server.ram_total)}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-sm bg-muted px-1 py-0.5 rounded text-xs">
|
||||
{server.ip_address}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1 min-w-[120px]">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>{cpuUsage.toFixed(1)}%</span>
|
||||
<span className="text-muted-foreground text-xs">{server.cpu_cores} cores</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={cpuUsage}
|
||||
className="h-2"
|
||||
indicatorClassName={
|
||||
cpuUsage > 90 ? "bg-red-500" :
|
||||
cpuUsage > 75 ? "bg-orange-500" :
|
||||
cpuUsage > 60 ? "bg-yellow-500" : "bg-green-500"
|
||||
}
|
||||
/>
|
||||
<Progress
|
||||
value={memoryUsage}
|
||||
className="h-2"
|
||||
indicatorClassName={
|
||||
memoryUsage > 90 ? "bg-red-500" :
|
||||
memoryUsage > 75 ? "bg-yellow-500" : "bg-blue-500"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">{diskUsage.toFixed(1)}%</span>
|
||||
<span className="text-xs">{serverService.formatBytes(server.disk_total)}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1 min-w-[120px]">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">{memoryUsage.toFixed(1)}%</span>
|
||||
<span className="text-xs">{serverService.formatBytes(server.ram_total)}</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={memoryUsage}
|
||||
className="h-2"
|
||||
indicatorClassName={
|
||||
memoryUsage > 90 ? "bg-red-500" :
|
||||
memoryUsage > 75 ? "bg-yellow-500" : "bg-blue-500"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1 min-w-[120px]">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">{diskUsage.toFixed(1)}%</span>
|
||||
<span className="text-xs">{serverService.formatBytes(server.disk_total)}</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={diskUsage}
|
||||
className="h-2"
|
||||
indicatorClassName={
|
||||
diskUsage > 95 ? "bg-red-500" :
|
||||
diskUsage > 85 ? "bg-yellow-500" : "bg-orange-500"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm truncate max-w-[80px]" title={server.uptime}>
|
||||
{server.uptime}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm text-muted-foreground text-xs">
|
||||
{new Date(server.last_checked).toLocaleString()}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0" disabled={isProcessing}>
|
||||
<span className="sr-only">Open menu</span>
|
||||
{isProcessing ? (
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[200px]">
|
||||
<DropdownMenuItem onClick={() => handleViewDetails(server.id)}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
View Server Detail
|
||||
</DropdownMenuItem>
|
||||
{server.docker === 'true' && (
|
||||
<DropdownMenuItem onClick={() => handleViewContainers(server.id)}>
|
||||
<Activity className="mr-2 h-4 w-4" />
|
||||
Container Monitoring
|
||||
</DropdownMenuItem>
|
||||
<Progress
|
||||
value={diskUsage}
|
||||
className="h-2"
|
||||
indicatorClassName={
|
||||
diskUsage > 95 ? "bg-red-500" :
|
||||
diskUsage > 85 ? "bg-yellow-500" : "bg-orange-500"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm truncate" title={server.uptime}>
|
||||
{server.uptime}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm text-muted-foreground text-xs">
|
||||
{new Date(server.last_checked).toLocaleString()}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0" disabled={isProcessing}>
|
||||
<span className="sr-only">Open menu</span>
|
||||
{isProcessing ? (
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => handlePauseResume(server)}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{isPaused ? (
|
||||
<>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Resume Monitoring
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Pause className="mr-2 h-4 w-4" />
|
||||
Pause Monitoring
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[200px]">
|
||||
<DropdownMenuItem onClick={() => handleViewDetails(server.id)}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
View Server Detail
|
||||
</DropdownMenuItem>
|
||||
{server.docker === 'true' && (
|
||||
<DropdownMenuItem onClick={() => handleViewContainers(server.id)}>
|
||||
<Activity className="mr-2 h-4 w-4" />
|
||||
Container Monitoring
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => handleEdit(server.id)}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit Server
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(server)}
|
||||
className="text-red-600 focus:text-red-600"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Server
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => handlePauseResume(server)}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{isPaused ? (
|
||||
<>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Resume Monitoring
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Pause className="mr-2 h-4 w-4" />
|
||||
Pause Monitoring
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => handleEdit(server)}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit Server
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(server)}
|
||||
className="text-red-600 focus:text-red-600"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Server
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Edit Server Dialog */}
|
||||
<EditServerDialog
|
||||
server={selectedServer}
|
||||
open={editDialogOpen}
|
||||
onOpenChange={setEditDialogOpen}
|
||||
onServerUpdated={onRefresh}
|
||||
/>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid } from "recharts";
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts";
|
||||
import { Cpu } from "lucide-react";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { DetailedTooltipContent } from "./tooltips/DetailedTooltipContent";
|
||||
@@ -18,65 +18,62 @@ export const CPUChart = ({ data, latestData }: CPUChartProps) => {
|
||||
const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280';
|
||||
|
||||
return (
|
||||
<Card className="bg-gradient-to-br from-background to-muted/20 border-border/50 shadow-lg hover:shadow-xl transition-all duration-300 backdrop-blur-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<Card className="bg-gradient-to-br from-background to-muted/20 border-border/50 shadow-lg hover:shadow-xl transition-all duration-300 backdrop-blur-sm h-full flex flex-col">
|
||||
<CardHeader className="pb-2 flex-shrink-0">
|
||||
<CardTitle className="flex items-center justify-between text-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-2 rounded-lg bg-blue-500/15">
|
||||
<Cpu className="h-5 w-5 text-blue-500" />
|
||||
<Cpu className="h-4 w-4 lg:h-5 lg:w-5 text-blue-500" />
|
||||
</div>
|
||||
CPU Usage
|
||||
<span className="text-sm lg:text-base">CPU Usage</span>
|
||||
</div>
|
||||
{latestData && (
|
||||
<div className="text-right text-sm">
|
||||
<div className="text-right text-xs lg:text-sm">
|
||||
<div className="text-blue-500 font-semibold">{latestData.cpuUsage}%</div>
|
||||
<div className="text-xs text-muted-foreground">{latestData.cpuCores} cores</div>
|
||||
</div>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-2">
|
||||
<ChartContainer config={{
|
||||
cpuUsage: {
|
||||
label: "CPU Usage (%)",
|
||||
color: theme === 'dark' ? "#3b82f6" : "#2563eb",
|
||||
}
|
||||
}} className="h-80">
|
||||
<AreaChart data={data}>
|
||||
<defs>
|
||||
<linearGradient id="cpuGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={theme === 'dark' ? "#3b82f6" : "#2563eb"} stopOpacity={0.4}/>
|
||||
<stop offset="95%" stopColor={theme === 'dark' ? "#1e40af" : "#1d4ed8"} stopOpacity={0.1}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} opacity={0.3} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
domain={[0, 100]}
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
label={{ value: 'CPU %', angle: -90, position: 'insideLeft' }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<DetailedTooltipContent />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="cpuUsage"
|
||||
stroke={theme === 'dark' ? "#60a5fa" : "#2563eb"}
|
||||
strokeWidth={3}
|
||||
dot={false}
|
||||
fill="url(#cpuGradient)"
|
||||
fillOpacity={1}
|
||||
name="CPU Usage (%)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
<CardContent className="pt-2 flex-1 min-h-0">
|
||||
<div className="w-full h-full min-h-[240px] lg:min-h-[320px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={data} margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
|
||||
<defs>
|
||||
<linearGradient id="cpuGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={theme === 'dark' ? "#3b82f6" : "#2563eb"} stopOpacity={0.4}/>
|
||||
<stop offset="95%" stopColor={theme === 'dark' ? "#1e40af" : "#1d4ed8"} stopOpacity={0.1}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} opacity={0.3} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
domain={[0, 100]}
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
label={{ value: 'CPU %', angle: -90, position: 'insideLeft' }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<DetailedTooltipContent />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="cpuUsage"
|
||||
stroke={theme === 'dark' ? "#60a5fa" : "#2563eb"}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
fill="url(#cpuGradient)"
|
||||
fillOpacity={1}
|
||||
name="CPU Usage (%)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid } from "recharts";
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts";
|
||||
import { HardDrive } from "lucide-react";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { DetailedTooltipContent } from "./tooltips/DetailedTooltipContent";
|
||||
@@ -18,65 +18,62 @@ export const DiskChart = ({ data, latestData }: DiskChartProps) => {
|
||||
const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280';
|
||||
|
||||
return (
|
||||
<Card className="bg-gradient-to-br from-background to-muted/20 border-border/50 shadow-lg hover:shadow-xl transition-all duration-300 backdrop-blur-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<Card className="bg-gradient-to-br from-background to-muted/20 border-border/50 shadow-lg hover:shadow-xl transition-all duration-300 backdrop-blur-sm h-full flex flex-col">
|
||||
<CardHeader className="pb-2 flex-shrink-0">
|
||||
<CardTitle className="flex items-center justify-between text-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-2 rounded-lg bg-amber-500/15">
|
||||
<HardDrive className="h-5 w-5 text-amber-500" />
|
||||
<HardDrive className="h-4 w-4 lg:h-5 lg:w-5 text-amber-500" />
|
||||
</div>
|
||||
Disk Usage
|
||||
<span className="text-sm lg:text-base">Disk Usage</span>
|
||||
</div>
|
||||
{latestData && (
|
||||
<div className="text-right text-sm">
|
||||
<div className="text-right text-xs lg:text-sm">
|
||||
<div className="text-amber-500 font-semibold">{latestData.diskUsagePercent}%</div>
|
||||
<div className="text-xs text-muted-foreground">{latestData.diskUsed} / {latestData.diskTotal}</div>
|
||||
</div>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-2">
|
||||
<ChartContainer config={{
|
||||
diskUsagePercent: {
|
||||
label: "Disk Usage (%)",
|
||||
color: theme === 'dark' ? "#f59e0b" : "#d97706",
|
||||
}
|
||||
}} className="h-80">
|
||||
<AreaChart data={data}>
|
||||
<defs>
|
||||
<linearGradient id="diskGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={theme === 'dark' ? "#f59e0b" : "#d97706"} stopOpacity={0.6}/>
|
||||
<stop offset="95%" stopColor={theme === 'dark' ? "#d97706" : "#b45309"} stopOpacity={0.1}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} opacity={0.3} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
domain={[0, 100]}
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
label={{ value: 'Disk %', angle: -90, position: 'insideLeft' }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<DetailedTooltipContent />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Area
|
||||
type="step"
|
||||
dataKey="diskUsagePercent"
|
||||
stroke={theme === 'dark' ? "#fbbf24" : "#d97706"}
|
||||
strokeWidth={3}
|
||||
dot={false}
|
||||
fill="url(#diskGradient)"
|
||||
fillOpacity={1}
|
||||
name="Disk Usage (%)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
<CardContent className="pt-2 flex-1 min-h-0">
|
||||
<div className="w-full h-full min-h-[240px] lg:min-h-[320px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={data} margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
|
||||
<defs>
|
||||
<linearGradient id="diskGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={theme === 'dark' ? "#f59e0b" : "#d97706"} stopOpacity={0.6}/>
|
||||
<stop offset="95%" stopColor={theme === 'dark' ? "#d97706" : "#b45309"} stopOpacity={0.1}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} opacity={0.3} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
domain={[0, 100]}
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
label={{ value: 'Disk %', angle: -90, position: 'insideLeft' }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<DetailedTooltipContent />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Area
|
||||
type="step"
|
||||
dataKey="diskUsagePercent"
|
||||
stroke={theme === 'dark' ? "#fbbf24" : "#d97706"}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
fill="url(#diskGradient)"
|
||||
fillOpacity={1}
|
||||
name="Disk Usage (%)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid } from "recharts";
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts";
|
||||
import { MemoryStick } from "lucide-react";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { DetailedTooltipContent } from "./tooltips/DetailedTooltipContent";
|
||||
@@ -18,65 +18,62 @@ export const MemoryChart = ({ data, latestData }: MemoryChartProps) => {
|
||||
const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280';
|
||||
|
||||
return (
|
||||
<Card className="bg-gradient-to-br from-background to-muted/20 border-border/50 shadow-lg hover:shadow-xl transition-all duration-300 backdrop-blur-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<Card className="bg-gradient-to-br from-background to-muted/20 border-border/50 shadow-lg hover:shadow-xl transition-all duration-300 backdrop-blur-sm h-full flex flex-col">
|
||||
<CardHeader className="pb-2 flex-shrink-0">
|
||||
<CardTitle className="flex items-center justify-between text-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-2 rounded-lg bg-green-500/15">
|
||||
<MemoryStick className="h-5 w-5 text-green-500" />
|
||||
<MemoryStick className="h-4 w-4 lg:h-5 lg:w-5 text-green-500" />
|
||||
</div>
|
||||
Memory Usage
|
||||
<span className="text-sm lg:text-base">Memory Usage</span>
|
||||
</div>
|
||||
{latestData && (
|
||||
<div className="text-right text-sm">
|
||||
<div className="text-right text-xs lg:text-sm">
|
||||
<div className="text-green-500 font-semibold">{latestData.ramUsagePercent}%</div>
|
||||
<div className="text-xs text-muted-foreground">{latestData.ramUsed} / {latestData.ramTotal}</div>
|
||||
</div>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-2">
|
||||
<ChartContainer config={{
|
||||
ramUsagePercent: {
|
||||
label: "Memory Usage (%)",
|
||||
color: theme === 'dark' ? "#10b981" : "#059669",
|
||||
}
|
||||
}} className="h-80">
|
||||
<AreaChart data={data}>
|
||||
<defs>
|
||||
<linearGradient id="memoryGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={theme === 'dark' ? "#10b981" : "#059669"} stopOpacity={0.5}/>
|
||||
<stop offset="95%" stopColor={theme === 'dark' ? "#047857" : "#065f46"} stopOpacity={0.1}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} opacity={0.3} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
domain={[0, 100]}
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
label={{ value: 'Memory %', angle: -90, position: 'insideLeft' }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<DetailedTooltipContent />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Area
|
||||
type="basis"
|
||||
dataKey="ramUsagePercent"
|
||||
stroke={theme === 'dark' ? "#34d399" : "#059669"}
|
||||
strokeWidth={3}
|
||||
dot={false}
|
||||
fill="url(#memoryGradient)"
|
||||
fillOpacity={1}
|
||||
name="Memory Usage (%)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
<CardContent className="pt-2 flex-1 min-h-0">
|
||||
<div className="w-full h-full min-h-[240px] lg:min-h-[320px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={data} margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
|
||||
<defs>
|
||||
<linearGradient id="memoryGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={theme === 'dark' ? "#10b981" : "#059669"} stopOpacity={0.5}/>
|
||||
<stop offset="95%" stopColor={theme === 'dark' ? "#047857" : "#065f46"} stopOpacity={0.1}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} opacity={0.3} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
domain={[0, 100]}
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
label={{ value: 'Memory %', angle: -90, position: 'insideLeft' }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<DetailedTooltipContent />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Area
|
||||
type="basis"
|
||||
dataKey="ramUsagePercent"
|
||||
stroke={theme === 'dark' ? "#34d399" : "#059669"}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
fill="url(#memoryGradient)"
|
||||
fillOpacity={1}
|
||||
name="Memory Usage (%)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid } from "recharts";
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts";
|
||||
import { Wifi } from "lucide-react";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { NetworkTooltipContent } from "./tooltips/NetworkTooltipContent";
|
||||
@@ -18,84 +18,77 @@ export const NetworkChart = ({ data, latestData }: NetworkChartProps) => {
|
||||
const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280';
|
||||
|
||||
return (
|
||||
<Card className="bg-gradient-to-br from-background to-muted/20 border-border/50 shadow-lg hover:shadow-xl transition-all duration-300 backdrop-blur-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<Card className="bg-gradient-to-br from-background to-muted/20 border-border/50 shadow-lg hover:shadow-xl transition-all duration-300 backdrop-blur-sm h-full flex flex-col">
|
||||
<CardHeader className="pb-2 flex-shrink-0">
|
||||
<CardTitle className="flex items-center justify-between text-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-2 rounded-lg bg-purple-500/15">
|
||||
<Wifi className="h-5 w-5 text-purple-500" />
|
||||
<Wifi className="h-4 w-4 lg:h-5 lg:w-5 text-purple-500" />
|
||||
</div>
|
||||
Network Traffic
|
||||
<span className="text-sm lg:text-base">Network Traffic</span>
|
||||
</div>
|
||||
{latestData && (
|
||||
<div className="text-right text-sm">
|
||||
<div className="text-right text-xs lg:text-sm">
|
||||
<div className="text-purple-500 font-semibold">{latestData.networkRxSpeed} KB/s ↓</div>
|
||||
<div className="text-red-500 font-semibold">{latestData.networkTxSpeed} KB/s ↑</div>
|
||||
</div>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-2">
|
||||
<ChartContainer config={{
|
||||
networkRxSpeed: {
|
||||
label: "RX Speed",
|
||||
color: theme === 'dark' ? "#8b5cf6" : "#7c3aed",
|
||||
},
|
||||
networkTxSpeed: {
|
||||
label: "TX Speed",
|
||||
color: theme === 'dark' ? "#ef4444" : "#dc2626",
|
||||
}
|
||||
}} className="h-80">
|
||||
<LineChart data={data}>
|
||||
<defs>
|
||||
<linearGradient id="networkGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={theme === 'dark' ? "#8b5cf6" : "#7c3aed"} stopOpacity={0.2}/>
|
||||
<stop offset="95%" stopColor={theme === 'dark' ? "#7c3aed" : "#6d28d9"} stopOpacity={0.05}/>
|
||||
</linearGradient>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} opacity={0.3} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
label={{ value: 'Network Speed (KB/s)', angle: -90, position: 'insideLeft' }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<NetworkTooltipContent />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="networkRxSpeed"
|
||||
stroke={theme === 'dark' ? "#a78bfa" : "#7c3aed"}
|
||||
strokeWidth={3}
|
||||
dot={false}
|
||||
name="RX Speed"
|
||||
filter="url(#glow)"
|
||||
strokeDasharray="0"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="networkTxSpeed"
|
||||
stroke={theme === 'dark' ? "#f87171" : "#dc2626"}
|
||||
strokeWidth={3}
|
||||
dot={false}
|
||||
name="TX Speed"
|
||||
strokeDasharray="5 5"
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
<CardContent className="pt-2 flex-1 min-h-0">
|
||||
<div className="w-full h-full min-h-[240px] lg:min-h-[320px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={data} margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
|
||||
<defs>
|
||||
<linearGradient id="networkGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={theme === 'dark' ? "#8b5cf6" : "#7c3aed"} stopOpacity={0.2}/>
|
||||
<stop offset="95%" stopColor={theme === 'dark' ? "#7c3aed" : "#6d28d9"} stopOpacity={0.05}/>
|
||||
</linearGradient>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} opacity={0.3} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 10, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
label={{ value: 'Network Speed (KB/s)', angle: -90, position: 'insideLeft' }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<NetworkTooltipContent />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="networkRxSpeed"
|
||||
stroke={theme === 'dark' ? "#a78bfa" : "#7c3aed"}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
name="RX Speed"
|
||||
filter="url(#glow)"
|
||||
strokeDasharray="0"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="networkTxSpeed"
|
||||
stroke={theme === 'dark' ? "#f87171" : "#dc2626"}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
name="TX Speed"
|
||||
strokeDasharray="5 5"
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export const formatBytes = (bytes: number, decimals = 2) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
@@ -49,34 +48,82 @@ export const timeRangeOptions = [
|
||||
{ value: '3m' as TimeRange, label: 'Last 90 days', hours: 24 * 90 },
|
||||
];
|
||||
|
||||
// Optimized time range filtering with early returns
|
||||
export const filterMetricsByTimeRange = (metrics: any[], timeRange: TimeRange): any[] => {
|
||||
if (!metrics?.length) return [];
|
||||
if (!metrics?.length) {
|
||||
console.log('🔍 filterMetricsByTimeRange: No metrics provided');
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('🔍 filterMetricsByTimeRange: Starting with', metrics.length, 'metrics for', timeRange);
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// For 60m, let's be very specific about what we're looking for
|
||||
if (timeRange === '60m') {
|
||||
console.log('⏰ 60m filter: Current time:', now.toISOString());
|
||||
|
||||
// Try exact 60 minutes first
|
||||
const cutoffTime60m = new Date(now.getTime() - (60 * 60 * 1000));
|
||||
console.log('⏰ 60m filter: Cutoff time:', cutoffTime60m.toISOString());
|
||||
|
||||
const filtered60m = metrics.filter(metric => {
|
||||
const metricTime = new Date(metric.created || metric.timestamp);
|
||||
const isWithinRange = metricTime >= cutoffTime60m && metricTime <= now;
|
||||
|
||||
if (!isWithinRange) {
|
||||
const ageMinutes = Math.round((now.getTime() - metricTime.getTime()) / (1000 * 60));
|
||||
console.log('⏰ Excluding record:', {
|
||||
created: metric.created || metric.timestamp,
|
||||
ageMinutes: ageMinutes,
|
||||
reason: ageMinutes > 60 ? 'too old' : 'future date'
|
||||
});
|
||||
}
|
||||
|
||||
return isWithinRange;
|
||||
});
|
||||
|
||||
console.log('✅ 60m strict filter result:', filtered60m.length, 'records');
|
||||
|
||||
if (filtered60m.length > 0) {
|
||||
return filtered60m.sort((a, b) =>
|
||||
new Date(a.created || a.timestamp).getTime() - new Date(b.created || b.timestamp).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
// If no data in exactly 60m, show what we have and return it anyway for debugging
|
||||
console.log('⚠️ No data in 60m range, showing all available data ages:');
|
||||
metrics.forEach((metric, index) => {
|
||||
const metricTime = new Date(metric.created || metric.timestamp);
|
||||
const ageMinutes = Math.round((now.getTime() - metricTime.getTime()) / (1000 * 60));
|
||||
console.log(`Record ${index}: ${metric.created || metric.timestamp} (${ageMinutes} minutes ago)`);
|
||||
});
|
||||
|
||||
// Return the most recent data regardless of age
|
||||
const sorted = metrics.sort((a, b) =>
|
||||
new Date(b.created || b.timestamp).getTime() - new Date(a.created || a.timestamp).getTime()
|
||||
);
|
||||
console.log('🔄 Returning most recent available data for 60m view');
|
||||
return sorted.slice(0, 20); // Show last 20 records
|
||||
}
|
||||
|
||||
// For other time ranges, use normal filtering
|
||||
const selectedRange = timeRangeOptions.find(opt => opt.value === timeRange);
|
||||
if (!selectedRange) return metrics;
|
||||
|
||||
// Add small buffer to avoid edge cases
|
||||
const bufferMinutes = timeRange === '60m' ? 5 : timeRange === '1d' ? 30 : 60;
|
||||
const cutoffTime = new Date(now.getTime() - (selectedRange.hours * 60 * 60 * 1000) - (bufferMinutes * 60 * 1000));
|
||||
|
||||
// console.log('filterMetricsByTimeRange: timeRange:', timeRange, 'cutoffTime:', cutoffTime.toISOString());
|
||||
const cutoffTime = new Date(now.getTime() - (selectedRange.hours * 60 * 60 * 1000));
|
||||
|
||||
const filtered = metrics.filter(metric => {
|
||||
const metricTime = new Date(metric.created || metric.timestamp);
|
||||
return metricTime >= cutoffTime && metricTime <= now;
|
||||
});
|
||||
|
||||
// console.log('filterMetricsByTimeRange: Filtered', metrics.length, 'to', filtered.length, 'metrics');
|
||||
console.log('✅ Filtered', metrics.length, 'to', filtered.length, 'metrics for', timeRange);
|
||||
|
||||
// Sort by timestamp for proper chart display
|
||||
return filtered.sort((a, b) =>
|
||||
new Date(a.created || a.timestamp).getTime() - new Date(b.created || b.timestamp).getTime()
|
||||
);
|
||||
};
|
||||
|
||||
// Optimized timestamp formatting with caching
|
||||
const timestampCache = new Map<string, string>();
|
||||
|
||||
const formatTimestamp = (timestamp: string, timeRange: TimeRange): string => {
|
||||
@@ -118,7 +165,6 @@ const formatTimestamp = (timestamp: string, timeRange: TimeRange): string => {
|
||||
});
|
||||
}
|
||||
|
||||
// Cache the result (limit cache size)
|
||||
if (timestampCache.size > 1000) {
|
||||
timestampCache.clear();
|
||||
}
|
||||
@@ -127,37 +173,49 @@ const formatTimestamp = (timestamp: string, timeRange: TimeRange): string => {
|
||||
return formatted;
|
||||
};
|
||||
|
||||
// Optimized chart data formatting with better performance
|
||||
export const formatChartData = (metrics: any[], timeRange: TimeRange) => {
|
||||
// console.log('formatChartData: Input metrics count:', metrics?.length || 0, 'timeRange:', timeRange);
|
||||
console.log('📊 formatChartData: Processing', {
|
||||
inputCount: metrics?.length || 0,
|
||||
timeRange,
|
||||
sampleMetric: metrics?.[0] ? {
|
||||
id: metrics[0].id,
|
||||
created: metrics[0].created,
|
||||
timestamp: metrics[0].timestamp,
|
||||
server_id: metrics[0].server_id
|
||||
} : null
|
||||
});
|
||||
|
||||
if (!metrics?.length) return [];
|
||||
if (!metrics?.length) {
|
||||
console.log('❌ formatChartData: No metrics provided');
|
||||
return [];
|
||||
}
|
||||
|
||||
const filteredMetrics = filterMetricsByTimeRange(metrics, timeRange);
|
||||
// console.log('formatChartData: After time filtering:', filteredMetrics?.length || 0, 'metrics');
|
||||
console.log('📊 formatChartData: After time filtering:', filteredMetrics?.length || 0, 'metrics');
|
||||
|
||||
if (!filteredMetrics.length) return [];
|
||||
if (!filteredMetrics.length) {
|
||||
console.log('❌ formatChartData: No metrics after time filtering');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Dynamic sampling based on time range and data volume
|
||||
let maxDataPoints: number;
|
||||
switch (timeRange) {
|
||||
case '60m': maxDataPoints = 60; break; // 1 point per minute max
|
||||
case '1d': maxDataPoints = 144; break; // 1 point per 10 minutes max
|
||||
case '7d': maxDataPoints = 168; break; // 1 point per hour max
|
||||
case '1m': maxDataPoints = 120; break; // 1 point per 6 hours max
|
||||
case '3m': maxDataPoints = 90; break; // 1 point per day max
|
||||
case '60m': maxDataPoints = 60; break;
|
||||
case '1d': maxDataPoints = 144; break;
|
||||
case '7d': maxDataPoints = 168; break;
|
||||
case '1m': maxDataPoints = 120; break;
|
||||
case '3m': maxDataPoints = 90; break;
|
||||
default: maxDataPoints = 100;
|
||||
}
|
||||
|
||||
// Smart sampling - only sample if we have significantly more data
|
||||
const sampledMetrics = filteredMetrics.length > maxDataPoints * 1.2
|
||||
? filteredMetrics.filter((_, index) => index % Math.ceil(filteredMetrics.length / maxDataPoints) === 0)
|
||||
: filteredMetrics;
|
||||
|
||||
// console.log('formatChartData: After sampling:', sampledMetrics.length, 'metrics for display');
|
||||
console.log('📊 formatChartData: After sampling:', sampledMetrics.length, 'metrics for display');
|
||||
|
||||
// Batch process the data transformation for better performance
|
||||
return sampledMetrics.map((metric) => {
|
||||
const formattedData = sampledMetrics.map((metric) => {
|
||||
const cpuUsage = typeof metric.cpu_usage === 'string' ?
|
||||
parseFloat(metric.cpu_usage.replace('%', '')) :
|
||||
parseFloat(metric.cpu_usage) || 0;
|
||||
@@ -210,4 +268,7 @@ export const formatChartData = (metrics: any[], timeRange: TimeRange) => {
|
||||
networkTxSpeed: Math.round(networkTxSpeed * 100) / 100,
|
||||
};
|
||||
});
|
||||
|
||||
console.log('✅ formatChartData: Final formatted data count:', formattedData.length);
|
||||
return formattedData;
|
||||
};
|
||||
@@ -98,9 +98,9 @@ const InstanceMonitoring = () => {
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
<main className="flex-1 overflow-auto">
|
||||
<div className="h-full flex flex-col p-4 sm:p-6 lg:p-8 space-y-6">
|
||||
<div className="p-4 sm:p-6 lg:p-8 space-y-6">
|
||||
{/* Header Section */}
|
||||
<div className="flex-shrink-0">
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="text-2xl lg:text-3xl font-bold text-foreground">
|
||||
@@ -118,12 +118,12 @@ const InstanceMonitoring = () => {
|
||||
</div>
|
||||
|
||||
{/* Stats Cards Section */}
|
||||
<div className="flex-shrink-0">
|
||||
<div>
|
||||
<ServerStatsCards stats={stats} />
|
||||
</div>
|
||||
|
||||
{/* Server Table Section - This will take remaining space */}
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
{/* Server Table Section */}
|
||||
<div>
|
||||
<ServerTable servers={servers} isLoading={isLoading} onRefresh={handleRefresh} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@@ -10,8 +9,7 @@ import { Header } from "@/components/dashboard/Header";
|
||||
import { serverService } from "@/services/serverService";
|
||||
import { authService } from "@/services/authService";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft, Server, Database } from "lucide-react";
|
||||
import { ServerMetricsCharts } from "@/components/servers/ServerMetricsCharts";
|
||||
import { ArrowLeft, Server } from "lucide-react";
|
||||
import { ServerMetricsOverview } from "@/components/servers/ServerMetricsOverview";
|
||||
import { ServerHistoryCharts } from "@/components/servers/ServerHistoryCharts";
|
||||
import { ServerSystemInfoCard } from "@/components/servers/ServerSystemInfoCard";
|
||||
@@ -24,7 +22,7 @@ const ServerDetail = () => {
|
||||
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
||||
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser());
|
||||
|
||||
//console.log('ServerDetail component loaded with serverId:', serverId);
|
||||
console.log('ServerDetail component loaded with serverId:', serverId);
|
||||
|
||||
const {
|
||||
data: server,
|
||||
@@ -36,6 +34,70 @@ const ServerDetail = () => {
|
||||
enabled: !!serverId
|
||||
});
|
||||
|
||||
const getOSLogo = (server: any) => {
|
||||
if (!server) return null;
|
||||
|
||||
// Parse system_info if it's a string, but handle both JSON and plain text
|
||||
let systemInfo: any = {};
|
||||
let systemInfoText = '';
|
||||
|
||||
if (server.system_info) {
|
||||
if (typeof server.system_info === 'string') {
|
||||
// Try to parse as JSON first
|
||||
try {
|
||||
systemInfo = JSON.parse(server.system_info);
|
||||
} catch (error) {
|
||||
// If JSON parsing fails, treat it as plain text
|
||||
console.log('system_info is plain text:', server.system_info);
|
||||
systemInfoText = server.system_info.toLowerCase();
|
||||
}
|
||||
} else {
|
||||
systemInfo = server.system_info;
|
||||
}
|
||||
}
|
||||
|
||||
// Check system_info (both JSON and plain text), then fallback to os_type
|
||||
const osFromJson = systemInfo.OSName || '';
|
||||
const osFromText = systemInfoText;
|
||||
const osFromType = server.os_type || '';
|
||||
|
||||
// Combine all OS information for detection
|
||||
const combinedOSInfo = `${osFromJson} ${osFromText} ${osFromType}`.toLowerCase();
|
||||
|
||||
console.log('OS detection info:', { osFromJson, osFromText, osFromType, combinedOSInfo });
|
||||
|
||||
// Check for specific OS distributions first (most specific to least specific)
|
||||
if (combinedOSInfo.includes('ubuntu')) {
|
||||
return '/upload/os/ubuntu.png';
|
||||
} else if (combinedOSInfo.includes('debian')) {
|
||||
return '/upload/os/debian.png';
|
||||
} else if (combinedOSInfo.includes('centos')) {
|
||||
return '/upload/os/centos.png';
|
||||
} else if (combinedOSInfo.includes('rhel') || combinedOSInfo.includes('red hat')) {
|
||||
return '/upload/os/rhel.png';
|
||||
} else if (combinedOSInfo.includes('fedora')) {
|
||||
return '/upload/os/fedora.png';
|
||||
} else if (combinedOSInfo.includes('suse') || combinedOSInfo.includes('opensuse')) {
|
||||
return '/upload/os/suse.png';
|
||||
} else if (combinedOSInfo.includes('arch')) {
|
||||
return '/upload/os/arch.png';
|
||||
} else if (combinedOSInfo.includes('alpine')) {
|
||||
return '/upload/os/alpine.png';
|
||||
} else if (combinedOSInfo.includes('windows')) {
|
||||
return '/upload/os/windows.png';
|
||||
} else if (combinedOSInfo.includes('macos') || combinedOSInfo.includes('darwin') || combinedOSInfo.includes('mac os')) {
|
||||
return '/upload/os/macos.png';
|
||||
} else if (combinedOSInfo.includes('freebsd')) {
|
||||
return '/upload/os/freebsd.png';
|
||||
} else if (combinedOSInfo.includes('linux') || combinedOSInfo.includes('gnu')) {
|
||||
// Default to linux.png for any Linux-based system that doesn't match specific distributions
|
||||
return '/upload/os/linux.png';
|
||||
}
|
||||
|
||||
// Final fallback - if we can't determine the OS, default to linux.png
|
||||
return '/upload/os/linux.png';
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
authService.logout();
|
||||
navigate('/login');
|
||||
@@ -46,7 +108,6 @@ const ServerDetail = () => {
|
||||
};
|
||||
|
||||
if (serverError) {
|
||||
// console.error('Server detail error:', serverError);
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-background text-foreground">
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
@@ -101,7 +162,7 @@ const ServerDetail = () => {
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-background text-foreground">
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
<div className="flex flex-col flex-1">
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
@@ -126,24 +187,32 @@ const ServerDetail = () => {
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="h-8 w-8 rounded bg-primary/10 flex items-center justify-center">
|
||||
<Database className="h-4 w-4 text-primary" />
|
||||
<div className="h-12 w-12 rounded bg-primary/10 flex items-center justify-center p-2">
|
||||
{server && getOSLogo(server) ? (
|
||||
<img
|
||||
src={getOSLogo(server)}
|
||||
alt="OS Logo"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<Server className="h-6 w-6 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{server?.name || 'Server Detail'}
|
||||
</h1>
|
||||
</div>
|
||||
<p className={`text-muted-foreground mt-1 sm:mt-2 transition-all duration-300 ${sidebarCollapsed ? 'text-base sm:text-lg' : 'text-sm sm:text-base'}`}>
|
||||
<p className="text-muted-foreground mt-1 sm:mt-2 text-sm sm:text-base">
|
||||
Monitor server performance metrics and system health
|
||||
{server && (
|
||||
<span className="block text-xs text-foreground/80 mt-1">
|
||||
<span className="block text-xs text-muted-foreground/70 mt-1">
|
||||
{server.hostname} • {server.ip_address} • {server.os_type}
|
||||
</span>
|
||||
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{/* System Info Card */}
|
||||
|
||||
{/* System Info Card */}
|
||||
{server && (
|
||||
<div className="flex-shrink-0">
|
||||
<ServerSystemInfoCard server={server} />
|
||||
@@ -159,17 +228,10 @@ const ServerDetail = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Historical Charts Section */}
|
||||
{server && (
|
||||
<div className="mb-6 lg:mb-8">
|
||||
<ServerHistoryCharts serverId={server.id} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metrics Charts Section */}
|
||||
{/* Historical Charts Section - Single comprehensive section */}
|
||||
{server && (
|
||||
<div className="min-w-0">
|
||||
<ServerMetricsCharts serverId={server.id} />
|
||||
<ServerHistoryCharts serverId={server.id} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ export const serverService = {
|
||||
const records = await pb.collection('servers').getFullList<Server>();
|
||||
return records;
|
||||
} catch (error) {
|
||||
// console.error('Error fetching servers:', error);
|
||||
console.error('Error fetching servers:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
@@ -17,190 +17,151 @@ export const serverService = {
|
||||
const record = await pb.collection('servers').getOne<Server>(serverId);
|
||||
return record;
|
||||
} catch (error) {
|
||||
// console.error('Error fetching server:', error);
|
||||
console.error('Error fetching server:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getServerMetrics(serverId: string, timeRange?: string): Promise<any[]> {
|
||||
try {
|
||||
// console.log('serverService.getServerMetrics: Fetching metrics for serverId:', serverId, 'timeRange:', timeRange);
|
||||
console.log('🔍 serverService.getServerMetrics: Starting with serverId:', serverId, 'timeRange:', timeRange);
|
||||
|
||||
// First, get the server to find the correct server_id for metrics
|
||||
let server;
|
||||
try {
|
||||
server = await this.getServer(serverId);
|
||||
// console.log('serverService.getServerMetrics: Found server:', server);
|
||||
console.log('✅ serverService.getServerMetrics: Found server:', {
|
||||
id: server.id,
|
||||
server_id: server.server_id,
|
||||
name: server.name
|
||||
});
|
||||
} catch (error) {
|
||||
// console.log('serverService.getServerMetrics: Could not fetch server details:', error);
|
||||
console.log('❌ serverService.getServerMetrics: Could not fetch server details:', error);
|
||||
}
|
||||
|
||||
// Try multiple filter strategies to find data
|
||||
let filter = '';
|
||||
let metricsServerId = serverId;
|
||||
|
||||
// Strategy 1: Use server.server_id if available
|
||||
if (server && server.server_id) {
|
||||
metricsServerId = server.server_id;
|
||||
filter = `server_id = "${metricsServerId}"`;
|
||||
// console.log('serverService.getServerMetrics: Strategy 1 - Using server.server_id for metrics:', metricsServerId);
|
||||
} else {
|
||||
// Strategy 2: Use the serverId directly
|
||||
filter = `server_id = "${serverId}"`;
|
||||
// console.log('serverService.getServerMetrics: Strategy 2 - Using serverId directly for metrics:', serverId);
|
||||
}
|
||||
|
||||
// Add agent_id filter if available in server data
|
||||
if (server && server.agent_id) {
|
||||
filter += ` && agent_id = "${server.agent_id}"`;
|
||||
// console.log('serverService.getServerMetrics: Added agent_id filter:', server.agent_id);
|
||||
}
|
||||
|
||||
// Add time range filter
|
||||
if (timeRange) {
|
||||
const now = new Date();
|
||||
let cutoffTime;
|
||||
|
||||
switch (timeRange) {
|
||||
case '60m':
|
||||
cutoffTime = new Date(now.getTime() - (60 * 60 * 1000));
|
||||
break;
|
||||
case '1d':
|
||||
cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
|
||||
break;
|
||||
case '7d':
|
||||
cutoffTime = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
|
||||
break;
|
||||
case '1m':
|
||||
cutoffTime = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000));
|
||||
break;
|
||||
case '3m':
|
||||
cutoffTime = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000));
|
||||
break;
|
||||
default:
|
||||
cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
|
||||
}
|
||||
|
||||
const cutoffISO = cutoffTime.toISOString();
|
||||
filter += ` && created >= "${cutoffISO}"`;
|
||||
// console.log('serverService.getServerMetrics: Using time filter from:', cutoffISO, 'to now');
|
||||
}
|
||||
|
||||
// console.log('serverService.getServerMetrics: Final filter:', filter);
|
||||
|
||||
// Fetch filtered records with proper sorting
|
||||
let records = await pb.collection('server_metrics').getFullList({
|
||||
filter: filter,
|
||||
// Let's first check what data exists in the database for this server
|
||||
console.log('🔍 Checking all records for this server...');
|
||||
const allServerRecords = await pb.collection('server_metrics').getFullList({
|
||||
filter: `server_id = "${serverId}" || server_id = "${server?.server_id}" || server_id = "${server?.id}"`,
|
||||
sort: '-created',
|
||||
requestKey: null
|
||||
});
|
||||
|
||||
// console.log('serverService.getServerMetrics: Found', records.length, 'records with primary filter');
|
||||
|
||||
// If no records found with primary strategy, try fallback strategies
|
||||
if (records.length === 0) {
|
||||
// console.log('serverService.getServerMetrics: No records found, trying fallback strategies...');
|
||||
|
||||
// Fallback 1: Try without agent_id filter
|
||||
let fallbackFilter = `server_id = "${metricsServerId}"`;
|
||||
if (timeRange) {
|
||||
const now = new Date();
|
||||
let cutoffTime;
|
||||
|
||||
switch (timeRange) {
|
||||
case '60m':
|
||||
cutoffTime = new Date(now.getTime() - (60 * 60 * 1000));
|
||||
break;
|
||||
case '1d':
|
||||
cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
|
||||
break;
|
||||
case '7d':
|
||||
cutoffTime = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
|
||||
break;
|
||||
case '1m':
|
||||
cutoffTime = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000));
|
||||
break;
|
||||
case '3m':
|
||||
cutoffTime = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000));
|
||||
break;
|
||||
default:
|
||||
cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
|
||||
}
|
||||
|
||||
const cutoffISO = cutoffTime.toISOString();
|
||||
fallbackFilter += ` && created >= "${cutoffISO}"`;
|
||||
}
|
||||
|
||||
// console.log('serverService.getServerMetrics: Trying fallback filter without agent_id:', fallbackFilter);
|
||||
records = await pb.collection('server_metrics').getFullList({
|
||||
filter: fallbackFilter,
|
||||
sort: '-created',
|
||||
requestKey: null
|
||||
console.log('📊 Found total records for server:', allServerRecords.length);
|
||||
if (allServerRecords.length > 0) {
|
||||
console.log('📅 Date range of all records:', {
|
||||
newest: allServerRecords[0]?.created,
|
||||
oldest: allServerRecords[allServerRecords.length - 1]?.created
|
||||
});
|
||||
|
||||
// console.log('serverService.getServerMetrics: Fallback found', records.length, 'records');
|
||||
|
||||
// Fallback 2: Try with different server_id strategies
|
||||
if (records.length === 0) {
|
||||
const alternativeIds = [serverId, server?.server_id, server?.id].filter(Boolean);
|
||||
// console.log('serverService.getServerMetrics: Trying alternative server IDs:', alternativeIds);
|
||||
// Check last 5 records
|
||||
console.log('🔄 Last 5 records timestamps:', allServerRecords.slice(0, 5).map(r => ({
|
||||
created: r.created,
|
||||
age_minutes: Math.round((new Date().getTime() - new Date(r.created).getTime()) / (1000 * 60))
|
||||
})));
|
||||
}
|
||||
|
||||
// Calculate time range for filtering
|
||||
const now = new Date();
|
||||
let cutoffTime;
|
||||
|
||||
if (timeRange === '60m') {
|
||||
cutoffTime = new Date(now.getTime() - (60 * 60 * 1000)); // Exactly 60 minutes
|
||||
console.log('⏰ 60m filter: Looking for records newer than:', cutoffTime.toISOString());
|
||||
console.log('⏰ Current time:', now.toISOString());
|
||||
console.log('⏰ Time difference in minutes:', Math.round((now.getTime() - cutoffTime.getTime()) / (1000 * 60)));
|
||||
} else if (timeRange === '1d') {
|
||||
cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
|
||||
} else if (timeRange === '7d') {
|
||||
cutoffTime = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
|
||||
} else if (timeRange === '1m') {
|
||||
cutoffTime = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000));
|
||||
} else if (timeRange === '3m') {
|
||||
cutoffTime = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000));
|
||||
} else {
|
||||
cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
|
||||
}
|
||||
|
||||
// Try to get filtered records
|
||||
const searchStrategies = [
|
||||
server?.server_id,
|
||||
serverId,
|
||||
server?.id
|
||||
].filter(Boolean);
|
||||
|
||||
let filteredRecords: any[] = [];
|
||||
|
||||
for (const strategy of searchStrategies) {
|
||||
try {
|
||||
const cutoffISO = cutoffTime.toISOString();
|
||||
const filter = `server_id = "${strategy}" && created >= "${cutoffISO}"`;
|
||||
|
||||
for (const altId of alternativeIds) {
|
||||
if (altId && altId !== metricsServerId) {
|
||||
let altFilter = `server_id = "${altId}"`;
|
||||
if (timeRange) {
|
||||
const now = new Date();
|
||||
let cutoffTime;
|
||||
|
||||
switch (timeRange) {
|
||||
case '60m':
|
||||
cutoffTime = new Date(now.getTime() - (60 * 60 * 1000));
|
||||
break;
|
||||
case '1d':
|
||||
cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
|
||||
break;
|
||||
case '7d':
|
||||
cutoffTime = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
|
||||
break;
|
||||
case '1m':
|
||||
cutoffTime = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000));
|
||||
break;
|
||||
case '3m':
|
||||
cutoffTime = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000));
|
||||
break;
|
||||
default:
|
||||
cutoffTime = new Date(now.getTime() - (24 * 60 * 60 * 1000));
|
||||
}
|
||||
|
||||
const cutoffISO = cutoffTime.toISOString();
|
||||
altFilter += ` && created >= "${cutoffISO}"`;
|
||||
}
|
||||
console.log(`🔍 Trying filter: ${filter}`);
|
||||
|
||||
const records = await pb.collection('server_metrics').getFullList({
|
||||
filter: filter,
|
||||
sort: '-created',
|
||||
requestKey: null
|
||||
});
|
||||
|
||||
console.log(`📊 Strategy "${strategy}" found ${records.length} records within time range`);
|
||||
|
||||
if (records.length > 0) {
|
||||
filteredRecords = records;
|
||||
console.log('✅ Using records from strategy:', strategy);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error with strategy ${strategy}:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If no filtered records found and it's 60m, let's see what we have in a larger window
|
||||
if (filteredRecords.length === 0 && timeRange === '60m') {
|
||||
console.log('⚠️ No records found in 60m window, checking last 24 hours...');
|
||||
|
||||
const last24h = new Date(now.getTime() - (24 * 60 * 60 * 1000));
|
||||
|
||||
for (const strategy of searchStrategies) {
|
||||
try {
|
||||
const filter = `server_id = "${strategy}" && created >= "${last24h.toISOString()}"`;
|
||||
|
||||
const records = await pb.collection('server_metrics').getFullList({
|
||||
filter: filter,
|
||||
sort: '-created',
|
||||
requestKey: null
|
||||
});
|
||||
|
||||
console.log(`📊 Last 24h check for "${strategy}": ${records.length} records`);
|
||||
|
||||
if (records.length > 0) {
|
||||
console.log('📅 Sample record ages (minutes ago):', records.slice(0, 3).map(r =>
|
||||
Math.round((now.getTime() - new Date(r.created).getTime()) / (1000 * 60))
|
||||
));
|
||||
|
||||
// console.log('serverService.getServerMetrics: Trying alternative ID filter:', altFilter);
|
||||
const altRecords = await pb.collection('server_metrics').getFullList({
|
||||
filter: altFilter,
|
||||
sort: '-created',
|
||||
requestKey: null
|
||||
});
|
||||
|
||||
if (altRecords.length > 0) {
|
||||
// console.log('serverService.getServerMetrics: Alternative ID found', altRecords.length, 'records');
|
||||
records = altRecords;
|
||||
break;
|
||||
}
|
||||
// Return all recent records for 60m if we have any
|
||||
filteredRecords = records;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error with 24h fallback for ${strategy}:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('serverService.getServerMetrics: Final result:', records.length, 'records found');
|
||||
if (records.length > 0) {
|
||||
// console.log('serverService.getServerMetrics: Sample record:', records[0]);
|
||||
console.log('🎯 Final result:', filteredRecords.length, 'records found for', timeRange);
|
||||
if (filteredRecords.length > 0) {
|
||||
console.log('📅 Returned records age range (minutes ago):', {
|
||||
newest: Math.round((now.getTime() - new Date(filteredRecords[0].created).getTime()) / (1000 * 60)),
|
||||
oldest: Math.round((now.getTime() - new Date(filteredRecords[filteredRecords.length - 1].created).getTime()) / (1000 * 60))
|
||||
});
|
||||
}
|
||||
|
||||
return records;
|
||||
return filteredRecords;
|
||||
} catch (error) {
|
||||
// console.error('Error fetching server metrics:', error);
|
||||
console.error('❌ Error fetching server metrics:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
|
||||
export interface ServerThreshold {
|
||||
id: string;
|
||||
name: string;
|
||||
cpu_threshold: number;
|
||||
ram_threshold: number;
|
||||
disk_threshold: number;
|
||||
network_threshold: number;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface CreateUpdateServerThresholdData {
|
||||
name: string;
|
||||
cpu_threshold: number;
|
||||
ram_threshold: number;
|
||||
disk_threshold: number;
|
||||
network_threshold: number;
|
||||
}
|
||||
|
||||
export const serverThresholdService = {
|
||||
async getServerThresholds(): Promise<ServerThreshold[]> {
|
||||
try {
|
||||
console.log("Fetching server threshold templates");
|
||||
const response = await pb.collection('server_threshold_templates').getList(1, 50, {
|
||||
sort: '-created',
|
||||
});
|
||||
console.log("Server threshold templates response:", response);
|
||||
return response.items as unknown as ServerThreshold[];
|
||||
} catch (error) {
|
||||
console.error("Error fetching server threshold templates:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getServerThreshold(id: string): Promise<ServerThreshold> {
|
||||
try {
|
||||
console.log(`Fetching server threshold template with id: ${id}`);
|
||||
const response = await pb.collection('server_threshold_templates').getOne(id);
|
||||
console.log("Server threshold template response:", response);
|
||||
return response as unknown as ServerThreshold;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching server threshold template with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async createServerThreshold(data: CreateUpdateServerThresholdData): Promise<ServerThreshold> {
|
||||
try {
|
||||
console.log("Creating new server threshold template with data:", data);
|
||||
const response = await pb.collection('server_threshold_templates').create(data);
|
||||
console.log("Create server threshold template response:", response);
|
||||
return response as unknown as ServerThreshold;
|
||||
} catch (error) {
|
||||
console.error("Error creating server threshold template:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async updateServerThreshold(id: string, data: Partial<CreateUpdateServerThresholdData>): Promise<ServerThreshold> {
|
||||
try {
|
||||
console.log(`Updating server threshold template with id: ${id}`, data);
|
||||
const response = await pb.collection('server_threshold_templates').update(id, data);
|
||||
console.log("Update server threshold template response:", response);
|
||||
return response as unknown as ServerThreshold;
|
||||
} catch (error) {
|
||||
console.error(`Error updating server threshold template with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteServerThreshold(id: string): Promise<boolean> {
|
||||
try {
|
||||
console.log(`Deleting server threshold template with id: ${id}`);
|
||||
await pb.collection('server_threshold_templates').delete(id);
|
||||
console.log("Server threshold template deleted successfully");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error deleting server threshold template with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
|
||||
export interface NotificationTemplate {
|
||||
@@ -37,62 +38,62 @@ export interface CreateUpdateTemplateData {
|
||||
export const templateService = {
|
||||
async getTemplates(): Promise<NotificationTemplate[]> {
|
||||
try {
|
||||
console.log("Fetching notification templates");
|
||||
const response = await pb.collection('notification_templates').getList(1, 50, {
|
||||
console.log("Fetching server notification templates");
|
||||
const response = await pb.collection('server_notification_templates').getList(1, 50, {
|
||||
sort: '-created',
|
||||
});
|
||||
console.log("Templates response:", response);
|
||||
console.log("Server templates response:", response);
|
||||
return response.items as unknown as NotificationTemplate[];
|
||||
} catch (error) {
|
||||
console.error("Error fetching templates:", error);
|
||||
console.error("Error fetching server templates:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getTemplate(id: string): Promise<NotificationTemplate> {
|
||||
try {
|
||||
console.log(`Fetching template with id: ${id}`);
|
||||
const response = await pb.collection('notification_templates').getOne(id);
|
||||
console.log("Template response:", response);
|
||||
console.log(`Fetching server template with id: ${id}`);
|
||||
const response = await pb.collection('server_notification_templates').getOne(id);
|
||||
console.log("Server template response:", response);
|
||||
return response as unknown as NotificationTemplate;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching template with id ${id}:`, error);
|
||||
console.error(`Error fetching server template with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async createTemplate(data: CreateUpdateTemplateData): Promise<NotificationTemplate> {
|
||||
try {
|
||||
console.log("Creating new template with data:", data);
|
||||
const response = await pb.collection('notification_templates').create(data);
|
||||
console.log("Create template response:", response);
|
||||
console.log("Creating new server template with data:", data);
|
||||
const response = await pb.collection('server_notification_templates').create(data);
|
||||
console.log("Create server template response:", response);
|
||||
return response as unknown as NotificationTemplate;
|
||||
} catch (error) {
|
||||
console.error("Error creating template:", error);
|
||||
console.error("Error creating server template:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async updateTemplate(id: string, data: Partial<CreateUpdateTemplateData>): Promise<NotificationTemplate> {
|
||||
try {
|
||||
console.log(`Updating template with id: ${id}`, data);
|
||||
const response = await pb.collection('notification_templates').update(id, data);
|
||||
console.log("Update template response:", response);
|
||||
console.log(`Updating server template with id: ${id}`, data);
|
||||
const response = await pb.collection('server_notification_templates').update(id, data);
|
||||
console.log("Update server template response:", response);
|
||||
return response as unknown as NotificationTemplate;
|
||||
} catch (error) {
|
||||
console.error(`Error updating template with id ${id}:`, error);
|
||||
console.error(`Error updating server template with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteTemplate(id: string): Promise<boolean> {
|
||||
try {
|
||||
console.log(`Deleting template with id: ${id}`);
|
||||
await pb.collection('notification_templates').delete(id);
|
||||
console.log("Template deleted successfully");
|
||||
console.log(`Deleting server template with id: ${id}`);
|
||||
await pb.collection('server_notification_templates').delete(id);
|
||||
console.log("Server template deleted successfully");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error deleting template with id ${id}:`, error);
|
||||
console.error(`Error deleting server template with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface Server {
|
||||
last_checked: string;
|
||||
server_token: string;
|
||||
template_id: string;
|
||||
threshold_id: string;
|
||||
notification_id: string;
|
||||
timestamp: string;
|
||||
connection: string;
|
||||
|
||||
Reference in New Issue
Block a user