import { useMemo } from "react"; import { ArrowDownRight, ArrowUpRight, Minus } from "lucide-react"; import { getServicePalette } from "../lib/otel-utils"; export interface GaugeMeterProps { name: string; service?: string; value: number; min?: number; max?: number; avg?: number; unit?: string; size?: "sm" | "md" | "lg"; thresholds?: { warn: number; danger: number; }; subtitle?: string; history?: number[]; showTrend?: boolean; isSum?: boolean; aggLabel?: string; soloPod?: string; podCount?: number; podSummaries?: Array<{ pod: string; value: number }>; } const formatNumber = (val: number, maxDecimals = 2) => { if (Math.abs(val) >= 1_000_000) { return `${(val / 1_000_000).toFixed(1)}M`; } if (Math.abs(val) >= 1_000) { return `${(val / 1_000).toFixed(1)}k`; } if (Number.isInteger(val)) { return val.toString(); } return val.toFixed(maxDecimals); }; // SVG arc calculations const polarToCartesian = (centerX: number, centerY: number, radius: number, angleInDegrees: number) => { const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0; return { x: centerX + radius * Math.cos(angleInRadians), y: centerY + radius * Math.sin(angleInRadians), }; }; const describeArc = (x: number, y: number, radius: number, startAngle: number, endAngle: number) => { const start = polarToCartesian(x, y, radius, endAngle); const end = polarToCartesian(x, y, radius, startAngle); const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1"; return ["M", start.x, start.y, "A", radius, radius, 0, largeArcFlag, 0, end.x, end.y].join(" "); }; export const GaugeMeter = ({ name, service, value, min: customMin, max: customMax, avg, unit = "", size = "md", thresholds, subtitle, history = [], showTrend = true, isSum = false, aggLabel, soloPod, podCount, podSummaries, }: GaugeMeterProps) => { const servicePalette = service ? getServicePalette(service) : null; // Determine min/max range const { min, max } = useMemo(() => { let computedMin = customMin !== undefined ? customMin : 0; let computedMax = customMax !== undefined ? customMax : 100; if (history.length > 0 && customMin === undefined && customMax === undefined) { const histMin = Math.min(...history); const histMax = Math.max(...history); if (histMax <= 1 && histMin >= 0) { // Percentage ratio 0..1 computedMin = 0; computedMax = 1; } else if (histMax <= 100 && histMin >= 0) { // 0..100% computedMin = 0; computedMax = 100; } else { computedMin = Math.floor(Math.min(0, histMin)); computedMax = Math.ceil(histMax * 1.15) || 100; } } if (computedMax <= computedMin) { computedMax = computedMin + 1; } return { min: computedMin, max: computedMax }; }, [customMin, customMax, history]); // Normalized percent 0..100 const percent = useMemo(() => { const clamped = Math.max(min, Math.min(value, max)); return ((clamped - min) / (max - min)) * 100; }, [value, min, max]); // Color determination based on thresholds const warnThreshold = thresholds?.warn ?? 70; const dangerThreshold = thresholds?.danger ?? 90; const statusColor = useMemo(() => { if (percent >= dangerThreshold) { return { text: "text-ds-critical", stroke: "var(--ds-critical)", gradientStart: "var(--ds-critical)", gradientEnd: "var(--ds-critical)", bg: "bg-ds-critical-bg", border: "border-ds-critical/30", glow: "color-mix(in srgb, var(--ds-critical) 35%, transparent)", label: "Critical", }; } if (percent >= warnThreshold) { return { text: "text-ds-warn", stroke: "var(--ds-warn)", gradientStart: "var(--ds-warn)", gradientEnd: "var(--ds-warn)", bg: "bg-ds-warn-bg", border: "border-ds-warn/30", glow: "color-mix(in srgb, var(--ds-warn) 35%, transparent)", label: "Warning", }; } return { text: "text-ds-accent", stroke: "var(--ds-accent)", gradientStart: "var(--ds-accent)", gradientEnd: "var(--ds-accent)", bg: "bg-ds-accent-bg", border: "border-ds-accent/30", glow: "color-mix(in srgb, var(--ds-accent) 35%, transparent)", label: "Nominal", }; }, [percent, warnThreshold, dangerThreshold]); // Trend calculation const trend = useMemo(() => { if (!showTrend || history.length < 2) return null; const prev = history[history.length - 2]; const diff = value - prev; if (Math.abs(diff) < 0.0001) return { direction: "flat", text: "steady", color: "text-ds-text-3" }; if (diff > 0) return { direction: "up", text: `+${formatNumber(diff)}`, color: "text-ds-critical" }; return { direction: "down", text: `${formatNumber(diff)}`, color: "text-ds-ok" }; }, [value, history, showTrend]); // Dimensions const dim = size === "sm" ? 120 : size === "lg" ? 220 : 160; const strokeWidth = size === "sm" ? 8 : size === "lg" ? 14 : 10; const radius = (dim - strokeWidth * 2) / 2; const centerX = dim / 2; const centerY = dim / 2 + (size === "sm" ? 4 : 8); // 240 degree arc from -120 to +120 degrees const startAngle = -120; const endAngle = 120; const totalAngle = endAngle - startAngle; const valueAngle = startAngle + (percent / 100) * totalAngle; const bgArc = describeArc(centerX, centerY, radius, startAngle, endAngle); const valueArc = describeArc(centerX, centerY, radius, startAngle, Math.min(valueAngle, endAngle)); // Ticks at 0, 50, 100 const tickAngles = [startAngle, startAngle + totalAngle * 0.5, endAngle]; const gaugeId = useMemo( () => `${service ? service.replace(/[^a-zA-Z0-9]/g, "_") + "_" : ""}${name.replace(/[^a-zA-Z0-9]/g, "_")}`, [service, name] ); return (