import { useEffect, useState, useRef } from "react"; import { TrendingUp } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; import { MetricsChartEditForm } from "./MetricsChartEditForm"; import type { MetricsChartConfig, MetricsChartMetric, WidgetComponentProps, WidgetTypeDefinition, } from "@/types/homepage-types"; import { GRID_SIZE } from "@/types/homepage-types"; import { getMetricsHistory, type MetricsHistoryRow } from "./host-metrics-api"; import { getSSHHosts } from "@/api/ssh-host-management-api"; import { WidgetTitle } from "@/features/homepage/widgets/WidgetTitle"; import { runVisibleInterval } from "@/features/homepage/use-visible-interval"; function getAccentColor(): string { return ( getComputedStyle(document.documentElement) .getPropertyValue("--accent-brand") .trim() || "#f59145" ); } function getMetricValue( row: MetricsHistoryRow, metric: MetricsChartMetric, ): number | null { switch (metric) { case "cpu": return row.cpu_percent; case "memory": return row.mem_percent; case "disk": return row.disk_percent; case "net_rx": return row.net_rx_bytes != null ? row.net_rx_bytes / 1_000_000 : null; case "net_tx": return row.net_tx_bytes != null ? row.net_tx_bytes / 1_000_000 : null; } } function getMetricLabel( metric: MetricsChartMetric, t: (k: string) => string, ): string { switch (metric) { case "cpu": return t("homepage.metricCpu"); case "memory": return t("homepage.metricMemory"); case "disk": return t("homepage.metricDisk"); case "net_rx": return "RX MB/s"; case "net_tx": return "TX MB/s"; } } function getMetricUnit(metric: MetricsChartMetric): string { if (metric === "net_rx" || metric === "net_tx") return " MB/s"; return "%"; } interface SparklineProps { values: number[]; width: number; height: number; color: string; } function Sparkline({ values, width, height, color }: SparklineProps) { if (values.length < 2) return null; const max = Math.max(...values, 1); const min = 0; const range = max - min; const pts = values.map((v, i) => { const x = (i / (values.length - 1)) * width; const y = height - ((v - min) / range) * height; return `${x.toFixed(1)},${y.toFixed(1)}`; }); const polyPts = [`0,${height}`, ...pts, `${width},${height}`].join(" "); return ( ); } function MetricsChartWidget({ widget, config, }: WidgetComponentProps) { const { t } = useTranslation(); const { hostId, metric, range, showCurrentValue } = config; const [hostName, setHostName] = useState(null); const [values, setValues] = useState([]); const [loading, setLoading] = useState(true); const containerRef = useRef(null); const [dims, setDims] = useState({ w: 0, h: 0 }); useEffect(() => { if (!hostId) return; getSSHHosts() .then((hosts) => { const h = hosts.find((x) => x.id === hostId); if (h) setHostName(h.name); }) .catch(() => {}); }, [hostId]); useEffect(() => { if (!containerRef.current) return; const obs = new ResizeObserver((entries) => { const e = entries[0]?.contentRect; if (e) setDims({ w: Math.floor(e.width), h: Math.floor(e.height) }); }); obs.observe(containerRef.current); return () => obs.disconnect(); }, []); const fetchData = () => { if (!hostId) return; getMetricsHistory(hostId, { range }) .then((res) => { const vals = res.rows .map((r) => getMetricValue(r, metric)) .filter((v): v is number => v != null); setValues(vals); setLoading(false); }) .catch(() => setLoading(false)); }; useEffect(() => { fetchData(); return runVisibleInterval(() => { void fetchData(); }, 60_000); }, [hostId, metric, range]); const accent = getAccentColor(); const current = values.length > 0 ? values[values.length - 1] : null; const unit = getMetricUnit(metric); const label = getMetricLabel(metric, t); if (!hostId) { return (
{t("homepage.noHostSelected")}
); } return (
} />
{hostName ?? `Host #${hostId}`} {label} ยท {range}
{showCurrentValue && current != null && ( {metric === "net_rx" || metric === "net_tx" ? current.toFixed(2) : Math.round(current)} {unit} )}
{loading ? (
{t("homepage.loading")}
) : values.length < 2 ? (
{t("homepage.noData")}
) : ( dims.w > 0 && dims.h > 0 && ( ) )}
); } /** Registered by the plugin while it runs. */ export const metricsChartWidget: WidgetTypeDefinition = { id: "metrics_chart", name: "Metrics Chart", description: "Historical CPU, memory, disk, or network chart for a host", category: "monitoring", icon: , defaultConfig: { hostId: 0, metric: "cpu", range: "1h", showCurrentValue: true, }, defaultSize: { w: GRID_SIZE * 12, h: GRID_SIZE * 6 }, minSize: { w: GRID_SIZE * 6, h: GRID_SIZE * 4 }, component: MetricsChartWidget, editFormComponent: MetricsChartEditForm, }; export { MetricsChartWidget };