@datalayer/core 1.0.2 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/api/constants.d.ts +6 -0
- package/lib/api/constants.js +6 -0
- package/lib/api/index.d.ts +1 -0
- package/lib/api/index.js +1 -0
- package/lib/api/otel/index.d.ts +12 -0
- package/lib/api/otel/index.js +16 -0
- package/lib/api/otel/logs.d.ts +19 -0
- package/lib/api/otel/logs.js +43 -0
- package/lib/api/otel/metrics.d.ts +31 -0
- package/lib/api/otel/metrics.js +65 -0
- package/lib/api/otel/query.d.ts +16 -0
- package/lib/api/otel/query.js +37 -0
- package/lib/api/otel/services.d.ts +39 -0
- package/lib/api/otel/services.js +81 -0
- package/lib/api/otel/traces.d.ts +24 -0
- package/lib/api/otel/traces.js +53 -0
- package/lib/api/otel/types.d.ts +112 -0
- package/lib/api/otel/types.js +5 -0
- package/lib/api/runtimes/checkpoints.d.ts +122 -0
- package/lib/api/runtimes/checkpoints.js +118 -0
- package/lib/api/runtimes/index.d.ts +1 -0
- package/lib/api/runtimes/index.js +1 -0
- package/lib/api/runtimes/runtimes.d.ts +84 -0
- package/lib/api/runtimes/runtimes.js +50 -0
- package/lib/components/auth/Login.js +1 -1
- package/lib/components/display/BusyDots.d.ts +9 -0
- package/lib/components/display/BusyDots.js +31 -0
- package/lib/components/display/LiveRelativeTime.d.ts +10 -0
- package/lib/components/display/LiveRelativeTime.js +21 -0
- package/lib/components/display/index.d.ts +2 -0
- package/lib/components/display/index.js +2 -0
- package/lib/components/flashes/FlashSurveys.js +1 -1
- package/lib/components/index.d.ts +1 -0
- package/lib/components/index.js +1 -0
- package/lib/components/navbar/SubdomainNavBar.js +1 -1
- package/lib/components/progress/ConsumptionBar.js +6 -7
- package/lib/components/progress/CreditsIndicator.js +2 -2
- package/lib/components/progress/consumption.d.ts +12 -0
- package/lib/components/progress/consumption.js +31 -0
- package/lib/components/progress/index.d.ts +1 -0
- package/lib/components/progress/index.js +1 -0
- package/lib/components/sparklines/Sparklines.d.ts +16 -0
- package/lib/components/sparklines/Sparklines.js +65 -0
- package/lib/components/sparklines/SparklinesLine.d.ts +8 -0
- package/lib/components/sparklines/SparklinesLine.js +37 -0
- package/lib/components/sparklines/dataProcessing.d.ts +25 -0
- package/lib/components/sparklines/dataProcessing.js +35 -0
- package/lib/components/sparklines/index.d.ts +4 -0
- package/lib/components/sparklines/index.js +7 -0
- package/lib/components/sparklines/types.d.ts +36 -0
- package/lib/components/sparklines/types.js +5 -0
- package/lib/components/storage/ContentsBrowser.js +17 -1
- package/lib/components/subnav/SubNav.js +1 -1
- package/lib/config/Configuration.d.ts +4 -0
- package/lib/hooks/useCache.d.ts +6 -63
- package/lib/hooks/useCache.js +35 -205
- package/lib/hooks/useProjects.d.ts +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.js +4 -0
- package/lib/models/ItemDTO.js +1 -1
- package/lib/models/RolesPlatform.js +2 -2
- package/lib/models/User.d.ts +2 -0
- package/lib/models/User.js +4 -1
- package/lib/otel/client/OtelClient.d.ts +93 -0
- package/lib/otel/client/OtelClient.js +232 -0
- package/lib/otel/client/index.d.ts +2 -0
- package/lib/otel/client/index.js +5 -0
- package/lib/otel/hooks/index.d.ts +186 -0
- package/lib/otel/hooks/index.js +532 -0
- package/lib/otel/index.d.ts +34 -0
- package/lib/otel/index.js +23 -0
- package/lib/otel/types.d.ts +190 -0
- package/lib/otel/types.js +5 -0
- package/lib/otel/utils.d.ts +33 -0
- package/lib/otel/utils.js +181 -0
- package/lib/otel/views/OtelLive.d.ts +12 -0
- package/lib/otel/views/OtelLive.js +372 -0
- package/lib/otel/views/OtelLogsList.d.ts +11 -0
- package/lib/otel/views/OtelLogsList.js +137 -0
- package/lib/otel/views/OtelMetricsChart.d.ts +22 -0
- package/lib/otel/views/OtelMetricsChart.js +300 -0
- package/lib/otel/views/OtelMetricsList.d.ts +15 -0
- package/lib/otel/views/OtelMetricsList.js +213 -0
- package/lib/otel/views/OtelSearchBar.d.ts +11 -0
- package/lib/otel/views/OtelSearchBar.js +22 -0
- package/lib/otel/views/OtelSpanDetail.d.ts +11 -0
- package/lib/otel/views/OtelSpanDetail.js +172 -0
- package/lib/otel/views/OtelSpanTree.d.ts +11 -0
- package/lib/otel/views/OtelSpanTree.js +176 -0
- package/lib/otel/views/OtelSqlView.d.ts +16 -0
- package/lib/otel/views/OtelSqlView.js +239 -0
- package/lib/otel/views/OtelSystemView.d.ts +15 -0
- package/lib/otel/views/OtelSystemView.js +75 -0
- package/lib/otel/views/OtelTimeline.d.ts +11 -0
- package/lib/otel/views/OtelTimeline.js +101 -0
- package/lib/otel/views/OtelTimelineRangeSlider.d.ts +16 -0
- package/lib/otel/views/OtelTimelineRangeSlider.js +338 -0
- package/lib/otel/views/OtelTracesList.d.ts +13 -0
- package/lib/otel/views/OtelTracesList.js +199 -0
- package/lib/otel/views/index.d.ts +20 -0
- package/lib/otel/views/index.js +21 -0
- package/lib/state/storage/IAMStorage.d.ts +2 -1
- package/lib/state/substates/CoreState.js +7 -6
- package/lib/utils/Date.d.ts +6 -0
- package/lib/utils/Date.js +37 -0
- package/lib/utils/Jwt.d.ts +42 -0
- package/lib/utils/Jwt.js +44 -0
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +1 -0
- package/lib/views/iam/SignInSimple.d.ts +43 -0
- package/lib/views/iam/SignInSimple.js +113 -0
- package/lib/views/iam/index.d.ts +2 -0
- package/lib/views/iam/index.js +5 -0
- package/lib/views/iam-tokens/IAMTokenEdit.d.ts +5 -1
- package/lib/views/iam-tokens/IAMTokenEdit.js +54 -5
- package/lib/views/iam-tokens/IAMTokenNew.js +2 -2
- package/lib/views/iam-tokens/IAMTokens.d.ts +4 -2
- package/lib/views/iam-tokens/IAMTokens.js +68 -36
- package/lib/views/iam-tokens/Tokens.js +63 -31
- package/lib/views/index.d.ts +3 -1
- package/lib/views/index.js +3 -1
- package/lib/views/otel/DashboardView.d.ts +16 -0
- package/lib/views/otel/DashboardView.js +4 -0
- package/lib/views/otel/LogsView.d.ts +12 -0
- package/lib/views/otel/LogsView.js +4 -0
- package/lib/views/otel/MetricsView.d.ts +12 -0
- package/lib/views/otel/MetricsView.js +4 -0
- package/lib/views/otel/OtelHeader.d.ts +33 -0
- package/lib/views/otel/OtelHeader.js +105 -0
- package/lib/views/otel/SqlView.d.ts +9 -0
- package/lib/views/otel/SqlView.js +4 -0
- package/lib/views/otel/SystemView.d.ts +9 -0
- package/lib/views/otel/SystemView.js +4 -0
- package/lib/views/otel/TracesView.d.ts +12 -0
- package/lib/views/otel/TracesView.js +4 -0
- package/lib/views/otel/index.d.ts +16 -0
- package/lib/views/otel/index.js +12 -0
- package/lib/views/otel/simpleAuthStore.d.ts +21 -0
- package/lib/views/otel/simpleAuthStore.js +22 -0
- package/lib/views/profile/UserBadge.d.ts +20 -0
- package/lib/views/profile/UserBadge.js +101 -0
- package/lib/views/profile/index.d.ts +2 -0
- package/lib/views/profile/index.js +5 -0
- package/package.json +3 -4
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* OtelLive – Full-featured observability dashboard that combines the
|
|
8
|
+
* search bar, signal list, timeline, span tree, and detail panel into
|
|
9
|
+
* a Logfire-inspired experience.
|
|
10
|
+
*
|
|
11
|
+
* Uses Primer React components for consistent theming.
|
|
12
|
+
*
|
|
13
|
+
* @module otel/OtelLive
|
|
14
|
+
*/
|
|
15
|
+
import { useState, useCallback, useMemo, useEffect, useRef, } from 'react';
|
|
16
|
+
import { Box, Text, Button, Label } from '@primer/react';
|
|
17
|
+
import { GitBranchIcon, ClockIcon } from '@primer/octicons-react';
|
|
18
|
+
import { coreStore } from '../../state/substates/CoreState';
|
|
19
|
+
import { useOtelTraces, useOtelTrace, useOtelLogs, useOtelMetrics, useOtelServices, useOtelWebSocket, } from '../hooks';
|
|
20
|
+
import { OtelSearchBar } from './OtelSearchBar';
|
|
21
|
+
import { OtelTimelineRangeSlider } from './OtelTimelineRangeSlider';
|
|
22
|
+
import { OtelTracesList } from './OtelTracesList';
|
|
23
|
+
import { OtelLogsList } from './OtelLogsList';
|
|
24
|
+
import { OtelMetricsList } from './OtelMetricsList';
|
|
25
|
+
import { OtelSpanDetail } from './OtelSpanDetail';
|
|
26
|
+
import { OtelTimeline } from './OtelTimeline';
|
|
27
|
+
import { OtelSpanTree } from './OtelSpanTree';
|
|
28
|
+
import { buildSpanTree } from '../utils';
|
|
29
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
30
|
+
const BOTTOM_PANE_VIEWS = ['timeline', 'tree'];
|
|
31
|
+
const HISTOGRAM_BUCKETS = 60;
|
|
32
|
+
/** Extract timestamp from any signal record. */
|
|
33
|
+
function signalTs(signal, item) {
|
|
34
|
+
if (signal === 'traces')
|
|
35
|
+
return new Date(item.start_time).getTime();
|
|
36
|
+
return new Date(item.timestamp).getTime();
|
|
37
|
+
}
|
|
38
|
+
/** Build histogram buckets from a list of raw timestamps. */
|
|
39
|
+
function buildHistogram(timestamps, start, end, buckets) {
|
|
40
|
+
const range = end - start || 1;
|
|
41
|
+
const step = range / buckets;
|
|
42
|
+
const counts = new Array(buckets).fill(0);
|
|
43
|
+
for (const ts of timestamps) {
|
|
44
|
+
const idx = Math.min(Math.floor((ts - start) / step), buckets - 1);
|
|
45
|
+
if (idx >= 0)
|
|
46
|
+
counts[idx]++;
|
|
47
|
+
}
|
|
48
|
+
return counts.map((count, i) => ({
|
|
49
|
+
time: new Date(start + i * step + step / 2),
|
|
50
|
+
count,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
// ── OtelLive ────────────────────────────────────────────────────────
|
|
54
|
+
export const OtelLive = ({ baseUrl = coreStore.getState().configuration.otelRunUrl, wsBaseUrl, token, autoRefreshMs = 5000, defaultSignal = 'traces', limit = 200, onSignalRef, }) => {
|
|
55
|
+
// ── state ──
|
|
56
|
+
const [signal, setSignalState] = useState(() => {
|
|
57
|
+
try {
|
|
58
|
+
const match = document.cookie.match(/(?:^|;\s*)otel_signal=([^;]+)/);
|
|
59
|
+
if (match && ['traces', 'logs', 'metrics'].includes(match[1])) {
|
|
60
|
+
return match[1];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// ignore
|
|
65
|
+
}
|
|
66
|
+
return defaultSignal;
|
|
67
|
+
});
|
|
68
|
+
const setSignal = (s) => {
|
|
69
|
+
try {
|
|
70
|
+
document.cookie = `otel_signal=${s};path=/;max-age=31536000`;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// ignore
|
|
74
|
+
}
|
|
75
|
+
setSignalState(s);
|
|
76
|
+
};
|
|
77
|
+
// Expose signal setter to parent so external controls (e.g. generate
|
|
78
|
+
// buttons) can navigate to the right tab.
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
onSignalRef?.(setSignal);
|
|
81
|
+
}, [onSignalRef]);
|
|
82
|
+
const [service, setService] = useState('');
|
|
83
|
+
const [query, setQuery] = useState('');
|
|
84
|
+
const [selectedSpan, setSelectedSpan] = useState(null);
|
|
85
|
+
const [selectedLogIdx, setSelectedLogIdx] = useState(null);
|
|
86
|
+
const [bottomPane, setBottomPane] = useState(null);
|
|
87
|
+
const [rangeStart, setRangeStart] = useState(null);
|
|
88
|
+
const [rangeEnd, setRangeEnd] = useState(null);
|
|
89
|
+
// Refs so the bounds-change effect can read current range without making
|
|
90
|
+
// rangeStart/rangeEnd deps (which would cause infinite loops).
|
|
91
|
+
const rangeStartRef = useRef(null);
|
|
92
|
+
const rangeEndRef = useRef(null);
|
|
93
|
+
rangeStartRef.current = rangeStart;
|
|
94
|
+
rangeEndRef.current = rangeEnd;
|
|
95
|
+
const prevBoundsRef = useRef(null);
|
|
96
|
+
// ── data hooks ──
|
|
97
|
+
const { traces, loading: tracesLoading, refetch: refetchTraces, } = useOtelTraces({
|
|
98
|
+
baseUrl,
|
|
99
|
+
token,
|
|
100
|
+
limit,
|
|
101
|
+
serviceName: service || undefined,
|
|
102
|
+
autoRefreshMs,
|
|
103
|
+
});
|
|
104
|
+
const { logs, loading: logsLoading, refetch: refetchLogs, } = useOtelLogs({
|
|
105
|
+
baseUrl,
|
|
106
|
+
token,
|
|
107
|
+
limit,
|
|
108
|
+
serviceName: service || undefined,
|
|
109
|
+
autoRefreshMs,
|
|
110
|
+
});
|
|
111
|
+
const { metrics, loading: metricsLoading, refetch: refetchMetrics, } = useOtelMetrics({
|
|
112
|
+
baseUrl,
|
|
113
|
+
token,
|
|
114
|
+
limit,
|
|
115
|
+
serviceName: service || undefined,
|
|
116
|
+
autoRefreshMs,
|
|
117
|
+
});
|
|
118
|
+
const { services } = useOtelServices({ baseUrl, token });
|
|
119
|
+
const effectiveServices = useMemo(() => {
|
|
120
|
+
const fromApi = (services ?? []).map(s => s.trim()).filter(Boolean);
|
|
121
|
+
const fromTraces = (traces ?? [])
|
|
122
|
+
.map(s => (s.service_name ?? '').trim())
|
|
123
|
+
.filter(Boolean);
|
|
124
|
+
const fromLogs = (logs ?? [])
|
|
125
|
+
.map(l => (l.service_name ?? '').trim())
|
|
126
|
+
.filter(Boolean);
|
|
127
|
+
const fromMetrics = (metrics ?? [])
|
|
128
|
+
.map(m => (m.service_name ?? '').trim())
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
const fromSignal = signal === 'traces'
|
|
131
|
+
? fromTraces
|
|
132
|
+
: signal === 'logs'
|
|
133
|
+
? fromLogs
|
|
134
|
+
: fromMetrics;
|
|
135
|
+
return Array.from(new Set([...fromApi, ...fromSignal])).sort((a, b) => a.localeCompare(b));
|
|
136
|
+
}, [services, traces, logs, metrics, signal]);
|
|
137
|
+
// ── WebSocket live updates ──
|
|
138
|
+
// When a WS message arrives for a signal, refetch the corresponding hook
|
|
139
|
+
// so the data stays fresh without polling.
|
|
140
|
+
const wsCallbacks = useMemo(() => ({
|
|
141
|
+
onTraces: () => void refetchTraces(),
|
|
142
|
+
onLogs: () => void refetchLogs(),
|
|
143
|
+
onMetrics: () => void refetchMetrics(),
|
|
144
|
+
}), [refetchTraces, refetchLogs, refetchMetrics]);
|
|
145
|
+
const { connected: wsConnected } = useOtelWebSocket({
|
|
146
|
+
baseUrl: wsBaseUrl ?? baseUrl,
|
|
147
|
+
token,
|
|
148
|
+
callbacks: wsCallbacks,
|
|
149
|
+
});
|
|
150
|
+
// trace-detail fetch (when a span is selected)
|
|
151
|
+
const { spans: traceSpans } = useOtelTrace({
|
|
152
|
+
baseUrl,
|
|
153
|
+
token,
|
|
154
|
+
traceId: selectedSpan?.trace_id ?? '',
|
|
155
|
+
});
|
|
156
|
+
// Build tree from traceSpans for bottom pane
|
|
157
|
+
const spanTree = useMemo(() => traceSpans && traceSpans.length > 0 ? buildSpanTree(traceSpans) : [], [traceSpans]);
|
|
158
|
+
// ── Timeline range slider data ──
|
|
159
|
+
const allTimestamps = useMemo(() => {
|
|
160
|
+
const ts = [];
|
|
161
|
+
if (traces)
|
|
162
|
+
for (const s of traces)
|
|
163
|
+
ts.push(signalTs('traces', s));
|
|
164
|
+
if (logs)
|
|
165
|
+
for (const l of logs)
|
|
166
|
+
ts.push(signalTs('logs', l));
|
|
167
|
+
if (metrics)
|
|
168
|
+
for (const m of metrics)
|
|
169
|
+
ts.push(signalTs('metrics', m));
|
|
170
|
+
// Drop invalid timestamps (NaN) so the slider doesn't collapse
|
|
171
|
+
return ts.filter(t => Number.isFinite(t) && t > 0);
|
|
172
|
+
}, [traces, logs, metrics]);
|
|
173
|
+
const timelineBounds = useMemo(() => {
|
|
174
|
+
if (allTimestamps.length === 0)
|
|
175
|
+
return null;
|
|
176
|
+
const min = Math.min(...allTimestamps);
|
|
177
|
+
const max = Math.max(...allTimestamps);
|
|
178
|
+
// Add a small padding (2 %) so edge items aren't clipped
|
|
179
|
+
const pad = Math.max((max - min) * 0.02, 1000);
|
|
180
|
+
return { start: new Date(min - pad), end: new Date(max + pad) };
|
|
181
|
+
}, [allTimestamps]);
|
|
182
|
+
const histogram = useMemo(() => {
|
|
183
|
+
if (!timelineBounds || allTimestamps.length === 0)
|
|
184
|
+
return undefined;
|
|
185
|
+
return buildHistogram(allTimestamps, timelineBounds.start.getTime(), timelineBounds.end.getTime(), HISTOGRAM_BUCKETS);
|
|
186
|
+
}, [allTimestamps, timelineBounds]);
|
|
187
|
+
// Auto-adapt slider range when data bounds change.
|
|
188
|
+
// Behaviour:
|
|
189
|
+
// – First load (rangeStart is null): initialise to the full range.
|
|
190
|
+
// – Subsequent updates: if the user's range handle was at the previous
|
|
191
|
+
// bounds edge (within 600 ms), advance it to the new edge so the view
|
|
192
|
+
// "follows" live data. If the handle was moved inward, leave it alone.
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (!timelineBounds)
|
|
195
|
+
return;
|
|
196
|
+
const prev = prevBoundsRef.current;
|
|
197
|
+
prevBoundsRef.current = {
|
|
198
|
+
start: timelineBounds.start.getTime(),
|
|
199
|
+
end: timelineBounds.end.getTime(),
|
|
200
|
+
};
|
|
201
|
+
// First load
|
|
202
|
+
if (!prev ||
|
|
203
|
+
rangeStartRef.current === null ||
|
|
204
|
+
rangeEndRef.current === null) {
|
|
205
|
+
setRangeStart(timelineBounds.start);
|
|
206
|
+
setRangeEnd(timelineBounds.end);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const STICKY_THRESHOLD = 600; // ms – how close to the edge counts as "at edge"
|
|
210
|
+
if (rangeEndRef.current.getTime() >= prev.end - STICKY_THRESHOLD) {
|
|
211
|
+
setRangeEnd(timelineBounds.end);
|
|
212
|
+
}
|
|
213
|
+
if (rangeStartRef.current.getTime() <= prev.start + STICKY_THRESHOLD) {
|
|
214
|
+
setRangeStart(timelineBounds.start);
|
|
215
|
+
}
|
|
216
|
+
}, [timelineBounds?.start.getTime(), timelineBounds?.end.getTime()]);
|
|
217
|
+
// Reset selection on signal change
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
setSelectedSpan(null);
|
|
220
|
+
setSelectedLogIdx(null);
|
|
221
|
+
setBottomPane(null);
|
|
222
|
+
}, [signal]);
|
|
223
|
+
// ── handlers ──
|
|
224
|
+
const handleRefresh = useCallback(() => {
|
|
225
|
+
if (signal === 'traces')
|
|
226
|
+
void refetchTraces();
|
|
227
|
+
if (signal === 'logs')
|
|
228
|
+
void refetchLogs();
|
|
229
|
+
if (signal === 'metrics')
|
|
230
|
+
void refetchMetrics();
|
|
231
|
+
}, [signal, refetchTraces, refetchLogs, refetchMetrics]);
|
|
232
|
+
const handleSpanSelect = useCallback((span) => {
|
|
233
|
+
setSelectedSpan(span);
|
|
234
|
+
setSelectedLogIdx(null);
|
|
235
|
+
}, []);
|
|
236
|
+
const handleLogSelect = useCallback((log, idx) => {
|
|
237
|
+
setSelectedLogIdx(idx);
|
|
238
|
+
setSelectedSpan(null);
|
|
239
|
+
}, []);
|
|
240
|
+
const handleCloseDetail = useCallback(() => {
|
|
241
|
+
setSelectedSpan(null);
|
|
242
|
+
setSelectedLogIdx(null);
|
|
243
|
+
}, []);
|
|
244
|
+
const handleRangeChange = useCallback((start, end) => {
|
|
245
|
+
setRangeStart(start);
|
|
246
|
+
setRangeEnd(end);
|
|
247
|
+
}, []);
|
|
248
|
+
const toggleBottomPane = useCallback((pane) => setBottomPane(cur => (cur === pane ? null : pane)), []);
|
|
249
|
+
const hasDetail = (signal === 'traces' && selectedSpan !== null) ||
|
|
250
|
+
(signal === 'logs' && selectedLogIdx !== null);
|
|
251
|
+
// ── Time-range-filtered data ──
|
|
252
|
+
const isRangeActive = rangeStart !== null &&
|
|
253
|
+
rangeEnd !== null &&
|
|
254
|
+
timelineBounds !== null &&
|
|
255
|
+
(rangeStart.getTime() > timelineBounds.start.getTime() + 100 ||
|
|
256
|
+
rangeEnd.getTime() < timelineBounds.end.getTime() - 100);
|
|
257
|
+
const filteredTraces = useMemo(() => {
|
|
258
|
+
const list = filterSpans(traces ?? [], query);
|
|
259
|
+
if (!isRangeActive || !rangeStart || !rangeEnd)
|
|
260
|
+
return list;
|
|
261
|
+
const s = rangeStart.getTime();
|
|
262
|
+
const e = rangeEnd.getTime();
|
|
263
|
+
return list.filter(t => {
|
|
264
|
+
const ts = new Date(t.start_time).getTime();
|
|
265
|
+
return ts >= s && ts <= e;
|
|
266
|
+
});
|
|
267
|
+
}, [traces, query, isRangeActive, rangeStart, rangeEnd]);
|
|
268
|
+
const filteredLogs = useMemo(() => {
|
|
269
|
+
const list = filterLogs(logs ?? [], query);
|
|
270
|
+
if (!isRangeActive || !rangeStart || !rangeEnd)
|
|
271
|
+
return list;
|
|
272
|
+
const s = rangeStart.getTime();
|
|
273
|
+
const e = rangeEnd.getTime();
|
|
274
|
+
return list.filter(l => {
|
|
275
|
+
const ts = new Date(l.timestamp).getTime();
|
|
276
|
+
return ts >= s && ts <= e;
|
|
277
|
+
});
|
|
278
|
+
}, [logs, query, isRangeActive, rangeStart, rangeEnd]);
|
|
279
|
+
const filteredMetrics = useMemo(() => {
|
|
280
|
+
const list = metrics ?? [];
|
|
281
|
+
if (!isRangeActive || !rangeStart || !rangeEnd)
|
|
282
|
+
return list;
|
|
283
|
+
const s = rangeStart.getTime();
|
|
284
|
+
const e = rangeEnd.getTime();
|
|
285
|
+
return list.filter(m => {
|
|
286
|
+
const ts = new Date(m.timestamp).getTime();
|
|
287
|
+
return ts >= s && ts <= e;
|
|
288
|
+
});
|
|
289
|
+
}, [metrics, isRangeActive, rangeStart, rangeEnd]);
|
|
290
|
+
return (_jsxs(Box, { sx: {
|
|
291
|
+
display: 'flex',
|
|
292
|
+
flexDirection: 'column',
|
|
293
|
+
flex: 1,
|
|
294
|
+
minHeight: 0,
|
|
295
|
+
height: '100%',
|
|
296
|
+
color: 'fg.default',
|
|
297
|
+
bg: 'canvas.default',
|
|
298
|
+
border: '1px solid',
|
|
299
|
+
borderColor: 'border.default',
|
|
300
|
+
borderRadius: 2,
|
|
301
|
+
overflow: 'hidden',
|
|
302
|
+
}, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0 }, children: [_jsx(Box, { sx: { flex: 1 }, children: _jsx(OtelSearchBar, { signal: signal, onSignalChange: setSignal, services: effectiveServices, selectedService: service, onServiceChange: setService, query: query, onQueryChange: setQuery, onRefresh: handleRefresh, loading: signal === 'traces'
|
|
303
|
+
? tracesLoading
|
|
304
|
+
: signal === 'logs'
|
|
305
|
+
? logsLoading
|
|
306
|
+
: metricsLoading }) }), _jsx(Box, { sx: { pr: 2, flexShrink: 0 }, children: _jsx(Label, { variant: wsConnected ? 'success' : 'secondary', size: "small", children: wsConnected ? '● Live' : '○ Polling' }) })] }), timelineBounds && rangeStart && rangeEnd && (_jsx(Box, { sx: {
|
|
307
|
+
px: 3,
|
|
308
|
+
pt: 2,
|
|
309
|
+
pb: 1,
|
|
310
|
+
borderBottom: '1px solid',
|
|
311
|
+
borderColor: 'border.default',
|
|
312
|
+
bg: 'canvas.subtle',
|
|
313
|
+
flexShrink: 0,
|
|
314
|
+
position: 'relative',
|
|
315
|
+
zIndex: 0,
|
|
316
|
+
}, children: _jsx(OtelTimelineRangeSlider, { timelineStart: timelineBounds.start, timelineEnd: timelineBounds.end, selectedStart: rangeStart, selectedEnd: rangeEnd, onRangeChange: handleRangeChange, histogram: histogram, height: 48, tickCount: 6 }) })), _jsxs(Box, { sx: { display: 'flex', flex: 1, minHeight: 0, overflow: 'hidden' }, children: [_jsxs(Box, { sx: {
|
|
317
|
+
display: 'flex',
|
|
318
|
+
flexDirection: 'column',
|
|
319
|
+
flex: hasDetail ? '0 0 55%' : '1 1 100%',
|
|
320
|
+
minHeight: 0,
|
|
321
|
+
overflow: 'hidden',
|
|
322
|
+
}, children: [signal === 'traces' && (_jsx(OtelTracesList, { spans: filteredTraces, loading: tracesLoading, selectedSpanId: selectedSpan?.span_id, onSelectSpan: handleSpanSelect })), signal === 'logs' && (_jsx(OtelLogsList, { logs: filteredLogs, loading: logsLoading, selectedLogIndex: selectedLogIdx, onSelectLog: handleLogSelect })), signal === 'metrics' && (_jsx(OtelMetricsList, { metrics: filteredMetrics, loading: metricsLoading }))] }), hasDetail && (_jsxs(Box, { sx: {
|
|
323
|
+
flex: '0 0 45%',
|
|
324
|
+
minHeight: 0,
|
|
325
|
+
overflow: 'auto',
|
|
326
|
+
borderLeft: '1px solid',
|
|
327
|
+
borderColor: 'border.default',
|
|
328
|
+
}, children: [signal === 'traces' && selectedSpan && (_jsx(OtelSpanDetail, { span: selectedSpan, traceSpans: traceSpans ?? undefined, onClose: handleCloseDetail })), signal === 'logs' && selectedLogIdx !== null && logs && (_jsxs(Box, { sx: { p: 3 }, children: [_jsxs(Box, { sx: {
|
|
329
|
+
display: 'flex',
|
|
330
|
+
justifyContent: 'space-between',
|
|
331
|
+
mb: 3,
|
|
332
|
+
}, children: [_jsx(Text, { sx: { fontWeight: 'bold', fontSize: 2 }, children: "Log Detail" }), _jsx(Button, { size: "small", variant: "invisible", onClick: handleCloseDetail, children: "\u2715" })] }), _jsx(Box, { as: "pre", sx: {
|
|
333
|
+
m: 0,
|
|
334
|
+
fontSize: 1,
|
|
335
|
+
fontFamily: 'mono',
|
|
336
|
+
whiteSpace: 'pre-wrap',
|
|
337
|
+
wordBreak: 'break-word',
|
|
338
|
+
}, children: JSON.stringify(logs[selectedLogIdx], null, 2) })] }))] }))] }), signal === 'traces' && selectedSpan && spanTree.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Box, { sx: {
|
|
339
|
+
display: 'flex',
|
|
340
|
+
gap: 1,
|
|
341
|
+
px: 3,
|
|
342
|
+
py: 1,
|
|
343
|
+
bg: 'canvas.subtle',
|
|
344
|
+
borderTop: '1px solid',
|
|
345
|
+
borderColor: 'border.default',
|
|
346
|
+
flexShrink: 0,
|
|
347
|
+
}, children: [_jsx(Button, { size: "small", variant: bottomPane === 'timeline' ? 'primary' : 'invisible', leadingVisual: ClockIcon, onClick: () => toggleBottomPane('timeline'), children: "Timeline" }), _jsx(Button, { size: "small", variant: bottomPane === 'tree' ? 'primary' : 'invisible', leadingVisual: GitBranchIcon, onClick: () => toggleBottomPane('tree'), children: "Span Tree" })] }), bottomPane && (_jsxs(Box, { sx: {
|
|
348
|
+
height: 260,
|
|
349
|
+
overflow: 'auto',
|
|
350
|
+
borderTop: '1px solid',
|
|
351
|
+
borderColor: 'border.default',
|
|
352
|
+
flexShrink: 0,
|
|
353
|
+
}, children: [bottomPane === 'timeline' && (_jsx(OtelTimeline, { spans: traceSpans ?? [], selectedSpanId: selectedSpan?.span_id, onSelectSpan: handleSpanSelect })), bottomPane === 'tree' && (_jsx(OtelSpanTree, { spans: spanTree, selectedSpanId: selectedSpan?.span_id, onSelectSpan: handleSpanSelect, defaultExpandDepth: 3 }))] }))] }))] }));
|
|
354
|
+
};
|
|
355
|
+
// ── Client-side filter helpers ──────────────────────────────────────
|
|
356
|
+
function filterSpans(spans, q) {
|
|
357
|
+
if (!q.trim())
|
|
358
|
+
return spans;
|
|
359
|
+
const lq = q.toLowerCase();
|
|
360
|
+
return spans.filter(s => s.span_name.toLowerCase().includes(lq) ||
|
|
361
|
+
s.service_name.toLowerCase().includes(lq) ||
|
|
362
|
+
(s.otel_scope_name ?? '').toLowerCase().includes(lq) ||
|
|
363
|
+
(s.status_message ?? '').toLowerCase().includes(lq));
|
|
364
|
+
}
|
|
365
|
+
function filterLogs(logs, q) {
|
|
366
|
+
if (!q.trim())
|
|
367
|
+
return logs;
|
|
368
|
+
const lq = q.toLowerCase();
|
|
369
|
+
return logs.filter(l => l.body.toLowerCase().includes(lq) ||
|
|
370
|
+
l.service_name.toLowerCase().includes(lq) ||
|
|
371
|
+
l.severity_text.toLowerCase().includes(lq));
|
|
372
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OtelLogsList – Tabular log-records view with severity colour coding,
|
|
3
|
+
* expandable body/attributes, and trace correlation links.
|
|
4
|
+
*
|
|
5
|
+
* Uses Primer React components for consistent theming.
|
|
6
|
+
*
|
|
7
|
+
* @module otel/OtelLogsList
|
|
8
|
+
*/
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import type { OtelLogsListProps } from '../types';
|
|
11
|
+
export declare const OtelLogsList: React.FC<OtelLogsListProps>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* OtelLogsList – Tabular log-records view with severity colour coding,
|
|
8
|
+
* expandable body/attributes, and trace correlation links.
|
|
9
|
+
*
|
|
10
|
+
* Uses Primer React components for consistent theming.
|
|
11
|
+
*
|
|
12
|
+
* @module otel/OtelLogsList
|
|
13
|
+
*/
|
|
14
|
+
import React, { useState } from 'react';
|
|
15
|
+
import { Box, Text, Label, Spinner } from '@primer/react';
|
|
16
|
+
import { Blankslate } from '@primer/react/experimental';
|
|
17
|
+
import { LogIcon } from '@primer/octicons-react';
|
|
18
|
+
import { formatTime, severityVariant } from '../utils';
|
|
19
|
+
// ── helpers ─────────────────────────────────────────────────────────
|
|
20
|
+
/** Severity badge using Primer Label, centered in its grid cell. */
|
|
21
|
+
const Severity = ({ text }) => (_jsx(Box, { sx: { display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: _jsx(Label, { size: "small", variant: severityVariant(text), children: text }) }));
|
|
22
|
+
/** Expandable row detail for a single log record. */
|
|
23
|
+
const LogDetail = ({ log }) => (_jsxs(Box, { sx: {
|
|
24
|
+
gridColumn: '1 / -1',
|
|
25
|
+
bg: 'canvas.subtle',
|
|
26
|
+
borderBottom: '1px solid',
|
|
27
|
+
borderColor: 'border.default',
|
|
28
|
+
p: 3,
|
|
29
|
+
}, children: [_jsxs(Box, { sx: { mb: 2 }, children: [_jsx(Text, { sx: {
|
|
30
|
+
fontSize: 0,
|
|
31
|
+
fontWeight: 'bold',
|
|
32
|
+
color: 'fg.muted',
|
|
33
|
+
display: 'block',
|
|
34
|
+
mb: 1,
|
|
35
|
+
}, children: "Body" }), _jsx(Box, { as: "pre", sx: {
|
|
36
|
+
fontSize: 1,
|
|
37
|
+
fontFamily: 'mono',
|
|
38
|
+
whiteSpace: 'pre-wrap',
|
|
39
|
+
wordBreak: 'break-word',
|
|
40
|
+
m: 0,
|
|
41
|
+
bg: 'canvas.default',
|
|
42
|
+
border: '1px solid',
|
|
43
|
+
borderColor: 'border.default',
|
|
44
|
+
borderRadius: 2,
|
|
45
|
+
p: 2,
|
|
46
|
+
}, children: log.body })] }), log.trace_id && (_jsxs(Box, { sx: { display: 'flex', gap: 3, mb: 2 }, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', fontWeight: 'bold' }, children: "trace_id" }), _jsx(Text, { sx: { fontFamily: 'mono', fontSize: 0 }, children: log.trace_id }), log.span_id && (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', fontWeight: 'bold' }, children: "span_id" }), _jsx(Text, { sx: { fontFamily: 'mono', fontSize: 0 }, children: log.span_id })] }))] })), log.attributes && Object.keys(log.attributes).length > 0 && (_jsxs(Box, { children: [_jsx(Text, { sx: {
|
|
47
|
+
fontSize: 0,
|
|
48
|
+
fontWeight: 'bold',
|
|
49
|
+
color: 'fg.muted',
|
|
50
|
+
display: 'block',
|
|
51
|
+
mb: 1,
|
|
52
|
+
}, children: "Attributes" }), _jsx(Box, { sx: {
|
|
53
|
+
bg: 'canvas.default',
|
|
54
|
+
border: '1px solid',
|
|
55
|
+
borderColor: 'border.default',
|
|
56
|
+
borderRadius: 2,
|
|
57
|
+
p: 2,
|
|
58
|
+
}, children: Object.entries(log.attributes).map(([k, v]) => (_jsxs(Box, { sx: {
|
|
59
|
+
display: 'flex',
|
|
60
|
+
gap: 2,
|
|
61
|
+
py: 1,
|
|
62
|
+
borderBottom: '1px solid',
|
|
63
|
+
borderColor: 'border.muted',
|
|
64
|
+
}, children: [_jsx(Text, { sx: {
|
|
65
|
+
color: 'accent.fg',
|
|
66
|
+
fontSize: 0,
|
|
67
|
+
fontFamily: 'mono',
|
|
68
|
+
minWidth: 150,
|
|
69
|
+
}, children: k }), _jsx(Text, { sx: {
|
|
70
|
+
fontSize: 0,
|
|
71
|
+
fontFamily: 'mono',
|
|
72
|
+
wordBreak: 'break-word',
|
|
73
|
+
}, children: typeof v === 'string' ? v : JSON.stringify(v) })] }, k))) })] }))] }));
|
|
74
|
+
// ── Main component ──────────────────────────────────────────────────
|
|
75
|
+
export const OtelLogsList = ({ logs, loading, selectedLogIndex, onSelectLog, }) => {
|
|
76
|
+
const [expandedIdx, setExpandedIdx] = useState(null);
|
|
77
|
+
const colTemplate = '140px 80px 160px 1fr';
|
|
78
|
+
if (loading && logs.length === 0) {
|
|
79
|
+
return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', p: 5 }, children: _jsx(Spinner, { size: "medium" }) }));
|
|
80
|
+
}
|
|
81
|
+
if (!loading && logs.length === 0) {
|
|
82
|
+
return (_jsxs(Blankslate, { children: [_jsx(Blankslate.Visual, { children: _jsx(LogIcon, { size: 24 }) }), _jsx(Blankslate.Heading, { children: "No log records found" }), _jsx(Blankslate.Description, { children: "Send some log data first." })] }));
|
|
83
|
+
}
|
|
84
|
+
return (_jsxs(Box, { sx: { flex: 1, minHeight: 0, overflow: 'auto' }, children: [_jsx(Box, { sx: {
|
|
85
|
+
display: 'grid',
|
|
86
|
+
gridTemplateColumns: colTemplate,
|
|
87
|
+
bg: 'canvas.subtle',
|
|
88
|
+
borderBottom: '2px solid',
|
|
89
|
+
borderColor: 'border.default',
|
|
90
|
+
px: 3,
|
|
91
|
+
py: 1,
|
|
92
|
+
position: 'sticky',
|
|
93
|
+
top: 0,
|
|
94
|
+
zIndex: 1,
|
|
95
|
+
}, children: ['Time', 'Severity', 'Service', 'Body'].map(h => (_jsx(Text, { sx: {
|
|
96
|
+
fontSize: 0,
|
|
97
|
+
fontWeight: 'bold',
|
|
98
|
+
color: 'fg.muted',
|
|
99
|
+
textTransform: 'uppercase',
|
|
100
|
+
letterSpacing: '0.05em',
|
|
101
|
+
textAlign: h === 'Severity' ? 'center' : undefined,
|
|
102
|
+
}, children: h }, h))) }), logs.map((log, idx) => {
|
|
103
|
+
const selected = idx === selectedLogIndex;
|
|
104
|
+
const expanded = idx === expandedIdx;
|
|
105
|
+
return (_jsxs(React.Fragment, { children: [_jsxs(Box, { sx: {
|
|
106
|
+
display: 'grid',
|
|
107
|
+
gridTemplateColumns: colTemplate,
|
|
108
|
+
px: 3,
|
|
109
|
+
py: '5px',
|
|
110
|
+
borderBottom: '1px solid',
|
|
111
|
+
borderColor: 'border.muted',
|
|
112
|
+
cursor: 'pointer',
|
|
113
|
+
bg: selected
|
|
114
|
+
? 'accent.subtle'
|
|
115
|
+
: expanded
|
|
116
|
+
? 'canvas.subtle'
|
|
117
|
+
: 'canvas.default',
|
|
118
|
+
':hover': {
|
|
119
|
+
bg: selected || expanded ? undefined : 'canvas.subtle',
|
|
120
|
+
},
|
|
121
|
+
}, onClick: () => {
|
|
122
|
+
setExpandedIdx(expanded ? null : idx);
|
|
123
|
+
onSelectLog?.(log, idx);
|
|
124
|
+
}, children: [_jsx(Text, { sx: { fontSize: 1, fontFamily: 'mono', color: 'fg.default' }, children: formatTime(log.timestamp) }), _jsx(Severity, { text: log.severity_text }), _jsx(Text, { sx: {
|
|
125
|
+
fontSize: 1,
|
|
126
|
+
color: 'fg.muted',
|
|
127
|
+
overflow: 'hidden',
|
|
128
|
+
textOverflow: 'ellipsis',
|
|
129
|
+
whiteSpace: 'nowrap',
|
|
130
|
+
}, children: log.service_name }), _jsx(Text, { sx: {
|
|
131
|
+
fontSize: 1,
|
|
132
|
+
overflow: 'hidden',
|
|
133
|
+
textOverflow: 'ellipsis',
|
|
134
|
+
whiteSpace: 'nowrap',
|
|
135
|
+
}, children: log.body })] }), expanded && _jsx(LogDetail, { log: log })] }, idx));
|
|
136
|
+
})] }));
|
|
137
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OtelMetricsChart – Type-aware ECharts visualisation for OTEL metrics.
|
|
3
|
+
*
|
|
4
|
+
* Renders one chart section per `metric_type` (`sum`, `histogram`, `gauge`,
|
|
5
|
+
* and any other type present) with an appropriate chart style:
|
|
6
|
+
*
|
|
7
|
+
* - **sum** (counters): area-line chart with gradient fill
|
|
8
|
+
* - **histogram**: bar chart with grouped series
|
|
9
|
+
* - **gauge**: plain line chart (no area fill)
|
|
10
|
+
* - **other**: falls back to area-line
|
|
11
|
+
*
|
|
12
|
+
* @module otel/OtelMetricsChart
|
|
13
|
+
*/
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import type { OtelMetric } from '../types';
|
|
16
|
+
export interface OtelMetricsChartProps {
|
|
17
|
+
metrics: OtelMetric[];
|
|
18
|
+
/** Height per chart panel in px. Default 240. */
|
|
19
|
+
height?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const OtelMetricsChart: React.FC<OtelMetricsChartProps>;
|
|
22
|
+
export default OtelMetricsChart;
|