mirror of
				https://github.com/csehviktor/status-monitor.git
				synced 2025-08-08 18:06:14 +02:00 
			
		
		
		
	finish ui design
This commit is contained in:
		| @@ -1,6 +1,6 @@ | ||||
| import { UptimeMessage } from "@/services/types"; | ||||
| import type { UptimeMessage } from "@/services/types"; | ||||
| import { Progressbar } from "@/components/Progressbar"; | ||||
| import { formatRelativeTime, isAgentOnline } from "@/services/utils"; | ||||
| import { Progressbar } from "./Progressbar"; | ||||
|  | ||||
| type AgentOverviewCardProps = { | ||||
|     count: number; | ||||
|   | ||||
| @@ -50,7 +50,7 @@ export function ConnectionStatus({ status, timestamp }: ConnectionStatusProps) { | ||||
|                 {config.text} | ||||
|             </span> | ||||
|             {timestamp && ( | ||||
|                 <span className="text-xs text-gray-500"> | ||||
|                 <span className="text-xs text-gray-500 hidden md:block"> | ||||
|                     {new Date(timestamp).toLocaleTimeString()} | ||||
|                 </span> | ||||
|             )} | ||||
|   | ||||
							
								
								
									
										67
									
								
								ui/src/components/DonutChart.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								ui/src/components/DonutChart.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| import { ArcElement, Chart, Legend, Tooltip } from "chart.js"; | ||||
| import { Doughnut } from "react-chartjs-2"; | ||||
|  | ||||
| Chart.register(ArcElement, Tooltip, Legend); | ||||
|  | ||||
| type DonutChartProps = { | ||||
|     data: { | ||||
|         labels: string[]; | ||||
|         values: number[]; | ||||
|         colors: string[]; | ||||
|     }; | ||||
|     centerText?: string; | ||||
|     size?: "sm" | "md" | "lg"; | ||||
| }; | ||||
|  | ||||
| export function DonutChart({ data, centerText, size = "md" }: DonutChartProps) { | ||||
|     const sizeClasses = { | ||||
|         sm: "w-24 h-24", | ||||
|         md: "w-32 h-32", | ||||
|         lg: "w-40 h-40", | ||||
|     }; | ||||
|  | ||||
|     const chartData = { | ||||
|         labels: data.labels, | ||||
|         datasets: [ | ||||
|             { | ||||
|                 data: data.values, | ||||
|                 backgroundColor: data.colors, | ||||
|                 borderWidth: 0, | ||||
|                 cutout: "70%", | ||||
|             }, | ||||
|         ], | ||||
|     }; | ||||
|  | ||||
|     const options = { | ||||
|         responsive: true, | ||||
|         maintainAspectRatio: false, | ||||
|         plugins: { | ||||
|             legend: { | ||||
|                 display: false, | ||||
|             }, | ||||
|             tooltip: { | ||||
|                 backgroundColor: "rgba(17, 17, 17, 0.95)", | ||||
|                 titleColor: "#ffffff", | ||||
|                 bodyColor: "#ffffff", | ||||
|                 borderColor: "rgba(64, 64, 64, 0.5)", | ||||
|                 borderWidth: 1, | ||||
|                 cornerRadius: 8, | ||||
|             }, | ||||
|         }, | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|         <div className="relative"> | ||||
|             <div className={`relative ${sizeClasses[size]}`}> | ||||
|                 <Doughnut data={chartData} options={options} /> | ||||
|                 {centerText && ( | ||||
|                     <div className="absolute inset-0 flex items-center justify-center"> | ||||
|                         <span className="text-sm font-bold text-white"> | ||||
|                             {centerText} | ||||
|                         </span> | ||||
|                     </div> | ||||
|                 )} | ||||
|             </div> | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
| @@ -12,7 +12,7 @@ export function Header({ props }: { props: HeaderProps }) { | ||||
|     const navigate = useNavigate(); | ||||
|  | ||||
|     return ( | ||||
|         <header className="bg-[#0d0d0d] border-b border-[#191919]"> | ||||
|         <header className="bg-[#0d0d0d] border-b border-[#191919] px-2"> | ||||
|             <div className="max-w-7xl mx-auto py-4 flex justify-between items-center"> | ||||
|                 <div className="flex items-center space-x-3"> | ||||
|                     {props.hasBackButton && ( | ||||
|   | ||||
							
								
								
									
										107
									
								
								ui/src/components/LineChart.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								ui/src/components/LineChart.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| import type { ChartData } from "@/hooks/useChartData"; | ||||
| import { | ||||
|     CategoryScale, | ||||
|     Chart, | ||||
|     Filler, | ||||
|     Legend, | ||||
|     LinearScale, | ||||
|     LineElement, | ||||
|     PointElement, | ||||
|     Title, | ||||
|     Tooltip, | ||||
| } from "chart.js"; | ||||
| import { Line } from "react-chartjs-2"; | ||||
|  | ||||
| Chart.register( | ||||
|     CategoryScale, | ||||
|     LinearScale, | ||||
|     PointElement, | ||||
|     LineElement, | ||||
|     Title, | ||||
|     Tooltip, | ||||
|     Legend, | ||||
|     Filler, | ||||
| ); | ||||
|  | ||||
| type LineChartProps = { | ||||
|     data: ChartData; | ||||
|     height?: number; | ||||
| }; | ||||
|  | ||||
| export function LineChart({ data, height = 200 }: LineChartProps) { | ||||
|     const chartData = { | ||||
|         labels: data.labels, | ||||
|         datasets: data.datasets.map((dataset) => ({ | ||||
|             label: dataset.label, | ||||
|             data: dataset.data, | ||||
|             borderColor: dataset.color, | ||||
|             backgroundColor: dataset.color + "20", | ||||
|             fill: true, | ||||
|             tension: 0.4, | ||||
|             pointRadius: 0, | ||||
|             pointHoverRadius: 4, | ||||
|             borderWidth: 2, | ||||
|         })), | ||||
|     }; | ||||
|  | ||||
|     const options = { | ||||
|         responsive: true, | ||||
|         maintainAspectRatio: false, | ||||
|         scales: { | ||||
|             x: { | ||||
|                 display: true, | ||||
|                 grid: { | ||||
|                     color: "rgba(64, 64, 64, 0.3)", | ||||
|                     drawBorder: false, | ||||
|                 }, | ||||
|                 ticks: { | ||||
|                     color: "rgba(156, 163, 175, 0.8)", | ||||
|                     maxTicksLimit: 6, | ||||
|                 }, | ||||
|             }, | ||||
|             y: { | ||||
|                 display: true, | ||||
|                 beginAtZero: true, | ||||
|                 grid: { | ||||
|                     color: "rgba(64, 64, 64, 0.3)", | ||||
|                     drawBorder: false, | ||||
|                 }, | ||||
|                 ticks: { | ||||
|                     color: "rgba(156, 163, 175, 0.8)", | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|         plugins: { | ||||
|             legend: { | ||||
|                 display: data.datasets.length > 1, | ||||
|                 position: "top" as const, | ||||
|                 labels: { | ||||
|                     color: "rgba(156, 163, 175, 0.8)", | ||||
|                     usePointStyle: true, | ||||
|                     pointStyle: "circle", | ||||
|                 }, | ||||
|             }, | ||||
|             tooltip: { | ||||
|                 backgroundColor: "rgba(17, 17, 17, 0.95)", | ||||
|                 titleColor: "#ffffff", | ||||
|                 bodyColor: "#ffffff", | ||||
|                 borderColor: "rgba(64, 64, 64, 0.5)", | ||||
|                 borderWidth: 1, | ||||
|                 cornerRadius: 8, | ||||
|             }, | ||||
|         }, | ||||
|         interaction: { | ||||
|             intersect: false, | ||||
|             mode: "index" as const, | ||||
|         }, | ||||
|         animation: { | ||||
|             duration: 300, | ||||
|         }, | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|         <div style={{ height }}> | ||||
|             <Line data={chartData} options={options} /> | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
							
								
								
									
										27
									
								
								ui/src/components/LineChartCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								ui/src/components/LineChartCard.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| import type { ChartData } from "@/hooks/useChartData"; | ||||
| import { LineChart } from "@/components/LineChart"; | ||||
|  | ||||
| export function LineChartCard({ | ||||
|     title, | ||||
|     data, | ||||
| }: { | ||||
|     title: string; | ||||
|     data: ChartData; | ||||
| }) { | ||||
|     return ( | ||||
|         <div className="bg-[#0d0d0d] border border-[#191919] rounded-lg p-6"> | ||||
|             {data.labels.length > 0 ? ( | ||||
|                 <> | ||||
|                     <h3 className="text-lg font-semibold mb-4">{title}</h3> | ||||
|                     <div className="h-48"> | ||||
|                         <LineChart data={data} /> | ||||
|                     </div> | ||||
|                 </> | ||||
|             ) : ( | ||||
|                 <div className="h-[200px] flex items-center justify-center text-gray-500"> | ||||
|                     Waiting for data... | ||||
|                 </div> | ||||
|             )} | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
| @@ -31,7 +31,7 @@ export function MetricCard({ | ||||
|                     {props.value} | ||||
|                 </div> | ||||
|                 <div className="text-sm text-gray-500">{props.subtitle}</div> | ||||
|                 <div>{children}</div> | ||||
|                 <div className="mt-5">{children}</div> | ||||
|             </div> | ||||
|         </div> | ||||
|     ); | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| "use client"; | ||||
|  | ||||
| import { useEffect, useRef, useState } from "react"; | ||||
|  | ||||
| export function Progressbar({ | ||||
|   | ||||
							
								
								
									
										56
									
								
								ui/src/components/SysinfoCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								ui/src/components/SysinfoCard.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| import type { SystemInfo } from "@/services/types"; | ||||
| import { formatUptime } from "@/services/utils"; | ||||
|  | ||||
| export function SysinfoCard({ sysinfo }: { sysinfo: SystemInfo | undefined }) { | ||||
|     return ( | ||||
|         <div className="bg-[#0d0d0d] border border-[#191919] rounded-lg p-6"> | ||||
|             {sysinfo ? ( | ||||
|                 <> | ||||
|                     <h3 className="text-lg font-semibold mb-4"> | ||||
|                         System Information | ||||
|                     </h3> | ||||
|                     <div className="space-y-3"> | ||||
|                         <div className="flex justify-between items-center"> | ||||
|                             <span className="text-gray-500">Host</span> | ||||
|                             <span className="text-white font-mono text-sm"> | ||||
|                                 {sysinfo.host} | ||||
|                             </span> | ||||
|                         </div> | ||||
|  | ||||
|                         <div className="flex justify-between items-center"> | ||||
|                             <span className="text-gray-500">OS</span> | ||||
|                             <span className="text-white text-sm"> | ||||
|                                 {sysinfo.name} | ||||
|                             </span> | ||||
|                         </div> | ||||
|  | ||||
|                         <div className="flex justify-between items-center"> | ||||
|                             <span className="text-gray-500">Kernel</span> | ||||
|                             <span className="text-white font-mono text-sm"> | ||||
|                                 {sysinfo.kernel} | ||||
|                             </span> | ||||
|                         </div> | ||||
|  | ||||
|                         <div className="flex justify-between items-center"> | ||||
|                             <span className="text-gray-500">Uptime</span> | ||||
|                             <span className="text-white text-sm"> | ||||
|                                 {formatUptime(sysinfo.uptime)} | ||||
|                             </span> | ||||
|                         </div> | ||||
|  | ||||
|                         <div className="flex justify-between items-center"> | ||||
|                             <span className="text-gray-500">Version</span> | ||||
|                             <span className="text-white font-mono text-sm"> | ||||
|                                 {sysinfo.os_version} | ||||
|                             </span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </> | ||||
|             ) : ( | ||||
|                 <div className="h-[200px] flex items-center justify-center text-gray-500"> | ||||
|                     Waiting for data... | ||||
|                 </div> | ||||
|             )} | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
							
								
								
									
										106
									
								
								ui/src/hooks/useChartData.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								ui/src/hooks/useChartData.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| import type { StatusMessage } from "@/services/types"; | ||||
| import { addRealtimeData, getRealtimeData } from "@/services/data_service"; | ||||
| import { formatTimestamp } from "@/services/utils"; | ||||
| import { useEffect } from "react"; | ||||
|  | ||||
| export type ChartData = { | ||||
|     labels: string[]; | ||||
|     datasets: { | ||||
|         label: string; | ||||
|         data: number[]; | ||||
|         color: string; | ||||
|     }[]; | ||||
| }; | ||||
|  | ||||
| type ChartDataReturns = { | ||||
|     cpuData: ChartData; | ||||
|     memoryData: ChartData; | ||||
|     networkData: ChartData; | ||||
| }; | ||||
|  | ||||
| export function useChartData(data: StatusMessage | null): ChartDataReturns { | ||||
|     useEffect(() => { | ||||
|         if (!data) return; | ||||
|  | ||||
|         addRealtimeData(data); | ||||
|     }, [data]); | ||||
|  | ||||
|     const cpuPoints = getRealtimeData("cpu"); | ||||
|     const cpuUserPoints = getRealtimeData("cpu_user"); | ||||
|     const cpuSystemPoints = getRealtimeData("cpu_system"); | ||||
|     const cpuIdlePoints = getRealtimeData("cpu_idle"); | ||||
|     const cpuStealPoints = getRealtimeData("cpu_steal"); | ||||
|     const cpuIowaitPoints = getRealtimeData("cpu_iowait"); | ||||
|     const memoryPoints = getRealtimeData("memory"); | ||||
|     const swapPoints = getRealtimeData("swap"); | ||||
|     const networkUpPoints = getRealtimeData("network_up"); | ||||
|     const networkDownPoints = getRealtimeData("network_down"); | ||||
|  | ||||
|     return { | ||||
|         cpuData: { | ||||
|             labels: cpuPoints.map((p) => formatTimestamp(p.timestamp)), | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     label: "Total CPU (%)", | ||||
|                     data: cpuPoints.map((p) => p.value), | ||||
|                     color: "#3b82f6", | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "User (%)", | ||||
|                     data: cpuUserPoints.map((p) => p.value), | ||||
|                     color: "#10b981", | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "System (%)", | ||||
|                     data: cpuSystemPoints.map((p) => p.value), | ||||
|                     color: "#f59e0b", | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "I/O Wait (%)", | ||||
|                     data: cpuIowaitPoints.map((p) => p.value), | ||||
|                     color: "#ef4444", | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "Steal (%)", | ||||
|                     data: cpuStealPoints.map((p) => p.value), | ||||
|                     color: "#8b5cf6", | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "Idle (%)", | ||||
|                     data: cpuIdlePoints.map((p) => p.value), | ||||
|                     color: "#6b7280", | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|         memoryData: { | ||||
|             labels: memoryPoints.map((p) => formatTimestamp(p.timestamp)), | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     label: "Memory Usage (%)", | ||||
|                     data: memoryPoints.map((p) => p.value), | ||||
|                     color: "#10b981", | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "Swap Usage (%)", | ||||
|                     data: swapPoints.map((p) => p.value), | ||||
|                     color: "#f59e0b", | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|         networkData: { | ||||
|             labels: networkUpPoints.map((p) => formatTimestamp(p.timestamp)), | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     label: "Upload (B/s)", | ||||
|                     data: networkUpPoints.map((p) => p.value), | ||||
|                     color: "#ef4444", | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "Download (B/s)", | ||||
|                     data: networkDownPoints.map((p) => p.value), | ||||
|                     color: "#3b82f6", | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|     }; | ||||
| } | ||||
| @@ -1,15 +1,19 @@ | ||||
| import { | ||||
|     type WebsocketStatus, | ||||
|     ConnectionStatus, | ||||
|     WebsocketStatus, | ||||
| } from "@/components/ConnectionStatus"; | ||||
| import type { StatusMessage } from "@/services/types"; | ||||
| import { Header } from "@/components/Header"; | ||||
| import { DonutChart } from "@/components/DonutChart"; | ||||
| import { LineChartCard } from "@/components/LineChartCard"; | ||||
| import { MetricCard } from "@/components/MetricCard"; | ||||
| import { SysinfoCard } from "@/components/SysinfoCard"; | ||||
| import { useChartData } from "@/hooks/useChartData"; | ||||
| import { getLastMessage, setLastMessage } from "@/services/store"; | ||||
| import { StatusMessage } from "@/services/types"; | ||||
| import { | ||||
|     formatBytes, | ||||
|     calcPercentage, | ||||
|     formatPercentage, | ||||
|     breakdownMetrics, | ||||
| } from "@/services/utils"; | ||||
| import { initializeConnection } from "@/services/websocket"; | ||||
| import { useEffect, useState } from "react"; | ||||
| @@ -39,9 +43,10 @@ export function AgentPage() { | ||||
|         }); | ||||
|     }, [agent]); | ||||
|  | ||||
|     const { metrics } = message || {}; | ||||
|     const { metrics } = message ?? {}; | ||||
|     const { cpuData, memoryData, networkData } = useChartData(message); | ||||
|  | ||||
|     const getMetricsStatus = (percentage: number | undefined) => { | ||||
|     const getMetricStatus = (percentage: number | undefined) => { | ||||
|         if (!percentage) return "nil"; | ||||
|  | ||||
|         if (percentage < 50) return "good"; | ||||
| @@ -49,17 +54,20 @@ export function AgentPage() { | ||||
|         return "critical"; | ||||
|     }; | ||||
|  | ||||
|     const cpuUsage = metrics?.cpu.usage ?? 0; | ||||
|     const memoryUsage = calcPercentage( | ||||
|         metrics?.memory.used, | ||||
|         metrics?.memory.total, | ||||
|     ); | ||||
|     const diskUsage = calcPercentage( | ||||
|         (metrics?.disk.total ?? 0) - (metrics?.disk.free ?? 0), | ||||
|         metrics?.disk.total, | ||||
|     ); | ||||
|     const networkUsage = | ||||
|         (metrics?.network.up ?? 0) + (metrics?.network.down ?? 0); | ||||
|     const { | ||||
|         cpuThreads, | ||||
|         cpuUsage, | ||||
|         memoryUsage, | ||||
|         memoryUsed, | ||||
|         memoryTotal, | ||||
|         diskUsage, | ||||
|         diskFree, | ||||
|         diskTotal, | ||||
|         networkUp, | ||||
|         networkDown, | ||||
|     } = breakdownMetrics(message?.metrics); | ||||
|  | ||||
|     const networkUsage = networkUp + networkDown; | ||||
|  | ||||
|     return ( | ||||
|         <div> | ||||
| @@ -76,64 +84,103 @@ export function AgentPage() { | ||||
|                     ), | ||||
|                 }} | ||||
|             /> | ||||
|  | ||||
|             <main className="max-w-7xl mx-auto py-8"> | ||||
|                 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8"> | ||||
|                     <MetricCard | ||||
|                         props={{ | ||||
|                             title: "CPU USAGE", | ||||
|                             value: formatPercentage(cpuUsage), | ||||
|                             status: getMetricsStatus(cpuUsage), | ||||
|                             subtitle: `${metrics?.cpu.threads ?? 0} threads`, | ||||
|                         }} | ||||
|                     > | ||||
|                         <h1>cpu chart</h1> | ||||
|                     </MetricCard> | ||||
|                 <div className="mx-2"> | ||||
|                     <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8"> | ||||
|                         <MetricCard | ||||
|                             props={{ | ||||
|                                 title: "CPU USAGE", | ||||
|                                 value: formatPercentage(cpuUsage), | ||||
|                                 status: getMetricStatus(cpuUsage), | ||||
|                                 subtitle: `${cpuThreads} threads`, | ||||
|                             }} | ||||
|                         > | ||||
|                             <DonutChart | ||||
|                                 data={{ | ||||
|                                     labels: ["Usage", "Total"], | ||||
|                                     values: [cpuUsage, 100 - cpuUsage], | ||||
|                                     colors: ["#3b82f6", "#262626"], | ||||
|                                 }} | ||||
|                                 centerText={formatPercentage(cpuUsage)} | ||||
|                                 size="sm" | ||||
|                             /> | ||||
|                         </MetricCard> | ||||
|  | ||||
|                     <MetricCard | ||||
|                         props={{ | ||||
|                             title: "MEMORY USAGE", | ||||
|                             value: formatPercentage(memoryUsage), | ||||
|                             status: getMetricsStatus(memoryUsage), | ||||
|                             subtitle: `${formatBytes(metrics?.memory.used)} / ${formatBytes(metrics?.memory.total)}`, | ||||
|                         }} | ||||
|                     > | ||||
|                         <h1>memory chart</h1> | ||||
|                     </MetricCard> | ||||
|                         <MetricCard | ||||
|                             props={{ | ||||
|                                 title: "MEMORY USAGE", | ||||
|                                 value: formatPercentage(memoryUsage), | ||||
|                                 status: getMetricStatus(memoryUsage), | ||||
|                                 subtitle: `${formatBytes(memoryUsed)} / ${formatBytes(memoryTotal)}`, | ||||
|                             }} | ||||
|                         > | ||||
|                             <DonutChart | ||||
|                                 data={{ | ||||
|                                     labels: ["Used", "Free"], | ||||
|                                     values: [memoryUsage, 100 - memoryUsage], | ||||
|                                     colors: ["#3b82f6", "#262626"], | ||||
|                                 }} | ||||
|                                 centerText={formatPercentage(memoryUsage)} | ||||
|                                 size="sm" | ||||
|                             /> | ||||
|                         </MetricCard> | ||||
|  | ||||
|                     <MetricCard | ||||
|                         props={{ | ||||
|                             title: "NETWORK ACTIVITY", | ||||
|                             value: formatBytes(networkUsage), | ||||
|                             status: getMetricsStatus(networkUsage), | ||||
|                             subtitle: `↑ ${formatBytes(metrics?.network.up)}/s ↓ ${formatBytes(metrics?.network.down)}/s`, | ||||
|                         }} | ||||
|                     > | ||||
|                         <h1>network chart</h1> | ||||
|                     </MetricCard> | ||||
|                 </div> | ||||
|  | ||||
|                 <MetricCard | ||||
|                     props={{ | ||||
|                         title: "DISK USAGE", | ||||
|                         value: formatPercentage(diskUsage), | ||||
|                         status: getMetricsStatus(diskUsage), | ||||
|                         subtitle: `${formatBytes(metrics?.disk.free)} free of ${formatBytes(metrics?.disk.total)}`, | ||||
|                     }} | ||||
|                 > | ||||
|                     <div className="w-full bg-[#141414] rounded-full h-4"> | ||||
|                         <div | ||||
|                             className={`h-4 rounded-full transition-all duration-500 ${ | ||||
|                                 getMetricsStatus(diskUsage) == "good" | ||||
|                                     ? "bg-green-500" | ||||
|                                     : getMetricsStatus(diskUsage) == "warning" | ||||
|                                       ? "bg-yellow-500" | ||||
|                                       : "bg-red-500" | ||||
|                             }`} | ||||
|                             style={{ width: `${diskUsage}%` }} | ||||
|                         /> | ||||
|                         <MetricCard | ||||
|                             props={{ | ||||
|                                 title: "NETWORK ACTIVITY", | ||||
|                                 value: formatBytes(networkUsage), | ||||
|                                 status: "nil", | ||||
|                                 subtitle: `↑ ${formatBytes(networkUp)}/s ↓ ${formatBytes(networkDown)}/s`, | ||||
|                             }} | ||||
|                         > | ||||
|                             <DonutChart | ||||
|                                 data={{ | ||||
|                                     labels: ["Upload", "Download"], | ||||
|                                     values: [ | ||||
|                                         message ? networkUp : 1, | ||||
|                                         message ? networkDown : 1, | ||||
|                                     ], | ||||
|                                     colors: ["#ef4444", "#3b82f6"], | ||||
|                                 }} | ||||
|                                 size="sm" | ||||
|                             /> | ||||
|                         </MetricCard> | ||||
|                     </div> | ||||
|                 </MetricCard> | ||||
|  | ||||
|                     <MetricCard | ||||
|                         props={{ | ||||
|                             title: "DISK USAGE", | ||||
|                             value: formatPercentage(diskUsage), | ||||
|                             status: getMetricStatus(diskUsage), | ||||
|                             subtitle: `${formatBytes(diskFree)} free of ${formatBytes(diskTotal)}`, | ||||
|                         }} | ||||
|                     > | ||||
|                         <div className="w-full bg-[#141414] rounded-full h-4"> | ||||
|                             <div | ||||
|                                 className={`h-4 rounded-full transition-all duration-500 ${ | ||||
|                                     getMetricStatus(diskUsage) == "good" | ||||
|                                         ? "bg-green-500" | ||||
|                                         : getMetricStatus(diskUsage) == | ||||
|                                             "warning" | ||||
|                                           ? "bg-yellow-500" | ||||
|                                           : "bg-red-500" | ||||
|                                 }`} | ||||
|                                 style={{ width: `${diskUsage}%` }} | ||||
|                             /> | ||||
|                         </div> | ||||
|                     </MetricCard> | ||||
|  | ||||
|                     <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 my-8"> | ||||
|                         <LineChartCard title="CPU Usage" data={cpuData} /> | ||||
|                         <LineChartCard title="Memory Usage" data={memoryData} /> | ||||
|                         <LineChartCard | ||||
|                             title="Network Activity" | ||||
|                             data={networkData} | ||||
|                         /> | ||||
|  | ||||
|                         <SysinfoCard sysinfo={metrics?.system_info} /> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </main> | ||||
|         </div> | ||||
|     ); | ||||
|   | ||||
| @@ -1,8 +1,6 @@ | ||||
| "use client"; | ||||
|  | ||||
| import type { UptimeMessage } from "@/services/types"; | ||||
| import { AgentCard, AgentOverviewCard } from "@/components/AgentCard"; | ||||
| import { Header } from "@/components/Header"; | ||||
| import { UptimeMessage } from "@/services/types"; | ||||
| import { isAgentOnline } from "@/services/utils"; | ||||
| import { Box } from "lucide-react"; | ||||
| import { useEffect, useState } from "react"; | ||||
|   | ||||
							
								
								
									
										82
									
								
								ui/src/services/data_service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								ui/src/services/data_service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| import type { StatusMessage } from "@/services/types"; | ||||
| import { breakdownMetrics } from "@/services/utils"; | ||||
|  | ||||
| type HistoricalDataPoint = { | ||||
|     timestamp: string; | ||||
|     value: number; | ||||
| }; | ||||
|  | ||||
| /* | ||||
| type HistoricalMetrics = { | ||||
|     cpu: HistoricalDataPoint[]; | ||||
|     cpu_user: HistoricalDataPoint[]; | ||||
|     cpu_system: HistoricalDataPoint[]; | ||||
|     cpu_idle: HistoricalDataPoint[]; | ||||
|     cpu_steal: HistoricalDataPoint[]; | ||||
|     cpu_iowait: HistoricalDataPoint[]; | ||||
|     memory: HistoricalDataPoint[]; | ||||
|     swap: HistoricalDataPoint[]; | ||||
|     network_up: HistoricalDataPoint[]; | ||||
|     network_down: HistoricalDataPoint[]; | ||||
| }; | ||||
| */ | ||||
|  | ||||
| //export type TimePeriod = "realtime" | "hour" | "day" | "week"; | ||||
|  | ||||
| const realtimeData = new Map<string, HistoricalDataPoint[]>(); | ||||
| const maxRealtimePoints = 50; | ||||
|  | ||||
| function addDataPoint(metric: string, timestamp: string, value: number) { | ||||
|     if (!realtimeData.has(metric)) { | ||||
|         realtimeData.set(metric, []); | ||||
|     } | ||||
|  | ||||
|     const data = realtimeData.get(metric)!; | ||||
|     data.push({ timestamp, value }); | ||||
|  | ||||
|     if (data.length > maxRealtimePoints) { | ||||
|         data.shift(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export const addRealtimeData = (data: StatusMessage): void => { | ||||
|     const timestamp = data.timestamp; | ||||
|  | ||||
|     const { | ||||
|         cpuUsage, | ||||
|         cpuUser, | ||||
|         cpuSystem, | ||||
|         cpuIdle, | ||||
|         cpuSteal, | ||||
|         cpuIowait, | ||||
|         memoryUsage, | ||||
|         swapUsage, | ||||
|         networkUp, | ||||
|         networkDown, | ||||
|     } = breakdownMetrics(data.metrics); | ||||
|  | ||||
|     addDataPoint("cpu", timestamp, cpuUsage); | ||||
|     addDataPoint("cpu_user", timestamp, cpuUser); | ||||
|     addDataPoint("cpu_system", timestamp, cpuSystem); | ||||
|     addDataPoint("cpu_idle", timestamp, cpuIdle); | ||||
|     addDataPoint("cpu_steal", timestamp, cpuSteal); | ||||
|     addDataPoint("cpu_iowait", timestamp, cpuIowait); | ||||
|     addDataPoint("memory", timestamp, memoryUsage); | ||||
|     addDataPoint("swap", timestamp, swapUsage); | ||||
|     addDataPoint("network_up", timestamp, networkUp); | ||||
|     addDataPoint("network_down", timestamp, networkDown); | ||||
| }; | ||||
|  | ||||
| export function getRealtimeData(metric: string): HistoricalDataPoint[] { | ||||
|     return realtimeData.get(metric) || []; | ||||
| } | ||||
|  | ||||
| /* | ||||
| export async function getHistoricalData(period: TimePeriod): Promise<HistoricalMetrics> { | ||||
|     return ...todo | ||||
| } | ||||
|  | ||||
| function clearRealtimeData() { | ||||
|     realtimeData.clear(); | ||||
| } | ||||
| */ | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { UptimeMessage } from "@/services/types"; | ||||
| import type { Metrics, UptimeMessage } from "@/services/types"; | ||||
|  | ||||
| export function isAgentOnline(data: UptimeMessage): boolean { | ||||
|     const timeDiff = new Date().getTime() - new Date(data.last_seen).getTime(); | ||||
| @@ -6,6 +6,24 @@ export function isAgentOnline(data: UptimeMessage): boolean { | ||||
|     return timeDiff < 10000; | ||||
| } | ||||
|  | ||||
| export function formatTimestamp(timestamp: string) { | ||||
|     return new Date(timestamp).toLocaleTimeString(); | ||||
| } | ||||
|  | ||||
| export const formatUptime = (seconds: number): string => { | ||||
|     const days = Math.floor(seconds / 86400); | ||||
|     const hours = Math.floor((seconds % 86400) / 3600); | ||||
|     const minutes = Math.floor((seconds % 3600) / 60); | ||||
|  | ||||
|     if (days > 0) { | ||||
|         return `${days}d ${hours}h`; | ||||
|     } else if (hours > 0) { | ||||
|         return `${hours}h ${minutes}m`; | ||||
|     } else { | ||||
|         return `${minutes}m`; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| export function formatRelativeTime(timestamp: string): string { | ||||
|     const now = new Date(); | ||||
|     const past = new Date(timestamp); | ||||
| @@ -48,3 +66,54 @@ export function calcPercentage( | ||||
| export function formatPercentage(val: number | undefined) { | ||||
|     return `${(val ?? 0).toFixed(2)}%`; | ||||
| } | ||||
|  | ||||
| export type BrokedownMetrics = { | ||||
|     cpuThreads: number; | ||||
|     cpuUsage: number; | ||||
|     cpuUser: number; | ||||
|     cpuSystem: number; | ||||
|     cpuIdle: number; | ||||
|     cpuSteal: number; | ||||
|     cpuIowait: number; | ||||
|     memoryUsage: number; | ||||
|     memoryUsed: number; | ||||
|     memoryTotal: number; | ||||
|     swapUsage: number; | ||||
|     diskUsage: number; | ||||
|     diskFree: number; | ||||
|     diskTotal: number; | ||||
|     networkUp: number; | ||||
|     networkDown: number; | ||||
| }; | ||||
|  | ||||
| export function breakdownMetrics( | ||||
|     metrics: Metrics | undefined, | ||||
| ): BrokedownMetrics { | ||||
|     return { | ||||
|         cpuThreads: metrics?.cpu.threads ?? 0, | ||||
|         cpuUsage: metrics?.cpu.usage ?? 0, | ||||
|         cpuUser: metrics?.cpu.breakdown.user ?? 0, | ||||
|         cpuSystem: metrics?.cpu.breakdown.system ?? 0, | ||||
|         cpuIdle: metrics?.cpu.breakdown.idle ?? 0, | ||||
|         cpuSteal: metrics?.cpu.breakdown.steal ?? 0, | ||||
|         cpuIowait: metrics?.cpu.breakdown.iowait ?? 0, | ||||
|         memoryUsage: calcPercentage( | ||||
|             metrics?.memory.used, | ||||
|             metrics?.memory.total, | ||||
|         ), | ||||
|         memoryUsed: metrics?.memory.used ?? 0, | ||||
|         memoryTotal: metrics?.memory.total ?? 0, | ||||
|         swapUsage: calcPercentage( | ||||
|             metrics?.memory.swap_used, | ||||
|             metrics?.memory.swap_total, | ||||
|         ), | ||||
|         diskUsage: calcPercentage( | ||||
|             (metrics?.disk.total ?? 0) - (metrics?.disk.free ?? 0), | ||||
|             metrics?.disk.total, | ||||
|         ), | ||||
|         diskFree: metrics?.disk.free ?? 0, | ||||
|         diskTotal: metrics?.disk.total ?? 0, | ||||
|         networkUp: (metrics?.network.up ?? 0) / 1024, | ||||
|         networkDown: (metrics?.network.down ?? 0) / 1024, | ||||
|     }; | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user