@datalayer/core 1.2.17 → 1.2.19

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.
Files changed (122) hide show
  1. package/lib/cli-login.js +5 -51
  2. package/lib/components/anonymous/AnonymousKeyExpired.js +1 -15
  3. package/lib/components/anonymous/AnonymousKeyTimer.js +5 -15
  4. package/lib/components/auth/SignIn.js +2 -2
  5. package/lib/components/avatars/UserAvatar.js +3 -27
  6. package/lib/components/billing/BillingEntitySelect.js +6 -48
  7. package/lib/components/checkout/StripeCheckout.js +33 -217
  8. package/lib/components/contents/ContentSourceCard.js +2 -11
  9. package/lib/components/contents/HomeFolderObjectsBrowser.d.ts +1 -1
  10. package/lib/components/contents/HomeFolderObjectsBrowser.js +11 -35
  11. package/lib/components/display/CenteredSpinner.js +1 -6
  12. package/lib/components/display/DatalayerBox.js +2 -14
  13. package/lib/components/flashes/FlashClosable.js +2 -17
  14. package/lib/components/mcp/ClientBadge.js +1 -1
  15. package/lib/components/mcp/McpErrorBlankslate.js +1 -1
  16. package/lib/components/mcp/ScopeList.js +1 -1
  17. package/lib/components/notebooks/JupyterNotebook.js +1 -2
  18. package/lib/components/principal/Principal.js +2 -10
  19. package/lib/components/principal/PrincipalAppearance.js +19 -63
  20. package/lib/components/principal/PrincipalAvatar.js +5 -25
  21. package/lib/components/principal/PrincipalBadge.js +3 -13
  22. package/lib/components/principal/PrincipalBanner.js +4 -32
  23. package/lib/components/principal/PrincipalDetailsOverlay.js +7 -23
  24. package/lib/components/principal/PrincipalHoverCard.js +15 -18
  25. package/lib/components/principal/PrincipalSwitcherMenu.js +15 -83
  26. package/lib/components/progress/ConsumptionBar.js +1 -1
  27. package/lib/components/sharing/ShareAccessComponent.js +21 -229
  28. package/lib/components/sharing/SharingEditor.js +1 -1
  29. package/lib/components/spaces/SpaceDetailsOverlay.js +4 -35
  30. package/lib/components/spaces/SpaceDisplay.js +1 -8
  31. package/lib/components/storage/ContentsBrowser.js +3 -26
  32. package/lib/components/storage/ContentsTable.js +4 -23
  33. package/lib/components/students/StudentResultCircle.js +7 -29
  34. package/lib/components/timeline/Timeline.js +4 -36
  35. package/lib/hooks/layouts/LayoutBackdrop.js +2 -1
  36. package/lib/hooks/layouts/LayoutScreencapture.js +1 -1
  37. package/lib/hooks/libraryApp.d.ts +6 -1
  38. package/lib/hooks/libraryApp.js +10 -1
  39. package/lib/hooks/useAuthorization.js +6 -1
  40. package/lib/mocks/components/FlashMock.js +3 -8
  41. package/lib/mocks/views/DashboardMock.js +6 -72
  42. package/lib/mocks/views/FormMock.js +3 -2
  43. package/lib/mocks/views/TableMock.js +3 -2
  44. package/lib/mocks/views/WipMock.js +2 -1
  45. package/lib/models/Person.d.ts +9 -0
  46. package/lib/models/Person.js +23 -0
  47. package/lib/otel/index.d.ts +4 -0
  48. package/lib/otel/index.js +3 -0
  49. package/lib/otel/live/filter.d.ts +15 -0
  50. package/lib/otel/live/filter.js +16 -0
  51. package/lib/otel/live/index.d.ts +10 -0
  52. package/lib/otel/live/index.js +14 -0
  53. package/lib/otel/live/tracer.d.ts +94 -0
  54. package/lib/otel/live/tracer.js +223 -0
  55. package/lib/otel/live/useOtelLiveSpans.d.ts +6 -0
  56. package/lib/otel/live/useOtelLiveSpans.js +22 -0
  57. package/lib/otel/types.d.ts +17 -0
  58. package/lib/otel/views/OtelLive.js +4 -62
  59. package/lib/otel/views/OtelLiveSpans.d.ts +52 -0
  60. package/lib/otel/views/OtelLiveSpans.js +114 -0
  61. package/lib/otel/views/OtelLogsList.js +13 -64
  62. package/lib/otel/views/OtelMetricsChart.js +4 -19
  63. package/lib/otel/views/OtelMetricsList.js +8 -77
  64. package/lib/otel/views/OtelSearchBar.js +3 -12
  65. package/lib/otel/views/OtelSpanDetail.d.ts +6 -0
  66. package/lib/otel/views/OtelSpanDetail.js +31 -81
  67. package/lib/otel/views/OtelSpanTree.js +7 -57
  68. package/lib/otel/views/OtelSqlView.js +5 -99
  69. package/lib/otel/views/OtelSystemView.js +4 -34
  70. package/lib/otel/views/OtelTimeline.js +7 -37
  71. package/lib/otel/views/OtelTimelineRangeSlider.js +7 -140
  72. package/lib/otel/views/OtelTracesList.js +39 -87
  73. package/lib/otel/views/index.d.ts +3 -0
  74. package/lib/otel/views/index.js +2 -0
  75. package/lib/portable/index.d.ts +8 -0
  76. package/lib/portable/index.js +12 -0
  77. package/lib/state/substates/NavigationState.d.ts +6 -4
  78. package/lib/state/substates/NavigationState.js +6 -5
  79. package/lib/views/api-keys/APIKeyEdit.js +2 -11
  80. package/lib/views/api-keys/APIKeyNew.js +3 -19
  81. package/lib/views/api-keys/APIKeys.js +6 -11
  82. package/lib/views/api-keys/APIKeysStandalone.js +1 -1
  83. package/lib/views/datasources/DatasourceDetail.d.ts +1 -1
  84. package/lib/views/datasources/DatasourceDetail.js +24 -17
  85. package/lib/views/datasources/DatasourceNew.js +2 -6
  86. package/lib/views/datasources/Datasources.js +2 -14
  87. package/lib/views/iam/SignInSimple.js +8 -64
  88. package/lib/views/mcp/AdmittedClients.js +3 -16
  89. package/lib/views/mcp/AlertDestinations.js +2 -10
  90. package/lib/views/mcp/AlertRules.js +4 -9
  91. package/lib/views/mcp/ApprovalQueue.js +4 -21
  92. package/lib/views/mcp/AuditLog.js +7 -44
  93. package/lib/views/mcp/ConnectedAgents.js +1 -6
  94. package/lib/views/mcp/EnterpriseConsole.js +10 -23
  95. package/lib/views/mcp/IdentityProviders.js +7 -31
  96. package/lib/views/mcp/McpDashboard.js +14 -30
  97. package/lib/views/mcp/McpHome.js +7 -54
  98. package/lib/views/mcp/McpObservability.js +13 -85
  99. package/lib/views/mcp/NotebookRuns.js +5 -12
  100. package/lib/views/mcp/OrganizationPolicy.js +3 -9
  101. package/lib/views/mcp/OrganizationUsage.js +4 -11
  102. package/lib/views/mcp/PersonalPolicy.js +3 -3
  103. package/lib/views/mcp/Policies.js +5 -46
  104. package/lib/views/mcp/PolicyForm.js +1 -9
  105. package/lib/views/mcp/PolicyHistory.js +2 -8
  106. package/lib/views/mcp/RunDetail.js +3 -25
  107. package/lib/views/mcp/ScimProvisioning.js +3 -15
  108. package/lib/views/mcp/ServiceAgents.js +4 -17
  109. package/lib/views/mcp/TeamPolicies.js +7 -19
  110. package/lib/views/mcp/ToolAccess.js +15 -22
  111. package/lib/views/mcp/TraceTimeline.js +8 -23
  112. package/lib/views/otel/DashboardView.js +2 -2
  113. package/lib/views/otel/LogsView.js +2 -2
  114. package/lib/views/otel/MetricsView.js +2 -2
  115. package/lib/views/otel/OtelHeader.js +6 -40
  116. package/lib/views/otel/SqlView.js +2 -2
  117. package/lib/views/otel/SystemView.js +2 -2
  118. package/lib/views/otel/TracesView.js +2 -2
  119. package/lib/views/profile/UserBadge.js +6 -98
  120. package/lib/views/secrets/SecretEdit.js +2 -11
  121. package/lib/views/secrets/SecretNew.js +1 -1
  122. package/package.json +1 -1
