Merge pull request #59 from operacle/develop
feat: Implement instance and docker monitoring dashboard and detail metrics page
This commit is contained in:
@@ -77,7 +77,7 @@ services:
|
||||
|
||||
###
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 📝 Development Roadmap
|
||||
@@ -92,10 +92,10 @@ services:
|
||||
- ✅ SSL & Domain Monitoring
|
||||
- ✅ Schedule Maintenance
|
||||
- ✅ Incident Management
|
||||
- [ ] Uptime monitoring (PING - Inprogress)
|
||||
- [ ] Infrastructure Server Monitoring
|
||||
- ✅ Operational Status / Public Status Pages
|
||||
- [ ] Uptime monitoring (TCP, PING, DNS)
|
||||
- ✅ Uptime monitoring (HTTP, TCP, PING, DNS) Full functionality
|
||||
- ✅ Distributed Regional Monitoring Agent [Support Operation](https://github.com/operacle/Distributed-Regional-Monitoring)
|
||||
- ✅ System Setting Panel and Mail Settings
|
||||
- ✅ User Permission Roles
|
||||
- [ ] Notifications (Email/Slack/Discord/Signal)
|
||||
|
||||
+61
-105
@@ -1,115 +1,71 @@
|
||||
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import React, { useState } from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ThemeProvider } from '@/contexts/ThemeContext';
|
||||
import { LanguageProvider } from '@/contexts/LanguageContext';
|
||||
import { SidebarProvider } from '@/contexts/SidebarContext';
|
||||
import { Toaster } from '@/components/ui/toaster';
|
||||
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
|
||||
// Pages
|
||||
import Index from '@/pages/Index';
|
||||
import Login from '@/pages/Login';
|
||||
import Dashboard from '@/pages/Dashboard';
|
||||
import ServiceDetail from '@/pages/ServiceDetail';
|
||||
import Settings from '@/pages/Settings';
|
||||
import Profile from '@/pages/Profile';
|
||||
import SslDomain from '@/pages/SslDomain';
|
||||
import ScheduleIncident from '@/pages/ScheduleIncident';
|
||||
import OperationalPage from '@/pages/OperationalPage';
|
||||
import PublicStatusPage from '@/pages/PublicStatusPage';
|
||||
import RegionalMonitoring from '@/pages/RegionalMonitoring';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import { LanguageProvider } from './contexts/LanguageContext';
|
||||
import { SidebarProvider } from './contexts/SidebarContext';
|
||||
|
||||
import { authService } from '@/services/authService';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 2,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Protected Route wrapper
|
||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||
const isAuthenticated = authService.isAuthenticated();
|
||||
return isAuthenticated ? <>{children}</> : <Navigate to="/login" replace />;
|
||||
};
|
||||
import Index from './pages/Index';
|
||||
import Login from './pages/Login';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import InstanceMonitoring from './pages/InstanceMonitoring';
|
||||
import ContainerMonitoring from './pages/ContainerMonitoring';
|
||||
import ServiceDetail from './pages/ServiceDetail';
|
||||
import SslDomain from './pages/SslDomain';
|
||||
import ScheduleIncident from './pages/ScheduleIncident';
|
||||
import OperationalPage from './pages/OperationalPage';
|
||||
import RegionalMonitoring from './pages/RegionalMonitoring';
|
||||
import Settings from './pages/Settings';
|
||||
import Profile from './pages/Profile';
|
||||
import NotFound from './pages/NotFound';
|
||||
import PublicStatusPage from './pages/PublicStatusPage';
|
||||
import { ProtectedRoute } from './components/auth/ProtectedRoute';
|
||||
import ServerDetail from './pages/ServerDetail';
|
||||
|
||||
function App() {
|
||||
const [queryClient] = useState(() => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<LanguageProvider>
|
||||
<SidebarProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/public/:slug" element={<PublicStatusPage />} />
|
||||
|
||||
{/* Protected Routes */}
|
||||
<Route path="/dashboard" element={
|
||||
<ProtectedRoute>
|
||||
<Dashboard />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/service/:id" element={
|
||||
<ProtectedRoute>
|
||||
<ServiceDetail />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/settings" element={
|
||||
<ProtectedRoute>
|
||||
<Settings />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/profile" element={
|
||||
<ProtectedRoute>
|
||||
<Profile />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/ssl-domain" element={
|
||||
<ProtectedRoute>
|
||||
<SslDomain />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/schedule-incident" element={
|
||||
<ProtectedRoute>
|
||||
<ScheduleIncident />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/operational-page" element={
|
||||
<ProtectedRoute>
|
||||
<OperationalPage />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/regional-monitoring" element={
|
||||
<ProtectedRoute>
|
||||
<RegionalMonitoring />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
<BrowserRouter>
|
||||
<ThemeProvider>
|
||||
<LanguageProvider>
|
||||
<SidebarProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Toaster />
|
||||
</SidebarProvider>
|
||||
</LanguageProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</ErrorBoundary>
|
||||
<Routes>
|
||||
{/* Public routes */}
|
||||
<Route path="/public/:slug" element={<PublicStatusPage />} />
|
||||
|
||||
{/* Protected routes */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={<ProtectedRoute><Index /></ProtectedRoute>} />
|
||||
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
|
||||
<Route path="/instance-monitoring" element={<ProtectedRoute><InstanceMonitoring /></ProtectedRoute>} />
|
||||
<Route path="/server-detail/:serverId" element={<ProtectedRoute><ServerDetail /></ProtectedRoute>} />
|
||||
<Route path="/container-monitoring" element={<ProtectedRoute><ContainerMonitoring /></ProtectedRoute>} />
|
||||
<Route path="/container-monitoring/:serverId" element={<ProtectedRoute><ContainerMonitoring /></ProtectedRoute>} />
|
||||
<Route path="/service/:id" element={<ProtectedRoute><ServiceDetail /></ProtectedRoute>} />
|
||||
<Route path="/ssl-domain" element={<ProtectedRoute><SslDomain /></ProtectedRoute>} />
|
||||
<Route path="/schedule-incident" element={<ProtectedRoute><ScheduleIncident /></ProtectedRoute>} />
|
||||
<Route path="/operational-page" element={<ProtectedRoute><OperationalPage /></ProtectedRoute>} />
|
||||
<Route path="/regional-monitoring" element={<ProtectedRoute><RegionalMonitoring /></ProtectedRoute>} />
|
||||
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
|
||||
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</QueryClientProvider>
|
||||
</SidebarProvider>
|
||||
</LanguageProvider>
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { authService } from '@/services/authService';
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
||||
if (!authService.isAuthenticated()) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import { Globe, Boxes, Radar, Calendar, BarChart2, LineChart, MapPin, Settings, User, Bell, Database, Info, BookOpen } from "lucide-react";
|
||||
|
||||
export const mainMenuItems = [
|
||||
@@ -11,11 +12,11 @@ export const mainMenuItems = [
|
||||
},
|
||||
{
|
||||
id: 'instance-monitoring',
|
||||
path: null,
|
||||
path: '/instance-monitoring',
|
||||
icon: Boxes,
|
||||
translationKey: 'instanceMonitoring',
|
||||
color: 'text-blue-400',
|
||||
hasNavigation: false
|
||||
hasNavigation: true
|
||||
},
|
||||
{
|
||||
id: 'ssl-domain',
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Table, TableBody } from "@/components/ui/table";
|
||||
import { DockerContainer } from "@/types/docker.types";
|
||||
import { DockerMetricsDialog } from "./DockerMetricsDialog";
|
||||
import {
|
||||
DockerTableSearch,
|
||||
DockerTableHeader,
|
||||
DockerTableRow,
|
||||
DockerEmptyState
|
||||
} from "./table";
|
||||
|
||||
interface DockerContainersTableProps {
|
||||
containers: DockerContainer[];
|
||||
isLoading: boolean;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
export const DockerContainersTable = ({ containers, isLoading, onRefresh }: DockerContainersTableProps) => {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [selectedContainer, setSelectedContainer] = useState<DockerContainer | null>(null);
|
||||
const [metricsDialogOpen, setMetricsDialogOpen] = useState(false);
|
||||
|
||||
const filteredContainers = containers.filter(container =>
|
||||
container.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
container.docker_id.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
container.hostname.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const handleContainerAction = (action: string, containerId: string, containerName: string) => {
|
||||
console.log(`${action} action for container ${containerName} (${containerId})`);
|
||||
// TODO: Implement container actions
|
||||
};
|
||||
|
||||
const handleRowClick = (container: DockerContainer) => {
|
||||
setSelectedContainer(container);
|
||||
setMetricsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleViewMetrics = (container: DockerContainer) => {
|
||||
setSelectedContainer(container);
|
||||
setMetricsDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="w-full bg-transparent border-0 shadow-none">
|
||||
<CardHeader className="pb-4 px-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<CardTitle className="text-lg sm:text-xl font-semibold">Docker Containers</CardTitle>
|
||||
<DockerTableSearch
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
onRefresh={onRefresh}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="min-w-full inline-block align-middle">
|
||||
<div className="overflow-hidden border border-border rounded-lg shadow-sm bg-card">
|
||||
<Table>
|
||||
<DockerTableHeader />
|
||||
<TableBody>
|
||||
{filteredContainers.length === 0 ? (
|
||||
<DockerEmptyState searchTerm={searchTerm} />
|
||||
) : (
|
||||
filteredContainers.map((container) => (
|
||||
<DockerTableRow
|
||||
key={container.id}
|
||||
container={container}
|
||||
onRowClick={handleRowClick}
|
||||
onContainerAction={handleContainerAction}
|
||||
onViewMetrics={handleViewMetrics}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<DockerMetricsDialog
|
||||
container={selectedContainer}
|
||||
open={metricsDialogOpen}
|
||||
onOpenChange={setMetricsDialogOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,695 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer, AreaChart, Area } from "recharts";
|
||||
import { DockerContainer, DockerMetrics } from "@/types/docker.types";
|
||||
import { dockerService } from "@/services/dockerService";
|
||||
import { Loader2, Cpu, HardDrive, Network, MemoryStick } from "lucide-react";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
interface DockerMetricsDialogProps {
|
||||
container: DockerContainer | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
type TimeRange = '60m' | '1d' | '7d' | '1m' | '3m';
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
export const DockerMetricsDialog = ({ container, open, onOpenChange }: DockerMetricsDialogProps) => {
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>("1d");
|
||||
const { theme } = useTheme();
|
||||
|
||||
const {
|
||||
data: metrics = [],
|
||||
isLoading,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['docker-metrics', container?.docker_id, timeRange],
|
||||
queryFn: () => container ? dockerService.getContainerMetrics(container.docker_id) : Promise.resolve([]),
|
||||
enabled: !!container && open,
|
||||
refetchInterval: 30000
|
||||
});
|
||||
|
||||
const formatBytes = (bytes: number, decimals = 2) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const parseValueWithUnit = (value: string | number): { numeric: number; unit: string; original: string } => {
|
||||
if (typeof value === 'number') {
|
||||
return { numeric: value, unit: 'B', original: value.toString() };
|
||||
}
|
||||
|
||||
const str = value.toString();
|
||||
const match = str.match(/^([\d.]+)\s*([A-Za-z%]*)/);
|
||||
if (match) {
|
||||
const numeric = parseFloat(match[1]);
|
||||
const unit = match[2] || '';
|
||||
return { numeric, unit, original: str };
|
||||
}
|
||||
return { numeric: 0, unit: '', original: str };
|
||||
};
|
||||
|
||||
const convertToBytes = (value: string | number): number => {
|
||||
if (typeof value === 'number') return value;
|
||||
|
||||
const parsed = parseValueWithUnit(value);
|
||||
const multipliers: { [key: string]: number } = {
|
||||
'B': 1,
|
||||
'KB': 1024,
|
||||
'MB': 1024 * 1024,
|
||||
'GB': 1024 * 1024 * 1024,
|
||||
'TB': 1024 * 1024 * 1024 * 1024
|
||||
};
|
||||
|
||||
const multiplier = multipliers[parsed.unit.toUpperCase()] || 1;
|
||||
return parsed.numeric * multiplier;
|
||||
};
|
||||
|
||||
const filterMetricsByTimeRange = (metrics: DockerMetrics[], timeRange: TimeRange): DockerMetrics[] => {
|
||||
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));
|
||||
|
||||
return metrics.filter(metric => {
|
||||
const metricTime = new Date(metric.timestamp);
|
||||
return metricTime >= cutoffTime;
|
||||
});
|
||||
};
|
||||
|
||||
const formatChartData = (metrics: DockerMetrics[]) => {
|
||||
const filteredMetrics = filterMetricsByTimeRange(metrics, timeRange);
|
||||
|
||||
return filteredMetrics.slice(0, 100).reverse().map((metric, index) => {
|
||||
// Parse CPU usage
|
||||
const cpuUsage = typeof metric.cpu_usage === 'string' ?
|
||||
parseFloat(metric.cpu_usage.replace('%', '')) :
|
||||
parseFloat(metric.cpu_usage) || 0;
|
||||
|
||||
// Parse memory values
|
||||
const ramUsedBytes = convertToBytes(metric.ram_used);
|
||||
const ramTotalBytes = convertToBytes(metric.ram_total);
|
||||
const ramFreeBytes = convertToBytes(metric.ram_free);
|
||||
const ramUsagePercent = ramTotalBytes > 0 ? (ramUsedBytes / ramTotalBytes) * 100 : 0;
|
||||
|
||||
// Parse disk values
|
||||
const diskUsedBytes = convertToBytes(metric.disk_used);
|
||||
const diskTotalBytes = convertToBytes(metric.disk_total);
|
||||
const diskFreeBytes = convertToBytes(metric.disk_free);
|
||||
const diskUsagePercent = diskTotalBytes > 0 ? (diskUsedBytes / diskTotalBytes) * 100 : 0;
|
||||
|
||||
// Network values
|
||||
const networkRxBytes = metric.network_rx_bytes || 0;
|
||||
const networkTxBytes = metric.network_tx_bytes || 0;
|
||||
const networkRxSpeed = metric.network_rx_speed || 0;
|
||||
const networkTxSpeed = metric.network_tx_speed || 0;
|
||||
|
||||
return {
|
||||
timestamp: new Date(metric.timestamp).toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}),
|
||||
// CPU
|
||||
cpuUsage: Math.round(cpuUsage * 100) / 100,
|
||||
cpuCores: parseInt(metric.cpu_cores) || 0,
|
||||
cpuFree: 100 - cpuUsage,
|
||||
|
||||
// Memory
|
||||
ramUsedBytes,
|
||||
ramTotalBytes,
|
||||
ramFreeBytes,
|
||||
ramUsed: formatBytes(ramUsedBytes),
|
||||
ramTotal: formatBytes(ramTotalBytes),
|
||||
ramFree: formatBytes(ramFreeBytes),
|
||||
ramUsagePercent: Math.round(ramUsagePercent * 100) / 100,
|
||||
|
||||
// Disk
|
||||
diskUsedBytes,
|
||||
diskTotalBytes,
|
||||
diskFreeBytes,
|
||||
diskUsed: formatBytes(diskUsedBytes),
|
||||
diskTotal: formatBytes(diskTotalBytes),
|
||||
diskFree: formatBytes(diskFreeBytes),
|
||||
diskUsagePercent: Math.round(diskUsagePercent * 100) / 100,
|
||||
|
||||
// Network
|
||||
networkRxBytes,
|
||||
networkTxBytes,
|
||||
networkRx: formatBytes(networkRxBytes),
|
||||
networkTx: formatBytes(networkTxBytes),
|
||||
networkRxSpeed: Math.round(networkRxSpeed * 100) / 100,
|
||||
networkTxSpeed: Math.round(networkTxSpeed * 100) / 100,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const chartData = formatChartData(metrics);
|
||||
const latestMetric = chartData[chartData.length - 1];
|
||||
|
||||
const chartConfig = {
|
||||
cpuUsage: {
|
||||
label: "CPU Usage (%)",
|
||||
color: theme === 'dark' ? "#3b82f6" : "#2563eb",
|
||||
},
|
||||
ramUsagePercent: {
|
||||
label: "RAM Usage (%)",
|
||||
color: theme === 'dark' ? "#10b981" : "#059669",
|
||||
},
|
||||
diskUsagePercent: {
|
||||
label: "Disk Usage (%)",
|
||||
color: theme === 'dark' ? "#f59e0b" : "#d97706",
|
||||
},
|
||||
networkRx: {
|
||||
label: "Network RX",
|
||||
color: theme === 'dark' ? "#8b5cf6" : "#7c3aed",
|
||||
},
|
||||
networkTx: {
|
||||
label: "Network TX",
|
||||
color: theme === 'dark' ? "#ef4444" : "#dc2626",
|
||||
},
|
||||
};
|
||||
|
||||
const getGridColor = () => theme === 'dark' ? '#374151' : '#e5e7eb';
|
||||
const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280';
|
||||
|
||||
const MetricCard = ({ title, used, total, free, percentage, icon: Icon, color }: {
|
||||
title: string;
|
||||
used: string;
|
||||
total: string;
|
||||
free: string;
|
||||
percentage: number;
|
||||
icon: any;
|
||||
color: string;
|
||||
}) => (
|
||||
<div className="bg-muted/30 rounded-lg p-4 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="h-4 w-4" style={{ color }} />
|
||||
<span className="text-sm font-medium">{title}</span>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Used:</span>
|
||||
<span className="font-mono">{used}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Free:</span>
|
||||
<span className="font-mono">{free}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Total:</span>
|
||||
<span className="font-mono">{total}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Usage:</span>
|
||||
<span className="font-mono">{percentage.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!container) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-7xl max-h-[95vh] overflow-y-auto bg-background text-foreground">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded bg-primary/10 flex items-center justify-center">
|
||||
<Cpu className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
Container Metrics: {container.name}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={timeRange} onValueChange={(value: TimeRange) => setTimeRange(value)}>
|
||||
<SelectTrigger className="w-[140px] h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{timeRangeOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Docker ID: {container.docker_id} • {container.hostname}
|
||||
</p>
|
||||
</DialogHeader>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-96">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
<span className="ml-2">Loading metrics...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex items-center justify-center h-96 text-muted-foreground">
|
||||
<p>Error loading metrics: {error.message}</p>
|
||||
</div>
|
||||
) : chartData.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-96 text-muted-foreground">
|
||||
<p>No metrics data available for this container</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Current Metrics Summary */}
|
||||
{latestMetric && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<MetricCard
|
||||
title="CPU"
|
||||
used={`${latestMetric.cpuUsage}%`}
|
||||
total={`${latestMetric.cpuCores} cores`}
|
||||
free={`${(100 - latestMetric.cpuUsage).toFixed(1)}%`}
|
||||
percentage={latestMetric.cpuUsage}
|
||||
icon={Cpu}
|
||||
color={chartConfig.cpuUsage.color}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Memory"
|
||||
used={latestMetric.ramUsed}
|
||||
total={latestMetric.ramTotal}
|
||||
free={latestMetric.ramFree}
|
||||
percentage={latestMetric.ramUsagePercent}
|
||||
icon={MemoryStick}
|
||||
color={chartConfig.ramUsagePercent.color}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Disk"
|
||||
used={latestMetric.diskUsed}
|
||||
total={latestMetric.diskTotal}
|
||||
free={latestMetric.diskFree}
|
||||
percentage={latestMetric.diskUsagePercent}
|
||||
icon={HardDrive}
|
||||
color={chartConfig.diskUsagePercent.color}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Network"
|
||||
used={`RX: ${latestMetric.networkRx}`}
|
||||
total={`TX: ${latestMetric.networkTx}`}
|
||||
free={`Speed: ${latestMetric.networkRxSpeed} KB/s`}
|
||||
percentage={0}
|
||||
icon={Network}
|
||||
color={chartConfig.networkRx.color}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Tabs defaultValue="cpu" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4 bg-muted">
|
||||
<TabsTrigger value="cpu" className="flex items-center gap-2 data-[state=active]:bg-background">
|
||||
<Cpu className="h-4 w-4" />
|
||||
CPU
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="memory" className="flex items-center gap-2 data-[state=active]:bg-background">
|
||||
<MemoryStick className="h-4 w-4" />
|
||||
Memory
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="disk" className="flex items-center gap-2 data-[state=active]:bg-background">
|
||||
<HardDrive className="h-4 w-4" />
|
||||
Disk
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="network" className="flex items-center gap-2 data-[state=active]:bg-background">
|
||||
<Network className="h-4 w-4" />
|
||||
Network
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="cpu" className="space-y-4 mt-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">CPU Usage (%)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-80">
|
||||
<LineChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
domain={[0, 100]}
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent className="bg-popover border-border" />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="cpuUsage"
|
||||
stroke={chartConfig.cpuUsage.color}
|
||||
strokeWidth={2}
|
||||
dot={{ r: 3, fill: chartConfig.cpuUsage.color }}
|
||||
name="CPU Usage (%)"
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">CPU Usage vs Available</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-80">
|
||||
<AreaChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
domain={[0, 100]}
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent className="bg-popover border-border" />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="cpuFree"
|
||||
stackId="1"
|
||||
stroke={theme === 'dark' ? "#6b7280" : "#9ca3af"}
|
||||
fill={theme === 'dark' ? "#6b7280" : "#9ca3af"}
|
||||
fillOpacity={0.3}
|
||||
name="CPU Available (%)"
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="cpuUsage"
|
||||
stackId="1"
|
||||
stroke={chartConfig.cpuUsage.color}
|
||||
fill={chartConfig.cpuUsage.color}
|
||||
fillOpacity={0.6}
|
||||
name="CPU Usage (%)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="memory" className="space-y-4 mt-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">Memory Usage (%)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-80">
|
||||
<AreaChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
domain={[0, 100]}
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent className="bg-popover border-border" />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="ramUsagePercent"
|
||||
stackId="1"
|
||||
stroke={chartConfig.ramUsagePercent.color}
|
||||
fill={chartConfig.ramUsagePercent.color}
|
||||
fillOpacity={0.6}
|
||||
name="RAM Usage (%)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">Memory Usage (Bytes)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-80">
|
||||
<AreaChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
tickFormatter={(value) => formatBytes(value)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent className="bg-popover border-border" />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
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={theme === 'dark' ? "#6b7280" : "#9ca3af"}
|
||||
fill={theme === 'dark' ? "#6b7280" : "#9ca3af"}
|
||||
fillOpacity={0.3}
|
||||
name="Total Memory"
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="ramUsedBytes"
|
||||
stackId="1"
|
||||
stroke={chartConfig.ramUsagePercent.color}
|
||||
fill={chartConfig.ramUsagePercent.color}
|
||||
fillOpacity={0.6}
|
||||
name="Used Memory"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="disk" className="space-y-4 mt-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">Disk Usage (%)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-80">
|
||||
<AreaChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
domain={[0, 100]}
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent className="bg-popover border-border" />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="diskUsagePercent"
|
||||
stackId="1"
|
||||
stroke={chartConfig.diskUsagePercent.color}
|
||||
fill={chartConfig.diskUsagePercent.color}
|
||||
fillOpacity={0.6}
|
||||
name="Disk Usage (%)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">Disk Usage (Bytes)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-80">
|
||||
<AreaChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
tickFormatter={(value) => formatBytes(value)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent className="bg-popover border-border" />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
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={theme === 'dark' ? "#6b7280" : "#9ca3af"}
|
||||
fill={theme === 'dark' ? "#6b7280" : "#9ca3af"}
|
||||
fillOpacity={0.3}
|
||||
name="Total Disk"
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="diskUsedBytes"
|
||||
stackId="1"
|
||||
stroke={chartConfig.diskUsagePercent.color}
|
||||
fill={chartConfig.diskUsagePercent.color}
|
||||
fillOpacity={0.6}
|
||||
name="Used Disk"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="network" className="space-y-4 mt-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">Network Traffic</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-64">
|
||||
<LineChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent className="bg-popover border-border" />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="networkRxBytes"
|
||||
stroke={chartConfig.networkRx.color}
|
||||
strokeWidth={2}
|
||||
name="RX Bytes"
|
||||
dot={{ r: 2 }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="networkTxBytes"
|
||||
stroke={chartConfig.networkTx.color}
|
||||
strokeWidth={2}
|
||||
name="TX Bytes"
|
||||
dot={{ r: 2 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-foreground">Network Speed (KB/s)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-64">
|
||||
<LineChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={getGridColor()} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 12, fill: getAxisColor() }}
|
||||
axisLine={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent className="bg-popover border-border" />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="networkRxSpeed"
|
||||
stroke={chartConfig.networkRx.color}
|
||||
strokeWidth={2}
|
||||
name="RX Speed (KB/s)"
|
||||
dot={{ r: 2 }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="networkTxSpeed"
|
||||
stroke={chartConfig.networkTx.color}
|
||||
strokeWidth={2}
|
||||
name="TX Speed (KB/s)"
|
||||
dot={{ r: 2 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Container, Play, Square, AlertTriangle } from "lucide-react";
|
||||
import { DockerStats } from "@/types/docker.types";
|
||||
|
||||
interface DockerStatsCardsProps {
|
||||
stats: DockerStats;
|
||||
}
|
||||
|
||||
export const DockerStatsCards = ({ stats }: DockerStatsCardsProps) => {
|
||||
const cards = [
|
||||
{
|
||||
title: "Total Containers",
|
||||
value: stats.total,
|
||||
icon: Container,
|
||||
color: "text-blue-600",
|
||||
bgColor: "bg-blue-50",
|
||||
borderColor: "border-blue-200",
|
||||
},
|
||||
{
|
||||
title: "Running",
|
||||
value: stats.running,
|
||||
icon: Play,
|
||||
color: "text-green-600",
|
||||
bgColor: "bg-green-50",
|
||||
borderColor: "border-green-200",
|
||||
},
|
||||
{
|
||||
title: "Stopped",
|
||||
value: stats.stopped,
|
||||
icon: Square,
|
||||
color: "text-gray-600",
|
||||
bgColor: "bg-gray-50",
|
||||
borderColor: "border-gray-200",
|
||||
},
|
||||
{
|
||||
title: "Warning",
|
||||
value: stats.warning,
|
||||
icon: AlertTriangle,
|
||||
color: "text-amber-600",
|
||||
bgColor: "bg-amber-50",
|
||||
borderColor: "border-amber-200",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6">
|
||||
{cards.map((card) => {
|
||||
const IconComponent = card.icon;
|
||||
return (
|
||||
<Card key={card.title} className={`${card.borderColor} bg-card hover:shadow-md transition-shadow`}>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{card.title}
|
||||
</CardTitle>
|
||||
<div className={`${card.bgColor} ${card.color} p-2 rounded-md`}>
|
||||
<IconComponent className="h-4 w-4" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
{card.value}
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${card.color} ${card.borderColor} text-xs`}
|
||||
>
|
||||
Containers
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface DockerStatusBadgeProps {
|
||||
status: 'running' | 'stopped' | 'warning';
|
||||
}
|
||||
|
||||
export const DockerStatusBadge = ({ status }: DockerStatusBadgeProps) => {
|
||||
const getStatusConfig = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return {
|
||||
variant: 'default' as const,
|
||||
className: 'bg-emerald-100 text-emerald-800 border-emerald-200 hover:bg-emerald-200',
|
||||
label: 'Running'
|
||||
};
|
||||
case 'stopped':
|
||||
return {
|
||||
variant: 'secondary' as const,
|
||||
className: 'bg-gray-100 text-gray-800 border-gray-200 hover:bg-gray-200',
|
||||
label: 'Stopped'
|
||||
};
|
||||
case 'warning':
|
||||
return {
|
||||
variant: 'destructive' as const,
|
||||
className: 'bg-amber-100 text-amber-800 border-amber-200 hover:bg-amber-200',
|
||||
label: 'Warning'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
variant: 'outline' as const,
|
||||
className: 'bg-gray-100 text-gray-600 border-gray-200',
|
||||
label: 'Unknown'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getStatusConfig(status);
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant={config.variant}
|
||||
className={`${config.className} font-medium text-xs px-2 py-1`}
|
||||
>
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
import { TableCell, TableRow } from "@/components/ui/table";
|
||||
|
||||
interface DockerEmptyStateProps {
|
||||
searchTerm: string;
|
||||
}
|
||||
|
||||
export const DockerEmptyState = ({ searchTerm }: DockerEmptyStateProps) => {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-12 text-muted-foreground">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="text-lg font-medium">
|
||||
{searchTerm ? "No containers found" : "No containers running"}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{searchTerm ? "Try adjusting your search terms." : "Start some containers to see them here."}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
|
||||
import { MoreHorizontal, Eye, Play, Pause, Square, Trash2, BarChart3, RefreshCw } from "lucide-react";
|
||||
import { DockerContainer } from "@/types/docker.types";
|
||||
|
||||
interface DockerRowActionsProps {
|
||||
container: DockerContainer;
|
||||
containerStatus: 'running' | 'stopped' | 'warning';
|
||||
onContainerAction: (action: string, containerId: string, containerName: string) => void;
|
||||
onViewMetrics: (container: DockerContainer) => void;
|
||||
}
|
||||
|
||||
export const DockerRowActions = ({ container, containerStatus, onContainerAction, onViewMetrics }: DockerRowActionsProps) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0 hover:bg-muted">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48 bg-popover border-border shadow-md">
|
||||
<DropdownMenuItem
|
||||
onClick={() => onViewMetrics(container)}
|
||||
className="cursor-pointer hover:bg-muted"
|
||||
>
|
||||
<BarChart3 className="mr-2 h-4 w-4" />
|
||||
View Metrics
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onContainerAction('view-detail', container.id, container.name)}
|
||||
className="cursor-pointer hover:bg-muted"
|
||||
>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
View Details
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="bg-border" />
|
||||
<DropdownMenuItem
|
||||
onClick={() => onContainerAction(containerStatus === 'running' ? 'stop' : 'start', container.id, container.name)}
|
||||
className="cursor-pointer hover:bg-muted"
|
||||
>
|
||||
{containerStatus === 'running' ? (
|
||||
<>
|
||||
<Square className="mr-2 h-4 w-4" />
|
||||
Stop Container
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Start Container
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onContainerAction('restart', container.id, container.name)}
|
||||
className="cursor-pointer hover:bg-muted"
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Restart Container
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="bg-border" />
|
||||
<DropdownMenuItem
|
||||
onClick={() => onContainerAction('delete', container.id, container.name)}
|
||||
className="cursor-pointer hover:bg-muted text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Remove Container
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
import { TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
|
||||
export const DockerTableHeader = () => {
|
||||
return (
|
||||
<TableHeader>
|
||||
<TableRow className="border-border bg-muted/30">
|
||||
<TableHead className="min-w-[200px] font-semibold">Container</TableHead>
|
||||
<TableHead className="min-w-[100px] font-semibold">Status</TableHead>
|
||||
<TableHead className="min-w-[140px] font-semibold">CPU Usage</TableHead>
|
||||
<TableHead className="min-w-[160px] font-semibold">Memory</TableHead>
|
||||
<TableHead className="min-w-[160px] font-semibold">Disk</TableHead>
|
||||
<TableHead className="min-w-[100px] font-semibold">Uptime</TableHead>
|
||||
<TableHead className="min-w-[160px] font-semibold">Last Checked</TableHead>
|
||||
<TableHead className="min-w-[80px] text-center font-semibold">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
|
||||
import { TableCell, TableRow } from "@/components/ui/table";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { DockerContainer } from "@/types/docker.types";
|
||||
import { DockerStatusBadge } from "../DockerStatusBadge";
|
||||
import { DockerRowActions } from "./DockerRowActions";
|
||||
import { dockerService } from "@/services/dockerService";
|
||||
|
||||
interface DockerTableRowProps {
|
||||
container: DockerContainer;
|
||||
onRowClick: (container: DockerContainer) => void;
|
||||
onContainerAction: (action: string, containerId: string, containerName: string) => void;
|
||||
onViewMetrics: (container: DockerContainer) => void;
|
||||
}
|
||||
|
||||
export const DockerTableRow = ({ container, onRowClick, onContainerAction, onViewMetrics }: DockerTableRowProps) => {
|
||||
const cpuPercentage = container.cpu_usage;
|
||||
const memoryPercentage = Math.round((container.ram_used / container.ram_total) * 100);
|
||||
const diskPercentage = Math.round((container.disk_used / container.disk_total) * 100);
|
||||
const containerStatus = dockerService.getStatusFromDockerStatus(container.status);
|
||||
|
||||
const formatPercentage = (used: number, total: number) => {
|
||||
if (total === 0) return "0%";
|
||||
return `${Math.round((used / total) * 100)}%`;
|
||||
};
|
||||
|
||||
const getUsageColor = (percentage: number) => {
|
||||
if (percentage >= 90) return "text-red-500";
|
||||
if (percentage >= 70) return "text-amber-500";
|
||||
return "text-emerald-500";
|
||||
};
|
||||
|
||||
const getProgressColor = (percentage: number) => {
|
||||
if (percentage >= 90) return "bg-red-500";
|
||||
if (percentage >= 70) return "bg-amber-500";
|
||||
return "bg-emerald-500";
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
className="hover:bg-muted/50 transition-colors border-border cursor-pointer"
|
||||
onClick={() => onRowClick(container)}
|
||||
>
|
||||
<TableCell className="font-medium">
|
||||
<div className="space-y-1">
|
||||
<div className="font-semibold text-sm sm:text-base text-foreground">{container.name}</div>
|
||||
<div className="text-xs sm:text-sm text-muted-foreground">
|
||||
<div className="font-mono">{container.docker_id}</div>
|
||||
<div className="font-mono">{container.hostname}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DockerStatusBadge status={containerStatus} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Progress
|
||||
value={cpuPercentage}
|
||||
className="flex-1 h-2 bg-muted/50"
|
||||
indicatorClassName={getProgressColor(cpuPercentage)}
|
||||
/>
|
||||
<span className={`font-semibold text-sm min-w-[40px] text-right ${getUsageColor(cpuPercentage)}`}>
|
||||
{cpuPercentage}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Progress
|
||||
value={memoryPercentage}
|
||||
className="flex-1 h-2 bg-muted/50"
|
||||
indicatorClassName={getProgressColor(memoryPercentage)}
|
||||
/>
|
||||
<span className={`font-semibold text-sm min-w-[40px] text-right ${getUsageColor(memoryPercentage)}`}>
|
||||
{formatPercentage(container.ram_used, container.ram_total)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground font-mono">
|
||||
{dockerService.formatBytes(container.ram_used)} / {dockerService.formatBytes(container.ram_total)}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Progress
|
||||
value={diskPercentage}
|
||||
className="flex-1 h-2 bg-muted/50"
|
||||
indicatorClassName={getProgressColor(diskPercentage)}
|
||||
/>
|
||||
<span className={`font-semibold text-sm min-w-[40px] text-right ${getUsageColor(diskPercentage)}`}>
|
||||
{formatPercentage(container.disk_used, container.disk_total)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground font-mono">
|
||||
{dockerService.formatBytes(container.disk_used)} / {dockerService.formatBytes(container.disk_total)}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-xs sm:text-sm font-medium font-mono">
|
||||
{dockerService.formatUptime(container.uptime)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-xs sm:text-sm text-muted-foreground">
|
||||
{new Date(container.last_checked).toLocaleString()}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
<DockerRowActions
|
||||
container={container}
|
||||
containerStatus={containerStatus}
|
||||
onContainerAction={onContainerAction}
|
||||
onViewMetrics={onViewMetrics}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search, RefreshCw } from "lucide-react";
|
||||
|
||||
interface DockerTableSearchProps {
|
||||
searchTerm: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
onRefresh: () => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const DockerTableSearch = ({ searchTerm, onSearchChange, onRefresh, isLoading }: DockerTableSearchProps) => {
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row gap-3 w-full sm:w-auto">
|
||||
<div className="relative flex-1 sm:flex-initial">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Search containers..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="pl-10 sm:w-64 bg-background border-border"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading}
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="w-full sm:w-auto bg-background border-border hover:bg-muted"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
<span className="sm:inline">Refresh</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
export { DockerTableSearch } from './DockerTableSearch';
|
||||
export { DockerTableHeader } from './DockerTableHeader';
|
||||
export { DockerTableRow } from './DockerTableRow';
|
||||
export { DockerRowActions } from './DockerRowActions';
|
||||
export { DockerEmptyState } from './DockerEmptyState';
|
||||
@@ -250,7 +250,7 @@ export const AddRegionalAgentDialog: React.FC<AddRegionalAgentDialogProps> = ({
|
||||
<Textarea
|
||||
readOnly
|
||||
value={`# One-click installation command:
|
||||
curl -fsSL https://raw.githubusercontent.com/checkcle/scripts/main/install.sh | sudo bash -s -- \\
|
||||
curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/install-regional-agent.sh | sudo bash -s -- \\
|
||||
--region-name="${regionName}" \\
|
||||
--agent-id="${installCommand.agent_id}" \\
|
||||
--agent-ip="${agentIp}" \\
|
||||
@@ -262,7 +262,7 @@ curl -fsSL https://raw.githubusercontent.com/checkcle/scripts/main/install.sh |
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="absolute top-2 right-2"
|
||||
onClick={() => copyToClipboard(`curl -fsSL https://raw.githubusercontent.com/checkcle/scripts/main/install.sh | sudo bash -s -- --region-name="${regionName}" --agent-id="${installCommand.agent_id}" --agent-ip="${agentIp}" --token="${installCommand.token}" --pocketbase-url="${installCommand.api_endpoint}"`, "Installation command")}
|
||||
onClick={() => copyToClipboard(`curl -fsSL https://raw.githubusercontent.com/operacle/checkcle/refs/heads/main/scripts/install-regional-agent.sh | sudo bash -s -- --region-name="${regionName}" --agent-id="${installCommand.agent_id}" --agent-ip="${agentIp}" --token="${installCommand.token}" --pocketbase-url="${installCommand.api_endpoint}"`, "Installation command")}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -395,7 +395,12 @@ curl -fsSL https://raw.githubusercontent.com/checkcle/scripts/main/install.sh |
|
||||
<div className="border-l-4 border-blue-500 pl-4">
|
||||
<p className="font-medium">Step 1: Download Package</p>
|
||||
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||
wget https://github.com/checkcle/distributed-regional-monitoring/releases/latest/download/distributed-regional-check-agent_1.0.0_amd64.deb
|
||||
<p className="text-xs text-muted-foreground mt-1">✅ If you’re using the amd64 architecture, please download this package. </p> <br/>
|
||||
wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_amd64.deb
|
||||
</code>
|
||||
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||
<p className="text-xs text-muted-foreground mt-1">✅ If you’re using the arm64 architecture, please download this package. </p> <br/>
|
||||
wget https://github.com/operacle/Distributed-Regional-Monitoring/releases/download/V1.0.0/distributed-regional-check-agent_1.0.0_arm64.deb
|
||||
</code>
|
||||
</div>
|
||||
|
||||
@@ -412,6 +417,9 @@ curl -fsSL https://raw.githubusercontent.com/checkcle/scripts/main/install.sh |
|
||||
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||
sudo nano /etc/regional-check-agent/regional-check-agent.env
|
||||
</code>
|
||||
<code className="text-xs bg-muted p-2 rounded block mt-1">
|
||||
Copy sample configuration from https://github.com/operacle/Distributed-Regional-Monitoring/blob/main/packaging/regional-check-agent.conf
|
||||
</code>
|
||||
<p className="text-xs text-muted-foreground mt-1">Add the configuration values shown in the Agent Details tab</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
import { Monitor, Server, Smartphone, Laptop } from "lucide-react";
|
||||
|
||||
interface OSTypeIconProps {
|
||||
osType: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const OSTypeIcon = ({ osType, className = "h-4 w-4" }: OSTypeIconProps) => {
|
||||
const getOSIcon = (os: string) => {
|
||||
const osLower = os.toLowerCase();
|
||||
|
||||
if (osLower.includes('linux') || osLower.includes('ubuntu') || osLower.includes('centos')) {
|
||||
return <Server className={className} />;
|
||||
} else if (osLower.includes('windows')) {
|
||||
return <Monitor className={className} />;
|
||||
} else if (osLower.includes('mac') || osLower.includes('darwin')) {
|
||||
return <Laptop className={className} />;
|
||||
} else if (osLower.includes('android') || osLower.includes('ios')) {
|
||||
return <Smartphone className={className} />;
|
||||
} else {
|
||||
return <Server className={className} />;
|
||||
}
|
||||
};
|
||||
|
||||
return getOSIcon(osType);
|
||||
};
|
||||
@@ -0,0 +1,467 @@
|
||||
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, 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 { DateRangeFilter } from "@/components/services/DateRangeFilter";
|
||||
import { useState } from "react";
|
||||
|
||||
interface ServerHistoryChartsProps {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export const ServerHistoryCharts = ({ serverId }: ServerHistoryChartsProps) => {
|
||||
const { theme } = useTheme();
|
||||
const [dateRange, setDateRange] = useState({ start: new Date(Date.now() - 24 * 60 * 60 * 1000), end: new Date() });
|
||||
|
||||
console.log('ServerHistoryCharts: Rendering with serverId:', serverId);
|
||||
|
||||
const {
|
||||
data: metrics = [],
|
||||
isLoading,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['server-metrics-history', serverId],
|
||||
queryFn: async () => {
|
||||
console.log('ServerHistoryCharts: Fetching metrics for serverId:', serverId);
|
||||
const result = await serverService.getServerMetrics(serverId);
|
||||
console.log('ServerHistoryCharts: Raw metrics result:', result);
|
||||
return result;
|
||||
},
|
||||
enabled: !!serverId,
|
||||
refetchInterval: 60000, // Refresh every minute
|
||||
retry: 1
|
||||
});
|
||||
|
||||
const handleDateRangeChange = (startDate: Date, endDate: Date, option: any) => {
|
||||
setDateRange({ start: startDate, end: endDate });
|
||||
console.log('Date range changed:', { startDate, endDate, option });
|
||||
};
|
||||
|
||||
console.log('ServerHistoryCharts: Query state:', {
|
||||
metricsCount: metrics.length,
|
||||
isLoading,
|
||||
error: error?.message,
|
||||
firstMetric: metrics[0],
|
||||
serverId
|
||||
});
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const parseValueWithUnit = (value: string | number) => {
|
||||
if (typeof value === 'number') return value;
|
||||
const match = value.toString().match(/^([\d.]+)/);
|
||||
return match ? parseFloat(match[1]) : 0;
|
||||
};
|
||||
|
||||
const convertToBytes = (value: string | number): number => {
|
||||
if (typeof value === 'number') return value;
|
||||
|
||||
const str = value.toString();
|
||||
const numMatch = str.match(/^([\d.]+)/);
|
||||
const unitMatch = str.match(/([A-Za-z]+)$/);
|
||||
|
||||
if (!numMatch) return 0;
|
||||
|
||||
const num = parseFloat(numMatch[1]);
|
||||
const unit = unitMatch ? unitMatch[1].toUpperCase() : 'B';
|
||||
|
||||
const multipliers: { [key: string]: number } = {
|
||||
'B': 1,
|
||||
'KB': 1024,
|
||||
'MB': 1024 * 1024,
|
||||
'GB': 1024 * 1024 * 1024,
|
||||
'TB': 1024 * 1024 * 1024 * 1024
|
||||
};
|
||||
|
||||
return num * (multipliers[unit] || 1);
|
||||
};
|
||||
|
||||
const formatChartData = (metrics: any[]) => {
|
||||
console.log('ServerHistoryCharts: Formatting chart data for', metrics.length, 'metrics');
|
||||
|
||||
if (!Array.isArray(metrics) || metrics.length === 0) {
|
||||
console.log('ServerHistoryCharts: No metrics to format');
|
||||
return [];
|
||||
}
|
||||
|
||||
const formattedData = metrics.slice(-50).reverse().map((metric, index) => {
|
||||
console.log(`ServerHistoryCharts: Processing metric ${index}:`, metric);
|
||||
|
||||
const cpuUsage = parseValueWithUnit(metric.cpu_usage || 0);
|
||||
const ramUsedBytes = convertToBytes(metric.ram_used || 0);
|
||||
const ramTotalBytes = convertToBytes(metric.ram_total || 0);
|
||||
const diskUsedBytes = convertToBytes(metric.disk_used || 0);
|
||||
const diskTotalBytes = convertToBytes(metric.disk_total || 0);
|
||||
|
||||
const ramUsagePercent = ramTotalBytes > 0 ? (ramUsedBytes / ramTotalBytes) * 100 : 0;
|
||||
const diskUsagePercent = diskTotalBytes > 0 ? (diskUsedBytes / diskTotalBytes) * 100 : 0;
|
||||
|
||||
return {
|
||||
timestamp: new Date(metric.timestamp || metric.created).toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}),
|
||||
fullTimestamp: new Date(metric.timestamp || metric.created).toLocaleString(),
|
||||
cpuUsage: Math.round(cpuUsage * 100) / 100,
|
||||
ramUsagePercent: Math.round(ramUsagePercent * 100) / 100,
|
||||
diskUsagePercent: Math.round(diskUsagePercent * 100) / 100,
|
||||
ramUsedBytes,
|
||||
ramTotalBytes,
|
||||
diskUsedBytes,
|
||||
diskTotalBytes,
|
||||
networkRxBytes: metric.network_rx_bytes || 0,
|
||||
networkTxBytes: metric.network_tx_bytes || 0,
|
||||
networkRxSpeed: parseValueWithUnit(metric.network_rx_speed || 0),
|
||||
networkTxSpeed: parseValueWithUnit(metric.network_tx_speed || 0),
|
||||
};
|
||||
});
|
||||
|
||||
console.log('ServerHistoryCharts: Formatted chart data:', formattedData);
|
||||
return formattedData;
|
||||
};
|
||||
|
||||
const chartData = formatChartData(metrics);
|
||||
|
||||
const getGridColor = () => theme === 'dark' ? '#374151' : '#e5e7eb';
|
||||
const getAxisColor = () => theme === 'dark' ? '#9ca3af' : '#6b7280';
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<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>
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error('ServerHistoryCharts: Error loading data:', error);
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
</div>
|
||||
<DateRangeFilter onRangeChange={handleDateRangeChange} />
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<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>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (chartData.length === 0) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
</div>
|
||||
<DateRangeFilter onRangeChange={handleDateRangeChange} />
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<p className="text-muted-foreground">No historical data available</p>
|
||||
<p className="text-xs mt-2">Metrics count: {metrics.length}</p>
|
||||
<p className="text-xs mt-1">Server ID: {serverId}</p>
|
||||
<p className="text-xs mt-1 text-muted-foreground">
|
||||
{metrics.length > 0 ? 'Data exists but failed to format' : 'No metrics data found'}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
console.log('ServerHistoryCharts: Rendering individual charts with', chartData.length, 'data points');
|
||||
|
||||
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)</span>
|
||||
</div>
|
||||
<DateRangeFilter onRangeChange={handleDateRangeChange} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* CPU Usage Chart */}
|
||||
<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 gap-2 text-foreground">
|
||||
<div className="p-2 rounded-lg bg-blue-500/15">
|
||||
<Cpu className="h-5 w-5 text-blue-500" />
|
||||
</div>
|
||||
CPU Usage
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-2">
|
||||
<ChartContainer config={{
|
||||
cpuUsage: {
|
||||
label: "CPU Usage (%)",
|
||||
color: theme === 'dark' ? "#3b82f6" : "#2563eb",
|
||||
}
|
||||
}} className="h-80">
|
||||
<LineChart data={chartData}>
|
||||
<defs>
|
||||
<linearGradient id="cpuGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={theme === 'dark' ? "#3b82f6" : "#2563eb"} stopOpacity={0.3}/>
|
||||
<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={<ChartTooltipContent className="bg-popover border-border" />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="cpuUsage"
|
||||
stroke={theme === 'dark' ? "#60a5fa" : "#2563eb"}
|
||||
strokeWidth={3}
|
||||
dot={false}
|
||||
fill="url(#cpuGradient)"
|
||||
fillOpacity={1}
|
||||
name="CPU Usage (%)"
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Memory Usage Chart */}
|
||||
<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 gap-2 text-foreground">
|
||||
<div className="p-2 rounded-lg bg-green-500/15">
|
||||
<MemoryStick className="h-5 w-5 text-green-500" />
|
||||
</div>
|
||||
Memory Usage
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-2">
|
||||
<ChartContainer config={{
|
||||
ramUsagePercent: {
|
||||
label: "Memory Usage (%)",
|
||||
color: theme === 'dark' ? "#10b981" : "#059669",
|
||||
}
|
||||
}} className="h-80">
|
||||
<LineChart data={chartData}>
|
||||
<defs>
|
||||
<linearGradient id="memoryGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={theme === 'dark' ? "#10b981" : "#059669"} stopOpacity={0.3}/>
|
||||
<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={<ChartTooltipContent className="bg-popover border-border" />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="ramUsagePercent"
|
||||
stroke={theme === 'dark' ? "#34d399" : "#059669"}
|
||||
strokeWidth={3}
|
||||
dot={false}
|
||||
fill="url(#memoryGradient)"
|
||||
fillOpacity={1}
|
||||
name="Memory Usage (%)"
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Disk Usage Chart */}
|
||||
<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 gap-2 text-foreground">
|
||||
<div className="p-2 rounded-lg bg-amber-500/15">
|
||||
<HardDrive className="h-5 w-5 text-amber-500" />
|
||||
</div>
|
||||
Disk Usage
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-2">
|
||||
<ChartContainer config={{
|
||||
diskUsagePercent: {
|
||||
label: "Disk Usage (%)",
|
||||
color: theme === 'dark' ? "#f59e0b" : "#d97706",
|
||||
}
|
||||
}} className="h-80">
|
||||
<LineChart data={chartData}>
|
||||
<defs>
|
||||
<linearGradient id="diskGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={theme === 'dark' ? "#f59e0b" : "#d97706"} stopOpacity={0.3}/>
|
||||
<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={<ChartTooltipContent className="bg-popover border-border" />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="diskUsagePercent"
|
||||
stroke={theme === 'dark' ? "#fbbf24" : "#d97706"}
|
||||
strokeWidth={3}
|
||||
dot={false}
|
||||
fill="url(#diskGradient)"
|
||||
fillOpacity={1}
|
||||
name="Disk Usage (%)"
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Network Traffic Chart */}
|
||||
<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 gap-2 text-foreground">
|
||||
<div className="p-2 rounded-lg bg-purple-500/15">
|
||||
<Wifi className="h-5 w-5 text-purple-500" />
|
||||
</div>
|
||||
Network Traffic
|
||||
</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>
|
||||
</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() }}
|
||||
tickFormatter={(value) => formatBytes(value) + '/s'}
|
||||
label={{ value: 'Network Speed', angle: -90, position: 'insideLeft' }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent className="bg-popover border-border" />}
|
||||
cursor={{ stroke: getGridColor() }}
|
||||
formatter={(value, name) => [formatBytes(Number(value)) + '/s', name]}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="networkRxSpeed"
|
||||
stroke={theme === 'dark' ? "#a78bfa" : "#7c3aed"}
|
||||
strokeWidth={3}
|
||||
dot={false}
|
||||
fill="url(#networkGradient)"
|
||||
fillOpacity={0.5}
|
||||
name="RX Speed"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="networkTxSpeed"
|
||||
stroke={theme === 'dark' ? "#f87171" : "#dc2626"}
|
||||
strokeWidth={3}
|
||||
dot={false}
|
||||
name="TX Speed"
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
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";
|
||||
|
||||
interface ServerMetricsChartsProps {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
type TimeRange = '60m' | '1d' | '7d' | '1m' | '3m';
|
||||
|
||||
export const ServerMetricsCharts = ({ serverId }: ServerMetricsChartsProps) => {
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>("1d");
|
||||
|
||||
const {
|
||||
data: metrics = [],
|
||||
isLoading,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['server-metrics', serverId, timeRange],
|
||||
queryFn: () => serverService.getServerMetrics(serverId),
|
||||
enabled: !!serverId,
|
||||
refetchInterval: 30000
|
||||
});
|
||||
|
||||
const chartData = formatChartData(metrics, timeRange);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-96">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
<span className="ml-2">Loading server metrics...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-96 text-muted-foreground">
|
||||
<p>Error loading server metrics: {error.message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (chartData.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-96 text-muted-foreground">
|
||||
<p>No server metrics data available</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-medium">Server Metrics</h2>
|
||||
<span className="text-xs text-muted-foreground">({chartData.length} data points)</span>
|
||||
</div>
|
||||
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="cpu" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4 bg-muted/50 backdrop-blur-sm">
|
||||
<TabsTrigger value="cpu" className="flex items-center gap-2 data-[state=active]:bg-background data-[state=active]:shadow-sm">
|
||||
<Cpu className="h-4 w-4" />
|
||||
CPU
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="memory" className="flex items-center gap-2 data-[state=active]:bg-background data-[state=active]:shadow-sm">
|
||||
<MemoryStick className="h-4 w-4" />
|
||||
Memory
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="disk" className="flex items-center gap-2 data-[state=active]:bg-background data-[state=active]:shadow-sm">
|
||||
<HardDrive className="h-4 w-4" />
|
||||
Disk
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="network" className="flex items-center gap-2 data-[state=active]:bg-background data-[state=active]:shadow-sm">
|
||||
<Network className="h-4 w-4" />
|
||||
Network
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="cpu" className="space-y-4 mt-6">
|
||||
<CPUCharts data={chartData} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="memory" className="space-y-4 mt-6">
|
||||
<MemoryCharts data={chartData} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="disk" className="space-y-4 mt-6">
|
||||
<DiskCharts data={chartData} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="network" className="space-y-4 mt-6">
|
||||
<NetworkCharts data={chartData} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,249 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Server } from "@/types/server.types";
|
||||
import { Cpu, HardDrive, MemoryStick, Clock, Activity, Info } from "lucide-react";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
interface ServerMetricsOverviewProps {
|
||||
server: Server;
|
||||
}
|
||||
|
||||
export const ServerMetricsOverview = ({ server }: ServerMetricsOverviewProps) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'up':
|
||||
return 'text-green-500';
|
||||
case 'down':
|
||||
return 'text-red-500';
|
||||
case 'warning':
|
||||
return 'text-yellow-500';
|
||||
default:
|
||||
return 'text-muted-foreground';
|
||||
}
|
||||
};
|
||||
|
||||
const cpuUsagePercent = server.cpu_usage || 0;
|
||||
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 }: {
|
||||
title: string;
|
||||
used: string;
|
||||
total: string;
|
||||
free: string;
|
||||
percentage: number;
|
||||
icon: any;
|
||||
color: 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">
|
||||
<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>
|
||||
<span className="text-foreground/90">{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`
|
||||
}}>
|
||||
{percentage.toFixed(1)}%
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 relative">
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Free:</span>
|
||||
<span className="font-mono font-semibold text-foreground">{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>
|
||||
</div>
|
||||
{additionalInfo && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Cores:</span>
|
||||
<span className="font-mono font-semibold text-foreground">{additionalInfo}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-muted/40 rounded-full h-3 overflow-hidden shadow-inner">
|
||||
<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)`
|
||||
}}
|
||||
>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
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">
|
||||
<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>
|
||||
<span className="text-foreground/90">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'}`
|
||||
}} />
|
||||
{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>
|
||||
</div>
|
||||
<div className="flex justify-between py-1">
|
||||
<span className="text-muted-foreground">Connection:</span>
|
||||
<span className="font-semibold text-foreground">{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">
|
||||
<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>
|
||||
<span className="text-foreground/90">Uptime</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 relative">
|
||||
<div className="text-lg font-bold text-blue-500">
|
||||
{server.uptime}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<div>Last Check:</div>
|
||||
<div className="font-mono text-[10px] text-foreground/80 bg-muted/30 px-2 py-1 rounded">
|
||||
{new Date(server.last_checked).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* CPU Metric */}
|
||||
<MetricCard
|
||||
title="CPU"
|
||||
used={`${cpuUsagePercent.toFixed(1)}%`}
|
||||
total={`${server.cpu_cores} cores`}
|
||||
free={`${(100 - cpuUsagePercent).toFixed(1)}%`}
|
||||
percentage={cpuUsagePercent}
|
||||
icon={Cpu}
|
||||
color={theme === 'dark' ? "#3b82f6" : "#2563eb"}
|
||||
additionalInfo={server.cpu_cores?.toString()}
|
||||
/>
|
||||
|
||||
{/* Memory Metric */}
|
||||
<MetricCard
|
||||
title="Memory"
|
||||
used={formatBytes(server.ram_used)}
|
||||
total={formatBytes(server.ram_total)}
|
||||
free={formatBytes(server.ram_total - server.ram_used)}
|
||||
percentage={ramUsagePercent}
|
||||
icon={MemoryStick}
|
||||
color={theme === 'dark' ? "#10b981" : "#059669"}
|
||||
/>
|
||||
|
||||
{/* Disk Metric */}
|
||||
<MetricCard
|
||||
title="Disk"
|
||||
used={formatBytes(server.disk_used)}
|
||||
total={formatBytes(server.disk_total)}
|
||||
free={formatBytes(server.disk_total - server.disk_used)}
|
||||
percentage={diskUsagePercent}
|
||||
icon={HardDrive}
|
||||
color={theme === 'dark' ? "#f59e0b" : "#d97706"}
|
||||
/>
|
||||
|
||||
{/* System Info 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 xl:col-span-2">
|
||||
<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">
|
||||
<CardTitle className="flex items-center gap-3 text-sm font-semibold">
|
||||
<div className="p-2.5 rounded-xl bg-purple-500/15 shadow-sm ring-1 ring-white/10 transition-all duration-300 group-hover:scale-110" style={{ boxShadow: '0 4px 12px rgba(168, 85, 247, 0.2)' }}>
|
||||
<Info className="h-4 w-4 text-purple-500" />
|
||||
</div>
|
||||
<span className="text-foreground/90">System Information</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 relative">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
|
||||
<div className="space-y-3">
|
||||
<div className="bg-muted/20 rounded-lg p-3">
|
||||
<span className="text-muted-foreground block mb-1">Operating System:</span>
|
||||
<div className="font-semibold text-foreground">{server.os_type}</div>
|
||||
</div>
|
||||
<div className="bg-muted/20 rounded-lg p-3">
|
||||
<span className="text-muted-foreground block mb-1">Hostname:</span>
|
||||
<div className="font-mono text-xs text-foreground break-all">{server.hostname}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="bg-muted/20 rounded-lg p-3">
|
||||
<span className="text-muted-foreground block mb-1">IP Address:</span>
|
||||
<div className="font-mono text-xs text-foreground">{server.ip_address}</div>
|
||||
</div>
|
||||
{server.system_info && (
|
||||
<div className="bg-muted/20 rounded-lg p-3">
|
||||
<span className="text-muted-foreground block mb-1">System Details:</span>
|
||||
<div className="font-mono text-[10px] text-foreground/80 line-clamp-3 leading-relaxed" title={server.system_info}>
|
||||
{server.system_info}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Server, Activity, AlertTriangle, Power } from "lucide-react";
|
||||
import { ServerStats } from "@/types/server.types";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
interface ServerStatsCardsProps {
|
||||
stats: ServerStats;
|
||||
}
|
||||
|
||||
export const ServerStatsCards = ({ stats }: ServerStatsCardsProps) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const cards = [
|
||||
{
|
||||
title: "TOTAL SERVERS",
|
||||
value: stats.total,
|
||||
icon: Server,
|
||||
gradient: theme === 'dark'
|
||||
? "linear-gradient(135deg, rgba(139, 69, 19, 0.8) 0%, rgba(160, 82, 45, 0.6) 100%)"
|
||||
: "linear-gradient(135deg, #8b4513 0%, #a0522d 100%)"
|
||||
},
|
||||
{
|
||||
title: "ONLINE SERVERS",
|
||||
value: stats.online,
|
||||
icon: Activity,
|
||||
gradient: theme === 'dark'
|
||||
? "linear-gradient(135deg, rgba(67, 160, 71, 0.8) 0%, rgba(102, 187, 106, 0.6) 100%)"
|
||||
: "linear-gradient(135deg, #43a047 0%, #66bb6a 100%)"
|
||||
},
|
||||
{
|
||||
title: "OFFLINE SERVERS",
|
||||
value: stats.offline,
|
||||
icon: Power,
|
||||
gradient: theme === 'dark'
|
||||
? "linear-gradient(135deg, rgba(229, 57, 53, 0.8) 0%, rgba(239, 83, 80, 0.6) 100%)"
|
||||
: "linear-gradient(135deg, #e53935 0%, #ef5350 100%)"
|
||||
},
|
||||
{
|
||||
title: "WARNING SERVERS",
|
||||
value: stats.warning,
|
||||
icon: AlertTriangle,
|
||||
gradient: theme === 'dark'
|
||||
? "linear-gradient(135deg, rgba(255, 152, 0, 0.8) 0%, rgba(255, 183, 77, 0.6) 100%)"
|
||||
: "linear-gradient(135deg, #ff9800 0%, #ffb74d 100%)"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8 w-full">
|
||||
{cards.map((card, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={`border-none rounded-xl shadow-lg overflow-hidden transition-all duration-300 hover:shadow-xl transform hover:-translate-y-1 ${
|
||||
theme === 'dark' ? 'dark-card' : ''
|
||||
} relative z-10`}
|
||||
style={{
|
||||
background: card.gradient
|
||||
}}
|
||||
>
|
||||
{/* Grid Pattern Overlay */}
|
||||
<div className="absolute inset-0 z-0 opacity-10">
|
||||
<div className="w-full h-full"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(#fff 1px, transparent 1px),
|
||||
linear-gradient(90deg, #fff 1px, transparent 1px)`,
|
||||
backgroundSize: '20px 20px'
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<CardHeader className="pb-2 relative z-10">
|
||||
<CardTitle className="text-sm font-medium text-white">{card.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-between relative z-10">
|
||||
<span className="text-5xl font-bold text-white">{card.value}</span>
|
||||
<div className="rounded-full p-3 bg-white/25 backdrop-blur-sm">
|
||||
<card.icon className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface ServerStatusBadgeProps {
|
||||
status: 'up' | 'down' | 'warning';
|
||||
}
|
||||
|
||||
export const ServerStatusBadge = ({ status }: ServerStatusBadgeProps) => {
|
||||
const getStatusConfig = (status: string) => {
|
||||
switch (status) {
|
||||
case 'up':
|
||||
return {
|
||||
label: 'Online',
|
||||
className: 'bg-green-100 text-green-800 border-green-200'
|
||||
};
|
||||
case 'down':
|
||||
return {
|
||||
label: 'Offline',
|
||||
className: 'bg-red-100 text-red-800 border-red-200'
|
||||
};
|
||||
case 'warning':
|
||||
return {
|
||||
label: 'Warning',
|
||||
className: 'bg-yellow-100 text-yellow-800 border-yellow-200'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: 'Unknown',
|
||||
className: 'bg-gray-100 text-gray-800 border-gray-200'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getStatusConfig(status);
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className={config.className}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,280 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { RefreshCw, Search, Eye, Activity, MoreHorizontal, Pause, Play, Edit, Trash2 } from "lucide-react";
|
||||
import { Server } from "@/types/server.types";
|
||||
import { ServerStatusBadge } from "./ServerStatusBadge";
|
||||
import { OSTypeIcon } from "./OSTypeIcon";
|
||||
import { serverService } from "@/services/serverService";
|
||||
|
||||
interface ServerTableProps {
|
||||
servers: Server[];
|
||||
isLoading: boolean;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [pausedServers, setPausedServers] = useState<Set<string>>(new Set());
|
||||
const navigate = useNavigate();
|
||||
|
||||
const filteredServers = servers.filter(server =>
|
||||
server.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
server.hostname.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
server.ip_address.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const handleViewDetails = (serverId: string) => {
|
||||
navigate(`/server-detail/${serverId}`);
|
||||
};
|
||||
|
||||
const handleViewContainers = (serverId: string) => {
|
||||
navigate(`/container-monitoring/${serverId}`);
|
||||
};
|
||||
|
||||
const handlePauseResume = (serverId: string) => {
|
||||
const isPaused = pausedServers.has(serverId);
|
||||
if (isPaused) {
|
||||
setPausedServers(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(serverId);
|
||||
return newSet;
|
||||
});
|
||||
console.log('Resume server monitoring:', serverId);
|
||||
} else {
|
||||
setPausedServers(prev => new Set(prev).add(serverId));
|
||||
console.log('Pause server monitoring:', serverId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (serverId: string) => {
|
||||
// TODO: Implement edit functionality
|
||||
console.log('Edit server:', serverId);
|
||||
};
|
||||
|
||||
const handleDelete = (serverId: string) => {
|
||||
// TODO: Implement delete functionality
|
||||
console.log('Delete server:', serverId);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Servers</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<RefreshCw className="h-6 w-6 animate-spin" />
|
||||
<span className="ml-2">Loading servers...</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<CardTitle className="text-xl font-semibold">Servers</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1 sm:w-64">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search servers..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={onRefresh} variant="outline" size="icon">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{filteredServers.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-muted-foreground">No servers found</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>OS</TableHead>
|
||||
<TableHead>IP Address</TableHead>
|
||||
<TableHead>CPU</TableHead>
|
||||
<TableHead>Memory</TableHead>
|
||||
<TableHead>Disk</TableHead>
|
||||
<TableHead>Uptime</TableHead>
|
||||
<TableHead>Docker</TableHead>
|
||||
<TableHead>Last Checked</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredServers.map((server) => {
|
||||
const cpuUsage = server.cpu_usage || 0;
|
||||
const memoryUsage = server.ram_total > 0 ? (server.ram_used / server.ram_total) * 100 : 0;
|
||||
const diskUsage = server.disk_total > 0 ? (server.disk_used / server.disk_total) * 100 : 0;
|
||||
const isPaused = pausedServers.has(server.id);
|
||||
|
||||
return (
|
||||
<TableRow key={server.id} className="hover:bg-muted/50">
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className="font-medium">{server.name}</div>
|
||||
<div className="text-sm text-muted-foreground">{server.hostname}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ServerStatusBadge status={server.status} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<OSTypeIcon osType={server.os_type} />
|
||||
<span className="text-sm">{server.os_type}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-sm bg-muted px-1 py-0.5 rounded">{server.ip_address}</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1 min-w-[120px]">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>{cpuUsage.toFixed(1)}%</span>
|
||||
<span className="text-muted-foreground">{server.cpu_cores} cores</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={cpuUsage}
|
||||
className="h-2"
|
||||
indicatorClassName={
|
||||
cpuUsage > 80 ? "bg-red-500" :
|
||||
cpuUsage > 60 ? "bg-yellow-500" : "bg-green-500"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1 min-w-[120px]">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>{serverService.formatBytes(server.ram_used)}</span>
|
||||
<span className="text-muted-foreground">{memoryUsage.toFixed(1)}%</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={memoryUsage}
|
||||
className="h-2"
|
||||
indicatorClassName={
|
||||
memoryUsage > 90 ? "bg-red-500" :
|
||||
memoryUsage > 75 ? "bg-yellow-500" : "bg-blue-500"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1 min-w-[120px]">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>{serverService.formatBytes(server.disk_used)}</span>
|
||||
<span className="text-muted-foreground">{diskUsage.toFixed(1)}%</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={diskUsage}
|
||||
className="h-2"
|
||||
indicatorClassName={
|
||||
diskUsage > 95 ? "bg-red-500" :
|
||||
diskUsage > 85 ? "bg-yellow-500" : "bg-orange-500"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm">{server.uptime}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{server.docker === 'true' ? (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<Activity className="h-3 w-3 mr-1" />
|
||||
Enabled
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Disabled
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{new Date(server.last_checked).toLocaleString()}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[200px]">
|
||||
<DropdownMenuItem onClick={() => handleViewDetails(server.id)}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
View Server Detail
|
||||
</DropdownMenuItem>
|
||||
{server.docker === 'true' && (
|
||||
<DropdownMenuItem onClick={() => handleViewContainers(server.id)}>
|
||||
<Activity className="mr-2 h-4 w-4" />
|
||||
Container Monitoring
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => handlePauseResume(server.id)}>
|
||||
{isPaused ? (
|
||||
<>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Resume Monitoring
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Pause className="mr-2 h-4 w-4" />
|
||||
Pause Monitoring
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => handleEdit(server.id)}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit Server
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(server.id)}
|
||||
className="text-red-600 focus:text-red-600"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Server
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
|
||||
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,138 @@
|
||||
|
||||
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,138 @@
|
||||
|
||||
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,140 @@
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { timeRangeOptions } from "./dataUtils";
|
||||
|
||||
type TimeRange = '60m' | '1d' | '7d' | '1m' | '3m';
|
||||
|
||||
interface TimeRangeSelectorProps {
|
||||
value: TimeRange;
|
||||
onChange: (value: TimeRange) => void;
|
||||
}
|
||||
|
||||
export const TimeRangeSelector = ({ value, onChange }: TimeRangeSelectorProps) => {
|
||||
return (
|
||||
<Select value={value} onValueChange={(value: TimeRange) => onChange(value)}>
|
||||
<SelectTrigger className="w-[140px] h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{timeRangeOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
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 };
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
|
||||
export const formatBytes = (bytes: number, decimals = 2) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
export const parseValueWithUnit = (value: string | number): { numeric: number; unit: string; original: string } => {
|
||||
if (typeof value === 'number') {
|
||||
return { numeric: value, unit: 'B', original: value.toString() };
|
||||
}
|
||||
|
||||
const str = value.toString();
|
||||
const match = str.match(/^([\d.]+)\s*([A-Za-z%]*)/);
|
||||
if (match) {
|
||||
const numeric = parseFloat(match[1]);
|
||||
const unit = match[2] || '';
|
||||
return { numeric, unit, original: str };
|
||||
}
|
||||
return { numeric: 0, unit: '', original: str };
|
||||
};
|
||||
|
||||
export const convertToBytes = (value: string | number): number => {
|
||||
if (typeof value === 'number') return value;
|
||||
|
||||
const parsed = parseValueWithUnit(value);
|
||||
const multipliers: { [key: string]: number } = {
|
||||
'B': 1,
|
||||
'KB': 1024,
|
||||
'MB': 1024 * 1024,
|
||||
'GB': 1024 * 1024 * 1024,
|
||||
'TB': 1024 * 1024 * 1024 * 1024
|
||||
};
|
||||
|
||||
const multiplier = multipliers[parsed.unit.toUpperCase()] || 1;
|
||||
return parsed.numeric * multiplier;
|
||||
};
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
export const filterMetricsByTimeRange = (metrics: any[], timeRange: TimeRange): any[] => {
|
||||
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));
|
||||
|
||||
return metrics.filter(metric => {
|
||||
const metricTime = new Date(metric.timestamp);
|
||||
return metricTime >= cutoffTime;
|
||||
});
|
||||
};
|
||||
|
||||
export const formatChartData = (metrics: any[], timeRange: TimeRange) => {
|
||||
const filteredMetrics = filterMetricsByTimeRange(metrics, timeRange);
|
||||
|
||||
return filteredMetrics.slice(0, 100).reverse().map((metric, index) => {
|
||||
const cpuUsage = typeof metric.cpu_usage === 'string' ?
|
||||
parseFloat(metric.cpu_usage.replace('%', '')) :
|
||||
parseFloat(metric.cpu_usage) || 0;
|
||||
|
||||
const ramUsedBytes = convertToBytes(metric.ram_used);
|
||||
const ramTotalBytes = convertToBytes(metric.ram_total);
|
||||
const ramFreeBytes = convertToBytes(metric.ram_free);
|
||||
const ramUsagePercent = ramTotalBytes > 0 ? (ramUsedBytes / ramTotalBytes) * 100 : 0;
|
||||
|
||||
const diskUsedBytes = convertToBytes(metric.disk_used);
|
||||
const diskTotalBytes = convertToBytes(metric.disk_total);
|
||||
const diskFreeBytes = convertToBytes(metric.disk_free);
|
||||
const diskUsagePercent = diskTotalBytes > 0 ? (diskUsedBytes / diskTotalBytes) * 100 : 0;
|
||||
|
||||
const networkRxBytes = metric.network_rx_bytes || 0;
|
||||
const networkTxBytes = metric.network_tx_bytes || 0;
|
||||
const networkRxSpeed = metric.network_rx_speed || 0;
|
||||
const networkTxSpeed = metric.network_tx_speed || 0;
|
||||
|
||||
return {
|
||||
timestamp: new Date(metric.timestamp).toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}),
|
||||
cpuUsage: Math.round(cpuUsage * 100) / 100,
|
||||
cpuCores: parseInt(metric.cpu_cores) || 0,
|
||||
cpuFree: 100 - cpuUsage,
|
||||
|
||||
ramUsedBytes,
|
||||
ramTotalBytes,
|
||||
ramFreeBytes,
|
||||
ramUsed: formatBytes(ramUsedBytes),
|
||||
ramTotal: formatBytes(ramTotalBytes),
|
||||
ramFree: formatBytes(ramFreeBytes),
|
||||
ramUsagePercent: Math.round(ramUsagePercent * 100) / 100,
|
||||
|
||||
diskUsedBytes,
|
||||
diskTotalBytes,
|
||||
diskFreeBytes,
|
||||
diskUsed: formatBytes(diskUsedBytes),
|
||||
diskTotal: formatBytes(diskTotalBytes),
|
||||
diskFree: formatBytes(diskFreeBytes),
|
||||
diskUsagePercent: Math.round(diskUsagePercent * 100) / 100,
|
||||
|
||||
networkRxBytes,
|
||||
networkTxBytes,
|
||||
networkRx: formatBytes(networkRxBytes),
|
||||
networkTx: formatBytes(networkTxBytes),
|
||||
networkRxSpeed: Math.round(networkRxSpeed * 100) / 100,
|
||||
networkTxSpeed: Math.round(networkTxSpeed * 100) / 100,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, LineChart, Line } from "recharts";
|
||||
import { UptimeData } from "@/types/service.types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -13,16 +12,16 @@ interface ResponseTimeChartProps {
|
||||
export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
||||
const { theme } = useTheme();
|
||||
|
||||
// Modern color palette for different chart lines (only for All Monitoring)
|
||||
// Modern color palette for different chart lines with solid colors at 90-100% opacity
|
||||
const modernColors = [
|
||||
{ stroke: '#3b82f6', fill: 'rgba(59, 130, 246, 0.1)' }, // Blue
|
||||
{ stroke: '#10b981', fill: 'rgba(16, 185, 129, 0.1)' }, // Emerald
|
||||
{ stroke: '#f59e0b', fill: 'rgba(245, 158, 11, 0.1)' }, // Amber
|
||||
{ stroke: '#ef4444', fill: 'rgba(239, 68, 68, 0.1)' }, // Red
|
||||
{ stroke: '#8b5cf6', fill: 'rgba(139, 92, 246, 0.1)' }, // Violet
|
||||
{ stroke: '#06b6d4', fill: 'rgba(6, 182, 212, 0.1)' }, // Cyan
|
||||
{ stroke: '#f97316', fill: 'rgba(249, 115, 22, 0.1)' }, // Orange
|
||||
{ stroke: '#84cc16', fill: 'rgba(132, 204, 22, 0.1)' }, // Lime
|
||||
{ stroke: '#f59e0b', fill: 'rgba(245, 158, 11, 0.95)' }, // Yellow (changed from blue)
|
||||
{ stroke: '#10b981', fill: 'rgba(16, 185, 129, 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: '#84cc16', fill: 'rgba(132, 204, 22, 0.95)' }, // Lime
|
||||
];
|
||||
|
||||
// Check if we have data from multiple sources
|
||||
@@ -350,14 +349,6 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
{hasMultipleSources ? (
|
||||
<AreaChart data={chartData} margin={{ top: 10, right: 30, left: 0, bottom: 30 }}>
|
||||
<defs>
|
||||
{sources.map(source => (
|
||||
<linearGradient key={`gradient-${source.key}`} id={`gradient-${source.key}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={source.stroke} stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor={source.stroke} stopOpacity={0.05}/>
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={theme === 'dark' ? '#333' : '#e5e7eb'} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
@@ -376,27 +367,27 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
|
||||
{/* Default monitoring area with modern background - only if exists and no regional default */}
|
||||
{/* Default monitoring area with modern solid background */}
|
||||
{sources.find(s => s.key === 'default') && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="defaultValue"
|
||||
stroke={sources.find(s => s.key === 'default')?.stroke}
|
||||
fill={`url(#gradient-default)`}
|
||||
fill={sources.find(s => s.key === 'default')?.fill}
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
connectNulls={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Regional monitoring areas with modern backgrounds */}
|
||||
{/* Regional monitoring areas with modern solid backgrounds */}
|
||||
{sources.filter(s => s.key !== 'default').map((source) => (
|
||||
<Area
|
||||
key={source.key}
|
||||
type="monotone"
|
||||
dataKey={`${source.key}_value`}
|
||||
stroke={source.stroke}
|
||||
fill={`url(#gradient-${source.key})`}
|
||||
fill={source.fill}
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
connectNulls={false}
|
||||
@@ -404,22 +395,8 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
||||
))}
|
||||
</AreaChart>
|
||||
) : (
|
||||
// For single regional agent or default monitoring only - use original AreaChart styling
|
||||
// For single regional agent or default monitoring only - use solid background colors
|
||||
<AreaChart data={chartData} margin={{ top: 10, right: 30, left: 0, bottom: 30 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorUp" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor="#10b981" stopOpacity={0.05}/>
|
||||
</linearGradient>
|
||||
<linearGradient id="colorDown" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#ef4444" stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor="#ef4444" stopOpacity={0.05}/>
|
||||
</linearGradient>
|
||||
<linearGradient id="colorWarning" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#f59e0b" stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor="#f59e0b" stopOpacity={0.05}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={theme === 'dark' ? '#333' : '#e5e7eb'} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
@@ -438,12 +415,12 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
|
||||
{/* Original area charts for different statuses with background colors */}
|
||||
{/* Modern solid background areas for different statuses */}
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="upValue"
|
||||
stroke="#10b981"
|
||||
fill="url(#colorUp)"
|
||||
fill="rgba(16, 185, 129, 0.9)"
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
connectNulls={false}
|
||||
@@ -453,7 +430,7 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
||||
type="monotone"
|
||||
dataKey="downValue"
|
||||
stroke="#ef4444"
|
||||
fill="url(#colorDown)"
|
||||
fill="rgba(239, 68, 68, 0.9)"
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
connectNulls={false}
|
||||
@@ -463,7 +440,7 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
||||
type="monotone"
|
||||
dataKey="warningValue"
|
||||
stroke="#f59e0b"
|
||||
fill="url(#colorWarning)"
|
||||
fill="rgba(245, 158, 11, 0.9)"
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
connectNulls={false}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
@@ -50,4 +51,4 @@ const TabsContent = React.forwardRef<
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
@@ -0,0 +1,172 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { useSidebar } from "@/contexts/SidebarContext";
|
||||
import { Sidebar } from "@/components/dashboard/Sidebar";
|
||||
import { Header } from "@/components/dashboard/Header";
|
||||
import { DockerStatsCards } from "@/components/docker/DockerStatsCards";
|
||||
import { DockerContainersTable } from "@/components/docker/DockerContainersTable";
|
||||
import { dockerService } from "@/services/dockerService";
|
||||
import { DockerContainer, DockerStats } from "@/types/docker.types";
|
||||
import { authService } from "@/services/authService";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
const ContainerMonitoring = () => {
|
||||
const { serverId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { theme } = useTheme();
|
||||
const { t } = useLanguage();
|
||||
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
||||
const [stats, setStats] = useState<DockerStats>({
|
||||
total: 0,
|
||||
running: 0,
|
||||
stopped: 0,
|
||||
warning: 0
|
||||
});
|
||||
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser());
|
||||
|
||||
console.log('ContainerMonitoring component loaded with serverId:', serverId);
|
||||
|
||||
const {
|
||||
data: containers = [],
|
||||
isLoading,
|
||||
error,
|
||||
refetch
|
||||
} = useQuery({
|
||||
queryKey: ['docker-containers', serverId],
|
||||
queryFn: () => {
|
||||
console.log('Query function called with serverId:', serverId);
|
||||
return serverId ? dockerService.getContainersByServerId(serverId) : dockerService.getContainers();
|
||||
},
|
||||
refetchInterval: 30000 // Refetch every 30 seconds
|
||||
});
|
||||
|
||||
console.log('Query state:', { containers, isLoading, error });
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Containers changed:', containers);
|
||||
if (containers.length > 0) {
|
||||
dockerService.getContainerStats(containers).then(newStats => {
|
||||
console.log('Stats calculated:', newStats);
|
||||
setStats(newStats);
|
||||
});
|
||||
}
|
||||
}, [containers]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
console.log('Manual refresh triggered');
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
authService.logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const handleBackToServers = () => {
|
||||
navigate('/instance-monitoring');
|
||||
};
|
||||
|
||||
if (error) {
|
||||
console.error('Container monitoring error:', error);
|
||||
return (
|
||||
<div className={`min-h-screen flex ${theme === 'dark' ? 'bg-[#0a0a0a] text-white' : 'bg-gray-50 text-gray-900'}`}>
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
<main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6">
|
||||
<div className="text-center max-w-md w-full">
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-4">Error loading containers</h2>
|
||||
<p className="text-muted-foreground mb-4 text-sm sm:text-base">
|
||||
Unable to fetch container data. Please check your connection and try again.
|
||||
</p>
|
||||
<div className="text-xs text-muted-foreground mb-4 font-mono">
|
||||
Error: {error?.message || 'Unknown error'}
|
||||
</div>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<Button onClick={handleRefresh} className="text-sm sm:text-base">
|
||||
Retry
|
||||
</Button>
|
||||
<Button onClick={handleBackToServers} variant="outline" className="text-sm sm:text-base">
|
||||
Back to Servers
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`min-h-screen flex ${theme === 'dark' ? 'bg-[#0a0a0a] text-white' : 'bg-gray-50 text-gray-900'}`}>
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
<main className="flex-1 overflow-auto">
|
||||
<div className="mx-[20px] my-[20px]">
|
||||
{/* Header Section */}
|
||||
<div className="mb-6 lg:mb-8">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Button
|
||||
onClick={handleBackToServers}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Servers
|
||||
</Button>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Container Monitoring
|
||||
</h1>
|
||||
<p className={`text-muted-foreground mt-1 sm:mt-2 transition-all duration-300 ${sidebarCollapsed ? 'text-base sm:text-lg' : 'text-sm sm:text-base'}`}>
|
||||
Monitor and manage your Docker containers in real-time
|
||||
{serverId && (
|
||||
<span className="block text-xs text-muted-foreground/70 mt-1">
|
||||
Server ID: {serverId}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards Section */}
|
||||
<div className="mb-6 lg:mb-8">
|
||||
<DockerStatsCards stats={stats} />
|
||||
</div>
|
||||
|
||||
{/* Containers Table Section */}
|
||||
<div className="min-w-0">
|
||||
<DockerContainersTable
|
||||
containers={containers}
|
||||
isLoading={isLoading}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContainerMonitoring;
|
||||
@@ -0,0 +1,108 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { Sidebar } from "@/components/dashboard/Sidebar";
|
||||
import { Header } from "@/components/dashboard/Header";
|
||||
import { ServerStatsCards } from "@/components/servers/ServerStatsCards";
|
||||
import { ServerTable } from "@/components/servers/ServerTable";
|
||||
import { serverService } from "@/services/serverService";
|
||||
import { Server, ServerStats } from "@/types/server.types";
|
||||
import { useSidebar } from "@/contexts/SidebarContext";
|
||||
import { authService } from "@/services/authService";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
const InstanceMonitoring = () => {
|
||||
const {
|
||||
theme
|
||||
} = useTheme();
|
||||
const {
|
||||
t
|
||||
} = useLanguage();
|
||||
const {
|
||||
sidebarCollapsed,
|
||||
toggleSidebar
|
||||
} = useSidebar();
|
||||
const navigate = useNavigate();
|
||||
const [stats, setStats] = useState<ServerStats>({
|
||||
total: 0,
|
||||
online: 0,
|
||||
offline: 0,
|
||||
warning: 0
|
||||
});
|
||||
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser());
|
||||
const {
|
||||
data: servers = [],
|
||||
isLoading,
|
||||
error,
|
||||
refetch
|
||||
} = useQuery({
|
||||
queryKey: ['servers'],
|
||||
queryFn: serverService.getServers,
|
||||
refetchInterval: 30000 // Refetch every 30 seconds
|
||||
});
|
||||
useEffect(() => {
|
||||
if (servers.length > 0) {
|
||||
serverService.getServerStats(servers).then(setStats);
|
||||
}
|
||||
}, [servers]);
|
||||
const handleRefresh = () => {
|
||||
refetch();
|
||||
};
|
||||
const handleLogout = () => {
|
||||
authService.logout();
|
||||
navigate('/login');
|
||||
};
|
||||
if (error) {
|
||||
return <div className={`min-h-screen flex ${theme === 'dark' ? 'bg-[#0a0a0a] text-white' : 'bg-gray-50 text-gray-900'}`}>
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<Header currentUser={currentUser} onLogout={handleLogout} sidebarCollapsed={sidebarCollapsed} toggleSidebar={toggleSidebar} />
|
||||
<main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6">
|
||||
<div className="text-center max-w-md w-full">
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-4">Error loading servers</h2>
|
||||
<p className="text-muted-foreground mb-4 text-sm sm:text-base">
|
||||
Unable to fetch server data. Please check your connection and try again.
|
||||
</p>
|
||||
<button onClick={handleRefresh} className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 text-sm sm:text-base">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
return <div className={`min-h-screen flex ${theme === 'dark' ? 'bg-[#0a0a0a] text-white' : 'bg-gray-50 text-gray-900'}`}>
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<Header currentUser={currentUser} onLogout={handleLogout} sidebarCollapsed={sidebarCollapsed} toggleSidebar={toggleSidebar} />
|
||||
<main className="flex-1 overflow-auto">
|
||||
<div className="mx-[20px] my-[20px]">
|
||||
{/* Header Section */}
|
||||
<div className="mb-6 lg:mb-8">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Instance Monitoring
|
||||
</h1>
|
||||
<p className={`text-muted-foreground mt-1 sm:mt-2 transition-all duration-300 ${sidebarCollapsed ? 'text-base sm:text-lg' : 'text-sm sm:text-base'}`}>
|
||||
Monitor and manage your server instances in real-time
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards Section */}
|
||||
<div className="mb-6 lg:mb-8">
|
||||
<ServerStatsCards stats={stats} />
|
||||
</div>
|
||||
|
||||
{/* Server Table Section */}
|
||||
<div className="min-w-0">
|
||||
<ServerTable servers={servers} isLoading={isLoading} onRefresh={handleRefresh} />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
export default InstanceMonitoring;
|
||||
@@ -0,0 +1,174 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useLanguage } from "@/contexts/LanguageContext";
|
||||
import { useSidebar } from "@/contexts/SidebarContext";
|
||||
import { Sidebar } from "@/components/dashboard/Sidebar";
|
||||
import { Header } from "@/components/dashboard/Header";
|
||||
import { serverService } from "@/services/serverService";
|
||||
import { authService } from "@/services/authService";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft, Server } from "lucide-react";
|
||||
import { ServerMetricsCharts } from "@/components/servers/ServerMetricsCharts";
|
||||
import { ServerMetricsOverview } from "@/components/servers/ServerMetricsOverview";
|
||||
import { ServerHistoryCharts } from "@/components/servers/ServerHistoryCharts";
|
||||
|
||||
const ServerDetail = () => {
|
||||
const { serverId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { theme } = useTheme();
|
||||
const { t } = useLanguage();
|
||||
const { sidebarCollapsed, toggleSidebar } = useSidebar();
|
||||
const [currentUser, setCurrentUser] = useState(authService.getCurrentUser());
|
||||
|
||||
console.log('ServerDetail component loaded with serverId:', serverId);
|
||||
|
||||
const {
|
||||
data: server,
|
||||
isLoading: serverLoading,
|
||||
error: serverError
|
||||
} = useQuery({
|
||||
queryKey: ['server', serverId],
|
||||
queryFn: () => serverService.getServer(serverId!),
|
||||
enabled: !!serverId
|
||||
});
|
||||
|
||||
const handleLogout = () => {
|
||||
authService.logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const handleBackToServers = () => {
|
||||
navigate('/instance-monitoring');
|
||||
};
|
||||
|
||||
if (serverError) {
|
||||
console.error('Server detail error:', serverError);
|
||||
return (
|
||||
<div className={`min-h-screen flex ${theme === 'dark' ? 'bg-[#0a0a0a] text-white' : 'bg-gray-50 text-gray-900'}`}>
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
<main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6">
|
||||
<div className="text-center max-w-md w-full">
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-4">Error loading server</h2>
|
||||
<p className="text-muted-foreground mb-4 text-sm sm:text-base">
|
||||
Unable to fetch server data. Please check your connection and try again.
|
||||
</p>
|
||||
<div className="text-xs text-muted-foreground mb-4 font-mono">
|
||||
Error: {serverError?.message || 'Unknown error'}
|
||||
</div>
|
||||
<Button onClick={handleBackToServers} variant="outline" className="text-sm sm:text-base">
|
||||
Back to Servers
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (serverLoading) {
|
||||
return (
|
||||
<div className={`min-h-screen flex ${theme === 'dark' ? 'bg-[#0a0a0a] text-white' : 'bg-gray-50 text-gray-900'}`}>
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
<main className="flex-1 flex flex-col items-center justify-center p-4 sm:p-6">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
||||
<p className="text-muted-foreground">Loading server details...</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`min-h-screen flex ${theme === 'dark' ? 'bg-[#0a0a0a] text-white' : 'bg-gray-50 text-gray-900'}`}>
|
||||
<Sidebar collapsed={sidebarCollapsed} />
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<Header
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
<main className="flex-1 overflow-auto">
|
||||
<div className="mx-[20px] my-[20px]">
|
||||
{/* Header Section */}
|
||||
<div className="mb-6 lg:mb-8">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Button
|
||||
onClick={handleBackToServers}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Servers
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="h-8 w-8 rounded bg-primary/10 flex items-center justify-center">
|
||||
<Server className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{server?.name || 'Server Detail'}
|
||||
</h1>
|
||||
</div>
|
||||
<p className={`text-muted-foreground mt-1 sm:mt-2 transition-all duration-300 ${sidebarCollapsed ? 'text-base sm:text-lg' : 'text-sm sm:text-base'}`}>
|
||||
Monitor server performance metrics and system health
|
||||
{server && (
|
||||
<span className="block text-xs text-muted-foreground/70 mt-1">
|
||||
{server.hostname} • {server.ip_address} • {server.os_type}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Server Overview Cards */}
|
||||
{server && (
|
||||
<div className="mb-6 lg:mb-8">
|
||||
<ServerMetricsOverview server={server} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Historical Charts Section */}
|
||||
{server && (
|
||||
<div className="mb-6 lg:mb-8">
|
||||
<ServerHistoryCharts serverId={server.id} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metrics Charts Section */}
|
||||
{server && (
|
||||
<div className="min-w-0">
|
||||
<ServerMetricsCharts serverId={server.id} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerDetail;
|
||||
@@ -0,0 +1,154 @@
|
||||
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { DockerContainer, DockerMetrics, DockerStats } from "@/types/docker.types";
|
||||
|
||||
class DockerService {
|
||||
async getContainers(): Promise<DockerContainer[]> {
|
||||
try {
|
||||
console.log('Fetching all Docker containers...');
|
||||
const records = await pb.collection('dockers').getFullList({
|
||||
sort: '-created',
|
||||
});
|
||||
console.log('Docker containers fetched:', records);
|
||||
return records as DockerContainer[];
|
||||
} catch (error) {
|
||||
console.error('Error fetching Docker containers:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getContainersByServerId(serverId: string): Promise<DockerContainer[]> {
|
||||
try {
|
||||
console.log('Fetching Docker containers for server ID:', serverId);
|
||||
|
||||
// First, try to get the server details to find the correct hostname
|
||||
const server = await pb.collection('servers').getOne(serverId);
|
||||
console.log('Server details:', server);
|
||||
|
||||
// Try multiple filter approaches to find containers
|
||||
const filters = [
|
||||
`server_id = "${serverId}"`,
|
||||
`hostname = "${server.hostname}"`,
|
||||
`hostname ~ "${server.hostname}"`,
|
||||
`ip_address = "${server.ip_address}"`,
|
||||
];
|
||||
|
||||
let containers: DockerContainer[] = [];
|
||||
|
||||
for (const filter of filters) {
|
||||
console.log('Trying filter:', filter);
|
||||
try {
|
||||
const records = await pb.collection('dockers').getFullList({
|
||||
filter: filter,
|
||||
sort: '-created',
|
||||
});
|
||||
console.log(`Filter "${filter}" returned:`, records);
|
||||
|
||||
if (records.length > 0) {
|
||||
containers = records as DockerContainer[];
|
||||
break;
|
||||
}
|
||||
} catch (filterError) {
|
||||
console.warn(`Filter "${filter}" failed:`, filterError);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If no containers found with filters, get all and log for debugging
|
||||
if (containers.length === 0) {
|
||||
console.log('No containers found with filters, fetching all for debugging...');
|
||||
const allContainers = await pb.collection('dockers').getFullList({
|
||||
sort: '-created',
|
||||
});
|
||||
console.log('All available Docker containers:', allContainers);
|
||||
console.log('Looking for containers that might match server:', {
|
||||
serverId,
|
||||
serverHostname: server.hostname,
|
||||
serverIp: server.ip_address
|
||||
});
|
||||
}
|
||||
|
||||
return containers;
|
||||
} catch (error) {
|
||||
console.error('Error fetching Docker containers by server ID:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getContainerMetrics(dockerId: string): Promise<DockerMetrics[]> {
|
||||
try {
|
||||
console.log('Fetching metrics for docker ID:', dockerId);
|
||||
const records = await pb.collection('docker_metrics').getFullList({
|
||||
filter: `docker_id = "${dockerId}"`,
|
||||
sort: '-timestamp',
|
||||
perPage: 100,
|
||||
});
|
||||
console.log('Docker metrics fetched:', records);
|
||||
return records as DockerMetrics[];
|
||||
} catch (error) {
|
||||
console.error('Error fetching Docker metrics:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getContainerStats(containers: DockerContainer[]): Promise<DockerStats> {
|
||||
const total = containers.length;
|
||||
|
||||
// Parse status field to determine if container is running
|
||||
const running = containers.filter(c => {
|
||||
const status = c.status?.toLowerCase() || '';
|
||||
return status.includes('up') && !status.includes('exited');
|
||||
}).length;
|
||||
|
||||
const stopped = containers.filter(c => {
|
||||
const status = c.status?.toLowerCase() || '';
|
||||
return status.includes('exited') || status.includes('stopped');
|
||||
}).length;
|
||||
|
||||
const warning = containers.filter(c => {
|
||||
const status = c.status?.toLowerCase() || '';
|
||||
return status.includes('unhealthy') || status.includes('restarting');
|
||||
}).length;
|
||||
|
||||
return {
|
||||
total,
|
||||
running,
|
||||
stopped,
|
||||
warning,
|
||||
};
|
||||
}
|
||||
|
||||
formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', '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];
|
||||
}
|
||||
|
||||
formatUptime(uptime: string): string {
|
||||
return uptime || 'N/A';
|
||||
}
|
||||
|
||||
parseMetricValue(value: string): number {
|
||||
if (!value || value === 'N/A') return 0;
|
||||
|
||||
// Remove units and convert to number
|
||||
const numericValue = parseFloat(value.toString().replace(/[^\d.]/g, ''));
|
||||
return isNaN(numericValue) ? 0 : numericValue;
|
||||
}
|
||||
|
||||
getStatusFromDockerStatus(status: string): 'running' | 'stopped' | 'warning' {
|
||||
const statusLower = status?.toLowerCase() || '';
|
||||
|
||||
if (statusLower.includes('up') && !statusLower.includes('exited')) {
|
||||
return 'running';
|
||||
} else if (statusLower.includes('exited') || statusLower.includes('stopped')) {
|
||||
return 'stopped';
|
||||
} else {
|
||||
return 'warning';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const dockerService = new DockerService();
|
||||
@@ -0,0 +1,113 @@
|
||||
|
||||
import { pb } from '@/lib/pocketbase';
|
||||
import { Server, ServerStats } from '@/types/server.types';
|
||||
|
||||
export const serverService = {
|
||||
async getServers(): Promise<Server[]> {
|
||||
try {
|
||||
const records = await pb.collection('servers').getFullList<Server>();
|
||||
return records;
|
||||
} catch (error) {
|
||||
console.error('Error fetching servers:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getServer(serverId: string): Promise<Server> {
|
||||
try {
|
||||
const record = await pb.collection('servers').getOne<Server>(serverId);
|
||||
return record;
|
||||
} catch (error) {
|
||||
console.error('Error fetching server:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getServerMetrics(serverId: string): Promise<any[]> {
|
||||
try {
|
||||
console.log('serverService.getServerMetrics: Fetching metrics for serverId:', serverId);
|
||||
|
||||
// First, get the server to find the correct server_id for metrics
|
||||
let server;
|
||||
try {
|
||||
server = await this.getServer(serverId);
|
||||
console.log('serverService.getServerMetrics: Found server:', server);
|
||||
} catch (error) {
|
||||
console.log('serverService.getServerMetrics: Could not fetch server details:', error);
|
||||
}
|
||||
|
||||
// Try to get metrics using the server's server_id field if available
|
||||
let metricsServerId = serverId;
|
||||
if (server && server.server_id) {
|
||||
metricsServerId = server.server_id;
|
||||
console.log('serverService.getServerMetrics: Using server.server_id for metrics:', metricsServerId);
|
||||
}
|
||||
|
||||
// Try filtering by server_id first
|
||||
let filteredRecords = await pb.collection('server_metrics').getFullList({
|
||||
filter: `server_id = "${metricsServerId}"`,
|
||||
sort: '-created',
|
||||
requestKey: null
|
||||
});
|
||||
|
||||
console.log('serverService.getServerMetrics: Filtered records by server_id:', filteredRecords.length);
|
||||
|
||||
// If no records found with server_id, try alternative approaches
|
||||
if (filteredRecords.length === 0) {
|
||||
console.log('serverService.getServerMetrics: No records found with server_id filter, trying alternatives...');
|
||||
|
||||
// Get all records to see what's available
|
||||
const allRecords = await pb.collection('server_metrics').getFullList({
|
||||
sort: '-created',
|
||||
requestKey: null
|
||||
});
|
||||
|
||||
console.log('serverService.getServerMetrics: Total server_metrics records:', allRecords.length);
|
||||
if (allRecords.length > 0) {
|
||||
console.log('serverService.getServerMetrics: Sample record fields:', Object.keys(allRecords[0]));
|
||||
console.log('serverService.getServerMetrics: Sample server_id values:', allRecords.slice(0, 5).map(r => r.server_id));
|
||||
}
|
||||
|
||||
// For now, return some sample data from available records if server matches pattern
|
||||
// This is temporary until the correct server_id mapping is established
|
||||
if (allRecords.length > 0) {
|
||||
console.log('serverService.getServerMetrics: Using available records as fallback');
|
||||
filteredRecords = allRecords.slice(0, 50); // Get recent 50 records
|
||||
}
|
||||
}
|
||||
|
||||
console.log('serverService.getServerMetrics: Returning', filteredRecords.length, 'records');
|
||||
return filteredRecords;
|
||||
} catch (error) {
|
||||
console.error('Error fetching server metrics:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getServerStats(servers: Server[]): Promise<ServerStats> {
|
||||
const total = servers.length;
|
||||
const online = servers.filter(server => server.status === 'up').length;
|
||||
const offline = servers.filter(server => server.status === 'down').length;
|
||||
const warning = servers.filter(server => server.status === 'warning').length;
|
||||
|
||||
return {
|
||||
total,
|
||||
online,
|
||||
offline,
|
||||
warning
|
||||
};
|
||||
},
|
||||
|
||||
formatBytes(bytes: number): string {
|
||||
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];
|
||||
},
|
||||
|
||||
formatUptime(uptime: string): string {
|
||||
// Simple uptime formatting - can be enhanced based on actual format
|
||||
return uptime;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
|
||||
import { pb } from '@/lib/pocketbase';
|
||||
import { Server, ServerStats } from '@/types/server.types';
|
||||
|
||||
export const serverService = {
|
||||
async getServers(): Promise<Server[]> {
|
||||
try {
|
||||
const records = await pb.collection('servers').getFullList<Server>();
|
||||
return records;
|
||||
} catch (error) {
|
||||
console.error('Error fetching servers:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getServer(serverId: string): Promise<Server> {
|
||||
try {
|
||||
const record = await pb.collection('servers').getOne<Server>(serverId);
|
||||
return record;
|
||||
} catch (error) {
|
||||
console.error('Error fetching server:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getServerMetrics(serverId: string): Promise<any[]> {
|
||||
try {
|
||||
console.log('serverService.getServerMetrics: Fetching metrics for serverId:', serverId);
|
||||
|
||||
// First, get the server to find the correct server_id for metrics
|
||||
let server;
|
||||
try {
|
||||
server = await this.getServer(serverId);
|
||||
console.log('serverService.getServerMetrics: Found server:', server);
|
||||
} catch (error) {
|
||||
console.log('serverService.getServerMetrics: Could not fetch server details:', error);
|
||||
}
|
||||
|
||||
// Try to get metrics using the server's server_id field if available
|
||||
let metricsServerId = serverId;
|
||||
if (server && server.server_id) {
|
||||
metricsServerId = server.server_id;
|
||||
console.log('serverService.getServerMetrics: Using server.server_id for metrics:', metricsServerId);
|
||||
}
|
||||
|
||||
// Try filtering by server_id first
|
||||
let filteredRecords = await pb.collection('server_metrics').getFullList({
|
||||
filter: `server_id = "${metricsServerId}"`,
|
||||
sort: '-created',
|
||||
requestKey: null
|
||||
});
|
||||
|
||||
console.log('serverService.getServerMetrics: Filtered records by server_id:', filteredRecords.length);
|
||||
|
||||
// If no records found with server_id, try alternative approaches
|
||||
if (filteredRecords.length === 0) {
|
||||
console.log('serverService.getServerMetrics: No records found with server_id filter, trying alternatives...');
|
||||
|
||||
// Get all records to see what's available
|
||||
const allRecords = await pb.collection('server_metrics').getFullList({
|
||||
sort: '-created',
|
||||
requestKey: null
|
||||
});
|
||||
|
||||
console.log('serverService.getServerMetrics: Total server_metrics records:', allRecords.length);
|
||||
if (allRecords.length > 0) {
|
||||
console.log('serverService.getServerMetrics: Sample record fields:', Object.keys(allRecords[0]));
|
||||
console.log('serverService.getServerMetrics: Sample server_id values:', allRecords.slice(0, 5).map(r => r.server_id));
|
||||
}
|
||||
|
||||
// For now, return some sample data from available records if server matches pattern
|
||||
// This is temporary until the correct server_id mapping is established
|
||||
if (allRecords.length > 0) {
|
||||
console.log('serverService.getServerMetrics: Using available records as fallback');
|
||||
filteredRecords = allRecords.slice(0, 50); // Get recent 50 records
|
||||
}
|
||||
}
|
||||
|
||||
console.log('serverService.getServerMetrics: Returning', filteredRecords.length, 'records');
|
||||
return filteredRecords;
|
||||
} catch (error) {
|
||||
console.error('Error fetching server metrics:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getServerStats(servers: Server[]): Promise<ServerStats> {
|
||||
const total = servers.length;
|
||||
const online = servers.filter(server => server.status === 'up').length;
|
||||
const offline = servers.filter(server => server.status === 'down').length;
|
||||
const warning = servers.filter(server => server.status === 'warning').length;
|
||||
|
||||
return {
|
||||
total,
|
||||
online,
|
||||
offline,
|
||||
warning
|
||||
};
|
||||
},
|
||||
|
||||
formatBytes(bytes: number): string {
|
||||
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];
|
||||
},
|
||||
|
||||
formatUptime(uptime: string): string {
|
||||
// Simple uptime formatting - can be enhanced based on actual format
|
||||
return uptime;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
|
||||
export interface DockerContainer {
|
||||
collectionId: string;
|
||||
collectionName: string;
|
||||
id: string;
|
||||
docker_id: string;
|
||||
name: string;
|
||||
hostname: string;
|
||||
ip_address: string;
|
||||
os_template: string;
|
||||
uptime: string;
|
||||
ram_total: number;
|
||||
ram_used: number;
|
||||
cpu_cores: number;
|
||||
cpu_usage: number;
|
||||
disk_total: number;
|
||||
disk_used: number;
|
||||
last_checked: string;
|
||||
template_id: string;
|
||||
notification_id: string;
|
||||
timestamp: string;
|
||||
status: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface DockerMetrics {
|
||||
collectionId: string;
|
||||
collectionName: string;
|
||||
id: string;
|
||||
docker_id: string;
|
||||
timestamp: string;
|
||||
ram_total: string;
|
||||
ram_used: string;
|
||||
ram_free: string;
|
||||
cpu_cores: string;
|
||||
cpu_usage: string;
|
||||
cpu_free: string;
|
||||
disk_total: string;
|
||||
disk_used: string;
|
||||
disk_free: string;
|
||||
status: string;
|
||||
network_rx_bytes: number;
|
||||
network_tx_bytes: number;
|
||||
network_rx_speed: number;
|
||||
network_tx_speed: number;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface DockerStats {
|
||||
total: number;
|
||||
running: number;
|
||||
stopped: number;
|
||||
warning: number;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
export interface Server {
|
||||
collectionId: string;
|
||||
collectionName: string;
|
||||
id: string;
|
||||
server_id: string;
|
||||
name: string;
|
||||
hostname: string;
|
||||
ip_address: string;
|
||||
os_type: string;
|
||||
status: 'up' | 'down' | 'warning';
|
||||
uptime: string;
|
||||
ram_total: number;
|
||||
ram_used: number;
|
||||
cpu_cores: number;
|
||||
cpu_usage: number;
|
||||
disk_total: number;
|
||||
disk_used: number;
|
||||
last_checked: string;
|
||||
server_token: string;
|
||||
template_id: string;
|
||||
notification_id: string;
|
||||
timestamp: string;
|
||||
connection: string;
|
||||
agent_status: string;
|
||||
system_info: string;
|
||||
network_rx_bytes: string;
|
||||
network_tx_bytes: string;
|
||||
network_rx_speed: string;
|
||||
network_tx_speed: string;
|
||||
check_interval: number;
|
||||
docker: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface ServerStats {
|
||||
total: number;
|
||||
online: number;
|
||||
offline: number;
|
||||
warning: number;
|
||||
}
|
||||
@@ -29,17 +29,17 @@ func main() {
|
||||
log.Printf("Warning: Failed to initialize PocketBase client: %v", err)
|
||||
} else {
|
||||
if err := pbClient.TestConnection(); err != nil {
|
||||
log.Printf("Warning: PocketBase connection test failed: %v", err)
|
||||
log.Printf("Warning: Backend connection test failed: %v", err)
|
||||
} else {
|
||||
// Initialize and start monitoring service with regional support
|
||||
monitoringService = monitoring.NewMonitoringService(pbClient)
|
||||
go monitoringService.Start()
|
||||
log.Println("Monitoring service started with regional agent support")
|
||||
log.Println("Uptime Monitoring service started with Default System agent support")
|
||||
|
||||
// Initialize and start SSL monitoring service (unchanged - no regional support)
|
||||
sslMonitoringService = monitoring.NewSSLMonitoringService(pbClient)
|
||||
go sslMonitoringService.Start()
|
||||
log.Println("SSL monitoring service started (independent of regional agents)")
|
||||
log.Println("SSL monitoring service started (independent of Default System Agent)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func main() {
|
||||
|
||||
log.Printf("Service Operation starting on port %s", cfg.Port)
|
||||
if pbClient != nil {
|
||||
log.Printf("PocketBase integration enabled at %s (public access)", pbClient.GetBaseURL())
|
||||
log.Printf("Backend Server integration enabled at %s", pbClient.GetBaseURL())
|
||||
}
|
||||
if monitoringService != nil {
|
||||
log.Printf("Automatic service monitoring enabled with regional agent support")
|
||||
@@ -77,8 +77,8 @@ func main() {
|
||||
log.Printf(" POST /ping - Legacy ping endpoint")
|
||||
log.Printf(" GET /ping/quick?host=<host> - Legacy quick ping test")
|
||||
log.Printf(" GET /health - Health check")
|
||||
log.Printf("Supported operations: ping, dns, tcp, http, ssl")
|
||||
log.Printf("Regional monitoring: Tracks 'Default' region connection status")
|
||||
log.Printf("Default SystemSupported operations: ping, dns, tcp, http, ssl")
|
||||
log.Printf("Default System: Tracks 'Default' region connection status")
|
||||
|
||||
// Setup graceful shutdown
|
||||
c := make(chan os.Signal, 1)
|
||||
|
||||
Reference in New Issue
Block a user