diff --git a/Cargo.lock b/Cargo.lock index c2eb6a7..feb4b1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2528,9 +2528,21 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "socket2 0.6.1", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 6aa4ce8..eada969 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,4 @@ serde = { version = "1.0.228", features = ["serde_derive", "derive"] } serde_derive = "1.0.228" serde_json = "1.0.147" sqlx = { version = "0.8", features = [ "runtime-tokio", "tls-native-tls", "sqlite" ] } -tokio = "1.48.0" +tokio = { version = "1.48.0", features = ["full"] } diff --git a/frontend/src/components/server_stats.tsx b/frontend/src/components/server_stats.tsx new file mode 100644 index 0000000..7c4ba94 --- /dev/null +++ b/frontend/src/components/server_stats.tsx @@ -0,0 +1,174 @@ +import type { Metrics } from "@/models/metrics"; +import type { MinimalServer } from "@/models/minimal_servers"; +import { + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, +} from "./ui/chart"; +import { Line, LineChart, YAxis } from "recharts"; +import { colors64 } from "@/lib/colors"; +import { Card } from "./ui/card"; + +export function ServerStats({ + server, + metrics, +}: { + server: MinimalServer; + metrics: Metrics; +}) { + function getServerName() { + if (server.nickname) { + return server.nickname; + } + if (server.hostname) { + return server.hostname; + } + if (server.name) { + return server.name; + } + + return server.id.toString(); + } + + function getCputColor(cpu: string) { + const cpuIndex = parseInt(cpu.replace("CPU", "")); + if (cpuIndex >= 0 && cpuIndex < colors64.length) { + return colors64[cpuIndex]; + } + return "#8884d8"; + } + + function getInterfaceColor(interfaceName: string) { + if (interfaceName.includes("RX")) { + return "#008000"; + } + if (interfaceName.includes("TX")) { + return "#FF0000"; + } + return "#8884d8"; + } + + function getDiskColor(diskName: string) { + if (diskName.includes("Read")) { + return "#0000FF"; + } + if (diskName.includes("Write")) { + return "#FF00FF"; + } + return "#8884d8"; + } + + return ( +
+
+

{getServerName()}

+
+
+ +

CPU Stats

+ + + } /> + } /> + + + + {Object.keys(metrics.cpu[Object.keys(metrics.cpu)[0]]).map( + (cpu) => { + return ( + + ); + }, + )} + + +
+ +

Network Stats

+ + + } /> + } /> + + + + {Object.keys( + metrics.network[Object.keys(metrics.network)[0]], + ).map((interf) => { + return ( + + ); + })} + + +
+ +

Disk Stats

