Add Dashboard stats

This commit is contained in:
2025-12-31 00:16:47 +01:00
parent c44a900b20
commit 0fba6c1445
8 changed files with 344 additions and 3 deletions

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

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

View File

@@ -0,0 +1,6 @@
export interface Metrics {
server_id: number;
cpu: any;
disk: any;
network: any;
}

View File

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