@datalayer/core 1.2.15 → 1.2.18

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.
@@ -51,6 +51,12 @@ export type AnonymousKeyExpiredProps = {
51
51
  temporary?: boolean;
52
52
  /** Called after a successful sign-in, once the token is stored. */
53
53
  onSignedIn?: () => void;
54
+ /**
55
+ * Called on *Sign up with email*, for a host with a router to send the
56
+ * reader to its own sign-up page; without it, the form opens its sign-up
57
+ * address.
58
+ */
59
+ onSignUp?: () => void;
54
60
  };
55
- export declare function AnonymousKeyExpired({ agentName, sandboxStillRuns, temporary, onSignedIn, }: AnonymousKeyExpiredProps): JSX.Element;
61
+ export declare function AnonymousKeyExpired({ agentName, sandboxStillRuns, temporary, onSignedIn, onSignUp, }: AnonymousKeyExpiredProps): JSX.Element;
56
62
  export default AnonymousKeyExpired;
@@ -5,7 +5,7 @@ import { Box } from '@datalayer/primer-addons';
5
5
  import { KeyIcon } from '@primer/octicons-react';
6
6
  import { SignInSimple } from '../../views/iam/SignInSimple.js';
7
7
  import { useCoreStore, iamStore } from '../../state/substates/index.js';
