import { useCallback, useEffect, useState } from "react" import * as Tabs from "@radix-ui/react-tabs" import { Activity, AlertTriangle, Info, CheckCircle2, ChevronDown, ChevronUp, X, } from "lucide-react" import type { LucideIcon } from "lucide-react" import { MetricsVisualizer } from "./MetricsVisualizer" import TraceVisualization from "./TraceVisualization" import { IssuesVisualizer } from "./IssuesVisualizer" import { SetupPanel } from "./SetupPanel" import { SearchAndWatcher } from "./SearchAndWatcher" import { RumSessionsVisualizer } from "./RumSessionsVisualizer" import { ViewErrorBoundary } from "./ViewErrorBoundary" import { useObservabilityData } from "../hooks/useObservabilityData" import { useIssues } from "../hooks/useIssues" import { useStatsAvailability } from "../hooks/useStats" import { TrendsVisualizer } from "./TrendsVisualizer" import { authFetch } from "../lib/auth" import type { Issue, LogEntry, OtelAttribute } from "../lib/types" import { formatAttributeValue, getServicePalette, getServiceName } from "../lib/otel-utils" import { KeyValue } from "./common/DetailField" import { TelemetryFilters } from "./TelemetryFilters" import { PatternsVisualizer } from "./PatternsVisualizer" import { RolloutsVisualizer } from "./RolloutsVisualizer" import { SourceIpsVisualizer } from "./SourceIpsVisualizer" import { CspReportsVisualizer } from "./CspReportsVisualizer" import { TodosVisualizer } from "./TodosVisualizer" type SeverityMeta = { baseLabel: string bar: string badge: string ring: string icon: LucideIcon } const severityStyles: Record = { ERROR: { baseLabel: "Error", bar: "bg-ds-critical", badge: "bg-ds-critical-bg text-ds-critical", ring: "ring-ds-critical/40", icon: AlertTriangle, }, WARN: { baseLabel: "Warning", bar: "bg-ds-warn", badge: "bg-ds-warn-bg text-ds-warn", ring: "ring-ds-warn/40", icon: AlertTriangle, }, INFO: { baseLabel: "Info", bar: "bg-ds-accent", badge: "bg-ds-accent-bg text-ds-accent", ring: "ring-ds-accent/40", icon: Info, }, DEBUG: { baseLabel: "Debug", bar: "bg-ds-ok", badge: "bg-ds-ok/15 text-ds-ok", ring: "ring-ds-ok/40", icon: CheckCircle2, }, } const fallbackSeverity: SeverityMeta = { baseLabel: "Log", bar: "bg-ds-line-strong", badge: "bg-ds-surface-2 text-ds-text-2", ring: "ring-ds-line-strong", icon: Info, } const getSeverityPresentation = ( severityText?: string, severityNumber?: number, ) => { if (severityText) { const normalized = severityText.toUpperCase() if (severityStyles[normalized]) { const meta = severityStyles[normalized] return { ...meta, display: meta.baseLabel } } return { ...fallbackSeverity, display: severityText } } if (severityNumber !== undefined) { if (severityNumber >= 17) { const meta = severityStyles.ERROR return { ...meta, display: meta.baseLabel } } if (severityNumber >= 13) { const meta = severityStyles.WARN return { ...meta, display: meta.baseLabel } } if (severityNumber >= 9) { const meta = severityStyles.INFO return { ...meta, display: meta.baseLabel } } const meta = severityStyles.DEBUG return { ...meta, display: meta.baseLabel } } return { ...fallbackSeverity, display: fallbackSeverity.baseLabel } } // tabItems moved inside component to access live data export const LogEntryView = ({ log }: { log: LogEntry }) => { const { setActiveTraceId, setActiveSpanId, setActiveTab } = useObservabilityData() const [expandedId, setExpandedId] = useState(null) const logRecords = (log.scopeLogs ?? []).flatMap((scopeLog) => (scopeLog.logRecords ?? []).map((record) => ({ ...record, scope: scopeLog.scope?.name, resourceAttributes: log.resource?.attributes ?? [], })), ) const getRecordServiceName = (recordAttributes: OtelAttribute[]) => { return getServiceName(log.resource?.attributes ?? [], recordAttributes) } const navigateToSpan = (traceId: string, spanId?: string) => { setActiveTraceId(traceId) if (spanId) setActiveSpanId(spanId) setActiveTab("tab1") } const formatTimestamp = (timeUnixNano: string) => { const timestamp = (parseInt(timeUnixNano ?? "0") || 0) / 1e9 const date = new Date(timestamp * 1000) return date.toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 }) } if (logRecords.length === 0) { return null } return (
{logRecords.map((record) => { const severity = getSeverityPresentation( record.severityText, record.severityNumber, ) const bodyMessage = record.body?.stringValue ?? "(no body)" const recordKey = record.uid const isExpanded = expandedId === recordKey return (
{/* The Main Log Row */}
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]" >
{/* Timestamp */} {formatTimestamp(record.timeUnixNano)} {/* Service Badge */} {(() => { const sName = getRecordServiceName(record.attributes ?? []) const palette = getServicePalette(sName) return ( {sName} ) })()} {/* Severity Badge */} {severity.display} {/* Log Message */} {bodyMessage} {/* Action Icons */}
{record.traceId && ( )} {isExpanded ? : }
{/* Expansion Content */} {isExpanded && (
{(record.attributes ?? []).length > 0 && (

Attributes

{(record.attributes ?? []).map(attr => ( ))}
)} {(record.resourceAttributes ?? []).length > 0 && (

Resource

{(record.resourceAttributes ?? []).map(attr => ( ))}
)}
Scope: {record.scope || "unknown"} {record.spanId && Span ID: {record.spanId}} {record.traceId && Trace ID: {record.traceId}}
{record.traceId && ( )}
)}
) })}
) } import { LogsVisualizer } from "./LogsVisualizer" export const TabsComponent = () => { const { traces, logs, activeTab, setActiveTab } = useObservabilityData() const issuesState = useIssues() const [statusBusy, setStatusBusy] = useState(false) const [statusNotice, setStatusNotice] = useState<{ text: string; undo?: () => void; error?: boolean } | null>(null) // Trends tab only exists when the backend has ClickHouse-backed stats; // null while the capability check is in flight. const statsAvailable = useStatsAvailability() // Triage action: apply optimistically, PATCH, then refresh so counts and // badges match the server. A failure rolls back and says so; a success // offers Undo because the card moves into the collapsed resolved/ignored // group and would otherwise just vanish. const handleIssueStatusChange = useCallback( async (issue: Issue, status: string) => { const previous = issue.status ?? "open" setStatusBusy(true) void issuesState.patch(issue.id, { status, regressedAt: undefined }) try { const res = await authFetch(`/api/issues/${issue.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status }), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const verb = status === "resolved" ? "Resolved" : status === "ignored" ? "Ignored (alerts muted)" : "Reopened" setStatusNotice({ text: `${verb}: ${issue.title}`, undo: status === "open" ? undefined : () => void handleIssueStatusChange({ ...issue, status }, previous), }) } catch (err) { console.error("Failed to update issue status:", err) setStatusNotice({ text: `Couldn't update issue: ${err instanceof Error ? err.message : String(err)}`, error: true }) } finally { await issuesState.refresh() setStatusBusy(false) } }, [issuesState], ) useEffect(() => { if (!statusNotice) return const t = setTimeout(() => setStatusNotice(null), statusNotice.error ? 8000 : 6000) return () => clearTimeout(t) }, [statusNotice]) return (
{activeTab === "tab0" && ( )} {activeTab === "tab1" && } {activeTab === "tab8" && } {activeTab === "tab9" && } {activeTab === "tab10" && } {activeTab === "tab12" && } {activeTab === "tab11" && } {activeTab === "tab2" && } {activeTab === "tab3" && } {activeTab === "tab7" && } {activeTab === "tab4" && } {statsAvailable && ( {activeTab === "tab5" && } )} {activeTab === "tab6" && } {statusNotice && (
)}
) }