more otel
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 58s

This commit is contained in:
mats committed 2026-09-18 20:51:37 +00:00
1 parent 9620e3bfa6
commit 21f04d696b
88 files changed
+23419 -192

No files matched your search

+372
View File
@@ -0,0 +1,372 @@
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-rose-400",
stroke: "#f43f5e",
gradientStart: "#fb7185",
gradientEnd: "#e11d48",
bg: "bg-rose-500/10",
border: "border-rose-500/30",
glow: "rgba(244, 63, 94, 0.35)",
label: "Critical",
};
}
if (percent >= warnThreshold) {
return {
text: "text-amber-400",
stroke: "#f59e0b",
gradientStart: "#fbbf24",
gradientEnd: "#d97706",
bg: "bg-amber-500/10",
border: "border-amber-500/30",
glow: "rgba(245, 158, 11, 0.35)",
label: "Warning",
};
}
return {
text: "text-cyan-400",
stroke: "#06b6d4",
gradientStart: "#38bdf8",
gradientEnd: "#0284c7",
bg: "bg-cyan-500/10",
border: "border-cyan-500/30",
glow: "rgba(6, 182, 212, 0.35)",
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-slate-500" };
if (diff > 0) return { direction: "up", text: `+${formatNumber(diff)}`, color: "text-rose-400" };
return { direction: "down", text: `${formatNumber(diff)}`, color: "text-emerald-400" };
}, [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-white/10 bg-slate-900/40 p-4 shadow-lg backdrop-blur-sm transition-all hover:border-white/20">
<div className="flex w-full items-start justify-between gap-2 border-b border-white/5 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-sky-500/15 px-1.5 py-0.5 text-[9px] font-mono font-bold text-sky-300 border border-sky-500/30"
title={`Sum of values across ${podCount} pods`}
>
<span className="text-[11px] font-black leading-none text-sky-400">∑</span>
<span>{podCount} pods</span>
</span>
)}
</div>
<span className="truncate text-xs font-bold uppercase tracking-wider text-slate-300" 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="rgba(255, 255, 255, 0.08)"
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="rgba(255, 255, 255, 0.2)"
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="#ffffff"
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-amber-400 tracking-wider truncate max-w-[100px]" title={`Solo: ${soloPod}`}>
{soloPod}
</span>
) : aggLabel ? (
<span className="text-[8.5px] font-mono uppercase font-black text-sky-400 tracking-wider">
{aggLabel}
</span>
) : podCount !== undefined && podCount > 1 ? (
<span className="text-[8.5px] font-mono uppercase font-black text-sky-400 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-slate-400">
{unit}
</span>
)}
</div>
{podCount !== undefined && podCount > 1 ? (
<span
className="text-[9px] font-mono text-slate-400 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-slate-500 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-white/5 pt-2 text-[10px] text-slate-400">
<span className="font-mono">
<span className="text-slate-600">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-slate-400">
<span className="text-slate-600">avg</span> {formatNumber(avg)}
</span>
)}
<span className="font-mono">
<span className="text-slate-600">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-white/5 pt-1.5">
{podSummaries.slice(0, 3).map((ps) => (
<div
key={ps.pod}
className="flex items-center gap-1 rounded bg-white/5 px-1.5 py-0.5 text-[8.5px] font-mono text-slate-400"
title={`${ps.pod}: ${formatNumber(ps.value)} ${unit || ""}`}
>
<span className="truncate max-w-[65px] text-slate-300">{ps.pod}</span>
<span className="font-bold text-sky-300">{formatNumber(ps.value)}</span>
</div>
))}
{podSummaries.length > 3 && (
<span className="text-[8px] font-mono text-slate-500 self-center">
+{podSummaries.length - 3}
</span>
)}
</div>
)}
</div>
);
};