diff --git a/application/src/components/docker/DockerContainersTable.tsx b/application/src/components/docker/DockerContainersTable.tsx new file mode 100644 index 0000000..5ec906c --- /dev/null +++ b/application/src/components/docker/DockerContainersTable.tsx @@ -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(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 ( + <> + + +
+
+ Docker Containers + +
+
+
+ +
+
+
+ + + + {filteredContainers.length === 0 ? ( + + ) : ( + filteredContainers.map((container) => ( + + )) + )} + +
+
+
+
+
+
+ + + + ); +}; \ No newline at end of file diff --git a/application/src/components/docker/DockerMetricsDialog.tsx b/application/src/components/docker/DockerMetricsDialog.tsx new file mode 100644 index 0000000..0e50531 --- /dev/null +++ b/application/src/components/docker/DockerMetricsDialog.tsx @@ -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("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; + }) => ( +
+
+ + {title} +
+
+
+ Used: + {used} +
+
+ Free: + {free} +
+
+ Total: + {total} +
+
+ Usage: + {percentage.toFixed(1)}% +
+
+
+ ); + + if (!container) return null; + + return ( + + + + +
+
+ +
+ Container Metrics: {container.name} +
+
+ +
+
+

+ Docker ID: {container.docker_id} • {container.hostname} +

+
+ + {isLoading ? ( +
+ + Loading metrics... +
+ ) : error ? ( +
+

Error loading metrics: {error.message}

+
+ ) : chartData.length === 0 ? ( +
+

No metrics data available for this container

+
+ ) : ( + <> + {/* Current Metrics Summary */} + {latestMetric && ( +
+ + + + +
+ )} + + + + + + CPU + + + + Memory + + + + Disk + + + + Network + + + + +
+ + + CPU Usage (%) + + + + + + + + } + cursor={{ stroke: getGridColor() }} + /> + + + + + + + + + CPU Usage vs Available + + + + + + + + } + cursor={{ stroke: getGridColor() }} + /> + + + + + + +
+
+ + +
+ + + Memory Usage (%) + + + + + + + + } + cursor={{ stroke: getGridColor() }} + /> + + + + + + + + + Memory Usage (Bytes) + + + + + + + formatBytes(value)} + /> + } + cursor={{ stroke: getGridColor() }} + formatter={(value, name) => [ + name === 'Used Memory' ? formatBytes(Number(value)) : + name === 'Total Memory' ? formatBytes(Number(value)) : value, + name + ]} + /> + + + + + + +
+
+ + +
+ + + Disk Usage (%) + + + + + + + + } + cursor={{ stroke: getGridColor() }} + /> + + + + + + + + + Disk Usage (Bytes) + + + + + + + formatBytes(value)} + /> + } + cursor={{ stroke: getGridColor() }} + formatter={(value, name) => [ + name === 'Used Disk' ? formatBytes(Number(value)) : + name === 'Total Disk' ? formatBytes(Number(value)) : value, + name + ]} + /> + + + + + + +
+
+ + +
+ + + Network Traffic + + + + + + + + } + cursor={{ stroke: getGridColor() }} + /> + + + + + + + + + + Network Speed (KB/s) + + + + + + + + } + cursor={{ stroke: getGridColor() }} + /> + + + + + + +
+
+
+ + )} +
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/docker/DockerStatsCards.tsx b/application/src/components/docker/DockerStatsCards.tsx new file mode 100644 index 0000000..6376b6d --- /dev/null +++ b/application/src/components/docker/DockerStatsCards.tsx @@ -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 ( +
+ {cards.map((card) => { + const IconComponent = card.icon; + return ( + + + + {card.title} + +
+ +
+
+ +
+
+ {card.value} +
+ + Containers + +
+
+
+ ); + })} +
+ ); +}; \ No newline at end of file diff --git a/application/src/components/docker/DockerStatusBadge.tsx b/application/src/components/docker/DockerStatusBadge.tsx new file mode 100644 index 0000000..9d5d4d6 --- /dev/null +++ b/application/src/components/docker/DockerStatusBadge.tsx @@ -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 ( + + {config.label} + + ); +}; \ No newline at end of file diff --git a/application/src/components/docker/table/DockerEmptyState.tsx b/application/src/components/docker/table/DockerEmptyState.tsx new file mode 100644 index 0000000..137333b --- /dev/null +++ b/application/src/components/docker/table/DockerEmptyState.tsx @@ -0,0 +1,23 @@ + +import { TableCell, TableRow } from "@/components/ui/table"; + +interface DockerEmptyStateProps { + searchTerm: string; +} + +export const DockerEmptyState = ({ searchTerm }: DockerEmptyStateProps) => { + return ( + + +
+
+ {searchTerm ? "No containers found" : "No containers running"} +
+
+ {searchTerm ? "Try adjusting your search terms." : "Start some containers to see them here."} +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/docker/table/DockerRowActions.tsx b/application/src/components/docker/table/DockerRowActions.tsx new file mode 100644 index 0000000..5ec429f --- /dev/null +++ b/application/src/components/docker/table/DockerRowActions.tsx @@ -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 ( + + + + + + onViewMetrics(container)} + className="cursor-pointer hover:bg-muted" + > + + View Metrics + + onContainerAction('view-detail', container.id, container.name)} + className="cursor-pointer hover:bg-muted" + > + + View Details + + + onContainerAction(containerStatus === 'running' ? 'stop' : 'start', container.id, container.name)} + className="cursor-pointer hover:bg-muted" + > + {containerStatus === 'running' ? ( + <> + + Stop Container + + ) : ( + <> + + Start Container + + )} + + onContainerAction('restart', container.id, container.name)} + className="cursor-pointer hover:bg-muted" + > + + Restart Container + + + onContainerAction('delete', container.id, container.name)} + className="cursor-pointer hover:bg-muted text-destructive focus:text-destructive" + > + + Remove Container + + + + ); +}; \ No newline at end of file diff --git a/application/src/components/docker/table/DockerTableHeader.tsx b/application/src/components/docker/table/DockerTableHeader.tsx new file mode 100644 index 0000000..927fcaf --- /dev/null +++ b/application/src/components/docker/table/DockerTableHeader.tsx @@ -0,0 +1,19 @@ + +import { TableHead, TableHeader, TableRow } from "@/components/ui/table"; + +export const DockerTableHeader = () => { + return ( + + + Container + Status + CPU Usage + Memory + Disk + Uptime + Last Checked + Actions + + + ); +}; \ No newline at end of file diff --git a/application/src/components/docker/table/DockerTableRow.tsx b/application/src/components/docker/table/DockerTableRow.tsx new file mode 100644 index 0000000..910d69a --- /dev/null +++ b/application/src/components/docker/table/DockerTableRow.tsx @@ -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 ( + onRowClick(container)} + > + +
+
{container.name}
+
+
{container.docker_id}
+
{container.hostname}
+
+
+
+ + + + +
+
+ + + {cpuPercentage}% + +
+
+
+ +
+
+ + + {formatPercentage(container.ram_used, container.ram_total)} + +
+
+ {dockerService.formatBytes(container.ram_used)} / {dockerService.formatBytes(container.ram_total)} +
+
+
+ +
+
+ + + {formatPercentage(container.disk_used, container.disk_total)} + +
+
+ {dockerService.formatBytes(container.disk_used)} / {dockerService.formatBytes(container.disk_total)} +
+
+
+ + + {dockerService.formatUptime(container.uptime)} + + + + + {new Date(container.last_checked).toLocaleString()} + + + e.stopPropagation()}> + + +
+ ); +}; \ No newline at end of file diff --git a/application/src/components/docker/table/DockerTableSearch.tsx b/application/src/components/docker/table/DockerTableSearch.tsx new file mode 100644 index 0000000..1953245 --- /dev/null +++ b/application/src/components/docker/table/DockerTableSearch.tsx @@ -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 ( +
+
+ + onSearchChange(e.target.value)} + className="pl-10 sm:w-64 bg-background border-border" + /> +
+ +
+ ); +}; \ No newline at end of file diff --git a/application/src/components/docker/table/index.ts b/application/src/components/docker/table/index.ts new file mode 100644 index 0000000..559a629 --- /dev/null +++ b/application/src/components/docker/table/index.ts @@ -0,0 +1,6 @@ + +export { DockerTableSearch } from './DockerTableSearch'; +export { DockerTableHeader } from './DockerTableHeader'; +export { DockerTableRow } from './DockerTableRow'; +export { DockerRowActions } from './DockerRowActions'; +export { DockerEmptyState } from './DockerEmptyState'; \ No newline at end of file diff --git a/application/src/pages/ContainerMonitoring.tsx b/application/src/pages/ContainerMonitoring.tsx new file mode 100644 index 0000000..8e760e2 --- /dev/null +++ b/application/src/pages/ContainerMonitoring.tsx @@ -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({ + 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 ( +
+ +
+
+
+
+

Error loading containers

+

+ Unable to fetch container data. Please check your connection and try again. +

+
+ Error: {error?.message || 'Unknown error'} +
+
+ + +
+
+
+
+
+ ); + } + + return ( +
+ +
+
+
+
+ {/* Header Section */} +
+
+
+
+ +
+

+ Container Monitoring +

+

+ Monitor and manage your Docker containers in real-time + {serverId && ( + + Server ID: {serverId} + + )} +

+
+
+
+ + {/* Stats Cards Section */} +
+ +
+ + {/* Containers Table Section */} +
+ +
+
+
+
+
+ ); +}; + +export default ContainerMonitoring; \ No newline at end of file diff --git a/application/src/services/dockerService.ts b/application/src/services/dockerService.ts new file mode 100644 index 0000000..d01e98e --- /dev/null +++ b/application/src/services/dockerService.ts @@ -0,0 +1,154 @@ + +import { pb } from "@/lib/pocketbase"; +import { DockerContainer, DockerMetrics, DockerStats } from "@/types/docker.types"; + +class DockerService { + async getContainers(): Promise { + 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 { + 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 { + 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 { + 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(); \ No newline at end of file diff --git a/application/src/types/docker.types.ts b/application/src/types/docker.types.ts new file mode 100644 index 0000000..453e54b --- /dev/null +++ b/application/src/types/docker.types.ts @@ -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; +} \ No newline at end of file