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>
This commit is contained in:
1 parent
67ce4197da
commit
1e9482ed21
23 files changed
+1583
-1648
No files matched your search
+13
-13
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { CloudCog } from "lucide-react";
|
||||
import { AlertCircle, CloudCog, Lock } from "lucide-react";
|
||||
import { useObservabilityData } from "./hooks/useObservabilityData";
|
||||
import { IconRail } from "./components/layout/IconRail";
|
||||
import { TopBar } from "./components/layout/TopBar";
|
||||
@@ -27,37 +27,37 @@ function AuthCallbackView() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-on-surface flex items-center justify-center p-6">
|
||||
<div className="max-w-md w-full rounded-2xl border border-surface-container bg-surface-container-low p-8 text-center shadow-xl">
|
||||
<div className="flex min-h-screen items-center justify-center bg-ds-bg p-6 font-body text-ds-text">
|
||||
<main className="w-full max-w-md rounded-[14px] border border-ds-line bg-ds-surface p-8 text-center">
|
||||
{error ? (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-full bg-error-container text-on-error-container flex items-center justify-center">
|
||||
<span className="material-symbols-outlined text-[32px]">error</span>
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-ds-critical-bg text-ds-critical">
|
||||
<AlertCircle className="h-7 w-7" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-error">Authentication Failed</h2>
|
||||
<p className="text-xs text-outline mt-1">{error}</p>
|
||||
<h2 className="font-[family-name:var(--font-display)] text-lg font-bold text-ds-critical">Authentication failed</h2>
|
||||
<p className="mt-1 font-[family-name:var(--font-data)] text-xs text-ds-text-3">{error}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => login()}
|
||||
className="mt-2 px-4 py-2 rounded bg-primary text-on-primary font-mono text-xs hover:bg-primary/90 transition-colors"
|
||||
className="mt-2 inline-flex h-9 items-center rounded-[9px] bg-ds-accent px-4 text-[13px] font-semibold text-ds-on-accent"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-full bg-primary/10 text-primary flex items-center justify-center animate-pulse">
|
||||
<span className="material-symbols-outlined text-[32px]">lock</span>
|
||||
<div className="flex h-14 w-14 animate-pulse items-center justify-center rounded-full bg-ds-accent-bg text-ds-accent">
|
||||
<Lock className="h-7 w-7" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-on-surface">Completing sign in</h2>
|
||||
<p className="text-xs text-outline mt-1">Exchanging authorization code with OIDC provider…</p>
|
||||
<h2 className="font-[family-name:var(--font-display)] text-lg font-semibold text-ds-text">Completing sign in</h2>
|
||||
<p className="mt-1 text-xs text-ds-text-3">Exchanging authorization code with OIDC provider…</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AlertTriangle, Download } from "lucide-react"
|
||||
import React from "react";
|
||||
import { useObservabilityData } from "../hooks/useObservabilityData";
|
||||
import { useIssues } from "../hooks/useIssues";
|
||||
@@ -101,25 +102,25 @@ export const CommandAndQueryBar: React.FC = () => {
|
||||
<div className="flex flex-col gap-2.5 mb-4 font-body">
|
||||
{/* Real Anomaly Radar Banner */}
|
||||
{(errorStats.errorCount > 0 || topOpenIssue) && (
|
||||
<div className="rounded-xl border border-rose-500/30 bg-gradient-to-r from-rose-950/40 via-slate-950/60 to-rose-950/20 p-3 shadow-lg flex flex-col sm:flex-row sm:items-center justify-between gap-3 animate-in fade-in duration-300">
|
||||
<div className="rounded-xl border border-ds-critical/30 bg-ds-surface-2 p-3 shadow-lg flex flex-col sm:flex-row sm:items-center justify-between gap-3 animate-in fade-in duration-300">
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-rose-500/20 text-rose-400 border border-rose-500/40">
|
||||
<span className="material-symbols-outlined text-[18px]">warning</span>
|
||||
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-ds-critical-bg text-ds-critical border border-ds-critical/40">
|
||||
<AlertTriangle className="h-[18px] w-[18px] shrink-0" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-xs font-bold text-rose-300 uppercase tracking-wider">
|
||||
<span className="font-mono text-xs font-bold text-ds-critical uppercase tracking-wider">
|
||||
Anomaly Radar:
|
||||
</span>
|
||||
<span className="font-mono text-xs text-white truncate">
|
||||
<span className="font-mono text-xs text-ds-text truncate">
|
||||
{errorStats.errorCount > 0
|
||||
? `${errorStats.errorCount} errors in active window (${errorStats.services.join(", ") || "various"})`
|
||||
: `${topOpenIssue?.count} occurrences of "${topOpenIssue?.title}"`}
|
||||
</span>
|
||||
</div>
|
||||
{topOpenIssue && (
|
||||
<span className="text-[11px] text-slate-400 truncate">
|
||||
Top Issue: <span className="text-slate-200 font-semibold">{topOpenIssue.title}</span> (Service: {topOpenIssue.service}, ×{topOpenIssue.count})
|
||||
<span className="text-[11px] text-ds-text-2 truncate">
|
||||
Top Issue: <span className="text-ds-text font-semibold">{topOpenIssue.title}</span> (Service: {topOpenIssue.service}, ×{topOpenIssue.count})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -129,7 +130,7 @@ export const CommandAndQueryBar: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("tab0")}
|
||||
className="px-3 py-1 rounded-lg bg-rose-500 hover:bg-rose-600 text-white font-mono text-xs font-bold transition-colors shadow-sm"
|
||||
className="px-3 py-1 rounded-lg bg-ds-critical hover:bg-ds-critical text-ds-text font-mono text-xs font-bold transition-colors shadow-sm"
|
||||
>
|
||||
Triage Issues
|
||||
</button>
|
||||
@@ -137,7 +138,7 @@ export const CommandAndQueryBar: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowErrorsOnly(true)}
|
||||
className="px-2.5 py-1 rounded-lg bg-white/10 hover:bg-white/20 text-slate-200 font-mono text-xs transition-colors"
|
||||
className="px-2.5 py-1 rounded-lg bg-ds-fill hover:bg-ds-fill text-ds-text font-mono text-xs transition-colors"
|
||||
>
|
||||
Isolate Errors
|
||||
</button>
|
||||
@@ -147,13 +148,13 @@ export const CommandAndQueryBar: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Filter and Telemetry Status Bar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-2.5 p-3 rounded-xl bg-surface-container-lowest border border-surface-container/50 shadow-sm">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2.5 p-3 rounded-xl bg-ds-bg border border-ds-line shadow-sm">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-mono text-[10px] text-outline uppercase tracking-wider font-semibold shrink-0">
|
||||
<span className="font-mono text-[10px] text-ds-text-3 uppercase tracking-wider font-semibold shrink-0">
|
||||
Active Context:
|
||||
</span>
|
||||
|
||||
<span className="font-mono text-xs text-secondary font-semibold">
|
||||
<span className="font-mono text-xs text-ds-accent font-semibold">
|
||||
{totalFilteredEvents} Signals ({timeWindow === "all" ? "All Time" : `Last ${timeWindow}`})
|
||||
</span>
|
||||
|
||||
@@ -161,13 +162,13 @@ export const CommandAndQueryBar: React.FC = () => {
|
||||
{Array.from(selectedServices).map((service) => (
|
||||
<span
|
||||
key={service}
|
||||
className="inline-flex items-center gap-1 rounded-full bg-primary-container/20 text-primary-container border border-primary-container/40 px-2.5 py-0.5 font-mono text-xs font-semibold"
|
||||
className="inline-flex items-center gap-1 rounded-full bg-ds-accent-bg text-ds-accent border border-ds-line px-2.5 py-0.5 font-mono text-xs font-semibold"
|
||||
>
|
||||
<span>service:{service}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleService(service)}
|
||||
className="hover:text-white"
|
||||
className="hover:text-ds-text"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -176,12 +177,12 @@ export const CommandAndQueryBar: React.FC = () => {
|
||||
|
||||
{/* Errors Only Pill */}
|
||||
{showErrorsOnly && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-error-container text-on-error-container px-2.5 py-0.5 font-mono text-xs font-semibold">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-ds-critical-bg text-ds-critical px-2.5 py-0.5 font-mono text-xs font-semibold">
|
||||
<span>Errors Only</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowErrorsOnly(false)}
|
||||
className="hover:text-white"
|
||||
className="hover:text-ds-text"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -190,12 +191,12 @@ export const CommandAndQueryBar: React.FC = () => {
|
||||
|
||||
{/* Slow Spans Pill */}
|
||||
{minDurationMs > 0 && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-amber-500/20 text-amber-300 border border-amber-500/40 px-2.5 py-0.5 font-mono text-xs font-semibold">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-ds-warn-bg text-ds-warn border border-ds-warn/40 px-2.5 py-0.5 font-mono text-xs font-semibold">
|
||||
<span>Latency >{minDurationMs}ms</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMinDurationMs(0)}
|
||||
className="hover:text-white"
|
||||
className="hover:text-ds-text"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -204,12 +205,12 @@ export const CommandAndQueryBar: React.FC = () => {
|
||||
|
||||
{/* Search Query Pill */}
|
||||
{searchQuery && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-surface-container text-on-surface border border-surface-container-high px-2.5 py-0.5 font-mono text-xs">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-ds-surface-2 text-ds-text border border-ds-line px-2.5 py-0.5 font-mono text-xs">
|
||||
<span>query: "{searchQuery}"</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="hover:text-white"
|
||||
className="hover:text-ds-text"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -224,7 +225,7 @@ export const CommandAndQueryBar: React.FC = () => {
|
||||
setMinDurationMs(0);
|
||||
setTimeWindow("all");
|
||||
}}
|
||||
className="text-outline hover:text-white font-mono text-xs underline ml-1"
|
||||
className="text-ds-text-3 hover:text-ds-text font-mono text-xs underline ml-1"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
@@ -236,10 +237,10 @@ export const CommandAndQueryBar: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={exportNDJSON}
|
||||
className="flex items-center gap-1 px-2.5 py-1 bg-surface-container-low hover:bg-surface-container text-on-surface font-mono text-xs rounded-lg transition-colors border border-surface-container/40 shadow-sm"
|
||||
className="flex items-center gap-1 px-2.5 py-1 bg-ds-surface hover:bg-ds-surface-2 text-ds-text font-mono text-xs rounded-lg transition-colors border border-ds-line shadow-sm"
|
||||
title="Export currently filtered telemetry signals as NDJSON"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[15px]">download</span>
|
||||
<Download className="h-[15px] w-[15px] shrink-0" aria-hidden="true" />
|
||||
<span>Export NDJSON</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -116,36 +116,36 @@ export const GaugeMeter = ({
|
||||
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)",
|
||||
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-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)",
|
||||
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-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)",
|
||||
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]);
|
||||
@@ -155,9 +155,9 @@ export const GaugeMeter = ({
|
||||
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" };
|
||||
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
|
||||
@@ -185,8 +185,8 @@ export const GaugeMeter = ({
|
||||
);
|
||||
|
||||
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 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 && (
|
||||
@@ -200,15 +200,15 @@ export const GaugeMeter = ({
|
||||
)}
|
||||
{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"
|
||||
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-sky-400">∑</span>
|
||||
<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-slate-300" title={name}>
|
||||
<span className="truncate text-xs font-bold uppercase tracking-wider text-ds-text-2" title={name}>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
@@ -233,7 +233,7 @@ export const GaugeMeter = ({
|
||||
<path
|
||||
d={bgArc}
|
||||
fill="none"
|
||||
stroke="rgba(255, 255, 255, 0.08)"
|
||||
stroke="var(--ds-line-strong)"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
@@ -249,7 +249,7 @@ export const GaugeMeter = ({
|
||||
y1={inner.y}
|
||||
x2={outer.x}
|
||||
y2={outer.y}
|
||||
stroke="rgba(255, 255, 255, 0.2)"
|
||||
stroke="var(--ds-line-strong)"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
);
|
||||
@@ -275,7 +275,7 @@ export const GaugeMeter = ({
|
||||
cx={tip.x}
|
||||
cy={tip.y}
|
||||
r={strokeWidth * 0.6}
|
||||
fill="#ffffff"
|
||||
fill="var(--ds-text)"
|
||||
stroke={statusColor.stroke}
|
||||
strokeWidth={2}
|
||||
className="transition-all duration-300"
|
||||
@@ -287,15 +287,15 @@ export const GaugeMeter = ({
|
||||
{/* 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}`}>
|
||||
<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-sky-400 tracking-wider">
|
||||
<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-sky-400 tracking-wider">
|
||||
<span className="text-[8.5px] font-mono uppercase font-black text-ds-accent tracking-wider">
|
||||
{isSum ? "Sum (∑)" : "Avg (μ)"}
|
||||
</span>
|
||||
) : null}
|
||||
@@ -304,20 +304,20 @@ export const GaugeMeter = ({
|
||||
{formatNumber(value)}
|
||||
</span>
|
||||
{unit && (
|
||||
<span className="text-[10px] font-bold uppercase text-slate-400">
|
||||
<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-slate-400 truncate max-w-[100px]"
|
||||
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-slate-500 truncate max-w-[90px]">
|
||||
<span className="text-[10px] text-ds-text-3 truncate max-w-[90px]">
|
||||
{subtitle}
|
||||
</span>
|
||||
) : null}
|
||||
@@ -325,9 +325,9 @@ export const GaugeMeter = ({
|
||||
</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">
|
||||
<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-slate-600">min</span> {formatNumber(min)}
|
||||
<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}`}>
|
||||
@@ -338,30 +338,30 @@ export const GaugeMeter = ({
|
||||
</span>
|
||||
)}
|
||||
{avg !== undefined && (
|
||||
<span className="font-mono text-slate-400">
|
||||
<span className="text-slate-600">avg</span> {formatNumber(avg)}
|
||||
<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-slate-600">max</span> {formatNumber(max)}
|
||||
<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-white/5 pt-1.5">
|
||||
<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-white/5 px-1.5 py-0.5 text-[8.5px] font-mono text-slate-400"
|
||||
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-slate-300">{ps.pod}</span>
|
||||
<span className="font-bold text-sky-300">{formatNumber(ps.value)}</span>
|
||||
<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-slate-500 self-center">
|
||||
<span className="text-[8px] font-mono text-ds-text-3 self-center">
|
||||
+{podSummaries.length - 3}
|
||||
</span>
|
||||
)}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -291,10 +291,10 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
}, [selectedPod, isMultiPod, podLatestMap, latestPoint, aggMode, activePods]);
|
||||
|
||||
const gradients = [
|
||||
{ from: "#818CF8", to: "#38BDF8" },
|
||||
{ from: "#34D399", to: "#38BDF8" },
|
||||
{ from: "#F472B6", to: "#FB7185" },
|
||||
{ from: "#FBBF24", to: "#F59E0B" },
|
||||
{ from: "var(--ds-accent)", to: "var(--ds-accent)" },
|
||||
{ from: "var(--ds-ok)", to: "var(--ds-accent)" },
|
||||
{ from: "var(--ds-accent)", to: "var(--ds-critical)" },
|
||||
{ from: "var(--ds-warn)", to: "var(--ds-warn)" },
|
||||
];
|
||||
const color = gradients[index % gradients.length];
|
||||
const gradientId = `gradient-${index}-${name.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
||||
@@ -303,9 +303,9 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload;
|
||||
return (
|
||||
<div className="z-50 flex flex-col gap-2 rounded-xl border border-white/10 bg-slate-950/95 p-3 text-xs shadow-2xl backdrop-blur-md min-w-[220px]">
|
||||
<div className="flex items-center justify-between gap-4 border-b border-white/5 pb-2">
|
||||
<span className="font-mono text-slate-400">
|
||||
<div className="z-50 flex flex-col gap-2 rounded-xl border border-ds-line bg-ds-bg/95 p-3 text-xs shadow-2xl backdrop-blur-md min-w-[220px]">
|
||||
<div className="flex items-center justify-between gap-4 border-b border-ds-line pb-2">
|
||||
<span className="font-mono text-ds-text-2">
|
||||
{new Date(data.time).toLocaleTimeString([], {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
@@ -315,7 +315,7 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
})}
|
||||
</span>
|
||||
<div className="text-right">
|
||||
<div className="text-[9px] font-bold text-sky-400 uppercase tracking-wider">
|
||||
<div className="text-[9px] font-bold text-ds-accent uppercase tracking-wider">
|
||||
{selectedPod !== "all"
|
||||
? "Solo Value"
|
||||
: isMultiPod
|
||||
@@ -328,7 +328,7 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
: "Value"
|
||||
: "Value"}
|
||||
</div>
|
||||
<span className="text-sm font-bold text-sky-400 font-mono">
|
||||
<span className="text-sm font-bold text-ds-accent font-mono">
|
||||
{valueFormatter.format(data.value ?? data.avg ?? data.total ?? 0)} {unit || ""}
|
||||
</span>
|
||||
</div>
|
||||
@@ -336,32 +336,32 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
|
||||
{/* If fleet average, show min-to-max envelope in tooltip */}
|
||||
{isMultiPod && selectedPod === "all" && data.min !== undefined && data.max !== undefined && (
|
||||
<div className="flex items-center justify-between text-[10px] font-mono text-slate-400 bg-white/5 rounded px-2 py-1">
|
||||
<div className="flex items-center justify-between text-[10px] font-mono text-ds-text-2 bg-ds-surface-2 rounded px-2 py-1">
|
||||
<span>Spread:</span>
|
||||
<span>
|
||||
min <strong className="text-slate-200">{valueFormatter.format(data.min)}</strong> • max{" "}
|
||||
<strong className="text-slate-200">{valueFormatter.format(data.max)}</strong>
|
||||
min <strong className="text-ds-text">{valueFormatter.format(data.min)}</strong> • max{" "}
|
||||
<strong className="text-ds-text">{valueFormatter.format(data.max)}</strong>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pod breakdown in tooltip */}
|
||||
{isMultiPod && selectedPod === "all" && (
|
||||
<div className="space-y-1.5 py-1 border-b border-white/5">
|
||||
<div className="flex items-center justify-between text-[9px] font-bold uppercase tracking-wider text-slate-500">
|
||||
<div className="space-y-1.5 py-1 border-b border-ds-line">
|
||||
<div className="flex items-center justify-between text-[9px] font-bold uppercase tracking-wider text-ds-text-3">
|
||||
<span>Pod Breakdown ({activePods.length}):</span>
|
||||
<span className="text-[8px] text-slate-600 font-normal">click pod to solo</span>
|
||||
<span className="text-[8px] text-ds-text-3 font-normal">click pod to solo</span>
|
||||
</div>
|
||||
{activePods.map((pod) => {
|
||||
const pal = getPodPalette(pod);
|
||||
const val = data[pod] ?? 0;
|
||||
return (
|
||||
<div key={pod} className="flex items-center justify-between gap-3 text-[11px] font-mono">
|
||||
<span className="flex items-center gap-1.5 text-slate-300 truncate max-w-[130px]">
|
||||
<span className="flex items-center gap-1.5 text-ds-text-2 truncate max-w-[130px]">
|
||||
<span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: pal.stroke }} />
|
||||
<span className="truncate">{pod}</span>
|
||||
</span>
|
||||
<span className="font-bold text-white">
|
||||
<span className="font-bold text-ds-text">
|
||||
{valueFormatter.format(val)} {unit || ""}
|
||||
</span>
|
||||
</div>
|
||||
@@ -371,16 +371,16 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1.5 pt-1 text-[10px]">
|
||||
<span className="text-slate-500 font-mono uppercase">Service:</span>
|
||||
<span className="text-ds-text-3 font-mono uppercase">Service:</span>
|
||||
<span className={`font-mono font-bold ${servicePalette.text}`}>{service}</span>
|
||||
</div>
|
||||
|
||||
{data.attributes && data.attributes.length > 0 && (
|
||||
<div className="mt-1 flex flex-col gap-1 border-t border-white/5 pt-2">
|
||||
<div className="mt-1 flex flex-col gap-1 border-t border-ds-line pt-2">
|
||||
{data.attributes.map((attr: OtelAttribute) => (
|
||||
<div key={attr.key} className="flex gap-2">
|
||||
<span className="text-emerald-400/80">{attr.key}:</span>
|
||||
<span className="max-w-[200px] truncate text-slate-300">
|
||||
<span className="text-ds-ok">{attr.key}:</span>
|
||||
<span className="max-w-[200px] truncate text-ds-text-2">
|
||||
{formatAttributeValue(attr.value)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -394,14 +394,14 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
};
|
||||
|
||||
return (
|
||||
<article className="group relative flex flex-col overflow-hidden rounded-3xl border border-white/5 bg-slate-950/30 transition-all hover:border-white/10 hover:bg-slate-900/40">
|
||||
<header className="flex items-start justify-between p-5 border-b border-white/5 gap-3 flex-wrap sm:flex-nowrap">
|
||||
<article className="group relative flex flex-col overflow-hidden rounded-3xl border border-ds-line bg-ds-surface-2 transition-all hover:border-ds-line hover:bg-ds-surface-2">
|
||||
<header className="flex items-start justify-between p-5 border-b border-ds-line gap-3 flex-wrap sm:flex-nowrap">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br shadow-md"
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-ds-surface-2 shadow-md"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(to bottom right, ${color.from}, ${color.to})`,
|
||||
color: "#020617",
|
||||
color: "var(--ds-bg)",
|
||||
}}
|
||||
>
|
||||
{type === "gauge" ? (
|
||||
@@ -426,19 +426,19 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
{selectedPod !== "all" ? (
|
||||
<button
|
||||
onClick={() => setSelectedPod("all")}
|
||||
className="inline-flex items-center gap-1 rounded bg-amber-500/15 px-1.5 py-0.5 text-[9px] font-mono font-bold uppercase tracking-wider text-amber-300 border border-amber-500/30 hover:bg-amber-500/25 transition-all"
|
||||
className="inline-flex items-center gap-1 rounded bg-ds-warn-bg px-1.5 py-0.5 text-[9px] font-mono font-bold uppercase tracking-wider text-ds-warn border border-ds-warn/30 hover:bg-ds-warn-bg transition-all"
|
||||
title="Click to exit solo mode and show all pods"
|
||||
>
|
||||
<Eye className="h-2.5 w-2.5" />
|
||||
<span className="truncate max-w-[110px]">Solo: {selectedPod}</span>
|
||||
<X className="h-2.5 w-2.5 ml-0.5 text-amber-400" />
|
||||
<X className="h-2.5 w-2.5 ml-0.5 text-ds-warn" />
|
||||
</button>
|
||||
) : isMultiPod ? (
|
||||
<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 uppercase tracking-wider text-sky-300 border border-sky-500/30"
|
||||
className="inline-flex items-center gap-1 rounded bg-ds-accent-bg px-1.5 py-0.5 text-[9px] font-mono font-bold uppercase tracking-wider text-ds-accent border border-ds-accent/30"
|
||||
title={`${activePods.length} pods reporting: ${activePods.join(", ")}`}
|
||||
>
|
||||
<Boxes className="h-2.5 w-2.5 text-sky-400" />
|
||||
<Boxes className="h-2.5 w-2.5 text-ds-accent" />
|
||||
<span>{activePods.length} pods</span>
|
||||
</span>
|
||||
) : null}
|
||||
@@ -446,18 +446,18 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-[9px] font-black uppercase tracking-wider ${
|
||||
type === "gauge"
|
||||
? "bg-cyan-500/10 text-cyan-300 border border-cyan-500/20"
|
||||
: "bg-white/5 text-slate-400"
|
||||
? "bg-ds-accent-bg text-ds-accent border border-ds-accent/20"
|
||||
: "bg-ds-surface-2 text-ds-text-2"
|
||||
}`}
|
||||
>
|
||||
{type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-sm font-bold text-white max-w-[200px] sm:max-w-[280px] truncate" title={name}>
|
||||
<h3 className="text-sm font-bold text-ds-text max-w-[200px] sm:max-w-[280px] truncate" title={name}>
|
||||
{name}
|
||||
</h3>
|
||||
<p className="text-[11px] font-medium text-slate-500">
|
||||
<p className="text-[11px] font-medium text-ds-text-3">
|
||||
{stats?.count} samples {unit ? `• ${unit}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
@@ -466,13 +466,13 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
<div className="flex items-center gap-3 ml-auto sm:ml-0">
|
||||
{/* Multi-pod aggregation selector if in chart mode and not soloed */}
|
||||
{viewMode === "chart" && isMultiPod && selectedPod === "all" && (
|
||||
<div className="flex rounded-lg bg-black/40 p-0.5 border border-white/5">
|
||||
<div className="flex rounded-lg bg-ds-surface-2 p-0.5 border border-ds-line">
|
||||
<button
|
||||
onClick={() => setAggMode("avg")}
|
||||
className={`px-2 py-1 rounded text-[10px] font-bold transition-all ${
|
||||
aggMode === "avg"
|
||||
? "bg-sky-500/20 text-sky-300 shadow"
|
||||
: "text-slate-500 hover:text-slate-300"
|
||||
? "bg-ds-accent-bg text-ds-accent shadow"
|
||||
: "text-ds-text-3 hover:text-ds-text-2"
|
||||
}`}
|
||||
title="Fleet Average (mean across pods with min-max envelope)"
|
||||
>
|
||||
@@ -482,8 +482,8 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
onClick={() => setAggMode("lines")}
|
||||
className={`px-2 py-1 rounded text-[10px] font-bold transition-all ${
|
||||
aggMode === "lines"
|
||||
? "bg-sky-500/20 text-sky-300 shadow"
|
||||
: "text-slate-500 hover:text-slate-300"
|
||||
? "bg-ds-accent-bg text-ds-accent shadow"
|
||||
: "text-ds-text-3 hover:text-ds-text-2"
|
||||
}`}
|
||||
title="Overlay Lines (individual per-pod comparison)"
|
||||
>
|
||||
@@ -493,8 +493,8 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
onClick={() => setAggMode("max")}
|
||||
className={`px-2 py-1 rounded text-[10px] font-bold transition-all ${
|
||||
aggMode === "max"
|
||||
? "bg-sky-500/20 text-sky-300 shadow"
|
||||
: "text-slate-500 hover:text-slate-300"
|
||||
? "bg-ds-accent-bg text-ds-accent shadow"
|
||||
: "text-ds-text-3 hover:text-ds-text-2"
|
||||
}`}
|
||||
title="Fleet Maximum (peak / outlier pod over time)"
|
||||
>
|
||||
@@ -504,8 +504,8 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
onClick={() => setAggMode("sum")}
|
||||
className={`px-2 py-1 rounded text-[10px] font-bold transition-all ${
|
||||
aggMode === "sum"
|
||||
? "bg-sky-500/20 text-sky-300 shadow"
|
||||
: "text-slate-500 hover:text-slate-300"
|
||||
? "bg-ds-accent-bg text-ds-accent shadow"
|
||||
: "text-ds-text-3 hover:text-ds-text-2"
|
||||
}`}
|
||||
title="Fleet Total (sum of all pods)"
|
||||
>
|
||||
@@ -515,8 +515,8 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
onClick={() => setAggMode("stacked")}
|
||||
className={`px-2 py-1 rounded text-[10px] font-bold transition-all ${
|
||||
aggMode === "stacked"
|
||||
? "bg-sky-500/20 text-sky-300 shadow"
|
||||
: "text-slate-500 hover:text-slate-300"
|
||||
? "bg-ds-accent-bg text-ds-accent shadow"
|
||||
: "text-ds-text-3 hover:text-ds-text-2"
|
||||
}`}
|
||||
title="Stacked Area (cumulative volume)"
|
||||
>
|
||||
@@ -526,13 +526,13 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
)}
|
||||
|
||||
{/* View switcher: Gauge vs Timeline Chart */}
|
||||
<div className="flex rounded-lg bg-black/40 p-0.5 border border-white/5">
|
||||
<div className="flex rounded-lg bg-ds-surface-2 p-0.5 border border-ds-line">
|
||||
<button
|
||||
onClick={() => setViewMode("chart")}
|
||||
className={`px-2 py-1 rounded text-[10px] font-bold transition-all ${
|
||||
viewMode === "chart"
|
||||
? "bg-white/10 text-white shadow"
|
||||
: "text-slate-500 hover:text-slate-300"
|
||||
? "bg-ds-fill text-ds-text shadow"
|
||||
: "text-ds-text-3 hover:text-ds-text-2"
|
||||
}`}
|
||||
title="Time series area chart"
|
||||
>
|
||||
@@ -542,8 +542,8 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
onClick={() => setViewMode("gauge")}
|
||||
className={`px-2 py-1 rounded text-[10px] font-bold transition-all ${
|
||||
viewMode === "gauge"
|
||||
? "bg-cyan-500/20 text-cyan-300 shadow"
|
||||
: "text-slate-500 hover:text-slate-300"
|
||||
? "bg-ds-accent-bg text-ds-accent shadow"
|
||||
: "text-ds-text-3 hover:text-ds-text-2"
|
||||
}`}
|
||||
title="Gauge dial view"
|
||||
>
|
||||
@@ -552,15 +552,15 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<div className="text-[9px] font-bold uppercase tracking-[0.2em] text-slate-500">
|
||||
<div className="text-[9px] font-bold uppercase tracking-[0.2em] text-ds-text-3">
|
||||
{currentDisplay.label}
|
||||
</div>
|
||||
<div className="text-lg font-black text-white font-mono">
|
||||
<div className="text-lg font-black text-ds-text font-mono">
|
||||
{valueFormatter.format(currentDisplay.value)}
|
||||
{unit && <span className="ml-1 text-xs text-slate-400 font-normal">{unit}</span>}
|
||||
{unit && <span className="ml-1 text-xs text-ds-text-2 font-normal">{unit}</span>}
|
||||
</div>
|
||||
{currentDisplay.subtext && (
|
||||
<div className="text-[9.5px] text-slate-400 font-mono truncate max-w-[140px]">
|
||||
<div className="text-[9.5px] text-ds-text-2 font-mono truncate max-w-[140px]">
|
||||
{currentDisplay.subtext}
|
||||
</div>
|
||||
)}
|
||||
@@ -598,8 +598,8 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
<stop offset="95%" stopColor={color.from} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="avgEnvelopeGrad" x1="0" y1="0" x2="0" y2="100%">
|
||||
<stop offset="5%" stopColor="#94a3b8" stopOpacity={0.12} />
|
||||
<stop offset="95%" stopColor="#94a3b8" stopOpacity={0.02} />
|
||||
<stop offset="5%" stopColor="var(--ds-text-2)" stopOpacity={0.12} />
|
||||
<stop offset="95%" stopColor="var(--ds-text-2)" stopOpacity={0.02} />
|
||||
</linearGradient>
|
||||
{activePods.map((pod) => {
|
||||
const pal = getPodPalette(pod);
|
||||
@@ -612,7 +612,7 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
);
|
||||
})}
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="rgba(255,255,255,0.05)" />
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--ds-line-strong)" />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
type="number"
|
||||
@@ -624,14 +624,14 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
second: "2-digit",
|
||||
})
|
||||
}
|
||||
stroke="#475569"
|
||||
stroke="var(--ds-text-3)"
|
||||
fontSize={10}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
minTickGap={30}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#475569"
|
||||
stroke="var(--ds-text-3)"
|
||||
fontSize={10}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
@@ -667,7 +667,7 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="max"
|
||||
stroke="rgba(148, 163, 184, 0.35)"
|
||||
stroke="color-mix(in srgb, var(--ds-text-2) 35%, transparent)"
|
||||
strokeDasharray="2 2"
|
||||
strokeWidth={1}
|
||||
fill="url(#avgEnvelopeGrad)"
|
||||
@@ -686,7 +686,7 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="min"
|
||||
stroke="rgba(100, 116, 139, 0.4)"
|
||||
stroke="color-mix(in srgb, var(--ds-text-3) 40%, transparent)"
|
||||
strokeDasharray="2 2"
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
@@ -734,7 +734,7 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
type="monotone"
|
||||
dataKey="sum"
|
||||
name="Total (Sum)"
|
||||
stroke="#ffffff"
|
||||
stroke="var(--ds-text)"
|
||||
strokeDasharray="3 3"
|
||||
strokeWidth={1.5}
|
||||
dot={false}
|
||||
@@ -745,7 +745,7 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="max"
|
||||
stroke="#f43f5e"
|
||||
stroke="var(--ds-critical)"
|
||||
strokeWidth={2.5}
|
||||
fill={`url(#${gradientId})`}
|
||||
isAnimationActive={false}
|
||||
@@ -769,16 +769,16 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
|
||||
{/* Pod breakdown pill list with click-to-solo */}
|
||||
{isMultiPod && (
|
||||
<div className="flex flex-wrap items-center gap-1.5 border-t border-white/5 px-4 py-2 bg-black/15">
|
||||
<div className="flex items-center gap-1 text-[9px] font-bold uppercase tracking-wider text-slate-500 mr-1">
|
||||
<Boxes className="h-3 w-3 text-sky-400" />
|
||||
<div className="flex flex-wrap items-center gap-1.5 border-t border-ds-line px-4 py-2 bg-ds-surface-2">
|
||||
<div className="flex items-center gap-1 text-[9px] font-bold uppercase tracking-wider text-ds-text-3 mr-1">
|
||||
<Boxes className="h-3 w-3 text-ds-accent" />
|
||||
<span>Pods ({activePods.length}):</span>
|
||||
</div>
|
||||
|
||||
{selectedPod !== "all" && (
|
||||
<button
|
||||
onClick={() => setSelectedPod("all")}
|
||||
className="inline-flex items-center gap-1 rounded-lg px-2 py-0.5 text-[10px] font-mono font-bold bg-white/10 hover:bg-white/20 text-white transition-all border border-white/10"
|
||||
className="inline-flex items-center gap-1 rounded-lg px-2 py-0.5 text-[10px] font-mono font-bold bg-ds-fill hover:bg-ds-fill text-ds-text transition-all border border-ds-line"
|
||||
title="Show all pods"
|
||||
>
|
||||
All Pods
|
||||
@@ -794,57 +794,57 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
onClick={() => setSelectedPod(isSelected ? "all" : ps.pod)}
|
||||
className={`inline-flex items-center gap-1.5 rounded-lg px-2 py-0.5 text-[10px] font-mono border transition-all ${
|
||||
isSelected
|
||||
? "bg-amber-500/20 text-amber-200 border-amber-400/50 ring-2 ring-amber-400/30 shadow-md font-bold"
|
||||
: `${pal.bg} ${pal.border} hover:border-white/20 hover:brightness-110`
|
||||
? "bg-ds-warn-bg text-ds-warn border-ds-warn/50 ring-2 ring-ds-warn/40 shadow-md font-bold"
|
||||
: `${pal.bg} ${pal.border} hover:border-ds-line-strong hover:brightness-110`
|
||||
}`}
|
||||
title={`Click to ${isSelected ? "unsolo" : "solo"} ${ps.pod} (${valueFormatter.format(ps.value)} ${unit || ""})`}
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ backgroundColor: pal.stroke }} />
|
||||
<span className="truncate max-w-[120px]">{ps.pod}</span>
|
||||
<span className={`font-bold ${isSelected ? "text-amber-300" : pal.text}`}>
|
||||
<span className={`font-bold ${isSelected ? "text-ds-warn" : pal.text}`}>
|
||||
{valueFormatter.format(ps.value)}
|
||||
</span>
|
||||
{isSelected && <span className="text-[8.5px] uppercase font-black text-amber-400">SOLO</span>}
|
||||
{isSelected && <span className="text-[8.5px] uppercase font-black text-ds-warn">SOLO</span>}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<footer className="mt-auto border-t border-white/5 bg-black/20 p-3 text-[11px]">
|
||||
<footer className="mt-auto border-t border-ds-line bg-ds-surface-2 p-3 text-[11px]">
|
||||
<div className="grid grid-cols-4 gap-2 text-center">
|
||||
<div>
|
||||
<div className="text-[9px] font-bold uppercase tracking-widest text-slate-600">
|
||||
<div className="text-[9px] font-bold uppercase tracking-widest text-ds-text-3">
|
||||
Min
|
||||
</div>
|
||||
<div className="font-mono text-slate-300">
|
||||
<div className="font-mono text-ds-text-2">
|
||||
{stats ? compactFormatter.format(stats.min) : "—"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[9px] font-bold uppercase tracking-widest text-slate-600">
|
||||
<div className="text-[9px] font-bold uppercase tracking-widest text-ds-text-3">
|
||||
Avg
|
||||
</div>
|
||||
<div className="font-mono text-slate-300">
|
||||
<div className="font-mono text-ds-text-2">
|
||||
{stats ? compactFormatter.format(stats.avg) : "—"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[9px] font-bold uppercase tracking-widest text-slate-600">
|
||||
<div className="text-[9px] font-bold uppercase tracking-widest text-ds-text-3">
|
||||
Max
|
||||
</div>
|
||||
<div className="font-mono text-slate-300">
|
||||
<div className="font-mono text-ds-text-2">
|
||||
{stats ? compactFormatter.format(stats.max) : "—"}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="flex flex-col items-center justify-center hover:text-white transition-colors"
|
||||
className="flex flex-col items-center justify-center hover:text-ds-text transition-colors"
|
||||
>
|
||||
<div className="text-[9px] font-bold uppercase tracking-widest text-slate-600">
|
||||
<div className="text-[9px] font-bold uppercase tracking-widest text-ds-text-3">
|
||||
Attributes
|
||||
</div>
|
||||
<div className="flex items-center gap-1 font-mono text-slate-300">
|
||||
<div className="flex items-center gap-1 font-mono text-ds-text-2">
|
||||
<Filter className="h-2.5 w-2.5" />
|
||||
{latestPoint?.attributes?.length ?? 0}
|
||||
</div>
|
||||
@@ -852,21 +852,21 @@ const MetricCard = ({ name, service, type, points, unit, pods = [], index }: Met
|
||||
</div>
|
||||
|
||||
{isExpanded && latestPoint && (
|
||||
<div className="mt-3 space-y-1.5 border-t border-white/5 pt-3">
|
||||
<div className="mt-3 space-y-1.5 border-t border-ds-line pt-3">
|
||||
{latestPoint.attributes && latestPoint.attributes.length > 0 ? (
|
||||
latestPoint.attributes.map((attr: OtelAttribute) => (
|
||||
<div
|
||||
key={attr.key}
|
||||
className="flex items-center justify-between gap-4 rounded-lg bg-white/5 px-2.5 py-1 text-[11px]"
|
||||
className="flex items-center justify-between gap-4 rounded-lg bg-ds-surface-2 px-2.5 py-1 text-[11px]"
|
||||
>
|
||||
<span className="font-semibold text-emerald-400 font-mono text-[10px]">{attr.key}</span>
|
||||
<span className="truncate text-slate-400 font-mono text-[10px]">
|
||||
<span className="font-semibold text-ds-ok font-mono text-[10px]">{attr.key}</span>
|
||||
<span className="truncate text-ds-text-2 font-mono text-[10px]">
|
||||
{formatAttributeValue(attr.value)}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-2 text-[10px] text-slate-600 italic">
|
||||
<div className="text-center py-2 text-[10px] text-ds-text-3 italic">
|
||||
No attributes present
|
||||
</div>
|
||||
)}
|
||||
@@ -917,12 +917,12 @@ export const MetricsVisualizer = () => {
|
||||
|
||||
if (metricList.length === 0) {
|
||||
return (
|
||||
<div className="flex min-h-[400px] flex-col items-center justify-center gap-6 rounded-[2.5rem] border border-dashed border-white/10 bg-slate-950/40 p-12 text-center text-slate-400">
|
||||
<div className="rounded-full bg-emerald-500/10 p-4">
|
||||
<Sparkles className="h-10 w-10 text-emerald-400" />
|
||||
<div className="flex min-h-[400px] flex-col items-center justify-center gap-6 rounded-[2.5rem] border border-dashed border-ds-line bg-ds-surface-2 p-12 text-center text-ds-text-2">
|
||||
<div className="rounded-full bg-ds-ok/15 p-4">
|
||||
<Sparkles className="h-10 w-10 text-ds-ok" />
|
||||
</div>
|
||||
<div className="max-w-md space-y-2">
|
||||
<h3 className="text-xl font-bold text-white">No metric signals detected</h3>
|
||||
<h3 className="text-xl font-bold text-ds-text">No metric signals detected</h3>
|
||||
<p className="text-sm leading-relaxed">
|
||||
The telemetry pipeline is established and waiting for metrics. Send gauge, counter, or histogram data to see interactive gauges and trend visualizations.
|
||||
</p>
|
||||
@@ -938,12 +938,12 @@ export const MetricsVisualizer = () => {
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Gauge className="h-4 w-4 text-cyan-400" />
|
||||
<h2 className="text-xs font-black uppercase tracking-[0.2em] text-slate-400">
|
||||
<Gauge className="h-4 w-4 text-ds-accent" />
|
||||
<h2 className="text-xs font-black uppercase tracking-[0.2em] text-ds-text-2">
|
||||
Live Gauge Dashboard
|
||||
</h2>
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-500 font-mono">
|
||||
<span className="text-[10px] text-ds-text-3 font-mono">
|
||||
{topGauges.length} Active Gauges
|
||||
</span>
|
||||
</div>
|
||||
@@ -994,21 +994,21 @@ export const MetricsVisualizer = () => {
|
||||
)}
|
||||
|
||||
{/* Metric List Toolbar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 border-b border-white/5 pb-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 border-b border-ds-line pb-4">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="relative flex items-center">
|
||||
<Search className="absolute left-3 h-3.5 w-3.5 text-slate-500 pointer-events-none" />
|
||||
<Search className="absolute left-3 h-3.5 w-3.5 text-ds-text-3 pointer-events-none" />
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Filter metrics by name..."
|
||||
className="h-9 w-52 sm:w-64 rounded-xl border border-white/10 bg-slate-900/50 pl-8 pr-8 text-xs text-white placeholder-slate-500 focus:border-sky-500/50 focus:outline-none"
|
||||
className="h-9 w-52 sm:w-64 rounded-xl border border-ds-line bg-ds-surface-2 pl-8 pr-8 text-xs text-ds-text placeholder:text-ds-text-3 focus:border-ds-accent/50 focus:outline-none"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
onClick={() => setSearch("")}
|
||||
className="absolute right-2.5 text-slate-500 hover:text-white"
|
||||
className="absolute right-2.5 text-ds-text-3 hover:text-ds-text"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
@@ -1019,7 +1019,7 @@ export const MetricsVisualizer = () => {
|
||||
<select
|
||||
value={selectedServiceFilter}
|
||||
onChange={(e) => setSelectedServiceFilter(e.target.value)}
|
||||
className="h-9 rounded-xl border border-white/10 bg-slate-900/50 px-2.5 text-[11px] font-bold text-slate-300 outline-none focus:border-sky-500/50"
|
||||
className="h-9 rounded-xl border border-ds-line bg-ds-surface-2 px-2.5 text-[11px] font-bold text-ds-text-2 outline-none focus:border-ds-accent/50"
|
||||
aria-label="Filter metrics by owning service"
|
||||
>
|
||||
<option value="all">All Services ({availableServices.length})</option>
|
||||
@@ -1032,13 +1032,13 @@ export const MetricsVisualizer = () => {
|
||||
)}
|
||||
|
||||
{/* Type filters */}
|
||||
<div className="flex rounded-xl bg-slate-900/50 border border-white/10 p-1">
|
||||
<div className="flex rounded-xl bg-ds-surface-2 border border-ds-line p-1">
|
||||
<button
|
||||
onClick={() => setFilterType("all")}
|
||||
className={`rounded-lg px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider transition-all ${
|
||||
filterType === "all"
|
||||
? "bg-sky-500/20 text-sky-400 shadow-sm"
|
||||
: "text-slate-400 hover:text-white"
|
||||
? "bg-ds-accent-bg text-ds-accent shadow-sm"
|
||||
: "text-ds-text-2 hover:text-ds-text"
|
||||
}`}
|
||||
>
|
||||
All ({metricList.length})
|
||||
@@ -1047,8 +1047,8 @@ export const MetricsVisualizer = () => {
|
||||
onClick={() => setFilterType("gauge")}
|
||||
className={`rounded-lg px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider transition-all ${
|
||||
filterType === "gauge"
|
||||
? "bg-cyan-500/20 text-cyan-300 shadow-sm"
|
||||
: "text-slate-400 hover:text-white"
|
||||
? "bg-ds-accent-bg text-ds-accent shadow-sm"
|
||||
: "text-ds-text-2 hover:text-ds-text"
|
||||
}`}
|
||||
>
|
||||
Gauges
|
||||
@@ -1057,8 +1057,8 @@ export const MetricsVisualizer = () => {
|
||||
onClick={() => setFilterType("sum")}
|
||||
className={`rounded-lg px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider transition-all ${
|
||||
filterType === "sum"
|
||||
? "bg-violet-500/20 text-violet-300 shadow-sm"
|
||||
: "text-slate-400 hover:text-white"
|
||||
? "bg-ds-accent-bg text-ds-accent shadow-sm"
|
||||
: "text-ds-text-2 hover:text-ds-text"
|
||||
}`}
|
||||
>
|
||||
Sums
|
||||
@@ -1067,8 +1067,8 @@ export const MetricsVisualizer = () => {
|
||||
onClick={() => setFilterType("histogram")}
|
||||
className={`rounded-lg px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider transition-all ${
|
||||
filterType === "histogram"
|
||||
? "bg-amber-500/20 text-amber-300 shadow-sm"
|
||||
: "text-slate-400 hover:text-white"
|
||||
? "bg-ds-warn-bg text-ds-warn shadow-sm"
|
||||
: "text-ds-text-2 hover:text-ds-text"
|
||||
}`}
|
||||
>
|
||||
Histograms
|
||||
@@ -1076,14 +1076,14 @@ export const MetricsVisualizer = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-[10px] font-mono text-slate-500">
|
||||
<div className="text-[10px] font-mono text-ds-text-3">
|
||||
Showing {filteredMetrics.length} of {metricList.length} metrics
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
{filteredMetrics.length === 0 ? (
|
||||
<div className="py-16 text-center text-sm text-slate-500">
|
||||
<div className="py-16 text-center text-sm text-ds-text-3">
|
||||
No metrics match your search criteria.
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -73,22 +73,22 @@ export const NeedleTestModal: React.FC<NeedleTestModalProps> = ({ isOpen, onClos
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 p-4 backdrop-blur-sm animate-fade-in">
|
||||
<div className="relative flex flex-col w-full max-w-2xl rounded-2xl border border-emerald-500/30 bg-slate-950 p-6 shadow-2xl shadow-emerald-500/10 max-h-[90vh] overflow-y-auto">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-ds-bg/80 p-4 backdrop-blur-sm animate-fade-in">
|
||||
<div className="relative flex flex-col w-full max-w-2xl rounded-2xl border border-ds-ok/30 bg-ds-bg/95 p-6 shadow-2xl max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-white/10 pb-4">
|
||||
<div className="flex items-center justify-between border-b border-ds-line pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-500/15 border border-emerald-500/30 text-emerald-400">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-ds-ok/15 border border-ds-ok/30 text-ds-ok">
|
||||
<Cpu className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-slate-100 flex items-center gap-2">
|
||||
<h2 className="text-base font-bold text-ds-text flex items-center gap-2">
|
||||
Needle 3 AI Telemetry Classifier
|
||||
<span className="rounded-full bg-emerald-500/20 border border-emerald-500/30 px-2 py-0.5 text-[10px] font-mono text-emerald-300">
|
||||
<span className="rounded-full bg-ds-ok/15 border border-ds-ok/30 px-2 py-0.5 text-[10px] font-mono text-ds-ok">
|
||||
Cactus-Compute
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-xs text-slate-400">
|
||||
<p className="text-xs text-ds-text-2">
|
||||
On-device ~35MB foundation model executing real-time tool calling and extraction on local CPU.
|
||||
</p>
|
||||
</div>
|
||||
@@ -96,7 +96,7 @@ export const NeedleTestModal: React.FC<NeedleTestModalProps> = ({ isOpen, onClos
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-lg p-1.5 text-slate-400 hover:bg-white/10 hover:text-white transition-colors"
|
||||
className="rounded-lg p-1.5 text-ds-text-2 hover:bg-ds-fill hover:text-ds-text transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -104,7 +104,7 @@ export const NeedleTestModal: React.FC<NeedleTestModalProps> = ({ isOpen, onClos
|
||||
|
||||
{/* Preset Selector */}
|
||||
<div className="mt-4">
|
||||
<label className="text-[11px] font-bold uppercase tracking-wider text-slate-400">
|
||||
<label className="text-[11px] font-bold uppercase tracking-wider text-ds-text-2">
|
||||
Quick Telemetry Presets (from ClickHouse)
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
@@ -118,8 +118,8 @@ export const NeedleTestModal: React.FC<NeedleTestModalProps> = ({ isOpen, onClos
|
||||
}}
|
||||
className={`rounded-lg border px-2.5 py-1 text-[11px] font-medium transition-all ${
|
||||
inputText === p.text
|
||||
? "border-emerald-500 bg-emerald-500/20 text-emerald-200"
|
||||
: "border-white/10 bg-white/5 text-slate-300 hover:border-white/20 hover:bg-white/10"
|
||||
? "border-ds-ok bg-ds-ok/15 text-ds-ok"
|
||||
: "border-ds-line bg-ds-surface-2 text-ds-text-2 hover:border-ds-line-strong hover:bg-ds-fill"
|
||||
}`}
|
||||
>
|
||||
{p.label}
|
||||
@@ -130,14 +130,14 @@ export const NeedleTestModal: React.FC<NeedleTestModalProps> = ({ isOpen, onClos
|
||||
|
||||
{/* Input Textarea */}
|
||||
<div className="mt-4">
|
||||
<label className="text-[11px] font-bold uppercase tracking-wider text-slate-400">
|
||||
<label className="text-[11px] font-bold uppercase tracking-wider text-ds-text-2">
|
||||
Telemetry Event / Log / Trace Summary
|
||||
</label>
|
||||
<textarea
|
||||
value={inputText}
|
||||
onChange={(e) => setInputText(e.target.value)}
|
||||
rows={3}
|
||||
className="mt-1.5 w-full rounded-xl border border-white/10 bg-black/50 p-3 font-mono text-xs text-slate-200 placeholder-slate-600 focus:border-emerald-500/50 focus:outline-none focus:ring-1 focus:ring-emerald-500/50"
|
||||
className="mt-1.5 w-full rounded-xl border border-ds-line bg-ds-surface-2 p-3 font-mono text-xs text-ds-text placeholder:text-ds-text-3 focus:border-ds-ok/50 focus:outline-none focus:ring-1 focus:ring-ds-ok/40"
|
||||
placeholder="Type or paste telemetry event..."
|
||||
/>
|
||||
</div>
|
||||
@@ -148,7 +148,7 @@ export const NeedleTestModal: React.FC<NeedleTestModalProps> = ({ isOpen, onClos
|
||||
type="button"
|
||||
onClick={handleClassify}
|
||||
disabled={loading || !inputText.trim()}
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-emerald-500 to-teal-400 px-4 py-2 text-xs font-bold text-slate-950 transition-all hover:opacity-95 active:scale-95 disabled:opacity-50 shadow-lg shadow-emerald-500/20"
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-ds-surface-2 px-4 py-2 text-xs font-bold text-ds-on-accent transition-all hover:opacity-95 active:scale-95 disabled:opacity-50 shadow-lg "
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
@@ -166,57 +166,57 @@ export const NeedleTestModal: React.FC<NeedleTestModalProps> = ({ isOpen, onClos
|
||||
|
||||
{/* Results Area */}
|
||||
{result && (
|
||||
<div className="mt-5 rounded-xl border border-emerald-500/30 bg-emerald-950/20 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between border-b border-emerald-500/20 pb-3">
|
||||
<div className="mt-5 rounded-xl border border-ds-ok/30 bg-ds-ok/15 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between border-b border-ds-ok/20 pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{result.error ? (
|
||||
<AlertTriangle className="h-4 w-4 text-rose-400" />
|
||||
<AlertTriangle className="h-4 w-4 text-ds-critical" />
|
||||
) : (
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
|
||||
<CheckCircle2 className="h-4 w-4 text-ds-ok" />
|
||||
)}
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-slate-300">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-ds-text-2">
|
||||
{result.error ? "Error" : "Triage Result"}
|
||||
</span>
|
||||
{result.action && (
|
||||
<span className="rounded-lg bg-emerald-500/25 border border-emerald-500/40 px-2 py-0.5 font-mono text-xs font-bold text-emerald-200">
|
||||
<span className="rounded-lg bg-ds-ok/15 border border-ds-ok/40 px-2 py-0.5 font-mono text-xs font-bold text-ds-ok">
|
||||
{result.action}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{result.confidence !== undefined && (
|
||||
<div className="flex items-center gap-3 text-xs font-mono">
|
||||
<span className="text-slate-400">
|
||||
<span className="text-ds-text-2">
|
||||
Confidence:{" "}
|
||||
<span className="font-bold text-emerald-300">
|
||||
<span className="font-bold text-ds-ok">
|
||||
{(result.confidence * 100).toFixed(0)}%
|
||||
</span>
|
||||
</span>
|
||||
{result.latency_ms !== undefined && (
|
||||
<span className="text-slate-500">({result.latency_ms}ms)</span>
|
||||
<span className="text-ds-text-3">({result.latency_ms}ms)</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{result.reasoning && (
|
||||
<p className="text-xs text-slate-300 italic">
|
||||
<p className="text-xs text-ds-text-2 italic">
|
||||
“{result.reasoning}”
|
||||
</p>
|
||||
)}
|
||||
|
||||
{result.arguments && Object.keys(result.arguments).length > 0 && (
|
||||
<div className="space-y-1.5 pt-1">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-slate-400">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-ds-text-2">
|
||||
Extracted Typed Arguments:
|
||||
</span>
|
||||
<div className="grid grid-cols-2 gap-2 font-mono text-xs">
|
||||
{Object.entries(result.arguments).map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="rounded-lg border border-white/10 bg-black/40 p-2 text-slate-200 break-all"
|
||||
className="rounded-lg border border-ds-line bg-ds-surface-2 p-2 text-ds-text break-all"
|
||||
>
|
||||
<span className="text-slate-500">{key}: </span>
|
||||
<span className="text-emerald-300 font-semibold">{String(value)}</span>
|
||||
<span className="text-ds-text-3">{key}: </span>
|
||||
<span className="text-ds-ok font-semibold">{String(value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -224,7 +224,7 @@ export const NeedleTestModal: React.FC<NeedleTestModalProps> = ({ isOpen, onClos
|
||||
)}
|
||||
|
||||
{result.error && (
|
||||
<div className="rounded-lg bg-rose-500/10 border border-rose-500/20 p-3 text-xs text-rose-300">
|
||||
<div className="rounded-lg bg-ds-critical-bg border border-ds-critical/20 p-3 text-xs text-ds-critical">
|
||||
{result.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -34,19 +34,19 @@ const PatternIssues = ({ pattern, issues, similar, review }: {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 space-y-2 border-t border-white/5 pt-3">
|
||||
<div className="mt-3 space-y-2 border-t border-ds-line pt-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{members.map((issue) => <button key={issue.id} onClick={() => void toggle(issue)} className="rounded-lg border border-white/10 bg-white/5 px-2 py-1 text-left text-[10px] text-slate-300 hover:border-sky-400/40">{issue.title} <span className="text-slate-500">×{issue.count}</span></button>)}
|
||||
{members.map((issue) => <button key={issue.id} onClick={() => void toggle(issue)} className="rounded-lg border border-ds-line bg-ds-surface-2 px-2 py-1 text-left text-[10px] text-ds-text-2 hover:border-ds-accent/40">{issue.title} <span className="text-ds-text-3">×{issue.count}</span></button>)}
|
||||
</div>
|
||||
{expanded && members[0] && <div className="space-y-2 rounded-lg bg-black/20 p-3">
|
||||
<div className="text-[10px] uppercase tracking-widest text-slate-500">Deterministic suggestions for {members[0].title}</div>
|
||||
{expanded && members[0] && <div className="space-y-2 rounded-lg bg-ds-surface-2 p-3">
|
||||
<div className="text-[10px] uppercase tracking-widest text-ds-text-3">Deterministic suggestions for {members[0].title}</div>
|
||||
{(related[members[0].id] ?? []).map((item) => {
|
||||
const key = `${members[0].id}:${item.issue.id}`
|
||||
return <div key={item.issue.id} className="flex flex-wrap items-center gap-2 text-[11px]">
|
||||
<span className="text-slate-300">{item.issue.title}</span><span className="text-sky-300">{Math.round(item.score * 100)}%</span><span className="text-slate-500">{item.reasons.join(" · ")}</span>
|
||||
<button disabled={busy === key} onClick={() => void submitReview(members[0], item, "same")} className="ml-auto inline-flex items-center gap-1 rounded border border-emerald-400/20 px-2 py-1 text-emerald-300 hover:bg-emerald-400/10"><ThumbsUp className="h-3 w-3" /> same</button>
|
||||
<button disabled={busy === key} onClick={() => void submitReview(members[0], item, "different")} className="inline-flex items-center gap-1 rounded border border-rose-400/20 px-2 py-1 text-rose-300 hover:bg-rose-400/10"><ThumbsDown className="h-3 w-3" /> separate</button>
|
||||
{item.decision && <span className="text-slate-500">reviewed: {item.decision}</span>}
|
||||
<span className="text-ds-text-2">{item.issue.title}</span><span className="text-ds-accent">{Math.round(item.score * 100)}%</span><span className="text-ds-text-3">{item.reasons.join(" · ")}</span>
|
||||
<button disabled={busy === key} onClick={() => void submitReview(members[0], item, "same")} className="ml-auto inline-flex items-center gap-1 rounded border border-ds-ok/20 px-2 py-1 text-ds-ok hover:bg-ds-ok/15"><ThumbsUp className="h-3 w-3" /> same</button>
|
||||
<button disabled={busy === key} onClick={() => void submitReview(members[0], item, "different")} className="inline-flex items-center gap-1 rounded border border-ds-critical/20 px-2 py-1 text-ds-critical hover:bg-ds-critical-bg"><ThumbsDown className="h-3 w-3" /> separate</button>
|
||||
{item.decision && <span className="text-ds-text-3">reviewed: {item.decision}</span>}
|
||||
</div>
|
||||
})}
|
||||
</div>}
|
||||
@@ -59,17 +59,17 @@ export function PatternsVisualizer() {
|
||||
const [query, setQuery] = useState("")
|
||||
const visible = useMemo(() => patterns.filter((pattern) => `${pattern.service} ${pattern.kind} ${pattern.signature}`.toLowerCase().includes(query.toLowerCase())), [patterns, query])
|
||||
|
||||
if (loading && patterns.length === 0) return <div className="py-16 text-center text-sm text-slate-500">Loading deterministic patterns…</div>
|
||||
if (loading && patterns.length === 0) return <div className="py-16 text-center text-sm text-ds-text-3">Loading deterministic patterns…</div>
|
||||
return <div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-3 border-b border-white/5 pb-3">
|
||||
<div><div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-slate-500"><GitBranch className="h-3 w-3 text-violet-400" /> Similarity Patterns</div><div className="mt-1 text-xs text-slate-500">Exact issues stay separate; broader deterministic patterns are reviewable.</div></div>
|
||||
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Filter patterns…" className="ml-auto rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-xs text-slate-200 outline-none placeholder:text-slate-600 focus:border-sky-400/40" />
|
||||
<button onClick={() => void refresh()} className="inline-flex items-center gap-2 rounded-lg border border-white/10 px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-slate-400 hover:text-white"><RefreshCw className="h-3 w-3" /> Refresh</button>
|
||||
<div className="flex flex-wrap items-center gap-3 border-b border-ds-line pb-3">
|
||||
<div><div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-ds-text-3"><GitBranch className="h-3 w-3 text-ds-accent" /> Similarity Patterns</div><div className="mt-1 text-xs text-ds-text-3">Exact issues stay separate; broader deterministic patterns are reviewable.</div></div>
|
||||
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Filter patterns…" className="ml-auto rounded-lg border border-ds-line bg-ds-surface-2 px-3 py-2 text-xs text-ds-text outline-none placeholder:text-ds-text-3 focus:border-ds-accent/40" />
|
||||
<button onClick={() => void refresh()} className="inline-flex items-center gap-2 rounded-lg border border-ds-line px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-ds-text-2 hover:text-ds-text"><RefreshCw className="h-3 w-3" /> Refresh</button>
|
||||
</div>
|
||||
{error && <div className="rounded-lg border border-rose-400/20 bg-rose-400/5 p-3 text-xs text-rose-300">Failed to load patterns: {String(error)}</div>}
|
||||
{visible.length === 0 ? <div className="py-16 text-center text-sm text-slate-500">No deterministic patterns observed yet.</div> : <div className="space-y-3">{visible.map((pattern) => <div key={pattern.id} className="rounded-xl border border-violet-400/15 bg-slate-950/40 p-4">
|
||||
<div className="flex flex-wrap items-center gap-3"><span className="rounded bg-violet-500/15 px-2 py-1 text-[10px] font-bold uppercase text-violet-300">{pattern.service}</span><span className="text-xs text-slate-400">{pattern.kind}</span><span className="text-xs text-slate-300">{pattern.issueCount} exact issues · {pattern.occurrenceCount} occurrences</span><span className="ml-auto text-[10px] text-slate-600">last seen {formatRelativeTime(pattern.lastSeen / 1e6)}</span></div>
|
||||
<div className="mt-3 break-all font-mono text-[10px] text-slate-500">{pattern.signature.split("\u0000").join(" · ")}</div>
|
||||
{error && <div className="rounded-lg border border-ds-critical/20 bg-ds-critical-bg p-3 text-xs text-ds-critical">Failed to load patterns: {String(error)}</div>}
|
||||
{visible.length === 0 ? <div className="py-16 text-center text-sm text-ds-text-3">No deterministic patterns observed yet.</div> : <div className="space-y-3">{visible.map((pattern) => <div key={pattern.id} className="rounded-xl border border-ds-accent/15 bg-ds-surface-2 p-4">
|
||||
<div className="flex flex-wrap items-center gap-3"><span className="rounded bg-ds-accent-bg px-2 py-1 text-[10px] font-bold uppercase text-ds-accent">{pattern.service}</span><span className="text-xs text-ds-text-2">{pattern.kind}</span><span className="text-xs text-ds-text-2">{pattern.issueCount} exact issues · {pattern.occurrenceCount} occurrences</span><span className="ml-auto text-[10px] text-ds-text-3">last seen {formatRelativeTime(pattern.lastSeen / 1e6)}</span></div>
|
||||
<div className="mt-3 break-all font-mono text-[10px] text-ds-text-3">{pattern.signature.split("\u0000").join(" · ")}</div>
|
||||
<PatternIssues pattern={pattern} issues={issues} similar={similar} review={review} />
|
||||
</div>)}</div>}
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,9 @@ import { formatRelativeTime } from "../lib/timeUtils"
|
||||
import type { RolloutImpact } from "../lib/types"
|
||||
|
||||
const phaseStyle: Record<string, { className: string; Icon: typeof Activity }> = {
|
||||
completed: { className: "text-emerald-300 bg-emerald-400/10 border-emerald-400/20", Icon: CheckCircle2 },
|
||||
failed: { className: "text-rose-300 bg-rose-400/10 border-rose-400/20", Icon: XCircle },
|
||||
progressing: { className: "text-amber-300 bg-amber-400/10 border-amber-400/20", Icon: Activity },
|
||||
completed: { className: "text-ds-ok bg-ds-ok/15 border-ds-ok/20", Icon: CheckCircle2 },
|
||||
failed: { className: "text-ds-critical bg-ds-critical-bg border-ds-critical/20", Icon: XCircle },
|
||||
progressing: { className: "text-ds-warn bg-ds-warn-bg border-ds-warn/20", Icon: Activity },
|
||||
}
|
||||
|
||||
export function RolloutsVisualizer() {
|
||||
@@ -27,21 +27,21 @@ export function RolloutsVisualizer() {
|
||||
catch (cause) { setImpactError(cause instanceof Error ? cause.message : String(cause)) }
|
||||
}
|
||||
|
||||
if (loading && rollouts.length === 0) return <div className="py-16 text-center text-sm text-slate-500">Loading cluster rollouts…</div>
|
||||
if (loading && rollouts.length === 0) return <div className="py-16 text-center text-sm text-ds-text-3">Loading cluster rollouts…</div>
|
||||
return <div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-3 border-b border-white/5 pb-3">
|
||||
<div><div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-slate-500"><CloudCog className="h-3 w-3 text-sky-400" /> Cluster Rollouts</div><div className="mt-1 text-xs text-slate-500">Read-only Kubernetes deployment state with optional Prometheus impact data.</div></div>
|
||||
<button onClick={() => void refresh()} className="ml-auto inline-flex items-center gap-2 rounded-lg border border-white/10 px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-slate-400 hover:text-white"><RefreshCw className="h-3 w-3" /> Refresh</button>
|
||||
<div className="flex flex-wrap items-center gap-3 border-b border-ds-line pb-3">
|
||||
<div><div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-ds-text-3"><CloudCog className="h-3 w-3 text-ds-accent" /> Cluster Rollouts</div><div className="mt-1 text-xs text-ds-text-3">Read-only Kubernetes deployment state with optional Prometheus impact data.</div></div>
|
||||
<button onClick={() => void refresh()} className="ml-auto inline-flex items-center gap-2 rounded-lg border border-ds-line px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-ds-text-2 hover:text-ds-text"><RefreshCw className="h-3 w-3" /> Refresh</button>
|
||||
</div>
|
||||
{error && <div className="rounded-lg border border-amber-400/20 bg-amber-400/5 p-3 text-xs text-amber-200">Rollout watcher unavailable: {String(error)}</div>}
|
||||
{impactError && <div className="rounded-lg border border-rose-400/20 bg-rose-400/5 p-3 text-xs text-rose-300">Impact query failed: {impactError}</div>}
|
||||
{rollouts.length === 0 ? <div className="py-16 text-center text-sm text-slate-500">No rollout data. Enable the Kubernetes watcher and grant its read-only RBAC permissions.</div> : <div className="space-y-2">{rollouts.map((rollout) => {
|
||||
{error && <div className="rounded-lg border border-ds-warn/20 bg-ds-warn-bg p-3 text-xs text-ds-warn">Rollout watcher unavailable: {String(error)}</div>}
|
||||
{impactError && <div className="rounded-lg border border-ds-critical/20 bg-ds-critical-bg p-3 text-xs text-ds-critical">Impact query failed: {impactError}</div>}
|
||||
{rollouts.length === 0 ? <div className="py-16 text-center text-sm text-ds-text-3">No rollout data. Enable the Kubernetes watcher and grant its read-only RBAC permissions.</div> : <div className="space-y-2">{rollouts.map((rollout) => {
|
||||
const status = phaseStyle[rollout.phase] ?? phaseStyle.progressing
|
||||
const Icon = status.Icon
|
||||
const data = impactData[rollout.id]
|
||||
return <div key={rollout.id} className="rounded-xl border border-white/10 bg-slate-950/40 p-4">
|
||||
<button onClick={() => void select(rollout.id)} className="w-full text-left"><div className="flex flex-wrap items-center gap-3"><span className={`inline-flex items-center gap-1 rounded border px-2 py-1 text-[10px] font-bold uppercase ${status.className}`}><Icon className="h-3 w-3" /> {rollout.phase}</span><span className="font-mono text-xs text-slate-200">{rollout.namespace}/{rollout.workload}</span><span className="text-xs text-slate-400">revision {rollout.revision || "unknown"}</span><span className="ml-auto text-[10px] text-slate-500">{formatRelativeTime(rollout.lastSeen / 1e6)}</span></div><div className="mt-3 grid gap-2 text-[11px] text-slate-400 sm:grid-cols-2 lg:grid-cols-4"><span>release: <b className="text-slate-200">{rollout.release || "unlabeled"}</b></span><span>image: <b className="font-mono text-slate-200">{rollout.image || "unknown"}</b></span><span>replicas: <b className="text-slate-200">{rollout.availableReplicas}/{rollout.desiredReplicas} ready</b></span><span>reason: <b className="text-slate-200">{rollout.reason || "unknown"}</b></span></div></button>
|
||||
{selected === rollout.id && <div className="mt-4 border-t border-white/5 pt-3 text-xs text-slate-400"><div>Started {formatRelativeTime(rollout.startedAt / 1e6)}{rollout.completedAt ? ` · completed ${formatRelativeTime(rollout.completedAt / 1e6)}` : ""}</div><div className="mt-2 text-slate-500">{rollout.message || "No rollout message reported."}</div>{data && <div className="mt-3 rounded-lg bg-black/20 p-3"><div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-slate-500"><Activity className="h-3 w-3" /> Prometheus error-rate points</div>{!data.configured ? <div className="mt-2 text-slate-600">Prometheus is not configured.</div> : data.points.length === 0 ? <div className="mt-2 text-slate-600">No matching Prometheus series.</div> : <div className="mt-2 flex flex-wrap gap-2">{data.points.slice(-12).map((point) => <span key={`${point.timestamp}-${point.value}`} className="rounded bg-sky-400/10 px-2 py-1 font-mono text-[10px] text-sky-300">{new Date(point.timestamp * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}: {point.value}</span>)}</div>}</div>}</div>}
|
||||
return <div key={rollout.id} className="rounded-xl border border-ds-line bg-ds-surface-2 p-4">
|
||||
<button onClick={() => void select(rollout.id)} className="w-full text-left"><div className="flex flex-wrap items-center gap-3"><span className={`inline-flex items-center gap-1 rounded border px-2 py-1 text-[10px] font-bold uppercase ${status.className}`}><Icon className="h-3 w-3" /> {rollout.phase}</span><span className="font-mono text-xs text-ds-text">{rollout.namespace}/{rollout.workload}</span><span className="text-xs text-ds-text-2">revision {rollout.revision || "unknown"}</span><span className="ml-auto text-[10px] text-ds-text-3">{formatRelativeTime(rollout.lastSeen / 1e6)}</span></div><div className="mt-3 grid gap-2 text-[11px] text-ds-text-2 sm:grid-cols-2 lg:grid-cols-4"><span>release: <b className="text-ds-text">{rollout.release || "unlabeled"}</b></span><span>image: <b className="font-mono text-ds-text">{rollout.image || "unknown"}</b></span><span>replicas: <b className="text-ds-text">{rollout.availableReplicas}/{rollout.desiredReplicas} ready</b></span><span>reason: <b className="text-ds-text">{rollout.reason || "unknown"}</b></span></div></button>
|
||||
{selected === rollout.id && <div className="mt-4 border-t border-ds-line pt-3 text-xs text-ds-text-2"><div>Started {formatRelativeTime(rollout.startedAt / 1e6)}{rollout.completedAt ? ` · completed ${formatRelativeTime(rollout.completedAt / 1e6)}` : ""}</div><div className="mt-2 text-ds-text-3">{rollout.message || "No rollout message reported."}</div>{data && <div className="mt-3 rounded-lg bg-ds-surface-2 p-3"><div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-ds-text-3"><Activity className="h-3 w-3" /> Prometheus error-rate points</div>{!data.configured ? <div className="mt-2 text-ds-text-3">Prometheus is not configured.</div> : data.points.length === 0 ? <div className="mt-2 text-ds-text-3">No matching Prometheus series.</div> : <div className="mt-2 flex flex-wrap gap-2">{data.points.slice(-12).map((point) => <span key={`${point.timestamp}-${point.value}`} className="rounded bg-ds-accent-bg px-2 py-1 font-mono text-[10px] text-ds-accent">{new Date(point.timestamp * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}: {point.value}</span>)}</div>}</div>}</div>}
|
||||
</div>
|
||||
})}</div>}
|
||||
</div>
|
||||
|
||||
@@ -62,10 +62,10 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
|
||||
if (searchTimeRange !== "all") {
|
||||
const msMap: Record<string, number> = {
|
||||
"1h": 60 * 60 * 1000,
|
||||
"6h": 6 * 60 * 60 * 1000,
|
||||
"24h": 24 * 60 * 60 * 1000,
|
||||
"7d": 7 * 24 * 60 * 60 * 1000,
|
||||
"1h": 60 * 60 * 1000,
|
||||
"6h": 6 * 60 * 60 * 1000,
|
||||
"24h": 24 * 60 * 60 * 1000,
|
||||
"7d": 7 * 24 * 60 * 60 * 1000,
|
||||
};
|
||||
const ms = msMap[searchTimeRange] || 24 * 60 * 60 * 1000;
|
||||
const sinceNano = BigInt(Date.now() - ms) * 1_000_000n;
|
||||
@@ -143,18 +143,18 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
return (
|
||||
<div className="space-y-8 max-w-6xl font-body">
|
||||
{/* Top Banner */}
|
||||
<div className="rounded-2xl border border-sky-500/20 bg-gradient-to-r from-sky-950/40 via-slate-900/60 to-slate-950/80 p-5 shadow-xl backdrop-blur-md">
|
||||
<div className="rounded-2xl border border-ds-accent/20 bg-ds-surface-2 p-5 shadow-xl backdrop-blur-md">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-sky-500/20 text-sky-400">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-ds-accent-bg text-ds-accent">
|
||||
<Radar className="h-4 w-4 animate-pulse" />
|
||||
</div>
|
||||
<h2 className="text-base font-bold text-white tracking-tight">
|
||||
<h2 className="text-base font-bold text-ds-text tracking-tight">
|
||||
Archive Search & Live Signal Trap
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 max-w-2xl leading-relaxed">
|
||||
<p className="text-xs text-ds-text-2 max-w-2xl leading-relaxed">
|
||||
Expose deep telemetry data beyond the browser buffer. Query full ClickHouse & persistent storage logs/traces, or arm an OTLP signal trap to catch anomalies as they hit the server.
|
||||
</p>
|
||||
</div>
|
||||
@@ -162,22 +162,22 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* SECTION 1: Live Signal Trap */}
|
||||
<section className="rounded-2xl border border-white/10 bg-slate-950/40 p-5 shadow-xl space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-white/5 pb-3">
|
||||
<section className="rounded-2xl border border-ds-line bg-ds-surface-2 p-5 shadow-xl space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-ds-line pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Radar className="h-4 w-4 text-violet-400" />
|
||||
<h3 className="text-xs font-black uppercase tracking-[0.2em] text-slate-400">
|
||||
<Radar className="h-4 w-4 text-ds-accent" />
|
||||
<h3 className="text-xs font-black uppercase tracking-[0.2em] text-ds-text-2">
|
||||
Live Signal Trap (/api/history/wait)
|
||||
</h3>
|
||||
</div>
|
||||
<span className="text-[11px] font-mono text-slate-500">
|
||||
<span className="text-[11px] font-mono text-ds-text-3">
|
||||
Long-poll subscriber for active debugging
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-3 items-center">
|
||||
<div className="md:col-span-6">
|
||||
<label className="block font-mono text-[10px] text-slate-400 uppercase tracking-wider mb-1">
|
||||
<label className="block font-mono text-[10px] text-ds-text-2 uppercase tracking-wider mb-1">
|
||||
Target Keyword / Pattern / Trace ID
|
||||
</label>
|
||||
<div className="relative flex items-center">
|
||||
@@ -187,20 +187,20 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
onChange={(e) => setWatchQuery(e.target.value)}
|
||||
placeholder="e.g. 'error', '500', 'db_timeout', or service name..."
|
||||
disabled={isWatching}
|
||||
className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white placeholder-slate-500 focus:border-violet-500/50 focus:outline-none disabled:opacity-50 font-mono"
|
||||
className="w-full h-9 rounded-xl border border-ds-line bg-ds-surface-2 px-3 text-xs text-ds-text placeholder:text-ds-text-3 focus:border-ds-accent/50 focus:outline-none disabled:opacity-50 font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-3">
|
||||
<label className="block font-mono text-[10px] text-slate-400 uppercase tracking-wider mb-1">
|
||||
<label className="block font-mono text-[10px] text-ds-text-2 uppercase tracking-wider mb-1">
|
||||
Signal Type
|
||||
</label>
|
||||
<select
|
||||
value={watchType}
|
||||
onChange={(e) => setWatchType(e.target.value)}
|
||||
disabled={isWatching}
|
||||
className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white focus:border-violet-500/50 focus:outline-none disabled:opacity-50 font-mono"
|
||||
className="w-full h-9 rounded-xl border border-ds-line bg-ds-surface-2 px-3 text-xs text-ds-text focus:border-ds-accent/50 focus:outline-none disabled:opacity-50 font-mono"
|
||||
>
|
||||
<option value="">Any Telemetry</option>
|
||||
<option value="traces">Traces Only</option>
|
||||
@@ -211,14 +211,14 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
|
||||
<div className="md:col-span-3 flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<label className="block font-mono text-[10px] text-slate-400 uppercase tracking-wider mb-1">
|
||||
<label className="block font-mono text-[10px] text-ds-text-2 uppercase tracking-wider mb-1">
|
||||
Timeout
|
||||
</label>
|
||||
<select
|
||||
value={watchTimeout}
|
||||
onChange={(e) => setWatchTimeout(Number(e.target.value))}
|
||||
disabled={isWatching}
|
||||
className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white focus:border-violet-500/50 focus:outline-none disabled:opacity-50 font-mono"
|
||||
className="w-full h-9 rounded-xl border border-ds-line bg-ds-surface-2 px-3 text-xs text-ds-text focus:border-ds-accent/50 focus:outline-none disabled:opacity-50 font-mono"
|
||||
>
|
||||
<option value={15}>15 seconds</option>
|
||||
<option value={30}>30 seconds</option>
|
||||
@@ -230,7 +230,7 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={cancelWatcher}
|
||||
className="h-9 px-4 rounded-xl bg-rose-500 hover:bg-rose-600 text-white font-mono text-xs font-bold transition-colors flex items-center gap-1.5 shadow-lg shadow-rose-500/20"
|
||||
className="h-9 px-4 rounded-xl bg-ds-critical hover:bg-ds-critical text-ds-text font-mono text-xs font-bold transition-colors flex items-center gap-1.5 shadow-lg "
|
||||
>
|
||||
<Square className="h-3.5 w-3.5 fill-current" />
|
||||
<span>Cancel</span>
|
||||
@@ -239,7 +239,7 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={startWatcher}
|
||||
className="h-9 px-4 rounded-xl bg-violet-500 hover:bg-violet-600 text-white font-mono text-xs font-bold transition-colors flex items-center gap-1.5 shadow-lg shadow-violet-500/20"
|
||||
className="h-9 px-4 rounded-xl bg-ds-accent hover:bg-ds-accent text-ds-text font-mono text-xs font-bold transition-colors flex items-center gap-1.5 shadow-lg "
|
||||
>
|
||||
<Play className="h-3.5 w-3.5 fill-current" />
|
||||
<span>Arm Trap</span>
|
||||
@@ -253,16 +253,16 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
<div
|
||||
className={`rounded-xl p-3 text-xs font-mono flex items-center justify-between border ${
|
||||
isWatching
|
||||
? "bg-violet-500/10 border-violet-500/30 text-violet-300 animate-pulse"
|
||||
? "bg-ds-accent-bg border-ds-accent/30 text-ds-accent animate-pulse"
|
||||
: caughtSignal
|
||||
? "bg-emerald-500/10 border-emerald-500/30 text-emerald-300"
|
||||
: "bg-slate-900/60 border-white/5 text-slate-400"
|
||||
? "bg-ds-ok/15 border-ds-ok/30 text-ds-ok"
|
||||
: "bg-ds-surface-2 border-ds-line text-ds-text-2"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`h-2 w-2 rounded-full ${
|
||||
isWatching ? "bg-violet-400 animate-ping" : caughtSignal ? "bg-emerald-400" : "bg-slate-500"
|
||||
isWatching ? "bg-ds-accent animate-ping" : caughtSignal ? "bg-ds-ok" : "bg-ds-fill"
|
||||
}`}
|
||||
/>
|
||||
<span>{watchStatus}</span>
|
||||
@@ -272,13 +272,13 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
|
||||
{/* Captured Signal Preview */}
|
||||
{caughtSignal && (
|
||||
<div className="rounded-xl border border-emerald-500/20 bg-black/40 p-4 space-y-2 animate-in fade-in duration-300">
|
||||
<div className="rounded-xl border border-ds-ok/20 bg-ds-surface-2 p-4 space-y-2 animate-in fade-in duration-300">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 px-2 py-0.5 text-[10px] font-bold uppercase">
|
||||
<span className="rounded bg-ds-ok/15 text-ds-ok border border-ds-ok/40 px-2 py-0.5 text-[10px] font-bold uppercase">
|
||||
{caughtSignal.type}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-slate-300">
|
||||
<span className="font-mono text-xs text-ds-text-2">
|
||||
Captured at {new Date(caughtSignal.timestamp / 1_000_000).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
@@ -292,7 +292,7 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
setActiveTab("tab1");
|
||||
}
|
||||
}}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-sky-500 px-3 py-1 text-[11px] font-bold text-slate-950 hover:bg-sky-400 transition-colors"
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-ds-accent px-3 py-1 text-[11px] font-bold text-ds-on-accent hover:bg-ds-accent transition-colors"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
Jump to Captured Trace
|
||||
@@ -300,7 +300,7 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<pre className="max-h-60 overflow-auto rounded-lg border border-white/5 bg-slate-950 p-3 font-mono text-[11px] text-slate-300">
|
||||
<pre className="max-h-60 overflow-auto rounded-lg border border-ds-line bg-ds-bg/95 p-3 font-mono text-[11px] text-ds-text-2">
|
||||
<code>{JSON.stringify(caughtSignal.data, null, 2)}</code>
|
||||
</pre>
|
||||
</div>
|
||||
@@ -308,15 +308,15 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
</section>
|
||||
|
||||
{/* SECTION 2: Persistent Storage Search */}
|
||||
<section className="rounded-2xl border border-white/10 bg-slate-950/40 p-5 shadow-xl space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-white/5 pb-3">
|
||||
<section className="rounded-2xl border border-ds-line bg-ds-surface-2 p-5 shadow-xl space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-ds-line pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Search className="h-4 w-4 text-sky-400" />
|
||||
<h3 className="text-xs font-black uppercase tracking-[0.2em] text-slate-400">
|
||||
<Search className="h-4 w-4 text-ds-accent" />
|
||||
<h3 className="text-xs font-black uppercase tracking-[0.2em] text-ds-text-2">
|
||||
Query Persistent Telemetry Store (/api/history/search)
|
||||
</h3>
|
||||
</div>
|
||||
<span className="text-[11px] font-mono text-slate-500">
|
||||
<span className="text-[11px] font-mono text-ds-text-3">
|
||||
Searches full database history beyond active memory
|
||||
</span>
|
||||
</div>
|
||||
@@ -324,7 +324,7 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
<form onSubmit={handleRunSearch} className="space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-3">
|
||||
<div className="md:col-span-4">
|
||||
<label className="block font-mono text-[10px] text-slate-400 uppercase tracking-wider mb-1">
|
||||
<label className="block font-mono text-[10px] text-ds-text-2 uppercase tracking-wider mb-1">
|
||||
Search Query
|
||||
</label>
|
||||
<input
|
||||
@@ -332,18 +332,18 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Free-text substring in telemetry JSON..."
|
||||
className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white placeholder-slate-500 focus:border-sky-500/50 focus:outline-none font-mono"
|
||||
className="w-full h-9 rounded-xl border border-ds-line bg-ds-surface-2 px-3 text-xs text-ds-text placeholder:text-ds-text-3 focus:border-ds-accent/50 focus:outline-none font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block font-mono text-[10px] text-slate-400 uppercase tracking-wider mb-1">
|
||||
<label className="block font-mono text-[10px] text-ds-text-2 uppercase tracking-wider mb-1">
|
||||
Type
|
||||
</label>
|
||||
<select
|
||||
value={searchType}
|
||||
onChange={(e) => setSearchType(e.target.value)}
|
||||
className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white focus:border-sky-500/50 focus:outline-none font-mono"
|
||||
className="w-full h-9 rounded-xl border border-ds-line bg-ds-surface-2 px-3 text-xs text-ds-text focus:border-ds-accent/50 focus:outline-none font-mono"
|
||||
>
|
||||
<option value="">All Types</option>
|
||||
<option value="traces">Traces</option>
|
||||
@@ -353,13 +353,13 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block font-mono text-[10px] text-slate-400 uppercase tracking-wider mb-1">
|
||||
<label className="block font-mono text-[10px] text-ds-text-2 uppercase tracking-wider mb-1">
|
||||
Time Window
|
||||
</label>
|
||||
<select
|
||||
value={searchTimeRange}
|
||||
onChange={(e) => setSearchTimeRange(e.target.value)}
|
||||
className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white focus:border-sky-500/50 focus:outline-none font-mono"
|
||||
className="w-full h-9 rounded-xl border border-ds-line bg-ds-surface-2 px-3 text-xs text-ds-text focus:border-ds-accent/50 focus:outline-none font-mono"
|
||||
>
|
||||
<option value="1h">Last 1 Hour</option>
|
||||
<option value="6h">Last 6 Hours</option>
|
||||
@@ -370,13 +370,13 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block font-mono text-[10px] text-slate-400 uppercase tracking-wider mb-1">
|
||||
<label className="block font-mono text-[10px] text-ds-text-2 uppercase tracking-wider mb-1">
|
||||
Limit
|
||||
</label>
|
||||
<select
|
||||
value={searchLimit}
|
||||
onChange={(e) => setSearchLimit(Number(e.target.value))}
|
||||
className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white focus:border-sky-500/50 focus:outline-none font-mono"
|
||||
className="w-full h-9 rounded-xl border border-ds-line bg-ds-surface-2 px-3 text-xs text-ds-text focus:border-ds-accent/50 focus:outline-none font-mono"
|
||||
>
|
||||
<option value={50}>50 entries</option>
|
||||
<option value={100}>100 entries</option>
|
||||
@@ -389,7 +389,7 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={searchLoading}
|
||||
className="w-full h-9 rounded-xl bg-sky-500 hover:bg-sky-400 text-slate-950 font-mono text-xs font-bold transition-colors flex items-center justify-center gap-1.5 shadow-lg shadow-sky-500/20 disabled:opacity-50"
|
||||
className="w-full h-9 rounded-xl bg-ds-accent hover:bg-ds-accent text-ds-on-accent font-mono text-xs font-bold transition-colors flex items-center justify-center gap-1.5 shadow-lg disabled:opacity-50"
|
||||
>
|
||||
<Search className="h-3.5 w-3.5" />
|
||||
<span>{searchLoading ? "Searching..." : "Search"}</span>
|
||||
@@ -400,7 +400,7 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
</form>
|
||||
|
||||
{searchError && (
|
||||
<div className="rounded-xl border border-rose-500/20 bg-rose-500/10 p-3 text-xs font-mono text-rose-300">
|
||||
<div className="rounded-xl border border-ds-critical/20 bg-ds-critical-bg p-3 text-xs font-mono text-ds-critical">
|
||||
{searchError}
|
||||
</div>
|
||||
)}
|
||||
@@ -408,12 +408,12 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
{/* Results List */}
|
||||
{searchResults !== null && (
|
||||
<div className="space-y-3 pt-2">
|
||||
<div className="flex items-center justify-between text-[11px] font-mono text-slate-400 border-b border-white/5 pb-2">
|
||||
<div className="flex items-center justify-between text-[11px] font-mono text-ds-text-2 border-b border-ds-line pb-2">
|
||||
<span>Found {searchResults.length} persistent storage matches</span>
|
||||
</div>
|
||||
|
||||
{searchResults.length === 0 ? (
|
||||
<div className="py-12 text-center text-sm text-slate-500 font-mono">
|
||||
<div className="py-12 text-center text-sm text-ds-text-3 font-mono">
|
||||
No telemetry entries matched your search criteria in the selected time range.
|
||||
</div>
|
||||
) : (
|
||||
@@ -428,7 +428,7 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="rounded-xl border border-white/5 bg-black/30 p-3 text-xs font-mono transition-colors hover:border-white/15 space-y-2"
|
||||
className="rounded-xl border border-ds-line bg-ds-surface-2 p-3 text-xs font-mono transition-colors hover:border-ds-line-strong space-y-2"
|
||||
>
|
||||
<div
|
||||
onClick={() => setExpandedIndex(isExpanded ? null : idx)}
|
||||
@@ -438,17 +438,17 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
<span
|
||||
className={`rounded px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wider ${
|
||||
entry.type === "traces"
|
||||
? "bg-sky-500/20 text-sky-300 border border-sky-500/30"
|
||||
? "bg-ds-accent-bg text-ds-accent border border-ds-accent/30"
|
||||
: entry.type === "logs"
|
||||
? "bg-amber-500/20 text-amber-300 border border-amber-500/30"
|
||||
: "bg-emerald-500/20 text-emerald-300 border border-emerald-500/30"
|
||||
? "bg-ds-warn-bg text-ds-warn border border-ds-warn/30"
|
||||
: "bg-ds-ok/15 text-ds-ok border border-ds-ok/30"
|
||||
}`}
|
||||
>
|
||||
{entry.type}
|
||||
</span>
|
||||
<span className="text-slate-400 text-[11px]">{timeFormatted}</span>
|
||||
<span className="text-ds-text-2 text-[11px]">{timeFormatted}</span>
|
||||
{traceId && (
|
||||
<span className="text-sky-400 font-bold">
|
||||
<span className="text-ds-accent font-bold">
|
||||
trace:{traceId.slice(0, 8)}...
|
||||
</span>
|
||||
)}
|
||||
@@ -462,7 +462,7 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
setActiveTraceId(traceId);
|
||||
setActiveTab("tab1");
|
||||
}}
|
||||
className="rounded bg-sky-500/10 border border-sky-500/30 px-2 py-0.5 text-[10px] text-sky-300 hover:bg-sky-500/20 transition-colors"
|
||||
className="rounded bg-ds-accent-bg border border-ds-accent/30 px-2 py-0.5 text-[10px] text-ds-accent hover:bg-ds-accent-bg transition-colors"
|
||||
>
|
||||
Open Trace
|
||||
</button>
|
||||
@@ -472,11 +472,11 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(JSON.stringify(entry.data, null, 2), `res-${idx}`);
|
||||
}}
|
||||
className="p-1 text-slate-500 hover:text-white transition-colors"
|
||||
className="p-1 text-ds-text-3 hover:text-ds-text transition-colors"
|
||||
title="Copy JSON"
|
||||
>
|
||||
{copiedId === `res-${idx}` ? (
|
||||
<Check className="h-3 w-3 text-emerald-400" />
|
||||
<Check className="h-3 w-3 text-ds-ok" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
@@ -485,7 +485,7 @@ export const SearchAndWatcher: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<pre className="mt-2 max-h-56 overflow-auto rounded-lg border border-white/5 bg-slate-950 p-2.5 text-[10px] text-slate-300 leading-relaxed">
|
||||
<pre className="mt-2 max-h-56 overflow-auto rounded-lg border border-ds-line bg-ds-bg/95 p-2.5 text-[10px] text-ds-text-2 leading-relaxed">
|
||||
<code>{JSON.stringify(entry.data, null, 2)}</code>
|
||||
</pre>
|
||||
)}
|
||||
|
||||
@@ -211,7 +211,7 @@ export const ServiceGraph = ({
|
||||
|
||||
if (nodes.length === 0) {
|
||||
return (
|
||||
<div className="py-12 text-center text-sm text-slate-500">
|
||||
<div className="py-12 text-center text-sm text-ds-text-3">
|
||||
No service nodes available to visualize.
|
||||
</div>
|
||||
);
|
||||
@@ -220,25 +220,25 @@ export const ServiceGraph = ({
|
||||
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden rounded-2xl border border-white/10 bg-slate-950/60 p-4 shadow-2xl backdrop-blur-md">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-4 border-b border-white/5 pb-3">
|
||||
<div className="relative overflow-hidden rounded-2xl border border-ds-line bg-ds-surface-2 p-4 shadow-2xl backdrop-blur-md">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-4 border-b border-ds-line pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="h-4 w-4 text-sky-400" />
|
||||
<span className="text-xs font-bold uppercase tracking-widest text-slate-300">
|
||||
<Layers className="h-4 w-4 text-ds-accent" />
|
||||
<span className="text-xs font-bold uppercase tracking-widest text-ds-text-2">
|
||||
Service Call Graph ({nodes.length} Services, {edges.length} Cross-Service Links)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[10px] text-slate-400">
|
||||
<div className="flex items-center gap-2 text-[10px] text-ds-text-2">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-emerald-400" /> Healthy
|
||||
<span className="h-2 w-2 rounded-full bg-ds-ok" /> Healthy
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-rose-400" /> Errors
|
||||
<span className="h-2 w-2 rounded-full bg-ds-critical" /> Errors
|
||||
</span>
|
||||
{selectedService && (
|
||||
<button
|
||||
onClick={() => onSelectService?.(null)}
|
||||
className="ml-2 rounded-lg bg-sky-500/20 px-2 py-0.5 text-sky-300 hover:bg-sky-500/30 transition-all font-semibold"
|
||||
className="ml-2 rounded-lg bg-ds-accent-bg px-2 py-0.5 text-ds-accent hover:bg-ds-accent-bg transition-all font-semibold"
|
||||
>
|
||||
Clear filter ({selectedService})
|
||||
</button>
|
||||
@@ -263,7 +263,7 @@ export const ServiceGraph = ({
|
||||
markerHeight="6"
|
||||
orient="auto-start-reverse"
|
||||
>
|
||||
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#38bdf8" />
|
||||
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="var(--ds-accent)" />
|
||||
</marker>
|
||||
<marker
|
||||
id="arrow-error"
|
||||
@@ -274,15 +274,15 @@ export const ServiceGraph = ({
|
||||
markerHeight="6"
|
||||
orient="auto-start-reverse"
|
||||
>
|
||||
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#f43f5e" />
|
||||
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="var(--ds-critical)" />
|
||||
</marker>
|
||||
<linearGradient id="edge-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="#38bdf8" stopOpacity="0.8" />
|
||||
<stop offset="100%" stopColor="#818cf8" stopOpacity="0.8" />
|
||||
<stop offset="0%" stopColor="var(--ds-accent)" stopOpacity="0.8" />
|
||||
<stop offset="100%" stopColor="var(--ds-accent)" stopOpacity="0.8" />
|
||||
</linearGradient>
|
||||
<linearGradient id="edge-error-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="#f43f5e" stopOpacity="0.9" />
|
||||
<stop offset="100%" stopColor="#fb7185" stopOpacity="0.9" />
|
||||
<stop offset="0%" stopColor="var(--ds-critical)" stopOpacity="0.9" />
|
||||
<stop offset="100%" stopColor="var(--ds-critical)" stopOpacity="0.9" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
@@ -331,8 +331,8 @@ export const ServiceGraph = ({
|
||||
edge.hasError
|
||||
? "url(#edge-error-grad)"
|
||||
: isEdgeHighlighted
|
||||
? "#38bdf8"
|
||||
: "rgba(56, 189, 248, 0.4)"
|
||||
? "var(--ds-accent)"
|
||||
: "color-mix(in srgb, var(--ds-accent) 40%, transparent)"
|
||||
}
|
||||
strokeWidth={isEdgeHighlighted ? 3 : 2}
|
||||
strokeDasharray={edge.hasError ? "4 2" : undefined}
|
||||
@@ -344,15 +344,15 @@ export const ServiceGraph = ({
|
||||
width={70}
|
||||
height={22}
|
||||
rx={6}
|
||||
fill="#030712"
|
||||
stroke={edge.hasError ? "rgba(244,63,94,0.4)" : "rgba(255,255,255,0.1)"}
|
||||
fill="var(--ds-bg)"
|
||||
stroke={edge.hasError ? "color-mix(in srgb, var(--ds-critical) 40%, transparent)" : "var(--ds-line-strong)"}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
<text
|
||||
x={35}
|
||||
y={14}
|
||||
textAnchor="middle"
|
||||
fill={edge.hasError ? "#fb7185" : "#94a3b8"}
|
||||
fill={edge.hasError ? "var(--ds-critical)" : "var(--ds-text-2)"}
|
||||
fontSize={9}
|
||||
fontWeight="bold"
|
||||
fontFamily="monospace"
|
||||
@@ -384,18 +384,18 @@ export const ServiceGraph = ({
|
||||
width={200}
|
||||
height={84}
|
||||
rx={16}
|
||||
fill="#090d16"
|
||||
fill="var(--ds-bg)"
|
||||
stroke={
|
||||
isSelected
|
||||
? "#38bdf8"
|
||||
? "var(--ds-accent)"
|
||||
: hasError
|
||||
? "#f43f5e"
|
||||
? "var(--ds-critical)"
|
||||
: isHovered
|
||||
? "rgba(255, 255, 255, 0.3)"
|
||||
: "rgba(255, 255, 255, 0.1)"
|
||||
? "var(--ds-line-strong)"
|
||||
: "var(--ds-line-strong)"
|
||||
}
|
||||
strokeWidth={isSelected ? 2.5 : hasError ? 2 : 1}
|
||||
filter={isSelected ? "drop-shadow(0 0 12px rgba(56,189,248,0.4))" : undefined}
|
||||
filter={isSelected ? "drop-shadow(0 0 12px color-mix(in srgb, var(--ds-accent) 40%, transparent))" : undefined}
|
||||
/>
|
||||
|
||||
{/* Left Accent Stripe */}
|
||||
@@ -405,7 +405,7 @@ export const ServiceGraph = ({
|
||||
width={6}
|
||||
height={84}
|
||||
rx={3}
|
||||
fill={hasError ? "#f43f5e" : "#38bdf8"}
|
||||
fill={hasError ? "var(--ds-critical)" : "var(--ds-accent)"}
|
||||
/>
|
||||
|
||||
{/* Service Name Header */}
|
||||
@@ -413,7 +413,7 @@ export const ServiceGraph = ({
|
||||
<text
|
||||
x={0}
|
||||
y={0}
|
||||
fill="#ffffff"
|
||||
fill="var(--ds-text)"
|
||||
fontSize={13}
|
||||
fontWeight="bold"
|
||||
fontFamily="sans-serif"
|
||||
@@ -424,11 +424,11 @@ export const ServiceGraph = ({
|
||||
|
||||
{/* Spans Count & Latency Pills */}
|
||||
<g transform="translate(16, 44)">
|
||||
<text x={0} y={10} fill="#64748b" fontSize={10} fontFamily="monospace">
|
||||
Spans: <tspan fill="#e2e8f0">{node.spanCount}</tspan>
|
||||
<text x={0} y={10} fill="var(--ds-text-3)" fontSize={10} fontFamily="monospace">
|
||||
Spans: <tspan fill="var(--ds-text)">{node.spanCount}</tspan>
|
||||
</text>
|
||||
<text x={75} y={10} fill="#64748b" fontSize={10} fontFamily="monospace">
|
||||
Time: <tspan fill="#38bdf8">{formatDuration(node.totalDuration)}</tspan>
|
||||
<text x={75} y={10} fill="var(--ds-text-3)" fontSize={10} fontFamily="monospace">
|
||||
Time: <tspan fill="var(--ds-accent)">{formatDuration(node.totalDuration)}</tspan>
|
||||
</text>
|
||||
</g>
|
||||
|
||||
@@ -436,12 +436,12 @@ export const ServiceGraph = ({
|
||||
<g transform="translate(16, 68)">
|
||||
{hasError ? (
|
||||
<g>
|
||||
<rect width={80} height={16} rx={4} fill="rgba(244,63,94,0.15)" />
|
||||
<rect width={80} height={16} rx={4} fill="color-mix(in srgb, var(--ds-critical) 15%, transparent)" />
|
||||
<text
|
||||
x={40}
|
||||
y={11}
|
||||
textAnchor="middle"
|
||||
fill="#fb7185"
|
||||
fill="var(--ds-critical)"
|
||||
fontSize={9}
|
||||
fontWeight="bold"
|
||||
>
|
||||
@@ -450,12 +450,12 @@ export const ServiceGraph = ({
|
||||
</g>
|
||||
) : (
|
||||
<g>
|
||||
<rect width={60} height={16} rx={4} fill="rgba(16,185,129,0.15)" />
|
||||
<rect width={60} height={16} rx={4} fill="color-mix(in srgb, var(--ds-ok) 15%, transparent)" />
|
||||
<text
|
||||
x={30}
|
||||
y={11}
|
||||
textAnchor="middle"
|
||||
fill="#34d399"
|
||||
fill="var(--ds-ok)"
|
||||
fontSize={9}
|
||||
fontWeight="bold"
|
||||
>
|
||||
@@ -465,12 +465,12 @@ export const ServiceGraph = ({
|
||||
)}
|
||||
{isSelected && (
|
||||
<g transform="translate(90, 0)">
|
||||
<rect width={55} height={16} rx={4} fill="rgba(56,189,248,0.2)" />
|
||||
<rect width={55} height={16} rx={4} fill="color-mix(in srgb, var(--ds-accent) 20%, transparent)" />
|
||||
<text
|
||||
x={27.5}
|
||||
y={11}
|
||||
textAnchor="middle"
|
||||
fill="#38bdf8"
|
||||
fill="var(--ds-accent)"
|
||||
fontSize={9}
|
||||
fontWeight="bold"
|
||||
>
|
||||
@@ -486,7 +486,7 @@ export const ServiceGraph = ({
|
||||
</div>
|
||||
|
||||
{nodes.length === 1 && (
|
||||
<div className="mt-3 text-center text-xs text-slate-500">
|
||||
<div className="mt-3 text-center text-xs text-ds-text-3">
|
||||
This trace was executed inside a single service ({nodes[0].name}). No cross-service network calls were made.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -34,9 +34,9 @@ const CopyButton = ({ text }: { text: string }) => {
|
||||
// clipboard API unavailable; nothing to fall back to here
|
||||
}
|
||||
}}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-white/10 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-slate-400 hover:text-white hover:border-white/20 transition-all"
|
||||
className="flex items-center gap-1.5 rounded-lg border border-ds-line px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-ds-text-2 hover:text-ds-text hover:border-ds-line-strong transition-all"
|
||||
>
|
||||
{copied ? <Check className="h-3 w-3 text-emerald-400" /> : <Copy className="h-3 w-3" />}
|
||||
{copied ? <Check className="h-3 w-3 text-ds-ok" /> : <Copy className="h-3 w-3" />}
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</button>
|
||||
)
|
||||
@@ -46,22 +46,22 @@ export const SetupPanel = () => {
|
||||
const { setActiveTab } = useObservabilityData()
|
||||
return (
|
||||
<div className="space-y-6 max-w-3xl">
|
||||
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-5">
|
||||
<div className="rounded-xl border border-ds-line bg-ds-surface-2 p-5">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-slate-500">
|
||||
<Globe className="h-3.5 w-3.5 text-violet-400" />
|
||||
<span className="inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-ds-text-3">
|
||||
<Globe className="h-3.5 w-3.5 text-ds-accent" />
|
||||
Browser instrumentation (otel-rum.js)
|
||||
</span>
|
||||
<CopyButton text={scriptSnippet} />
|
||||
</div>
|
||||
<p className="mt-2 text-[12px] text-slate-400">
|
||||
<p className="mt-2 text-[12px] text-ds-text-2">
|
||||
Drop this into any page to capture uncaught errors, unhandled rejections and failed
|
||||
resource loads as OTLP logs. Errors show up here under the{" "}
|
||||
<span className="text-slate-200 font-semibold">Issues</span> tab tagged{" "}
|
||||
<span className="text-violet-300 font-semibold">Frontend Error</span>, and correlate with
|
||||
backend traces via <code className="text-sky-300">traceHeaders()</code>.
|
||||
<span className="text-ds-text font-semibold">Issues</span> tab tagged{" "}
|
||||
<span className="text-ds-accent font-semibold">Frontend Error</span>, and correlate with
|
||||
backend traces via <code className="text-ds-accent">traceHeaders()</code>.
|
||||
</p>
|
||||
<pre className="mt-3 overflow-x-auto rounded-lg border border-white/5 bg-black/50 p-3 text-[11px] leading-relaxed text-slate-300">
|
||||
<pre className="mt-3 overflow-x-auto rounded-lg border border-ds-line bg-ds-surface-2 p-3 text-[11px] leading-relaxed text-ds-text-2">
|
||||
<code>{scriptSnippet}</code>
|
||||
</pre>
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
@@ -69,16 +69,16 @@ export const SetupPanel = () => {
|
||||
href="/static/otel-rum.js"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-[11px] font-semibold text-sky-400 hover:text-sky-300"
|
||||
className="inline-flex items-center gap-1.5 text-[11px] font-semibold text-ds-accent hover:text-ds-accent"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
View raw script at /static/otel-rum.js
|
||||
</a>
|
||||
<span className="text-slate-600">•</span>
|
||||
<span className="text-ds-text-3">•</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("tab7")}
|
||||
className="inline-flex items-center gap-1.5 text-[11px] font-semibold text-indigo-400 hover:text-indigo-300"
|
||||
className="inline-flex items-center gap-1.5 text-[11px] font-semibold text-ds-accent hover:text-ds-accent"
|
||||
>
|
||||
<Globe className="h-3 w-3" />
|
||||
Open Frontend & RUM Explorer →
|
||||
@@ -86,12 +86,12 @@ export const SetupPanel = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-5">
|
||||
<span className="inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-slate-500">
|
||||
<FileText className="h-3.5 w-3.5 text-sky-400" />
|
||||
<div className="rounded-xl border border-ds-line bg-ds-surface-2 p-5">
|
||||
<span className="inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-ds-text-3">
|
||||
<FileText className="h-3.5 w-3.5 text-ds-accent" />
|
||||
Agent instructions
|
||||
</span>
|
||||
<p className="mt-2 text-[12px] text-slate-400">
|
||||
<p className="mt-2 text-[12px] text-ds-text-2">
|
||||
A single doc an AI coding agent can fetch before touching this API - endpoints, query
|
||||
params and response shapes for traces, logs, metrics and issues.
|
||||
</p>
|
||||
@@ -99,7 +99,7 @@ export const SetupPanel = () => {
|
||||
href="/agents_example.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mt-3 inline-flex items-center gap-1.5 text-[11px] font-semibold text-sky-400 hover:text-sky-300"
|
||||
className="mt-3 inline-flex items-center gap-1.5 text-[11px] font-semibold text-ds-accent hover:text-ds-accent"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
Open /agents_example.md
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ArrowRight, Brain, ChevronsDownUp, ChevronsUpDown, Copy, CopyPlus, Eraser, Filter, Link, ListFilter } from "lucide-react"
|
||||
import React, { useMemo, useState } from "react";
|
||||
import type { LogEntry, LogRecord, OtelAttribute } from "../lib/types";
|
||||
import { useObservabilityData } from "../hooks/useObservabilityData";
|
||||
@@ -164,97 +165,97 @@ export const StructuredLogTerminal: React.FC<{ logs: LogEntry[] }> = ({ logs })
|
||||
{/* LEFT PANEL: Faceted Filter Breakdown (3 Cols) */}
|
||||
<aside className="lg:col-span-3 flex flex-col gap-4">
|
||||
{/* Severity Breakdown Card */}
|
||||
<div className="bg-surface-container-lowest p-4 rounded-xl shadow-md flex flex-col gap-2 border border-surface-container/50">
|
||||
<div className="flex items-center justify-between pb-1 border-b border-surface-container/40">
|
||||
<span className="font-mono text-[10px] text-outline uppercase tracking-wider font-semibold">
|
||||
<div className="bg-ds-bg p-4 rounded-xl shadow-md flex flex-col gap-2 border border-ds-line">
|
||||
<div className="flex items-center justify-between pb-1 border-b border-ds-line">
|
||||
<span className="font-mono text-[10px] text-ds-text-3 uppercase tracking-wider font-semibold">
|
||||
Severity Breakdown
|
||||
</span>
|
||||
<span className="font-mono text-xs text-outline">{allRecords.length} Total</span>
|
||||
<span className="font-mono text-xs text-ds-text-3">{allRecords.length} Total</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
{/* FATAL */}
|
||||
<label className="flex items-center justify-between p-1.5 rounded-lg bg-surface-container-low hover:bg-surface-container transition-colors cursor-pointer group">
|
||||
<label className="flex items-center justify-between p-1.5 rounded-lg bg-ds-surface hover:bg-ds-surface-2 transition-colors cursor-pointer group">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSeverities.FATAL}
|
||||
onChange={() => toggleSeverity("FATAL")}
|
||||
className="rounded bg-surface-container-high border-outline text-error focus:ring-0"
|
||||
className="rounded bg-ds-fill border-ds-line-strong text-ds-critical focus:ring-0"
|
||||
/>
|
||||
<span className="font-mono text-xs text-error font-bold tracking-tight">FATAL</span>
|
||||
<span className="font-mono text-xs text-ds-critical font-bold tracking-tight">FATAL</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-error-container text-on-error-container font-semibold">
|
||||
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-ds-critical-bg text-ds-critical font-semibold">
|
||||
{severityCounts.FATAL}
|
||||
</span>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-error" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-ds-critical" />
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* ERROR */}
|
||||
<label className="flex items-center justify-between p-1.5 rounded-lg bg-surface-container-low hover:bg-surface-container transition-colors cursor-pointer group">
|
||||
<label className="flex items-center justify-between p-1.5 rounded-lg bg-ds-surface hover:bg-ds-surface-2 transition-colors cursor-pointer group">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSeverities.ERROR}
|
||||
onChange={() => toggleSeverity("ERROR")}
|
||||
className="rounded bg-surface-container-high border-outline text-error focus:ring-0"
|
||||
className="rounded bg-ds-fill border-ds-line-strong text-ds-critical focus:ring-0"
|
||||
/>
|
||||
<span className="font-mono text-xs text-error font-medium">ERROR</span>
|
||||
<span className="font-mono text-xs text-ds-critical font-medium">ERROR</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-error-container text-on-error-container font-semibold">
|
||||
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-ds-critical-bg text-ds-critical font-semibold">
|
||||
{severityCounts.ERROR}
|
||||
</span>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-error" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-ds-critical" />
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* WARN */}
|
||||
<label className="flex items-center justify-between p-1.5 rounded-lg bg-surface-container-low hover:bg-surface-container transition-colors cursor-pointer group">
|
||||
<label className="flex items-center justify-between p-1.5 rounded-lg bg-ds-surface hover:bg-ds-surface-2 transition-colors cursor-pointer group">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSeverities.WARN}
|
||||
onChange={() => toggleSeverity("WARN")}
|
||||
className="rounded bg-surface-container-high border-outline text-primary-fixed-dim focus:ring-0"
|
||||
className="rounded bg-ds-fill border-ds-line-strong text-ds-accent focus:ring-0"
|
||||
/>
|
||||
<span className="font-mono text-xs text-primary-fixed-dim">WARN</span>
|
||||
<span className="font-mono text-xs text-ds-accent">WARN</span>
|
||||
</div>
|
||||
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-surface-container-high text-on-surface font-medium">
|
||||
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-ds-fill text-ds-text font-medium">
|
||||
{severityCounts.WARN}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{/* INFO */}
|
||||
<label className="flex items-center justify-between p-1.5 rounded-lg bg-surface-container-low hover:bg-surface-container transition-colors cursor-pointer group">
|
||||
<label className="flex items-center justify-between p-1.5 rounded-lg bg-ds-surface hover:bg-ds-surface-2 transition-colors cursor-pointer group">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSeverities.INFO}
|
||||
onChange={() => toggleSeverity("INFO")}
|
||||
className="rounded bg-surface-container-high border-outline text-secondary focus:ring-0"
|
||||
className="rounded bg-ds-fill border-ds-line-strong text-ds-accent focus:ring-0"
|
||||
/>
|
||||
<span className="font-mono text-xs text-secondary">INFO</span>
|
||||
<span className="font-mono text-xs text-ds-accent">INFO</span>
|
||||
</div>
|
||||
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-surface-container-high text-on-surface-variant">
|
||||
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-ds-fill text-ds-text-2">
|
||||
{severityCounts.INFO}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{/* DEBUG */}
|
||||
<label className="flex items-center justify-between p-1.5 rounded-lg bg-surface-container-low hover:bg-surface-container transition-colors cursor-pointer group">
|
||||
<label className="flex items-center justify-between p-1.5 rounded-lg bg-ds-surface hover:bg-ds-surface-2 transition-colors cursor-pointer group">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSeverities.DEBUG}
|
||||
onChange={() => toggleSeverity("DEBUG")}
|
||||
className="rounded bg-surface-container-high border-outline text-outline focus:ring-0"
|
||||
className="rounded bg-ds-fill border-ds-line-strong text-ds-text-3 focus:ring-0"
|
||||
/>
|
||||
<span className="font-mono text-xs text-outline">DEBUG</span>
|
||||
<span className="font-mono text-xs text-ds-text-3">DEBUG</span>
|
||||
</div>
|
||||
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-surface-container-high text-outline">
|
||||
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-ds-fill text-ds-text-3">
|
||||
{severityCounts.DEBUG}
|
||||
</span>
|
||||
</label>
|
||||
@@ -262,12 +263,12 @@ export const StructuredLogTerminal: React.FC<{ logs: LogEntry[] }> = ({ logs })
|
||||
</div>
|
||||
|
||||
{/* Service Facet Card */}
|
||||
<div className="bg-surface-container-lowest p-4 rounded-xl shadow-md flex flex-col gap-2 border border-surface-container/50">
|
||||
<div className="flex items-center justify-between pb-1 border-b border-surface-container/40">
|
||||
<span className="font-mono text-[10px] text-outline uppercase tracking-wider font-semibold">
|
||||
<div className="bg-ds-bg p-4 rounded-xl shadow-md flex flex-col gap-2 border border-ds-line">
|
||||
<div className="flex items-center justify-between pb-1 border-b border-ds-line">
|
||||
<span className="font-mono text-[10px] text-ds-text-3 uppercase tracking-wider font-semibold">
|
||||
Service Name
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-outline text-[16px]">filter_alt</span>
|
||||
<Filter className="text-ds-text-3 h-[16px] w-[16px] shrink-0" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 font-mono text-xs">
|
||||
@@ -279,37 +280,37 @@ export const StructuredLogTerminal: React.FC<{ logs: LogEntry[] }> = ({ logs })
|
||||
onClick={() => toggleService(serviceName)}
|
||||
className={`flex items-center justify-between px-2.5 py-1.5 rounded-lg cursor-pointer transition-colors ${
|
||||
isSelected
|
||||
? "bg-surface-container text-primary-container font-semibold"
|
||||
: "hover:bg-surface-container text-on-surface-variant"
|
||||
? "bg-ds-surface-2 text-ds-accent font-semibold"
|
||||
: "hover:bg-ds-surface-2 text-ds-text-2"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full shrink-0 ${
|
||||
isSelected ? "bg-primary-container" : "bg-outline"
|
||||
isSelected ? "bg-ds-accent" : "bg-ds-fill"
|
||||
}`}
|
||||
/>
|
||||
<span className="truncate">{serviceName}</span>
|
||||
</div>
|
||||
<span className="font-mono text-[10px] text-outline">{count}</span>
|
||||
<span className="font-mono text-[10px] text-ds-text-3">{count}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{serviceCounts.length === 0 && (
|
||||
<p className="text-center py-2 text-outline text-xs italic">No services detected</p>
|
||||
<p className="text-center py-2 text-ds-text-3 text-xs italic">No services detected</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Root Cause Diagnostic Hint Box */}
|
||||
<div className="bg-surface-container-low p-4 rounded-xl shadow-md flex flex-col gap-1.5 border border-surface-container/60">
|
||||
<div className="flex items-center gap-2 text-primary-container">
|
||||
<span className="material-symbols-outlined text-[18px]">psychology</span>
|
||||
<span className="font-headline font-semibold text-xs leading-none">
|
||||
<div className="bg-ds-surface p-4 rounded-xl shadow-md flex flex-col gap-1.5 border border-ds-line">
|
||||
<div className="flex items-center gap-2 text-ds-accent">
|
||||
<Brain className="h-[18px] w-[18px] shrink-0" aria-hidden="true" />
|
||||
<span className="font-[family-name:var(--font-display)] font-semibold text-xs leading-none">
|
||||
Diagnostic Heuristic
|
||||
</span>
|
||||
</div>
|
||||
<p className="font-body text-xs text-on-surface-variant leading-relaxed">
|
||||
<p className="font-body text-xs text-ds-text-2 leading-relaxed">
|
||||
{errorHeuristic.text}
|
||||
</p>
|
||||
<div className="pt-1 flex items-center justify-between">
|
||||
@@ -317,67 +318,65 @@ export const StructuredLogTerminal: React.FC<{ logs: LogEntry[] }> = ({ logs })
|
||||
onClick={() => {
|
||||
setSelectedSeverities({ FATAL: true, ERROR: true, WARN: false, INFO: false, DEBUG: false });
|
||||
}}
|
||||
className="font-mono text-[10px] text-primary-container hover:underline uppercase flex items-center gap-1 font-semibold"
|
||||
className="font-mono text-[10px] text-ds-accent hover:underline uppercase flex items-center gap-1 font-semibold"
|
||||
>
|
||||
<span>Filter Only Error Signatures</span>
|
||||
<span className="material-symbols-outlined text-[14px]">arrow_forward</span>
|
||||
<ArrowRight className="h-[14px] w-[14px] shrink-0" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* RIGHT PANEL: High-Density Structured Live Log Stream (9 Cols) */}
|
||||
<main className="lg:col-span-9 flex flex-col bg-surface-container-lowest rounded-xl shadow-md overflow-hidden border border-surface-container/50">
|
||||
<main className="lg:col-span-9 flex flex-col bg-ds-bg rounded-xl shadow-md overflow-hidden border border-ds-line">
|
||||
{/* Stream Controls Bar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 p-2.5 bg-surface-container-low border-b border-surface-container/40">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 p-2.5 bg-ds-surface border-b border-ds-line">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-[240px]">
|
||||
<div className="relative flex-1">
|
||||
<span className="material-symbols-outlined absolute left-2.5 top-1/2 -translate-y-1/2 text-outline text-[16px] pointer-events-none">
|
||||
filter_list
|
||||
</span>
|
||||
<ListFilter className="absolute left-2.5 top-1/2 -translate-y-1/2 text-ds-text-3 h-[16px] w-[16px] pointer-events-none shrink-0" aria-hidden="true" />
|
||||
<input
|
||||
type="text"
|
||||
value={streamFilter}
|
||||
onChange={(e) => setStreamFilter(e.target.value)}
|
||||
placeholder="Filter current buffer (e.g. 'timeout', 'status:504')..."
|
||||
className="w-full h-7 pl-8 pr-3 rounded bg-surface-container text-on-surface font-mono text-xs placeholder:text-outline focus:outline-none focus:bg-surface-container-high transition-colors border border-surface-container/50"
|
||||
className="w-full h-7 pl-8 pr-3 rounded bg-ds-surface-2 text-ds-text font-mono text-xs placeholder:text-ds-text-3 focus:outline-none focus:bg-ds-fill transition-colors border border-ds-line"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center gap-1.5 font-mono text-xs text-outline cursor-pointer select-none">
|
||||
<label className="flex items-center gap-1.5 font-mono text-xs text-ds-text-3 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={wrapText}
|
||||
onChange={(e) => setWrapText(e.target.checked)}
|
||||
className="rounded bg-surface-container border-outline text-primary-container focus:ring-0"
|
||||
className="rounded bg-ds-surface-2 border-ds-line-strong text-ds-accent focus:ring-0"
|
||||
/>
|
||||
<span>Wrap Text</span>
|
||||
</label>
|
||||
|
||||
<div className="h-3 w-px bg-surface-container-high" />
|
||||
<div className="h-3 w-px bg-ds-fill" />
|
||||
|
||||
<button
|
||||
onClick={() => setCleared(true)}
|
||||
className="text-outline hover:text-on-surface p-1 rounded hover:bg-surface-container transition-colors"
|
||||
className="text-ds-text-3 hover:text-ds-text p-1 rounded hover:bg-ds-surface-2 transition-colors"
|
||||
title="Clear Buffer"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">clear_all</span>
|
||||
<Eraser className="h-[18px] w-[18px] shrink-0" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={copyLogStream}
|
||||
className="text-outline hover:text-on-surface p-1 rounded hover:bg-surface-container transition-colors"
|
||||
className="text-ds-text-3 hover:text-ds-text p-1 rounded hover:bg-ds-surface-2 transition-colors"
|
||||
title="Copy Visible Buffer"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">content_copy</span>
|
||||
<Copy className="h-[18px] w-[18px] shrink-0" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Log Terminal Headers */}
|
||||
<div className="grid grid-cols-12 px-4 py-1.5 bg-surface-container text-outline font-mono uppercase tracking-wider select-none text-[11px] border-b border-surface-container/60">
|
||||
<div className="grid grid-cols-12 px-4 py-1.5 bg-ds-surface-2 text-ds-text-3 font-mono uppercase tracking-wider select-none text-[11px] border-b border-ds-line">
|
||||
<div className="col-span-2">Timestamp (UTC)</div>
|
||||
<div className="col-span-1">Level</div>
|
||||
<div className="col-span-2">Service & Trace</div>
|
||||
@@ -385,9 +384,9 @@ export const StructuredLogTerminal: React.FC<{ logs: LogEntry[] }> = ({ logs })
|
||||
</div>
|
||||
|
||||
{/* Realtime Log Entries List */}
|
||||
<div className="divide-y divide-surface-container-high/40 font-mono text-xs max-h-[640px] overflow-y-auto select-text">
|
||||
<div className="divide-y divide-ds-line font-mono text-xs max-h-[640px] overflow-y-auto select-text">
|
||||
{visibleRecords.length === 0 ? (
|
||||
<div className="p-8 text-center text-outline text-xs">
|
||||
<div className="p-8 text-center text-ds-text-3 text-xs">
|
||||
{cleared
|
||||
? "Stream buffer cleared. Waiting for new telemetry records..."
|
||||
: "No log records match the current filters."}
|
||||
@@ -396,17 +395,17 @@ export const StructuredLogTerminal: React.FC<{ logs: LogEntry[] }> = ({ logs })
|
||||
visibleRecords.map((record) => {
|
||||
const num = record.severityNumber ?? 0;
|
||||
let sevLabel = record.severityText?.toUpperCase() || "INFO";
|
||||
let sevClass = "bg-surface-container-high text-secondary";
|
||||
let sevClass = "bg-ds-fill text-ds-accent";
|
||||
|
||||
if (num >= 21) {
|
||||
sevLabel = "FATAL";
|
||||
sevClass = "bg-error text-on-error font-extrabold";
|
||||
sevClass = "bg-ds-critical text-ds-critical font-extrabold";
|
||||
} else if (num >= 17) {
|
||||
sevLabel = "ERROR";
|
||||
sevClass = "bg-error-container text-on-error-container font-bold";
|
||||
sevClass = "bg-ds-critical-bg text-ds-critical font-bold";
|
||||
} else if (num >= 13) {
|
||||
sevLabel = "WARN";
|
||||
sevClass = "bg-surface-container-high text-primary-fixed-dim font-medium";
|
||||
sevClass = "bg-ds-fill text-ds-accent font-medium";
|
||||
}
|
||||
|
||||
const rowId = record.uid;
|
||||
@@ -417,13 +416,13 @@ export const StructuredLogTerminal: React.FC<{ logs: LogEntry[] }> = ({ logs })
|
||||
return (
|
||||
<div
|
||||
key={rowId}
|
||||
className="flex flex-col bg-surface-container-lowest hover:bg-surface-container/30 transition-colors"
|
||||
className="flex flex-col bg-ds-bg hover:bg-ds-surface-2 transition-colors"
|
||||
>
|
||||
<div
|
||||
onClick={() => setExpandedRowId(isExpanded ? null : rowId)}
|
||||
className="grid grid-cols-12 px-4 py-2 items-start cursor-pointer group"
|
||||
>
|
||||
<div className="col-span-2 text-outline whitespace-nowrap pt-0.5 text-[11px]">
|
||||
<div className="col-span-2 text-ds-text-3 whitespace-nowrap pt-0.5 text-[11px]">
|
||||
{isoTime}
|
||||
</div>
|
||||
|
||||
@@ -436,18 +435,16 @@ export const StructuredLogTerminal: React.FC<{ logs: LogEntry[] }> = ({ logs })
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 flex flex-col gap-0.5 min-w-0 pr-2">
|
||||
<span className="text-primary truncate font-medium">{record.serviceName}</span>
|
||||
<span className="text-ds-accent truncate font-medium">{record.serviceName}</span>
|
||||
{record.traceId && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="material-symbols-outlined text-[12px] text-primary-container">
|
||||
link
|
||||
</span>
|
||||
<Link className="h-[12px] w-[12px] text-ds-accent shrink-0" aria-hidden="true" />
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigateToTraceSpan(record.traceId!, record.spanId);
|
||||
}}
|
||||
className="text-primary-container hover:underline truncate font-mono text-[10px]"
|
||||
className="text-ds-accent hover:underline truncate font-mono text-[10px]"
|
||||
>
|
||||
{record.traceId.slice(0, 12)}
|
||||
</button>
|
||||
@@ -457,40 +454,38 @@ export const StructuredLogTerminal: React.FC<{ logs: LogEntry[] }> = ({ logs })
|
||||
|
||||
<div className="col-span-7 flex items-start justify-between min-w-0">
|
||||
<div
|
||||
className={`text-on-surface font-normal ${
|
||||
className={`text-ds-text font-normal ${
|
||||
wrapText ? "break-words" : "truncate"
|
||||
}`}
|
||||
>
|
||||
{num >= 17 && (
|
||||
<span className="text-error font-medium mr-1">Exception:</span>
|
||||
<span className="text-ds-critical font-medium mr-1">Exception:</span>
|
||||
)}
|
||||
{bodyMessage}
|
||||
</div>
|
||||
|
||||
<span className="material-symbols-outlined text-outline text-[16px] group-hover:text-on-surface transition-transform shrink-0 ml-2">
|
||||
{isExpanded ? "unfold_less" : "unfold_more"}
|
||||
</span>
|
||||
{isExpanded ? <ChevronsDownUp className="text-ds-text-3 h-[16px] w-[16px] group-hover:text-ds-text transition-transform shrink-0 ml-2 shrink-0" aria-hidden="true" /> : <ChevronsUpDown className="text-ds-text-3 h-[16px] w-[16px] group-hover:text-ds-text transition-transform shrink-0 ml-2 shrink-0" aria-hidden="true" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Structured Payload Drawer (Expanded) */}
|
||||
{isExpanded && (
|
||||
<div className="px-4 py-3 bg-surface-container-low text-xs flex flex-col gap-2 border-t border-surface-container">
|
||||
<div className="flex items-center justify-between border-b border-surface-container pb-1">
|
||||
<span className="font-mono text-[10px] text-outline uppercase tracking-wider">
|
||||
<div className="px-4 py-3 bg-ds-surface text-xs flex flex-col gap-2 border-t border-ds-line">
|
||||
<div className="flex items-center justify-between border-b border-ds-line pb-1">
|
||||
<span className="font-mono text-[10px] text-ds-text-3 uppercase tracking-wider">
|
||||
OpenTelemetry Attributes & Resource Scope
|
||||
</span>
|
||||
{record.traceId && (
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
`trace:${record.traceId} span:${record.spanId || ""}`,
|
||||
`trace:${record.traceId} span:${record.spanId || ""}`,
|
||||
);
|
||||
alert("Copied trace context to clipboard.");
|
||||
}}
|
||||
className="flex items-center gap-1 font-mono text-[10px] text-primary-container hover:text-primary transition-colors"
|
||||
className="flex items-center gap-1 font-mono text-[10px] text-ds-accent hover:text-ds-accent transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">copy_all</span>
|
||||
<CopyPlus className="h-[14px] w-[14px] shrink-0" aria-hidden="true" />
|
||||
<span>Copy Trace Context</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -499,33 +494,33 @@ export const StructuredLogTerminal: React.FC<{ logs: LogEntry[] }> = ({ logs })
|
||||
{/* Attributes Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-2 font-mono text-xs">
|
||||
{record.spanId && (
|
||||
<div className="p-1.5 rounded bg-surface-container flex flex-col">
|
||||
<span className="text-outline text-[10px] uppercase">span_id</span>
|
||||
<span className="text-primary-container truncate font-medium">
|
||||
<div className="p-1.5 rounded bg-ds-surface-2 flex flex-col">
|
||||
<span className="text-ds-text-3 text-[10px] uppercase">span_id</span>
|
||||
<span className="text-ds-accent truncate font-medium">
|
||||
{record.spanId}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{record.traceId && (
|
||||
<div className="p-1.5 rounded bg-surface-container flex flex-col">
|
||||
<span className="text-outline text-[10px] uppercase">trace_id</span>
|
||||
<span className="text-on-surface truncate">{record.traceId}</span>
|
||||
<div className="p-1.5 rounded bg-ds-surface-2 flex flex-col">
|
||||
<span className="text-ds-text-3 text-[10px] uppercase">trace_id</span>
|
||||
<span className="text-ds-text truncate">{record.traceId}</span>
|
||||
</div>
|
||||
)}
|
||||
{record.scope && (
|
||||
<div className="p-1.5 rounded bg-surface-container flex flex-col">
|
||||
<span className="text-outline text-[10px] uppercase">scope</span>
|
||||
<span className="text-secondary truncate">{record.scope}</span>
|
||||
<div className="p-1.5 rounded bg-ds-surface-2 flex flex-col">
|
||||
<span className="text-ds-text-3 text-[10px] uppercase">scope</span>
|
||||
<span className="text-ds-accent truncate">{record.scope}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-1.5 rounded bg-surface-container flex flex-col">
|
||||
<span className="text-outline text-[10px] uppercase">service.name</span>
|
||||
<span className="text-primary truncate">{record.serviceName}</span>
|
||||
<div className="p-1.5 rounded bg-ds-surface-2 flex flex-col">
|
||||
<span className="text-ds-text-3 text-[10px] uppercase">service.name</span>
|
||||
<span className="text-ds-accent truncate">{record.serviceName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Raw JSON block */}
|
||||
<pre className="p-2.5 rounded bg-surface-container text-on-surface overflow-x-auto text-[11px] leading-relaxed border border-surface-container-high/40">
|
||||
<pre className="p-2.5 rounded bg-ds-surface-2 text-ds-text overflow-x-auto text-[11px] leading-relaxed border border-ds-line">
|
||||
<code>
|
||||
{JSON.stringify(
|
||||
{
|
||||
@@ -562,18 +557,18 @@ export const StructuredLogTerminal: React.FC<{ logs: LogEntry[] }> = ({ logs })
|
||||
</div>
|
||||
|
||||
{/* Log Terminal Footer Status */}
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-surface-container-low text-outline font-mono text-xs border-t border-surface-container">
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-ds-surface text-ds-text-3 font-mono text-xs border-t border-ds-line">
|
||||
<div className="flex items-center gap-3">
|
||||
<span>
|
||||
Showing {visibleRecords.length} of {allRecords.length} matched events
|
||||
</span>
|
||||
<span className="h-3 w-px bg-surface-container-high" />
|
||||
<span className="h-3 w-px bg-ds-fill" />
|
||||
<span>Buffer: Healthy</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-secondary" />
|
||||
<span className="text-on-surface">Auto-scroll: LIVE STREAM</span>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-ds-accent" />
|
||||
<span className="text-ds-text">Auto-scroll: LIVE STREAM</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
+35
-164
@@ -2,20 +2,11 @@ import { useCallback, useState } from "react"
|
||||
import * as Tabs from "@radix-ui/react-tabs"
|
||||
import {
|
||||
Activity,
|
||||
AlertOctagon,
|
||||
LineChart,
|
||||
ScrollText,
|
||||
AlertTriangle,
|
||||
Info,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
TrendingUp,
|
||||
Wrench,
|
||||
Radar,
|
||||
Globe,
|
||||
GitBranch,
|
||||
CloudCog,
|
||||
} from "lucide-react"
|
||||
import type { LucideIcon } from "lucide-react"
|
||||
import { MetricsVisualizer } from "./MetricsVisualizer"
|
||||
@@ -39,7 +30,8 @@ import { RolloutsVisualizer } from "./RolloutsVisualizer"
|
||||
|
||||
type SeverityMeta = {
|
||||
baseLabel: string
|
||||
gradient: string
|
||||
bar: string
|
||||
badge: string
|
||||
ring: string
|
||||
icon: LucideIcon
|
||||
}
|
||||
@@ -47,34 +39,39 @@ type SeverityMeta = {
|
||||
const severityStyles: Record<string, SeverityMeta> = {
|
||||
ERROR: {
|
||||
baseLabel: "Error",
|
||||
gradient: "from-rose-500 via-red-500 to-orange-400",
|
||||
ring: "ring-rose-400/30",
|
||||
bar: "bg-ds-critical",
|
||||
badge: "bg-ds-critical-bg text-ds-critical",
|
||||
ring: "ring-ds-critical/40",
|
||||
icon: AlertTriangle,
|
||||
},
|
||||
WARN: {
|
||||
baseLabel: "Warning",
|
||||
gradient: "from-amber-400 via-orange-400 to-rose-400",
|
||||
ring: "ring-amber-300/30",
|
||||
bar: "bg-ds-warn",
|
||||
badge: "bg-ds-warn-bg text-ds-warn",
|
||||
ring: "ring-ds-warn/40",
|
||||
icon: AlertTriangle,
|
||||
},
|
||||
INFO: {
|
||||
baseLabel: "Info",
|
||||
gradient: "from-sky-500 via-blue-500 to-violet-500",
|
||||
ring: "ring-sky-400/30",
|
||||
bar: "bg-ds-accent",
|
||||
badge: "bg-ds-accent-bg text-ds-accent",
|
||||
ring: "ring-ds-accent/40",
|
||||
icon: Info,
|
||||
},
|
||||
DEBUG: {
|
||||
baseLabel: "Debug",
|
||||
gradient: "from-emerald-400 via-teal-400 to-cyan-400",
|
||||
ring: "ring-emerald-300/30",
|
||||
bar: "bg-ds-ok",
|
||||
badge: "bg-ds-ok/15 text-ds-ok",
|
||||
ring: "ring-ds-ok/40",
|
||||
icon: CheckCircle2,
|
||||
},
|
||||
}
|
||||
|
||||
const fallbackSeverity: SeverityMeta = {
|
||||
baseLabel: "Log",
|
||||
gradient: "from-slate-600 via-slate-500 to-slate-400",
|
||||
ring: "ring-white/20",
|
||||
bar: "bg-ds-line-strong",
|
||||
badge: "bg-ds-surface-2 text-ds-text-2",
|
||||
ring: "ring-ds-line-strong",
|
||||
icon: Info,
|
||||
}
|
||||
|
||||
@@ -159,17 +156,17 @@ export const LogEntryView = ({ log }: { log: LogEntry }) => {
|
||||
return (
|
||||
<div
|
||||
key={recordKey}
|
||||
className="group relative flex flex-col overflow-hidden rounded-xl border border-white/5 bg-slate-950/40 transition-all hover:border-white/10 hover:bg-slate-900/50"
|
||||
className="group relative flex flex-col overflow-hidden rounded-xl border border-ds-line bg-ds-surface-2 transition-all hover:border-ds-line hover:bg-ds-surface-2"
|
||||
>
|
||||
{/* The Main Log Row */}
|
||||
<div
|
||||
onClick={() => setExpandedId(isExpanded ? null : recordKey)}
|
||||
className="flex cursor-pointer flex-wrap items-center gap-x-4 gap-y-2 p-2.5 font-mono text-[12px]"
|
||||
>
|
||||
<div className={`absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b ${severity.gradient} opacity-40 transition-opacity group-hover:opacity-100`} />
|
||||
<div className={`absolute left-0 top-0 bottom-0 w-1 ${severity.bar} opacity-40 transition-opacity group-hover:opacity-100`} />
|
||||
|
||||
{/* Timestamp */}
|
||||
<span className="shrink-0 text-slate-500 whitespace-nowrap ml-1">
|
||||
<span className="shrink-0 text-ds-text-3 whitespace-nowrap ml-1">
|
||||
{formatTimestamp(record.timeUnixNano)}
|
||||
</span>
|
||||
|
||||
@@ -185,12 +182,12 @@ export const LogEntryView = ({ log }: { log: LogEntry }) => {
|
||||
})()}
|
||||
|
||||
{/* Severity Badge */}
|
||||
<span className={`shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider bg-gradient-to-r ${severity.gradient} text-slate-950`}>
|
||||
<span className={`shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider ${severity.badge}`}>
|
||||
{severity.display}
|
||||
</span>
|
||||
|
||||
{/* Log Message */}
|
||||
<span className="flex-1 min-w-[300px] break-all text-slate-200">
|
||||
<span className="flex-1 min-w-[300px] break-all text-ds-text">
|
||||
{bodyMessage}
|
||||
</span>
|
||||
|
||||
@@ -202,28 +199,28 @@ export const LogEntryView = ({ log }: { log: LogEntry }) => {
|
||||
e.stopPropagation()
|
||||
navigateToSpan(record.traceId!, record.spanId)
|
||||
}}
|
||||
className="group/trace flex items-center gap-1.5 rounded-full bg-sky-500/10 border border-sky-400/20 px-2.5 py-0.5 text-[10px] font-bold text-sky-400 hover:bg-sky-500/20 hover:border-sky-400/30 transition-all"
|
||||
className="group/trace flex items-center gap-1.5 rounded-full bg-ds-accent-bg border border-ds-accent/20 px-2.5 py-0.5 text-[10px] font-bold text-ds-accent hover:bg-ds-accent-bg hover:border-ds-accent/30 transition-all"
|
||||
>
|
||||
<Activity className="h-2.5 w-2.5 group-hover/trace:animate-pulse" />
|
||||
TRACE:{record.traceId.slice(0, 6)}
|
||||
</button>
|
||||
)}
|
||||
{isExpanded ? <ChevronUp className="h-3 w-3 text-slate-500" /> : <ChevronDown className="h-3 w-3 text-slate-500" />}
|
||||
{isExpanded ? <ChevronUp className="h-3 w-3 text-ds-text-3" /> : <ChevronDown className="h-3 w-3 text-ds-text-3" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expansion Content */}
|
||||
{isExpanded && (
|
||||
<div className="border-t border-white/5 bg-black/40 p-4 text-[12px]">
|
||||
<div className="border-t border-ds-line bg-ds-surface-2 p-4 text-[12px]">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{(record.attributes ?? []).length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-500">Attributes</p>
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-ds-text-3">Attributes</p>
|
||||
<div className="space-y-1">
|
||||
{(record.attributes ?? []).map(attr => (
|
||||
<div key={attr.key} className="flex gap-2">
|
||||
<span className="text-sky-400 shrink-0">{attr.key}:</span>
|
||||
<span className="text-slate-300 break-all">{formatAttributeValue(attr.value)}</span>
|
||||
<span className="text-ds-accent shrink-0">{attr.key}:</span>
|
||||
<span className="text-ds-text-2 break-all">{formatAttributeValue(attr.value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -231,20 +228,20 @@ export const LogEntryView = ({ log }: { log: LogEntry }) => {
|
||||
)}
|
||||
{(record.resourceAttributes ?? []).length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-500">Resource</p>
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-ds-text-3">Resource</p>
|
||||
<div className="space-y-1">
|
||||
{(record.resourceAttributes ?? []).map(attr => (
|
||||
<div key={attr.key} className="flex gap-2">
|
||||
<span className="text-emerald-400 shrink-0">{attr.key}:</span>
|
||||
<span className="text-slate-400 break-all">{formatAttributeValue(attr.value)}</span>
|
||||
<span className="text-ds-ok shrink-0">{attr.key}:</span>
|
||||
<span className="text-ds-text-2 break-all">{formatAttributeValue(attr.value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-white/5 flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex gap-4 text-[10px] text-slate-500">
|
||||
<div className="mt-4 pt-4 border-t border-ds-line flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex gap-4 text-[10px] text-ds-text-3">
|
||||
<span>Scope: {record.scope || "unknown"}</span>
|
||||
{record.spanId && <span>Span ID: {record.spanId}</span>}
|
||||
{record.traceId && <span>Trace ID: {record.traceId}</span>}
|
||||
@@ -253,7 +250,7 @@ export const LogEntryView = ({ log }: { log: LogEntry }) => {
|
||||
{record.traceId && (
|
||||
<button
|
||||
onClick={() => navigateToSpan(record.traceId!, record.spanId)}
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-sky-500 px-4 py-2 text-[10px] font-black uppercase tracking-widest text-slate-950 transition-all hover:bg-sky-400 hover:scale-[1.02] active:scale-[0.98] shadow-lg shadow-sky-500/20"
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-ds-accent px-4 py-2 text-[10px] font-black uppercase tracking-widest text-ds-on-accent transition-all hover:bg-ds-accent hover:scale-[1.02] active:scale-[0.98] shadow-lg "
|
||||
>
|
||||
<Activity className="h-3 w-3" />
|
||||
Jump to Span in Trace
|
||||
@@ -276,7 +273,7 @@ const LogVisualizer = ({ logs }: { logs: LogEntry[] }) => {
|
||||
}
|
||||
|
||||
export const TabsComponent = () => {
|
||||
const { traces, logs, metrics, activeTab, setActiveTab } = useObservabilityData()
|
||||
const { traces, logs, activeTab, setActiveTab } = useObservabilityData()
|
||||
const issuesState = useIssues()
|
||||
const [statusBusy, setStatusBusy] = useState(false)
|
||||
// Trends tab only exists when the backend has ClickHouse-backed stats;
|
||||
@@ -305,109 +302,6 @@ export const TabsComponent = () => {
|
||||
[issuesState],
|
||||
)
|
||||
|
||||
const numberFormatter = new Intl.NumberFormat("en-US", {
|
||||
notation: "compact",
|
||||
maximumFractionDigits: 1,
|
||||
})
|
||||
|
||||
const frontendCount = logs.reduce((acc, log) => {
|
||||
return (
|
||||
acc +
|
||||
(log.scopeLogs ?? []).reduce((sAcc, sl) => {
|
||||
return (
|
||||
sAcc +
|
||||
(sl.logRecords ?? []).filter((r) => {
|
||||
const attrs = r.attributes ?? []
|
||||
return (
|
||||
attrs.some((a) => a.key === "source" && a.value?.stringValue === "frontend") ||
|
||||
sl.scope?.name === "otel-rum" ||
|
||||
attrs.some((a) => a.key === "rum.vital") ||
|
||||
attrs.some((a) => a.key === "browser.url")
|
||||
)
|
||||
}).length
|
||||
)
|
||||
}, 0)
|
||||
)
|
||||
}, 0)
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
value: "tab0",
|
||||
label: "Issues",
|
||||
description: "Deduplicated errors",
|
||||
Icon: AlertOctagon,
|
||||
count: issuesState.issues.length
|
||||
},
|
||||
{
|
||||
value: "tab8",
|
||||
label: "Patterns",
|
||||
description: "Deterministic similarity review",
|
||||
Icon: GitBranch,
|
||||
count: undefined as number | undefined,
|
||||
},
|
||||
{
|
||||
value: "tab9",
|
||||
label: "Rollouts",
|
||||
description: "Cluster deploy correlation",
|
||||
Icon: CloudCog,
|
||||
count: undefined as number | undefined,
|
||||
},
|
||||
{
|
||||
value: "tab1",
|
||||
label: "Traces",
|
||||
description: "Span timelines & focus",
|
||||
Icon: Activity,
|
||||
count: traces.length
|
||||
},
|
||||
{
|
||||
value: "tab2",
|
||||
label: "Metrics",
|
||||
description: "Performance trends",
|
||||
Icon: LineChart,
|
||||
count: metrics.length
|
||||
},
|
||||
{
|
||||
value: "tab3",
|
||||
label: "Logs",
|
||||
description: "System log streams",
|
||||
Icon: ScrollText,
|
||||
count: logs.length
|
||||
},
|
||||
{
|
||||
value: "tab7",
|
||||
label: "RUM sessions",
|
||||
description: "Real-user sessions, worst experience first",
|
||||
Icon: Globe,
|
||||
count: frontendCount,
|
||||
},
|
||||
// Historical trends need the ClickHouse-backed stats endpoints.
|
||||
...(statsAvailable
|
||||
? [
|
||||
{
|
||||
value: "tab5",
|
||||
label: "Trends",
|
||||
description: "History graphs & release comparison",
|
||||
Icon: TrendingUp,
|
||||
count: undefined as number | undefined,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
value: "tab6",
|
||||
label: "Archive & Trap",
|
||||
description: "Persistent storage search & signal trap",
|
||||
Icon: Radar,
|
||||
count: undefined,
|
||||
},
|
||||
{
|
||||
value: "tab4",
|
||||
label: "Setup",
|
||||
description: "Instrument a new service",
|
||||
Icon: Wrench,
|
||||
count: undefined
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<TelemetryFilters />
|
||||
@@ -416,29 +310,6 @@ export const TabsComponent = () => {
|
||||
onValueChange={setActiveTab}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<Tabs.List
|
||||
className="flex gap-8 border-b border-white/10 pb-2 mb-2 w-full overflow-x-auto"
|
||||
aria-label="Telemetry explorer views"
|
||||
>
|
||||
{tabItems.map(({ value, label, Icon, count }) => (
|
||||
<Tabs.Trigger
|
||||
key={value}
|
||||
value={value}
|
||||
id={`tab-trigger-${value}`}
|
||||
aria-label={`${label} tab`}
|
||||
className="group relative flex items-center gap-2.5 pb-2 text-sm font-semibold text-slate-400 outline-none transition-colors hover:text-slate-200 data-[state=active]:text-white"
|
||||
>
|
||||
<div className="absolute -bottom-[2px] left-0 right-0 h-0.5 bg-sky-400 opacity-0 transition-opacity group-data-[state=active]:opacity-100" />
|
||||
<Icon className="h-4 w-4 text-slate-500 group-data-[state=active]:text-sky-400" />
|
||||
<span aria-hidden="true">{label}</span>
|
||||
{count !== undefined && (
|
||||
<span className="inline-flex rounded-full bg-slate-800 px-2 py-0.5 text-[10px] font-bold text-slate-300 group-data-[state=active]:bg-sky-500/20 group-data-[state=active]:text-sky-300">
|
||||
{numberFormatter.format(count)}
|
||||
</span>
|
||||
)}
|
||||
</Tabs.Trigger>
|
||||
))}
|
||||
</Tabs.List>
|
||||
<Tabs.Content
|
||||
className="outline-none py-2"
|
||||
value="tab0"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { X, Activity, AlertTriangle } from "lucide-react"
|
||||
import { useObservabilityData } from "../hooks/useObservabilityData"
|
||||
import { getServicePalette } from "../lib/otel-utils"
|
||||
import { FilterBar, type FilterChip } from "./FilterBar"
|
||||
|
||||
/**
|
||||
* Service focus + attribute exclusions, expressed through the shared
|
||||
* FilterBar so every screen filters the same way.
|
||||
*/
|
||||
export const TelemetryFilters = () => {
|
||||
const {
|
||||
selectedServices,
|
||||
@@ -13,112 +16,43 @@ export const TelemetryFilters = () => {
|
||||
filtersExpanded: isExpanded,
|
||||
} = useObservabilityData()
|
||||
|
||||
const hasExcludedKeys = Object.keys(excludedAttributes).length > 0
|
||||
|
||||
if (!isExpanded) return null
|
||||
|
||||
return (
|
||||
<div className="border-b border-white/10 pb-8 mb-6 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
<div className="grid gap-8 lg:grid-cols-2">
|
||||
{/* Service Multi-Select */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-3 w-3 text-slate-500" />
|
||||
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-500">
|
||||
Service Focus
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{uniqueServices.map((service) => {
|
||||
const isActive = selectedServices.has(service)
|
||||
const palette = getServicePalette(service)
|
||||
return (
|
||||
<button
|
||||
key={service}
|
||||
type="button"
|
||||
onClick={() => toggleService(service)}
|
||||
className={`group relative inline-flex items-center gap-3 rounded-2xl border px-5 py-2.5 text-[10px] font-bold uppercase tracking-[0.2em] transition-all duration-300 ${isActive
|
||||
? `border-transparent bg-gradient-to-br ${palette.gradient} text-slate-950 shadow-lg shadow-sky-500/20`
|
||||
: "border-white/10 bg-slate-900/40 text-slate-400 hover:border-white/20 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{!isActive && (
|
||||
<span
|
||||
className={`h-2 w-2 rounded-full bg-gradient-to-r ${palette.gradient} opacity-50 block`}
|
||||
/>
|
||||
)}
|
||||
{service}
|
||||
{isActive && (
|
||||
<span className="ml-1 flex h-4 w-4 items-center justify-center rounded-full bg-black/10">
|
||||
<X className="h-3 w-3" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{uniqueServices.length === 0 && (
|
||||
<span className="text-[11px] text-slate-600 italic font-medium px-1 underline decoration-white/5 underline-offset-4">
|
||||
Listening for service signals...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
const chips: FilterChip[] = [
|
||||
...Array.from(selectedServices).map((service) => ({
|
||||
id: `service-${service}`,
|
||||
filterKey: "service",
|
||||
value: service,
|
||||
tone: "neutral" as const,
|
||||
onRemove: () => toggleService(service),
|
||||
})),
|
||||
...Object.entries(excludedAttributes).flatMap(([key, valueSet]) =>
|
||||
Array.from(valueSet).map((value) => ({
|
||||
id: `exclude-${key}-${value}`,
|
||||
filterKey: `not ${key}`,
|
||||
value,
|
||||
tone: "warn" as const,
|
||||
onRemove: () => removeExcludedValue(key, value),
|
||||
})),
|
||||
),
|
||||
]
|
||||
|
||||
{/* Attribute Exclusions */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-3 w-3 text-slate-500" />
|
||||
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-500">
|
||||
Active Noise Suppression
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{hasExcludedKeys ? (
|
||||
Object.entries(excludedAttributes).map(([key, valueSet]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex flex-col gap-2 rounded-2xl border border-white/5 bg-slate-900/40 p-3 transition-all hover:border-white/10"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
<span className="text-[10px] font-bold text-sky-400 font-mono">
|
||||
{key}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => clearAttributeKey(key)}
|
||||
className="text-[9px] font-black uppercase tracking-widest text-slate-600 hover:text-white transition-colors ml-4"
|
||||
>
|
||||
Remove All
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{Array.from(valueSet).map((value) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => removeExcludedValue(key, value)}
|
||||
className="group inline-flex items-center gap-2 rounded-full border border-white/5 bg-slate-950/80 px-2.5 py-0.5 text-[10px] text-slate-300 transition-all hover:border-rose-500/50 hover:text-rose-400"
|
||||
>
|
||||
<span className="max-w-[10rem] truncate">{value}</span>
|
||||
<X className="h-2.5 w-2.5 text-slate-600 group-hover:text-rose-500" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center w-full py-4 border border-dashed border-white/5 rounded-3xl bg-slate-900/20">
|
||||
<p className="text-[11px] text-slate-600 italic font-medium">
|
||||
No active attribute exclusions.
|
||||
</p>
|
||||
<p className="text-[9px] text-slate-700 uppercase tracking-widest mt-1">
|
||||
Click attributes in details to suppress chatty items
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
const addFilterOptions = uniqueServices
|
||||
.filter((s) => !selectedServices.has(s))
|
||||
.map((s) => ({ value: s, label: s }))
|
||||
|
||||
const clearAll = () => {
|
||||
Array.from(selectedServices).forEach((s) => toggleService(s))
|
||||
Object.keys(excludedAttributes).forEach((k) => clearAttributeKey(k))
|
||||
}
|
||||
|
||||
return (
|
||||
<FilterBar
|
||||
chips={chips}
|
||||
addFilterOptions={addFilterOptions}
|
||||
onAddFilter={(v) => toggleService(v)}
|
||||
matchText={`${selectedServices.size} service filter${selectedServices.size === 1 ? "" : "s"} · ${chips.length - selectedServices.size} attribute exclusion${chips.length - selectedServices.size === 1 ? "" : "s"}`}
|
||||
onClearAll={chips.length > 0 ? clearAll : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -21,7 +21,7 @@ const compactFormatter = new Intl.NumberFormat("en-US", {
|
||||
})
|
||||
|
||||
// Stable palette for release lines; cycles when there are many releases.
|
||||
const RELEASE_COLORS = ["#38bdf8", "#f472b6", "#34d399", "#fbbf24", "#a78bfa", "#fb7185"]
|
||||
const RELEASE_COLORS = ["var(--ds-accent)", "var(--ds-accent)", "var(--ds-ok)", "var(--ds-warn)", "var(--ds-accent)", "var(--ds-critical)"]
|
||||
|
||||
const label = (bucket: string, granularity: "hour" | "day") => {
|
||||
const d = new Date(bucket.replace(" ", "T") + "Z")
|
||||
@@ -36,8 +36,8 @@ const ChartTooltip = ({ active, payload, granularity }: any) => {
|
||||
const p = payload[0].payload
|
||||
const d = new Date(String(p.bucket).replace(" ", "T") + "Z")
|
||||
return (
|
||||
<div className="rounded-xl border border-white/10 bg-slate-950/95 p-3 text-xs shadow-2xl backdrop-blur-md">
|
||||
<div className="mb-1 font-mono text-[10px] text-slate-400">
|
||||
<div className="rounded-xl border border-ds-line bg-ds-bg/95 p-3 text-xs shadow-2xl backdrop-blur-md">
|
||||
<div className="mb-1 font-mono text-[10px] text-ds-text-2">
|
||||
{d.toLocaleString([], {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
@@ -48,11 +48,11 @@ const ChartTooltip = ({ active, payload, granularity }: any) => {
|
||||
</div>
|
||||
{payload.map((entry: any) => (
|
||||
<div key={entry.name} className="flex items-center justify-between gap-4">
|
||||
<span className="flex items-center gap-1.5 text-slate-300">
|
||||
<span className="flex items-center gap-1.5 text-ds-text-2">
|
||||
<span className="h-1.5 w-1.5 rounded-full" style={{ background: entry.color }} />
|
||||
{entry.name}
|
||||
</span>
|
||||
<span className="font-mono font-bold text-white">{entry.value}</span>
|
||||
<span className="font-mono font-bold text-ds-text">{entry.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -106,7 +106,7 @@ export const TrendsVisualizer = () => {
|
||||
|
||||
if (loading && errorPoints.length === 0) {
|
||||
return (
|
||||
<div className="flex min-h-[240px] flex-col items-center justify-center gap-3 text-slate-400">
|
||||
<div className="flex min-h-[240px] flex-col items-center justify-center gap-3 text-ds-text-2">
|
||||
<RefreshCw className="h-6 w-6 animate-spin opacity-40" />
|
||||
<p className="text-sm">Loading trends…</p>
|
||||
</div>
|
||||
@@ -116,21 +116,21 @@ export const TrendsVisualizer = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Toolbar */}
|
||||
<div className="sticky top-0 z-20 flex flex-wrap items-center justify-between gap-3 border-b border-white/5 bg-slate-950/80 py-2 backdrop-blur">
|
||||
<div className="sticky top-0 z-20 flex flex-wrap items-center justify-between gap-3 border-b border-ds-line bg-ds-bg/95 py-2 backdrop-blur">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className="inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-slate-500">
|
||||
<GitCompareArrows className="h-3 w-3 text-sky-400" />
|
||||
<span className="inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-ds-text-3">
|
||||
<GitCompareArrows className="h-3 w-3 text-ds-accent" />
|
||||
Historical Trends
|
||||
</span>
|
||||
<div className="flex items-center gap-1 rounded-lg border border-white/5 bg-white/5 p-0.5">
|
||||
<div className="flex items-center gap-1 rounded-lg border border-ds-line bg-ds-surface-2 p-0.5">
|
||||
{rangeOptions.map((h) => (
|
||||
<button
|
||||
key={h}
|
||||
onClick={() => setHours(h)}
|
||||
className={`rounded px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider transition-all ${
|
||||
hours === h
|
||||
? "bg-sky-500/20 text-sky-300 ring-1 ring-sky-500/30"
|
||||
: "text-slate-500 hover:text-slate-300"
|
||||
? "bg-ds-accent-bg text-ds-accent ring-1 ring-ds-accent/40"
|
||||
: "text-ds-text-3 hover:text-ds-text-2"
|
||||
}`}
|
||||
>
|
||||
{h < 24 ? `${h}h` : `${h / 24}d`}
|
||||
@@ -141,7 +141,7 @@ export const TrendsVisualizer = () => {
|
||||
<select
|
||||
value={service}
|
||||
onChange={(e) => setService(e.target.value)}
|
||||
className="rounded-lg border border-white/10 bg-slate-900/60 px-2 py-1 text-[10px] font-bold uppercase tracking-wider text-slate-300 outline-none"
|
||||
className="rounded-lg border border-ds-line bg-ds-surface-2 px-2 py-1 text-[10px] font-bold uppercase tracking-wider text-ds-text-2 outline-none"
|
||||
aria-label="Filter by service"
|
||||
>
|
||||
<option value="">All services</option>
|
||||
@@ -154,15 +154,15 @@ export const TrendsVisualizer = () => {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-[10px] text-slate-500">
|
||||
<span className="font-mono text-[10px] text-ds-text-3">
|
||||
{compactFormatter.format(totals.total)} entries ·{" "}
|
||||
<span className={totals.errors > 0 ? "text-rose-400" : "text-emerald-400"}>
|
||||
<span className={totals.errors > 0 ? "text-ds-critical" : "text-ds-ok"}>
|
||||
{compactFormatter.format(totals.errors)} errors
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
onClick={() => refresh()}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-white/10 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-slate-400 transition-all hover:border-white/20 hover:text-white"
|
||||
className="flex items-center gap-1.5 rounded-lg border border-ds-line px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-ds-text-2 transition-all hover:border-ds-line-strong hover:text-ds-text"
|
||||
>
|
||||
<RefreshCw className={`h-3 w-3 ${loading ? "animate-spin" : ""}`} />
|
||||
Refresh
|
||||
@@ -171,25 +171,25 @@ export const TrendsVisualizer = () => {
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 rounded-xl border border-rose-500/20 bg-rose-500/5 p-4 text-sm text-rose-300">
|
||||
<div className="flex items-center gap-2 rounded-xl border border-ds-critical/20 bg-ds-critical-bg p-4 text-sm text-ds-critical">
|
||||
<AlertOctagon className="h-4 w-4 shrink-0" />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error trend */}
|
||||
<section className="rounded-2xl border border-white/5 bg-slate-950/40 p-4">
|
||||
<section className="rounded-2xl border border-ds-line bg-ds-surface-2 p-4">
|
||||
<header className="mb-3 flex items-center justify-between">
|
||||
<h3 className="flex items-center gap-2 text-xs font-bold uppercase tracking-[0.2em] text-slate-400">
|
||||
<AlertOctagon className="h-3.5 w-3.5 text-rose-400" />
|
||||
<h3 className="flex items-center gap-2 text-xs font-bold uppercase tracking-[0.2em] text-ds-text-2">
|
||||
<AlertOctagon className="h-3.5 w-3.5 text-ds-critical" />
|
||||
Entries & errors over time
|
||||
</h3>
|
||||
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
||||
<span className="text-[10px] uppercase tracking-wider text-ds-text-3">
|
||||
per {granularity}
|
||||
</span>
|
||||
</header>
|
||||
{errorData.length === 0 ? (
|
||||
<p className="py-12 text-center text-sm text-slate-500">
|
||||
<p className="py-12 text-center text-sm text-ds-text-3">
|
||||
No data in this range yet.
|
||||
</p>
|
||||
) : (
|
||||
@@ -198,26 +198,26 @@ export const TrendsVisualizer = () => {
|
||||
<AreaChart data={errorData} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="trendFill" x1="0" y1="0" x2="0" y2="100%">
|
||||
<stop offset="5%" stopColor="#38bdf8" stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor="#38bdf8" stopOpacity={0} />
|
||||
<stop offset="5%" stopColor="var(--ds-accent)" stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor="var(--ds-accent)" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="trendErrFill" x1="0" y1="0" x2="0" y2="100%">
|
||||
<stop offset="5%" stopColor="#fb7185" stopOpacity={0.45} />
|
||||
<stop offset="95%" stopColor="#fb7185" stopOpacity={0} />
|
||||
<stop offset="5%" stopColor="var(--ds-critical)" stopOpacity={0.45} />
|
||||
<stop offset="95%" stopColor="var(--ds-critical)" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="rgba(255,255,255,0.05)" />
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--ds-line-strong)" />
|
||||
<XAxis
|
||||
dataKey="bucket"
|
||||
tickFormatter={(b) => label(b, granularity)}
|
||||
stroke="#475569"
|
||||
stroke="var(--ds-text-3)"
|
||||
fontSize={10}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
minTickGap={40}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#475569"
|
||||
stroke="var(--ds-text-3)"
|
||||
fontSize={10}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
@@ -232,7 +232,7 @@ export const TrendsVisualizer = () => {
|
||||
type="monotone"
|
||||
dataKey="total"
|
||||
name="Total"
|
||||
stroke="#38bdf8"
|
||||
stroke="var(--ds-accent)"
|
||||
strokeWidth={2}
|
||||
fill="url(#trendFill)"
|
||||
isAnimationActive={false}
|
||||
@@ -241,7 +241,7 @@ export const TrendsVisualizer = () => {
|
||||
type="monotone"
|
||||
dataKey="errors"
|
||||
name="Errors"
|
||||
stroke="#fb7185"
|
||||
stroke="var(--ds-critical)"
|
||||
strokeWidth={2}
|
||||
fill="url(#trendErrFill)"
|
||||
isAnimationActive={false}
|
||||
@@ -253,38 +253,38 @@ export const TrendsVisualizer = () => {
|
||||
</section>
|
||||
|
||||
{/* Release comparison */}
|
||||
<section className="rounded-2xl border border-white/5 bg-slate-950/40 p-4">
|
||||
<section className="rounded-2xl border border-ds-line bg-ds-surface-2 p-4">
|
||||
<header className="mb-3 flex items-center justify-between">
|
||||
<h3 className="flex items-center gap-2 text-xs font-bold uppercase tracking-[0.2em] text-slate-400">
|
||||
<GitCompareArrows className="h-3.5 w-3.5 text-sky-400" />
|
||||
<h3 className="flex items-center gap-2 text-xs font-bold uppercase tracking-[0.2em] text-ds-text-2">
|
||||
<GitCompareArrows className="h-3.5 w-3.5 text-ds-accent" />
|
||||
Errors by release
|
||||
</h3>
|
||||
<span className="text-[10px] uppercase tracking-wider text-slate-600">
|
||||
<span className="text-[10px] uppercase tracking-wider text-ds-text-3">
|
||||
{releases.length} release{releases.length === 1 ? "" : "s"} · overlay
|
||||
</span>
|
||||
</header>
|
||||
{releaseData.length === 0 || releases.length === 0 ? (
|
||||
<p className="py-12 text-center text-sm text-slate-500">
|
||||
<p className="py-12 text-center text-sm text-ds-text-3">
|
||||
No release-tagged telemetry in this range. Have services send{" "}
|
||||
<code className="rounded bg-white/5 px-1 font-mono text-[10px]">service.release</code>{" "}
|
||||
<code className="rounded bg-ds-surface-2 px-1 font-mono text-[10px]">service.release</code>{" "}
|
||||
to compare deploys here.
|
||||
</p>
|
||||
) : (
|
||||
<div className="h-64 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={releaseData} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="rgba(255,255,255,0.05)" />
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--ds-line-strong)" />
|
||||
<XAxis
|
||||
dataKey="bucket"
|
||||
tickFormatter={(b) => label(b, "hour")}
|
||||
stroke="#475569"
|
||||
stroke="var(--ds-text-3)"
|
||||
fontSize={10}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
minTickGap={40}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#475569"
|
||||
stroke="var(--ds-text-3)"
|
||||
fontSize={10}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
@@ -312,7 +312,7 @@ export const TrendsVisualizer = () => {
|
||||
)}
|
||||
</section>
|
||||
|
||||
<p className="flex items-center justify-center gap-1.5 pb-2 text-center text-[10px] text-slate-600">
|
||||
<p className="flex items-center justify-center gap-1.5 pb-2 text-center text-[10px] text-ds-text-3">
|
||||
<Server className="h-3 w-3" />
|
||||
Served from the ClickHouse hourly rollup ({service || "all services"}, last {hours}h)
|
||||
</p>
|
||||
|
||||
@@ -27,28 +27,28 @@ export class ViewErrorBoundary extends Component<Props, State> {
|
||||
}
|
||||
}).OtelRUM?.last
|
||||
rum?.captureException(error, {
|
||||
"exception.type": "ReactErrorBoundary",
|
||||
"react.component_stack": info.componentStack,
|
||||
"exception.type": "ReactErrorBoundary",
|
||||
"react.component_stack": info.componentStack,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<div className="flex min-h-[240px] flex-col items-center justify-center gap-4 rounded-3xl border border-rose-500/20 bg-rose-500/5 p-12 text-center">
|
||||
<AlertTriangle className="h-10 w-10 text-rose-400" />
|
||||
<div className="flex min-h-[240px] flex-col items-center justify-center gap-4 rounded-3xl border border-ds-critical/20 bg-ds-critical-bg p-12 text-center">
|
||||
<AlertTriangle className="h-10 w-10 text-ds-critical" />
|
||||
<div>
|
||||
<p className="text-lg font-semibold text-white">
|
||||
<p className="text-lg font-semibold text-ds-text">
|
||||
This view hit an unexpected error
|
||||
</p>
|
||||
<p className="mt-1 max-w-md truncate font-mono text-xs text-rose-300/80">
|
||||
<p className="mt-1 max-w-md truncate font-mono text-xs text-ds-critical">
|
||||
{this.state.error.message}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => this.setState({ error: null })}
|
||||
className="inline-flex items-center gap-2 rounded-xl border border-rose-500/30 bg-rose-500/10 px-4 py-2 text-[10px] font-black uppercase tracking-[0.2em] text-rose-300 transition-all hover:bg-rose-500/20"
|
||||
className="inline-flex items-center gap-2 rounded-xl border border-ds-critical/30 bg-ds-critical-bg px-4 py-2 text-[10px] font-black uppercase tracking-[0.2em] text-ds-critical transition-all hover:bg-ds-critical-bg"
|
||||
>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
Retry
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Activity, ChevronDown, ChevronUp, ScrollText, X } from "lucide-react"
|
||||
import { useMemo, useState } from "react";
|
||||
import { useObservabilityData } from "../hooks/useObservabilityData";
|
||||
import { isSpanError } from "../lib/otel-utils";
|
||||
@@ -167,53 +168,47 @@ export const WaveformHistogram = () => {
|
||||
|
||||
if (buckets.length === 0) {
|
||||
return (
|
||||
<div className="relative bg-surface-container-lowest p-3.5 rounded-xl shadow-md mb-4 border border-surface-container/50">
|
||||
<div className="relative bg-ds-bg p-3.5 rounded-xl shadow-md mb-4 border border-ds-line">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-primary-container text-[18px]">
|
||||
monitoring
|
||||
</span>
|
||||
<h2 className="font-headline font-semibold text-xs text-on-surface">
|
||||
<Activity className="text-ds-accent h-[18px] w-[18px] shrink-0" aria-hidden="true" />
|
||||
<h2 className="font-[family-name:var(--font-display)] font-semibold text-xs text-ds-text">
|
||||
Telemetry Volume & Error Waveform
|
||||
</h2>
|
||||
</div>
|
||||
<span className="font-mono text-xs text-outline">Listening for OTLP traffic...</span>
|
||||
<span className="font-mono text-xs text-ds-text-3">Listening for OTLP traffic...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative bg-surface-container-lowest p-3.5 rounded-xl shadow-md mb-4 overflow-hidden border border-surface-container/50 font-body">
|
||||
<div className="relative bg-ds-bg p-3.5 rounded-xl shadow-md mb-4 overflow-hidden border border-ds-line font-body">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-2">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className="flex items-center gap-1.5 hover:text-white transition-colors"
|
||||
className="flex items-center gap-1.5 hover:text-ds-text transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-primary-container text-[18px]">
|
||||
monitoring
|
||||
</span>
|
||||
<h2 className="font-headline font-semibold text-xs uppercase tracking-wider text-on-surface leading-none">
|
||||
<Activity className="text-ds-accent h-[18px] w-[18px] shrink-0" aria-hidden="true" />
|
||||
<h2 className="font-[family-name:var(--font-display)] font-semibold text-xs uppercase tracking-wider text-ds-text leading-none">
|
||||
Telemetry Volume Waveform
|
||||
</h2>
|
||||
<span className="material-symbols-outlined text-outline text-[16px]">
|
||||
{isCollapsed ? "expand_more" : "expand_less"}
|
||||
</span>
|
||||
{isCollapsed ? <ChevronDown className="text-ds-text-3 h-[16px] w-[16px] shrink-0" aria-hidden="true" /> : <ChevronUp className="text-ds-text-3 h-[16px] w-[16px] shrink-0" aria-hidden="true" />}
|
||||
</button>
|
||||
<span className="font-mono text-xs text-outline">
|
||||
<span className="font-mono text-xs text-ds-text-3">
|
||||
{totalCount} events in current buffer
|
||||
</span>
|
||||
{hasErrorSurge && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-error-container text-on-error-container font-mono text-[10px] font-bold animate-pulse flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-error" />
|
||||
<span className="px-2 py-0.5 rounded-full bg-ds-critical-bg text-ds-critical font-mono text-[10px] font-bold animate-pulse flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-ds-critical" />
|
||||
SURGE +{surgePercent}%
|
||||
</span>
|
||||
)}
|
||||
{selectedTimeRange && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-sky-500/20 text-sky-300 font-mono text-[10px] font-bold border border-sky-400/30 flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-sky-400 animate-ping" />
|
||||
<span className="px-2 py-0.5 rounded-full bg-ds-accent-bg text-ds-accent font-mono text-[10px] font-bold border border-ds-accent/30 flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-ds-accent animate-ping" />
|
||||
Time Focus Active
|
||||
<button
|
||||
type="button"
|
||||
@@ -221,7 +216,7 @@ export const WaveformHistogram = () => {
|
||||
e.stopPropagation();
|
||||
setSelectedTimeRange(null);
|
||||
}}
|
||||
className="ml-1 hover:text-white"
|
||||
className="ml-1 hover:text-ds-text"
|
||||
title="Clear time focus"
|
||||
>
|
||||
✕
|
||||
@@ -233,16 +228,16 @@ export const WaveformHistogram = () => {
|
||||
{/* Legend */}
|
||||
<div className="flex items-center gap-4 font-mono text-xs select-none">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-2.5 h-2.5 rounded-sm bg-error"></span>
|
||||
<span className="text-on-surface">ERROR</span>
|
||||
<span className="w-2.5 h-2.5 rounded-sm bg-ds-critical"></span>
|
||||
<span className="text-ds-text">ERROR</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-2.5 h-2.5 rounded-sm bg-primary-fixed-dim"></span>
|
||||
<span className="text-on-surface">WARN</span>
|
||||
<span className="w-2.5 h-2.5 rounded-sm bg-ds-accent"></span>
|
||||
<span className="text-ds-text">WARN</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-2.5 h-2.5 rounded-sm bg-secondary"></span>
|
||||
<span className="text-on-surface">INFO</span>
|
||||
<span className="w-2.5 h-2.5 rounded-sm bg-ds-accent"></span>
|
||||
<span className="text-ds-text">INFO</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -263,7 +258,7 @@ export const WaveformHistogram = () => {
|
||||
onClick={() => handleBucketClick(b)}
|
||||
className={`flex-1 flex flex-col justify-end h-full gap-0.5 group cursor-pointer relative transition-all rounded-sm ${
|
||||
isSelected
|
||||
? "ring-2 ring-sky-400 bg-sky-400/25 scale-y-105 z-20 shadow-md shadow-sky-500/20"
|
||||
? "ring-2 ring-ds-accent/40 bg-ds-accent-bg scale-y-105 z-20 shadow-md "
|
||||
: hasAnySelected
|
||||
? "opacity-40 hover:opacity-100 hover:brightness-125"
|
||||
: "hover:brightness-125"
|
||||
@@ -273,28 +268,28 @@ export const WaveformHistogram = () => {
|
||||
{/* Active Selection Focus Pin Indicator */}
|
||||
{isSelected && (
|
||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2 flex flex-col items-center pointer-events-none z-20 whitespace-nowrap">
|
||||
<span className="px-1.5 py-0.5 bg-sky-500 text-slate-950 font-mono text-[9px] font-black rounded shadow">
|
||||
<span className="px-1.5 py-0.5 bg-ds-accent text-ds-on-accent font-mono text-[9px] font-black rounded shadow">
|
||||
FOCUS
|
||||
</span>
|
||||
<span className="w-0.5 h-1 bg-sky-400"></span>
|
||||
<span className="w-0.5 h-1 bg-ds-accent"></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Incident Flag Marker on Real Surge */}
|
||||
{!isSelected && isSurgeBar && b.idx === 15 && (
|
||||
<div className="absolute -top-3 left-1/2 -translate-x-1/2 flex flex-col items-center pointer-events-none z-10 whitespace-nowrap animate-bounce">
|
||||
<span className="px-1.5 py-0.5 bg-error-container text-on-error-container font-mono text-[10px] rounded shadow font-semibold flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-error animate-ping"></span>
|
||||
<span className="px-1.5 py-0.5 bg-ds-critical-bg text-ds-critical font-mono text-[10px] rounded shadow font-semibold flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-ds-critical animate-ping"></span>
|
||||
SURGE +{surgePercent}%
|
||||
</span>
|
||||
<span className="w-0.5 h-2 bg-error"></span>
|
||||
<span className="w-0.5 h-2 bg-ds-critical"></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error segment */}
|
||||
{b.error > 0 && (
|
||||
<div
|
||||
className="w-full bg-error rounded-t-sm transition-all group-hover:brightness-125"
|
||||
className="w-full bg-ds-critical rounded-t-sm transition-all group-hover:brightness-125"
|
||||
style={{ height: `${b.error}%` }}
|
||||
/>
|
||||
)}
|
||||
@@ -302,7 +297,7 @@ export const WaveformHistogram = () => {
|
||||
{/* Warn segment */}
|
||||
{b.warn > 0 && (
|
||||
<div
|
||||
className="w-full bg-primary-fixed-dim transition-all group-hover:brightness-125"
|
||||
className="w-full bg-ds-accent transition-all group-hover:brightness-125"
|
||||
style={{ height: `${b.warn}%` }}
|
||||
/>
|
||||
)}
|
||||
@@ -310,7 +305,7 @@ export const WaveformHistogram = () => {
|
||||
{/* Info segment */}
|
||||
{b.info > 0 && (
|
||||
<div
|
||||
className="w-full bg-secondary/80 rounded-b-sm transition-all group-hover:brightness-125"
|
||||
className="w-full bg-ds-accent-bg rounded-b-sm transition-all group-hover:brightness-125"
|
||||
style={{ height: `${b.info}%` }}
|
||||
/>
|
||||
)}
|
||||
@@ -321,10 +316,10 @@ export const WaveformHistogram = () => {
|
||||
</div>
|
||||
|
||||
{/* Real Time Scale Axes */}
|
||||
<div className="flex justify-between items-center text-outline font-mono text-xs pt-2 select-none border-t border-surface-container/40 mt-1">
|
||||
<div className="flex justify-between items-center text-ds-text-3 font-mono text-xs pt-2 select-none border-t border-ds-line mt-1">
|
||||
<span>{timeLabels[0]}</span>
|
||||
<span>{timeLabels[1]}</span>
|
||||
<span className={hasErrorSurge ? "text-error font-semibold" : ""}>
|
||||
<span className={hasErrorSurge ? "text-ds-critical font-semibold" : ""}>
|
||||
{timeLabels[2]} {hasErrorSurge ? "(SURGE)" : ""}
|
||||
</span>
|
||||
<span>{timeLabels[3]}</span>
|
||||
@@ -332,17 +327,17 @@ export const WaveformHistogram = () => {
|
||||
|
||||
{/* Time Focus Bar with quick tab navigation and clear button */}
|
||||
{activeBucket && (
|
||||
<div className="mt-2.5 flex flex-wrap items-center justify-between gap-3 rounded-xl bg-sky-500/10 border border-sky-500/30 px-3.5 py-2 animate-in fade-in slide-in-from-top-1 duration-200">
|
||||
<div className="mt-2.5 flex flex-wrap items-center justify-between gap-3 rounded-xl bg-ds-accent-bg border border-ds-accent/30 px-3.5 py-2 animate-in fade-in slide-in-from-top-1 duration-200">
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<span className="flex h-2 w-2 rounded-full bg-sky-400 animate-ping" />
|
||||
<span className="flex h-2 w-2 rounded-full bg-ds-accent animate-ping" />
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-semibold text-xs text-white">
|
||||
<span className="font-semibold text-xs text-ds-text">
|
||||
Focused Time Period:
|
||||
</span>
|
||||
<span className="font-mono text-xs text-sky-300 font-bold">
|
||||
<span className="font-mono text-xs text-ds-accent font-bold">
|
||||
{activeBucket.timeLabel}
|
||||
</span>
|
||||
<span className="text-[11px] text-slate-400 font-mono">
|
||||
<span className="text-[11px] text-ds-text-2 font-mono">
|
||||
({activeBucket.traceCount} trace events • {activeBucket.logCount} log records
|
||||
{activeBucket.rawError > 0 ? ` • ${activeBucket.rawError} errors` : ""})
|
||||
</span>
|
||||
@@ -353,26 +348,26 @@ export const WaveformHistogram = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("tab1")}
|
||||
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-sky-500/20 hover:bg-sky-500/30 text-sky-300 font-bold transition-all border border-sky-400/30"
|
||||
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-ds-accent-bg hover:bg-ds-accent-bg text-ds-accent font-bold transition-all border border-ds-accent/30"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">timeline</span>
|
||||
<Activity className="h-[14px] w-[14px] shrink-0" aria-hidden="true" />
|
||||
Show Traces ({activeBucket.traceCount})
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("tab3")}
|
||||
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-sky-500/20 hover:bg-sky-500/30 text-sky-300 font-bold transition-all border border-sky-400/30"
|
||||
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-ds-accent-bg hover:bg-ds-accent-bg text-ds-accent font-bold transition-all border border-ds-accent/30"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">receipt_long</span>
|
||||
<ScrollText className="h-[14px] w-[14px] shrink-0" aria-hidden="true" />
|
||||
Show Logs ({activeBucket.logCount})
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedTimeRange(null)}
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-lg bg-white/10 hover:bg-white/20 text-slate-300 transition-colors"
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-lg bg-ds-fill hover:bg-ds-fill text-ds-text-2 transition-colors"
|
||||
title="Clear time focus"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">close</span>
|
||||
<X className="h-[14px] w-[14px] shrink-0" aria-hidden="true" />
|
||||
Clear Focus
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+10
-87
@@ -1,73 +1,8 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-background: #0f131c;
|
||||
--color-surface: #0f131c;
|
||||
--color-surface-dim: #0f131c;
|
||||
--color-surface-bright: #353942;
|
||||
--color-surface-container-lowest: #0a0e16;
|
||||
--color-surface-container-low: #181c24;
|
||||
--color-surface-container: #1c2028;
|
||||
--color-surface-container-high: #262a33;
|
||||
--color-surface-container-highest: #31353e;
|
||||
--color-surface-variant: #31353e;
|
||||
--color-surface-tint: #00dbe9;
|
||||
|
||||
--color-on-background: #dfe2ee;
|
||||
--color-on-surface: #dfe2ee;
|
||||
--color-on-surface-variant: #b9cacb;
|
||||
--color-inverse-surface: #dfe2ee;
|
||||
--color-inverse-on-surface: #2c3039;
|
||||
|
||||
--color-primary: #dbfcff;
|
||||
--color-primary-container: #00f0ff;
|
||||
--color-on-primary: #00363a;
|
||||
--color-on-primary-container: #006970;
|
||||
--color-primary-fixed: #7df4ff;
|
||||
--color-primary-fixed-dim: #00dbe9;
|
||||
--color-on-primary-fixed: #002022;
|
||||
--color-on-primary-fixed-variant: #004f54;
|
||||
--color-inverse-primary: #006970;
|
||||
|
||||
--color-secondary: #4edea3;
|
||||
--color-secondary-container: #00a572;
|
||||
--color-on-secondary: #003824;
|
||||
--color-on-secondary-container: #00311f;
|
||||
--color-secondary-fixed: #6ffbbe;
|
||||
--color-secondary-fixed-dim: #4edea3;
|
||||
--color-on-secondary-fixed: #002113;
|
||||
--color-on-secondary-fixed-variant: #005236;
|
||||
|
||||
--color-tertiary: #fff3f3;
|
||||
--color-tertiary-container: #ffcdd1;
|
||||
--color-on-tertiary: #670020;
|
||||
--color-on-tertiary-container: #be0042;
|
||||
--color-tertiary-fixed: #ffd9dc;
|
||||
--color-tertiary-fixed-dim: #ffb2ba;
|
||||
--color-on-tertiary-fixed: #400011;
|
||||
--color-on-tertiary-fixed-variant: #910030;
|
||||
|
||||
--color-error: #ffb4ab;
|
||||
--color-error-container: #93000a;
|
||||
--color-on-error: #690005;
|
||||
--color-on-error-container: #ffdad6;
|
||||
|
||||
--color-outline: #849495;
|
||||
--color-outline-variant: #3b494b;
|
||||
|
||||
--font-headline: "Bricolage Grotesque", "Geist", sans-serif;
|
||||
--font-body: "Instrument Sans", "Helvetica Neue", system-ui, sans-serif;
|
||||
--font-mono: "IBM Plex Mono", "JetBrains Mono", monospace;
|
||||
}
|
||||
|
||||
/*
|
||||
* Design-spec tokens ("ds-*"): the neutral-chrome, colour-is-signal palette
|
||||
* from the Stackship observability console spec. Namespaced separately from
|
||||
* the legacy --color-* tokens above so screens can migrate one at a time
|
||||
* without the old MD3 palette and the new spec palette fighting over the
|
||||
* same variable names. New screens (icon rail, top bar, filter bar, triage
|
||||
* overview) are built on --color-ds-*; unconverted tabs keep using the
|
||||
* tokens above until they're redesigned too.
|
||||
* The single design system ("ds-*"): neutral chrome, colour is a signal.
|
||||
* Dark is the default, light is :root[data-theme="light"]. See AGENTS.md.
|
||||
*/
|
||||
:root {
|
||||
--ds-bg: #0d1014;
|
||||
@@ -131,36 +66,24 @@
|
||||
--color-ds-accent-bg: var(--ds-accent-bg);
|
||||
--color-ds-on-accent: var(--ds-on-accent);
|
||||
|
||||
--font-display: "Bricolage Grotesque", "Geist", sans-serif;
|
||||
--font-display: "Bricolage Grotesque", system-ui, sans-serif;
|
||||
--font-ui: "Instrument Sans", "Helvetica Neue", system-ui, sans-serif;
|
||||
--font-data: "IBM Plex Mono", "JetBrains Mono", monospace;
|
||||
--font-data: "IBM Plex Mono", ui-monospace, monospace;
|
||||
--font-body: var(--font-ui);
|
||||
--font-sans: var(--font-ui);
|
||||
--font-mono: var(--font-data);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #0f131c;
|
||||
color: #dfe2ee;
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background-color: var(--ds-bg);
|
||||
color: var(--ds-text);
|
||||
font-family: var(--font-ui);
|
||||
}
|
||||
body {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
-webkit-font-feature-settings: 'liga';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
@@ -55,12 +55,12 @@ export function parseBreadcrumbs(raw?: string): ParsedBreadcrumb[] {
|
||||
export type IssueStatus = "open" | "resolved" | "ignored"
|
||||
|
||||
export const issueStatusMeta: Record<IssueStatus, { label: string; className: string }> = {
|
||||
open: { label: "Open", className: "bg-rose-500/10 text-rose-300 border-rose-500/20" },
|
||||
open: { label: "Open", className: "bg-ds-critical-bg text-ds-critical border-ds-critical/20" },
|
||||
resolved: {
|
||||
label: "Resolved",
|
||||
className: "bg-emerald-500/10 text-emerald-300 border-emerald-500/20",
|
||||
className: "bg-ds-ok/15 text-ds-ok border-ds-ok/20",
|
||||
},
|
||||
ignored: { label: "Ignored", className: "bg-slate-500/10 text-slate-400 border-white/10" },
|
||||
ignored: { label: "Ignored", className: "bg-ds-fill text-ds-text-2 border-ds-line" },
|
||||
}
|
||||
|
||||
export function issueStatusOf(issue: Issue): IssueStatus {
|
||||
|
||||
@@ -124,16 +124,26 @@ export function getPodName(
|
||||
return "";
|
||||
}
|
||||
|
||||
export const POD_COLORS = [
|
||||
{ stroke: "#38bdf8", fill: "#38bdf8", text: "text-sky-400", bg: "bg-sky-500/10", border: "border-sky-500/20" },
|
||||
{ stroke: "#a855f7", fill: "#a855f7", text: "text-purple-400", bg: "bg-purple-500/10", border: "border-purple-500/20" },
|
||||
{ stroke: "#34d399", fill: "#34d399", text: "text-emerald-400", bg: "bg-emerald-500/10", border: "border-emerald-500/20" },
|
||||
{ stroke: "#fbbf24", fill: "#fbbf24", text: "text-amber-400", bg: "bg-amber-500/10", border: "border-amber-500/20" },
|
||||
{ stroke: "#fb7185", fill: "#fb7185", text: "text-rose-400", bg: "bg-rose-500/10", border: "border-rose-500/20" },
|
||||
{ stroke: "#22d3ee", fill: "#22d3ee", text: "text-cyan-400", bg: "bg-cyan-500/10", border: "border-cyan-500/20" },
|
||||
{ stroke: "#f472b6", fill: "#f472b6", text: "text-pink-400", bg: "bg-pink-500/10", border: "border-pink-500/20" },
|
||||
{ stroke: "#818cf8", fill: "#818cf8", text: "text-indigo-400", bg: "bg-indigo-500/10", border: "border-indigo-500/20" },
|
||||
// Chart-series colours (metrics lines, pod chips). Muted, distinct hues that
|
||||
// read on both themes. Data-viz exception to the "one accent" rule; chrome
|
||||
// around them (chip background/border/text) stays neutral ds-*.
|
||||
const SERIES_HUES = [
|
||||
"var(--ds-accent)",
|
||||
"var(--ds-ok)",
|
||||
"var(--ds-warn)",
|
||||
"#B07CC6",
|
||||
"#4FB3BF",
|
||||
"#C9788F",
|
||||
"#8FA35A",
|
||||
"#7F8FA6",
|
||||
];
|
||||
export const POD_COLORS = SERIES_HUES.map((c) => ({
|
||||
stroke: c,
|
||||
fill: c,
|
||||
text: "text-ds-text-2",
|
||||
bg: "bg-ds-surface-2",
|
||||
border: "border-ds-line-strong",
|
||||
}));
|
||||
|
||||
export function getPodPalette(pod: string) {
|
||||
return POD_COLORS[hashString(pod) % POD_COLORS.length] ?? POD_COLORS[0];
|
||||
@@ -147,50 +157,12 @@ export const formatAttributeValue = (value: AttributeValue): string => {
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
export const servicePalettes = [
|
||||
{
|
||||
gradient: "from-violet-600 via-indigo-600 to-sky-600",
|
||||
ring: "ring-violet-400/40",
|
||||
text: "text-violet-400",
|
||||
bg: "bg-violet-400/10",
|
||||
border: "border-violet-400/20",
|
||||
},
|
||||
{
|
||||
gradient: "from-emerald-500 via-teal-600 to-cyan-700",
|
||||
ring: "ring-emerald-400/40",
|
||||
text: "text-emerald-400",
|
||||
bg: "bg-emerald-400/10",
|
||||
border: "border-emerald-400/20",
|
||||
},
|
||||
{
|
||||
gradient: "from-amber-600 via-orange-600 to-rose-600",
|
||||
ring: "ring-amber-400/40",
|
||||
text: "text-amber-400",
|
||||
bg: "bg-amber-400/10",
|
||||
border: "border-amber-400/20",
|
||||
},
|
||||
{
|
||||
gradient: "from-fuchsia-600 via-purple-600 to-indigo-700",
|
||||
ring: "ring-purple-400/40",
|
||||
text: "text-fuchsia-400",
|
||||
bg: "bg-fuchsia-400/10",
|
||||
border: "border-fuchsia-400/20",
|
||||
},
|
||||
{
|
||||
gradient: "from-sky-500 via-blue-600 to-indigo-700",
|
||||
ring: "ring-sky-400/40",
|
||||
text: "text-sky-400",
|
||||
bg: "bg-sky-400/10",
|
||||
border: "border-sky-400/20",
|
||||
},
|
||||
{
|
||||
gradient: "from-lime-500 via-emerald-600 to-teal-700",
|
||||
ring: "ring-lime-400/40",
|
||||
text: "text-lime-400",
|
||||
bg: "bg-lime-400/10",
|
||||
border: "border-lime-400/20",
|
||||
},
|
||||
];
|
||||
// Muted per-service colours, reserved for trace-waterfall span bars and the
|
||||
// service time-distribution bar. Never use for general UI chrome.
|
||||
export const spanColors = ["#6C7BD6", "#3FA37F", "#C8963E", "#A56BB8", "#4A9DB5", "#8DA04E"];
|
||||
|
||||
export const getSpanColor = (service: string): string =>
|
||||
spanColors[hashString(service) % spanColors.length] ?? spanColors[0];
|
||||
|
||||
export const hashString = (value: string): number => {
|
||||
let hash = 0;
|
||||
@@ -201,9 +173,13 @@ export const hashString = (value: string): number => {
|
||||
return Math.abs(hash);
|
||||
};
|
||||
|
||||
export const getServicePalette = (service: string) => {
|
||||
const palette =
|
||||
servicePalettes[hashString(service) % servicePalettes.length] ??
|
||||
servicePalettes[0];
|
||||
return palette;
|
||||
// Neutral service badge classes. Services are told apart by name, not colour.
|
||||
const neutralServicePalette = {
|
||||
gradient: "",
|
||||
ring: "ring-ds-line-strong",
|
||||
text: "text-ds-text-2",
|
||||
bg: "bg-ds-surface-2",
|
||||
border: "border-ds-line-strong",
|
||||
};
|
||||
|
||||
export const getServicePalette = (_service: string) => neutralServicePalette;
|
||||
Reference in new issue
Block a user