diff --git a/README.md b/README.md index 0cb8ef2..34ecf1f 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ services: ### ![checkcle-collapse-black](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-black.png) -![Service Detail Page](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/checkcle-detailpage.png) +![Service Detail Page](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/uptime-service-detail.png) ![Schedule Maintenance](https://pub-4a4062303020445f8f289a2fee84f9e8.r2.dev/images/maintenance-dahboard.png) ## 📝 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) diff --git a/application/src/App.tsx b/application/src/App.tsx index cd62250..e7f1e0b 100644 --- a/application/src/App.tsx +++ b/application/src/App.tsx @@ -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'; - -// 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 { Toaster } from '@/components/ui/sonner'; -import { authService } from '@/services/authService'; +import { ThemeProvider } from './contexts/ThemeContext'; +import { LanguageProvider } from './contexts/LanguageContext'; +import { SidebarProvider } from './contexts/SidebarContext'; -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} : ; -}; +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 ( - - - - - - - - } /> - } /> - } /> - - {/* Protected Routes */} - - - - } /> - - - - - } /> - - - - - } /> - - - - - } /> - - - - - } /> - - - - - } /> - - - - - } /> - - - - - } /> - - } /> - - + + + + + - - - - - + + {/* Public routes */} + } /> + + {/* Protected routes */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + + ); } diff --git a/application/src/components/auth/ProtectedRoute.tsx b/application/src/components/auth/ProtectedRoute.tsx new file mode 100644 index 0000000..22dad62 --- /dev/null +++ b/application/src/components/auth/ProtectedRoute.tsx @@ -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 ; + } + + return <>{children}; +}; \ No newline at end of file diff --git a/application/src/components/dashboard/sidebar/navigationData.ts b/application/src/components/dashboard/sidebar/navigationData.ts index f9956c5..0e2685f 100644 --- a/application/src/components/dashboard/sidebar/navigationData.ts +++ b/application/src/components/dashboard/sidebar/navigationData.ts @@ -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', 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/components/regional-monitoring/AddRegionalAgentDialog.tsx b/application/src/components/regional-monitoring/AddRegionalAgentDialog.tsx index 70caad3..c18cd18 100644 --- a/application/src/components/regional-monitoring/AddRegionalAgentDialog.tsx +++ b/application/src/components/regional-monitoring/AddRegionalAgentDialog.tsx @@ -250,7 +250,7 @@ export const AddRegionalAgentDialog: React.FC = ({