mirror of
				https://github.com/csehviktor/status-monitor.git
				synced 2025-08-08 18:06:14 +02:00 
			
		
		
		
	improve ui
This commit is contained in:
		| @@ -1,11 +1,13 @@ | ||||
| import { BrowserRouter, Route, Routes } from "react-router-dom"; | ||||
| import { HomePage } from "@/pages/home"; | ||||
| import { AgentPage } from "@/pages/agent"; | ||||
|  | ||||
| export default function App() { | ||||
|     return ( | ||||
|         <BrowserRouter> | ||||
|             <Routes> | ||||
|                 <Route path="/" element={<HomePage />} /> | ||||
|                 <Route path="/agents/:agent" element={<AgentPage />} /> | ||||
|             </Routes> | ||||
|         </BrowserRouter> | ||||
|     ); | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { UptimeMessage } from "@/services/types"; | ||||
| import { formatRelativeTime, isAgentOnline } from "@/services/utils"; | ||||
| import { Progressbar } from "./Progressbar"; | ||||
|  | ||||
| export type AgentOverviewCardProps = { | ||||
| type AgentOverviewCardProps = { | ||||
|     count: number; | ||||
|     title: string; | ||||
|     icon: { | ||||
| @@ -42,13 +42,13 @@ export function AgentOverviewCard({ | ||||
|     ); | ||||
| } | ||||
|  | ||||
| export type AgentCardProps = { | ||||
| type AgentCardProps = { | ||||
|     data: UptimeMessage; | ||||
|     onClick?: () => void; | ||||
|     onClick: () => void; | ||||
| }; | ||||
|  | ||||
| export function AgentCard({ props }: { props: AgentCardProps }) { | ||||
|     const status = isAgentOnline(props.data) | ||||
| export function AgentCard({ data, onClick }: AgentCardProps) { | ||||
|     const status = isAgentOnline(data) | ||||
|         ? { | ||||
|               name: "online", | ||||
|               color: "bg-green-500", | ||||
| @@ -66,7 +66,7 @@ export function AgentCard({ props }: { props: AgentCardProps }) { | ||||
|  | ||||
|     return ( | ||||
|         <div | ||||
|             onClick={() => props.onClick?.()} | ||||
|             onClick={() => onClick()} | ||||
|             className="bg-[#111111] border border-[#262626] rounded-lg p-6 hover:bg-[#151515] transition-all cursor-pointer group" | ||||
|         > | ||||
|             <div className="flex items-center justify-between space-x-10"> | ||||
| @@ -77,7 +77,7 @@ export function AgentCard({ props }: { props: AgentCardProps }) { | ||||
|                         {status.name.toUpperCase()} | ||||
|                     </div> | ||||
|                     <h3 className="text-sm md:text-md lg:text-lg font-semibold text-white transition-colors"> | ||||
|                         {props.data.agent} | ||||
|                         {data.agent} | ||||
|                     </h3> | ||||
|                 </div> | ||||
|  | ||||
| @@ -85,13 +85,11 @@ export function AgentCard({ props }: { props: AgentCardProps }) { | ||||
|                     <div className="flex flex-col max-w-lg gap-y-2"> | ||||
|                         <Progressbar | ||||
|                             color={status.color} | ||||
|                             percentage={props.data.uptime} | ||||
|                             percentage={data.uptime} | ||||
|                         /> | ||||
|                         <div className="flex justify-between items-center text-xs"> | ||||
|                             <span> | ||||
|                                 {formatRelativeTime(props.data.last_seen)} | ||||
|                             </span> | ||||
|                             <span>{props.data.uptime.toFixed(2)}%</span> | ||||
|                             <span>{formatRelativeTime(data.last_seen)}</span> | ||||
|                             <span>{data.uptime.toFixed(2)}%</span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|   | ||||
							
								
								
									
										59
									
								
								ui/src/components/ConnectionStatus.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								ui/src/components/ConnectionStatus.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| export type WebsocketStatus = | ||||
|     | "connected" | ||||
|     | "connecting" | ||||
|     | "disconnected" | ||||
|     | "error"; | ||||
|  | ||||
| type ConnectionStatusProps = { | ||||
|     status: WebsocketStatus; | ||||
|     timestamp: string | undefined; | ||||
| }; | ||||
|  | ||||
| export function ConnectionStatus({ status, timestamp }: ConnectionStatusProps) { | ||||
|     const getConnectionStatus = (status: WebsocketStatus) => { | ||||
|         switch (status) { | ||||
|             case "connected": | ||||
|                 return { | ||||
|                     color: "bg-green-500", | ||||
|                     text: "connected", | ||||
|                     animation: "animate-pulse-slow", | ||||
|                 }; | ||||
|             case "connecting": | ||||
|                 return { | ||||
|                     color: "bg-yellow-500", | ||||
|                     text: "connecting", | ||||
|                     animation: "animate-pulse", | ||||
|                 }; | ||||
|             case "disconnected": | ||||
|                 return { | ||||
|                     color: "bg-gray-500", | ||||
|                     text: "disconnected", | ||||
|                     animation: "", | ||||
|                 }; | ||||
|             case "error": | ||||
|                 return { | ||||
|                     color: "bg-red-500", | ||||
|                     text: "error", | ||||
|                     animation: "animate-pulse", | ||||
|                 }; | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const config = getConnectionStatus(status); | ||||
|  | ||||
|     return ( | ||||
|         <div className="flex items-center space-x-3 bg-[#141414] p-2 rounded-lg"> | ||||
|             <div | ||||
|                 className={`w-2 h-2 rounded-full mt-[3px] ${config.color} ${config.animation}`} | ||||
|             /> | ||||
|             <span className="text-sm text-gray-300 font-semibold"> | ||||
|                 {config.text} | ||||
|             </span> | ||||
|             {timestamp && ( | ||||
|                 <span className="text-xs text-gray-500"> | ||||
|                     {new Date(timestamp).toLocaleTimeString()} | ||||
|                 </span> | ||||
|             )} | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { Zap } from "lucide-react"; | ||||
| import { ChevronLeft, Zap } from "lucide-react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
|  | ||||
| export type HeaderProps = { | ||||
| type HeaderProps = { | ||||
|     title: string; | ||||
|     subtitle: string; | ||||
|     hasBackButton: boolean; | ||||
| @@ -8,10 +9,21 @@ export type HeaderProps = { | ||||
| }; | ||||
|  | ||||
| export function Header({ props }: { props: HeaderProps }) { | ||||
|     const navigate = useNavigate(); | ||||
|  | ||||
|     return ( | ||||
|         <header className="bg-[#0d0d0d] border-b border-[#191919]"> | ||||
|             <div className="max-w-7xl mx-auto py-4 flex justify-between items-center"> | ||||
|                 <div className="flex items-center space-x-3"> | ||||
|                     {props.hasBackButton && ( | ||||
|                         <button | ||||
|                             onClick={() => navigate(-1)} | ||||
|                             className="w-8 h-8 bg-[#262626] hover:bg-[#404040] rounded-lg flex items-center justify-center transition-colors" | ||||
|                         > | ||||
|                             <ChevronLeft /> | ||||
|                         </button> | ||||
|                     )} | ||||
|  | ||||
|                     <div className="w-11 h-11 text-yellow-500 flex items-center justify-center"> | ||||
|                         <Zap className="w-9 h-9" /> | ||||
|                     </div> | ||||
|   | ||||
							
								
								
									
										51
									
								
								ui/src/components/MetricCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								ui/src/components/MetricCard.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| export type MetricStatus = "nil" | "good" | "warning" | "critical"; | ||||
|  | ||||
| type MetricCardProps = { | ||||
|     title: string; | ||||
|     value: string; | ||||
|     status: MetricStatus; | ||||
|     subtitle?: string; | ||||
| }; | ||||
|  | ||||
| export function MetricCard({ | ||||
|     props, | ||||
|     children, | ||||
| }: { | ||||
|     props: MetricCardProps; | ||||
|     children: React.ReactNode; | ||||
| }) { | ||||
|     const indicateColor = getStatusColor(props.status); | ||||
|  | ||||
|     return ( | ||||
|         <div className="bg-[#0d0d0d] rounded-lg p-6 shadow-lg hover:shadow-xl border border-[#191919]"> | ||||
|             <div className="flex items-center justify-between mb-4"> | ||||
|                 <h3 className="text-gray-300 text-sm font-medium uppercase tracking-wider"> | ||||
|                     {props.title} | ||||
|                 </h3> | ||||
|                 <div | ||||
|                     className={`w-3 h-3 rounded-full animate-pulse-slow ${indicateColor}`} | ||||
|                 /> | ||||
|             </div> | ||||
|             <div className="space-y-2"> | ||||
|                 <div className="text-2xl font-bold text-white"> | ||||
|                     {props.value} | ||||
|                 </div> | ||||
|                 <div className="text-sm text-gray-500">{props.subtitle}</div> | ||||
|                 <div>{children}</div> | ||||
|             </div> | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| const getStatusColor = (status: MetricStatus) => { | ||||
|     switch (status) { | ||||
|         case "nil": | ||||
|             return "bg-gray-500"; | ||||
|         case "good": | ||||
|             return "bg-green-500"; | ||||
|         case "warning": | ||||
|             return "bg-yellow-500"; | ||||
|         case "critical": | ||||
|             return "bg-red-500"; | ||||
|     } | ||||
| }; | ||||
							
								
								
									
										140
									
								
								ui/src/pages/agent.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								ui/src/pages/agent.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | ||||
| import { | ||||
|     ConnectionStatus, | ||||
|     WebsocketStatus, | ||||
| } from "@/components/ConnectionStatus"; | ||||
| import { Header } from "@/components/Header"; | ||||
| import { MetricCard } from "@/components/MetricCard"; | ||||
| import { getLastMessage, setLastMessage } from "@/services/store"; | ||||
| import { StatusMessage } from "@/services/types"; | ||||
| import { | ||||
|     formatBytes, | ||||
|     calcPercentage, | ||||
|     formatPercentage, | ||||
| } from "@/services/utils"; | ||||
| import { initializeConnection } from "@/services/websocket"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { useParams } from "react-router-dom"; | ||||
|  | ||||
| export function AgentPage() { | ||||
|     const { agent } = useParams(); | ||||
|     const [status, setStatus] = useState<WebsocketStatus>(); | ||||
|     const [message, setMessage] = useState<StatusMessage | null>( | ||||
|         getLastMessage(), | ||||
|     ); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (!agent) return; | ||||
|  | ||||
|         initializeConnection(agent, { | ||||
|             onMessage: (data) => { | ||||
|                 const parsed = JSON.parse(data); | ||||
|  | ||||
|                 setStatus("connected"); | ||||
|                 setMessage(parsed); | ||||
|                 setLastMessage(parsed); | ||||
|             }, | ||||
|             onOpen: () => setStatus("connecting"), | ||||
|             onClose: () => setStatus("disconnected"), | ||||
|             onError: () => setStatus("error"), | ||||
|         }); | ||||
|     }, [agent]); | ||||
|  | ||||
|     const { metrics } = message || {}; | ||||
|  | ||||
|     const getMetricsStatus = (percentage: number | undefined) => { | ||||
|         if (!percentage) return "nil"; | ||||
|  | ||||
|         if (percentage < 50) return "good"; | ||||
|         if (percentage < 80) return "warning"; | ||||
|         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); | ||||
|  | ||||
|     return ( | ||||
|         <div> | ||||
|             <Header | ||||
|                 props={{ | ||||
|                     title: "Status Monitor", | ||||
|                     subtitle: agent!, | ||||
|                     hasBackButton: true, | ||||
|                     rightComponent: ( | ||||
|                         <ConnectionStatus | ||||
|                             status={status ?? "disconnected"} | ||||
|                             timestamp={message?.timestamp} | ||||
|                         /> | ||||
|                     ), | ||||
|                 }} | ||||
|             /> | ||||
|  | ||||
|             <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> | ||||
|  | ||||
|                     <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: "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}%` }} | ||||
|                         /> | ||||
|                     </div> | ||||
|                 </MetricCard> | ||||
|             </main> | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
| @@ -6,8 +6,10 @@ import { UptimeMessage } from "@/services/types"; | ||||
| import { isAgentOnline } from "@/services/utils"; | ||||
| import { Box } from "lucide-react"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
|  | ||||
| export function HomePage() { | ||||
|     const navigate = useNavigate(); | ||||
|     const [agents, setAgents] = useState<UptimeMessage[]>([]); | ||||
|  | ||||
|     useEffect(() => { | ||||
| @@ -82,7 +84,10 @@ export function HomePage() { | ||||
|                             {agents.map((agent, index) => ( | ||||
|                                 <AgentCard | ||||
|                                     key={index} | ||||
|                                     props={{ data: agent }} | ||||
|                                     data={agent} | ||||
|                                     onClick={() => | ||||
|                                         navigate(`/agents/${agent.agent}`) | ||||
|                                     } | ||||
|                                 /> | ||||
|                             ))} | ||||
|                         </div> | ||||
|   | ||||
							
								
								
									
										12
									
								
								ui/src/services/store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								ui/src/services/store.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import type { StatusMessage } from "@/services/types"; | ||||
| import { atom } from "nanostores"; | ||||
|  | ||||
| const store = atom<StatusMessage | null>(null); | ||||
|  | ||||
| export function getLastMessage(): StatusMessage | null { | ||||
|     return store.get(); | ||||
| } | ||||
|  | ||||
| export function setLastMessage(message: StatusMessage) { | ||||
|     store.set(message); | ||||
| } | ||||
| @@ -6,7 +6,7 @@ export function isAgentOnline(data: UptimeMessage): boolean { | ||||
|     return timeDiff < 10000; | ||||
| } | ||||
|  | ||||
| export const formatRelativeTime = (timestamp: string): string => { | ||||
| export function formatRelativeTime(timestamp: string): string { | ||||
|     const now = new Date(); | ||||
|     const past = new Date(timestamp); | ||||
|     const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1000); | ||||
| @@ -25,4 +25,26 @@ export const formatRelativeTime = (timestamp: string): string => { | ||||
|         const days = Math.floor(diffInSeconds / 86400); | ||||
|         return `${days}d ago`; | ||||
|     } | ||||
| }; | ||||
| } | ||||
|  | ||||
| export function formatBytes(bytes: number | undefined): string { | ||||
|     if (!bytes || bytes === 0) return "0 B"; | ||||
|  | ||||
|     const k = 1024; | ||||
|     const sizes = ["B", "KB", "MB", "GB", "TB"]; | ||||
|     const i = Math.floor(Math.log(bytes) / Math.log(k)); | ||||
|  | ||||
|     return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; | ||||
| } | ||||
|  | ||||
| export function calcPercentage( | ||||
|     value: number | undefined, | ||||
|     total: number | undefined, | ||||
| ): number { | ||||
|     if (value === undefined || total === undefined) return 0; | ||||
|     return (value / total) * 100; | ||||
| } | ||||
|  | ||||
| export function formatPercentage(val: number | undefined) { | ||||
|     return `${(val ?? 0).toFixed(2)}%`; | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,17 @@ | ||||
| type Callbacks = { | ||||
|     onMessage: (message: string) => void; | ||||
|     onError?: (error: Event) => void; | ||||
|     onOpen?: () => void; | ||||
|     onClose?: () => void; | ||||
| }; | ||||
|  | ||||
| export function initializeConnection(agent: string, callbacks: Callbacks) { | ||||
|     const ws = new WebSocket(`ws://localhost:3000/ws/${agent}`); | ||||
|  | ||||
|     ws.onopen = () => callbacks.onOpen?.(); | ||||
|     ws.onmessage = (event) => callbacks.onMessage(event.data); | ||||
|     ws.onerror = (error) => callbacks.onError?.(error); | ||||
|     ws.onclose = () => callbacks.onClose?.(); | ||||
|  | ||||
|     return () => ws.close(); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user