Files
go-otel/otel-ui/src/components/GaugeMeter.tsx
T
matsandClaude Sonnet 5 1e9482ed21
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 56s
Unify otel-ui on ds-* theme; restore redesigned Issues view
Restore IssuesVisualizer redesign reverted by merge 67ce419 and re-apply
Needle triage/test additions. Migrate all remaining screens off the MD3
palette, raw Tailwind colors, gradients and Material Symbols. Remove the
legacy theme block and fonts, drop the duplicate tab strip, rebuild
TelemetryFilters on FilterBar, and document status in AGENTS.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 08:56:34 +00:00

373 lines
14 KiB
TypeScript

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 (
<div className="flex flex-col items-center justify-between rounded-2xl border border-ds-line bg-ds-surface-2 p-4 shadow-lg backdrop-blur-sm transition-all hover:border-ds-line-strong">
<div className="flex w-full items-start justify-between gap-2 border-b border-ds-line pb-2">
<div className="flex flex-col min-w-0 pr-1">
<div className="flex items-center gap-1 flex-wrap mb-1">
{service && (
<span
className={`inline-flex items-center gap-1 self-start rounded px-1.5 py-0.5 text-[9px] font-mono font-bold uppercase tracking-wider border ${servicePalette?.bg} ${servicePalette?.text} ${servicePalette?.border}`}
title={`Owning Service: ${service}`}
>
<span className="w-1.5 h-1.5 rounded-full bg-current opacity-80" />
<span className="truncate max-w-[120px]">{service}</span>
</span>
)}
{podCount !== undefined && podCount > 1 && (
<span
className="inline-flex items-center gap-1 rounded bg-ds-accent-bg px-1.5 py-0.5 text-[9px] font-mono font-bold text-ds-accent border border-ds-accent/30"
title={`Sum of values across ${podCount} pods`}
>
<span className="text-[11px] font-black leading-none text-ds-accent">∑</span>
<span>{podCount} pods</span>
</span>
)}
</div>
<span className="truncate text-xs font-bold uppercase tracking-wider text-ds-text-2" title={name}>
{name}
</span>
</div>
<span className={`shrink-0 rounded-full px-2 py-0.5 text-[9px] font-black uppercase tracking-wider ${statusColor.bg} ${statusColor.text} ${statusColor.border} border`}>
{statusColor.label}
</span>
</div>
<div className="relative my-2 flex items-center justify-center">
<svg width={dim} height={dim * 0.85} viewBox={`0 0 ${dim} ${dim * 0.85}`} className="overflow-visible">
<defs>
<linearGradient id={`gauge-grad-${gaugeId}`} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor={statusColor.gradientStart} />
<stop offset="100%" stopColor={statusColor.gradientEnd} />
</linearGradient>
<filter id={`gauge-glow-${gaugeId}`} x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="0" stdDeviation="3" floodColor={statusColor.glow} />
</filter>
</defs>
{/* Background Track */}
<path
d={bgArc}
fill="none"
stroke="var(--ds-line-strong)"
strokeWidth={strokeWidth}
strokeLinecap="round"
/>
{/* Ticks */}
{tickAngles.map((angle, idx) => {
const inner = polarToCartesian(centerX, centerY, radius - strokeWidth * 0.8, angle);
const outer = polarToCartesian(centerX, centerY, radius + strokeWidth * 0.8, angle);
return (
<line
key={idx}
x1={inner.x}
y1={inner.y}
x2={outer.x}
y2={outer.y}
stroke="var(--ds-line-strong)"
strokeWidth={1.5}
/>
);
})}
{/* Active Value Arc */}
{percent > 0 && (
<path
d={valueArc}
fill="none"
stroke={`url(#gauge-grad-${gaugeId})`}
strokeWidth={strokeWidth}
strokeLinecap="round"
filter={`url(#gauge-glow-${gaugeId})`}
/>
)}
{/* Needle / Value dot indicator */}
{percent > 0 && (() => {
const tip = polarToCartesian(centerX, centerY, radius, valueAngle);
return (
<circle
cx={tip.x}
cy={tip.y}
r={strokeWidth * 0.6}
fill="var(--ds-text)"
stroke={statusColor.stroke}
strokeWidth={2}
className="transition-all duration-300"
/>
);
})()}
</svg>
{/* Center Readout */}
<div className="absolute inset-0 flex flex-col items-center justify-center pt-3 text-center pointer-events-none">
{soloPod ? (
<span className="text-[8px] font-mono uppercase font-black text-ds-warn tracking-wider truncate max-w-[100px]" title={`Solo: ${soloPod}`}>
{soloPod}
</span>
) : aggLabel ? (
<span className="text-[8.5px] font-mono uppercase font-black text-ds-accent tracking-wider">
{aggLabel}
</span>
) : podCount !== undefined && podCount > 1 ? (
<span className="text-[8.5px] font-mono uppercase font-black text-ds-accent tracking-wider">
{isSum ? "Sum (∑)" : "Avg (μ)"}
</span>
) : null}
<div className="flex items-baseline gap-1">
<span className={`font-mono font-black tracking-tight ${size === "sm" ? "text-lg" : size === "lg" ? "text-3xl" : "text-2xl"} ${statusColor.text}`}>
{formatNumber(value)}
</span>
{unit && (
<span className="text-[10px] font-bold uppercase text-ds-text-2">
{unit}
</span>
)}
</div>
{podCount !== undefined && podCount > 1 ? (
<span
className="text-[9px] font-mono text-ds-text-2 truncate max-w-[100px]"
title={isSum ? `Average: ${formatNumber(value / podCount)} per pod` : `Fleet average across ${podCount} pods`}
>
{isSum ? `~${formatNumber(value / podCount)}/pod` : `${podCount} pods avg`}
</span>
) : subtitle ? (
<span className="text-[10px] text-ds-text-3 truncate max-w-[90px]">
{subtitle}
</span>
) : null}
</div>
</div>
{/* Footer range indicators & trend */}
<div className="flex w-full items-center justify-between border-t border-ds-line pt-2 text-[10px] text-ds-text-2">
<span className="font-mono">
<span className="text-ds-text-3">min</span> {formatNumber(min)}
</span>
{trend && (
<span className={`inline-flex items-center gap-0.5 font-mono text-[9px] font-bold ${trend.color}`}>
{trend.direction === "up" && <ArrowUpRight className="h-2.5 w-2.5" />}
{trend.direction === "down" && <ArrowDownRight className="h-2.5 w-2.5" />}
{trend.direction === "flat" && <Minus className="h-2.5 w-2.5" />}
{trend.text}
</span>
)}
{avg !== undefined && (
<span className="font-mono text-ds-text-2">
<span className="text-ds-text-3">avg</span> {formatNumber(avg)}
</span>
)}
<span className="font-mono">
<span className="text-ds-text-3">max</span> {formatNumber(max)}
</span>
</div>
{/* Pod Breakdown pills if available */}
{podSummaries && podSummaries.length > 1 && (
<div className="mt-2 flex w-full flex-wrap items-center gap-1 border-t border-ds-line pt-1.5">
{podSummaries.slice(0, 3).map((ps) => (
<div
key={ps.pod}
className="flex items-center gap-1 rounded bg-ds-surface-2 px-1.5 py-0.5 text-[8.5px] font-mono text-ds-text-2"
title={`${ps.pod}: ${formatNumber(ps.value)} ${unit || ""}`}
>
<span className="truncate max-w-[65px] text-ds-text-2">{ps.pod}</span>
<span className="font-bold text-ds-accent">{formatNumber(ps.value)}</span>
</div>
))}
{podSummaries.length > 3 && (
<span className="text-[8px] font-mono text-ds-text-3 self-center">
+{podSummaries.length - 3}
</span>
)}
</div>
)}
</div>
);
};