@datalayer/core 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/api/constants.d.ts +3 -0
- package/lib/api/constants.js +3 -0
- package/lib/api/index.d.ts +1 -0
- package/lib/api/index.js +1 -0
- package/lib/api/otel/index.d.ts +12 -0
- package/lib/api/otel/index.js +16 -0
- package/lib/api/otel/logs.d.ts +19 -0
- package/lib/api/otel/logs.js +43 -0
- package/lib/api/otel/metrics.d.ts +31 -0
- package/lib/api/otel/metrics.js +65 -0
- package/lib/api/otel/query.d.ts +16 -0
- package/lib/api/otel/query.js +37 -0
- package/lib/api/otel/services.d.ts +39 -0
- package/lib/api/otel/services.js +81 -0
- package/lib/api/otel/traces.d.ts +24 -0
- package/lib/api/otel/traces.js +53 -0
- package/lib/api/otel/types.d.ts +112 -0
- package/lib/api/otel/types.js +5 -0
- package/lib/api/spacer/index.d.ts +1 -2
- package/lib/api/spacer/index.js +1 -2
- package/lib/components/avatars/BoringAvatar.d.ts +3 -1
- package/lib/components/avatars/BoringAvatar.js +15 -14
- package/lib/components/avatars/BoringAvatar.stories.d.ts +2 -1
- package/lib/components/storage/ContentsBrowser.d.ts +6 -0
- package/lib/components/storage/ContentsBrowser.js +7 -8
- package/lib/config/Configuration.d.ts +4 -0
- package/lib/hooks/index.d.ts +2 -0
- package/lib/hooks/index.js +2 -0
- package/lib/hooks/useCache.d.ts +16 -40
- package/lib/hooks/useCache.js +28 -233
- package/lib/hooks/useProjectStore.d.ts +58 -0
- package/lib/hooks/useProjectStore.js +64 -0
- package/lib/hooks/useProjects.d.ts +590 -0
- package/lib/hooks/useProjects.js +166 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +4 -2
- package/lib/models/Page.d.ts +2 -0
- package/lib/otel/OtelLive.d.ts +12 -0
- package/lib/otel/OtelLive.js +354 -0
- package/lib/otel/OtelLogsList.d.ts +11 -0
- package/lib/otel/OtelLogsList.js +137 -0
- package/lib/otel/OtelMetricsChart.d.ts +22 -0
- package/lib/otel/OtelMetricsChart.js +300 -0
- package/lib/otel/OtelMetricsList.d.ts +15 -0
- package/lib/otel/OtelMetricsList.js +213 -0
- package/lib/otel/OtelSearchBar.d.ts +11 -0
- package/lib/otel/OtelSearchBar.js +22 -0
- package/lib/otel/OtelSpanDetail.d.ts +11 -0
- package/lib/otel/OtelSpanDetail.js +172 -0
- package/lib/otel/OtelSpanTree.d.ts +11 -0
- package/lib/otel/OtelSpanTree.js +176 -0
- package/lib/otel/OtelSqlView.d.ts +16 -0
- package/lib/otel/OtelSqlView.js +239 -0
- package/lib/otel/OtelSystemView.d.ts +15 -0
- package/lib/otel/OtelSystemView.js +75 -0
- package/lib/otel/OtelTimeline.d.ts +11 -0
- package/lib/otel/OtelTimeline.js +101 -0
- package/lib/otel/OtelTimelineRangeSlider.d.ts +16 -0
- package/lib/otel/OtelTimelineRangeSlider.js +338 -0
- package/lib/otel/OtelTracesList.d.ts +13 -0
- package/lib/otel/OtelTracesList.js +199 -0
- package/lib/otel/hooks.d.ts +172 -0
- package/lib/otel/hooks.js +490 -0
- package/lib/otel/index.d.ts +25 -0
- package/lib/otel/index.js +19 -0
- package/lib/otel/types.d.ts +190 -0
- package/lib/otel/types.js +5 -0
- package/lib/otel/utils.d.ts +33 -0
- package/lib/otel/utils.js +181 -0
- package/lib/state/storage/IAMStorage.d.ts +2 -1
- package/lib/state/substates/CoreState.js +1 -0
- package/lib/utils/Jwt.d.ts +42 -0
- package/lib/utils/Jwt.js +44 -0
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +1 -0
- package/lib/views/iam/SignInSimple.d.ts +38 -0
- package/lib/views/iam/SignInSimple.js +80 -0
- package/lib/views/iam/index.d.ts +2 -0
- package/lib/views/iam/index.js +5 -0
- package/lib/views/iam-tokens/IAMTokenEdit.js +53 -4
- package/lib/views/iam-tokens/IAMTokens.js +65 -33
- package/lib/views/iam-tokens/Tokens.js +64 -32
- package/lib/views/index.d.ts +2 -1
- package/lib/views/index.js +2 -1
- package/lib/views/profile/UserBadge.d.ts +18 -0
- package/lib/views/profile/UserBadge.js +101 -0
- package/lib/views/profile/index.d.ts +2 -0
- package/lib/views/profile/index.js +5 -0
- package/lib/views/secrets/Secrets.js +1 -1
- package/package.json +27 -3
- package/lib/api/spacer/agentSpaces.d.ts +0 -193
- package/lib/api/spacer/agentSpaces.js +0 -127
- package/lib/theme/DatalayerTheme.d.ts +0 -52
- package/lib/theme/DatalayerTheme.js +0 -228
- package/lib/theme/DatalayerThemeProvider.d.ts +0 -29
- package/lib/theme/DatalayerThemeProvider.js +0 -54
- package/lib/theme/Palette.d.ts +0 -4
- package/lib/theme/Palette.js +0 -10
- package/lib/theme/index.d.ts +0 -4
- package/lib/theme/index.js +0 -8
- package/lib/theme/useSystemColorMode.d.ts +0 -9
- package/lib/theme/useSystemColorMode.js +0 -26
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* OtelMetricsList – Tabular view of metric data points grouped by
|
|
8
|
+
* metric name, with expandable rows showing individual data points.
|
|
9
|
+
*
|
|
10
|
+
* Uses Primer React components for consistent theming.
|
|
11
|
+
*
|
|
12
|
+
* @module otel/OtelMetricsList
|
|
13
|
+
*/
|
|
14
|
+
import { useMemo, useState } from 'react';
|
|
15
|
+
import { Box, Text, Label, Spinner, CounterLabel, SegmentedControl, } from '@primer/react';
|
|
16
|
+
import { Blankslate } from '@primer/react/experimental';
|
|
17
|
+
import { MeterIcon, ChevronDownIcon, ChevronRightIcon, GraphIcon, TableIcon, } from '@primer/octicons-react';
|
|
18
|
+
import { formatTime } from './utils';
|
|
19
|
+
import { OtelMetricsChart } from './OtelMetricsChart';
|
|
20
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
21
|
+
/** Group metrics by metric_name. */
|
|
22
|
+
function groupByName(metrics) {
|
|
23
|
+
const map = new Map();
|
|
24
|
+
for (const m of metrics) {
|
|
25
|
+
const key = m.metric_name || '(unnamed)';
|
|
26
|
+
if (!map.has(key))
|
|
27
|
+
map.set(key, []);
|
|
28
|
+
const group = map.get(key);
|
|
29
|
+
if (group)
|
|
30
|
+
group.push(m);
|
|
31
|
+
}
|
|
32
|
+
return map;
|
|
33
|
+
}
|
|
34
|
+
/** Format a metric value with optional unit. */
|
|
35
|
+
function formatValue(value, unit) {
|
|
36
|
+
const formatted = Number.isInteger(value) ? String(value) : value.toFixed(3);
|
|
37
|
+
return unit ? `${formatted} ${unit}` : formatted;
|
|
38
|
+
}
|
|
39
|
+
/** Map metric_type to a Primer Label variant. */
|
|
40
|
+
function typeVariant(type) {
|
|
41
|
+
switch (type?.toLowerCase()) {
|
|
42
|
+
case 'gauge':
|
|
43
|
+
return 'accent';
|
|
44
|
+
case 'counter':
|
|
45
|
+
case 'sum':
|
|
46
|
+
return 'attention';
|
|
47
|
+
case 'histogram':
|
|
48
|
+
return 'secondary';
|
|
49
|
+
case 'exponentialhistogram':
|
|
50
|
+
return 'success';
|
|
51
|
+
default:
|
|
52
|
+
return 'primary';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// ── MetricGroup ────────────────────────────────────────────────────
|
|
56
|
+
const MetricGroup = ({ name, points }) => {
|
|
57
|
+
const [expanded, setExpanded] = useState(false);
|
|
58
|
+
const first = points[0];
|
|
59
|
+
// Compute stats
|
|
60
|
+
const stats = useMemo(() => {
|
|
61
|
+
const values = points.map(p => p.value);
|
|
62
|
+
const min = Math.min(...values);
|
|
63
|
+
const max = Math.max(...values);
|
|
64
|
+
const avg = values.reduce((a, b) => a + b, 0) / values.length;
|
|
65
|
+
const latest = values[values.length - 1];
|
|
66
|
+
const timestamps = points
|
|
67
|
+
.map(p => new Date(p.timestamp).getTime())
|
|
68
|
+
.filter(t => !isNaN(t));
|
|
69
|
+
const startTs = timestamps.length > 0 ? Math.min(...timestamps) : null;
|
|
70
|
+
const endTs = timestamps.length > 0 ? Math.max(...timestamps) : null;
|
|
71
|
+
return { min, max, avg, latest, startTs, endTs };
|
|
72
|
+
}, [points]);
|
|
73
|
+
return (_jsxs(Box, { sx: {
|
|
74
|
+
borderBottom: '1px solid',
|
|
75
|
+
borderColor: 'border.default',
|
|
76
|
+
}, children: [_jsxs(Box, { onClick: () => setExpanded(!expanded), sx: {
|
|
77
|
+
display: 'grid',
|
|
78
|
+
gridTemplateColumns: '20px 1fr auto auto auto auto',
|
|
79
|
+
gap: 3,
|
|
80
|
+
alignItems: 'center',
|
|
81
|
+
px: 3,
|
|
82
|
+
py: 2,
|
|
83
|
+
cursor: 'pointer',
|
|
84
|
+
':hover': { bg: 'canvas.subtle' },
|
|
85
|
+
}, children: [expanded ? (_jsx(ChevronDownIcon, { size: 16 })) : (_jsx(ChevronRightIcon, { size: 16 })), _jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 0 }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Text, { sx: { fontWeight: 'bold', fontSize: 1 }, children: name }), first?.metric_type && (_jsx(Label, { size: "small", variant: typeVariant(first.metric_type), children: first.metric_type })), first?.unit && (_jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["(", first.unit, ")"] }))] }), stats.startTs !== null && stats.endTs !== null && (_jsxs(Text, { sx: { fontSize: 0, color: 'fg.subtle', fontFamily: 'mono' }, children: [formatTime(new Date(stats.startTs).toISOString()), stats.startTs !== stats.endTs && (_jsxs(_Fragment, { children: [" \u2192 ", formatTime(new Date(stats.endTs).toISOString())] }))] }))] }), _jsxs(Box, { sx: { textAlign: 'right' }, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "latest" }), _jsx(Text, { sx: { fontSize: 1, fontWeight: 'bold', fontFamily: 'mono', ml: 1 }, children: formatValue(stats.latest, first?.unit) })] }), _jsxs(Box, { sx: { textAlign: 'right' }, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "avg" }), _jsx(Text, { sx: { fontSize: 1, fontFamily: 'mono', ml: 1 }, children: stats.avg.toFixed(2) })] }), _jsxs(Box, { sx: { textAlign: 'right' }, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "min/max" }), _jsxs(Text, { sx: { fontSize: 1, fontFamily: 'mono', ml: 1 }, children: [stats.min.toFixed(1), "\u2013", stats.max.toFixed(1)] })] }), _jsx(CounterLabel, { children: points.length })] }), expanded && (_jsxs(Box, { sx: { bg: 'canvas.subtle' }, children: [_jsxs(Box, { sx: {
|
|
86
|
+
display: 'grid',
|
|
87
|
+
gridTemplateColumns: '20px 180px 1fr 140px 160px',
|
|
88
|
+
gap: 3,
|
|
89
|
+
px: 3,
|
|
90
|
+
py: 1,
|
|
91
|
+
borderTop: '1px solid',
|
|
92
|
+
borderColor: 'border.muted',
|
|
93
|
+
}, children: [_jsx(Box, {}), _jsx(Text, { sx: { fontSize: 0, fontWeight: 'bold', color: 'fg.muted' }, children: "Time" }), _jsx(Text, { sx: { fontSize: 0, fontWeight: 'bold', color: 'fg.muted' }, children: "Service" }), _jsx(Text, { sx: {
|
|
94
|
+
fontSize: 0,
|
|
95
|
+
fontWeight: 'bold',
|
|
96
|
+
color: 'fg.muted',
|
|
97
|
+
textAlign: 'right',
|
|
98
|
+
}, children: "Value" }), _jsx(Text, { sx: {
|
|
99
|
+
fontSize: 0,
|
|
100
|
+
fontWeight: 'bold',
|
|
101
|
+
color: 'fg.muted',
|
|
102
|
+
textAlign: 'right',
|
|
103
|
+
}, children: "Attributes" })] }), points.map((point, idx) => (_jsx(MetricRow, { metric: point }, `${point.timestamp}-${idx}`)))] }))] }));
|
|
104
|
+
};
|
|
105
|
+
// ── MetricRow ───────────────────────────────────────────────────────
|
|
106
|
+
const MetricRow = ({ metric }) => {
|
|
107
|
+
const [showAttrs, setShowAttrs] = useState(false);
|
|
108
|
+
const attrCount = metric.attributes
|
|
109
|
+
? Object.keys(metric.attributes).length
|
|
110
|
+
: 0;
|
|
111
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Box, { sx: {
|
|
112
|
+
display: 'grid',
|
|
113
|
+
gridTemplateColumns: '20px 180px 1fr 140px 160px',
|
|
114
|
+
gap: 3,
|
|
115
|
+
alignItems: 'center',
|
|
116
|
+
px: 3,
|
|
117
|
+
py: 1,
|
|
118
|
+
borderTop: '1px solid',
|
|
119
|
+
borderColor: 'border.muted',
|
|
120
|
+
':hover': { bg: 'canvas.inset' },
|
|
121
|
+
}, children: [_jsx(Box, {}), _jsx(Text, { sx: { fontSize: 0, fontFamily: 'mono', color: 'fg.muted' }, children: formatTime(metric.timestamp) }), _jsx(Text, { sx: { fontSize: 0 }, children: metric.service_name }), _jsx(Text, { sx: {
|
|
122
|
+
fontSize: 1,
|
|
123
|
+
fontFamily: 'mono',
|
|
124
|
+
fontWeight: 'bold',
|
|
125
|
+
textAlign: 'right',
|
|
126
|
+
}, children: formatValue(metric.value, metric.unit) }), _jsx(Box, { sx: { textAlign: 'right' }, children: attrCount > 0 ? (_jsxs(Label, { size: "small", variant: "secondary", onClick: (e) => {
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
setShowAttrs(!showAttrs);
|
|
129
|
+
}, sx: { cursor: 'pointer' }, children: [attrCount, " attr", attrCount !== 1 ? 's' : ''] })) : (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "\u2014" })) })] }), showAttrs && metric.attributes && (_jsx(Box, { sx: { px: 5, py: 2, bg: 'canvas.inset' }, children: _jsx(Box, { as: "pre", sx: {
|
|
130
|
+
m: 0,
|
|
131
|
+
fontSize: 0,
|
|
132
|
+
fontFamily: 'mono',
|
|
133
|
+
whiteSpace: 'pre-wrap',
|
|
134
|
+
wordBreak: 'break-word',
|
|
135
|
+
}, children: JSON.stringify(metric.attributes, null, 2) }) }))] }));
|
|
136
|
+
};
|
|
137
|
+
// ── OtelMetricsList ─────────────────────────────────────────────────
|
|
138
|
+
const COOKIE_KEY = 'otel_metrics_view';
|
|
139
|
+
function readViewCookie() {
|
|
140
|
+
try {
|
|
141
|
+
const match = document.cookie.match(/(?:^|;\s*)otel_metrics_view=([^;]+)/);
|
|
142
|
+
if (match && (match[1] === 'chart' || match[1] === 'table'))
|
|
143
|
+
return match[1];
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// ignore
|
|
147
|
+
}
|
|
148
|
+
return 'table';
|
|
149
|
+
}
|
|
150
|
+
function writeViewCookie(v) {
|
|
151
|
+
try {
|
|
152
|
+
document.cookie = `${COOKIE_KEY}=${v};path=/;max-age=31536000`;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// ignore
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export const OtelMetricsList = ({ metrics, loading = false, }) => {
|
|
159
|
+
const [view, setView] = useState(readViewCookie);
|
|
160
|
+
const handleViewChange = (v) => {
|
|
161
|
+
writeViewCookie(v);
|
|
162
|
+
setView(v);
|
|
163
|
+
};
|
|
164
|
+
const grouped = useMemo(() => groupByName(metrics), [metrics]);
|
|
165
|
+
if (loading) {
|
|
166
|
+
return (_jsx(Box, { sx: {
|
|
167
|
+
display: 'flex',
|
|
168
|
+
justifyContent: 'center',
|
|
169
|
+
alignItems: 'center',
|
|
170
|
+
p: 5,
|
|
171
|
+
}, children: _jsx(Spinner, { size: "medium" }) }));
|
|
172
|
+
}
|
|
173
|
+
if (metrics.length === 0) {
|
|
174
|
+
return (_jsxs(Blankslate, { children: [_jsx(Blankslate.Visual, { children: _jsx(MeterIcon, { size: 20 }) }), _jsx(Blankslate.Heading, { children: "No metrics yet" }), _jsx(Blankslate.Description, { children: "Generate some metrics or adjust the filters above." })] }));
|
|
175
|
+
}
|
|
176
|
+
return (_jsxs(Box, { sx: {
|
|
177
|
+
display: 'flex',
|
|
178
|
+
flexDirection: 'column',
|
|
179
|
+
flex: 1,
|
|
180
|
+
minHeight: 0,
|
|
181
|
+
overflow: 'hidden',
|
|
182
|
+
}, children: [_jsx(Box, { sx: {
|
|
183
|
+
flexShrink: 0,
|
|
184
|
+
display: 'flex',
|
|
185
|
+
alignItems: 'center',
|
|
186
|
+
justifyContent: 'flex-end',
|
|
187
|
+
px: 3,
|
|
188
|
+
py: 2,
|
|
189
|
+
bg: 'canvas.subtle',
|
|
190
|
+
borderBottom: '1px solid',
|
|
191
|
+
borderColor: 'border.default',
|
|
192
|
+
zIndex: 0,
|
|
193
|
+
}, children: _jsxs(SegmentedControl, { "aria-label": "Metrics view", size: "small", onChange: (idx) => handleViewChange(idx === 0 ? 'chart' : 'table'), children: [_jsx(SegmentedControl.IconButton, { icon: GraphIcon, "aria-label": "Chart", selected: view === 'chart' }), _jsx(SegmentedControl.IconButton, { icon: TableIcon, "aria-label": "Table", selected: view === 'table' })] }) }), _jsx(Box, { sx: { flex: 1, minHeight: 0, overflow: 'auto' }, children: view === 'chart' ? (_jsx(Box, { sx: { px: 3, py: 2 }, children: _jsx(OtelMetricsChart, { metrics: metrics, height: 280 }) })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { sx: {
|
|
194
|
+
display: 'grid',
|
|
195
|
+
gridTemplateColumns: '20px 1fr auto auto auto auto',
|
|
196
|
+
gap: 3,
|
|
197
|
+
px: 3,
|
|
198
|
+
py: 1,
|
|
199
|
+
bg: 'canvas.subtle',
|
|
200
|
+
borderBottom: '2px solid',
|
|
201
|
+
borderColor: 'border.default',
|
|
202
|
+
position: 'sticky',
|
|
203
|
+
top: 0,
|
|
204
|
+
zIndex: 1,
|
|
205
|
+
}, children: [_jsx(Box, {}), ['Metric Name', 'Latest', 'Average', 'Min/Max', 'Points'].map((h, i) => (_jsx(Text, { sx: {
|
|
206
|
+
fontSize: 0,
|
|
207
|
+
fontWeight: 'bold',
|
|
208
|
+
color: 'fg.muted',
|
|
209
|
+
textTransform: 'uppercase',
|
|
210
|
+
letterSpacing: '0.05em',
|
|
211
|
+
textAlign: i === 0 ? undefined : 'right',
|
|
212
|
+
}, children: h }, h)))] }), [...grouped.entries()].map(([name, points]) => (_jsx(MetricGroup, { name: name, points: points }, name)))] })) })] }));
|
|
213
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OtelSearchBar – Filter toolbar with signal-type tabs, service selector,
|
|
3
|
+
* query input, and refresh action.
|
|
4
|
+
*
|
|
5
|
+
* Uses Primer React components for consistent theming.
|
|
6
|
+
*
|
|
7
|
+
* @module otel/OtelSearchBar
|
|
8
|
+
*/
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import type { OtelSearchBarProps } from './types';
|
|
11
|
+
export declare const OtelSearchBar: React.FC<OtelSearchBarProps>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, SegmentedControl, ActionMenu, ActionList, TextInput, Button, } from '@primer/react';
|
|
3
|
+
import { SyncIcon, SearchIcon } from '@primer/octicons-react';
|
|
4
|
+
const SIGNALS = [
|
|
5
|
+
{ value: 'traces', label: 'Traces' },
|
|
6
|
+
{ value: 'logs', label: 'Logs' },
|
|
7
|
+
{ value: 'metrics', label: 'Metrics' },
|
|
8
|
+
];
|
|
9
|
+
export const OtelSearchBar = ({ signal, onSignalChange, services, selectedService, onServiceChange, query, onQueryChange, onRefresh, loading, }) => {
|
|
10
|
+
const safeServices = Array.isArray(services) ? services : [];
|
|
11
|
+
return (_jsxs(Box, { sx: {
|
|
12
|
+
display: 'flex',
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
gap: 2,
|
|
15
|
+
px: 3,
|
|
16
|
+
py: 2,
|
|
17
|
+
bg: 'canvas.subtle',
|
|
18
|
+
borderBottom: '1px solid',
|
|
19
|
+
borderColor: 'border.default',
|
|
20
|
+
flexWrap: 'wrap',
|
|
21
|
+
}, children: [_jsx(SegmentedControl, { "aria-label": "Signal type", size: "small", onChange: idx => onSignalChange(SIGNALS[idx].value), children: SIGNALS.map(s => (_jsx(SegmentedControl.Button, { selected: s.value === signal, "aria-label": s.label, children: s.label }, s.value))) }), _jsxs(ActionMenu, { children: [_jsx(ActionMenu.Button, { size: "small", variant: "invisible", children: selectedService || 'All services' }), _jsx(ActionMenu.Overlay, { width: "auto", children: _jsxs(ActionList, { selectionVariant: "single", children: [_jsx(ActionList.Item, { selected: selectedService === '', onSelect: () => onServiceChange(''), children: "All services" }), safeServices.length > 0 && _jsx(ActionList.Divider, {}), safeServices.map(svc => (_jsx(ActionList.Item, { selected: selectedService === svc, onSelect: () => onServiceChange(svc), children: svc }, svc)))] }) })] }), _jsx(Box, { sx: { flex: 1, minWidth: 180 }, children: _jsx(TextInput, { leadingVisual: SearchIcon, value: query, onChange: e => onQueryChange(e.target.value), placeholder: "Search spans, logs, metrics\u2026", size: "small", block: true, "aria-label": "Search telemetry" }) }), onRefresh && (_jsx(Button, { size: "small", leadingVisual: SyncIcon, onClick: onRefresh, disabled: loading, "aria-label": "Refresh", children: "Refresh" }))] }));
|
|
22
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OtelSpanDetail – Detail panel for a selected span, with metadata,
|
|
3
|
+
* collapsible attributes, gen_ai arguments, events, and links.
|
|
4
|
+
*
|
|
5
|
+
* Uses Primer React components for consistent theming.
|
|
6
|
+
*
|
|
7
|
+
* @module otel/OtelSpanDetail
|
|
8
|
+
*/
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import type { OtelSpanDetailProps } from './types';
|
|
11
|
+
export declare const OtelSpanDetail: React.FC<OtelSpanDetailProps>;
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
* OtelSpanDetail – Detail panel for a selected span, with metadata,
|
|
8
|
+
* collapsible attributes, gen_ai arguments, events, and links.
|
|
9
|
+
*
|
|
10
|
+
* Uses Primer React components for consistent theming.
|
|
11
|
+
*
|
|
12
|
+
* @module otel/OtelSpanDetail
|
|
13
|
+
*/
|
|
14
|
+
import { useState } from 'react';
|
|
15
|
+
import { Box, Text, IconButton, UnderlineNav, CounterLabel, Label, } from '@primer/react';
|
|
16
|
+
import { XIcon, ChevronDownIcon, ChevronRightIcon, } from '@primer/octicons-react';
|
|
17
|
+
import { formatDuration, buildSpanTree } from './utils';
|
|
18
|
+
import { OtelSpanTree } from './OtelSpanTree';
|
|
19
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
20
|
+
/** Single key–value metadata row. */
|
|
21
|
+
const MetadataRow = ({ label, value, mono = false }) => {
|
|
22
|
+
if (value === undefined || value === null || value === '')
|
|
23
|
+
return null;
|
|
24
|
+
return (_jsxs(Box, { sx: {
|
|
25
|
+
display: 'flex',
|
|
26
|
+
py: 1,
|
|
27
|
+
borderBottom: '1px solid',
|
|
28
|
+
borderColor: 'border.muted',
|
|
29
|
+
gap: 2,
|
|
30
|
+
}, children: [_jsx(Text, { sx: {
|
|
31
|
+
width: 140,
|
|
32
|
+
minWidth: 140,
|
|
33
|
+
color: 'fg.muted',
|
|
34
|
+
fontSize: 1,
|
|
35
|
+
fontWeight: 'bold',
|
|
36
|
+
}, children: label }), _jsx(Text, { sx: {
|
|
37
|
+
fontSize: 1,
|
|
38
|
+
fontFamily: mono ? 'mono' : 'normal',
|
|
39
|
+
wordBreak: 'break-all',
|
|
40
|
+
}, children: String(value) })] }));
|
|
41
|
+
};
|
|
42
|
+
/** Collapsible section for nested JSON/attribute data with tree rendering. */
|
|
43
|
+
const CollapsibleSection = ({ title, data, defaultOpen = false }) => {
|
|
44
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
45
|
+
if (!data || Object.keys(data).length === 0)
|
|
46
|
+
return null;
|
|
47
|
+
return (_jsxs(Box, { sx: { mt: 3 }, children: [_jsxs(Box, { sx: {
|
|
48
|
+
display: 'flex',
|
|
49
|
+
alignItems: 'center',
|
|
50
|
+
gap: 1,
|
|
51
|
+
cursor: 'pointer',
|
|
52
|
+
py: 1,
|
|
53
|
+
userSelect: 'none',
|
|
54
|
+
}, 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(Box, { sx: {
|
|
55
|
+
bg: 'canvas.subtle',
|
|
56
|
+
borderRadius: 2,
|
|
57
|
+
border: '1px solid',
|
|
58
|
+
borderColor: 'border.default',
|
|
59
|
+
p: 2,
|
|
60
|
+
mt: 1,
|
|
61
|
+
overflowX: 'auto',
|
|
62
|
+
}, children: Object.entries(data).map(([key, val]) => (_jsx(AttributeRow, { attrKey: key, value: val, depth: 0 }, key))) }))] }));
|
|
63
|
+
};
|
|
64
|
+
/** Recursive attribute row – supports nested objects and arrays. */
|
|
65
|
+
const AttributeRow = ({ attrKey, value, depth }) => {
|
|
66
|
+
const [open, setOpen] = useState(depth < 1);
|
|
67
|
+
const isObject = typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
68
|
+
const isArray = Array.isArray(value);
|
|
69
|
+
const isNested = isObject || isArray;
|
|
70
|
+
return (_jsxs(Box, { sx: {
|
|
71
|
+
borderBottom: depth === 0 ? '1px solid' : 'none',
|
|
72
|
+
borderColor: 'border.muted',
|
|
73
|
+
pb: depth === 0 ? 1 : 0,
|
|
74
|
+
mb: depth === 0 ? 1 : 0,
|
|
75
|
+
}, children: [_jsxs(Box, { sx: {
|
|
76
|
+
display: 'flex',
|
|
77
|
+
gap: 2,
|
|
78
|
+
py: 1,
|
|
79
|
+
pl: depth * 16 + 'px',
|
|
80
|
+
alignItems: 'flex-start',
|
|
81
|
+
}, children: [isNested ? (_jsx(Box, { sx: {
|
|
82
|
+
cursor: 'pointer',
|
|
83
|
+
color: 'fg.muted',
|
|
84
|
+
userSelect: 'none',
|
|
85
|
+
width: 16,
|
|
86
|
+
flexShrink: 0,
|
|
87
|
+
}, onClick: () => setOpen(!open), children: open ? (_jsx(ChevronDownIcon, { size: 12 })) : (_jsx(ChevronRightIcon, { size: 12 })) })) : (_jsx(Box, { sx: { width: 16, flexShrink: 0 } })), _jsx(Text, { sx: {
|
|
88
|
+
color: 'accent.fg',
|
|
89
|
+
fontSize: 1,
|
|
90
|
+
fontFamily: 'mono',
|
|
91
|
+
minWidth: 180,
|
|
92
|
+
wordBreak: 'break-all',
|
|
93
|
+
flexShrink: 0,
|
|
94
|
+
}, children: attrKey }), !isNested && (_jsx(Text, { sx: {
|
|
95
|
+
fontSize: 1,
|
|
96
|
+
fontFamily: 'mono',
|
|
97
|
+
whiteSpace: 'pre-wrap',
|
|
98
|
+
wordBreak: 'break-word',
|
|
99
|
+
color: typeof value === 'string' ? 'accent.emphasis' : 'attention.fg',
|
|
100
|
+
}, children: typeof value === 'string' ? value : JSON.stringify(value) })), isNested && !open && (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', fontFamily: 'mono' }, children: isArray
|
|
101
|
+
? `[${value.length} items]`
|
|
102
|
+
: `{${Object.keys(value).length} keys}` }))] }), isNested && open && (_jsx(Box, { children: isArray
|
|
103
|
+
? value.map((item, idx) => (_jsx(AttributeRow, { attrKey: `[${idx}]`, value: item, depth: depth + 1 }, idx)))
|
|
104
|
+
: Object.entries(value).map(([k, v]) => (_jsx(AttributeRow, { attrKey: k, value: v, depth: depth + 1 }, k))) }))] }));
|
|
105
|
+
};
|
|
106
|
+
// ── Main component ──────────────────────────────────────────────────
|
|
107
|
+
export const OtelSpanDetail = ({ span, traceSpans, onClose, }) => {
|
|
108
|
+
const [activeTab, setActiveTab] = useState('details');
|
|
109
|
+
if (!span) {
|
|
110
|
+
return (_jsx(Box, { sx: { p: 5, color: 'fg.muted', textAlign: 'center' }, children: _jsx(Text, { children: "Select a span to view details." }) }));
|
|
111
|
+
}
|
|
112
|
+
// Separate gen_ai attributes from other attributes
|
|
113
|
+
const genAiAttrs = {};
|
|
114
|
+
const otherAttrs = {};
|
|
115
|
+
if (span.attributes) {
|
|
116
|
+
for (const [k, v] of Object.entries(span.attributes)) {
|
|
117
|
+
if (k.startsWith('gen_ai.') || k.startsWith('model_request')) {
|
|
118
|
+
genAiAttrs[k] = v;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
otherAttrs[k] = v;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Build tree from traceSpans for tree tab
|
|
126
|
+
const tree = traceSpans && traceSpans.length > 0 ? buildSpanTree(traceSpans) : undefined;
|
|
127
|
+
return (_jsxs(Box, { sx: {
|
|
128
|
+
height: '100%',
|
|
129
|
+
overflow: 'auto',
|
|
130
|
+
borderLeft: '1px solid',
|
|
131
|
+
borderColor: 'border.default',
|
|
132
|
+
bg: 'canvas.default',
|
|
133
|
+
display: 'flex',
|
|
134
|
+
flexDirection: 'column',
|
|
135
|
+
}, children: [_jsxs(Box, { sx: {
|
|
136
|
+
px: 3,
|
|
137
|
+
py: 2,
|
|
138
|
+
borderBottom: '1px solid',
|
|
139
|
+
borderColor: 'border.default',
|
|
140
|
+
display: 'flex',
|
|
141
|
+
justifyContent: 'space-between',
|
|
142
|
+
alignItems: 'center',
|
|
143
|
+
bg: 'canvas.subtle',
|
|
144
|
+
flexShrink: 0,
|
|
145
|
+
}, 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, { sx: { flex: 1, overflow: 'auto' }, children: [activeTab === 'details' && (_jsxs(Box, { sx: { 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, { sx: { mt: 3 }, children: [_jsxs(Box, { sx: { 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(Box, { sx: {
|
|
146
|
+
bg: 'canvas.subtle',
|
|
147
|
+
borderRadius: 2,
|
|
148
|
+
border: '1px solid',
|
|
149
|
+
borderColor: 'border.default',
|
|
150
|
+
p: 2,
|
|
151
|
+
mt: 2,
|
|
152
|
+
}, children: [_jsxs(Box, { sx: { display: 'flex', gap: 2 }, children: [_jsx(Label, { children: ev.name }), _jsx(Text, { sx: {
|
|
153
|
+
fontSize: 0,
|
|
154
|
+
color: 'fg.muted',
|
|
155
|
+
fontFamily: 'mono',
|
|
156
|
+
}, children: ev.timestamp })] }), ev.attributes && Object.keys(ev.attributes).length > 0 && (_jsx(Box, { sx: { mt: 1 }, children: Object.entries(ev.attributes).map(([k, v]) => (_jsx(AttributeRow, { attrKey: k, value: v, depth: 0 }, k))) }))] }, idx)))] })), span.links && span.links.length > 0 && (_jsxs(Box, { sx: { mt: 3 }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Text, { sx: { fontWeight: 'bold', fontSize: 1 }, children: "Links" }), _jsx(CounterLabel, { children: span.links.length })] }), span.links.map((link, idx) => (_jsxs(Text, { sx: {
|
|
157
|
+
display: 'block',
|
|
158
|
+
fontSize: 1,
|
|
159
|
+
fontFamily: 'mono',
|
|
160
|
+
py: 1,
|
|
161
|
+
borderBottom: '1px solid',
|
|
162
|
+
borderColor: 'border.muted',
|
|
163
|
+
}, children: ["trace: ", link.trace_id, " \u2192 span: ", link.span_id] }, idx)))] }))] })), activeTab === 'tree' && tree && (_jsx(OtelSpanTree, { spans: tree, selectedSpanId: span.span_id, defaultExpandDepth: 4 })), activeTab === 'raw' && (_jsx(Box, { as: "pre", sx: {
|
|
164
|
+
p: 3,
|
|
165
|
+
fontSize: 0,
|
|
166
|
+
fontFamily: 'mono',
|
|
167
|
+
whiteSpace: 'pre-wrap',
|
|
168
|
+
wordBreak: 'break-word',
|
|
169
|
+
m: 0,
|
|
170
|
+
bg: 'canvas.subtle',
|
|
171
|
+
}, children: JSON.stringify(span, null, 2) }))] })] }));
|
|
172
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OtelSpanTree – Collapsible tree view of nested spans within a trace.
|
|
3
|
+
*
|
|
4
|
+
* Renders spans as an indented, expandable tree with duration bars.
|
|
5
|
+
* Uses Primer React components for consistent theming.
|
|
6
|
+
*
|
|
7
|
+
* @module otel/OtelSpanTree
|
|
8
|
+
*/
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import type { OtelSpanTreeProps } from './types';
|
|
11
|
+
export declare const OtelSpanTree: React.FC<OtelSpanTreeProps>;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* OtelSpanTree – Collapsible tree view of nested spans within a trace.
|
|
8
|
+
*
|
|
9
|
+
* Renders spans as an indented, expandable tree with duration bars.
|
|
10
|
+
* Uses Primer React components for consistent theming.
|
|
11
|
+
*
|
|
12
|
+
* @module otel/OtelSpanTree
|
|
13
|
+
*/
|
|
14
|
+
import { useState, useMemo, useCallback } from 'react';
|
|
15
|
+
import { Box, Text } from '@primer/react';
|
|
16
|
+
import { ChevronDownIcon, ChevronRightIcon } from '@primer/octicons-react';
|
|
17
|
+
import { formatDuration, serviceColor, toMs } from './utils';
|
|
18
|
+
const INDENT_PX = 20;
|
|
19
|
+
const SpanNode = ({ span, depth, selectedSpanId, onSelectSpan, expandedIds, toggleExpanded, traceMinTime, traceDuration, }) => {
|
|
20
|
+
const hasChildren = (span.children?.length ?? 0) > 0;
|
|
21
|
+
const isExpanded = expandedIds.has(span.span_id);
|
|
22
|
+
const isSelected = selectedSpanId === span.span_id;
|
|
23
|
+
const color = serviceColor(span.service_name);
|
|
24
|
+
const startPct = ((toMs(span.start_time) - traceMinTime) / traceDuration) * 100;
|
|
25
|
+
const widthPct = Math.max(((span.duration_ms || 0) / traceDuration) * 100, 0.5);
|
|
26
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Box, { sx: {
|
|
27
|
+
display: 'flex',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
height: 28,
|
|
30
|
+
pl: `${depth * INDENT_PX}px`,
|
|
31
|
+
pr: 2,
|
|
32
|
+
cursor: 'pointer',
|
|
33
|
+
bg: isSelected ? 'accent.subtle' : 'canvas.default',
|
|
34
|
+
borderBottom: '1px solid',
|
|
35
|
+
borderColor: 'border.muted',
|
|
36
|
+
':hover': {
|
|
37
|
+
bg: isSelected ? 'accent.subtle' : 'canvas.subtle',
|
|
38
|
+
},
|
|
39
|
+
}, onClick: () => onSelectSpan?.(span), children: [_jsx(Box, { sx: {
|
|
40
|
+
width: 20,
|
|
41
|
+
textAlign: 'center',
|
|
42
|
+
color: 'fg.muted',
|
|
43
|
+
userSelect: 'none',
|
|
44
|
+
flexShrink: 0,
|
|
45
|
+
cursor: hasChildren ? 'pointer' : 'default',
|
|
46
|
+
}, onClick: e => {
|
|
47
|
+
e.stopPropagation();
|
|
48
|
+
if (hasChildren)
|
|
49
|
+
toggleExpanded(span.span_id);
|
|
50
|
+
}, children: hasChildren ? (isExpanded ? (_jsx(ChevronDownIcon, { size: 12 })) : (_jsx(ChevronRightIcon, { size: 12 }))) : (_jsx(Text, { sx: { color: 'fg.subtle', fontSize: 0 }, children: "\u00B7" })) }), _jsx(Box, { sx: {
|
|
51
|
+
width: 8,
|
|
52
|
+
height: 8,
|
|
53
|
+
borderRadius: '50%',
|
|
54
|
+
bg: color,
|
|
55
|
+
flexShrink: 0,
|
|
56
|
+
mr: 1,
|
|
57
|
+
} }), _jsx(Text, { sx: {
|
|
58
|
+
flex: '0 0 200px',
|
|
59
|
+
overflow: 'hidden',
|
|
60
|
+
textOverflow: 'ellipsis',
|
|
61
|
+
whiteSpace: 'nowrap',
|
|
62
|
+
fontSize: 1,
|
|
63
|
+
fontWeight: isSelected ? 'bold' : 'normal',
|
|
64
|
+
}, title: `${span.service_name} / ${span.span_name}`, children: span.span_name }), _jsx(Box, { sx: {
|
|
65
|
+
flex: 1,
|
|
66
|
+
position: 'relative',
|
|
67
|
+
height: 12,
|
|
68
|
+
bg: 'canvas.subtle',
|
|
69
|
+
borderRadius: 1,
|
|
70
|
+
mx: 2,
|
|
71
|
+
overflow: 'hidden',
|
|
72
|
+
}, children: _jsx(Box, { sx: {
|
|
73
|
+
position: 'absolute',
|
|
74
|
+
left: `${startPct}%`,
|
|
75
|
+
width: `${widthPct}%`,
|
|
76
|
+
height: '100%',
|
|
77
|
+
bg: color,
|
|
78
|
+
borderRadius: 1,
|
|
79
|
+
opacity: isSelected ? 1 : 0.7,
|
|
80
|
+
} }) }), _jsx(Text, { sx: {
|
|
81
|
+
minWidth: 60,
|
|
82
|
+
textAlign: 'right',
|
|
83
|
+
fontSize: 0,
|
|
84
|
+
fontFamily: 'mono',
|
|
85
|
+
color: 'fg.muted',
|
|
86
|
+
flexShrink: 0,
|
|
87
|
+
}, children: formatDuration(span.duration_ms) })] }), hasChildren &&
|
|
88
|
+
isExpanded &&
|
|
89
|
+
span.children.map(child => (_jsx(SpanNode, { span: child, depth: depth + 1, selectedSpanId: selectedSpanId, onSelectSpan: onSelectSpan, expandedIds: expandedIds, toggleExpanded: toggleExpanded, traceMinTime: traceMinTime, traceDuration: traceDuration }, child.span_id)))] }));
|
|
90
|
+
};
|
|
91
|
+
export const OtelSpanTree = ({ spans, selectedSpanId, onSelectSpan, defaultExpandDepth = 2, }) => {
|
|
92
|
+
// Pre-compute which IDs should be expanded by default
|
|
93
|
+
const defaultExpanded = useMemo(() => {
|
|
94
|
+
const ids = new Set();
|
|
95
|
+
function walk(nodes, depth) {
|
|
96
|
+
for (const n of nodes) {
|
|
97
|
+
if (depth < defaultExpandDepth) {
|
|
98
|
+
ids.add(n.span_id);
|
|
99
|
+
}
|
|
100
|
+
if (n.children)
|
|
101
|
+
walk(n.children, depth + 1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
walk(spans, 0);
|
|
105
|
+
return ids;
|
|
106
|
+
}, [spans, defaultExpandDepth]);
|
|
107
|
+
const [expandedIds, setExpandedIds] = useState(defaultExpanded);
|
|
108
|
+
const toggleExpanded = useCallback((id) => {
|
|
109
|
+
setExpandedIds(prev => {
|
|
110
|
+
const next = new Set(prev);
|
|
111
|
+
if (next.has(id)) {
|
|
112
|
+
next.delete(id);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
next.add(id);
|
|
116
|
+
}
|
|
117
|
+
return next;
|
|
118
|
+
});
|
|
119
|
+
}, []);
|
|
120
|
+
// Compute trace time bounds
|
|
121
|
+
const { traceMinTime, traceDuration } = useMemo(() => {
|
|
122
|
+
let min = Infinity;
|
|
123
|
+
let max = -Infinity;
|
|
124
|
+
function walk(nodes) {
|
|
125
|
+
for (const s of nodes) {
|
|
126
|
+
const start = toMs(s.start_time);
|
|
127
|
+
const end = toMs(s.end_time);
|
|
128
|
+
if (start < min)
|
|
129
|
+
min = start;
|
|
130
|
+
if (end > max)
|
|
131
|
+
max = end;
|
|
132
|
+
if (s.children)
|
|
133
|
+
walk(s.children);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
walk(spans);
|
|
137
|
+
if (min === Infinity)
|
|
138
|
+
return { traceMinTime: 0, traceDuration: 1 };
|
|
139
|
+
return { traceMinTime: min, traceDuration: max - min || 1 };
|
|
140
|
+
}, [spans]);
|
|
141
|
+
if (spans.length === 0) {
|
|
142
|
+
return (_jsx(Box, { sx: { p: 3, color: 'fg.muted', textAlign: 'center' }, children: _jsx(Text, { children: "No span tree to display." }) }));
|
|
143
|
+
}
|
|
144
|
+
return (_jsxs(Box, { sx: { width: '100%', overflow: 'auto' }, children: [_jsxs(Box, { sx: {
|
|
145
|
+
display: 'flex',
|
|
146
|
+
alignItems: 'center',
|
|
147
|
+
height: 28,
|
|
148
|
+
px: 2,
|
|
149
|
+
borderBottom: '2px solid',
|
|
150
|
+
borderColor: 'border.default',
|
|
151
|
+
bg: 'canvas.subtle',
|
|
152
|
+
}, children: [_jsx(Box, { sx: { width: 20 } }), _jsx(Box, { sx: { width: 8, mr: 1 } }), _jsx(Text, { sx: {
|
|
153
|
+
flex: '0 0 200px',
|
|
154
|
+
fontSize: 0,
|
|
155
|
+
fontWeight: 'bold',
|
|
156
|
+
color: 'fg.muted',
|
|
157
|
+
textTransform: 'uppercase',
|
|
158
|
+
letterSpacing: '0.05em',
|
|
159
|
+
}, children: "Span" }), _jsx(Text, { sx: {
|
|
160
|
+
flex: 1,
|
|
161
|
+
mx: 2,
|
|
162
|
+
fontSize: 0,
|
|
163
|
+
fontWeight: 'bold',
|
|
164
|
+
color: 'fg.muted',
|
|
165
|
+
textTransform: 'uppercase',
|
|
166
|
+
letterSpacing: '0.05em',
|
|
167
|
+
}, children: "Timeline" }), _jsx(Text, { sx: {
|
|
168
|
+
minWidth: 60,
|
|
169
|
+
textAlign: 'right',
|
|
170
|
+
fontSize: 0,
|
|
171
|
+
fontWeight: 'bold',
|
|
172
|
+
color: 'fg.muted',
|
|
173
|
+
textTransform: 'uppercase',
|
|
174
|
+
letterSpacing: '0.05em',
|
|
175
|
+
}, children: "Duration" })] }), spans.map(root => (_jsx(SpanNode, { span: root, depth: 0, selectedSpanId: selectedSpanId, onSelectSpan: onSelectSpan, expandedIds: expandedIds, toggleExpanded: toggleExpanded, traceMinTime: traceMinTime, traceDuration: traceDuration }, root.span_id)))] }));
|
|
176
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OtelSqlView – Ad-hoc DataFusion SQL query panel for the OTEL service.
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Preset query dropdown (spans, logs, metrics)
|
|
6
|
+
* - Query history persisted to cookie `otel_sql_history` (max 10 entries)
|
|
7
|
+
* - ⌘/Ctrl + Enter to run
|
|
8
|
+
*
|
|
9
|
+
* Available tables: `spans`, `metrics`, `logs`.
|
|
10
|
+
*/
|
|
11
|
+
import React from 'react';
|
|
12
|
+
export interface OtelSqlViewProps {
|
|
13
|
+
token?: string;
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const OtelSqlView: React.FC<OtelSqlViewProps>;
|