Merge pull request #64 from operacle/develop
feat: Improve server overview cards and fix regional_status to Service type
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
|
||||
# 🚀 What is CheckCle?
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const ServerMetricsOverview = ({ server }: ServerMetricsOverviewProps) =>
|
||||
const ramUsagePercent = server.ram_total > 0 ? (server.ram_used / server.ram_total) * 100 : 0;
|
||||
const diskUsagePercent = server.disk_total > 0 ? (server.disk_used / server.disk_total) * 100 : 0;
|
||||
|
||||
const MetricCard = ({ title, used, total, free, percentage, icon: Icon, color, additionalInfo }: {
|
||||
const MetricCard = ({ title, used, total, free, percentage, icon: Icon, color, gradient, additionalInfo }: {
|
||||
title: string;
|
||||
used: string;
|
||||
total: string;
|
||||
@@ -44,66 +44,69 @@ export const ServerMetricsOverview = ({ server }: ServerMetricsOverviewProps) =>
|
||||
percentage: number;
|
||||
icon: any;
|
||||
color: string;
|
||||
gradient: string;
|
||||
additionalInfo?: string;
|
||||
}) => (
|
||||
<Card className="relative overflow-hidden bg-gradient-to-br from-card/80 via-card to-card/60 border border-border/60 shadow-lg hover:shadow-xl hover:border-border/80 transition-all duration-300 backdrop-blur-sm group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-transparent via-white/[0.02] to-white/[0.05] opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
<CardHeader className="pb-3 relative">
|
||||
<Card
|
||||
className="border-none rounded-xl shadow-lg overflow-hidden transition-all duration-300 hover:shadow-xl transform hover:-translate-y-1 relative"
|
||||
style={{ background: 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="pb-3 relative z-10">
|
||||
<CardTitle className="flex items-center justify-between text-sm font-semibold">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="p-2.5 rounded-xl shadow-sm ring-1 ring-white/10 transition-all duration-300 group-hover:scale-110"
|
||||
style={{
|
||||
backgroundColor: `${color}15`,
|
||||
boxShadow: `0 4px 12px ${color}20`
|
||||
}}
|
||||
>
|
||||
<Icon className="h-4 w-4" style={{ color }} />
|
||||
<div className="p-2.5 rounded-xl bg-white/20 backdrop-blur-sm shadow-sm transition-all duration-300 group-hover:scale-110">
|
||||
<Icon className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="text-foreground/90">{title}</span>
|
||||
<span className="text-white">{title}</span>
|
||||
</div>
|
||||
<div className="text-xs font-mono font-bold px-2 py-1 rounded-md" style={{
|
||||
color,
|
||||
backgroundColor: `${color}10`,
|
||||
border: `1px solid ${color}30`
|
||||
}}>
|
||||
<div 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">
|
||||
{percentage.toFixed(1)}%
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 relative">
|
||||
|
||||
<CardContent className="space-y-4 relative z-10">
|
||||
<div className="grid grid-cols-2 gap-4 text-xs">
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Used:</span>
|
||||
<span className="font-mono font-semibold text-foreground">{used}</span>
|
||||
<span className="text-white/70">Used:</span>
|
||||
<span className="font-mono font-semibold text-white">{used}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Free:</span>
|
||||
<span className="font-mono font-semibold text-foreground">{free}</span>
|
||||
<span className="text-white/70">Free:</span>
|
||||
<span className="font-mono font-semibold text-white">{free}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Total:</span>
|
||||
<span className="font-mono font-semibold text-foreground">{total}</span>
|
||||
<span className="text-white/70">Total:</span>
|
||||
<span className="font-mono font-semibold text-white">{total}</span>
|
||||
</div>
|
||||
{additionalInfo && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Cores:</span>
|
||||
<span className="font-mono font-semibold text-foreground">{additionalInfo}</span>
|
||||
<span className="text-white/70">Cores:</span>
|
||||
<span className="font-mono font-semibold text-white">{additionalInfo}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-muted/40 rounded-full h-3 overflow-hidden shadow-inner">
|
||||
|
||||
<div className="w-full bg-white/20 rounded-full h-3 overflow-hidden shadow-inner backdrop-blur-sm">
|
||||
<div
|
||||
className="h-3 rounded-full transition-all duration-700 ease-out relative overflow-hidden"
|
||||
style={{
|
||||
width: `${Math.min(percentage, 100)}%`,
|
||||
backgroundColor: color,
|
||||
boxShadow: `0 0 12px ${color}50, inset 0 1px 0 rgba(255,255,255,0.2)`
|
||||
}}
|
||||
className="h-3 rounded-full transition-all duration-700 ease-out relative overflow-hidden bg-white/80"
|
||||
style={{ width: `${Math.min(percentage, 100)}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-pulse" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-white/10" />
|
||||
@@ -116,54 +119,90 @@ export const ServerMetricsOverview = ({ server }: ServerMetricsOverviewProps) =>
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-6">
|
||||
{/* Status Card */}
|
||||
<Card className="relative overflow-hidden bg-gradient-to-br from-card/80 via-card to-card/60 border border-border/60 shadow-lg hover:shadow-xl hover:border-border/80 transition-all duration-300 backdrop-blur-sm group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-transparent via-white/[0.02] to-white/[0.05] opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
<CardHeader className="pb-3 relative">
|
||||
<Card
|
||||
className="border-none rounded-xl shadow-lg overflow-hidden transition-all duration-300 hover:shadow-xl transform hover:-translate-y-1 relative"
|
||||
style={{
|
||||
background: 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%)"
|
||||
}}
|
||||
>
|
||||
{/* 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="pb-3 relative z-10">
|
||||
<CardTitle className="flex items-center gap-3 text-sm font-semibold">
|
||||
<div className="p-2.5 rounded-xl bg-primary/15 shadow-sm ring-1 ring-white/10 transition-all duration-300 group-hover:scale-110" style={{ boxShadow: '0 4px 12px rgba(59, 130, 246, 0.2)' }}>
|
||||
<Activity className="h-4 w-4 text-primary" />
|
||||
<div className="p-2.5 rounded-xl bg-white/20 backdrop-blur-sm shadow-sm transition-all duration-300 group-hover:scale-110">
|
||||
<Activity className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="text-foreground/90">Status</span>
|
||||
<span className="text-white">Status</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 relative">
|
||||
<div className={`text-lg font-bold capitalize ${getStatusColor(server.status)} flex items-center gap-3`}>
|
||||
<div className={`w-3 h-3 rounded-full ${server.status === 'up' ? 'bg-green-500' : server.status === 'down' ? 'bg-red-500' : 'bg-yellow-500'} animate-pulse shadow-sm`} style={{
|
||||
boxShadow: `0 0 8px ${server.status === 'up' ? '#10b981' : server.status === 'down' ? '#ef4444' : '#f59e0b'}`
|
||||
}} />
|
||||
|
||||
<CardContent className="space-y-4 relative z-10">
|
||||
<div className={`text-lg font-bold capitalize flex items-center gap-3 text-white`}>
|
||||
<div className={`w-3 h-3 rounded-full ${server.status === 'up' ? 'bg-green-400' : server.status === 'down' ? 'bg-red-400' : 'bg-yellow-400'} animate-pulse shadow-sm`} />
|
||||
{server.status}
|
||||
</div>
|
||||
<div className="space-y-2 text-xs">
|
||||
<div className="flex justify-between py-1">
|
||||
<span className="text-muted-foreground">Agent:</span>
|
||||
<span className="font-semibold text-foreground">{server.agent_status}</span>
|
||||
<span className="text-white/70">Agent:</span>
|
||||
<span className="font-semibold text-white">{server.agent_status}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-1">
|
||||
<span className="text-muted-foreground">Connection:</span>
|
||||
<span className="font-semibold text-foreground">{server.connection}</span>
|
||||
<span className="text-white/70">Connection:</span>
|
||||
<span className="font-semibold text-white">{server.connection}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Uptime Card */}
|
||||
<Card className="relative overflow-hidden bg-gradient-to-br from-card/80 via-card to-card/60 border border-border/60 shadow-lg hover:shadow-xl hover:border-border/80 transition-all duration-300 backdrop-blur-sm group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-transparent via-white/[0.02] to-white/[0.05] opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
<CardHeader className="pb-3 relative">
|
||||
<Card
|
||||
className="border-none rounded-xl shadow-lg overflow-hidden transition-all duration-300 hover:shadow-xl transform hover:-translate-y-1 relative"
|
||||
style={{
|
||||
background: theme === 'dark'
|
||||
? "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, rgba(34, 197, 94, 0.6) 100%)"
|
||||
: "linear-gradient(135deg, rgba(65, 59, 55, 0.8) 0%, #22c55e 100%)"
|
||||
}}
|
||||
>
|
||||
{/* 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="pb-3 relative z-10">
|
||||
<CardTitle className="flex items-center gap-3 text-sm font-semibold">
|
||||
<div className="p-2.5 rounded-xl bg-blue-500/15 shadow-sm ring-1 ring-white/10 transition-all duration-300 group-hover:scale-110" style={{ boxShadow: '0 4px 12px rgba(59, 130, 246, 0.2)' }}>
|
||||
<Clock className="h-4 w-4 text-blue-500" />
|
||||
<div className="p-2.5 rounded-xl bg-white/20 backdrop-blur-sm shadow-sm transition-all duration-300 group-hover:scale-110">
|
||||
<Clock className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="text-foreground/90">Uptime</span>
|
||||
<span className="text-white">Uptime</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 relative">
|
||||
<div className="text-lg font-bold text-blue-500">
|
||||
|
||||
<CardContent className="space-y-4 relative z-10">
|
||||
<div className="text-lg font-bold text-white">
|
||||
{server.uptime}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<div className="text-xs text-white/70 space-y-1">
|
||||
<div>Last Check:</div>
|
||||
<div className="font-mono text-[10px] text-foreground/80 bg-muted/30 px-2 py-1 rounded">
|
||||
<div className="font-mono text-[10px] text-white/90 bg-white/10 px-2 py-1 rounded backdrop-blur-sm">
|
||||
{new Date(server.last_checked).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,6 +218,10 @@ export const ServerMetricsOverview = ({ server }: ServerMetricsOverviewProps) =>
|
||||
percentage={cpuUsagePercent}
|
||||
icon={Cpu}
|
||||
color={theme === 'dark' ? "#3b82f6" : "#2563eb"}
|
||||
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%)"
|
||||
}
|
||||
additionalInfo={server.cpu_cores?.toString()}
|
||||
/>
|
||||
|
||||
@@ -191,6 +234,10 @@ export const ServerMetricsOverview = ({ server }: ServerMetricsOverviewProps) =>
|
||||
percentage={ramUsagePercent}
|
||||
icon={MemoryStick}
|
||||
color={theme === 'dark' ? "#10b981" : "#059669"}
|
||||
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%)"
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Disk Metric */}
|
||||
@@ -202,6 +249,10 @@ export const ServerMetricsOverview = ({ server }: ServerMetricsOverviewProps) =>
|
||||
percentage={diskUsagePercent}
|
||||
icon={HardDrive}
|
||||
color={theme === 'dark' ? "#f59e0b" : "#d97706"}
|
||||
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%)"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Server, Monitor, Cpu, HardDrive, DatabaseIcon, InfoIcon } from "lucide-react";
|
||||
import { Server as ServerType } from "@/types/server.types";
|
||||
|
||||
interface ServerSystemInfoCardProps {
|
||||
server: ServerType;
|
||||
}
|
||||
|
||||
export function ServerSystemInfoCard({ server }: ServerSystemInfoCardProps) {
|
||||
// Parse system_info if it's a string
|
||||
let systemInfo: any = {};
|
||||
if (server.system_info) {
|
||||
try {
|
||||
systemInfo = typeof server.system_info === 'string'
|
||||
? JSON.parse(server.system_info)
|
||||
: server.system_info;
|
||||
} catch (error) {
|
||||
console.log('Error parsing system_info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const formatUptime = (uptimeSeconds: string | number) => {
|
||||
const seconds = typeof uptimeSeconds === 'string' ? parseInt(uptimeSeconds) : uptimeSeconds;
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (days > 0) return `${days}d ${hours}h`;
|
||||
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||
return `${minutes}m`;
|
||||
};
|
||||
|
||||
// Format the detailed system info string
|
||||
const getDetailedSystemInfo = () => {
|
||||
if (typeof server.system_info === 'string' && server.system_info.includes('|')) {
|
||||
// If system_info is already in the detailed format, return it
|
||||
return server.system_info;
|
||||
}
|
||||
|
||||
// Otherwise, build the detailed string from available data
|
||||
const parts = [];
|
||||
|
||||
// Basic server info
|
||||
parts.push(server.hostname || 'Unknown');
|
||||
parts.push(server.ip_address || 'Unknown IP');
|
||||
parts.push(server.os_type || 'Unknown OS');
|
||||
|
||||
// Parse additional info from system_info if available
|
||||
if (systemInfo.OSName) {
|
||||
parts.push(systemInfo.OSName + (systemInfo.OSVersion ? ` ${systemInfo.OSVersion}` : ''));
|
||||
}
|
||||
|
||||
if (systemInfo.Architecture) {
|
||||
parts.push(`| ${systemInfo.Architecture}`);
|
||||
}
|
||||
|
||||
if (systemInfo.KernelVersion) {
|
||||
parts.push(`| Kernel: ${systemInfo.KernelVersion}`);
|
||||
}
|
||||
|
||||
if (systemInfo.CPUModel) {
|
||||
parts.push(`| CPU: ${systemInfo.CPUModel} (${server.cpu_cores || 0} cores)`);
|
||||
} else if (server.cpu_cores) {
|
||||
parts.push(`| CPU: ${server.cpu_cores} cores`);
|
||||
}
|
||||
|
||||
if (server.ram_total) {
|
||||
parts.push(`| RAM: ${formatBytes(server.ram_total)}`);
|
||||
}
|
||||
|
||||
if (systemInfo.GoVersion) {
|
||||
parts.push(`| Go ${systemInfo.GoVersion}`);
|
||||
}
|
||||
|
||||
if (systemInfo.IPAddress && systemInfo.IPAddress !== server.ip_address) {
|
||||
parts.push(`| IP: ${systemInfo.IPAddress}`);
|
||||
}
|
||||
|
||||
// Check for Docker info
|
||||
const dockerInfo = server.docker || systemInfo.Docker;
|
||||
if (dockerInfo !== undefined) {
|
||||
parts.push(`| Docker: ${dockerInfo}`);
|
||||
}
|
||||
|
||||
return parts.join(' • ').replace(/• \|/g, '|');
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-card/50 backdrop-blur-sm border-border/50 max-w-md">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="h-6 w-6 rounded bg-primary/10 flex items-center justify-center">
|
||||
<InfoIcon className="h-3 w-3 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-sm font-medium">System Information</h3>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground leading-relaxed break-all">
|
||||
{getDetailedSystemInfo()}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -14,13 +14,13 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
||||
|
||||
// Modern color palette for different chart lines with solid colors at 90-100% opacity
|
||||
const modernColors = [
|
||||
{ stroke: '#f59e0b', fill: 'rgba(245, 158, 11, 0.95)' }, // Yellow (changed from blue)
|
||||
{ stroke: '#10b981', fill: 'rgba(16, 185, 129, 0.95)' }, // Emerald
|
||||
{ stroke: '#f59e0b', fill: 'rgba(111, 86, 63, 0.95)' }, // Yellow (changed from blue)
|
||||
{ stroke: '#10b981', fill: 'rgba(0, 84, 56, 0.95)' }, // Emerald
|
||||
{ stroke: '#3b82f6', fill: 'rgba(59, 130, 246, 0.95)' }, // Blue (moved to second position)
|
||||
{ stroke: '#ef4444', fill: 'rgba(239, 68, 68, 0.95)' }, // Red
|
||||
{ stroke: '#8b5cf6', fill: 'rgba(139, 92, 246, 0.95)' }, // Violet
|
||||
{ stroke: '#06b6d4', fill: 'rgba(6, 182, 212, 0.95)' }, // Cyan
|
||||
{ stroke: '#f97316', fill: 'rgba(249, 115, 22, 0.95)' }, // Orange
|
||||
{ stroke: '#f97316', fill: 'rgba(231, 148, 89, 0.95)' }, // Orange
|
||||
{ stroke: '#84cc16', fill: 'rgba(132, 204, 22, 0.95)' }, // Lime
|
||||
];
|
||||
|
||||
|
||||
@@ -74,8 +74,9 @@ export function ServiceForm({
|
||||
urlValue = initialData.url || "";
|
||||
}
|
||||
|
||||
// Handle regional monitoring data - ensure proper assignment display
|
||||
const regionalAgent = initialData.region_name && initialData.agent_id
|
||||
// Handle regional monitoring data - check regional_status field
|
||||
const isRegionalEnabled = initialData.regional_status === "enabled";
|
||||
const regionalAgent = isRegionalEnabled && initialData.region_name && initialData.agent_id
|
||||
? `${initialData.region_name}|${initialData.agent_id}`
|
||||
: "";
|
||||
|
||||
@@ -89,7 +90,7 @@ export function ServiceForm({
|
||||
retries: String(initialData.retries || 3),
|
||||
notificationChannel: initialData.notificationChannel === "none" ? "" : initialData.notificationChannel || "",
|
||||
alertTemplate: initialData.alertTemplate === "default" ? "" : initialData.alertTemplate || "",
|
||||
regionalMonitoringEnabled: Boolean(initialData.regional_monitoring_enabled),
|
||||
regionalMonitoringEnabled: isRegionalEnabled,
|
||||
regionalAgent: regionalAgent,
|
||||
});
|
||||
|
||||
@@ -99,7 +100,8 @@ export function ServiceForm({
|
||||
url: urlValue,
|
||||
port: portValue,
|
||||
regionalAgent,
|
||||
regionalMonitoringEnabled: Boolean(initialData.regional_monitoring_enabled),
|
||||
regionalMonitoringEnabled: isRegionalEnabled,
|
||||
regional_status: initialData.regional_status,
|
||||
region_name: initialData.region_name,
|
||||
agent_id: initialData.agent_id
|
||||
});
|
||||
@@ -118,12 +120,16 @@ export function ServiceForm({
|
||||
// Parse regional agent selection
|
||||
let regionName = "";
|
||||
let agentId = "";
|
||||
let regionalStatus: "enabled" | "disabled" = "disabled";
|
||||
|
||||
// Only set region and agent if regional monitoring is enabled AND an agent is selected (not unassign)
|
||||
if (data.regionalMonitoringEnabled && data.regionalAgent && data.regionalAgent !== "") {
|
||||
const [parsedRegionName, parsedAgentId] = data.regionalAgent.split("|");
|
||||
regionName = parsedRegionName || "";
|
||||
agentId = parsedAgentId || "";
|
||||
// Set regional status and agent data based on form values
|
||||
if (data.regionalMonitoringEnabled) {
|
||||
regionalStatus = "enabled";
|
||||
if (data.regionalAgent && data.regionalAgent !== "") {
|
||||
const [parsedRegionName, parsedAgentId] = data.regionalAgent.split("|");
|
||||
regionName = parsedRegionName || "";
|
||||
agentId = parsedAgentId || "";
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare service data with proper field mapping
|
||||
@@ -134,8 +140,8 @@ export function ServiceForm({
|
||||
retries: parseInt(data.retries),
|
||||
notificationChannel: data.notificationChannel === "none" ? "" : data.notificationChannel,
|
||||
alertTemplate: data.alertTemplate === "default" ? "" : data.alertTemplate,
|
||||
regionalMonitoringEnabled: data.regionalMonitoringEnabled || false,
|
||||
// Always set region_name and agent_id - empty strings when unassigned
|
||||
// Use regional_status field instead of regionalMonitoringEnabled
|
||||
regionalStatus: regionalStatus,
|
||||
regionName: regionName,
|
||||
agentId: agentId,
|
||||
// Map the URL field to appropriate database field based on service type
|
||||
|
||||
@@ -24,7 +24,7 @@ export const UptimeBar = ({ uptime, status, serviceId, interval, serviceType }:
|
||||
|
||||
const getStatusColor = (itemStatus: string, hasData: boolean = true) => {
|
||||
if (!hasData) {
|
||||
return "bg-gray-300"; // No data color
|
||||
return "bg-gray-400"; // No data color - grey
|
||||
}
|
||||
|
||||
switch (itemStatus) {
|
||||
@@ -35,9 +35,11 @@ export const UptimeBar = ({ uptime, status, serviceId, interval, serviceType }:
|
||||
case "warning":
|
||||
return "bg-yellow-500";
|
||||
case "paused":
|
||||
return "bg-gray-500"; // Distinct paused color (darker grey)
|
||||
return "bg-gray-400"; // Paused status - grey
|
||||
case "unknown":
|
||||
case "NA":
|
||||
default:
|
||||
return "bg-gray-300"; // Default fallback
|
||||
return "bg-gray-400"; // Unknown/NA/default - grey
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,7 +104,7 @@ export const UptimeBar = ({ uptime, status, serviceId, interval, serviceType }:
|
||||
item.status === 'up' ? 'bg-emerald-500' :
|
||||
item.status === 'down' ? 'bg-red-500' :
|
||||
item.status === 'warning' ? 'bg-yellow-500' :
|
||||
item.status === 'paused' ? 'bg-gray-500' : 'bg-gray-300'
|
||||
item.status === 'paused' ? 'bg-gray-400' : 'bg-gray-400'
|
||||
}`} />
|
||||
<span className="text-xs font-medium truncate">{item.source}</span>
|
||||
</div>
|
||||
|
||||
@@ -22,8 +22,10 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
||||
enabled: regionalMonitoringEnabled,
|
||||
});
|
||||
|
||||
// Filter only online agents
|
||||
const onlineAgents = regionalAgents.filter(agent => agent.connection === 'online');
|
||||
// Filter only online agents and exclude the default localhost agent (ID 1)
|
||||
const onlineAgents = regionalAgents.filter(agent =>
|
||||
agent.connection === 'online' && agent.agent_id !== "1"
|
||||
);
|
||||
|
||||
// Find the current agent name for display
|
||||
const getCurrentAgentDisplay = () => {
|
||||
@@ -31,7 +33,7 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
||||
return "Select a regional agent or unassign";
|
||||
}
|
||||
|
||||
const [regionName] = currentRegionalAgent.split("|");
|
||||
const [regionName, agentId] = currentRegionalAgent.split("|");
|
||||
const agent = onlineAgents.find(agent =>
|
||||
`${agent.region_name}|${agent.agent_id}` === currentRegionalAgent
|
||||
);
|
||||
@@ -41,7 +43,25 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
||||
}
|
||||
|
||||
// If agent is not found in online agents, it might be offline but still assigned
|
||||
return regionName || "Select a regional agent or unassign";
|
||||
// Show the region name from the stored value
|
||||
if (regionName && agentId) {
|
||||
return `${regionName} (Agent ${agentId}) - Offline`;
|
||||
}
|
||||
|
||||
return "Select a regional agent or unassign";
|
||||
};
|
||||
|
||||
// Get the proper select value - handle both assigned and unassigned cases
|
||||
const getSelectValue = () => {
|
||||
if (!regionalMonitoringEnabled) {
|
||||
return "unassign";
|
||||
}
|
||||
|
||||
if (!currentRegionalAgent || currentRegionalAgent === "") {
|
||||
return "unassign";
|
||||
}
|
||||
|
||||
return currentRegionalAgent;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -82,7 +102,7 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
||||
// Handle the unassign case by setting to empty string
|
||||
field.onChange(value === "unassign" ? "" : value);
|
||||
}}
|
||||
value={field.value || "unassign"}
|
||||
value={getSelectValue()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<FormControl>
|
||||
@@ -135,12 +155,12 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
|
||||
No online regional agents found. Services will use default monitoring.
|
||||
</p>
|
||||
)}
|
||||
{currentRegionalAgent && currentRegionalAgent !== "" && currentRegionalAgent !== "unassign" && (
|
||||
{currentRegionalAgent && currentRegionalAgent !== "" && (
|
||||
<p className="text-sm text-green-600">
|
||||
Currently assigned to: {getCurrentAgentDisplay()}
|
||||
</p>
|
||||
)}
|
||||
{(!currentRegionalAgent || currentRegionalAgent === "" || currentRegionalAgent === "unassign") && (
|
||||
{(!currentRegionalAgent || currentRegionalAgent === "") && regionalMonitoringEnabled && (
|
||||
<p className="text-sm text-orange-600">
|
||||
Service is unassigned and will use default monitoring.
|
||||
</p>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { uptimeService } from '@/services/uptimeService';
|
||||
@@ -167,14 +166,72 @@ export const useConsolidatedUptimeData = ({ serviceId, serviceType, status, inte
|
||||
if (uptimeData && uptimeData.length > 0) {
|
||||
console.log(`Processing consolidated uptime data for service ${serviceId}`);
|
||||
const processedData = processConsolidatedData(uptimeData);
|
||||
setConsolidatedItems(processedData);
|
||||
|
||||
// If service is currently paused, override ONLY the latest (first) bar with paused status
|
||||
if (status === "paused" && processedData.length > 0) {
|
||||
console.log(`Service ${serviceId} is paused, overriding latest bar with paused status`);
|
||||
|
||||
// Create a paused entry for the latest timestamp
|
||||
const latestTimestamp = new Date();
|
||||
latestTimestamp.setSeconds(0, 0); // Normalize to minute precision
|
||||
|
||||
const pausedSlot: ConsolidatedTimeSlot = {
|
||||
timestamp: latestTimestamp.toISOString(),
|
||||
items: [{
|
||||
id: `paused-${serviceId}-latest`,
|
||||
service_id: serviceId || "",
|
||||
serviceId: serviceId || "",
|
||||
timestamp: latestTimestamp.toISOString(),
|
||||
status: "paused" as "up" | "down" | "warning" | "paused",
|
||||
responseTime: 0,
|
||||
source: 'Default (Agent 1)',
|
||||
isDefault: true
|
||||
}]
|
||||
};
|
||||
|
||||
// Replace the first item with paused status, keep the rest as historical data
|
||||
const updatedData = [pausedSlot, ...processedData.slice(0, 19)];
|
||||
console.log(`Updated data with paused latest bar, total bars: ${updatedData.length}`);
|
||||
setConsolidatedItems(updatedData);
|
||||
} else {
|
||||
// Service is active (up/down/warning) - merge real data with any existing paused bars
|
||||
console.log(`Service ${serviceId} is active with status: ${status}, merging with existing paused bars`);
|
||||
|
||||
// Get existing paused bars from current state
|
||||
const existingPausedBars = consolidatedItems.filter(slot =>
|
||||
slot.items.some(item => item.status === "paused")
|
||||
);
|
||||
|
||||
// Create a map of existing paused timestamps for quick lookup
|
||||
const pausedTimestamps = new Set(existingPausedBars.map(slot => slot.timestamp));
|
||||
|
||||
// Merge processed data with existing paused bars
|
||||
const mergedData = [...processedData];
|
||||
|
||||
// Add back any paused bars that don't conflict with new data
|
||||
existingPausedBars.forEach(pausedSlot => {
|
||||
const hasConflict = processedData.some(slot => slot.timestamp === pausedSlot.timestamp);
|
||||
if (!hasConflict) {
|
||||
mergedData.push(pausedSlot);
|
||||
console.log(`Preserved paused bar at timestamp: ${pausedSlot.timestamp}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by timestamp (newest first) and limit to 20
|
||||
const finalData = mergedData
|
||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||||
.slice(0, 20);
|
||||
|
||||
console.log(`Final merged data has ${finalData.length} slots`);
|
||||
setConsolidatedItems(finalData);
|
||||
}
|
||||
} else if (!serviceId || (uptimeData && uptimeData.length === 0)) {
|
||||
// Generate placeholder data when no real data is available
|
||||
console.log(`No uptime data available for service ${serviceId}, generating placeholder`);
|
||||
console.log(`No uptime data available for service ${serviceId}, generating placeholder with status: ${status}`);
|
||||
|
||||
const statusValue = (status === "up" || status === "down" || status === "warning" || status === "paused")
|
||||
? status
|
||||
: "paused";
|
||||
// Use the actual service status for placeholder data
|
||||
const statusValue = status === "paused" ? "paused" :
|
||||
(status === "up" || status === "down" || status === "warning") ? status : "unknown";
|
||||
|
||||
const placeholderHistory: ConsolidatedTimeSlot[] = Array(20).fill(null).map((_, index) => {
|
||||
const timestamp = new Date(Date.now() - (index * interval * 1000));
|
||||
@@ -188,16 +245,17 @@ export const useConsolidatedUptimeData = ({ serviceId, serviceType, status, inte
|
||||
serviceId: serviceId || "",
|
||||
timestamp: timestamp.toISOString(),
|
||||
status: statusValue as "up" | "down" | "warning" | "paused",
|
||||
responseTime: 0,
|
||||
responseTime: statusValue === "paused" ? 0 : (statusValue === "up" ? 200 : 0),
|
||||
source: 'Default (Agent 1)',
|
||||
isDefault: true
|
||||
}]
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`Generated ${placeholderHistory.length} placeholder slots with status: ${statusValue}`);
|
||||
setConsolidatedItems(placeholderHistory);
|
||||
}
|
||||
}, [uptimeData, serviceId, status, interval]);
|
||||
}, [uptimeData, serviceId, status, interval, consolidatedItems]);
|
||||
|
||||
return {
|
||||
consolidatedItems,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ArrowLeft, Server, Database } from "lucide-react";
|
||||
import { ServerMetricsCharts } from "@/components/servers/ServerMetricsCharts";
|
||||
import { ServerMetricsOverview } from "@/components/servers/ServerMetricsOverview";
|
||||
import { ServerHistoryCharts } from "@/components/servers/ServerHistoryCharts";
|
||||
import { ServerSystemInfoCard } from "@/components/servers/ServerSystemInfoCard";
|
||||
|
||||
const ServerDetail = () => {
|
||||
const { serverId } = useParams();
|
||||
@@ -136,14 +137,18 @@ const ServerDetail = () => {
|
||||
Monitor server performance metrics and system health
|
||||
{server && (
|
||||
<span className="block text-xs text-foreground/80 mt-1">
|
||||
{server.hostname} • {server.ip_address} • {server.os_type} • {server.system_info}
|
||||
{server.hostname} • {server.ip_address} • {server.os_type}
|
||||
</span>
|
||||
|
||||
)}
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
{/* System Info Card */}
|
||||
{server && (
|
||||
<div className="flex-shrink-0">
|
||||
<ServerSystemInfoCard server={server} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -32,10 +32,11 @@ export const serviceService = {
|
||||
muteAlerts: item.alerts === "muted", // Convert string to boolean for compatibility
|
||||
alerts: item.alerts || "unmuted", // Store actual database field
|
||||
muteChangedAt: item.mute_changed_at,
|
||||
// Regional monitoring fields
|
||||
// Regional monitoring fields - use regional_status
|
||||
region_name: item.region_name || "",
|
||||
agent_id: item.agent_id || "",
|
||||
regional_monitoring_enabled: item.regional_monitoring_enabled || false,
|
||||
regional_status: item.regional_status || "disabled",
|
||||
regional_monitoring_enabled: item.regional_status === "enabled", // Backward compatibility
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Error fetching services:", error);
|
||||
@@ -62,8 +63,8 @@ export const serviceService = {
|
||||
max_retries: params.retries,
|
||||
notification_id: params.notificationChannel,
|
||||
template_id: params.alertTemplate,
|
||||
// Regional monitoring fields
|
||||
regional_monitoring_enabled: params.regionalMonitoringEnabled || false,
|
||||
// Regional monitoring fields - use regional_status
|
||||
regional_status: params.regionalStatus || "disabled",
|
||||
region_name: params.regionName || "",
|
||||
agent_id: params.agentId || "",
|
||||
// Conditionally add fields based on service type
|
||||
@@ -98,7 +99,8 @@ export const serviceService = {
|
||||
retries: record.max_retries || 3,
|
||||
notificationChannel: record.notification_id,
|
||||
alertTemplate: record.template_id,
|
||||
regional_monitoring_enabled: record.regional_monitoring_enabled || false,
|
||||
regional_status: record.regional_status || "disabled",
|
||||
regional_monitoring_enabled: record.regional_status === "enabled",
|
||||
region_name: record.region_name || "",
|
||||
agent_id: record.agent_id || "",
|
||||
} as Service;
|
||||
@@ -128,8 +130,8 @@ export const serviceService = {
|
||||
max_retries: params.retries,
|
||||
notification_id: params.notificationChannel || null,
|
||||
template_id: params.alertTemplate || null,
|
||||
// Regional monitoring fields
|
||||
regional_monitoring_enabled: params.regionalMonitoringEnabled || false,
|
||||
// Regional monitoring fields - use regional_status
|
||||
regional_status: params.regionalStatus || "disabled",
|
||||
region_name: params.regionName || "",
|
||||
agent_id: params.agentId || "",
|
||||
// Conditionally update fields based on service type
|
||||
@@ -171,7 +173,8 @@ export const serviceService = {
|
||||
retries: record.max_retries || 3,
|
||||
notificationChannel: record.notification_id,
|
||||
alertTemplate: record.template_id,
|
||||
regional_monitoring_enabled: record.regional_monitoring_enabled || false,
|
||||
regional_status: record.regional_status || "disabled",
|
||||
regional_monitoring_enabled: record.regional_status === "enabled",
|
||||
region_name: record.region_name || "",
|
||||
agent_id: record.agent_id || "",
|
||||
} as Service;
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface Service {
|
||||
// Regional monitoring fields
|
||||
region_name?: string;
|
||||
agent_id?: string;
|
||||
regional_status?: "enabled" | "disabled"; // Add regional_status field
|
||||
regional_monitoring_enabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -50,6 +51,7 @@ export interface CreateServiceParams {
|
||||
alertTemplate?: string;
|
||||
// Regional monitoring params
|
||||
regionalMonitoringEnabled?: boolean;
|
||||
regionalStatus?: "enabled" | "disabled"; // Add regionalStatus field
|
||||
regionName?: string;
|
||||
agentId?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user