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 (
|
return (
|
||||||
<div className="bg-[#0d0d0d] border border-[#191919] rounded-lg p-6">
|
<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>
|
<h3 className="text-lg font-semibold mb-4">{title}</h3>
|
||||||
<div className="h-48">
|
<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;
|
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(() => {
|
useEffect(() => {
|
||||||
if (!data) return;
|
if (!data || Array.isArray(data)) return;
|
||||||
|
|
||||||
addDataPoint(data);
|
addDataPoint(data);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const realtimeData = getRealtimeData();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cpuData: {
|
cpuData: {
|
||||||
labels: realtimeData.map((p) => formatTimestamp(p.timestamp)),
|
labels: currentData.map((p) => formatTimestamp(p.timestamp)),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: "Total CPU (%)",
|
label: "Total CPU (%)",
|
||||||
data: realtimeData.map(({ metrics }) => metrics.cpu.usage),
|
data: currentData.map(({ metrics }) => metrics.cpu.usage),
|
||||||
color: "#3b82f6",
|
color: "#3b82f6",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "User (%)",
|
label: "User (%)",
|
||||||
data: realtimeData.map(
|
data: currentData.map(
|
||||||
({ metrics }) => metrics.cpu.breakdown.user,
|
({ metrics }) => metrics.cpu.breakdown.user,
|
||||||
),
|
),
|
||||||
color: "#10b981",
|
color: "#10b981",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "System (%)",
|
label: "System (%)",
|
||||||
data: realtimeData.map(
|
data: currentData.map(
|
||||||
({ metrics }) => metrics.cpu.breakdown.system,
|
({ metrics }) => metrics.cpu.breakdown.system,
|
||||||
),
|
),
|
||||||
color: "#f59e0b",
|
color: "#f59e0b",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "I/O Wait (%)",
|
label: "I/O Wait (%)",
|
||||||
data: realtimeData.map(
|
data: currentData.map(
|
||||||
({ metrics }) => metrics.cpu.breakdown.iowait,
|
({ metrics }) => metrics.cpu.breakdown.iowait,
|
||||||
),
|
),
|
||||||
color: "#ef4444",
|
color: "#ef4444",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Steal (%)",
|
label: "Steal (%)",
|
||||||
data: realtimeData.map(
|
data: currentData.map(
|
||||||
({ metrics }) => metrics.cpu.breakdown.steal,
|
({ metrics }) => metrics.cpu.breakdown.steal,
|
||||||
),
|
),
|
||||||
color: "#8b5cf6",
|
color: "#8b5cf6",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Idle (%)",
|
label: "Idle (%)",
|
||||||
data: realtimeData.map(
|
data: currentData.map(
|
||||||
({ metrics }) => metrics.cpu.breakdown.idle,
|
({ metrics }) => metrics.cpu.breakdown.idle,
|
||||||
),
|
),
|
||||||
color: "#6b7280",
|
color: "#6b7280",
|
||||||
@@ -74,11 +76,11 @@ export function useChartData(data: StatusMessage | null): ChartDataReturns {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
memoryData: {
|
memoryData: {
|
||||||
labels: realtimeData.map((p) => formatTimestamp(p.timestamp)),
|
labels: currentData.map((p) => formatTimestamp(p.timestamp)),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: "Memory Usage (%)",
|
label: "Memory Usage (%)",
|
||||||
data: realtimeData.map(({ metrics }) =>
|
data: currentData.map(({ metrics }) =>
|
||||||
calcPercentage(
|
calcPercentage(
|
||||||
metrics.memory.used,
|
metrics.memory.used,
|
||||||
metrics.memory.total,
|
metrics.memory.total,
|
||||||
@@ -88,7 +90,7 @@ export function useChartData(data: StatusMessage | null): ChartDataReturns {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Swap Usage (%)",
|
label: "Swap Usage (%)",
|
||||||
data: realtimeData.map(({ metrics }) =>
|
data: currentData.map(({ metrics }) =>
|
||||||
calcPercentage(
|
calcPercentage(
|
||||||
metrics.memory.swap_used,
|
metrics.memory.swap_used,
|
||||||
metrics.memory.swap_total,
|
metrics.memory.swap_total,
|
||||||
@@ -99,16 +101,16 @@ export function useChartData(data: StatusMessage | null): ChartDataReturns {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
networkData: {
|
networkData: {
|
||||||
labels: realtimeData.map((p) => formatTimestamp(p.timestamp)),
|
labels: currentData.map((p) => formatTimestamp(p.timestamp)),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: "Upload (bps)",
|
label: "Upload (bps)",
|
||||||
data: realtimeData.map(({ metrics }) => metrics.network.up),
|
data: currentData.map(({ metrics }) => metrics.network.up),
|
||||||
color: "#ef4444",
|
color: "#ef4444",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Download (bps)",
|
label: "Download (bps)",
|
||||||
data: realtimeData.map(
|
data: currentData.map(
|
||||||
({ metrics }) => metrics.network.down,
|
({ metrics }) => metrics.network.down,
|
||||||
),
|
),
|
||||||
color: "#3b82f6",
|
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 {
|
import { ConnectionStatus } from "@/components/ConnectionStatus";
|
||||||
type WebsocketStatus,
|
|
||||||
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 { DonutChart } from "@/components/DonutChart";
|
||||||
import { LineChartCard } from "@/components/LineChartCard";
|
import { LineChartCard } from "@/components/LineChartCard";
|
||||||
import { MetricCard } from "@/components/MetricCard";
|
import { MetricCard } from "@/components/MetricCard";
|
||||||
import { SysinfoCard } from "@/components/SysinfoCard";
|
import { SysinfoCard } from "@/components/SysinfoCard";
|
||||||
import { useChartData } from "@/hooks/useChartData";
|
import { useChartData } from "@/hooks/useChartData";
|
||||||
import { getLastMessage, setLastMessage } from "@/services/store";
|
|
||||||
import {
|
import {
|
||||||
formatBits,
|
formatBits,
|
||||||
formatPercentage,
|
formatPercentage,
|
||||||
calcPercentage,
|
calcPercentage,
|
||||||
formatBytes,
|
formatBytes,
|
||||||
} from "@/services/utils";
|
} from "@/services/utils";
|
||||||
import { initializeConnection } from "@/services/websocket";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useParams } from "react-router-dom";
|
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() {
|
export function AgentPage() {
|
||||||
const { agent } = useParams();
|
const { agent } = useParams();
|
||||||
const [status, setStatus] = useState<WebsocketStatus>();
|
const { status, message } = useWebsocket(`ws://localhost:3000/ws/${agent}`);
|
||||||
const [message, setMessage] = useState<StatusMessage | null>(
|
|
||||||
getLastMessage(),
|
const [period, setPeriod] = useState<TimePeriod | "realtime">("all");
|
||||||
);
|
const [history, setHistory] = useState<StatusMessage[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!agent) return;
|
const fetchData = async () => {
|
||||||
|
const data = await getHistoricalData(
|
||||||
|
`http://localhost:3000/history/${agent}/${period}`,
|
||||||
|
);
|
||||||
|
setHistory(data);
|
||||||
|
};
|
||||||
|
|
||||||
initializeConnection(agent, {
|
fetchData();
|
||||||
onMessage: (data) => {
|
}, [agent, period]);
|
||||||
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 { metrics } = message ?? {};
|
||||||
const { cpuData, memoryData, networkData } = useChartData(message);
|
const { cpuData, memoryData, networkData } = useChartData(
|
||||||
|
period === "realtime" ? message! : history,
|
||||||
|
);
|
||||||
|
|
||||||
const getMetricStatus = (percentage: number | undefined) => {
|
const getMetricStatus = (percentage: number | undefined) => {
|
||||||
if (!percentage) return "nil";
|
if (!percentage) return "nil";
|
||||||
@@ -86,6 +80,8 @@ 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="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">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
props={{
|
props={{
|
||||||
@@ -170,15 +166,28 @@ export function AgentPage() {
|
|||||||
</div>
|
</div>
|
||||||
</MetricCard>
|
</MetricCard>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 my-8">
|
<div className="flex flex-col my-10">
|
||||||
<LineChartCard title="CPU Usage" data={cpuData} />
|
<h2 className="text-xl font-semibold mb-4">Details</h2>
|
||||||
<LineChartCard title="Memory Usage" data={memoryData} />
|
<div className="mt-3">
|
||||||
<LineChartCard
|
<TimePeriodSelector
|
||||||
title="Network Activity"
|
currentPeriod={period}
|
||||||
data={networkData}
|
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>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
import type { StatusMessage } from "@/services/types";
|
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 {
|
let data: StatusMessage[] = [];
|
||||||
return store.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setLastMessage(message: StatusMessage) {
|
|
||||||
store.set(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const realtimeData: StatusMessage[] = [];
|
|
||||||
const maxRealtimePoints = 50;
|
const maxRealtimePoints = 50;
|
||||||
|
|
||||||
export function addDataPoint(value: StatusMessage) {
|
export function addDataPoint(value: StatusMessage) {
|
||||||
realtimeData.push(value);
|
data.push(value);
|
||||||
|
|
||||||
if (realtimeData.length > maxRealtimePoints) {
|
if (data.length > maxRealtimePoints) {
|
||||||
realtimeData.shift();
|
data.shift();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRealtimeData(): StatusMessage[] {
|
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;
|
onClose?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function initializeConnection(agent: string, callbacks: Callbacks) {
|
export function initializeConnection(url: string, callbacks: Callbacks) {
|
||||||
const ws = new WebSocket(`ws://localhost:3000/ws/${agent}`);
|
const ws = new WebSocket(url);
|
||||||
|
|
||||||
ws.onopen = () => callbacks.onOpen?.();
|
ws.onopen = () => callbacks.onOpen?.();
|
||||||
ws.onmessage = (event) => callbacks.onMessage(event.data);
|
ws.onmessage = (event) => callbacks.onMessage(event.data);
|
||||||
|
|||||||
Reference in New Issue
Block a user