@@ -0,0 +1,223 @@
1
+ /*
2
+ * Copyright (c) 2023-2025 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** Random bytes as lowercase hex. */
6
+ function cryptoHex(bytes) {
7
+ const values = new Uint8Array(bytes);
8
+ if (globalThis.crypto?.getRandomValues) {
9
+ globalThis.crypto.getRandomValues(values);
10
+ }
11
+ else {
12
+ for (let index = 0; index < bytes; index += 1) {
13
+ values[index] = Math.floor(Math.random() * 256);
14
+ }
15
+ }
16
+ return Array.from(values, value => value.toString(16).padStart(2, '0')).join('');
17
+ }
18
+ /** A W3C `traceparent` header for a span: `00-<trace>-<span>-01`. */
19
+ export function traceparentOf(context) {
20
+ return `00-${context.trace_id}-${context.span_id}-01`;
21
+ }
22
+ /** Attributes without their `undefined` values. */
23
+ function defined(attributes) {
24
+ const kept = {};
25
+ for (const [key, value] of Object.entries(attributes ?? {})) {
26
+ if (value !== undefined) {
27
+ kept[key] = value;
28
+ }
29
+ }
30
+ return kept;
31
+ }
32
+ /** Make a tracer for the page. */
33
+ export function createOtelLiveTracer(options = {}) {
34
+ const serviceName = options.serviceName ?? 'browser';
35
+ const scopeName = options.scopeName ?? '@datalayer/core/otel/live';
36
+ const now = options.now ?? Date.now;
37
+ const limit = options.limit ?? 2000;
38
+ const randomHex = options.randomHex ?? cryptoHex;
39
+ const iso = (ms) => new Date(ms).toISOString();
40
+ let list = [];
41
+ /** The spans still open, by service, innermost last. */
42
+ const open = new Map();
43
+ const listeners = new Set();
44
+ let scheduled = false;
45
+ /** Bumped by `clear`: a span started before it is recorded no more. */
46
+ let generation = 0;
47
+ const notify = () => {
48
+ if (scheduled) {
49
+ return;
50
+ }
51
+ scheduled = true;
52
+ queueMicrotask(() => {
53
+ scheduled = false;
54
+ listeners.forEach(listener => listener());
55
+ });
56
+ };
57
+ const trim = () => {
58
+ let excess = list.length - limit;
59
+ if (excess <= 0) {
60
+ return;
61
+ }
62
+ list = list.filter(span => {
63
+ if (excess > 0 && !span.in_progress) {
64
+ excess -= 1;
65
+ return false;
66
+ }
67
+ return true;
68
+ });
69
+ };
70
+ const closeOpen = (service, spanId) => {
71
+ const stack = open.get(service);
72
+ if (!stack) {
73
+ return;
74
+ }
75
+ const kept = stack.filter(id => id !== spanId);
76
+ if (kept.length) {
77
+ open.set(service, kept);
78
+ }
79
+ else {
80
+ open.delete(service);
81
+ }
82
+ };
83
+ const tracer = {
84
+ serviceName,
85
+ startSpan(name, start = {}) {
86
+ const service = start.serviceName ?? serviceName;
87
+ const parent = start.parent === undefined
88
+ ? tracer.activeSpan(service)
89
+ : (start.parent ?? undefined);
90
+ const context = {
91
+ trace_id: parent?.trace_id ?? randomHex(16),
92
+ span_id: randomHex(8),
93
+ };
94
+ const startedAt = start.startTime ?? now();
95
+ const born = generation;
96
+ let current = {
97
+ trace_id: context.trace_id,
98
+ span_id: context.span_id,
99
+ ...(parent ? { parent_span_id: parent.span_id } : {}),
100
+ span_name: name,
101
+ service_name: service,
102
+ kind: start.kind ?? 'INTERNAL',
103
+ start_time: iso(startedAt),
104
+ end_time: iso(startedAt),
105
+ duration_ms: 0,
106
+ status_code: 'UNSET',
107
+ otel_scope_name: start.scopeName ?? scopeName,
108
+ attributes: defined(start.attributes),
109
+ events: [],
110
+ ...(start.links?.length ? { links: start.links } : {}),
111
+ in_progress: true,
112
+ };
113
+ list = [...list, current];
114
+ trim();
115
+ open.set(service, [...(open.get(service) ?? []), context.span_id]);
116
+ notify();
117
+ let ended = false;
118
+ const change = (next) => {
119
+ if (born !== generation) {
120
+ return;
121
+ }
122
+ const updated = { ...current, ...next };
123
+ list = list.map(span => (span === current ? updated : span));
124
+ current = updated;
125
+ notify();
126
+ };
127
+ const span = {
128
+ context,
129
+ get ended() {
130
+ return ended;
131
+ },
132
+ setAttribute(key, value) {
133
+ span.setAttributes({ [key]: value });
134
+ },
135
+ setAttributes(attributes) {
136
+ if (ended) {
137
+ return;
138
+ }
139
+ change({
140
+ attributes: { ...current.attributes, ...defined(attributes) },
141
+ });
142
+ },
143
+ addEvent(eventName, attributes, time) {
144
+ if (ended) {
145
+ return;
146
+ }
147
+ const event = {
148
+ name: eventName,
149
+ timestamp: iso(time ?? now()),
150
+ attributes: defined(attributes),
151
+ };
152
+ change({ events: [...(current.events ?? []), event] });
153
+ },
154
+ setStatus(code, message) {
155
+ if (ended) {
156
+ return;
157
+ }
158
+ change({
159
+ status_code: code,
160
+ ...(message ? { status_message: message } : {}),
161
+ });
162
+ },
163
+ recordException(error, time) {
164
+ const message = error instanceof Error ? error.message : String(error);
165
+ span.addEvent('exception', {
166
+ 'exception.type': error instanceof Error ? error.name : typeof error,
167
+ 'exception.message': message,
168
+ 'exception.stacktrace': error instanceof Error ? error.stack : undefined,
169
+ }, time);
170
+ span.setStatus('ERROR', message);
171
+ },
172
+ updateName(next) {
173
+ if (!ended) {
174
+ change({ span_name: next });
175
+ }
176
+ },
177
+ end(time) {
178
+ if (ended) {
179
+ return;
180
+ }
181
+ ended = true;
182
+ closeOpen(service, context.span_id);
183
+ const endedAt = Math.max(time ?? now(), startedAt);
184
+ change({
185
+ end_time: iso(endedAt),
186
+ duration_ms: endedAt - startedAt,
187
+ in_progress: false,
188
+ });
189
+ },
190
+ };
191
+ return span;
192
+ },
193
+ activeSpan(service = serviceName) {
194
+ const stack = open.get(service);
195
+ const spanId = stack?.[stack.length - 1];
196
+ const span = spanId
197
+ ? list.find(candidate => candidate.span_id === spanId)
198
+ : undefined;
199
+ return span
200
+ ? { trace_id: span.trace_id, span_id: span.span_id }
201
+ : undefined;
202
+ },
203
+ spans() {
204
+ return list;
205
+ },
206
+ subscribe(listener) {
207
+ listeners.add(listener);
208
+ return () => {
209
+ listeners.delete(listener);
210
+ };
211
+ },
212
+ clear() {
213
+ generation += 1;
214
+ list = [];
215
+ open.clear();
216
+ notify();
217
+ },
218
+ exportSpans() {
219
+ return { exportedAt: iso(now()), spans: [...list] };
220
+ },
221
+ };
222
+ return tracer;
223
+ }
@@ -0,0 +1,6 @@
1
+ import type { OtelSpan } from '../types';
2
+ import { type OtelLiveTracer } from './tracer';
3
+ /** A tracer's spans, kept current; none without a tracer. */
4
+ export declare function useOtelLiveSpans(tracer: OtelLiveTracer | null | undefined): readonly OtelSpan[];
5
+ /** A tracer made once for a component (or the one given). */
6
+ export declare function useOtelLiveTracer(given?: OtelLiveTracer | null, serviceName?: string): OtelLiveTracer;
@@ -0,0 +1,22 @@
1
+ /*
2
+ * Copyright (c) 2023-2025 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * A live tracer's spans, kept current in a component.
7
+ *
8
+ * @module otel/live/useOtelLiveSpans
9
+ */
10
+ import { useState, useSyncExternalStore } from 'react';
11
+ import { createOtelLiveTracer } from './tracer.js';
12
+ const NO_SPANS = [];
13
+ const NO_SUBSCRIPTION = () => () => undefined;
14
+ /** A tracer's spans, kept current; none without a tracer. */
15
+ export function useOtelLiveSpans(tracer) {
16
+ return useSyncExternalStore(tracer ? tracer.subscribe : NO_SUBSCRIPTION, () => (tracer ? tracer.spans() : NO_SPANS), () => (tracer ? tracer.spans() : NO_SPANS));
17
+ }
18
+ /** A tracer made once for a component (or the one given). */
19
+ export function useOtelLiveTracer(given, serviceName) {
20
+ const [own] = useState(() => given ?? createOtelLiveTracer({ serviceName }));
21
+ return given ?? own;
22
+ }
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * @module otel/types
5
5
  */
