finish ui design

This commit is contained in:
csehviktor
2025-07-10 05:19:22 +02:00
parent aa6b171614
commit e7dda680ad
16 changed files with 653 additions and 86 deletions

View File

@@ -6,7 +6,7 @@
"dependencies": {
"lucide-react": "^0.525.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@eslint/js": "^9.29.0",
@@ -15,18 +15,20 @@
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.5.2",
"chart.js": "^4.5.0",
"eslint": "^9.29.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.2.0",
"nanostores": "^1.0.1",
"react-chartjs-2": "^5.3.0",
"react-router-dom": "^7.6.3",
"tailwindcss": "^4.1.11",
"typescript": "~5.8.3",
"typescript-eslint": "^8.34.1",
"vite": "^7.0.0",
},
},
"vite": "^7.0.0"
}
}
},
"packages": {
"@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=="],
"@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=="],
"@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=="],
"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=="],
"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-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-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
@@ -595,6 +603,6 @@
"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=="]
}
}

View File

@@ -21,11 +21,13 @@
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.5.2",
"chart.js": "^4.5.0",
"eslint": "^9.29.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.2.0",
"nanostores": "^1.0.1",
"react-chartjs-2": "^5.3.0",
"react-router-dom": "^7.6.3",
"tailwindcss": "^4.1.11",
"typescript": "~5.8.3",

View File

@@ -1,6 +1,6 @@
import { UptimeMessage } from "@/services/types";
import type { UptimeMessage } from "@/services/types";
import { Progressbar } from "@/components/Progressbar";
import { formatRelativeTime, isAgentOnline } from "@/services/utils";
import { Progressbar } from "./Progressbar";
type AgentOverviewCardProps = {
count: number;

View File

@@ -50,7 +50,7 @@ export function ConnectionStatus({ status, timestamp }: ConnectionStatusProps) {
{config.text}
</span>
{timestamp && (
<span className="text-xs text-gray-500">
<span className="text-xs text-gray-500 hidden md:block">
{new Date(timestamp).toLocaleTimeString()}
</span>
)}

View 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>
);
}

View File

@@ -12,7 +12,7 @@ export function Header({ props }: { props: HeaderProps }) {
const navigate = useNavigate();
return (
<header className="bg-[#0d0d0d] border-b border-[#191919]">
<header className="bg-[#0d0d0d] border-b border-[#191919] px-2">
<div className="max-w-7xl mx-auto py-4 flex justify-between items-center">
<div className="flex items-center space-x-3">
{props.hasBackButton && (

View 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>
);
}

View 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>
);
}

View File

@@ -31,7 +31,7 @@ export function MetricCard({
{props.value}
</div>
<div className="text-sm text-gray-500">{props.subtitle}</div>
<div>{children}</div>
<div className="mt-5">{children}</div>
</div>
</div>
);

View File

@@ -1,5 +1,3 @@
"use client";
import { useEffect, useRef, useState } from "react";
export function Progressbar({

View 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>
);
}

View 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",
},
],
},
};
}

View File

