@datalayer/core 1.2.27 → 1.2.28
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/api/evals/__tests__/external-definition.golden.json +110 -0
- package/lib/api/evals/client.d.ts +13 -1
- package/lib/api/evals/client.js +13 -0
- package/lib/api/evals/types.d.ts +168 -1
- package/lib/api/mcp/agentAccess.d.ts +10 -0
- package/lib/api/mcp/agentAccess.js +28 -0
- package/lib/api/mcp/generated.js +20 -0
- package/lib/api/mcp/index.d.ts +1 -0
- package/lib/api/mcp/index.js +1 -0
- package/lib/components/index.d.ts +1 -0
- package/lib/components/index.js +1 -0
- package/lib/components/mcp/ScopeList.js +10 -1
- package/lib/components/toast/ToastPanel.d.ts +11 -0
- package/lib/components/toast/ToastPanel.js +31 -0
- package/lib/components/toast/index.d.ts +2 -0
- package/lib/components/toast/index.js +6 -0
- package/lib/components/toast/toastQueue.d.ts +33 -0
- package/lib/components/toast/toastQueue.js +47 -0
- package/lib/hooks/useDatalayer.js +12 -16
- package/lib/hooks/useToast.d.ts +6 -0
- package/lib/hooks/useToast.js +40 -1
- package/lib/models/McpAgentAccess.d.ts +95 -0
- package/lib/models/McpAgentAccess.js +5 -0
- package/lib/models/index.d.ts +1 -0
- package/lib/models/index.js +1 -0
- package/lib/otel/views/OtelLive.js +2 -2
- package/lib/otel/views/OtelLiveSpans.js +2 -2
- package/lib/otel/views/OtelLogsList.js +3 -3
- package/lib/otel/views/OtelMetricsChart.d.ts +13 -0
- package/lib/otel/views/OtelMetricsChart.js +109 -26
- package/lib/otel/views/OtelSpanDetail.js +3 -3
- package/lib/otel/views/OtelSqlView.js +4 -4
- package/lib/otel/views/OtelSystemView.js +2 -2
- package/lib/otel/views/OtelTimelineRangeSlider.js +2 -2
- package/lib/otel/views/OtelTracesList.js +2 -2
- package/lib/views/mcp/AuditLog.js +1 -1
- package/lib/views/mcp/ConnectedAgents.d.ts +1 -1
- package/lib/views/mcp/ConnectedAgents.js +13 -10
- package/lib/views/mcp/McpDashboard.d.ts +1 -1
- package/lib/views/mcp/McpDashboard.js +6 -6
- package/lib/views/mcp/McpHome.d.ts +3 -1
- package/lib/views/mcp/McpHome.js +19 -9
- package/lib/views/mcp/Policies.js +1 -1
- package/lib/views/mcp/types.d.ts +8 -4
- package/lib/views/otel/OtelHeader.js +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
let toasts = [];
|
|
6
|
+
const listeners = new Set();
|
|
7
|
+
let next = 0;
|
|
8
|
+
const emit = () => {
|
|
9
|
+
for (const listener of listeners) {
|
|
10
|
+
listener();
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
/** Info and success go on their own after five seconds; a warning or an error stays. */
|
|
14
|
+
export const defaultAutoClose = (variant) => variant === 'info' || variant === 'success' ? 5000 : false;
|
|
15
|
+
export const toastQueue = {
|
|
16
|
+
subscribe(listener) {
|
|
17
|
+
listeners.add(listener);
|
|
18
|
+
return () => {
|
|
19
|
+
listeners.delete(listener);
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
snapshot() {
|
|
23
|
+
return toasts;
|
|
24
|
+
},
|
|
25
|
+
push(toast) {
|
|
26
|
+
// The same toast already waiting is not said twice: a page that asks
|
|
27
|
+
// twice, or two requests that fail alike, show it once.
|
|
28
|
+
const same = toasts.find(queued => queued.variant === toast.variant && queued.message === toast.message);
|
|
29
|
+
if (same) {
|
|
30
|
+
return same.id;
|
|
31
|
+
}
|
|
32
|
+
next += 1;
|
|
33
|
+
const id = `toast-${next}`;
|
|
34
|
+
toasts = [...toasts, { ...toast, id }];
|
|
35
|
+
emit();
|
|
36
|
+
return id;
|
|
37
|
+
},
|
|
38
|
+
update(id, patch) {
|
|
39
|
+
toasts = toasts.map(toast => toast.id === id ? { ...toast, ...patch } : toast);
|
|
40
|
+
emit();
|
|
41
|
+
},
|
|
42
|
+
/** Take one toast away, or every toast without an id. */
|
|
43
|
+
dismiss(id) {
|
|
44
|
+
toasts = id === undefined ? [] : toasts.filter(toast => toast.id !== id);
|
|
45
|
+
emit();
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -67,23 +67,19 @@ export function useDatalayer(props = {}) {
|
|
|
67
67
|
}
|
|
68
68
|
else {
|
|
69
69
|
if (notifyOnError_ && !isForbidden) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
enqueueToast(`${responseError.message}`, { variant: 'error' });
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
enqueueToast(`API Error`, { variant: 'error' });
|
|
81
|
-
}
|
|
82
|
-
if (responseError.exceptionMessage) {
|
|
83
|
-
enqueueToast(`${responseError.exceptionMessage}`, {
|
|
84
|
-
variant: 'error',
|
|
85
|
-
});
|
|
70
|
+
// One toast per failed request, its lines together and each once
|
|
71
|
+
// (2 November 2026: its message and its exception were a toast
|
|
72
|
+
// each, so a page that asked twice showed four).
|
|
73
|
+
const once = (lines) => Array.from(new Set(lines.map(line => `${line ?? ''}`.trim()).filter(Boolean))).join('\n');
|
|
74
|
+
const warnings = once(responseError.warnings ?? []);
|
|
75
|
+
if (warnings) {
|
|
76
|
+
enqueueToast(warnings, { variant: 'warning' });
|
|
86
77
|
}
|
|
78
|
+
enqueueToast(once([
|
|
79
|
+
...(responseError.errors ?? []),
|
|
80
|
+
responseError.message || 'API Error',
|
|
81
|
+
responseError.exceptionMessage,
|
|
82
|
+
]), { variant: 'error' });
|
|
87
83
|
}
|
|
88
84
|
}
|
|
89
85
|
const response = {
|
package/lib/hooks/useToast.d.ts
CHANGED
|
@@ -14,6 +14,12 @@ export type ToastProps = {
|
|
|
14
14
|
*/
|
|
15
15
|
variant: 'info' | 'success' | 'warning' | 'error';
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Toasts in JupyterLab, as they always were: its notifications, or
|
|
19
|
+
* react-toastify until they are loaded. Everywhere else (the SaaS, the
|
|
20
|
+
* desktop…) they go to `ToastPanel`, primer-addons' sliding panel, which the
|
|
21
|
+
* application mounts once (2 November 2026).
|
|
22
|
+
*/
|
|
17
23
|
export declare const useToast: () => Readonly<{
|
|
18
24
|
/**
|
|
19
25
|
* Emit a message as a toast.
|
package/lib/hooks/useToast.js
CHANGED
|
@@ -3,6 +3,7 @@ import React from 'react';
|
|
|
3
3
|
import { toast } from 'react-toastify';
|
|
4
4
|
import { Button } from '@primer/react';
|
|
5
5
|
import { isInsideJupyterLab } from '../utils/index.js';
|
|
6
|
+
import { defaultAutoClose, toastQueue } from '../components/toast/toastQueue.js';
|
|
6
7
|
// `@datalayer/jupyter-react` and `@jupyterlab/apputils` are heavy (they pull
|
|
7
8
|
// JupyterLab / CodeMirror / lumino) and are only needed when running INSIDE
|
|
8
9
|
// JupyterLab. They are lazily loaded here so the standalone bundle (landing app)
|
|
@@ -76,11 +77,25 @@ function createContent(message, closeHandler, actions) {
|
|
|
76
77
|
return (_jsx(React.Suspense, { fallback: null, children: _jsx(JupyterReactTheme, { children: _jsx(ToastButton, { action: action, closeToast: closeHandler }) }) }, 'button-' + idx));
|
|
77
78
|
})] }))] }));
|
|
78
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Toasts in JupyterLab, as they always were: its notifications, or
|
|
82
|
+
* react-toastify until they are loaded. Everywhere else (the SaaS, the
|
|
83
|
+
* desktop…) they go to `ToastPanel`, primer-addons' sliding panel, which the
|
|
84
|
+
* application mounts once (2 November 2026).
|
|
85
|
+
*/
|
|
79
86
|
export const useToast = () => {
|
|
80
87
|
// This CANNOT use Jupyter React hooks as it may be used outside Jupyter React context.
|
|
81
88
|
const insideJupyterLab = isInsideJupyterLab();
|
|
82
89
|
const enqueueToast = (message, options = { variant: 'info' }) => {
|
|
83
90
|
const { actions, autoClose } = options;
|
|
91
|
+
if (!insideJupyterLab) {
|
|
92
|
+
return toastQueue.push({
|
|
93
|
+
variant: options.variant,
|
|
94
|
+
message,
|
|
95
|
+
actions,
|
|
96
|
+
autoClose: autoClose ?? defaultAutoClose(options.variant),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
84
99
|
const theme = resolveToastTheme();
|
|
85
100
|
switch (options.variant) {
|
|
86
101
|
case 'info': {
|
|
@@ -138,6 +153,27 @@ export const useToast = () => {
|
|
|
138
153
|
}
|
|
139
154
|
};
|
|
140
155
|
const trackAsyncTask = (promise, options) => {
|
|
156
|
+
if (!insideJupyterLab) {
|
|
157
|
+
// One toast for the task: pending, then what it came to.
|
|
158
|
+
const message = (value, result) => typeof value === 'function'
|
|
159
|
+
? String(value(result))
|
|
160
|
+
: String(value);
|
|
161
|
+
const id = toastQueue.push({
|
|
162
|
+
variant: 'info',
|
|
163
|
+
message: message(options.pending.message, undefined),
|
|
164
|
+
autoClose: false,
|
|
165
|
+
});
|
|
166
|
+
promise.then(result => toastQueue.update(id, {
|
|
167
|
+
variant: 'success',
|
|
168
|
+
message: message(options.success.message, result),
|
|
169
|
+
autoClose: defaultAutoClose('success'),
|
|
170
|
+
}), reason => toastQueue.update(id, {
|
|
171
|
+
variant: 'error',
|
|
172
|
+
message: message(options.error.message, reason),
|
|
173
|
+
autoClose: defaultAutoClose('error'),
|
|
174
|
+
}));
|
|
175
|
+
return id;
|
|
176
|
+
}
|
|
141
177
|
const theme = resolveToastTheme();
|
|
142
178
|
return insideJupyterLab && NotificationApi
|
|
143
179
|
? NotificationApi.promise(promise, options)
|
|
@@ -169,7 +205,10 @@ export const useToast = () => {
|
|
|
169
205
|
* @param id Toast id
|
|
170
206
|
*/
|
|
171
207
|
dismiss: (id) => {
|
|
172
|
-
if (insideJupyterLab
|
|
208
|
+
if (!insideJupyterLab) {
|
|
209
|
+
toastQueue.dismiss(id === undefined ? undefined : String(id));
|
|
210
|
+
}
|
|
211
|
+
else if (insideJupyterLab && NotificationApi) {
|
|
173
212
|
NotificationApi.dismiss(id);
|
|
174
213
|
}
|
|
175
214
|
else {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one agent may execute and reach, as the Jupyter MCP Server decides it.
|
|
3
|
+
*
|
|
4
|
+
* Every answer here comes from the steps a call goes through — the token's
|
|
5
|
+
* scopes, the policy layers, then Spacer or Contents for the object — so a
|
|
6
|
+
* tool or a notebook listed as refused is one the agent would be refused.
|
|
7
|
+
*
|
|
8
|
+
* @module models/McpAgentAccess
|
|
9
|
+
*/
|
|
10
|
+
/** The agent an answer is about. `grant` is null for the token that asked. */
|
|
11
|
+
export interface McpAgentNamed {
|
|
12
|
+
grant: string | null;
|
|
13
|
+
clientId: string | null;
|
|
14
|
+
clientName: string | null;
|
|
15
|
+
scopes: string[];
|
|
16
|
+
orgUid: string | null;
|
|
17
|
+
teamUid: string | null;
|
|
18
|
+
}
|
|
19
|
+
export type McpToolsetHealthState = 'ready' | 'degraded' | 'unknown';
|
|
20
|
+
export interface McpToolsetHealth {
|
|
21
|
+
/** `unknown` is a toolset nothing here measures, never one reported well. */
|
|
22
|
+
state: McpToolsetHealthState;
|
|
23
|
+
detail: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* How a client comes to hold a toolset: the plain endpoint serves `base`, a
|
|
27
|
+
* URL names an `optional` one, a `source` is enabled in a session, and an
|
|
28
|
+
* `application` one is reached through an application's connection.
|
|
29
|
+
*/
|
|
30
|
+
export type McpToolsetSelected = 'base' | 'optional' | 'source' | 'application';
|
|
31
|
+
/** One tool, decided for this agent. */
|
|
32
|
+
export interface McpAgentTool {
|
|
33
|
+
name: string;
|
|
34
|
+
/** The scopes it requires, space-separated. */
|
|
35
|
+
scope: string;
|
|
36
|
+
allowed: boolean;
|
|
37
|
+
/** Why not: `scope`, a policy rule, `connection` or `item`. */
|
|
38
|
+
rule: string | null;
|
|
39
|
+
/** Which layer decided. */
|
|
40
|
+
decidedBy: string | null;
|
|
41
|
+
detail: string | null;
|
|
42
|
+
/** The layer that makes a person approve a call first, when one does. */
|
|
43
|
+
approval: string | null;
|
|
44
|
+
}
|
|
45
|
+
/** A source's own rule for its calls, as Contents keeps it. */
|
|
46
|
+
export type McpSourceApprovalPolicy = 'explicit' | 'auto-allowlisted' | 'never';
|
|
47
|
+
export interface McpAgentToolset {
|
|
48
|
+
kind: 'toolset' | 'source';
|
|
49
|
+
/** The source, when it is one. */
|
|
50
|
+
uid?: string;
|
|
51
|
+
name: string;
|
|
52
|
+
title: string;
|
|
53
|
+
description: string | null;
|
|
54
|
+
selected: McpToolsetSelected;
|
|
55
|
+
health: McpToolsetHealth;
|
|
56
|
+
/** A source's; a toolset's tools each say their own `approval`. */
|
|
57
|
+
approvalPolicy: McpSourceApprovalPolicy | null;
|
|
58
|
+
tools: McpAgentTool[];
|
|
59
|
+
}
|
|
60
|
+
export interface McpAgentToolsets {
|
|
61
|
+
agent: McpAgentNamed;
|
|
62
|
+
toolsets: McpAgentToolset[];
|
|
63
|
+
/** Why the sources are missing, when Contents could not be read. */
|
|
64
|
+
sourcesError: string | null;
|
|
65
|
+
}
|
|
66
|
+
/** One notebook or source, and how far this agent reaches it. */
|
|
67
|
+
export interface McpReachRow {
|
|
68
|
+
kind: 'notebook' | 'source';
|
|
69
|
+
uid: string;
|
|
70
|
+
name: string;
|
|
71
|
+
/** A notebook's Space, a source's kind. */
|
|
72
|
+
where: string | null;
|
|
73
|
+
read: boolean;
|
|
74
|
+
write: boolean;
|
|
75
|
+
execute: boolean;
|
|
76
|
+
/** Why it stops short of `execute`, in sentences. */
|
|
77
|
+
refused: string[];
|
|
78
|
+
/** Whether a source is the caller's own. */
|
|
79
|
+
yours?: boolean;
|
|
80
|
+
/** Set when the check could not run: an answer about the check, not the agent. */
|
|
81
|
+
unchecked: string | null;
|
|
82
|
+
}
|
|
83
|
+
export interface McpAgentReach {
|
|
84
|
+
agent: McpAgentNamed;
|
|
85
|
+
notebooks: McpReachRow[];
|
|
86
|
+
sources: McpReachRow[];
|
|
87
|
+
/** How many there are of each, of which `limit` were asked about. */
|
|
88
|
+
notebooksListed: number;
|
|
89
|
+
sourcesListed: number;
|
|
90
|
+
limit: number;
|
|
91
|
+
/** Contents has more than were read. */
|
|
92
|
+
sourcesTruncated: boolean;
|
|
93
|
+
notebooksError: string | null;
|
|
94
|
+
sourcesError: string | null;
|
|
95
|
+
}
|
package/lib/models/index.d.ts
CHANGED
package/lib/models/index.js
CHANGED
|
@@ -55,6 +55,7 @@ export * from './Invite.js';
|
|
|
55
55
|
export * from './LandingRoles.js';
|
|
56
56
|
export * from './McpAuditEvent.js';
|
|
57
57
|
export * from './McpBinding.js';
|
|
58
|
+
export * from './McpAgentAccess.js';
|
|
58
59
|
export * from './McpPolicy.js';
|
|
59
60
|
export * from './McpTask.js';
|
|
60
61
|
export * from './Member.js';
|
|
@@ -13,7 +13,7 @@ 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 } from '@datalayer/primer-addons';
|
|
16
|
+
import { Box, Card } from '@datalayer/primer-addons';
|
|
17
17
|
import { Text, Button, Label } from '@primer/react';
|
|
18
18
|
import { GitBranchIcon, ClockIcon } from '@primer/octicons-react';
|
|
19
19
|
import { getOtelConsumeUrl } from '../../state/substates/CoreState.js';
|
|
@@ -288,7 +288,7 @@ export const OtelLive = ({ baseUrl = getOtelConsumeUrl(), wsBaseUrl, token, auto
|
|
|
288
288
|
return ts >= s && ts <= e;
|
|
289
289
|
});
|
|
290
290
|
}, [metrics, isRangeActive, rangeStart, rangeEnd]);
|
|
291
|
-
return (_jsxs(
|
|
291
|
+
return (_jsxs(Card, { border: true, shadow: "none", display: "flex", flexDirection: "column", flex: 1, minHeight: 0, height: "100%", color: "fg.default", 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'
|
|
292
292
|
? tracesLoading
|
|
293
293
|
: signal === 'logs'
|
|
294
294
|
? logsLoading
|
|
@@ -16,7 +16,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
16
16
|
* @module otel/views/OtelLiveSpans
|
|
17
17
|
*/
|
|
18
18
|
import { useMemo, useState } from 'react';
|
|
19
|
-
import { Box } from '@datalayer/primer-addons';
|
|
19
|
+
import { Box, Card } from '@datalayer/primer-addons';
|
|
20
20
|
import { ActionList, ActionMenu, Button, Text } from '@primer/react';
|
|
21
21
|
import { DownloadIcon, PauseIcon, PlayIcon, TrashIcon, } from '@primer/octicons-react';
|
|
22
22
|
import { toMs } from '../utils.js';
|
|
@@ -94,7 +94,7 @@ export const OtelLiveSpans = ({ tracer, spans: given, service, facets = DEFAULT_
|
|
|
94
94
|
? everything.filter(span => span.trace_id === selectedSpan.trace_id)
|
|
95
95
|
: [], [everything, selectedSpan]);
|
|
96
96
|
const side = detailPlacement === 'side' && !compact;
|
|
97
|
-
return (_jsxs(
|
|
97
|
+
return (_jsxs(Card, { border: true, shadow: "none", display: "block", "data-otel-live-spans": "", 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
98
|
? `${rows.length} ${rows.length === 1 ? 'span' : 'spans'}`
|
|
99
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
100
|
setHeld(null);
|
|
@@ -12,7 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
12
12
|
* @module otel/OtelLogsList
|
|
13
13
|
*/
|
|
14
14
|
import React, { useState } from 'react';
|
|
15
|
-
import { Box } from '@datalayer/primer-addons';
|
|
15
|
+
import { Box, Card } from '@datalayer/primer-addons';
|
|
16
16
|
import { Text, Label, Spinner } from '@primer/react';
|
|
17
17
|
import { Blankslate } from '@primer/react/experimental';
|
|
18
18
|
import { LogIcon } from '@primer/octicons-react';
|
|
@@ -27,13 +27,13 @@ const LogDetail = ({ log }) => (_jsxs(Box, { gridColumn: "1 / -1", bg: "canvas.s
|
|
|
27
27
|
color: 'fg.muted',
|
|
28
28
|
display: 'block',
|
|
29
29
|
mb: 1,
|
|
30
|
-
}, children: "Body" }), _jsx(
|
|
30
|
+
}, children: "Body" }), _jsx(Card, { border: true, shadow: "none", display: "block", as: "pre", fontSize: 1, fontFamily: "mono", whiteSpace: "pre-wrap", wordBreak: "break-word", m: 0, p: 2, children: log.body })] }), log.trace_id && (_jsxs(Box, { display: "flex", gap: 3, mb: 2, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', fontWeight: 'bold' }, children: "trace_id" }), _jsx(Text, { sx: { fontFamily: 'mono', fontSize: 0 }, children: log.trace_id }), log.span_id && (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', fontWeight: 'bold' }, children: "span_id" }), _jsx(Text, { sx: { fontFamily: 'mono', fontSize: 0 }, children: log.span_id })] }))] })), log.attributes && Object.keys(log.attributes).length > 0 && (_jsxs(Box, { children: [_jsx(Text, { sx: {
|
|
31
31
|
fontSize: 0,
|
|
32
32
|
fontWeight: 'bold',
|
|
33
33
|
color: 'fg.muted',
|
|
34
34
|
display: 'block',
|
|
35
35
|
mb: 1,
|
|
36
|
-
}, children: "Attributes" }), _jsx(
|
|
36
|
+
}, children: "Attributes" }), _jsx(Card, { border: true, shadow: "none", display: "block", p: 2, children: Object.entries(log.attributes).map(([k, v]) => (_jsxs(Box, { display: "flex", gap: 2, py: 1, borderBottom: "1px solid", borderColor: "border.muted", children: [_jsx(Text, { sx: {
|
|
37
37
|
color: 'accent.fg',
|
|
38
38
|
fontSize: 0,
|
|
39
39
|
fontFamily: 'mono',
|
|
@@ -13,6 +13,19 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import React from 'react';
|
|
15
15
|
import type { OtelMetric } from '../types';
|
|
16
|
+
/**
|
|
17
|
+
* The colours ECharts draws its text, axes, grid lines and tooltip with.
|
|
18
|
+
* A canvas reads no CSS variable, so they are read off a probe the theme
|
|
19
|
+
* colours (fg.default, fg.muted, border.muted, canvas.overlay), resolved to
|
|
20
|
+
* what the colour mode makes them: hard-coded, the axes and the tooltip were
|
|
21
|
+
* light-mode only and unreadable in the dark (28 October 2026).
|
|
22
|
+
*/
|
|
23
|
+
export interface ChartInk {
|
|
24
|
+
text: string;
|
|
25
|
+
muted: string;
|
|
26
|
+
line: string;
|
|
27
|
+
surface: string;
|
|
28
|
+
}
|
|
16
29
|
export interface OtelMetricsChartProps {
|
|
17
30
|
metrics: OtelMetric[];
|
|
18
31
|
/** Height per chart panel in px. Default 240. */
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } 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
|
+
* OtelMetricsChart – Type-aware ECharts visualisation for OTEL metrics.
|
|
8
|
+
*
|
|
9
|
+
* Renders one chart section per `metric_type` (`sum`, `histogram`, `gauge`,
|
|
10
|
+
* and any other type present) with an appropriate chart style:
|
|
11
|
+
*
|
|
12
|
+
* - **sum** (counters): area-line chart with gradient fill
|
|
13
|
+
* - **histogram**: bar chart with grouped series
|
|
14
|
+
* - **gauge**: plain line chart (no area fill)
|
|
15
|
+
* - **other**: falls back to area-line
|
|
16
|
+
*
|
|
17
|
+
* @module otel/OtelMetricsChart
|
|
18
|
+
*/
|
|
19
|
+
import React, { useLayoutEffect, useRef, useState } from 'react';
|
|
2
20
|
import * as echarts from 'echarts';
|
|
3
21
|
import ReactECharts from 'echarts-for-react';
|
|
4
22
|
import { Box } from '@datalayer/primer-addons';
|
|
@@ -108,30 +126,90 @@ const TYPE_ORDER = {
|
|
|
108
126
|
gauge: 2,
|
|
109
127
|
exponentialHistogram: 3,
|
|
110
128
|
};
|
|
129
|
+
/** Light-mode values, until the probe has been read. */
|
|
130
|
+
const DEFAULT_INK = {
|
|
131
|
+
text: '#1f2328',
|
|
132
|
+
muted: '#59636e',
|
|
133
|
+
line: '#d1d9e0',
|
|
134
|
+
surface: '#ffffff',
|
|
135
|
+
};
|
|
136
|
+
function useChartInk() {
|
|
137
|
+
const probe = useRef(null);
|
|
138
|
+
const [ink, setInk] = useState(DEFAULT_INK);
|
|
139
|
+
useLayoutEffect(() => {
|
|
140
|
+
const node = probe.current;
|
|
141
|
+
if (!node || typeof getComputedStyle === 'undefined') {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const read = () => {
|
|
145
|
+
const style = getComputedStyle(node);
|
|
146
|
+
const muted = node.firstElementChild
|
|
147
|
+
? getComputedStyle(node.firstElementChild).color
|
|
148
|
+
: style.color;
|
|
149
|
+
const next = {
|
|
150
|
+
text: style.color || DEFAULT_INK.text,
|
|
151
|
+
muted: muted || DEFAULT_INK.muted,
|
|
152
|
+
line: style.borderTopColor || DEFAULT_INK.line,
|
|
153
|
+
surface: style.backgroundColor || DEFAULT_INK.surface,
|
|
154
|
+
};
|
|
155
|
+
setInk(current => current.text === next.text &&
|
|
156
|
+
current.muted === next.muted &&
|
|
157
|
+
current.line === next.line &&
|
|
158
|
+
current.surface === next.surface
|
|
159
|
+
? current
|
|
160
|
+
: next);
|
|
161
|
+
};
|
|
162
|
+
read();
|
|
163
|
+
// The colour mode changes on the document's attributes.
|
|
164
|
+
const observer = new MutationObserver(read);
|
|
165
|
+
observer.observe(document.documentElement, {
|
|
166
|
+
attributes: true,
|
|
167
|
+
subtree: false,
|
|
168
|
+
});
|
|
169
|
+
observer.observe(document.body, { attributes: true, subtree: false });
|
|
170
|
+
return () => observer.disconnect();
|
|
171
|
+
});
|
|
172
|
+
return [probe, ink];
|
|
173
|
+
}
|
|
174
|
+
/** The probe the ink is read off: drawn, never seen. */
|
|
175
|
+
const InkProbe = React.forwardRef((_, ref) => (_jsx(Box, { ref: ref, "aria-hidden": true, color: "fg.default", bg: "canvas.overlay", borderTop: "1px solid", borderColor: "border.muted", sx: {
|
|
176
|
+
position: 'absolute',
|
|
177
|
+
width: 0,
|
|
178
|
+
height: 0,
|
|
179
|
+
overflow: 'hidden',
|
|
180
|
+
pointerEvents: 'none',
|
|
181
|
+
}, children: _jsx(Box, { as: "span", color: "fg.muted" }) })));
|
|
182
|
+
InkProbe.displayName = 'InkProbe';
|
|
111
183
|
// ── Build ECharts option per type ──────────────────────────────────
|
|
112
|
-
function buildOption(mtype, nameMap) {
|
|
184
|
+
function buildOption(mtype, nameMap, ink = DEFAULT_INK) {
|
|
113
185
|
const names = [...nameMap.keys()];
|
|
114
186
|
const baseLegend = {
|
|
115
187
|
data: names,
|
|
116
188
|
top: 6,
|
|
117
|
-
textStyle: { fontSize: 11 },
|
|
189
|
+
textStyle: { fontSize: 11, color: ink.muted },
|
|
118
190
|
};
|
|
119
191
|
const baseGrid = { left: 50, right: 20, top: 40, bottom: 30 };
|
|
120
192
|
const baseTooltip = {
|
|
121
193
|
trigger: 'axis',
|
|
122
|
-
axisPointer: {
|
|
194
|
+
axisPointer: {
|
|
195
|
+
type: 'cross',
|
|
196
|
+
label: { backgroundColor: ink.muted, color: ink.surface },
|
|
197
|
+
},
|
|
198
|
+
backgroundColor: ink.surface,
|
|
199
|
+
borderColor: ink.line,
|
|
200
|
+
textStyle: { color: ink.text },
|
|
123
201
|
};
|
|
124
202
|
const baseXAxis = {
|
|
125
203
|
type: 'time',
|
|
126
204
|
boundaryGap: false,
|
|
127
|
-
axisLine: { lineStyle: { color:
|
|
128
|
-
axisLabel: { fontSize: 10 },
|
|
205
|
+
axisLine: { lineStyle: { color: ink.line } },
|
|
206
|
+
axisLabel: { fontSize: 10, color: ink.muted },
|
|
129
207
|
};
|
|
130
208
|
const baseYAxis = {
|
|
131
209
|
type: 'value',
|
|
132
|
-
splitLine: { lineStyle: { type: 'dashed', color:
|
|
210
|
+
splitLine: { lineStyle: { type: 'dashed', color: ink.line } },
|
|
133
211
|
axisLine: { show: false },
|
|
134
|
-
axisLabel: { fontSize: 10 },
|
|
212
|
+
axisLabel: { fontSize: 10, color: ink.muted },
|
|
135
213
|
};
|
|
136
214
|
if (mtype === 'histogram' || mtype === 'exponentialHistogram') {
|
|
137
215
|
// ── Bar chart ──
|
|
@@ -257,29 +335,34 @@ function formatDuration(ms) {
|
|
|
257
335
|
}
|
|
258
336
|
// ── Component ───────────────────────────────────────────────────────
|
|
259
337
|
export const OtelMetricsChart = ({ metrics, height = 240, }) => {
|
|
338
|
+
const [probe, ink] = useChartInk();
|
|
260
339
|
if (!metrics || metrics.length === 0) {
|
|
261
|
-
return (_jsx(ReactECharts, { echarts: echarts, option: {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
340
|
+
return (_jsxs(Box, { position: "relative", children: [_jsx(InkProbe, { ref: probe }), _jsx(ReactECharts, { echarts: echarts, option: {
|
|
341
|
+
title: {
|
|
342
|
+
text: 'No metrics data',
|
|
343
|
+
left: 'center',
|
|
344
|
+
top: 'center',
|
|
345
|
+
textStyle: {
|
|
346
|
+
color: ink.muted,
|
|
347
|
+
fontSize: 14,
|
|
348
|
+
fontWeight: 'normal',
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
}, style: { width: '100%', height: `${height}px` }, notMerge: true })] }));
|
|
269
352
|
}
|
|
270
353
|
const byType = groupByType(metrics);
|
|
271
354
|
// Sort types in a stable, user-friendly order.
|
|
272
355
|
const sortedTypes = [...byType.keys()].sort((a, b) => (TYPE_ORDER[a] ?? 99) - (TYPE_ORDER[b] ?? 99));
|
|
273
|
-
return (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
356
|
+
return (_jsxs(Box, { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 2, position: "relative", children: [_jsx(InkProbe, { ref: probe }), sortedTypes.map(mtype => {
|
|
357
|
+
const nameMap = byType.get(mtype);
|
|
358
|
+
const option = buildOption(mtype, nameMap, ink);
|
|
359
|
+
const stats = computeStats(nameMap);
|
|
360
|
+
const durationMs = stats.startTs !== null && stats.endTs !== null
|
|
361
|
+
? stats.endTs - stats.startTs
|
|
362
|
+
: null;
|
|
363
|
+
return (_jsxs(Box, { children: [_jsxs(Box, { display: "flex", alignItems: "center", gap: 2, px: 1, mb: 1, children: [_jsx(Label, { size: "small", variant: typeVariant(mtype), children: mtype }), _jsx(Text, { sx: { fontSize: 1, fontWeight: 'bold', color: 'fg.muted' }, children: typeTitle(mtype) }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.subtle' }, children: ["(", [...nameMap.keys()].join(', '), ")"] })] }), _jsxs(Box, { display: "flex", alignItems: "center", gap: 3, px: 1, mb: 1, children: [_jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: [_jsx(Text, { as: "span", sx: { fontWeight: 'bold', color: 'fg.default' }, children: "Sum:" }), ' ', stats.total.toLocaleString(undefined, {
|
|
364
|
+
maximumFractionDigits: 4,
|
|
365
|
+
})] }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: [_jsx(Text, { as: "span", sx: { fontWeight: 'bold', color: 'fg.default' }, children: "Points:" }), ' ', stats.count] }), stats.startTs !== null && stats.endTs !== null && (_jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: [_jsx(Text, { as: "span", sx: { fontWeight: 'bold', color: 'fg.default' }, children: "Interval:" }), ' ', formatTs(stats.startTs), " \u2192 ", formatTs(stats.endTs), durationMs !== null && durationMs > 0 && (_jsxs(Text, { as: "span", sx: { color: 'fg.subtle' }, children: [' ', "(", formatDuration(durationMs), ")"] }))] }))] }), _jsx(ReactECharts, { echarts: echarts, option: option, style: { width: '100%', height: `${height}px` }, notMerge: true })] }, mtype));
|
|
366
|
+
})] }));
|
|
284
367
|
};
|
|
285
368
|
export default OtelMetricsChart;
|
|
@@ -12,7 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* @module otel/OtelSpanDetail
|
|
13
13
|
*/
|
|
14
14
|
import { useMemo, useState } from 'react';
|
|
15
|
-
import { Box } from '@datalayer/primer-addons';
|
|
15
|
+
import { Box, Card } from '@datalayer/primer-addons';
|
|
16
16
|
import { Text, IconButton, UnderlineNav, CounterLabel, Label, } from '@primer/react';
|
|
17
17
|
import { XIcon, ChevronDownIcon, ChevronRightIcon, } from '@primer/octicons-react';
|
|
18
18
|
import { formatDuration, buildSpanTree } from '../utils.js';
|
|
@@ -39,7 +39,7 @@ const CollapsibleSection = ({ title, data, defaultOpen = false }) => {
|
|
|
39
39
|
const [open, setOpen] = useState(defaultOpen);
|
|
40
40
|
if (!data || Object.keys(data).length === 0)
|
|
41
41
|
return null;
|
|
42
|
-
return (_jsxs(Box, { mt: 3, children: [_jsxs(Box, { display: "flex", alignItems: "center", gap: 1, cursor: "pointer", py: 1, userSelect: "none", onClick: () => setOpen(!open), children: [open ? _jsx(ChevronDownIcon, { size: 16 }) : _jsx(ChevronRightIcon, { size: 16 }), _jsx(Text, { sx: { fontSize: 1, fontWeight: 'bold' }, children: title }), _jsx(CounterLabel, { children: Object.keys(data).length })] }), open && (_jsx(
|
|
42
|
+
return (_jsxs(Box, { mt: 3, children: [_jsxs(Box, { display: "flex", alignItems: "center", gap: 1, cursor: "pointer", py: 1, userSelect: "none", onClick: () => setOpen(!open), children: [open ? _jsx(ChevronDownIcon, { size: 16 }) : _jsx(ChevronRightIcon, { size: 16 }), _jsx(Text, { sx: { fontSize: 1, fontWeight: 'bold' }, children: title }), _jsx(CounterLabel, { children: Object.keys(data).length })] }), open && (_jsx(Card, { border: true, shadow: "none", variant: "subtle", display: "block", p: 2, mt: 1, overflowX: "auto", children: Object.entries(data).map(([key, val]) => (_jsx(AttributeRow, { attrKey: key, value: val, depth: 0 }, key))) }))] }));
|
|
43
43
|
};
|
|
44
44
|
/**
|
|
45
45
|
* A value as the detail unfolds it: JSON text of an object or an array (the
|
|
@@ -107,7 +107,7 @@ export const OtelSpanDetail = ({ span, traceSpans, onClose, }) => {
|
|
|
107
107
|
}
|
|
108
108
|
// Build tree from traceSpans for tree tab
|
|
109
109
|
const tree = traceSpans && traceSpans.length > 0 ? buildSpanTree(traceSpans) : undefined;
|
|
110
|
-
return (_jsxs(Box, { height: "100%", overflow: "auto", borderLeft: "1px solid", borderColor: "border.default", bg: "canvas.default", display: "flex", flexDirection: "column", children: [_jsxs(Box, { px: 3, py: 2, borderBottom: "1px solid", borderColor: "border.default", display: "flex", justifyContent: "space-between", alignItems: "center", bg: "canvas.subtle", flexShrink: 0, children: [_jsx(Text, { sx: { fontSize: 2, fontWeight: 'bold' }, children: span.span_name }), onClose && (_jsx(IconButton, { icon: XIcon, "aria-label": "Close detail panel", variant: "invisible", size: "small", onClick: onClose }))] }), _jsxs(UnderlineNav, { "aria-label": "Span detail tabs", children: [_jsx(UnderlineNav.Item, { "aria-current": activeTab === 'details' ? 'page' : undefined, onClick: () => setActiveTab('details'), children: "Details" }), tree && (_jsx(UnderlineNav.Item, { "aria-current": activeTab === 'tree' ? 'page' : undefined, onClick: () => setActiveTab('tree'), children: "Trace Tree" })), _jsx(UnderlineNav.Item, { "aria-current": activeTab === 'raw' ? 'page' : undefined, onClick: () => setActiveTab('raw'), children: "Raw Data" })] }), _jsxs(Box, { flex: 1, overflow: "auto", children: [activeTab === 'details' && (_jsxs(Box, { p: 3, children: [_jsx(MetadataRow, { label: "span_name", value: span.span_name }), _jsx(MetadataRow, { label: "service_name", value: span.service_name }), _jsx(MetadataRow, { label: "otel_scope_name", value: span.otel_scope_name }), _jsx(MetadataRow, { label: "kind", value: span.kind }), _jsx(MetadataRow, { label: "trace_id", value: span.trace_id, mono: true }), _jsx(MetadataRow, { label: "span_id", value: span.span_id, mono: true }), _jsx(MetadataRow, { label: "parent_span_id", value: span.parent_span_id, mono: true }), _jsx(MetadataRow, { label: "duration", value: formatDuration(span.duration_ms) }), _jsx(MetadataRow, { label: "start_time", value: span.start_time, mono: true }), _jsx(MetadataRow, { label: "end_time", value: span.end_time, mono: true }), _jsx(MetadataRow, { label: "status_code", value: span.status_code }), _jsx(MetadataRow, { label: "status_message", value: span.status_message }), _jsx(CollapsibleSection, { title: "Arguments", data: genAiAttrs, defaultOpen: Object.keys(genAiAttrs).length > 0 }), _jsx(CollapsibleSection, { title: "Attributes", data: otherAttrs }), span.events && span.events.length > 0 && (_jsxs(Box, { mt: 3, children: [_jsxs(Box, { display: "flex", alignItems: "center", gap: 1, children: [_jsx(Text, { sx: { fontWeight: 'bold', fontSize: 1 }, children: "Events" }), _jsx(CounterLabel, { children: span.events.length })] }), span.events.map((ev, idx) => (_jsxs(
|
|
110
|
+
return (_jsxs(Box, { height: "100%", overflow: "auto", borderLeft: "1px solid", borderColor: "border.default", bg: "canvas.default", display: "flex", flexDirection: "column", children: [_jsxs(Box, { px: 3, py: 2, borderBottom: "1px solid", borderColor: "border.default", display: "flex", justifyContent: "space-between", alignItems: "center", bg: "canvas.subtle", flexShrink: 0, children: [_jsx(Text, { sx: { fontSize: 2, fontWeight: 'bold' }, children: span.span_name }), onClose && (_jsx(IconButton, { icon: XIcon, "aria-label": "Close detail panel", variant: "invisible", size: "small", onClick: onClose }))] }), _jsxs(UnderlineNav, { "aria-label": "Span detail tabs", children: [_jsx(UnderlineNav.Item, { "aria-current": activeTab === 'details' ? 'page' : undefined, onClick: () => setActiveTab('details'), children: "Details" }), tree && (_jsx(UnderlineNav.Item, { "aria-current": activeTab === 'tree' ? 'page' : undefined, onClick: () => setActiveTab('tree'), children: "Trace Tree" })), _jsx(UnderlineNav.Item, { "aria-current": activeTab === 'raw' ? 'page' : undefined, onClick: () => setActiveTab('raw'), children: "Raw Data" })] }), _jsxs(Box, { flex: 1, overflow: "auto", children: [activeTab === 'details' && (_jsxs(Box, { p: 3, children: [_jsx(MetadataRow, { label: "span_name", value: span.span_name }), _jsx(MetadataRow, { label: "service_name", value: span.service_name }), _jsx(MetadataRow, { label: "otel_scope_name", value: span.otel_scope_name }), _jsx(MetadataRow, { label: "kind", value: span.kind }), _jsx(MetadataRow, { label: "trace_id", value: span.trace_id, mono: true }), _jsx(MetadataRow, { label: "span_id", value: span.span_id, mono: true }), _jsx(MetadataRow, { label: "parent_span_id", value: span.parent_span_id, mono: true }), _jsx(MetadataRow, { label: "duration", value: formatDuration(span.duration_ms) }), _jsx(MetadataRow, { label: "start_time", value: span.start_time, mono: true }), _jsx(MetadataRow, { label: "end_time", value: span.end_time, mono: true }), _jsx(MetadataRow, { label: "status_code", value: span.status_code }), _jsx(MetadataRow, { label: "status_message", value: span.status_message }), _jsx(CollapsibleSection, { title: "Arguments", data: genAiAttrs, defaultOpen: Object.keys(genAiAttrs).length > 0 }), _jsx(CollapsibleSection, { title: "Attributes", data: otherAttrs }), span.events && span.events.length > 0 && (_jsxs(Box, { mt: 3, children: [_jsxs(Box, { display: "flex", alignItems: "center", gap: 1, children: [_jsx(Text, { sx: { fontWeight: 'bold', fontSize: 1 }, children: "Events" }), _jsx(CounterLabel, { children: span.events.length })] }), span.events.map((ev, idx) => (_jsxs(Card, { border: true, shadow: "none", variant: "subtle", display: "block", p: 2, mt: 2, children: [_jsxs(Box, { display: "flex", gap: 2, children: [_jsx(Label, { children: ev.name }), _jsx(Text, { sx: {
|
|
111
111
|
fontSize: 0,
|
|
112
112
|
color: 'fg.muted',
|
|
113
113
|
fontFamily: 'mono',
|