Refactor: Split ServerHistory and Charts into smaller components

This commit is contained in:
Tola Leng
2025-07-06 15:12:03 +07:00
parent f0eec90e75
commit 5d2b8b2ed2
16 changed files with 662 additions and 1077 deletions
@@ -1,163 +1,37 @@
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
import { LineChart, Line, AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts";
import { serverService } from "@/services/serverService";
import { Loader2, TrendingUp, Cpu, HardDrive, Wifi, MemoryStick } from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
import { useMemo } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Loader2, TrendingUp } from "lucide-react";
import { TimeRangeSelector } from "./charts/TimeRangeSelector";
import { useState } from "react";
import { formatChartData, filterMetricsByTimeRange, formatBytes } from "./charts/dataUtils";
import { useServerHistoryData } from "./charts/hooks/useServerHistoryData";
import { CPUChart } from "./charts/CPUChart";
import { MemoryChart } from "./charts/MemoryChart";
import { DiskChart } from "./charts/DiskChart";
import { NetworkChart } from "./charts/NetworkChart";
interface ServerHistoryChartsProps {
serverId: string;
}
type TimeRange = '60m' | '1d' | '7d' | '1m' | '3m';
export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
const { theme } = useTheme();
const [timeRange, setTimeRange] = useState<TimeRange>("1d");
console.log('ServerHistoryCharts: Rendering with serverId:', serverId);
const {
data: metrics = [],
timeRange,
setTimeRange,
metrics,
chartData,
isLoading,
error
} = useQuery({
queryKey: ['server-metrics-history', serverId, timeRange],
queryFn: async () => {
console.log('ServerHistoryCharts: Fetching metrics for serverId:', serverId, 'timeRange:', timeRange);
const result = await serverService.getServerMetrics(serverId, timeRange);
console.log('ServerHistoryCharts: Raw metrics result for timeRange', timeRange, ':', result?.length || 0, 'records');
console.log('ServerHistoryCharts: First 3 records:', result?.slice(0, 3));
return result;
},
enabled: !!serverId,
refetchInterval: 60000,
retry: 1
});
error,
isFetching
} = useServerHistoryData(serverId);
console.log('ServerHistoryCharts: Query state:', {
metricsCount: metrics.length,
isLoading,
error: error?.message,
firstMetric: metrics[0],
serverId,
timeRange
});
console.log('ServerHistoryCharts: Rendering with serverId:', serverId, 'timeRange:', timeRange);
console.log('ServerHistoryCharts: About to format chart data with', metrics?.length || 0, 'metrics for timeRange:', timeRange);
const chartData = formatChartData(metrics, timeRange);
console.log('ServerHistoryCharts: After formatting, got', chartData?.length || 0, 'chart data points');
const getGridColor = () => theme === 'dark' ? '#374151' : '#e5e7eb';
const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280';
// Custom tooltip content with detailed information and full date/time
const DetailedTooltipContent = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
const data = payload[0]?.payload;
return (
<div className="bg-popover/95 border border-border backdrop-blur-sm rounded-lg p-3 shadow-lg">
<p className="font-medium text-popover-foreground mb-2">{label}</p>
{data?.fullTimestamp && (
<p className="text-xs text-muted-foreground mb-2">
{new Date(data.fullTimestamp).toLocaleString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
</p>
)}
{payload.map((entry: any, index: number) => {
const data = entry.payload;
return (
<div key={index} className="space-y-1">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span className="text-sm font-medium">{entry.name}: {entry.value}%</span>
</div>
{entry.dataKey === 'cpuUsage' && (
<div className="text-xs text-muted-foreground ml-5">
<div>CPU Cores: {data.cpuCores}</div>
<div>Free: {data.cpuFree}%</div>
</div>
)}
{entry.dataKey === 'ramUsagePercent' && (
<div className="text-xs text-muted-foreground ml-5">
<div>Used: {data.ramUsed}</div>
<div>Total: {data.ramTotal}</div>
<div>Free: {data.ramFree}</div>
</div>
)}
{entry.dataKey === 'diskUsagePercent' && (
<div className="text-xs text-muted-foreground ml-5">
<div>Used: {data.diskUsed}</div>
<div>Total: {data.diskTotal}</div>
<div>Free: {data.diskFree}</div>
</div>
)}
</div>
);
})}
</div>
);
}
return null;
};
// Network tooltip with detailed info and full date/time
const NetworkTooltipContent = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
const data = payload[0]?.payload;
return (
<div className="bg-popover/95 border border-border backdrop-blur-sm rounded-lg p-3 shadow-lg">
<p className="font-medium text-popover-foreground mb-2">{label}</p>
{data?.fullTimestamp && (
<p className="text-xs text-muted-foreground mb-2">
{new Date(data.fullTimestamp).toLocaleString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
</p>
)}
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2 mb-1">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span className="text-sm font-medium">
{entry.name}: {entry.dataKey.includes('Speed') ? `${entry.value} KB/s` : formatBytes(entry.value)}
</span>
</div>
))}
{data && (
<div className="text-xs text-muted-foreground mt-2 pt-2 border-t border-border">
<div>Total RX: {data.networkRx}</div>
<div>Total TX: {data.networkTx}</div>
</div>
)}
</div>
);
}
return null;
};
// Memoize latest data calculation to prevent unnecessary recalculations
const latestData = useMemo(() => {
return chartData.length > 0 ? chartData[chartData.length - 1] : null;
}, [chartData]);
// Show skeleton loading state for better UX
if (isLoading) {
return (
<div className="space-y-6">
@@ -165,22 +39,28 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
<div className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
<h2 className="text-lg font-medium">Historical Performance</h2>
<Loader2 className="h-4 w-4 animate-spin ml-2" />
<div className="flex items-center gap-2 ml-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-xs text-muted-foreground">Loading...</span>
</div>
</div>
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
</div>
{/* Skeleton loading cards */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{[1, 2, 3, 4].map((index) => (
<Card key={index}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<div className="h-5 w-5 bg-muted animate-pulse rounded" />
<div className="h-4 w-32 bg-muted animate-pulse rounded" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center h-80">
<div className="h-4 w-full bg-muted animate-pulse rounded" />
<Card key={index} className="bg-gradient-to-br from-background to-muted/20">
<CardContent className="p-6">
<div className="animate-pulse">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-muted rounded-lg"></div>
<div className="w-24 h-5 bg-muted rounded"></div>
</div>
<div className="w-16 h-4 bg-muted rounded"></div>
</div>
<div className="w-full h-80 bg-muted rounded"></div>
</div>
</CardContent>
</Card>
@@ -198,6 +78,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
<div className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
<h2 className="text-lg font-medium">Historical Performance</h2>
{isFetching && <Loader2 className="h-4 w-4 animate-spin ml-2" />}
</div>
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
</div>
@@ -206,7 +87,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
<div className="text-center">
<p className="text-muted-foreground">Error loading chart data</p>
<p className="text-xs mt-2 font-mono text-red-500">{error?.message}</p>
<p className="text-xs mt-1 text-muted-foreground">Server ID: {serverId}</p>
<p className="text-xs mt-1 text-muted-foreground">Server ID: {serverId} Time Range: {timeRange}</p>
</div>
</CardContent>
</Card>
@@ -221,6 +102,7 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
<div className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
<h2 className="text-lg font-medium">Historical Performance</h2>
{isFetching && <Loader2 className="h-4 w-4 animate-spin ml-2" />}
</div>
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
</div>
@@ -229,9 +111,9 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
<div className="text-center">
<p className="text-muted-foreground">No historical data available for {timeRange}</p>
<p className="text-xs mt-2">Raw metrics count: {metrics.length}</p>
<p className="text-xs mt-1">Server ID: {serverId}</p>
<p className="text-xs mt-1">Server ID: {serverId} Time Range: {timeRange}</p>
<p className="text-xs mt-1 text-muted-foreground">
{metrics.length > 0 ? 'Data exists but filtered out by time range' : 'No metrics data found'}
{metrics.length > 0 ? 'Data exists but may be outside selected time range' : 'No metrics data found'}
</p>
</div>
</CardContent>
@@ -242,291 +124,31 @@ export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
console.log('ServerHistoryCharts: Rendering charts with', chartData.length, 'data points for time range:', timeRange);
// Calculate summary stats from latest data point
const latestData = chartData[chartData.length - 1];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
<h2 className="text-lg font-medium">Historical Performance</h2>
<span className="text-xs text-muted-foreground">({chartData.length} data points {timeRange})</span>
<span className="text-xs text-muted-foreground">
({chartData.length} data points {timeRange})
{isFetching && (
<span className="inline-flex items-center gap-1 ml-2">
<Loader2 className="h-3 w-3 animate-spin" />
<span className="text-blue-500">Updating...</span>
</span>
)}
</span>
</div>
<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">
{/* CPU Usage Chart - Smooth Area Chart with Blue Gradient */}
<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">
<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" />
</div>
CPU Usage
</div>
{latestData && (
<div className="text-right 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={chartData}>
<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>
</Card>
{/* Memory Usage Chart - Basis Area Chart with Green Gradient */}
<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">
<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" />
</div>
Memory Usage
</div>
{latestData && (
<div className="text-right 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={chartData}>
<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>
</Card>
{/* Disk Usage Chart - Stepped Area Chart with Orange Gradient */}
<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">
<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" />
</div>
Disk Usage
</div>
{latestData && (
<div className="text-right 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={chartData}>
<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>
</Card>
{/* Network Traffic Chart - Dual Line Chart with Purple/Red Theme */}
<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">
<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" />
</div>
Network Traffic
</div>
{latestData && (
<div className="text-right 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={chartData}>
<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>
</Card>
<CPUChart data={chartData} latestData={latestData} />
<MemoryChart data={chartData} latestData={latestData} />
<DiskChart data={chartData} latestData={latestData} />
<NetworkChart data={chartData} latestData={latestData} />
</div>
</div>
);
@@ -6,10 +6,10 @@ import { serverService } from "@/services/serverService";
import { Loader2, Cpu, HardDrive, Network, MemoryStick } from "lucide-react";
import { formatChartData } from "./charts/dataUtils";
import { TimeRangeSelector } from "./charts/TimeRangeSelector";
import { CPUCharts } from "./charts/CPUCharts";
import { MemoryCharts } from "./charts/MemoryCharts";
import { DiskCharts } from "./charts/DiskCharts";
import { NetworkCharts } from "./charts/NetworkCharts";
import { CPUChart } from "./charts/CPUChart";
import { MemoryChart } from "./charts/MemoryChart";
import { DiskChart } from "./charts/DiskChart";
import { NetworkChart } from "./charts/NetworkChart";
interface ServerMetricsChartsProps {
serverId: string;
@@ -58,6 +58,9 @@ export const ServerMetricsCharts = ({ serverId }: ServerMetricsChartsProps) => {
);
}
// Calculate latest data for each chart
const latestData = chartData[chartData.length - 1];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
@@ -89,19 +92,19 @@ export const ServerMetricsCharts = ({ serverId }: ServerMetricsChartsProps) => {
</TabsList>
<TabsContent value="cpu" className="space-y-4 mt-6">
<CPUCharts data={chartData} />
<CPUChart data={chartData} latestData={latestData} />
</TabsContent>
<TabsContent value="memory" className="space-y-4 mt-6">
<MemoryCharts data={chartData} />
<MemoryChart data={chartData} latestData={latestData} />
</TabsContent>
<TabsContent value="disk" className="space-y-4 mt-6">
<DiskCharts data={chartData} />
<DiskChart data={chartData} latestData={latestData} />
</TabsContent>
<TabsContent value="network" className="space-y-4 mt-6">
<NetworkCharts data={chartData} />
<NetworkChart data={chartData} latestData={latestData} />
</TabsContent>
</Tabs>
</div>
@@ -0,0 +1,83 @@
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 { Cpu } from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
import { DetailedTooltipContent } from "./tooltips/DetailedTooltipContent";
interface CPUChartProps {
data: any[];
latestData?: any;
}
export const CPUChart = ({ data, latestData }: CPUChartProps) => {
const { theme } = useTheme();
const getGridColor = () => theme === 'dark' ? '#374151' : '#e5e7eb';
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">
<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" />
</div>
CPU Usage
</div>
{latestData && (
<div className="text-right 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>
</Card>
);
};
@@ -1,141 +0,0 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
import { AreaChart, Area, XAxis, YAxis, CartesianGrid } from "recharts";
import { Cpu } from "lucide-react";
import { useChartConfig } from "./chartConfig";
import { formatBytes } from "./dataUtils";
interface CPUChartsProps {
data: any[];
}
export const CPUCharts = ({ data }: CPUChartsProps) => {
const { chartConfig, getGridColor, getAxisColor } = useChartConfig();
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card className="bg-gradient-to-br from-card/90 via-card to-card/80 border-border/50 shadow-xl backdrop-blur-sm overflow-hidden group hover:shadow-2xl transition-all duration-300">
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 via-transparent to-purple-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<CardHeader className="relative">
<CardTitle className="text-foreground flex items-center gap-2">
<div className="p-2 rounded-lg bg-blue-500/10 border border-blue-500/20">
<Cpu className="h-4 w-4 text-blue-500" />
</div>
CPU Usage
</CardTitle>
</CardHeader>
<CardContent className="relative">
<ChartContainer config={chartConfig} className="h-80">
<AreaChart data={data}>
<defs>
<linearGradient id="cpuGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" stopOpacity={0.8}/>
<stop offset="50%" stopColor="#60a5fa" stopOpacity={0.4}/>
<stop offset="100%" stopColor="#93c5fd" stopOpacity={0.1}/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<CartesianGrid strokeDasharray="1 3" stroke={getGridColor()} opacity={0.3} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
/>
<YAxis
domain={[0, 100]}
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover/95 border-border backdrop-blur-sm" />}
cursor={{ stroke: '#3b82f6', strokeWidth: 2, strokeOpacity: 0.5 }}
/>
<Area
type="monotone"
dataKey="cpuUsage"
stroke="#3b82f6"
strokeWidth={3}
fill="url(#cpuGradient)"
dot={false}
name="CPU Usage (%)"
filter="url(#glow)"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-card/90 via-card to-card/80 border-border/50 shadow-xl backdrop-blur-sm overflow-hidden group hover:shadow-2xl transition-all duration-300">
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 via-transparent to-cyan-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<CardHeader className="relative">
<CardTitle className="text-foreground flex items-center gap-2">
<div className="p-2 rounded-lg bg-blue-500/10 border border-blue-500/20">
<Cpu className="h-4 w-4 text-blue-500" />
</div>
CPU Usage Distribution
</CardTitle>
</CardHeader>
<CardContent className="relative">
<ChartContainer config={chartConfig} className="h-80">
<AreaChart data={data}>
<defs>
<linearGradient id="cpuUsedGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" stopOpacity={0.9}/>
<stop offset="100%" stopColor="#1e40af" stopOpacity={0.3}/>
</linearGradient>
<linearGradient id="cpuFreeGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#94a3b8" stopOpacity={0.4}/>
<stop offset="100%" stopColor="#64748b" stopOpacity={0.1}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="1 3" stroke={getGridColor()} opacity={0.3} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
/>
<YAxis
domain={[0, 100]}
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover/95 border-border backdrop-blur-sm" />}
cursor={{ stroke: '#3b82f6', strokeWidth: 2, strokeOpacity: 0.5 }}
/>
<Area
type="monotone"
dataKey="cpuFree"
stackId="1"
stroke="#64748b"
strokeWidth={2}
fill="url(#cpuFreeGradient)"
name="CPU Available (%)"
/>
<Area
type="monotone"
dataKey="cpuUsage"
stackId="1"
stroke="#3b82f6"
strokeWidth={3}
fill="url(#cpuUsedGradient)"
name="CPU Usage (%)"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,83 @@
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 { HardDrive } from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
import { DetailedTooltipContent } from "./tooltips/DetailedTooltipContent";
interface DiskChartProps {
data: any[];
latestData?: any;
}
export const DiskChart = ({ data, latestData }: DiskChartProps) => {
const { theme } = useTheme();
const getGridColor = () => theme === 'dark' ? '#374151' : '#e5e7eb';
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">
<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" />
</div>
Disk Usage
</div>
{latestData && (
<div className="text-right 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>
</Card>
);
};
@@ -1,138 +0,0 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
import { AreaChart, Area, XAxis, YAxis, CartesianGrid } from "recharts";
import { HardDrive } from "lucide-react";
import { useChartConfig } from "./chartConfig";
import { formatBytes } from "./dataUtils";
interface DiskChartsProps {
data: any[];
}
export const DiskCharts = ({ data }: DiskChartsProps) => {
const { chartConfig, getGridColor, getAxisColor } = useChartConfig();
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card className="bg-gradient-to-br from-card/90 via-card to-card/80 border-border/50 shadow-xl backdrop-blur-sm overflow-hidden group hover:shadow-2xl transition-all duration-300">
<div className="absolute inset-0 bg-gradient-to-br from-amber-500/5 via-transparent to-orange-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<CardHeader className="relative">
<CardTitle className="text-foreground flex items-center gap-2">
<div className="p-2 rounded-lg bg-amber-500/10 border border-amber-500/20">
<HardDrive className="h-4 w-4 text-amber-500" />
</div>
Disk Usage
</CardTitle>
</CardHeader>
<CardContent className="relative">
<ChartContainer config={chartConfig} className="h-80">
<AreaChart data={data}>
<defs>
<linearGradient id="diskGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#f59e0b" stopOpacity={0.8}/>
<stop offset="50%" stopColor="#fbbf24" stopOpacity={0.4}/>
<stop offset="100%" stopColor="#fcd34d" stopOpacity={0.1}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="1 3" stroke={getGridColor()} opacity={0.3} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
/>
<YAxis
domain={[0, 100]}
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover/95 border-border backdrop-blur-sm" />}
cursor={{ stroke: '#f59e0b', strokeWidth: 2, strokeOpacity: 0.5 }}
/>
<Area
type="monotone"
dataKey="diskUsagePercent"
stroke="#f59e0b"
strokeWidth={3}
fill="url(#diskGradient)"
dot={false}
name="Disk Usage (%)"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-card/90 via-card to-card/80 border-border/50 shadow-xl backdrop-blur-sm overflow-hidden group hover:shadow-2xl transition-all duration-300">
<div className="absolute inset-0 bg-gradient-to-br from-amber-500/5 via-transparent to-yellow-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<CardHeader className="relative">
<CardTitle className="text-foreground flex items-center gap-2">
<div className="p-2 rounded-lg bg-amber-500/10 border border-amber-500/20">
<HardDrive className="h-4 w-4 text-amber-500" />
</div>
Disk Usage (Bytes)
</CardTitle>
</CardHeader>
<CardContent className="relative">
<ChartContainer config={chartConfig} className="h-80">
<AreaChart data={data}>
<defs>
<linearGradient id="diskUsedGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#f59e0b" stopOpacity={0.9}/>
<stop offset="100%" stopColor="#d97706" stopOpacity={0.3}/>
</linearGradient>
<linearGradient id="diskTotalGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#94a3b8" stopOpacity={0.4}/>
<stop offset="100%" stopColor="#64748b" stopOpacity={0.1}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="1 3" stroke={getGridColor()} opacity={0.3} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
/>
<YAxis
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
tickFormatter={(value) => formatBytes(value)}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover/95 border-border backdrop-blur-sm" />}
cursor={{ stroke: '#f59e0b', strokeWidth: 2, strokeOpacity: 0.5 }}
formatter={(value, name) => [
name === 'Used Disk' ? formatBytes(Number(value)) :
name === 'Total Disk' ? formatBytes(Number(value)) : value,
name
]}
/>
<Area
type="monotone"
dataKey="diskTotalBytes"
stackId="1"
stroke="#64748b"
strokeWidth={2}
fill="url(#diskTotalGradient)"
name="Total Disk"
/>
<Area
type="monotone"
dataKey="diskUsedBytes"
stackId="1"
stroke="#f59e0b"
strokeWidth={3}
fill="url(#diskUsedGradient)"
name="Used Disk"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,83 @@
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 { MemoryStick } from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
import { DetailedTooltipContent } from "./tooltips/DetailedTooltipContent";
interface MemoryChartProps {
data: any[];
latestData?: any;
}
export const MemoryChart = ({ data, latestData }: MemoryChartProps) => {
const { theme } = useTheme();
const getGridColor = () => theme === 'dark' ? '#374151' : '#e5e7eb';
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">
<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" />
</div>
Memory Usage
</div>
{latestData && (
<div className="text-right 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>
</Card>
);
};
@@ -1,138 +0,0 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
import { AreaChart, Area, XAxis, YAxis, CartesianGrid } from "recharts";
import { MemoryStick } from "lucide-react";
import { useChartConfig } from "./chartConfig";
import { formatBytes } from "./dataUtils";
interface MemoryChartsProps {
data: any[];
}
export const MemoryCharts = ({ data }: MemoryChartsProps) => {
const { chartConfig, getGridColor, getAxisColor } = useChartConfig();
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card className="bg-gradient-to-br from-card/90 via-card to-card/80 border-border/50 shadow-xl backdrop-blur-sm overflow-hidden group hover:shadow-2xl transition-all duration-300">
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 via-transparent to-emerald-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<CardHeader className="relative">
<CardTitle className="text-foreground flex items-center gap-2">
<div className="p-2 rounded-lg bg-green-500/10 border border-green-500/20">
<MemoryStick className="h-4 w-4 text-green-500" />
</div>
Memory Usage
</CardTitle>
</CardHeader>
<CardContent className="relative">
<ChartContainer config={chartConfig} className="h-80">
<AreaChart data={data}>
<defs>
<linearGradient id="memoryGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#10b981" stopOpacity={0.8}/>
<stop offset="50%" stopColor="#34d399" stopOpacity={0.4}/>
<stop offset="100%" stopColor="#6ee7b7" stopOpacity={0.1}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="1 3" stroke={getGridColor()} opacity={0.3} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
/>
<YAxis
domain={[0, 100]}
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover/95 border-border backdrop-blur-sm" />}
cursor={{ stroke: '#10b981', strokeWidth: 2, strokeOpacity: 0.5 }}
/>
<Area
type="monotone"
dataKey="ramUsagePercent"
stroke="#10b981"
strokeWidth={3}
fill="url(#memoryGradient)"
dot={false}
name="RAM Usage (%)"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-card/90 via-card to-card/80 border-border/50 shadow-xl backdrop-blur-sm overflow-hidden group hover:shadow-2xl transition-all duration-300">
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 via-transparent to-teal-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<CardHeader className="relative">
<CardTitle className="text-foreground flex items-center gap-2">
<div className="p-2 rounded-lg bg-green-500/10 border border-green-500/20">
<MemoryStick className="h-4 w-4 text-green-500" />
</div>
Memory Usage (Bytes)
</CardTitle>
</CardHeader>
<CardContent className="relative">
<ChartContainer config={chartConfig} className="h-80">
<AreaChart data={data}>
<defs>
<linearGradient id="memoryUsedGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#10b981" stopOpacity={0.9}/>
<stop offset="100%" stopColor="#047857" stopOpacity={0.3}/>
</linearGradient>
<linearGradient id="memoryTotalGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#94a3b8" stopOpacity={0.4}/>
<stop offset="100%" stopColor="#64748b" stopOpacity={0.1}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="1 3" stroke={getGridColor()} opacity={0.3} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
/>
<YAxis
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
tickFormatter={(value) => formatBytes(value)}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover/95 border-border backdrop-blur-sm" />}
cursor={{ stroke: '#10b981', strokeWidth: 2, strokeOpacity: 0.5 }}
formatter={(value, name) => [
name === 'Used Memory' ? formatBytes(Number(value)) :
name === 'Total Memory' ? formatBytes(Number(value)) : value,
name
]}
/>
<Area
type="monotone"
dataKey="ramTotalBytes"
stackId="1"
stroke="#64748b"
strokeWidth={2}
fill="url(#memoryTotalGradient)"
name="Total Memory"
/>
<Area
type="monotone"
dataKey="ramUsedBytes"
stackId="1"
stroke="#10b981"
strokeWidth={3}
fill="url(#memoryUsedGradient)"
name="Used Memory"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,102 @@
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 { Wifi } from "lucide-react";
import { useTheme } from "@/contexts/ThemeContext";
import { NetworkTooltipContent } from "./tooltips/NetworkTooltipContent";
interface NetworkChartProps {
data: any[];
latestData?: any;
}
export const NetworkChart = ({ data, latestData }: NetworkChartProps) => {
const { theme } = useTheme();
const getGridColor = () => theme === 'dark' ? '#374151' : '#e5e7eb';
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">
<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" />
</div>
Network Traffic
</div>
{latestData && (
<div className="text-right 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>
</Card>
);
};
@@ -1,140 +0,0 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
import { LineChart, Line, AreaChart, Area, XAxis, YAxis, CartesianGrid } from "recharts";
import { Network } from "lucide-react";
import { useChartConfig } from "./chartConfig";
interface NetworkChartsProps {
data: any[];
}
export const NetworkCharts = ({ data }: NetworkChartsProps) => {
const { chartConfig, getGridColor, getAxisColor } = useChartConfig();
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card className="bg-gradient-to-br from-card/90 via-card to-card/80 border-border/50 shadow-xl backdrop-blur-sm overflow-hidden group hover:shadow-2xl transition-all duration-300">
<div className="absolute inset-0 bg-gradient-to-br from-purple-500/5 via-transparent to-pink-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<CardHeader className="relative">
<CardTitle className="text-foreground flex items-center gap-2">
<div className="p-2 rounded-lg bg-purple-500/10 border border-purple-500/20">
<Network className="h-4 w-4 text-purple-500" />
</div>
Network Traffic
</CardTitle>
</CardHeader>
<CardContent className="relative">
<ChartContainer config={chartConfig} className="h-64">
<LineChart data={data}>
<defs>
<linearGradient id="networkRxGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#8b5cf6" stopOpacity={0.8}/>
<stop offset="100%" stopColor="#a78bfa" stopOpacity={0.2}/>
</linearGradient>
<linearGradient id="networkTxGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ef4444" stopOpacity={0.8}/>
<stop offset="100%" stopColor="#f87171" stopOpacity={0.2}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="1 3" stroke={getGridColor()} opacity={0.3} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
/>
<YAxis
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover/95 border-border backdrop-blur-sm" />}
cursor={{ stroke: '#8b5cf6', strokeWidth: 2, strokeOpacity: 0.5 }}
/>
<Line
type="monotone"
dataKey="networkRxBytes"
stroke="url(#networkRxGradient)"
strokeWidth={3}
name="RX Bytes"
dot={false}
/>
<Line
type="monotone"
dataKey="networkTxBytes"
stroke="url(#networkTxGradient)"
strokeWidth={3}
name="TX Bytes"
dot={false}
/>
</LineChart>
</ChartContainer>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-card/90 via-card to-card/80 border-border/50 shadow-xl backdrop-blur-sm overflow-hidden group hover:shadow-2xl transition-all duration-300">
<div className="absolute inset-0 bg-gradient-to-br from-purple-500/5 via-transparent to-violet-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<CardHeader className="relative">
<CardTitle className="text-foreground flex items-center gap-2">
<div className="p-2 rounded-lg bg-purple-500/10 border border-purple-500/20">
<Network className="h-4 w-4 text-purple-500" />
</div>
Network Speed (KB/s)
</CardTitle>
</CardHeader>
<CardContent className="relative">
<ChartContainer config={chartConfig} className="h-64">
<AreaChart data={data}>
<defs>
<linearGradient id="networkSpeedRxGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#8b5cf6" stopOpacity={0.6}/>
<stop offset="100%" stopColor="#a78bfa" stopOpacity={0.1}/>
</linearGradient>
<linearGradient id="networkSpeedTxGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ef4444" stopOpacity={0.6}/>
<stop offset="100%" stopColor="#f87171" stopOpacity={0.1}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="1 3" stroke={getGridColor()} opacity={0.3} />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
/>
<YAxis
tick={{ fontSize: 10, fill: getAxisColor() }}
axisLine={false}
tickLine={false}
/>
<ChartTooltip
content={<ChartTooltipContent className="bg-popover/95 border-border backdrop-blur-sm" />}
cursor={{ stroke: '#8b5cf6', strokeWidth: 2, strokeOpacity: 0.5 }}
/>
<Area
type="monotone"
dataKey="networkRxSpeed"
stroke="#8b5cf6"
strokeWidth={3}
fill="url(#networkSpeedRxGradient)"
name="RX Speed (KB/s)"
dot={false}
/>
<Area
type="monotone"
dataKey="networkTxSpeed"
stroke="#ef4444"
strokeWidth={3}
fill="url(#networkSpeedTxGradient)"
name="TX Speed (KB/s)"
dot={false}
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
</div>
);
};
@@ -1,6 +1,5 @@
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { timeRangeOptions } from "./dataUtils";
type TimeRange = '60m' | '1d' | '7d' | '1m' | '3m';
@@ -9,10 +8,18 @@ interface TimeRangeSelectorProps {
onChange: (value: TimeRange) => void;
}
const timeRangeOptions = [
{ value: '60m' as TimeRange, label: 'Last 60 minutes' },
{ value: '1d' as TimeRange, label: 'Last 24 hours' },
{ value: '7d' as TimeRange, label: 'Last 7 days' },
{ value: '1m' as TimeRange, label: 'Last 30 days' },
{ value: '3m' as TimeRange, label: 'Last 90 days' },
];
export const TimeRangeSelector = ({ value, onChange }: TimeRangeSelectorProps) => {
return (
<Select value={value} onValueChange={(value: TimeRange) => onChange(value)}>
<SelectTrigger className="w-[140px] h-8">
<SelectTrigger className="w-[160px] h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -1,34 +0,0 @@
import { useTheme } from "@/contexts/ThemeContext";
export const useChartConfig = () => {
const { theme } = useTheme();
const chartConfig = {
cpuUsage: {
label: "CPU Usage (%)",
color: theme === 'dark' ? "#60a5fa" : "#3b82f6",
},
ramUsagePercent: {
label: "RAM Usage (%)",
color: theme === 'dark' ? "#34d399" : "#10b981",
},
diskUsagePercent: {
label: "Disk Usage (%)",
color: theme === 'dark' ? "#fbbf24" : "#f59e0b",
},
networkRx: {
label: "Network RX",
color: theme === 'dark' ? "#a78bfa" : "#8b5cf6",
},
networkTx: {
label: "Network TX",
color: theme === 'dark' ? "#f87171" : "#ef4444",
},
};
const getGridColor = () => theme === 'dark' ? '#374151' : '#e5e7eb';
const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280';
return { chartConfig, getGridColor, getAxisColor, theme };
};
@@ -42,87 +42,122 @@ export const convertToBytes = (value: string | number): number => {
type TimeRange = '60m' | '1d' | '7d' | '1m' | '3m';
export const timeRangeOptions = [
{ value: '60m' as TimeRange, label: '60 minutes', hours: 1 },
{ value: '1d' as TimeRange, label: '1 day', hours: 24 },
{ value: '7d' as TimeRange, label: '7 days', hours: 24 * 7 },
{ value: '1m' as TimeRange, label: '1 month', hours: 24 * 30 },
{ value: '3m' as TimeRange, label: '3 months', hours: 24 * 90 },
{ value: '60m' as TimeRange, label: 'Last 60 minutes', hours: 1 },
{ value: '1d' as TimeRange, label: 'Last 24 hours', hours: 24 },
{ value: '7d' as TimeRange, label: 'Last 7 days', hours: 24 * 7 },
{ value: '1m' as TimeRange, label: 'Last 30 days', hours: 24 * 30 },
{ 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 [];
const now = new Date();
const selectedRange = timeRangeOptions.find(opt => opt.value === timeRange);
if (!selectedRange) return metrics;
const cutoffTime = new Date(now.getTime() - (selectedRange.hours * 60 * 60 * 1000));
console.log('filterMetricsByTimeRange: timeRange:', timeRange, 'cutoffTime:', cutoffTime.toISOString(), 'now:', now.toISOString());
// 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 filtered = metrics.filter(metric => {
const metricTime = new Date(metric.created || metric.timestamp);
const isValid = metricTime >= cutoffTime;
if (!isValid && metrics.indexOf(metric) < 3) {
console.log('filterMetricsByTimeRange: Filtered out metric:', {
metricTime: metricTime.toISOString(),
cutoffTime: cutoffTime.toISOString(),
created: metric.created,
timestamp: metric.timestamp
});
}
return isValid;
return metricTime >= cutoffTime && metricTime <= now;
});
console.log('filterMetricsByTimeRange: Filtered', metrics.length, 'to', filtered.length, 'metrics');
return filtered;
// 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 => {
const cacheKey = `${timestamp}-${timeRange}`;
if (timestampCache.has(cacheKey)) {
return timestampCache.get(cacheKey)!;
}
const date = new Date(timestamp);
let formatted: string;
if (timeRange === '60m') {
// For 60 minutes, show time with seconds
return date.toLocaleString('en-US', {
formatted = date.toLocaleString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
} else if (timeRange === '1d') {
formatted = date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
hour12: false
});
} else if (timeRange === '1d') {
// For 1 day, show date and time
return date.toLocaleString('en-US', {
} else if (timeRange === '7d') {
formatted = date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
hour: '2-digit',
hour12: false
});
} else {
// For longer periods, show date and time
return date.toLocaleString('en-US', {
formatted = date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
year: '2-digit'
});
}
// Cache the result (limit cache size)
if (timestampCache.size > 1000) {
timestampCache.clear();
}
timestampCache.set(cacheKey, formatted);
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);
if (!metrics?.length) return [];
const filteredMetrics = filterMetricsByTimeRange(metrics, timeRange);
console.log('formatChartData: After time filtering:', filteredMetrics?.length || 0, 'metrics');
if (filteredMetrics.length > 0) {
console.log('formatChartData: First filtered metric:', filteredMetrics[0]);
console.log('formatChartData: Sample timestamps:', filteredMetrics.slice(0, 3).map(m => ({
created: m.created,
timestamp: m.timestamp,
both: new Date(m.created || m.timestamp).toISOString()
})));
if (!filteredMetrics.length) 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
default: maxDataPoints = 100;
}
return filteredMetrics.slice(0, 100).reverse().map((metric, index) => {
// 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');
// Batch process the data transformation for better performance
return sampledMetrics.map((metric) => {
const cpuUsage = typeof metric.cpu_usage === 'string' ?
parseFloat(metric.cpu_usage.replace('%', '')) :
parseFloat(metric.cpu_usage) || 0;
@@ -149,7 +184,7 @@ export const formatChartData = (metrics: any[], timeRange: TimeRange) => {
fullTimestamp: timestamp,
cpuUsage: Math.round(cpuUsage * 100) / 100,
cpuCores: parseInt(metric.cpu_cores) || 0,
cpuFree: 100 - cpuUsage,
cpuFree: Math.round((100 - cpuUsage) * 100) / 100,
ramUsedBytes,
ramTotalBytes,
@@ -0,0 +1,45 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { serverService } from "@/services/serverService";
import { formatChartData } from "../dataUtils";
type TimeRange = '60m' | '1d' | '7d' | '1m' | '3m';
export const useServerHistoryData = (serverId: string) => {
const [timeRange, setTimeRange] = useState<TimeRange>("1d");
const {
data: metrics = [],
isLoading,
error,
isFetching
} = useQuery({
queryKey: ['server-metrics-history', serverId, timeRange],
queryFn: async () => {
console.log('useServerHistoryData: Fetching metrics for serverId:', serverId, 'timeRange:', timeRange);
const result = await serverService.getServerMetrics(serverId, timeRange);
console.log('useServerHistoryData: Raw metrics result for timeRange', timeRange, ':', result?.length || 0, 'records');
return result || [];
},
enabled: !!serverId,
refetchInterval: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Reduced frequency
staleTime: timeRange === '60m' ? 15000 : timeRange === '1d' ? 30000 : 60000, // Increased stale time
retry: 2, // Reduced retries
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 3000), // Faster retry
gcTime: 5 * 60 * 1000, // 5 minutes cache
});
// Memoize chart data formatting to prevent unnecessary recalculations
const chartData = formatChartData(metrics, timeRange);
return {
timeRange,
setTimeRange,
metrics,
chartData,
isLoading,
error,
isFetching
};
};
@@ -0,0 +1,65 @@
interface DetailedTooltipContentProps {
active?: boolean;
payload?: any[];
label?: string;
}
export const DetailedTooltipContent = ({ active, payload, label }: DetailedTooltipContentProps) => {
if (!active || !payload || !payload.length) return null;
const data = payload[0]?.payload;
return (
<div className="bg-popover/95 border border-border backdrop-blur-sm rounded-lg p-3 shadow-lg">
<p className="font-medium text-popover-foreground mb-2">{label}</p>
{data?.fullTimestamp && (
<p className="text-xs text-muted-foreground mb-2">
{new Date(data.fullTimestamp).toLocaleString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
</p>
)}
{payload.map((entry: any, index: number) => {
const data = entry.payload;
return (
<div key={index} className="space-y-1">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span className="text-sm font-medium">{entry.name}: {entry.value}%</span>
</div>
{entry.dataKey === 'cpuUsage' && (
<div className="text-xs text-muted-foreground ml-5">
<div>CPU Cores: {data.cpuCores}</div>
<div>Free: {data.cpuFree}%</div>
</div>
)}
{entry.dataKey === 'ramUsagePercent' && (
<div className="text-xs text-muted-foreground ml-5">
<div>Used: {data.ramUsed}</div>
<div>Total: {data.ramTotal}</div>
<div>Free: {data.ramFree}</div>
</div>
)}
{entry.dataKey === 'diskUsagePercent' && (
<div className="text-xs text-muted-foreground ml-5">
<div>Used: {data.diskUsed}</div>
<div>Total: {data.diskTotal}</div>
<div>Free: {data.diskFree}</div>
</div>
)}
</div>
);
})}
</div>
);
};
@@ -0,0 +1,48 @@
interface NetworkTooltipContentProps {
active?: boolean;
payload?: any[];
label?: string;
}
export const NetworkTooltipContent = ({ active, payload, label }: NetworkTooltipContentProps) => {
if (!active || !payload || !payload.length) return null;
const data = payload[0]?.payload;
return (
<div className="bg-popover/95 border border-border backdrop-blur-sm rounded-lg p-3 shadow-lg">
<p className="font-medium text-popover-foreground mb-2">{label}</p>
{data?.fullTimestamp && (
<p className="text-xs text-muted-foreground mb-2">
{new Date(data.fullTimestamp).toLocaleString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
</p>
)}
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2 mb-1">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span className="text-sm font-medium">
{entry.name}: {entry.dataKey.includes('Speed') ? `${entry.value} KB/s` : entry.value}
</span>
</div>
))}
{data && (
<div className="text-xs text-muted-foreground mt-2 pt-2 border-t border-border">
<div>Total RX: {data.networkRx}</div>
<div>Total TX: {data.networkTx}</div>
</div>
)}
</div>
);
};