6
+ import type { ReactNode } from 'react';
6
7
  /** A single span in a trace. */
7
8
  export interface OtelSpan {
8
9
  trace_id: string;
@@ -20,6 +21,12 @@ export interface OtelSpan {
20
21
  attributes?: Record<string, unknown>;
21
22
  events?: OtelSpanEvent[];
22
23
  links?: OtelSpanLink[];
24
+ /**
25
+ * The span has started and not ended yet: a live, in-page span
26
+ * (`createOtelLiveTracer`). Its `end_time` is its `start_time` and its
27
+ * `duration_ms` 0 until it ends.
28
+ */
29
+ in_progress?: boolean;
23
30
  /** Nested children (computed client-side from parent_span_id). */
24
31
  children?: OtelSpan[];
25
32
  /** Depth in a span tree (computed client-side). */
@@ -96,6 +103,16 @@ export interface OtelTracesListProps {
96
103
  loading?: boolean;
97
104
  selectedSpanId?: string | null;
98
105
  onSelectSpan?: (span: OtelSpan) => void;
106
+ /** What is said while there is no span. */
107
+ emptyText?: string;
108
+ /** Narrower columns, for a panel beside other content. */
109
+ compact?: boolean;
110
+ /** Every span with children starts unfolded: a live record, read as it grows. */
111
+ defaultExpanded?: boolean;
112
+ /** A mark drawn before a span's name (an icon, an emoji). */
113
+ renderSpanMark?: (span: OtelSpan) => ReactNode;
114
+ /** A line that says what a span carried, drawn after its name. */
115
+ describeSpan?: (span: OtelSpan) => string | undefined;
99
116
  }
100
117
  /** Props for the span detail panel. */
101
118
  export interface OtelSpanDetailProps {
@@ -13,7 +13,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
13
13
  * @module otel/OtelLive
14
14
  */
15
15
  import { useState, useCallback, useMemo, useEffect, useRef, } from 'react';
16
- import { Box, Text, Button, Label } from '@primer/react';
16
+ import { Box } from '@datalayer/primer-addons';
17
+ import { Text, Button, Label } from '@primer/react';
17
18
  import { GitBranchIcon, ClockIcon } from '@primer/octicons-react';
18
19
  import { getOtelConsumeUrl } from '../../state/substates/CoreState.js';
19
20
  import { useOtelTraces, useOtelTrace, useOtelLogs, useOtelMetrics, useOtelServices, useOtelWebSocket, } from '../hooks/index.js';
@@ -287,70 +288,11 @@ export const OtelLive = ({ baseUrl = getOtelConsumeUrl(), wsBaseUrl, token, auto
287
288
  return ts >= s && ts <= e;
288
289
  });
289
290
  }, [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'
291
+ return (_jsxs(Box, { display: "flex", flexDirection: "column", flex: 1, minHeight: 0, height: "100%", color: "fg.default", bg: "canvas.default", border: "1px solid", borderColor: "border.default", borderRadius: 2, overflow: "hidden", children: [_jsxs(Box, { display: "flex", alignItems: "center", gap: 0, children: [_jsx(Box, { 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
292
  ? tracesLoading
304
293
  : signal === 'logs'
305
294
  ? 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 }))] }))] }))] }));