8
- export function AnonymousKeyExpired({ agentName, sandboxStillRuns = false, temporary = true, onSignedIn, }) {
8
+ export function AnonymousKeyExpired({ agentName, sandboxStillRuns = false, temporary = true, onSignedIn, onSignUp, }) {
9
9
  const { configuration } = useCoreStore();
10
10
  const handleSignIn = useCallback((token) => {
11
11
  /*
@@ -91,6 +91,6 @@ export function AnonymousKeyExpired({ agentName, sandboxStillRuns = false, tempo
91
91
  // has one and harmless for one that does not.
92
92
  loginUrl: configuration?.iamUrl
93
93
  ? `${configuration.iamUrl}/api/iam/v1/login`
94
- : undefined, onSignIn: handleSignIn })] }) }));
94
+ : undefined, onSignIn: handleSignIn, onSignUp: onSignUp })] }) }));
95
95
  }
96
96
  export default AnonymousKeyExpired;
@@ -18,6 +18,17 @@ export type StatusLabelProps = {
18
18
  size?: 'small' | 'large';
19
19
  sx?: Record<string, unknown>;
20
20
  };
21
+ /**
22
+ * The tone of each named label. A role or a stage is a quality, not a
23
+ * verdict, so none is in attention's amber: the theme keeps success, attention
24
+ * and danger for verdicts (LOOP T-14, T-29) — *admin* is quiet.
25
+ */
26
+ export declare const NAMED_LABEL_TONES: {
27
+ readonly alpha: "accent";
28
+ readonly admin: "neutral";
29
+ readonly default: "accent";
30
+ readonly soon: "neutral";
31
+ };
21
32
  /**
22
33
  * The pill the named labels below are made of.
23
34
  *
@@ -22,6 +22,17 @@ const TONES = {
22
22
  borderColor: 'border.default',
23
23
  },
24
24
  };
25
+ /**
26
+ * The tone of each named label. A role or a stage is a quality, not a
27
+ * verdict, so none is in attention's amber: the theme keeps success, attention
28
+ * and danger for verdicts (LOOP T-14, T-29) — *admin* is quiet.
29
+ */
30
+ export const NAMED_LABEL_TONES = {
31
+ alpha: 'accent',
32
+ admin: 'neutral',
33
+ default: 'accent',
34
+ soon: 'neutral',
35
+ };
25
36
  /**
26
37
  * The pill the named labels below are made of.
27
38
  *
@@ -42,25 +53,25 @@ export function StatusLabel(props) {
42
53
  */
43
54
  export function AlphaLabel(props = {}) {
44
55
  const { children = 'alpha', ...rest } = props;
45
- return (_jsx(StatusLabel, { tone: "accent", ...rest, children: children }));
56
+ return (_jsx(StatusLabel, { tone: NAMED_LABEL_TONES.alpha, ...rest, children: children }));
46
57
  }
47
58
  /**
48
59
  * Something reserved to the administrators of the platform.
49
60
  */
50
61
  export function AdminLabel(props = {}) {
51
62
  const { children = 'admin', ...rest } = props;
52
- return (_jsx(StatusLabel, { tone: "attention", ...rest, children: children }));
63
+ return (_jsx(StatusLabel, { tone: NAMED_LABEL_TONES.admin, ...rest, children: children }));
53
64
  }
54
65
  /**
55
66
  * The one that is taken when none is chosen — the default space of an account.
56
67
  */
57
68
  export function DefaultLabel(props = {}) {
58
69
  const { children = 'default', ...rest } = props;
59
- return (_jsx(StatusLabel, { tone: "accent", ...rest, children: children }));
70
+ return (_jsx(StatusLabel, { tone: NAMED_LABEL_TONES.default, ...rest, children: children }));
60
71
  }
61
72
  /** "soon": what is announced but not yet there. */
62
73
  export function SoonLabel(props = {}) {
63
74
  const { children = 'soon', ...rest } = props;
64
- return (_jsx(StatusLabel, { tone: "neutral", ...rest, children: children }));
75
+ return (_jsx(StatusLabel, { tone: NAMED_LABEL_TONES.soon, ...rest, children: children }));
65
76
  }
66
77
  export default StatusLabel;
@@ -1840,6 +1840,9 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
1840
1840
  if (settings.docsInPlace !== undefined) {
1841
1841
  body.docs_in_place_b = settings.docsInPlace;
1842
1842
  }
1843
+ if (settings.showAdvanced !== undefined) {
1844
+ body.show_advanced_b = settings.showAdvanced;
1845
+ }
1843
1846
  return requestDatalayer({
1844
1847
  url: `${configuration.iamUrl}/api/iam/v1/users/${userId}/settings`,
1845
1848
  method: 'PUT',
@@ -1,6 +1,7 @@
1
1
  export declare const AI_AGENTS_URL_KEY = "aiAgents_url_s";
2
2
  export declare const CAN_INVITE_URL_KEY = "can_invite_b";
3
3
  export declare const DOCS_IN_PLACE_KEY = "docs_in_place_b";
4
+ export declare const SHOW_ADVANCED_KEY = "show_advanced_b";
4
5
  export type IUserSettings = {
5
6
  aiAgentsUrl?: string;
6
7
  canInvite?: boolean;
@@ -13,11 +14,19 @@ export type IUserSettings = {
13
14
  * full-window documentation.
14
15
  */
15
16
  docsInPlace?: boolean;
17
+ /**
18
+ * Whether the Studio shows what a Maker does not need — *Show advanced*.
19
+ *
20
+ * Undefined means the user has never chosen: the Studio makes a first
21
+ * choice for them, and keeps it here.
22
+ */
23
+ showAdvanced?: boolean;
16
24
  };
17
25
  export declare class UserSettings implements IUserSettings {
18
26
  private _aiAgentsUrl?;
19
27
  private _canInvite?;
20
28
  private _docsInPlace?;
29
+ private _showAdvanced?;
21
30
  constructor(s: any);
22
31
  get aiAgentsUrl(): string | undefined;
23
32
  set aiAgentsUrl(value: string | undefined);
@@ -25,5 +34,7 @@ export declare class UserSettings implements IUserSettings {
25
34
  set canInvite(value: boolean | undefined);
26
35
  get docsInPlace(): boolean | undefined;
27
36
  set docsInPlace(value: boolean | undefined);
37
+ get showAdvanced(): boolean | undefined;
38
+ set showAdvanced(value: boolean | undefined);
28
39
  }
29
40
  export default UserSettings;
@@ -5,14 +5,17 @@
5
5
  export const AI_AGENTS_URL_KEY = 'aiAgents_url_s';
6
6
  export const CAN_INVITE_URL_KEY = 'can_invite_b';
7
7
  export const DOCS_IN_PLACE_KEY = 'docs_in_place_b';
8
+ export const SHOW_ADVANCED_KEY = 'show_advanced_b';
8
9
  export class UserSettings {
9
10
  _aiAgentsUrl;
10
11
  _canInvite;
11
12
  _docsInPlace;
13
+ _showAdvanced;
12
14
  constructor(s) {
13
15
  this._aiAgentsUrl = s[AI_AGENTS_URL_KEY] ?? undefined;
14
16
  this._canInvite = s[CAN_INVITE_URL_KEY] ?? undefined;
15
17
  this._docsInPlace = s[DOCS_IN_PLACE_KEY] ?? undefined;
18
+ this._showAdvanced = s[SHOW_ADVANCED_KEY] ?? undefined;
16
19
  }
17
20
  get aiAgentsUrl() {
18
21
  return this._aiAgentsUrl;
@@ -32,5 +35,11 @@ export class UserSettings {
32
35
  set docsInPlace(value) {
33
36
  this._docsInPlace = value;
34
37
  }
38
+ get showAdvanced() {
39
+ return this._showAdvanced;
40
+ }
41
+ set showAdvanced(value) {
42
+ this._showAdvanced = value;
43
+ }
35
44
  }
36
45
  export default UserSettings;
@@ -13,6 +13,8 @@
13
13
  */
14
14
  export type { OtelSpan, OtelLog, OtelMetric, OtelSpanEvent, OtelSpanLink, SignalType, OtelLiveProps, OtelTracesListProps, OtelSpanDetailProps, OtelTimelineProps, OtelLogsListProps, OtelMetricsListProps, OtelSpanTreeProps, OtelSearchBarProps, OtelTimelineRangeSliderProps, OtelQueryRow, OtelQueryResult, } from './types';
15
15
  export { toMs, formatDuration, formatTime, serviceColor, kindColor, severityColor, severityVariant, buildSpanTree, flattenSpanTree, } from './utils';
16
+ export { createOtelLiveTracer, traceparentOf, PEER_SERVICE, spanConcerns, spansOfService, useOtelLiveSpans, useOtelLiveTracer, } from './live';
17
+ export type { OtelAttributeValue, OtelAttributes, OtelLiveExport, OtelLiveSpan, OtelLiveTracer, OtelLiveTracerOptions, OtelSpanContext, OtelSpanKindName, OtelStartSpanOptions, } from './live';
16
18
  export { resolveOtelAuth, resolveOtelUserUid, buildOtelWebSocketUrl, } from './auth';
17
19
  export type { ResolvedOtelAuth } from './auth';
18
20
  export { useOtelTraces, useOtelTrace, useOtelLogs, useOtelMetrics, useOtelServices, useOtelStats, useOtelQuery, useOtelSystem, useOtelWebSocket, setOtelOnUnauthorized, } from './hooks';
@@ -29,6 +31,8 @@ export { OtelMetricsList } from './views';
29
31
  export { OtelMetricsChart } from './views';
30
32
  export type { OtelMetricsChartProps } from './views';
31
33
  export { OtelTimeline } from './views';
34
+ export { OtelLiveSpans, DEFAULT_SPAN_FACETS, filterSpansByFacets, unfoldedValue, } from './views';
35
+ export type { OtelLiveSpansProps, OtelSpanFacet } from './views';
32
36
  export { OtelTimelineRangeSlider } from './views';
33
37
  export { OtelSqlView } from './views';
34
38
  export type { OtelSqlViewProps } from './views';
package/lib/otel/index.js CHANGED
@@ -4,6 +4,8 @@
4
4
  */
5
5
  // ── Utils ─────────────────────────────────────────────────────────────────────
6
6
  export { toMs, formatDuration, formatTime, serviceColor, kindColor, severityColor, severityVariant, buildSpanTree, flattenSpanTree, } from './utils.js';
7
+ // ── In the page ───────────────────────────────────────────────────────────────
8
+ export { createOtelLiveTracer, traceparentOf, PEER_SERVICE, spanConcerns, spansOfService, useOtelLiveSpans, useOtelLiveTracer, } from './live/index.js';
7
9
  // ── Auth helpers ──────────────────────────────────────────────────────────────
8
10
  export { resolveOtelAuth, resolveOtelUserUid, buildOtelWebSocketUrl, } from './auth.js';
9
11
  // ── Hooks ─────────────────────────────────────────────────────────────────────
@@ -20,6 +22,7 @@ export { OtelSearchBar } from './views/index.js';
20
22
  export { OtelMetricsList } from './views/index.js';
21
23
  export { OtelMetricsChart } from './views/index.js';
22
24
  export { OtelTimeline } from './views/index.js';
25
+ export { OtelLiveSpans, DEFAULT_SPAN_FACETS, filterSpansByFacets, unfoldedValue, } from './views/index.js';
23
26
  export { OtelTimelineRangeSlider } from './views/index.js';
24
27
  export { OtelSqlView } from './views/index.js';
25
28
  export { OtelSystemView } from './views/index.js';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Which spans concern a service: those it did, and those done toward it — a
3
+ * client span whose `peer.service` names it (the OpenTelemetry attribute for
4
+ * the remote service a call goes to). One message between two services shows
5
+ * in both: sent in one, received in the other.
6
+ *
7
+ * @module otel/live/filter
8
+ */
9
+ import type { OtelSpan } from '../types';
10
+ /** The OpenTelemetry attribute naming the service a call goes to. */
11
+ export declare const PEER_SERVICE = "peer.service";
12
+ /** Whether a span concerns a service: done by it, or toward it. */
13
+ export declare function spanConcerns(span: OtelSpan, service: string): boolean;
14
+ /** The spans that concern a service; every span without one. */
15
+ export declare function spansOfService(spans: readonly OtelSpan[], service: string | undefined): OtelSpan[];
@@ -0,0 +1,16 @@
1
+ /*
2
+ * Copyright (c) 2023-2025 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** The OpenTelemetry attribute naming the service a call goes to. */
6
+ export const PEER_SERVICE = 'peer.service';
7
+ /** Whether a span concerns a service: done by it, or toward it. */
8
+ export function spanConcerns(span, service) {
9
+ return (span.service_name === service || span.attributes?.[PEER_SERVICE] === service);
10
+ }
11
+ /** The spans that concern a service; every span without one. */
12
+ export function spansOfService(spans, service) {
13
+ return service
14
+ ? spans.filter(span => spanConcerns(span, service))
15
+ : [...spans];
16
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * OpenTelemetry in the page: a tracer whose spans the OTEL views draw as
3
+ * they happen, and which of them concern a service. Light: no view, no
4
+ * client, nothing sent.
5
+ *
6
+ * @module otel/live
7
+ */
8
+ export { createOtelLiveTracer, traceparentOf, type OtelAttributeValue, type OtelAttributes, type OtelLiveExport, type OtelLiveSpan, type OtelLiveTracer, type OtelLiveTracerOptions, type OtelSpanContext, type OtelSpanKindName, type OtelStartSpanOptions, } from './tracer';
9
+ export { PEER_SERVICE, spanConcerns, spansOfService } from './filter';
10
+ export { useOtelLiveSpans, useOtelLiveTracer } from './useOtelLiveSpans';
@@ -0,0 +1,14 @@
1
+ /*
2
+ * Copyright (c) 2023-2025 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * OpenTelemetry in the page: a tracer whose spans the OTEL views draw as
7
+ * they happen, and which of them concern a service. Light: no view, no
8
+ * client, nothing sent.
9
+ *
10
+ * @module otel/live
11
+ */
12
+ export { createOtelLiveTracer, traceparentOf, } from './tracer.js';
13
+ export { PEER_SERVICE, spanConcerns, spansOfService } from './filter.js';
14
+ export { useOtelLiveSpans, useOtelLiveTracer } from './useOtelLiveSpans.js';
@@ -0,0 +1,94 @@
1
+ /**
2
+ * A tracer that lives in the page: spans started and ended in the browser,
3
+ * kept as the OTEL views draw them (`OtelSpan`), as they happen — a span is
4
+ * there from its start (`in_progress`), its events added as they come, its
5
+ * end and status when it ends.
6
+ *
7
+ * What it records is OpenTelemetry: W3C trace and span ids, a span kind, a
8
+ * status, attributes of primitive values (structured values as JSON text, as
9
+ * the GenAI semantic conventions carry arguments and results), events, and an
10
+ * `exception` event for an error. Nothing is sent anywhere: the record is the
11
+ * page's, read by `useOtelLiveSpans` and drawn by `OtelLiveSpans`.
12
+ *
13
+ * A span started without a parent continues the innermost span still open
14
+ * of the same service — what one agent of the page is doing — so a tool call
15
+ * nests under its turn without the caller carrying a context; `parent: null`
16
+ * starts a trace of its own.
17
+ *
18
+ * @module otel/live/tracer
19
+ */
20
+ import type { OtelSpan, OtelSpanLink } from '../types';
21
+ /** What identifies a span: its trace and itself. */
22
+ export interface OtelSpanContext {
23
+ trace_id: string;
24
+ span_id: string;
25
+ }
26
+ /** An attribute's value, as OpenTelemetry allows it. */
27
+ export type OtelAttributeValue = string | number | boolean | readonly string[] | readonly number[] | readonly boolean[];
28
+ /** Attributes; an `undefined` value is left out. */
29
+ export type OtelAttributes = Record<string, OtelAttributeValue | undefined>;
30
+ /** A span's kind, as the OTEL views name it. */
31
+ export type OtelSpanKindName = 'INTERNAL' | 'SERVER' | 'CLIENT' | 'PRODUCER' | 'CONSUMER';
32
+ export interface OtelStartSpanOptions {
33
+ /** The service that does it: the tracer's own unless said. */
34
+ serviceName?: string;
35
+ kind?: OtelSpanKindName;
36
+ attributes?: OtelAttributes;
37
+ /**
38
+ * The span it is part of. Left out: the innermost span still open of the
39
+ * same service; `null`: a trace of its own.
40
+ */
41
+ parent?: OtelSpanContext | null;
42
+ /** When it started, in ms since the epoch: now unless said. */
43
+ startTime?: number;
44
+ /** The instrumentation scope: the tracer's own unless said. */
45
+ scopeName?: string;
46
+ links?: OtelSpanLink[];
47
+ }
48
+ /** A span being recorded. */
49
+ export interface OtelLiveSpan {
50
+ readonly context: OtelSpanContext;
51
+ /** Whether it has ended. */
52
+ readonly ended: boolean;
53
+ setAttribute(key: string, value: OtelAttributeValue | undefined): void;
54
+ setAttributes(attributes: OtelAttributes): void;
55
+ addEvent(name: string, attributes?: OtelAttributes, time?: number): void;
56
+ setStatus(code: 'OK' | 'ERROR', message?: string): void;
57
+ /** An `exception` event, and the status `ERROR`. */
58
+ recordException(error: unknown, time?: number): void;
59
+ updateName(name: string): void;
60
+ /** End it; a second end is ignored. */
61
+ end(time?: number): void;
62
+ }
63
+ /** The record, saved. */
64
+ export interface OtelLiveExport {
65
+ exportedAt: string;
66
+ spans: OtelSpan[];
67
+ }
68
+ export interface OtelLiveTracer {
69
+ /** The service a span is done by unless it says. */
70
+ readonly serviceName: string;
71
+ startSpan(name: string, options?: OtelStartSpanOptions): OtelLiveSpan;
72
+ /** The innermost span still open of a service (the tracer's own unless said). */
73
+ activeSpan(serviceName?: string): OtelSpanContext | undefined;
74
+ /** The spans, oldest first: a new array each time one changes. */
75
+ spans(): readonly OtelSpan[];
76
+ subscribe(listener: () => void): () => void;
77
+ /** Forget every span; those still open are recorded no more. */
78
+ clear(): void;
79
+ exportSpans(): OtelLiveExport;
80
+ }
81
+ export interface OtelLiveTracerOptions {
82
+ serviceName?: string;
83
+ scopeName?: string;
84
+ /** The clock: `Date.now` unless a test sets it. */
85
+ now?: () => number;
86
+ /** The most spans kept: the oldest ended ones go first. Default 2000. */
87
+ limit?: number;
88
+ /** Random bytes as hex, for ids: `crypto.getRandomValues` unless a test sets it. */
89
+ randomHex?: (bytes: number) => string;
90
+ }
91
+ /** A W3C `traceparent` header for a span: `00-<trace>-<span>-01`. */
92
+ export declare function traceparentOf(context: OtelSpanContext): string;
93
+ /** Make a tracer for the page. */
94
+ export declare function createOtelLiveTracer(options?: OtelLiveTracerOptions): OtelLiveTracer;
@@ -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 {
@@ -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,139 @@
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 { ActionList, ActionMenu, Box, Button, Text } from '@primer/react';
20
+ import { DownloadIcon, PauseIcon, PlayIcon, TrashIcon, } from '@primer/octicons-react';
21
+ import { toMs } from '../utils.js';
22
+ import { spansOfService } from '../live/filter.js';
23
+ import { useOtelLiveSpans } from '../live/useOtelLiveSpans.js';
24
+ import { OtelTracesList } from './OtelTracesList.js';
25
+ import { OtelSpanDetail } from './OtelSpanDetail.js';
26
+ /** The facets unless said: the service and the span kind. */
27
+ export const DEFAULT_SPAN_FACETS = [
28
+ { label: 'Service', value: span => span.service_name },
29
+ { label: 'Kind', value: span => span.kind },
30
+ ];
31
+ /** The spans each facet keeps: an empty selection keeps every value. */
32
+ export function filterSpansByFacets(spans, facets, selected) {
33
+ return spans.filter(span => facets.every((facet, index) => {
34
+ const values = selected[index];
35
+ if (!values?.size)
36
+ return true;
37
+ const value = facet.value(span);
38
+ return value !== undefined && values.has(value);
39
+ }));
40
+ }
41
+ function toggle(set, value) {
42
+ const next = new Set(set);
43
+ if (next.has(value)) {
44
+ next.delete(value);
45
+ }
46
+ else {
47
+ next.add(value);
48
+ }
49
+ return next;
50
+ }
51
+ function FacetMenu({ facet, options, selected, onChange, }) {
52
+ const name = facet.name ?? ((value) => value);
53
+ 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)))] }) })] }));
54
+ }
55
+ /** Save the spans as a JSON file. */
56
+ function download(name, record) {
57
+ const blob = new Blob([JSON.stringify(record, null, 2)], {
58
+ type: 'application/json',
59
+ });
60
+ const url = URL.createObjectURL(blob);
61
+ const link = document.createElement('a');
62
+ link.href = url;
63
+ link.download = `${name}.json`;
64
+ link.click();
65
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
66
+ }
67
+ 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, }) => {
68
+ const live = useOtelLiveSpans(given ? null : tracer);
69
+ const everything = given ?? live;
70
+ const [clearedAt, setClearedAt] = useState(0);
71
+ const [held, setHeld] = useState(null);
72
+ const [selected, setSelected] = useState(() => facets.map(() => new Set()));
73
+ const [selectedSpanId, setSelectedSpanId] = useState(null);
74
+ const recorded = useMemo(() => {
75
+ const own = spansOfService(everything, service);
76
+ return clearedAt
77
+ ? own.filter(span => toMs(span.start_time) >= clearedAt)
78
+ : own;
79
+ }, [everything, service, clearedAt]);
80
+ const shown = held ?? recorded;
81
+ const waiting = held ? recorded.length - held.length : 0;
82
+ const options = useMemo(() => facets.map(facet => [
83
+ ...new Set(recorded
84
+ .map(span => facet.value(span))
85
+ .filter((value) => value !== undefined)),
86
+ ].sort()), [facets, recorded]);
87
+ const rows = useMemo(() => filterSpansByFacets(shown, facets, selected), [shown, facets, selected]);
88
+ // The selected span as it is now: a live span changes as it goes.
89
+ const selectedSpan = useMemo(() => selectedSpanId
90
+ ? (everything.find(span => span.span_id === selectedSpanId) ?? null)
91
+ : null, [everything, selectedSpanId]);
92
+ const traceSpans = useMemo(() => selectedSpan
93
+ ? everything.filter(span => span.trace_id === selectedSpan.trace_id)
94
+ : [], [everything, selectedSpan]);
95
+ const side = detailPlacement === 'side' && !compact;
96
+ return (_jsxs(Box, { "data-otel-live-spans": "", sx: {
97
+ border: '1px solid',
98
+ borderColor: 'border.default',
99
+ borderRadius: 2,
100
+ bg: 'canvas.default',
101
+ color: 'fg.default',
102
+ minWidth: 0,
103
+ }, children: [_jsxs(Box, { role: "toolbar", "aria-label": label, sx: {
104
+ display: 'flex',
105
+ flexWrap: 'wrap',
106
+ gap: 2,
107
+ alignItems: 'center',
108
+ p: 2,
109
+ borderBottom: '1px solid',
110
+ borderColor: 'border.default',
111
+ }, 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, { sx: { flex: 1 } }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, "data-otel-span-count": "", children: [rows.length === shown.length
112
+ ? `${rows.length} ${rows.length === 1 ? 'span' : 'spans'}`
113
+ : `${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: () => {
114
+ setHeld(null);
115
+ setSelectedSpanId(null);
116
+ // One service's view of a shared record clears only itself.
117
+ if (tracer && !given && !service) {
118
+ tracer.clear();
119
+ }
120
+ else {
121
+ setClearedAt(Date.now());
122
+ }
123
+ }, "data-otel-clear": "", children: "Clear" }), _jsx(Button, { size: "small", leadingVisual: DownloadIcon, disabled: !recorded.length, onClick: () => download(exportName, {
124
+ exportedAt: new Date().toISOString(),
125
+ spans: [...recorded],
126
+ }), "data-otel-export": "", children: "Export" })] }), _jsxs(Box, { sx: {
127
+ display: side ? 'grid' : 'block',
128
+ gridTemplateColumns: side
129
+ ? 'minmax(0, 3fr) minmax(0, 2fr)'
130
+ : undefined,
131
+ }, children: [_jsx(Box, { sx: { 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": "", sx: {
132
+ maxHeight,
133
+ overflowY: 'auto',
134
+ minWidth: 0,
135
+ borderTop: side ? 'none' : '1px solid',
136
+ borderColor: 'border.default',
137
+ }, children: _jsx(OtelSpanDetail, { span: selectedSpan, traceSpans: traceSpans, onClose: () => setSelectedSpanId(null) }) }))] })] }));
138
+ };
139
+ export default OtelLiveSpans;
@@ -8,4 +8,10 @@
8
8
  */
9
9
  import React from 'react';
10
10
  import type { OtelSpanDetailProps } from '../types';
11
+ /**
12
+ * A value as the detail unfolds it: JSON text of an object or an array (the
13
+ * GenAI conventions carry a tool call's arguments and result so) is the value
14
+ * it says; anything else is itself.
15
+ */
16
+ export declare function unfoldedValue(value: unknown): unknown;
11
17
  export declare const OtelSpanDetail: React.FC<OtelSpanDetailProps>;
@@ -11,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  *
12
12
  * @module otel/OtelSpanDetail
13
13
  */
14
- import { useState } from 'react';
14
+ import { useMemo, useState } from 'react';
15
15
  import { Box, Text, IconButton, UnderlineNav, CounterLabel, Label, } from '@primer/react';
16
16
  import { XIcon, ChevronDownIcon, ChevronRightIcon, } from '@primer/octicons-react';
17
17
  import { formatDuration, buildSpanTree } from '../utils.js';
@@ -61,8 +61,28 @@ const CollapsibleSection = ({ title, data, defaultOpen = false }) => {
61
61
  overflowX: 'auto',
62
62
  }, children: Object.entries(data).map(([key, val]) => (_jsx(AttributeRow, { attrKey: key, value: val, depth: 0 }, key))) }))] }));
63
63
  };
64
+ /**
65
+ * A value as the detail unfolds it: JSON text of an object or an array (the
66
+ * GenAI conventions carry a tool call's arguments and result so) is the value
67
+ * it says; anything else is itself.
68
+ */
69
+ export function unfoldedValue(value) {
70
+ if (typeof value !== 'string')
71
+ return value;
72
+ const text = value.trim();
73
+ if (!/^[[{]/.test(text))
74
+ return value;
75
+ try {
76
+ const parsed = JSON.parse(text);
77
+ return typeof parsed === 'object' && parsed !== null ? parsed : value;
78
+ }
79
+ catch {
80
+ return value;
81
+ }
82
+ }
64
83
  /** Recursive attribute row – supports nested objects and arrays. */
65
- const AttributeRow = ({ attrKey, value, depth }) => {
84
+ const AttributeRow = ({ attrKey, value: given, depth }) => {
85
+ const value = useMemo(() => unfoldedValue(given), [given]);
66
86
  const [open, setOpen] = useState(depth < 1);
67
87
  const isObject = typeof value === 'object' && value !== null && !Array.isArray(value);
68
88
  const isArray = Array.isArray(value);
@@ -19,6 +19,8 @@ import { Blankslate } from '@primer/react/experimental';
19
19
  import { TelescopeIcon, ChevronRightIcon, ChevronDownIcon, } from '@primer/octicons-react';
20
20
  import { formatDuration, formatTime, buildSpanTree } from '../utils.js';
21
21
  const GRID_COLS = '140px 1fr 160px 90px';
22
+ /** Narrower: for a panel beside other content. */
23
+ const COMPACT_GRID_COLS = '96px minmax(0, 1fr) 96px 64px';
22
24
  // ── Helpers ─────────────────────────────────────────────────────────
23
25
  /** Extract gen_ai token counts from span attributes. */
24
26
  function getTokenUsage(span) {
@@ -35,9 +37,13 @@ function getTokenUsage(span) {
35
37
  return null;
36
38
  return { input, output };
37
39
  }
38
- export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }) => {
39
- // Set of expanded span_ids (show their children)
40
- const [expanded, setExpanded] = useState(new Set());
40
+ export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, emptyText, compact = false, defaultExpanded = false, renderSpanMark, describeSpan, }) => {
41
+ // The span_ids unfolded (or, when every span starts unfolded, folded).
42
+ const [toggled, setToggled] = useState(new Set());
43
+ const expanded = useMemo(() => ({
44
+ has: (spanId) => defaultExpanded ? !toggled.has(spanId) : toggled.has(spanId),
45
+ }), [toggled, defaultExpanded]);
46
+ const gridCols = compact ? COMPACT_GRID_COLS : GRID_COLS;
41
47
  // Build the span tree from flat input
42
48
  const roots = useMemo(() => buildSpanTree(spans), [spans]);
43
49
  // Flatten visible rows based on which nodes are expanded
@@ -58,7 +64,7 @@ export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }
58
64
  return rows;
59
65
  }, [roots, expanded]);
60
66
  const toggleExpand = useCallback((spanId) => {
61
- setExpanded(prev => {
67
+ setToggled(prev => {
62
68
  const next = new Set(prev);
63
69
  if (next.has(spanId)) {
64
70
  next.delete(spanId);
@@ -73,11 +79,11 @@ export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }
73
79
  return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', p: 5 }, children: _jsx(Spinner, { size: "medium" }) }));
74
80
  }
75
81
  if (spans.length === 0) {
76
- return (_jsxs(Blankslate, { children: [_jsx(Blankslate.Visual, { children: _jsx(TelescopeIcon, { size: 24 }) }), _jsx(Blankslate.Heading, { children: "No traces found" }), _jsx(Blankslate.Description, { children: "Send some telemetry data first." })] }));
82
+ return (_jsxs(Blankslate, { children: [_jsx(Blankslate.Visual, { children: _jsx(TelescopeIcon, { size: 24 }) }), _jsx(Blankslate.Heading, { children: "No traces found" }), _jsx(Blankslate.Description, { children: emptyText ?? 'Send some telemetry data first.' })] }));
77
83
  }
78
84
  return (_jsxs(Box, { sx: { width: '100%', flex: 1, minHeight: 0, overflow: 'auto' }, children: [_jsx(Box, { sx: {
79
85
  display: 'grid',
80
- gridTemplateColumns: GRID_COLS,
86
+ gridTemplateColumns: gridCols,
81
87
  gap: 2,
82
88
  px: 3,
83
89
  position: 'sticky',
@@ -97,15 +103,16 @@ export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }
97
103
  }, children: h }, h))) }), visibleRows.map(({ span, depth, hasChildren }, idx) => {
98
104
  const isSelected = selectedSpanId === span.span_id;
99
105
  const isExpanded = expanded.has(span.span_id);
100
- const indent = depth * 20;
101
- return (_jsxs(Box, { onClick: () => {
106
+ const indent = depth * (compact ? 12 : 20);
107
+ const description = describeSpan?.(span);
108
+ return (_jsxs(Box, { "data-otel-span": span.span_id, "data-otel-span-name": span.span_name, onClick: () => {
102
109
  if (hasChildren) {
103
110
  toggleExpand(span.span_id);
104
111
  }
105
112
  onSelectSpan?.(span);
106
113
  }, sx: {
107
114
  display: 'grid',
108
- gridTemplateColumns: GRID_COLS,
115
+ gridTemplateColumns: gridCols,
109
116
  gap: 2,
110
117
  px: 3,
111
118
  py: '5px',
@@ -121,7 +128,7 @@ export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }
121
128
  bg: isSelected ? 'accent.subtle' : 'canvas.subtle',
122
129
  },
123
130
  }, children: [_jsx(Text, { sx: {
124
- fontSize: 1,
131
+ fontSize: compact ? 0 : 1,
125
132
  fontFamily: 'mono',
126
133
  color: 'fg.muted',
127
134
  whiteSpace: 'nowrap',
@@ -148,12 +155,14 @@ export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }
148
155
  justifyContent: 'center',
149
156
  color: 'fg.subtle',
150
157
  fontSize: 0,
151
- }, children: "\u2500" })) : null, _jsx(Text, { sx: {
158
+ }, children: "\u2500" })) : null, renderSpanMark?.(span), _jsx(Text, { sx: {
152
159
  fontSize: 1,
153
160
  overflow: 'hidden',
154
161
  textOverflow: 'ellipsis',
155
162
  whiteSpace: 'nowrap',
156
- }, title: span.span_name, children: span.span_name }), span.status_code === 'ERROR' && (_jsx(Label, { variant: "danger", size: "small", children: "ERROR" })), (() => {
163
+ flexShrink: description ? 0 : 1,
164
+ maxWidth: description ? '60%' : undefined,
165
+ }, title: span.span_name, children: span.span_name }), span.in_progress && (_jsx(Label, { variant: "attention", size: "small", children: "running" })), span.status_code === 'ERROR' && (_jsx(Label, { variant: "danger", size: "small", children: "ERROR" })), (() => {
157
166
  const usage = getTokenUsage(span);
158
167
  if (!usage)
159
168
  return null;
@@ -179,7 +188,14 @@ export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }
179
188
  alignItems: 'center',
180
189
  gap: '1px',
181
190
  }, children: [_jsx(Text, { sx: { fontSize: '9px', color: 'fg.subtle' }, children: "\u2199" }), _jsx(Text, { children: usage.output })] }))] }));
182
- })(), span.otel_scope_name && (_jsx(Label, { size: "small", variant: "secondary", sx: { flexShrink: 0, fontSize: '10px' }, children: span.otel_scope_name }))] }), _jsx(Text, { sx: {
191
+ })(), description && (_jsx(Text, { sx: {
192
+ fontSize: 1,
193
+ color: 'fg.muted',
194
+ overflow: 'hidden',
195
+ textOverflow: 'ellipsis',
196
+ whiteSpace: 'nowrap',
197
+ minWidth: 0,
198
+ }, title: description, "data-otel-span-description": "", children: description })), span.otel_scope_name && !compact && (_jsx(Label, { size: "small", variant: "secondary", sx: { flexShrink: 0, fontSize: '10px' }, children: span.otel_scope_name }))] }), _jsx(Text, { sx: {
183
199
  fontSize: 1,
184
200
  color: 'fg.muted',
185
201
  overflow: 'hidden',
@@ -194,6 +210,6 @@ export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }
194
210
  color: span.duration_ms != null && span.duration_ms > 1000
195
211
  ? 'danger.fg'
196
212
  : 'fg.default',
197
- }, children: formatDuration(span.duration_ms) })] }, `${span.trace_id}-${span.span_id}-${idx}`));
213
+ }, children: span.in_progress ? '…' : formatDuration(span.duration_ms) })] }, `${span.trace_id}-${span.span_id}-${idx}`));
198
214
  })] }));
199
215
  };
@@ -14,6 +14,9 @@ export { OtelSearchBar } from './OtelSearchBar';
14
14
  export { OtelSpanDetail } from './OtelSpanDetail';
15
15
  export { OtelSpanTree } from './OtelSpanTree';
16
16
  export { OtelTimeline } from './OtelTimeline';
17
+ export { OtelLiveSpans, DEFAULT_SPAN_FACETS, filterSpansByFacets, } from './OtelLiveSpans';
18
+ export type { OtelLiveSpansProps, OtelSpanFacet } from './OtelLiveSpans';
19
+ export { unfoldedValue } from './OtelSpanDetail';
17
20
  export { OtelTimelineRangeSlider } from './OtelTimelineRangeSlider';
18
21
  export { OtelSqlView } from './OtelSqlView';
19
22
  export type { OtelSqlViewProps } from './OtelSqlView';
@@ -16,6 +16,8 @@ export { OtelSearchBar } from './OtelSearchBar.js';
16
16
  export { OtelSpanDetail } from './OtelSpanDetail.js';
17
17
  export { OtelSpanTree } from './OtelSpanTree.js';
18
18
  export { OtelTimeline } from './OtelTimeline.js';
19
+ export { OtelLiveSpans, DEFAULT_SPAN_FACETS, filterSpansByFacets, } from './OtelLiveSpans.js';
20
+ export { unfoldedValue } from './OtelSpanDetail.js';
19
21
  export { OtelTimelineRangeSlider } from './OtelTimelineRangeSlider.js';
20
22
  export { OtelSqlView } from './OtelSqlView.js';
21
23
  export { OtelSystemView } from './OtelSystemView.js';
@@ -122,6 +122,12 @@ export interface SignInSimpleProps {
122
122
  * Sign-up URL opened when clicking the sign-up button.
123
123
  */
124
124
  signUpHref?: string;
125
+ /**
126
+ * Called on the sign-up button instead of opening `signUpHref`: a host
127
+ * with a router sends the reader to its own sign-up page without leaving
128
+ * the app.
129
+ */
130
+ onSignUp?: () => void;
125
131
  /**
126
132
  * Forgot-password URL for password form.
127
133
  */
@@ -101,7 +101,7 @@ const SignInInputGlobalStyle = createGlobalStyle `
101
101
  }
102
102
  `;
103
103
  // ── Component ────────────────────────────────────────────────────────
104
- export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp, name, title = 'Datalayer OTEL', description = 'Sign in to access the observability dashboard.', icon, leadingIcon = _jsx(TelescopeIcon, { size: 24 }), github = false, google = false, linkedin = false, apiKey = false, signUp = true, socialSignInNavigationTarget, asDoc = false, fillHeight = true, hideHero = false, calloutTitle, calloutDescription, passwordToggleLabel = 'Sign in with a password', signUpTitle = "Don't have an account?", signUpDescription = 'Create a free Datalayer account with your email address.', signUpLabel = 'Sign up with email', signUpHref = 'https://datalayer.ai/signup', forgotPasswordHref = '/password', forgotPasswordLabel = 'Forgot password?', showForgotPassword = true, socialButtonMarginTop = 3, actionButtonMaxWidth = 320, }) => {
104
+ export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp, name, title = 'Datalayer OTEL', description = 'Sign in to access the observability dashboard.', icon, leadingIcon = _jsx(TelescopeIcon, { size: 24 }), github = false, google = false, linkedin = false, apiKey = false, signUp = true, socialSignInNavigationTarget, asDoc = false, fillHeight = true, hideHero = false, calloutTitle, calloutDescription, passwordToggleLabel = 'Sign in with a password', signUpTitle = "Don't have an account?", signUpDescription = 'Create a free Datalayer account with your email address.', signUpLabel = 'Sign up with email', signUpHref = 'https://datalayer.ai/signup', onSignUp, forgotPasswordHref = '/password', forgotPasswordLabel = 'Forgot password?', showForgotPassword = true, socialButtonMarginTop = 3, actionButtonMaxWidth = 320, }) => {
105
105
  const compactDocMode = asDoc && !hideHero;
106
106
  const headingText = name ?? (asDoc && title === 'Datalayer OTEL' ? 'Datalayer Sign In' : title);
107
107
  const headingIcon = icon ?? (asDoc ? _jsx(SignInIcon, { size: 24 }) : leadingIcon);
@@ -208,8 +208,12 @@ export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp,
208
208
  closeApiKeyDialog();
209
209
  }, [apiKeyValue, onApiKeySignIn, closeApiKeyDialog]);
210
210
  const handleSignUp = useCallback(() => {
211
+ if (onSignUp) {
212
+ onSignUp();
213
+ return;
214
+ }
211
215
  window.location.assign(signUpHref);
212
- }, [signUpHref]);
216
+ }, [onSignUp, signUpHref]);
213
217
  const handleForgotPassword = useCallback(() => {
214
218
  if (!forgotPasswordHref) {
215
219
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/core",
3
- "version": "1.2.15",
3
+ "version": "1.2.18",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",