@@ -1,15 +1,19 @@
import {
type WebsocketStatus,
ConnectionStatus,
WebsocketStatus,
} from "@/components/ConnectionStatus";
import type { StatusMessage } from "@/services/types";
import { Header } from "@/components/Header";
import { DonutChart } from "@/components/DonutChart";
import { LineChartCard } from "@/components/LineChartCard";
import { MetricCard } from "@/components/MetricCard";
import { SysinfoCard } from "@/components/SysinfoCard";
import { useChartData } from "@/hooks/useChartData";
import { getLastMessage, setLastMessage } from "@/services/store";
import { StatusMessage } from "@/services/types";
import {
formatBytes,
calcPercentage,
formatPercentage,
breakdownMetrics,
} from "@/services/utils";
import { initializeConnection } from "@/services/websocket";
import { useEffect, useState } from "react";
@@ -39,9 +43,10 @@ export function AgentPage() {
});
}, [agent]);
const { metrics } = message || {};
const { metrics } = message ?? {};
const { cpuData, memoryData, networkData } = useChartData(message);
const getMetricsStatus = (percentage: number | undefined) => {
const getMetricStatus = (percentage: number | undefined) => {
if (!percentage) return "nil";
if (percentage < 50) return "good";
@@ -49,17 +54,20 @@ export function AgentPage() {
return "critical";
};
const cpuUsage = metrics?.cpu.usage ?? 0;
const memoryUsage = calcPercentage(
metrics?.memory.used,
metrics?.memory.total,
);
const diskUsage = calcPercentage(
(metrics?.disk.total ?? 0) - (metrics?.disk.free ?? 0),
metrics?.disk.total,
);
const networkUsage =
(metrics?.network.up ?? 0) + (metrics?.network.down ?? 0);
const {
cpuThreads,
cpuUsage,
memoryUsage,
memoryUsed,
memoryTotal,
diskUsage,
diskFree,
diskTotal,
networkUp,
networkDown,
} = breakdownMetrics(message?.metrics);
const networkUsage = networkUp + networkDown;
return (
<div>
@@ -76,40 +84,66 @@ export function AgentPage() {
),
}}
/>
<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">
<MetricCard
props={{
title: "CPU USAGE",
value: formatPercentage(cpuUsage),
status: getMetricsStatus(cpuUsage),
subtitle: `${metrics?.cpu.threads ?? 0} threads`,
status: getMetricStatus(cpuUsage),
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
props={{
title: "MEMORY USAGE",
value: formatPercentage(memoryUsage),
status: getMetricsStatus(memoryUsage),
subtitle: `${formatBytes(metrics?.memory.used)} / ${formatBytes(metrics?.memory.total)}`,
status: getMetricStatus(memoryUsage),
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
props={{
title: "NETWORK ACTIVITY",
value: formatBytes(networkUsage),
status: getMetricsStatus(networkUsage),
subtitle: `${formatBytes(metrics?.network.up)}/s ↓ ${formatBytes(metrics?.network.down)}/s`,
status: "nil",
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>
</div>
@@ -117,16 +151,17 @@ export function AgentPage() {
props={{
title: "DISK USAGE",
value: formatPercentage(diskUsage),
status: getMetricsStatus(diskUsage),
subtitle: `${formatBytes(metrics?.disk.free)} free of ${formatBytes(metrics?.disk.total)}`,
status: getMetricStatus(diskUsage),
subtitle: `${formatBytes(diskFree)} free of ${formatBytes(diskTotal)}`,
}}
>
<div className="w-full bg-[#141414] rounded-full h-4">
<div
className={`h-4 rounded-full transition-all duration-500 ${
getMetricsStatus(diskUsage) == "good"
getMetricStatus(diskUsage) == "good"
? "bg-green-500"
: getMetricsStatus(diskUsage) == "warning"
: getMetricStatus(diskUsage) ==
"warning"
? "bg-yellow-500"
: "bg-red-500"
}`}
@@ -134,6 +169,18 @@ 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}
/>
<SysinfoCard sysinfo={metrics?.system_info} />
</div>
</div>
</main>
</div>
);

View File

@@ -1,8 +1,6 @@
"use client";
import type { UptimeMessage } from "@/services/types";
import { AgentCard, AgentOverviewCard } from "@/components/AgentCard";
import { Header } from "@/components/Header";
import { UptimeMessage } from "@/services/types";
import { isAgentOnline } from "@/services/utils";
import { Box } from "lucide-react";
import { useEffect, useState } from "react";

View 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();
}
*/

View File

@@ -1,4 +1,4 @@
import { UptimeMessage } from "@/services/types";
import type { Metrics, UptimeMessage } from "@/services/types";
export function isAgentOnline(data: UptimeMessage): boolean {
const timeDiff = new Date().getTime() - new Date(data.last_seen).getTime();
@@ -6,6 +6,24 @@ export function isAgentOnline(data: UptimeMessage): boolean {
return timeDiff < 10000;
}
export function formatTimestamp(timestamp: string) {
return new Date(timestamp).toLocaleTimeString();
}
export const formatUptime = (seconds: number): string => {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) {
return `${days}d ${hours}h`;
} else if (hours > 0) {
return `${hours}h ${minutes}m`;
} else {
return `${minutes}m`;
}
};
export function formatRelativeTime(timestamp: string): string {
const now = new Date();
const past = new Date(timestamp);
@@ -48,3 +66,54 @@ export function calcPercentage(
export function formatPercentage(val: number | undefined) {
return `${(val ?? 0).toFixed(2)}%`;
}
export type BrokedownMetrics = {
cpuThreads: number;
cpuUsage: number;
cpuUser: number;
cpuSystem: number;
cpuIdle: number;
cpuSteal: number;
cpuIowait: number;
memoryUsage: number;
memoryUsed: number;
memoryTotal: number;
swapUsage: number;
diskUsage: number;
diskFree: number;
diskTotal: number;
networkUp: number;
networkDown: number;
};
export function breakdownMetrics(
metrics: Metrics | undefined,
): BrokedownMetrics {
return {
cpuThreads: metrics?.cpu.threads ?? 0,
cpuUsage: metrics?.cpu.usage ?? 0,
cpuUser: metrics?.cpu.breakdown.user ?? 0,
cpuSystem: metrics?.cpu.breakdown.system ?? 0,
cpuIdle: metrics?.cpu.breakdown.idle ?? 0,
cpuSteal: metrics?.cpu.breakdown.steal ?? 0,
cpuIowait: metrics?.cpu.breakdown.iowait ?? 0,
memoryUsage: calcPercentage(
metrics?.memory.used,
metrics?.memory.total,
),
memoryUsed: metrics?.memory.used ?? 0,
memoryTotal: metrics?.memory.total ?? 0,
swapUsage: calcPercentage(
metrics?.memory.swap_used,
metrics?.memory.swap_total,
),
diskUsage: calcPercentage(
(metrics?.disk.total ?? 0) - (metrics?.disk.free ?? 0),
metrics?.disk.total,
),
diskFree: metrics?.disk.free ?? 0,
diskTotal: metrics?.disk.total ?? 0,
networkUp: (metrics?.network.up ?? 0) / 1024,
networkDown: (metrics?.network.down ?? 0) / 1024,
};
}