mirror of
				https://github.com/csehviktor/status-monitor.git
				synced 2025-08-08 18:06:14 +02:00 
			
		
		
		
	finish ui
This commit is contained in:
		| @@ -10,7 +10,7 @@ export function LineChartCard({ | ||||
| }) { | ||||
|     return ( | ||||
|         <div className="bg-[#0d0d0d] border border-[#191919] rounded-lg p-6"> | ||||
|             {data.labels.length > 0 ? ( | ||||
|             {data.labels.length > 1 ? ( | ||||
|                 <> | ||||
|                     <h3 className="text-lg font-semibold mb-4">{title}</h3> | ||||
|                     <div className="h-48"> | ||||
|   | ||||
							
								
								
									
										34
									
								
								ui/src/components/TimePeriodSelector.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								ui/src/components/TimePeriodSelector.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import { TimePeriod } from "@/services/store"; | ||||
|  | ||||
| type TimePeriodSelectorProps = { | ||||
|     currentPeriod: TimePeriod | "realtime"; | ||||
|     onChange: (period: TimePeriod | "realtime") => void; | ||||
| }; | ||||
|  | ||||
| export function TimePeriodSelector({ | ||||
|     currentPeriod, | ||||
|     onChange, | ||||
| }: TimePeriodSelectorProps) { | ||||
|     const periods: { value: TimePeriod | "realtime"; label: string }[] = [ | ||||
|         { value: "realtime", label: "Real time" }, | ||||
|         { value: "hour", label: "Past hour" }, | ||||
|         { value: "day", label: "Past day" }, | ||||
|         { value: "week", label: "Past week" }, | ||||
|         { value: "month", label: "Past month" }, | ||||
|         { value: "all", label: "All" }, | ||||
|     ]; | ||||
|  | ||||
|     return ( | ||||
|         <div className="space-x-3"> | ||||
|             {periods.map((period) => ( | ||||
|                 <button | ||||
|                     key={period.value} | ||||
|                     onClick={() => onChange(period.value)} | ||||
|                     className={`px-4 py-2 text-md font-medium rounded-full bg-[#0d0d0d] border border-[#101010] ${currentPeriod === period.value && "text-purple-500 font-semibold"}`} | ||||
|                 > | ||||
|                     {period.label} | ||||
|                 </button> | ||||
|             ))} | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
| @@ -18,55 +18,57 @@ type ChartDataReturns = { | ||||
|     networkData: ChartData; | ||||
| }; | ||||
|  | ||||
| export function useChartData(data: StatusMessage | null): ChartDataReturns { | ||||
| export function useChartData( | ||||
|     data: StatusMessage | StatusMessage[] | null, | ||||
| ): ChartDataReturns { | ||||
|     const currentData = Array.isArray(data) ? data : getRealtimeData(); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (!data) return; | ||||
|         if (!data || Array.isArray(data)) return; | ||||
|  | ||||
|         addDataPoint(data); | ||||
|     }, [data]); | ||||
|  | ||||
|     const realtimeData = getRealtimeData(); | ||||
|  | ||||
|     return { | ||||
|         cpuData: { | ||||
|             labels: realtimeData.map((p) => formatTimestamp(p.timestamp)), | ||||
|             labels: currentData.map((p) => formatTimestamp(p.timestamp)), | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     label: "Total CPU (%)", | ||||
|                     data: realtimeData.map(({ metrics }) => metrics.cpu.usage), | ||||
|                     data: currentData.map(({ metrics }) => metrics.cpu.usage), | ||||
|                     color: "#3b82f6", | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "User (%)", | ||||
|                     data: realtimeData.map( | ||||
|                     data: currentData.map( | ||||
|                         ({ metrics }) => metrics.cpu.breakdown.user, | ||||
|                     ), | ||||
|                     color: "#10b981", | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "System (%)", | ||||
|                     data: realtimeData.map( | ||||
|                     data: currentData.map( | ||||
|                         ({ metrics }) => metrics.cpu.breakdown.system, | ||||
|                     ), | ||||
|                     color: "#f59e0b", | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "I/O Wait (%)", | ||||
|                     data: realtimeData.map( | ||||
|                     data: currentData.map( | ||||
|                         ({ metrics }) => metrics.cpu.breakdown.iowait, | ||||
|                     ), | ||||
|                     color: "#ef4444", | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "Steal (%)", | ||||
|                     data: realtimeData.map( | ||||
|                     data: currentData.map( | ||||
|                         ({ metrics }) => metrics.cpu.breakdown.steal, | ||||
|                     ), | ||||
|                     color: "#8b5cf6", | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "Idle (%)", | ||||
|                     data: realtimeData.map( | ||||
|                     data: currentData.map( | ||||
|                         ({ metrics }) => metrics.cpu.breakdown.idle, | ||||
|                     ), | ||||
|                     color: "#6b7280", | ||||
| @@ -74,11 +76,11 @@ export function useChartData(data: StatusMessage | null): ChartDataReturns { | ||||
|             ], | ||||
|         }, | ||||
|         memoryData: { | ||||
|             labels: realtimeData.map((p) => formatTimestamp(p.timestamp)), | ||||
|             labels: currentData.map((p) => formatTimestamp(p.timestamp)), | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     label: "Memory Usage (%)", | ||||
|                     data: realtimeData.map(({ metrics }) => | ||||
|                     data: currentData.map(({ metrics }) => | ||||
|                         calcPercentage( | ||||
|                             metrics.memory.used, | ||||
|                             metrics.memory.total, | ||||
| @@ -88,7 +90,7 @@ export function useChartData(data: StatusMessage | null): ChartDataReturns { | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "Swap Usage (%)", | ||||
|                     data: realtimeData.map(({ metrics }) => | ||||
|                     data: currentData.map(({ metrics }) => | ||||
|                         calcPercentage( | ||||
|                             metrics.memory.swap_used, | ||||
|                             metrics.memory.swap_total, | ||||
| @@ -99,16 +101,16 @@ export function useChartData(data: StatusMessage | null): ChartDataReturns { | ||||
|             ], | ||||
|         }, | ||||
|         networkData: { | ||||
|             labels: realtimeData.map((p) => formatTimestamp(p.timestamp)), | ||||
|             labels: currentData.map((p) => formatTimestamp(p.timestamp)), | ||||
|             datasets: [ | ||||
|                 { | ||||
|                     label: "Upload (bps)", | ||||
|                     data: realtimeData.map(({ metrics }) => metrics.network.up), | ||||
|                     data: currentData.map(({ metrics }) => metrics.network.up), | ||||
|                     color: "#ef4444", | ||||
|                 }, | ||||
|                 { | ||||
|                     label: "Download (bps)", | ||||
|                     data: realtimeData.map( | ||||
|                     data: currentData.map( | ||||
|                         ({ metrics }) => metrics.network.down, | ||||
|                     ), | ||||
|                     color: "#3b82f6", | ||||
|   | ||||
							
								
								
									
										25
									
								
								ui/src/hooks/useWebsocket.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								ui/src/hooks/useWebsocket.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import type { WebsocketStatus } from "@/components/ConnectionStatus"; | ||||
| import type { StatusMessage } from "@/services/types"; | ||||
| import { initializeConnection } from "@/services/websocket"; | ||||
| import { useEffect, useState } from "react"; | ||||
|  | ||||
| export function useWebsocket(url: string) { | ||||
|     const [status, setStatus] = useState<WebsocketStatus>(); | ||||
|     const [message, setMessage] = useState<StatusMessage | null>(); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         initializeConnection(url, { | ||||
|             onMessage: (data) => { | ||||
|                 const parsed = JSON.parse(data); | ||||
|  | ||||
|                 setStatus("connected"); | ||||
|                 setMessage(parsed); | ||||
|             }, | ||||
|             onOpen: () => setStatus("connecting"), | ||||
|             onClose: () => setStatus("disconnected"), | ||||
|             onError: () => setStatus("error"), | ||||
|         }); | ||||
|     }, [url]); | ||||
|  | ||||
|     return { status, message }; | ||||
| } | ||||
| @@ -1,51 +1,45 @@ | ||||
| import { | ||||
|     type WebsocketStatus, | ||||
|     ConnectionStatus, | ||||
| } from "@/components/ConnectionStatus"; | ||||
| import type { StatusMessage } from "@/services/types"; | ||||
| import { ConnectionStatus } from "@/components/ConnectionStatus"; | ||||
| 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 { | ||||
|     formatBits, | ||||
|     formatPercentage, | ||||
|     calcPercentage, | ||||
|     formatBytes, | ||||
| } from "@/services/utils"; | ||||
| import { initializeConnection } from "@/services/websocket"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { useParams } from "react-router-dom"; | ||||
| import { useWebsocket } from "@/hooks/useWebsocket"; | ||||
| import { getHistoricalData, TimePeriod } from "@/services/store"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { StatusMessage } from "@/services/types"; | ||||
| import { TimePeriodSelector } from "@/components/TimePeriodSelector"; | ||||
|  | ||||
| export function AgentPage() { | ||||
|     const { agent } = useParams(); | ||||
|     const [status, setStatus] = useState<WebsocketStatus>(); | ||||
|     const [message, setMessage] = useState<StatusMessage | null>( | ||||
|         getLastMessage(), | ||||
|     ); | ||||
|     const { status, message } = useWebsocket(`ws://localhost:3000/ws/${agent}`); | ||||
|  | ||||
|     const [period, setPeriod] = useState<TimePeriod | "realtime">("all"); | ||||
|     const [history, setHistory] = useState<StatusMessage[] | null>(null); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (!agent) return; | ||||
|         const fetchData = async () => { | ||||
|             const data = await getHistoricalData( | ||||
|                 `http://localhost:3000/history/${agent}/${period}`, | ||||
|             ); | ||||
|             setHistory(data); | ||||
|         }; | ||||
|  | ||||
|         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]); | ||||
|         fetchData(); | ||||
|     }, [agent, period]); | ||||
|  | ||||
|     const { metrics } = message ?? {}; | ||||
|     const { cpuData, memoryData, networkData } = useChartData(message); | ||||
|     const { cpuData, memoryData, networkData } = useChartData( | ||||
|         period === "realtime" ? message! : history, | ||||
|     ); | ||||
|  | ||||
|     const getMetricStatus = (percentage: number | undefined) => { | ||||
|         if (!percentage) return "nil"; | ||||
| @@ -86,6 +80,8 @@ export function AgentPage() { | ||||
|             /> | ||||
|             <main className="max-w-7xl mx-auto py-8"> | ||||
|                 <div className="mx-2"> | ||||
|                     <h2 className="text-xl font-semibold mb-4">Overview</h2> | ||||
|  | ||||
|                     <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8"> | ||||
|                         <MetricCard | ||||
|                             props={{ | ||||
| @@ -170,15 +166,28 @@ export function AgentPage() { | ||||
|                         </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} | ||||
|                         /> | ||||
|                     <div className="flex flex-col my-10"> | ||||
|                         <h2 className="text-xl font-semibold mb-4">Details</h2> | ||||
|                         <div className="mt-3"> | ||||
|                             <TimePeriodSelector | ||||
|                                 currentPeriod={period} | ||||
|                                 onChange={setPeriod} | ||||
|                             /> | ||||
|                         </div> | ||||
|  | ||||
|                         <SysinfoCard sysinfo={metrics?.system_info} /> | ||||
|                         <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> | ||||
|                 </div> | ||||
|             </main> | ||||
|   | ||||
| @@ -1,26 +1,28 @@ | ||||
| import type { StatusMessage } from "@/services/types"; | ||||
| import { atom } from "nanostores"; | ||||
|  | ||||
| const store = atom<StatusMessage | null>(null); | ||||
| export type TimePeriod = "all" | "hour" | "day" | "week" | "month"; | ||||
|  | ||||
| export function getLastMessage(): StatusMessage | null { | ||||
|     return store.get(); | ||||
| } | ||||
|  | ||||
| export function setLastMessage(message: StatusMessage) { | ||||
|     store.set(message); | ||||
| } | ||||
|  | ||||
| const realtimeData: StatusMessage[] = []; | ||||
| let data: StatusMessage[] = []; | ||||
| const maxRealtimePoints = 50; | ||||
|  | ||||
| export function addDataPoint(value: StatusMessage) { | ||||
|     realtimeData.push(value); | ||||
|     data.push(value); | ||||
|  | ||||
|     if (realtimeData.length > maxRealtimePoints) { | ||||
|         realtimeData.shift(); | ||||
|     if (data.length > maxRealtimePoints) { | ||||
|         data.shift(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export function getRealtimeData(): StatusMessage[] { | ||||
|     return realtimeData ?? []; | ||||
|     return data ?? []; | ||||
| } | ||||
|  | ||||
| export function setHistoricalData(value: StatusMessage[]) { | ||||
|     data = value; | ||||
| } | ||||
|  | ||||
| export async function getHistoricalData(url: string): Promise<StatusMessage[]> { | ||||
|     return await fetch(url) | ||||
|         .then((res) => res.json()) | ||||
|         .then((data) => data as StatusMessage[]); | ||||
| } | ||||
|   | ||||
| @@ -5,8 +5,8 @@ type Callbacks = { | ||||
|     onClose?: () => void; | ||||
| }; | ||||
|  | ||||
| export function initializeConnection(agent: string, callbacks: Callbacks) { | ||||
|     const ws = new WebSocket(`ws://localhost:3000/ws/${agent}`); | ||||
| export function initializeConnection(url: string, callbacks: Callbacks) { | ||||
|     const ws = new WebSocket(url); | ||||
|  | ||||
|     ws.onopen = () => callbacks.onOpen?.(); | ||||
|     ws.onmessage = (event) => callbacks.onMessage(event.data); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user