mirror of
https://github.com/moghtech/komodo.git
synced 2026-04-29 04:10:01 -05:00
248 lines
7.1 KiB
TypeScript
248 lines
7.1 KiB
TypeScript
import { hex_color_by_intention } from "@lib/color";
|
|
import { useRead } from "@lib/hooks";
|
|
import { Types } from "komodo_client";
|
|
import { useMemo } from "react";
|
|
import { useStatsGranularity } from "./hooks";
|
|
import { Loader2, OctagonAlert } from "lucide-react";
|
|
import { AxisOptions, Chart } from "react-charts";
|
|
import { convertTsMsToLocalUnixTsInMs } from "@lib/utils";
|
|
import { useTheme } from "@ui/theme";
|
|
import { fmt_utc_date } from "@lib/formatting";
|
|
|
|
type StatType =
|
|
| "Cpu"
|
|
| "Memory"
|
|
| "Disk"
|
|
| "Network Ingress"
|
|
| "Network Egress"
|
|
| "Load Average";
|
|
|
|
type StatDatapoint = { date: number; value: number };
|
|
|
|
export const StatChart = ({
|
|
server_id,
|
|
type,
|
|
className,
|
|
}: {
|
|
server_id: string;
|
|
type: StatType;
|
|
className?: string;
|
|
}) => {
|
|
const [granularity] = useStatsGranularity();
|
|
|
|
const { data, isPending } = useRead(
|
|
"GetHistoricalServerStats",
|
|
{
|
|
server: server_id,
|
|
granularity,
|
|
},
|
|
{
|
|
refetchInterval:
|
|
granularity === Types.Timelength.FiveSeconds
|
|
? 5_000
|
|
: granularity === Types.Timelength.FifteenSeconds
|
|
? 10_000
|
|
: 15_000,
|
|
}
|
|
);
|
|
|
|
const seriesData = useMemo(() => {
|
|
if (!data?.stats) return [] as { label: string; data: StatDatapoint[] }[];
|
|
const records = [...data.stats].reverse();
|
|
if (type === "Load Average") {
|
|
const one = records.map((s) => ({
|
|
date: convertTsMsToLocalUnixTsInMs(s.ts),
|
|
value: s.load_average?.one ?? 0,
|
|
}));
|
|
const five = records.map((s) => ({
|
|
date: convertTsMsToLocalUnixTsInMs(s.ts),
|
|
value: s.load_average?.five ?? 0,
|
|
}));
|
|
const fifteen = records.map((s) => ({
|
|
date: convertTsMsToLocalUnixTsInMs(s.ts),
|
|
value: s.load_average?.fifteen ?? 0,
|
|
}));
|
|
return [
|
|
{ label: "1m", data: one },
|
|
{ label: "5m", data: five },
|
|
{ label: "15m", data: fifteen },
|
|
];
|
|
}
|
|
const single = records.map((stat) => ({
|
|
date: convertTsMsToLocalUnixTsInMs(stat.ts),
|
|
value: getStat(stat, type),
|
|
}));
|
|
return [{ label: type, data: single }];
|
|
}, [data, type]);
|
|
|
|
return (
|
|
<div className={className}>
|
|
<h1 className="px-2 py-1">{type}</h1>
|
|
{isPending ? (
|
|
<div className="w-full max-w-full h-full flex items-center justify-center">
|
|
<Loader2 className="w-8 h-8 animate-spin" />
|
|
</div>
|
|
) : (
|
|
<InnerStatChart
|
|
type={type}
|
|
stats={seriesData.flatMap((s) => s.data)}
|
|
seriesData={seriesData}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const BYTES_PER_GB = 1073741824.0;
|
|
const BYTES_PER_MB = 1048576.0;
|
|
const BYTES_PER_KB = 1024.0;
|
|
|
|
export const InnerStatChart = ({
|
|
type,
|
|
stats,
|
|
seriesData,
|
|
}: {
|
|
type: StatType;
|
|
stats: StatDatapoint[] | undefined;
|
|
seriesData?: { label: string; data: StatDatapoint[] }[];
|
|
}) => {
|
|
const { currentTheme } = useTheme();
|
|
|
|
const min = stats?.[0]?.date ?? 0;
|
|
const max = stats?.[stats.length - 1]?.date ?? 0;
|
|
const diff = max - min;
|
|
|
|
const timeAxis = useMemo((): AxisOptions<StatDatapoint> => {
|
|
return {
|
|
getValue: (datum) => new Date(datum.date),
|
|
hardMax: new Date(max + diff * 0.02),
|
|
hardMin: new Date(min - diff * 0.02),
|
|
tickCount: 6,
|
|
formatters: {
|
|
// scale: (value?: Date) => fmt_date(value ?? new Date()),
|
|
tooltip: (value?: Date) => (
|
|
<div className="text-lg font-mono">
|
|
{fmt_utc_date(value ?? new Date())}
|
|
</div>
|
|
),
|
|
cursor: (_value?: Date) => false,
|
|
},
|
|
};
|
|
}, [min, max, diff]);
|
|
|
|
const maxStatValue = stats ? Math.max(...stats.map((s) => s.value)) : 0;
|
|
|
|
const { maxUnitValue, unitValue, unit } = useMemo(() => {
|
|
if (type === "Network Ingress" || type === "Network Egress") {
|
|
const maxUnitValue = 2 ** Math.ceil(Math.log2(maxStatValue));
|
|
if (maxStatValue <= BYTES_PER_MB) {
|
|
return {
|
|
unit: "KB",
|
|
unitValue: BYTES_PER_KB,
|
|
maxUnitValue,
|
|
};
|
|
} else if (maxStatValue <= BYTES_PER_GB) {
|
|
return {
|
|
unit: "MB",
|
|
unitValue: BYTES_PER_MB,
|
|
maxUnitValue,
|
|
};
|
|
} else {
|
|
return {
|
|
unit: "GB",
|
|
unitValue: BYTES_PER_GB,
|
|
maxUnitValue,
|
|
};
|
|
}
|
|
}
|
|
if (type === "Load Average") {
|
|
return {
|
|
maxUnitValue: maxStatValue === 0 ? 1 : maxStatValue * 1.2,
|
|
};
|
|
}
|
|
if (type === "Cpu") {
|
|
return {
|
|
maxUnitValue: Math.min(2 ** (Math.log2(maxStatValue) + 0.3), 100),
|
|
};
|
|
}
|
|
return { maxUnitValue: 100 }; // Default for memory, disk
|
|
}, [type, maxStatValue]);
|
|
|
|
const valueAxis = useMemo(
|
|
(): AxisOptions<StatDatapoint>[] => [
|
|
{
|
|
getValue: (datum) => datum.value,
|
|
elementType: type === "Load Average" ? "line" : "area",
|
|
stacked: type !== "Load Average",
|
|
min: 0,
|
|
max: maxUnitValue,
|
|
formatters: {
|
|
tooltip: (value?: number) => (
|
|
<div className="text-lg font-mono">
|
|
{(type === "Network Ingress" || type === "Network Egress") && unit
|
|
? `${((value ?? 0) / unitValue).toFixed(2)} ${unit}`
|
|
: type === "Load Average"
|
|
? `${(value ?? 0).toFixed(2)}`
|
|
: `${value?.toFixed(2)}%`}
|
|
</div>
|
|
),
|
|
},
|
|
},
|
|
],
|
|
[type, maxUnitValue, unit]
|
|
);
|
|
|
|
if ((seriesData?.[0]?.data.length ?? 0) < 2) {
|
|
return (
|
|
<div className="w-full h-full flex gap-4 justify-center items-center">
|
|
<OctagonAlert className="w-6 h-6" />
|
|
<h1>Not enough data yet, choose a smaller interval.</h1>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Chart
|
|
options={{
|
|
data: seriesData ?? [{ label: type, data: stats ?? [] }],
|
|
primaryAxis: timeAxis,
|
|
secondaryAxes: valueAxis,
|
|
defaultColors:
|
|
type === "Load Average"
|
|
? [
|
|
hex_color_by_intention("Good"),
|
|
hex_color_by_intention("Neutral"),
|
|
hex_color_by_intention("Unknown"),
|
|
]
|
|
: [getColor(type)],
|
|
dark: currentTheme === "dark",
|
|
padding: {
|
|
left: 10,
|
|
right: 10,
|
|
},
|
|
// tooltip: {
|
|
// showDatumInTooltip: () => false,
|
|
// },
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const getStat = (stat: Types.SystemStatsRecord, type: StatType) => {
|
|
if (type === "Cpu") return stat.cpu_perc || 0;
|
|
if (type === "Memory") return (100 * stat.mem_used_gb) / stat.mem_total_gb;
|
|
if (type === "Disk") return (100 * stat.disk_used_gb) / stat.disk_total_gb;
|
|
if (type === "Network Ingress") return stat.network_ingress_bytes || 0;
|
|
if (type === "Network Egress") return stat.network_egress_bytes || 0;
|
|
return 0;
|
|
};
|
|
|
|
const getColor = (type: StatType) => {
|
|
if (type === "Cpu") return hex_color_by_intention("Good");
|
|
if (type === "Memory") return hex_color_by_intention("Warning");
|
|
if (type === "Disk") return hex_color_by_intention("Neutral");
|
|
if (type === "Network Ingress") return hex_color_by_intention("Good");
|
|
if (type === "Network Egress") return hex_color_by_intention("Critical");
|
|
return hex_color_by_intention("Unknown");
|
|
};
|