From 176b12f18c3b5fa0d8379ff45d539c201dee35d0 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 21 Apr 2024 19:22:22 -0700 Subject: [PATCH] server stats chart --- .../components/resources/build/dashboard.tsx | 9 +- .../src/components/resources/server/hooks.ts | 10 ++ .../src/components/resources/server/index.tsx | 8 + .../resources/server/stat-chart.tsx | 138 +++++++++++++++++ .../src/components/resources/server/stats.tsx | 145 +++++++++++++++--- frontend/src/lib/utils.ts | 5 + 6 files changed, 296 insertions(+), 19 deletions(-) create mode 100644 frontend/src/components/resources/server/hooks.ts create mode 100644 frontend/src/components/resources/server/stat-chart.tsx diff --git a/frontend/src/components/resources/build/dashboard.tsx b/frontend/src/components/resources/build/dashboard.tsx index 3e7c4be89..64d029d48 100644 --- a/frontend/src/components/resources/build/dashboard.tsx +++ b/frontend/src/components/resources/build/dashboard.tsx @@ -16,6 +16,7 @@ import { } from "@ui/card"; import { Hammer } from "lucide-react"; import { Link } from "react-router-dom"; +import { convertTsMsToLocalUnixTsInSecs } from "@lib/utils"; export const BuildChart = () => { const container_ref = useRef(null); @@ -34,6 +35,8 @@ export const BuildChart = () => { if (line_ref.current) line_ref.current.remove(); const init = () => { if (!container_ref.current) return; + + // INIT LINE line_ref.current = createChart(container_ref.current, { width: container_ref.current.clientWidth, height: container_ref.current.clientHeight, @@ -50,13 +53,15 @@ export const BuildChart = () => { handleScroll: false, }); line_ref.current.timeScale().fitContent(); + + // INIT SERIES series_ref.current = line_ref.current.addHistogramSeries({ priceLineVisible: false, }); const max = build_stats.days.reduce((m, c) => Math.max(m, c.time), 0); series_ref.current.setData( build_stats.days.map((d) => ({ - time: (d.ts / 1000) as Time, + time: convertTsMsToLocalUnixTsInSecs(d.ts) as Time, value: d.count, color: d.time > max * 0.7 @@ -67,6 +72,8 @@ export const BuildChart = () => { })) ?? [] ); }; + + // Run the effect init(); window.addEventListener("resize", handleResize); return () => { diff --git a/frontend/src/components/resources/server/hooks.ts b/frontend/src/components/resources/server/hooks.ts new file mode 100644 index 000000000..7916b5863 --- /dev/null +++ b/frontend/src/components/resources/server/hooks.ts @@ -0,0 +1,10 @@ +import { Types } from "@monitor/client"; +import { useAtom } from "jotai"; +import { atomWithStorage } from "jotai/utils"; + +const statsIntervalAtom = atomWithStorage( + "stats-interval-v0", + Types.Timelength.FiveMinutes +); + +export const useStatsInterval = () => useAtom(statsIntervalAtom); diff --git a/frontend/src/components/resources/server/index.tsx b/frontend/src/components/resources/server/index.tsx index 2a32f1ee0..51d56945e 100644 --- a/frontend/src/components/resources/server/index.tsx +++ b/frontend/src/components/resources/server/index.tsx @@ -10,6 +10,7 @@ import { Database, Scissors, XOctagon, + AreaChart, } from "lucide-react"; import { Section } from "@components/layouts"; import { RenameServer } from "./actions"; @@ -26,6 +27,7 @@ import { Link } from "react-router-dom"; import { DeleteResource, NewResource } from "../common"; import { ActionWithDialog, ConfirmButton } from "@components/util"; import { Card, CardHeader } from "@ui/card"; +import { Button } from "@ui/button"; export const useServer = (id?: string) => useRead("ListServers", {}).data?.find((d) => d.id === id); @@ -66,6 +68,12 @@ export const ServerComponents: RequiredResourceComponents = { ); }, + Stats: ({id}) => + + }, Info: { diff --git a/frontend/src/components/resources/server/stat-chart.tsx b/frontend/src/components/resources/server/stat-chart.tsx new file mode 100644 index 000000000..d5f75e8c4 --- /dev/null +++ b/frontend/src/components/resources/server/stat-chart.tsx @@ -0,0 +1,138 @@ +import { hex_color_by_intention } from "@lib/color"; +import { useRead } from "@lib/hooks"; +import { convertTsMsToLocalUnixTsInSecs } from "@lib/utils"; +import { Types } from "@monitor/client"; +import { + ColorType, + IChartApi, + ISeriesApi, + Time, + createChart, +} from "lightweight-charts"; +import { useEffect, useRef } from "react"; +import { useStatsInterval } from "./hooks"; +import { Loader2 } from "lucide-react"; + +type StatType = "cpu" | "mem" | "disk"; + +export const StatChart = ({ + server_id, + type, + className, +}: { + server_id: string; + type: StatType; + className?: string; +}) => { + const [interval] = useStatsInterval(); + + const { data, isPending } = useRead("GetHistoricalServerStats", { + server: server_id, + interval, + }); + + const stats = data?.stats + .map((stat) => { + return { + time: convertTsMsToLocalUnixTsInSecs(stat.ts) as Time, + value: getStat(stat, type), + }; + }) + .reverse(); + + return ( +
+ {isPending ? ( +
+ +
+ ) : ( + + )} +
+ ); +}; + +export const InnerStatChart = ({ + type, + stats, +}: { + type: StatType; + stats: + | { + time: Time; + value: number; + }[] + | undefined; +}) => { + const container_ref = useRef(null); + const line_ref = useRef(); + const series_ref = useRef>(); + const lineColor = getColor(type); + + const handleResize = () => + line_ref.current?.applyOptions({ + width: container_ref.current?.clientWidth, + }); + + useEffect(() => { + if (!stats) return; + if (line_ref.current) line_ref.current.remove(); + + const init = () => { + if (!container_ref.current) return; + + // INIT LINE + line_ref.current = createChart(container_ref.current, { + width: container_ref.current.clientWidth, + height: container_ref.current.clientHeight, + layout: { + background: { type: ColorType.Solid, color: "transparent" }, + textColor: "grey", + fontSize: 12, + }, + grid: { + horzLines: { color: "#3f454d" }, + vertLines: { color: "#3f454d" }, + }, + timeScale: { timeVisible: true }, + handleScale: false, + handleScroll: false, + }); + line_ref.current.timeScale().fitContent(); + + // INIT SERIES + series_ref.current = line_ref.current.addAreaSeries({ + priceLineVisible: false, + title: `${type} %`, + lineColor, + topColor: `${lineColor}B3`, + bottomColor: `${lineColor}0D`, + }); + series_ref.current.setData(stats); + }; + + // Run the effect + init(); + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [stats]); + + return
; +}; + +const getStat = (stat: Types.SystemStatsRecord, type: StatType) => { + if (type === "cpu") return stat.cpu_perc || 0; + if (type === "mem") return (100 * stat.mem_used_gb) / stat.mem_total_gb; + if (type === "disk") return (100 * stat.disk_used_gb) / stat.disk_total_gb; + return 0; +}; + +const getColor = (type: StatType) => { + if (type === "cpu") return hex_color_by_intention("Good"); + if (type === "mem") return hex_color_by_intention("Warning"); + if (type === "disk") return hex_color_by_intention("Neutral"); + return hex_color_by_intention("Unknown"); +}; diff --git a/frontend/src/components/resources/server/stats.tsx b/frontend/src/components/resources/server/stats.tsx index 0b548a634..bb84b4c20 100644 --- a/frontend/src/components/resources/server/stats.tsx +++ b/frontend/src/components/resources/server/stats.tsx @@ -10,12 +10,25 @@ import { Progress } from "@ui/progress"; import { Cpu, Database, MemoryStick } from "lucide-react"; import { useRead } from "@lib/hooks"; import { Types } from "@monitor/client"; -import { useServer } from "."; +import { ServerComponents, useServer } from "."; import { DataTable } from "@ui/data-table"; -import { useState } from "react"; +import { Fragment, useState } from "react"; import { Input } from "@ui/input"; +import { ResourceDescription } from "../common"; +import { AddTags, ResourceTags } from "@components/tags"; +import { StatChart } from "./stat-chart"; +import { useStatsInterval } from "./hooks"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@ui/select"; export const ServerStats = ({ id }: { id: string }) => { + const [interval, setInterval] = useStatsInterval(); + const server = useServer(id); const stats = useRead( "GetSystemStats", @@ -24,17 +37,58 @@ export const ServerStats = ({ id }: { id: string }) => { ).data; const info = useRead("GetSystemInformation", { server: id }).data; + const disk_used = stats?.disks.reduce( + (acc, curr) => (acc += curr.used_gb), + 0 + ); + const disk_total = stats?.disks.reduce( + (acc, curr) => (acc += curr.total_gb), + 0 + ); + return ( Stats
} + titleRight={ +
+ {Object.entries(ServerComponents.Status).map(([key, Status]) => ( + + ))} +
+ } + subtitle={ +
+
+ + {Object.entries(ServerComponents.Info).map(([key, Info], i) => ( + + | + + ))} +
+ +
+ } + actions={ +
+
tags:
+ + +
+ } > -
{/* */}
-
{ header: "Kernel", accessorKey: "kernel", }, + { + header: "Core Count", + accessorFn: ({ core_count }) => + `${core_count} Core${(core_count || 0) > 1 ? "s" : ""}`, + }, + { + header: "Total Memory", + accessorFn: ({ mem_total }) => `${mem_total?.toFixed(2)} GB`, + }, + { + header: "Total Disk Size", + accessorFn: ({ disk_total }) => `${disk_total?.toFixed(2)} GB`, + }, ]} />
-
+
@@ -60,19 +127,61 @@ export const ServerStats = ({ id }: { id: string }) => {
- {/*
-
- {["one", "five", "fifteen"].map((minutes) => ( - - ))} +
+
Interval:
+ +
+ } + > +
+ + +
-
*/} +
-
+
+
+
Used:
+ {disk_used?.toFixed(2)} GB +
+
+
Total:
+ {disk_total?.toFixed(2)} GB +
+ + } + > = level_to_number(greater_than); }; + +const tzOffset = new Date().getTimezoneOffset() * 60; + +export const convertTsMsToLocalUnixTsInSecs = (ts: number) => + ts / 1000 - tzOffset;