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,64 +84,103 @@ export function AgentPage() {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main className="max-w-7xl mx-auto py-8">
|
<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">
|
<div className="mx-2">
|
||||||
<MetricCard
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||||
props={{
|
<MetricCard
|
||||||
title: "CPU USAGE",
|
props={{
|
||||||
value: formatPercentage(cpuUsage),
|
title: "CPU USAGE",
|
||||||
status: getMetricsStatus(cpuUsage),
|
value: formatPercentage(cpuUsage),
|
||||||
subtitle: `${metrics?.cpu.threads ?? 0} threads`,
|
status: getMetricStatus(cpuUsage),
|
||||||
}}
|
subtitle: `${cpuThreads} threads`,
|
||||||
>
|
}}
|
||||||
<h1>cpu chart</h1>
|
>
|
||||||
</MetricCard>
|
<DonutChart
|
||||||
|
data={{
|
||||||
|
labels: ["Usage", "Total"],
|
||||||
|
values: [cpuUsage, 100 - cpuUsage],
|
||||||
|
colors: ["#3b82f6", "#262626"],
|
||||||
|
}}
|
||||||
|
centerText={formatPercentage(cpuUsage)}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</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
|
||||||
</MetricCard>
|
data={{
|
||||||
|
labels: ["Used", "Free"],
|
||||||
|
values: [memoryUsage, 100 - memoryUsage],
|
||||||
|
colors: ["#3b82f6", "#262626"],
|
||||||
|
}}
|
||||||
|
centerText={formatPercentage(memoryUsage)}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</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
|
||||||
</MetricCard>
|
data={{
|
||||||
</div>
|
labels: ["Upload", "Download"],
|
||||||
|
values: [
|
||||||
<MetricCard
|
message ? networkUp : 1,
|
||||||
props={{
|
message ? networkDown : 1,
|
||||||
title: "DISK USAGE",
|
],
|
||||||
value: formatPercentage(diskUsage),
|
colors: ["#ef4444", "#3b82f6"],
|
||||||
status: getMetricsStatus(diskUsage),
|
}}
|
||||||
subtitle: `${formatBytes(metrics?.disk.free)} free of ${formatBytes(metrics?.disk.total)}`,
|
size="sm"
|
||||||
}}
|
/>
|
||||||
>
|
</MetricCard>
|
||||||
<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>
|
</div>
|
||||||
</MetricCard>
|
|
||||||
|
<MetricCard
|
||||||
|
props={{
|
||||||
|
title: "DISK USAGE",
|
||||||
|
value: formatPercentage(diskUsage),
|
||||||
|
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 ${
|
||||||
|
getMetricStatus(diskUsage) == "good"
|
||||||
|
? "bg-green-500"
|
||||||
|
: getMetricStatus(diskUsage) ==
|
||||||
|
"warning"
|
||||||
|
? "bg-yellow-500"
|
||||||
|
: "bg-red-500"
|
||||||
|
}`}
|
||||||
|
style={{ width: `${diskUsage}%` }}
|
||||||
|
/>
|
||||||
|
</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>
|
</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