+ + + } /> + } /> + + + + {Object.keys(metrics.disk[Object.keys(metrics.disk)[0]]).map( + (disk) => { + return ( + + ); + }, + )} + + +
+
+
+ ); +} diff --git a/frontend/src/lib/colors.ts b/frontend/src/lib/colors.ts new file mode 100644 index 0000000..daedaf2 --- /dev/null +++ b/frontend/src/lib/colors.ts @@ -0,0 +1,67 @@ +export const colors64: string[] = [ + "#0000F0", + "#FFFFFF", + "#FF0000", + "#00FF00", + "#0000FF", + "#FFFF00", + "#00FFFF", + "#FF00FF", + "#800000", + "#008000", + "#000080", + "#808000", + "#008080", + "#800080", + "#C0C0C0", + "#808080", + "#FFA500", + "#A52A2A", + "#8B4513", + "#DEB887", + "#D2691E", + "#CD853F", + "#F4A460", + "#DAA520", + "#B8860B", + "#FFD700", + "#F0E68C", + "#EEE8AA", + "#98FB98", + "#90EE90", + "#00FA9A", + "#2E8B57", + "#3CB371", + "#66CDAA", + "#20B2AA", + "#5F9EA0", + "#4682B4", + "#1E90FF", + "#6495ED", + "#87CEEB", + "#87CEFA", + "#00BFFF", + "#4169E1", + "#00008B", + "#191970", + "#6A5ACD", + "#7B68EE", + "#8A2BE2", + "#4B0082", + "#9400D3", + "#9932CC", + "#BA55D3", + "#DDA0DD", + "#FFC0CB", + "#FF69B4", + "#FF1493", + "#DB7093", + "#DC143C", + "#B22222", + "#FA8072", + "#E9967A", + "#F08080", + "#CD5C5C", + "#FF6347", + "#FF7F50", +]; diff --git a/frontend/src/models/metrics.ts b/frontend/src/models/metrics.ts new file mode 100644 index 0000000..ae51980 --- /dev/null +++ b/frontend/src/models/metrics.ts @@ -0,0 +1,6 @@ +export interface Metrics { + server_id: number; + cpu: any; + disk: any; + network: any; +} diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index e1ecc12..82085c3 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -1,9 +1,51 @@ +import { ServerStats } from "@/components/server_stats"; +import { Card } from "@/components/ui/card"; +import type { Metrics } from "@/models/metrics"; +import type { MinimalServers } from "@/models/minimal_servers"; +import { useQueries, useQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; export const Route = createFileRoute("/")({ component: App, }); function App() { - return
Dashboard coming soon!
; + const { data: servers = [] } = useQuery({ + queryKey: ["servers"], + queryFn: async () => { + const res = await fetch("/api/servers"); + if (!res.ok) throw new Error("Failed to fetch servers"); + return (await res.json()) as MinimalServers; + }, + refetchInterval: 40000, + }); + + const { data: metrics = [] } = useQuery({ + queryKey: ["serversMetrics", servers.map((s) => s.id).join("-")], + enabled: servers.length > 0, + queryFn: async () => { + const results = await Promise.all( + servers.map(async (server) => { + const res = await fetch(`/api/servers/${server.id}/metrics`); + if (!res.ok) throw new Error(`Failed metrics for ${server.id}`); + return (await res.json()) as Metrics; + }), + ); + return results; + }, + refetchInterval: 30000, + }); + + return ( +
+ {metrics.map((metric, index) => ( +
+ + {metric && } + +
+ ))} +
+ ); } diff --git a/src/main.rs b/src/main.rs index a8f4a36..ebee468 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,6 +57,7 @@ async fn main() -> std::io::Result<()> { .service(servers::list_servers) .service(servers::get_server) .service(servers::update_server_hostname) + .service(servers::get_server_metrics) .service( Files::new("/", "./frontend/dist") .index_file("index.html") diff --git a/src/servers.rs b/src/servers.rs index 4eb1a6b..4be7ce4 100644 --- a/src/servers.rs +++ b/src/servers.rs @@ -1,5 +1,6 @@ use actix_web::{Responder, get, patch, web}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; +use serde_json::Value; use crate::helper; @@ -55,3 +56,41 @@ pub async fn update_server_hostname( serde_json::to_string(&response).unwrap() } + +#[derive(Deserialize, Serialize)] +struct Metrics { + server_id: i32, + cpu: Value, + disk: Value, + network: Value, +} + +#[get("/api/servers/{id}/metrics")] +pub async fn get_server_metrics(id: web::Path) -> impl Responder { + let config = helper::get_authed_api_config().await.unwrap(); + let cpu_future = scp_core::apis::default_api::api_v1_servers_server_id_metrics_cpu_get( + &config, + id.clone(), + Some(48), + ); + + let disk_future = scp_core::apis::default_api::api_v1_servers_server_id_metrics_disk_get( + &config, + id.clone(), + Some(48), + ); + let network_future = scp_core::apis::default_api::api_v1_servers_server_id_metrics_network_get( + &config, + id.clone(), + Some(48), + ); + + let result = Metrics { + server_id: id.into_inner(), + cpu: cpu_future.await.unwrap(), + disk: disk_future.await.unwrap(), + network: network_future.await.unwrap(), + }; + + serde_json::to_string(&result).unwrap() +}