@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.
- package/lib/cli-login.js +5 -51
- package/lib/components/anonymous/AnonymousKeyExpired.js +1 -15
- package/lib/components/anonymous/AnonymousKeyTimer.js +5 -15
- package/lib/components/auth/SignIn.js +2 -2
- package/lib/components/avatars/UserAvatar.js +3 -27
- package/lib/components/billing/BillingEntitySelect.js +6 -48
- package/lib/components/checkout/StripeCheckout.js +33 -217
- package/lib/components/contents/ContentSourceCard.js +2 -11
- package/lib/components/contents/HomeFolderObjectsBrowser.d.ts +1 -1
- package/lib/components/contents/HomeFolderObjectsBrowser.js +11 -35
- package/lib/components/display/CenteredSpinner.js +1 -6
- package/lib/components/display/DatalayerBox.js +2 -14
- package/lib/components/flashes/FlashClosable.js +2 -17
- package/lib/components/mcp/ClientBadge.js +1 -1
- package/lib/components/mcp/McpErrorBlankslate.js +1 -1
- package/lib/components/mcp/ScopeList.js +1 -1
- package/lib/components/notebooks/JupyterNotebook.js +1 -2
- package/lib/components/principal/Principal.js +2 -10
- package/lib/components/principal/PrincipalAppearance.js +19 -63
- package/lib/components/principal/PrincipalAvatar.js +5 -25
- package/lib/components/principal/PrincipalBadge.js +3 -13
- package/lib/components/principal/PrincipalBanner.js +4 -32
- package/lib/components/principal/PrincipalDetailsOverlay.js +7 -23
- package/lib/components/principal/PrincipalHoverCard.js +15 -18
- package/lib/components/principal/PrincipalSwitcherMenu.js +15 -83
- package/lib/components/progress/ConsumptionBar.js +1 -1
- package/lib/components/sharing/ShareAccessComponent.js +21 -229
- package/lib/components/sharing/SharingEditor.js +1 -1
- package/lib/components/spaces/SpaceDetailsOverlay.js +4 -35
- package/lib/components/spaces/SpaceDisplay.js +1 -8
- package/lib/components/storage/ContentsBrowser.js +3 -26
- package/lib/components/storage/ContentsTable.js +4 -23
- package/lib/components/students/StudentResultCircle.js +7 -29
- package/lib/components/timeline/Timeline.js +4 -36
- package/lib/hooks/layouts/LayoutBackdrop.js +2 -1
- package/lib/hooks/layouts/LayoutScreencapture.js +1 -1
- package/lib/hooks/libraryApp.d.ts +6 -1
- package/lib/hooks/libraryApp.js +10 -1
- package/lib/hooks/useAuthorization.js +6 -1
- package/lib/mocks/components/FlashMock.js +3 -8
- package/lib/mocks/views/DashboardMock.js +6 -72
- package/lib/mocks/views/FormMock.js +3 -2
- package/lib/mocks/views/TableMock.js +3 -2
- package/lib/mocks/views/WipMock.js +2 -1
- package/lib/models/Person.d.ts +9 -0
- package/lib/models/Person.js +23 -0
- package/lib/otel/index.d.ts +4 -0
- package/lib/otel/index.js +3 -0
- package/lib/otel/live/filter.d.ts +15 -0
- package/lib/otel/live/filter.js +16 -0
- package/lib/otel/live/index.d.ts +10 -0
- package/lib/otel/live/index.js +14 -0
- package/lib/otel/live/tracer.d.ts +94 -0
- package/lib/otel/live/tracer.js +223 -0
- package/lib/otel/live/useOtelLiveSpans.d.ts +6 -0
- package/lib/otel/live/useOtelLiveSpans.js +22 -0
- package/lib/otel/types.d.ts +17 -0
- package/lib/otel/views/OtelLive.js +4 -62
- package/lib/otel/views/OtelLiveSpans.d.ts +52 -0
- package/lib/otel/views/OtelLiveSpans.js +114 -0
- package/lib/otel/views/OtelLogsList.js +13 -64
- package/lib/otel/views/OtelMetricsChart.js +4 -19
- package/lib/otel/views/OtelMetricsList.js +8 -77
- package/lib/otel/views/OtelSearchBar.js +3 -12
- package/lib/otel/views/OtelSpanDetail.d.ts +6 -0
- package/lib/otel/views/OtelSpanDetail.js +31 -81
- package/lib/otel/views/OtelSpanTree.js +7 -57
- package/lib/otel/views/OtelSqlView.js +5 -99
- package/lib/otel/views/OtelSystemView.js +4 -34
- package/lib/otel/views/OtelTimeline.js +7 -37
- package/lib/otel/views/OtelTimelineRangeSlider.js +7 -140
- package/lib/otel/views/OtelTracesList.js +39 -87
- package/lib/otel/views/index.d.ts +3 -0
- package/lib/otel/views/index.js +2 -0
- package/lib/portable/index.d.ts +8 -0
- package/lib/portable/index.js +12 -0
- package/lib/state/substates/NavigationState.d.ts +6 -4
- package/lib/state/substates/NavigationState.js +6 -5
- package/lib/views/api-keys/APIKeyEdit.js +2 -11
- package/lib/views/api-keys/APIKeyNew.js +3 -19
- package/lib/views/api-keys/APIKeys.js +6 -11
- package/lib/views/api-keys/APIKeysStandalone.js +1 -1
- package/lib/views/datasources/DatasourceDetail.d.ts +1 -1
- package/lib/views/datasources/DatasourceDetail.js +24 -17
- package/lib/views/datasources/DatasourceNew.js +2 -6
- package/lib/views/datasources/Datasources.js +2 -14
- package/lib/views/iam/SignInSimple.js +8 -64
- package/lib/views/mcp/AdmittedClients.js +3 -16
- package/lib/views/mcp/AlertDestinations.js +2 -10
- package/lib/views/mcp/AlertRules.js +4 -9
- package/lib/views/mcp/ApprovalQueue.js +4 -21
- package/lib/views/mcp/AuditLog.js +7 -44
- package/lib/views/mcp/ConnectedAgents.js +1 -6
- package/lib/views/mcp/EnterpriseConsole.js +10 -23
- package/lib/views/mcp/IdentityProviders.js +7 -31
- package/lib/views/mcp/McpDashboard.js +14 -30
- package/lib/views/mcp/McpHome.js +7 -54
- package/lib/views/mcp/McpObservability.js +13 -85
- package/lib/views/mcp/NotebookRuns.js +5 -12
- package/lib/views/mcp/OrganizationPolicy.js +3 -9
- package/lib/views/mcp/OrganizationUsage.js +4 -11
- package/lib/views/mcp/PersonalPolicy.js +3 -3
- package/lib/views/mcp/Policies.js +5 -46
- package/lib/views/mcp/PolicyForm.js +1 -9
- package/lib/views/mcp/PolicyHistory.js +2 -8
- package/lib/views/mcp/RunDetail.js +3 -25
- package/lib/views/mcp/ScimProvisioning.js +3 -15
- package/lib/views/mcp/ServiceAgents.js +4 -17
- package/lib/views/mcp/TeamPolicies.js +7 -19
- package/lib/views/mcp/ToolAccess.js +15 -22
- package/lib/views/mcp/TraceTimeline.js +8 -23
- package/lib/views/otel/DashboardView.js +2 -2
- package/lib/views/otel/LogsView.js +2 -2
- package/lib/views/otel/MetricsView.js +2 -2
- package/lib/views/otel/OtelHeader.js +6 -40
- package/lib/views/otel/SqlView.js +2 -2
- package/lib/views/otel/SystemView.js +2 -2
- package/lib/views/otel/TracesView.js +2 -2
- package/lib/views/profile/UserBadge.js +6 -98
- package/lib/views/secrets/SecretEdit.js +2 -11
- package/lib/views/secrets/SecretNew.js +1 -1
- 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
|
+
}
|
package/lib/otel/types.d.ts
CHANGED
|
@@ -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
|
|
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, {
|
|
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, {
|
|
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;
|