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:
		
							
								
								
									
										18
									
								
								ui/bun.lock
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								ui/bun.lock
									
									
									
									
									
								
							| @@ -6,7 +6,7 @@ | |||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "lucide-react": "^0.525.0", |         "lucide-react": "^0.525.0", | ||||||
|         "react": "^19.1.0", |         "react": "^19.1.0", | ||||||
|         "react-dom": "^19.1.0", |         "react-dom": "^19.1.0" | ||||||
|       }, |       }, | ||||||
|       "devDependencies": { |       "devDependencies": { | ||||||
|         "@eslint/js": "^9.29.0", |         "@eslint/js": "^9.29.0", | ||||||
| @@ -15,18 +15,20 @@ | |||||||
|         "@types/react": "^19.1.8", |         "@types/react": "^19.1.8", | ||||||
|         "@types/react-dom": "^19.1.6", |         "@types/react-dom": "^19.1.6", | ||||||
|         "@vitejs/plugin-react": "^4.5.2", |         "@vitejs/plugin-react": "^4.5.2", | ||||||
|  |         "chart.js": "^4.5.0", | ||||||
|         "eslint": "^9.29.0", |         "eslint": "^9.29.0", | ||||||
|         "eslint-plugin-react-hooks": "^5.2.0", |         "eslint-plugin-react-hooks": "^5.2.0", | ||||||
|         "eslint-plugin-react-refresh": "^0.4.20", |         "eslint-plugin-react-refresh": "^0.4.20", | ||||||
|         "globals": "^16.2.0", |         "globals": "^16.2.0", | ||||||
|         "nanostores": "^1.0.1", |         "nanostores": "^1.0.1", | ||||||
|  |         "react-chartjs-2": "^5.3.0", | ||||||
|         "react-router-dom": "^7.6.3", |         "react-router-dom": "^7.6.3", | ||||||
|         "tailwindcss": "^4.1.11", |         "tailwindcss": "^4.1.11", | ||||||
|         "typescript": "~5.8.3", |         "typescript": "~5.8.3", | ||||||
|         "typescript-eslint": "^8.34.1", |         "typescript-eslint": "^8.34.1", | ||||||
|         "vite": "^7.0.0", |         "vite": "^7.0.0" | ||||||
|       }, |       } | ||||||
|     }, |     } | ||||||
|   }, |   }, | ||||||
|   "packages": { |   "packages": { | ||||||
|     "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], |     "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], | ||||||
| @@ -155,6 +157,8 @@ | |||||||
|  |  | ||||||
|     "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], |     "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], | ||||||
|  |  | ||||||
|  |     "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], | ||||||
|  |  | ||||||
|     "@nanostores/react": ["@nanostores/react@1.0.0", "", { "peerDependencies": { "nanostores": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^1.0.0", "react": ">=18.0.0" } }, "sha512-eDduyNy+lbQJMg6XxZ/YssQqF6b4OXMFEZMYKPJCCmBevp1lg0g+4ZRi94qGHirMtsNfAWKNwsjOhC+q1gvC+A=="], |     "@nanostores/react": ["@nanostores/react@1.0.0", "", { "peerDependencies": { "nanostores": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^1.0.0", "react": ">=18.0.0" } }, "sha512-eDduyNy+lbQJMg6XxZ/YssQqF6b4OXMFEZMYKPJCCmBevp1lg0g+4ZRi94qGHirMtsNfAWKNwsjOhC+q1gvC+A=="], | ||||||
|  |  | ||||||
|     "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], |     "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], | ||||||
| @@ -297,6 +301,8 @@ | |||||||
|  |  | ||||||
|     "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], |     "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], | ||||||
|  |  | ||||||
|  |     "chart.js": ["chart.js@4.5.0", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ=="], | ||||||
|  |  | ||||||
|     "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], |     "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], | ||||||
|  |  | ||||||
|     "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], |     "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], | ||||||
| @@ -499,6 +505,8 @@ | |||||||
|  |  | ||||||
|     "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], |     "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], | ||||||
|  |  | ||||||
|  |     "react-chartjs-2": ["react-chartjs-2@5.3.0", "", { "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw=="], | ||||||
|  |  | ||||||
|     "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], |     "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], | ||||||
|  |  | ||||||
|     "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], |     "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], | ||||||
| @@ -595,6 +603,6 @@ | |||||||
|  |  | ||||||
|     "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], |     "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], | ||||||
|  |  | ||||||
|     "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], |     "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="] | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -21,11 +21,13 @@ | |||||||
|         "@types/react": "^19.1.8", |         "@types/react": "^19.1.8", | ||||||
|         "@types/react-dom": "^19.1.6", |         "@types/react-dom": "^19.1.6", | ||||||
|         "@vitejs/plugin-react": "^4.5.2", |         "@vitejs/plugin-react": "^4.5.2", | ||||||
|  |         "chart.js": "^4.5.0", | ||||||
|         "eslint": "^9.29.0", |         "eslint": "^9.29.0", | ||||||
|         "eslint-plugin-react-hooks": "^5.2.0", |         "eslint-plugin-react-hooks": "^5.2.0", | ||||||
|         "eslint-plugin-react-refresh": "^0.4.20", |         "eslint-plugin-react-refresh": "^0.4.20", | ||||||
|         "globals": "^16.2.0", |         "globals": "^16.2.0", | ||||||
|         "nanostores": "^1.0.1", |         "nanostores": "^1.0.1", | ||||||
|  |         "react-chartjs-2": "^5.3.0", | ||||||
|         "react-router-dom": "^7.6.3", |         "react-router-dom": "^7.6.3", | ||||||
|         "tailwindcss": "^4.1.11", |         "tailwindcss": "^4.1.11", | ||||||
|         "typescript": "~5.8.3", |         "typescript": "~5.8.3", | ||||||
|   | |||||||
| @@ -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 { formatRelativeTime, isAgentOnline } from "@/services/utils"; | ||||||
| import { Progressbar } from "./Progressbar"; |  | ||||||
|  |  | ||||||
| type AgentOverviewCardProps = { | type AgentOverviewCardProps = { | ||||||
|     count: number; |     count: number; | ||||||
|   | |||||||
| @@ -50,7 +50,7 @@ export function ConnectionStatus({ status, timestamp }: ConnectionStatusProps) { | |||||||
|                 {config.text} |                 {config.text} | ||||||
|             </span> |             </span> | ||||||
|             {timestamp && ( |             {timestamp && ( | ||||||
|                 <span className="text-xs text-gray-500"> |                 <span className="text-xs text-gray-500 hidden md:block"> | ||||||
|                     {new Date(timestamp).toLocaleTimeString()} |                     {new Date(timestamp).toLocaleTimeString()} | ||||||
|                 </span> |                 </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(); |     const navigate = useNavigate(); | ||||||
|  |  | ||||||
|     return ( |     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="max-w-7xl mx-auto py-4 flex justify-between items-center"> | ||||||
|                 <div className="flex items-center space-x-3"> |                 <div className="flex items-center space-x-3"> | ||||||
|                     {props.hasBackButton && ( |                     {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} |                     {props.value} | ||||||
|                 </div> |                 </div> | ||||||
|                 <div className="text-sm text-gray-500">{props.subtitle}</div> |                 <div className="text-sm text-gray-500">{props.subtitle}</div> | ||||||
|                 <div>{children}</div> |                 <div className="mt-5">{children}</div> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -1,5 +1,3 @@ | |||||||
| "use client"; |  | ||||||
|  |  | ||||||
| import { useEffect, useRef, useState } from "react"; | import { useEffect, useRef, useState } from "react"; | ||||||
|  |  | ||||||
| export function Progressbar({ | 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 { | import { | ||||||
|  |     type WebsocketStatus, | ||||||
|     ConnectionStatus, |     ConnectionStatus, | ||||||
|     WebsocketStatus, |  | ||||||
| } from "@/components/ConnectionStatus"; | } from "@/components/ConnectionStatus"; | ||||||
|  | import type { StatusMessage } from "@/services/types"; | ||||||
| import { Header } from "@/components/Header"; | import { Header } from "@/components/Header"; | ||||||
|  | import { DonutChart } from "@/components/DonutChart"; | ||||||
|  | import { LineChartCard } from "@/components/LineChartCard"; | ||||||
| import { MetricCard } from "@/components/MetricCard"; | import { MetricCard } from "@/components/MetricCard"; | ||||||
|  | import { SysinfoCard } from "@/components/SysinfoCard"; | ||||||
|  | import { useChartData } from "@/hooks/useChartData"; | ||||||
| import { getLastMessage, setLastMessage } from "@/services/store"; | import { getLastMessage, setLastMessage } from "@/services/store"; | ||||||
| import { StatusMessage } from "@/services/types"; |  | ||||||
| import { | import { | ||||||
|     formatBytes, |     formatBytes, | ||||||
|     calcPercentage, |  | ||||||
|     formatPercentage, |     formatPercentage, | ||||||
|  |     breakdownMetrics, | ||||||
| } from "@/services/utils"; | } from "@/services/utils"; | ||||||
| import { initializeConnection } from "@/services/websocket"; | import { initializeConnection } from "@/services/websocket"; | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| @@ -39,9 +43,10 @@ export function AgentPage() { | |||||||
|         }); |         }); | ||||||
|     }, [agent]); |     }, [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) return "nil"; | ||||||
|  |  | ||||||
|         if (percentage < 50) return "good"; |         if (percentage < 50) return "good"; | ||||||
| @@ -49,17 +54,20 @@ export function AgentPage() { | |||||||
|         return "critical"; |         return "critical"; | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     const cpuUsage = metrics?.cpu.usage ?? 0; |     const { | ||||||
|     const memoryUsage = calcPercentage( |         cpuThreads, | ||||||
|         metrics?.memory.used, |         cpuUsage, | ||||||
|         metrics?.memory.total, |         memoryUsage, | ||||||
|     ); |         memoryUsed, | ||||||
|     const diskUsage = calcPercentage( |         memoryTotal, | ||||||
|         (metrics?.disk.total ?? 0) - (metrics?.disk.free ?? 0), |         diskUsage, | ||||||
|         metrics?.disk.total, |         diskFree, | ||||||
|     ); |         diskTotal, | ||||||
|     const networkUsage = |         networkUp, | ||||||
|         (metrics?.network.up ?? 0) + (metrics?.network.down ?? 0); |         networkDown, | ||||||
|  |     } = breakdownMetrics(message?.metrics); | ||||||
|  |  | ||||||
|  |     const networkUsage = networkUp + networkDown; | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <div> |         <div> | ||||||
| @@ -76,40 +84,66 @@ export function AgentPage() { | |||||||
|                     ), |                     ), | ||||||
|                 }} |                 }} | ||||||
|             /> |             /> | ||||||
|  |  | ||||||
|             <main className="max-w-7xl mx-auto py-8"> |             <main className="max-w-7xl mx-auto py-8"> | ||||||
|  |                 <div className="mx-2"> | ||||||
|                     <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8"> |                     <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8"> | ||||||
|                         <MetricCard |                         <MetricCard | ||||||
|                             props={{ |                             props={{ | ||||||
|                                 title: "CPU USAGE", |                                 title: "CPU USAGE", | ||||||
|                                 value: formatPercentage(cpuUsage), |                                 value: formatPercentage(cpuUsage), | ||||||
|                             status: getMetricsStatus(cpuUsage), |                                 status: getMetricStatus(cpuUsage), | ||||||
|                             subtitle: `${metrics?.cpu.threads ?? 0} threads`, |                                 subtitle: `${cpuThreads} threads`, | ||||||
|                             }} |                             }} | ||||||
|                         > |                         > | ||||||
|                         <h1>cpu chart</h1> |                             <DonutChart | ||||||
|  |                                 data={{ | ||||||
|  |                                     labels: ["Usage", "Total"], | ||||||
|  |                                     values: [cpuUsage, 100 - cpuUsage], | ||||||
|  |                                     colors: ["#3b82f6", "#262626"], | ||||||
|  |                                 }} | ||||||
|  |                                 centerText={formatPercentage(cpuUsage)} | ||||||
|  |                                 size="sm" | ||||||
|  |                             /> | ||||||
|                         </MetricCard> |                         </MetricCard> | ||||||
|  |  | ||||||
|                         <MetricCard |                         <MetricCard | ||||||
|                             props={{ |                             props={{ | ||||||
|                                 title: "MEMORY USAGE", |                                 title: "MEMORY USAGE", | ||||||
|                                 value: formatPercentage(memoryUsage), |                                 value: formatPercentage(memoryUsage), | ||||||
|                             status: getMetricsStatus(memoryUsage), |                                 status: getMetricStatus(memoryUsage), | ||||||
|                             subtitle: `${formatBytes(metrics?.memory.used)} / ${formatBytes(metrics?.memory.total)}`, |                                 subtitle: `${formatBytes(memoryUsed)} / ${formatBytes(memoryTotal)}`, | ||||||
|                             }} |                             }} | ||||||
|                         > |                         > | ||||||
|                         <h1>memory chart</h1> |                             <DonutChart | ||||||
|  |                                 data={{ | ||||||
|  |                                     labels: ["Used", "Free"], | ||||||
|  |                                     values: [memoryUsage, 100 - memoryUsage], | ||||||
|  |                                     colors: ["#3b82f6", "#262626"], | ||||||
|  |                                 }} | ||||||
|  |                                 centerText={formatPercentage(memoryUsage)} | ||||||
|  |                                 size="sm" | ||||||
|  |                             /> | ||||||
|                         </MetricCard> |                         </MetricCard> | ||||||
|  |  | ||||||
|                         <MetricCard |                         <MetricCard | ||||||
|                             props={{ |                             props={{ | ||||||
|                                 title: "NETWORK ACTIVITY", |                                 title: "NETWORK ACTIVITY", | ||||||
|                                 value: formatBytes(networkUsage), |                                 value: formatBytes(networkUsage), | ||||||
|                             status: getMetricsStatus(networkUsage), |                                 status: "nil", | ||||||
|                             subtitle: `↑ ${formatBytes(metrics?.network.up)}/s ↓ ${formatBytes(metrics?.network.down)}/s`, |                                 subtitle: `↑ ${formatBytes(networkUp)}/s ↓ ${formatBytes(networkDown)}/s`, | ||||||
|                             }} |                             }} | ||||||
|                         > |                         > | ||||||
|                         <h1>network chart</h1> |                             <DonutChart | ||||||
|  |                                 data={{ | ||||||
|  |                                     labels: ["Upload", "Download"], | ||||||
|  |                                     values: [ | ||||||
|  |                                         message ? networkUp : 1, | ||||||
|  |                                         message ? networkDown : 1, | ||||||
|  |                                     ], | ||||||
|  |                                     colors: ["#ef4444", "#3b82f6"], | ||||||
|  |                                 }} | ||||||
|  |                                 size="sm" | ||||||
|  |                             /> | ||||||
|                         </MetricCard> |                         </MetricCard> | ||||||
|                     </div> |                     </div> | ||||||
|  |  | ||||||
| @@ -117,16 +151,17 @@ export function AgentPage() { | |||||||
|                         props={{ |                         props={{ | ||||||
|                             title: "DISK USAGE", |                             title: "DISK USAGE", | ||||||
|                             value: formatPercentage(diskUsage), |                             value: formatPercentage(diskUsage), | ||||||
|                         status: getMetricsStatus(diskUsage), |                             status: getMetricStatus(diskUsage), | ||||||
|                         subtitle: `${formatBytes(metrics?.disk.free)} free of ${formatBytes(metrics?.disk.total)}`, |                             subtitle: `${formatBytes(diskFree)} free of ${formatBytes(diskTotal)}`, | ||||||
|                         }} |                         }} | ||||||
|                     > |                     > | ||||||
|                         <div className="w-full bg-[#141414] rounded-full h-4"> |                         <div className="w-full bg-[#141414] rounded-full h-4"> | ||||||
|                             <div |                             <div | ||||||
|                                 className={`h-4 rounded-full transition-all duration-500 ${ |                                 className={`h-4 rounded-full transition-all duration-500 ${ | ||||||
|                                 getMetricsStatus(diskUsage) == "good" |                                     getMetricStatus(diskUsage) == "good" | ||||||
|                                         ? "bg-green-500" |                                         ? "bg-green-500" | ||||||
|                                     : getMetricsStatus(diskUsage) == "warning" |                                         : getMetricStatus(diskUsage) == | ||||||
|  |                                             "warning" | ||||||
|                                           ? "bg-yellow-500" |                                           ? "bg-yellow-500" | ||||||
|                                           : "bg-red-500" |                                           : "bg-red-500" | ||||||
|                                 }`} |                                 }`} | ||||||
| @@ -134,6 +169,18 @@ export function AgentPage() { | |||||||
|                             /> |                             /> | ||||||
|                         </div> |                         </div> | ||||||
|                     </MetricCard> |                     </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> |             </main> | ||||||
|         </div> |         </div> | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -1,8 +1,6 @@ | |||||||
| "use client"; | import type { UptimeMessage } from "@/services/types"; | ||||||
|  |  | ||||||
| import { AgentCard, AgentOverviewCard } from "@/components/AgentCard"; | import { AgentCard, AgentOverviewCard } from "@/components/AgentCard"; | ||||||
| import { Header } from "@/components/Header"; | import { Header } from "@/components/Header"; | ||||||
| import { UptimeMessage } from "@/services/types"; |  | ||||||
| import { isAgentOnline } from "@/services/utils"; | import { isAgentOnline } from "@/services/utils"; | ||||||
| import { Box } from "lucide-react"; | import { Box } from "lucide-react"; | ||||||
| import { useEffect, useState } from "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 { | export function isAgentOnline(data: UptimeMessage): boolean { | ||||||
|     const timeDiff = new Date().getTime() - new Date(data.last_seen).getTime(); |     const timeDiff = new Date().getTime() - new Date(data.last_seen).getTime(); | ||||||
| @@ -6,6 +6,24 @@ export function isAgentOnline(data: UptimeMessage): boolean { | |||||||
|     return timeDiff < 10000; |     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 { | export function formatRelativeTime(timestamp: string): string { | ||||||
|     const now = new Date(); |     const now = new Date(); | ||||||
|     const past = new Date(timestamp); |     const past = new Date(timestamp); | ||||||
| @@ -48,3 +66,54 @@ export function calcPercentage( | |||||||
| export function formatPercentage(val: number | undefined) { | export function formatPercentage(val: number | undefined) { | ||||||
|     return `${(val ?? 0).toFixed(2)}%`; |     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