Add Dashboard stats
This commit is contained in:
12
Cargo.lock
generated
12
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
174
frontend/src/components/server_stats.tsx
Normal file
174
frontend/src/components/server_stats.tsx
Normal file
@@ -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 (
|
||||
<div className="text-white">
|
||||
<div>
|
||||
<p>{getServerName()}</p>
|
||||
</div>
|
||||
<div className="max-h-80 flex flex-col gap-2 p-2 lg:flex-row">
|
||||
<Card className="flex-1 p-2">
|
||||
<h2>CPU Stats</h2>
|
||||
<ChartContainer
|
||||
title="CPU Stats"
|
||||
config={{}}
|
||||
className="max-h-80 p-2"
|
||||
>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={Object.values((metrics?.cpu as unknown as any) ?? {})}
|
||||
title="CPU Usage (48h)"
|
||||
>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
|
||||
<YAxis />
|
||||
|
||||
{Object.keys(metrics.cpu[Object.keys(metrics.cpu)[0]]).map(
|
||||
(cpu) => {
|
||||
return (
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey={cpu}
|
||||
stroke={getCputColor(cpu)}
|
||||
activeDot
|
||||
dot={false}
|
||||
label={cpu}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</Card>
|
||||
<Card className="flex-1 p-2">
|
||||
<h2>Network Stats</h2>
|
||||
<ChartContainer
|
||||
title="Network Stats"
|
||||
config={{}}
|
||||
className="max-h-80 p-2"
|
||||
>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={Object.values((metrics?.network as unknown as any) ?? {})}
|
||||
title="Network Usage (48h)"
|
||||
>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
|
||||
<YAxis />
|
||||
|
||||
{Object.keys(
|
||||
metrics.network[Object.keys(metrics.network)[0]],
|
||||
).map((interf) => {
|
||||
return (
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey={interf}
|
||||
stroke={getInterfaceColor(interf)}
|
||||
activeDot
|
||||
dot={false}
|
||||
label={interf}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</Card>
|
||||
<Card className="flex-1 p-2">
|
||||
<h2>Disk Stats</h2>
|
||||
<ChartContainer
|
||||
title="Disk Stats"
|
||||
config={{}}
|
||||
className="max-h-80 p-2"
|
||||
>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={Object.values((metrics?.disk as unknown as any) ?? {})}
|
||||
title="Disk Usage (48h)"
|
||||
>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
|
||||
<YAxis />
|
||||
|
||||
{Object.keys(metrics.disk[Object.keys(metrics.disk)[0]]).map(
|
||||
(disk) => {
|
||||
return (
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey={disk}
|
||||
stroke={getDiskColor(disk)}
|
||||
activeDot
|
||||
dot={false}
|
||||
label={disk}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
67
frontend/src/lib/colors.ts
Normal file
67
frontend/src/lib/colors.ts
Normal file
@@ -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",
|
||||
];
|
||||
6
frontend/src/models/metrics.ts
Normal file
6
frontend/src/models/metrics.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Metrics {
|
||||
server_id: number;
|
||||
cpu: any;
|
||||
disk: any;
|
||||
network: any;
|
||||
}
|
||||
@@ -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 <div className="text-center">Dashboard coming soon!</div>;
|
||||
const { data: servers = [] } = useQuery<MinimalServers>({
|
||||
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<Metrics[]>({
|
||||
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 (
|
||||
<div className="p-4 flex flex-col gap-2">
|
||||
{metrics.map((metric, index) => (
|
||||
<div key={index}>
|
||||
<Card className="p-2">
|
||||
{metric && <ServerStats server={servers[index]} metrics={metric} />}
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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<i32>) -> 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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user