more otel
This commit is contained in:
1 parent
9620e3bfa6
commit
21f04d696b
88 files changed
+23419
-192
No files matched your search
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user