295
+ : metricsLoading }) }), _jsx(Box, { pr: 2, flexShrink: 0, children: _jsx(Label, { variant: wsConnected ? 'success' : 'secondary', size: "small", children: wsConnected ? '● Live' : '○ Polling' }) })] }), timelineBounds && rangeStart && rangeEnd && (_jsx(Box, { px: 3, pt: 2, pb: 1, borderBottom: "1px solid", borderColor: "border.default", bg: "canvas.subtle", flexShrink: 0, position: "relative", zIndex: 0, children: _jsx(OtelTimelineRangeSlider, { timelineStart: timelineBounds.start, timelineEnd: timelineBounds.end, selectedStart: rangeStart, selectedEnd: rangeEnd, onRangeChange: handleRangeChange, histogram: histogram, height: 48, tickCount: 6 }) })), _jsxs(Box, { display: "flex", flex: 1, minHeight: 0, overflow: "hidden", children: [_jsxs(Box, { display: "flex", flexDirection: "column", flex: hasDetail ? '0 0 55%' : '1 1 100%', minHeight: 0, overflow: "hidden", 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, { flex: "0 0 45%", minHeight: 0, overflow: "auto", borderLeft: "1px solid", borderColor: "border.default", children: [signal === 'traces' && selectedSpan && (_jsx(OtelSpanDetail, { span: selectedSpan, traceSpans: traceSpans ?? undefined, onClose: handleCloseDetail })), signal === 'logs' && selectedLogIdx !== null && logs && (_jsxs(Box, { p: 3, children: [_jsxs(Box, { display: "flex", justifyContent: "space-between", mb: 3, 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", m: 0, fontSize: 1, fontFamily: "mono", whiteSpace: "pre-wrap", wordBreak: "break-word", children: JSON.stringify(logs[selectedLogIdx], null, 2) })] }))] }))] }), signal === 'traces' && selectedSpan && spanTree.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Box, { display: "flex", gap: 1, px: 3, py: 1, bg: "canvas.subtle", borderTop: "1px solid", borderColor: "border.default", flexShrink: 0, 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, { height: 260, overflow: "auto", borderTop: "1px solid", borderColor: "border.default", flexShrink: 0, 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
296
  };
355
297
  // ── Client-side filter helpers ──────────────────────────────────────
356
298
  function filterSpans(spans, q) {
@@ -0,0 +1,52 @@
1
+ /**
2
+ * OtelLiveSpans – the spans of a page's own tracer (`createOtelLiveTracer`),
3
+ * drawn as they happen: the traces list (each trace a tree, unfolded), and
4
+ * the selected span's detail (its attributes, events, trace tree, raw data).
5
+ *
6
+ * One service's own (`service`): what it did, and the calls made toward it
7
+ * (`peer.service`). Filters by facets (the service, the span kind, or any
8
+ * value a span carries); *Pause* holds the rows still while the record goes
9
+ * on, *Clear* empties it, *Export* saves the spans as JSON.
10
+ *
11
+ * @module otel/views/OtelLiveSpans
12
+ */
13
+ import React from 'react';
14
+ import type { OtelSpan } from '../types';
15
+ import type { OtelLiveTracer } from '../live/tracer';
16
+ /** A filter menu: the values a span has for it, each to keep or not. */
17
+ export interface OtelSpanFacet {
18
+ label: string;
19
+ /** The span's value for it; `undefined` when it has none. */
20
+ value: (span: OtelSpan) => string | undefined;
21
+ /** A value, in words. */
22
+ name?: (value: string) => string;
23
+ }
24
+ /** The facets unless said: the service and the span kind. */
25
+ export declare const DEFAULT_SPAN_FACETS: readonly OtelSpanFacet[];
26
+ export interface OtelLiveSpansProps {
27
+ /** The tracer whose spans are drawn, kept current. */
28
+ tracer?: OtelLiveTracer | null;
29
+ /** Or the spans, as given. */
30
+ spans?: readonly OtelSpan[];
31
+ /** One service's own: done by it, or toward it. */
32
+ service?: string;
33
+ facets?: readonly OtelSpanFacet[];
34
+ /** Narrower columns, the detail under the list. */
35
+ compact?: boolean;
36
+ /** How tall the list grows before it scrolls. */
37
+ maxHeight?: number | string;
38
+ /** Where the selected span's detail goes. Default `below`. */
39
+ detailPlacement?: 'below' | 'side';
40
+ /** What is said while there is no span. */
41
+ emptyText?: string;
42
+ /** The file the spans are exported as, without `.json`. */
43
+ exportName?: string;
44
+ /** The toolbar's name. */
45
+ label?: string;
46
+ renderSpanMark?: (span: OtelSpan) => React.ReactNode;
47
+ describeSpan?: (span: OtelSpan) => string | undefined;
48
+ }
49
+ /** The spans each facet keeps: an empty selection keeps every value. */
50
+ export declare function filterSpansByFacets(spans: readonly OtelSpan[], facets: readonly OtelSpanFacet[], selected: readonly ReadonlySet<string>[]): OtelSpan[];
51
+ export declare const OtelLiveSpans: React.FC<OtelLiveSpansProps>;
52
+ export default OtelLiveSpans;
@@ -0,0 +1,114 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } 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
+ * OtelLiveSpans – the spans of a page's own tracer (`createOtelLiveTracer`),
8
+ * drawn as they happen: the traces list (each trace a tree, unfolded), and
9
+ * the selected span's detail (its attributes, events, trace tree, raw data).
10
+ *
11
+ * One service's own (`service`): what it did, and the calls made toward it
12
+ * (`peer.service`). Filters by facets (the service, the span kind, or any
13
+ * value a span carries); *Pause* holds the rows still while the record goes
14
+ * on, *Clear* empties it, *Export* saves the spans as JSON.
15
+ *
16
+ * @module otel/views/OtelLiveSpans
17
+ */
18
+ import { useMemo, useState } from 'react';
19
+ import { Box } from '@datalayer/primer-addons';
20
+ import { ActionList, ActionMenu, Button, Text } from '@primer/react';
21
+ import { DownloadIcon, PauseIcon, PlayIcon, TrashIcon, } from '@primer/octicons-react';
22
+ import { toMs } from '../utils.js';
23
+ import { spansOfService } from '../live/filter.js';
24
+ import { useOtelLiveSpans } from '../live/useOtelLiveSpans.js';
25
+ import { OtelTracesList } from './OtelTracesList.js';
26
+ import { OtelSpanDetail } from './OtelSpanDetail.js';
27
+ /** The facets unless said: the service and the span kind. */
28
+ export const DEFAULT_SPAN_FACETS = [
29
+ { label: 'Service', value: span => span.service_name },
30
+ { label: 'Kind', value: span => span.kind },
31
+ ];
32
+ /** The spans each facet keeps: an empty selection keeps every value. */
33
+ export function filterSpansByFacets(spans, facets, selected) {
34
+ return spans.filter(span => facets.every((facet, index) => {
35
+ const values = selected[index];
36
+ if (!values?.size)
37
+ return true;
38
+ const value = facet.value(span);
39
+ return value !== undefined && values.has(value);
40
+ }));
41
+ }
42
+ function toggle(set, value) {
43
+ const next = new Set(set);
44
+ if (next.has(value)) {
45
+ next.delete(value);
46
+ }
47
+ else {
48
+ next.add(value);
49
+ }
50
+ return next;
51
+ }
52
+ function FacetMenu({ facet, options, selected, onChange, }) {
53
+ const name = facet.name ?? ((value) => value);
54
+ return (_jsxs(ActionMenu, { children: [_jsxs(ActionMenu.Button, { size: "small", "data-otel-facet": facet.label.toLowerCase(), children: [facet.label, selected.size ? ` · ${selected.size}` : ''] }), _jsx(ActionMenu.Overlay, { width: "auto", children: _jsxs(ActionList, { selectionVariant: "multiple", "aria-label": facet.label, children: [_jsx(ActionList.Item, { selected: !selected.size, onSelect: () => onChange(new Set()), children: "All" }), _jsx(ActionList.Divider, {}), options.map(option => (_jsx(ActionList.Item, { selected: selected.has(option), onSelect: () => onChange(toggle(selected, option)), children: name(option) }, option)))] }) })] }));
55
+ }
56
+ /** Save the spans as a JSON file. */
57
+ function download(name, record) {
58
+ const blob = new Blob([JSON.stringify(record, null, 2)], {
59
+ type: 'application/json',
60
+ });
61
+ const url = URL.createObjectURL(blob);
62
+ const link = document.createElement('a');
63
+ link.href = url;
64
+ link.download = `${name}.json`;
65
+ link.click();
66
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
67
+ }
68
+ export const OtelLiveSpans = ({ tracer, spans: given, service, facets = DEFAULT_SPAN_FACETS, compact = false, maxHeight = 420, detailPlacement = 'below', emptyText = 'Nothing recorded yet.', exportName = 'otel-spans', label = 'Spans', renderSpanMark, describeSpan, }) => {
69
+ const live = useOtelLiveSpans(given ? null : tracer);
70
+ const everything = given ?? live;
71
+ const [clearedAt, setClearedAt] = useState(0);
72
+ const [held, setHeld] = useState(null);
73
+ const [selected, setSelected] = useState(() => facets.map(() => new Set()));
74
+ const [selectedSpanId, setSelectedSpanId] = useState(null);
75
+ const recorded = useMemo(() => {
76
+ const own = spansOfService(everything, service);
77
+ return clearedAt
78
+ ? own.filter(span => toMs(span.start_time) >= clearedAt)
79
+ : own;
80
+ }, [everything, service, clearedAt]);
81
+ const shown = held ?? recorded;
82
+ const waiting = held ? recorded.length - held.length : 0;
83
+ const options = useMemo(() => facets.map(facet => [
84
+ ...new Set(recorded
85
+ .map(span => facet.value(span))
86
+ .filter((value) => value !== undefined)),
87
+ ].sort()), [facets, recorded]);
88
+ const rows = useMemo(() => filterSpansByFacets(shown, facets, selected), [shown, facets, selected]);
89
+ // The selected span as it is now: a live span changes as it goes.
90
+ const selectedSpan = useMemo(() => selectedSpanId
91
+ ? (everything.find(span => span.span_id === selectedSpanId) ?? null)
92
+ : null, [everything, selectedSpanId]);
93
+ const traceSpans = useMemo(() => selectedSpan
94
+ ? everything.filter(span => span.trace_id === selectedSpan.trace_id)
95
+ : [], [everything, selectedSpan]);
96
+ const side = detailPlacement === 'side' && !compact;
97
+ return (_jsxs(Box, { "data-otel-live-spans": "", border: "1px solid", borderColor: "border.default", borderRadius: 2, bg: "canvas.default", color: "fg.default", minWidth: 0, children: [_jsxs(Box, { role: "toolbar", "aria-label": label, display: "flex", flexWrap: "wrap", gap: 2, alignItems: "center", p: 2, borderBottom: "1px solid", borderColor: "border.default", children: [facets.map((facet, index) => (_jsx(FacetMenu, { facet: facet, options: options[index] ?? [], selected: selected[index] ?? new Set(), onChange: next => setSelected(prev => facets.map((_, at) => at === index ? next : (prev[at] ?? new Set()))) }, facet.label))), _jsx(Box, { flex: 1 }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, "data-otel-span-count": "", children: [rows.length === shown.length
98
+ ? `${rows.length} ${rows.length === 1 ? 'span' : 'spans'}`
99
+ : `${rows.length} of ${shown.length}`, waiting > 0 ? ` · ${waiting} new` : ''] }), _jsx(Button, { size: "small", leadingVisual: held ? PlayIcon : PauseIcon, "aria-pressed": held !== null, onClick: () => setHeld(held ? null : recorded), "data-otel-pause": "", children: held ? 'Resume' : 'Pause' }), _jsx(Button, { size: "small", leadingVisual: TrashIcon, onClick: () => {
100
+ setHeld(null);
101
+ setSelectedSpanId(null);
102
+ // One service's view of a shared record clears only itself.
103
+ if (tracer && !given && !service) {
104
+ tracer.clear();
105
+ }
106
+ else {
107
+ setClearedAt(Date.now());
108
+ }
109
+ }, "data-otel-clear": "", children: "Clear" }), _jsx(Button, { size: "small", leadingVisual: DownloadIcon, disabled: !recorded.length, onClick: () => download(exportName, {
110
+ exportedAt: new Date().toISOString(),
111
+ spans: [...recorded],
112
+ }), "data-otel-export": "", children: "Export" })] }), _jsxs(Box, { display: side ? 'grid' : 'block', gridTemplateColumns: side ? 'minmax(0, 3fr) minmax(0, 2fr)' : undefined, children: [_jsx(Box, { maxHeight: maxHeight, overflowY: "auto", minWidth: 0, children: _jsx(OtelTracesList, { spans: rows, selectedSpanId: selectedSpanId, onSelectSpan: span => setSelectedSpanId(span.span_id), emptyText: emptyText, compact: compact, defaultExpanded: true, renderSpanMark: renderSpanMark, describeSpan: describeSpan }) }), selectedSpan && (_jsx(Box, { "data-otel-live-detail": "", maxHeight: maxHeight, overflowY: "auto", minWidth: 0, borderTop: side ? 'none' : '1px solid', borderColor: "border.default", children: _jsx(OtelSpanDetail, { span: selectedSpan, traceSpans: traceSpans, onClose: () => setSelectedSpanId(null) }) }))] })] }));
113
+ };
114
+ export default OtelLiveSpans;