@datalayer/core 1.0.2 → 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.
Files changed (76) hide show
  1. package/README.md +1 -1
  2. package/lib/api/constants.d.ts +3 -0
  3. package/lib/api/constants.js +3 -0
  4. package/lib/api/index.d.ts +1 -0
  5. package/lib/api/index.js +1 -0
  6. package/lib/api/otel/index.d.ts +12 -0
  7. package/lib/api/otel/index.js +16 -0
  8. package/lib/api/otel/logs.d.ts +19 -0
  9. package/lib/api/otel/logs.js +43 -0
  10. package/lib/api/otel/metrics.d.ts +31 -0
  11. package/lib/api/otel/metrics.js +65 -0
  12. package/lib/api/otel/query.d.ts +16 -0
  13. package/lib/api/otel/query.js +37 -0
  14. package/lib/api/otel/services.d.ts +39 -0
  15. package/lib/api/otel/services.js +81 -0
  16. package/lib/api/otel/traces.d.ts +24 -0
  17. package/lib/api/otel/traces.js +53 -0
  18. package/lib/api/otel/types.d.ts +112 -0
  19. package/lib/api/otel/types.js +5 -0
  20. package/lib/config/Configuration.d.ts +4 -0
  21. package/lib/hooks/useCache.d.ts +14 -9
  22. package/lib/hooks/useCache.js +28 -0
  23. package/lib/index.d.ts +2 -0
  24. package/lib/index.js +4 -0
  25. package/lib/otel/OtelLive.d.ts +12 -0
  26. package/lib/otel/OtelLive.js +354 -0
  27. package/lib/otel/OtelLogsList.d.ts +11 -0
  28. package/lib/otel/OtelLogsList.js +137 -0
  29. package/lib/otel/OtelMetricsChart.d.ts +22 -0
  30. package/lib/otel/OtelMetricsChart.js +300 -0
  31. package/lib/otel/OtelMetricsList.d.ts +15 -0
  32. package/lib/otel/OtelMetricsList.js +213 -0
  33. package/lib/otel/OtelSearchBar.d.ts +11 -0
  34. package/lib/otel/OtelSearchBar.js +22 -0
  35. package/lib/otel/OtelSpanDetail.d.ts +11 -0
  36. package/lib/otel/OtelSpanDetail.js +172 -0
  37. package/lib/otel/OtelSpanTree.d.ts +11 -0
  38. package/lib/otel/OtelSpanTree.js +176 -0
  39. package/lib/otel/OtelSqlView.d.ts +16 -0
  40. package/lib/otel/OtelSqlView.js +239 -0
  41. package/lib/otel/OtelSystemView.d.ts +15 -0
  42. package/lib/otel/OtelSystemView.js +75 -0
  43. package/lib/otel/OtelTimeline.d.ts +11 -0
  44. package/lib/otel/OtelTimeline.js +101 -0
  45. package/lib/otel/OtelTimelineRangeSlider.d.ts +16 -0
  46. package/lib/otel/OtelTimelineRangeSlider.js +338 -0
  47. package/lib/otel/OtelTracesList.d.ts +13 -0
  48. package/lib/otel/OtelTracesList.js +199 -0
  49. package/lib/otel/hooks.d.ts +172 -0
  50. package/lib/otel/hooks.js +490 -0
  51. package/lib/otel/index.d.ts +25 -0
  52. package/lib/otel/index.js +19 -0
  53. package/lib/otel/types.d.ts +190 -0
  54. package/lib/otel/types.js +5 -0
  55. package/lib/otel/utils.d.ts +33 -0
  56. package/lib/otel/utils.js +181 -0
  57. package/lib/state/storage/IAMStorage.d.ts +2 -1
  58. package/lib/state/substates/CoreState.js +1 -0
  59. package/lib/utils/Jwt.d.ts +42 -0
  60. package/lib/utils/Jwt.js +44 -0
  61. package/lib/utils/index.d.ts +1 -0
  62. package/lib/utils/index.js +1 -0
  63. package/lib/views/iam/SignInSimple.d.ts +38 -0
  64. package/lib/views/iam/SignInSimple.js +80 -0
  65. package/lib/views/iam/index.d.ts +2 -0
  66. package/lib/views/iam/index.js +5 -0
  67. package/lib/views/iam-tokens/IAMTokenEdit.js +53 -4
  68. package/lib/views/iam-tokens/IAMTokens.js +65 -33
  69. package/lib/views/iam-tokens/Tokens.js +63 -31
  70. package/lib/views/index.d.ts +2 -1
  71. package/lib/views/index.js +2 -1
  72. package/lib/views/profile/UserBadge.d.ts +18 -0
  73. package/lib/views/profile/UserBadge.js +101 -0
  74. package/lib/views/profile/index.d.ts +2 -0
  75. package/lib/views/profile/index.js +5 -0
  76. package/package.json +27 -3
@@ -45,6 +45,18 @@ export type ISearchOpts = {
45
45
  max: number;
46
46
  public: boolean;
47
47
  };
48
+ /** Request payload for creating a new agent runtime. */
49
+ export type CreateAgentRuntimeRequest = {
50
+ environmentName?: string;
51
+ givenName?: string;
52
+ creditsLimit?: number;
53
+ type?: string;
54
+ /** 'none', 'notebook', or 'document' */
55
+ editorVariant?: string;
56
+ enableCodemode?: boolean;
57
+ /** ID of the agent spec used to create this runtime */
58
+ agentSpecId?: string;
59
+ };
48
60
  /**
49
61
  * Centralized query key factories for all entities
50
62
  * Following TanStack Query best practices for key structure
@@ -520,15 +532,7 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
520
532
  token?: string;
521
533
  agentSpec?: any;
522
534
  }[], Error>;
523
- useCreateAgentRuntime: () => import("@tanstack/react-query").UseMutationResult<any, Error, {
524
- environmentName?: string;
525
- givenName?: string;
526
- creditsLimit?: number;
527
- type?: string;
528
- editorVariant?: string;
529
- enableCodemode?: boolean;
530
- agentSpecId?: string;
531
- }, unknown>;
535
+ useCreateAgentRuntime: () => import("@tanstack/react-query").UseMutationResult<any, Error, CreateAgentRuntimeRequest, unknown>;
532
536
  useDeleteAgentRuntime: () => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
533
537
  useRefreshAgentRuntimes: () => () => void;
534
538
  useCourse: (courseId: string) => import("@tanstack/react-query").UseQueryResult<ICourse | undefined, Error>;
@@ -733,6 +737,7 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
733
737
  useTokens: () => import("@tanstack/react-query").UseQueryResult<any, Error>;
734
738
  useCreateToken: () => import("@tanstack/react-query").UseMutationResult<any, Error, Omit<IIAMToken, "id" | "value">, unknown>;
735
739
  useUpdateToken: () => import("@tanstack/react-query").UseMutationResult<any, Error, IIAMToken, unknown>;
740
+ useDeleteToken: () => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
736
741
  useInvite: (token: string) => import("@tanstack/react-query").UseQueryResult<import("..").IInvite | undefined, Error>;
737
742
  useInvitesByUser: (accountId: string) => import("@tanstack/react-query").UseQueryResult<any, Error>;
738
743
  usePutInvite: () => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
@@ -1473,6 +1473,14 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
1473
1473
  enabled: !!podName,
1474
1474
  });
1475
1475
  };
1476
+ /**
1477
+ * Create a new agent runtime.
1478
+ *
1479
+ * Note on phase/status mapping:
1480
+ * Newly created runtimes are immediately active (the operator assigns a pod from the pool).
1481
+ * The response won't have a 'phase' field, so we default to 'running'.
1482
+ * See useAgentRuntimes JSDoc for full explanation.
1483
+ */
1476
1484
  const useCreateAgentRuntime = () => {
1477
1485
  return useMutation({
1478
1486
  mutationFn: async (data) => {
@@ -2497,6 +2505,25 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2497
2505
  },
2498
2506
  });
2499
2507
  };
2508
+ /**
2509
+ * Delete token by ID
2510
+ */
2511
+ const useDeleteToken = () => {
2512
+ return useMutation({
2513
+ mutationFn: async (tokenId) => {
2514
+ return requestDatalayer({
2515
+ url: `${configuration.iamRunUrl}/api/iam/v1/tokens/${tokenId}`,
2516
+ method: 'DELETE',
2517
+ });
2518
+ },
2519
+ onSuccess: (_, tokenId) => {
2520
+ queryClient.removeQueries({
2521
+ queryKey: queryKeys.tokens.detail(tokenId),
2522
+ });
2523
+ queryClient.invalidateQueries({ queryKey: queryKeys.tokens.all() });
2524
+ },
2525
+ });
2526
+ };
2500
2527
  /**
2501
2528
  * Get contact by handle
2502
2529
  */
@@ -6434,6 +6461,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
6434
6461
  useTokens,
6435
6462
  useCreateToken,
6436
6463
  useUpdateToken,
6464
+ useDeleteToken,
6437
6465
  // Invites
6438
6466
  useInvite,
6439
6467
  useInvitesByUser,
package/lib/index.d.ts CHANGED
@@ -13,3 +13,5 @@ export * as iamApi from './api/iam';
13
13
  export * as spacerApi from './api/spacer';
14
14
  export { DatalayerClient, type DatalayerClientConfig, type ClientHandlers, User, Runtime, Environment, Snapshot, Space, Notebook, LexicalDTO, Secret, Credits, Item, type RuntimeJSON, type EnvironmentJSON, type UserJSON, type RuntimeData, type EnvironmentData, type UserData, type SpaceData, type SpaceItem, type NotebookData, type LexicalData, type RuntimeSnapshotData, type CreditsInfo, type CreditReservation, type CreateRuntimeRequest, type CreateRuntimeResponse, type ListRuntimesResponse, type ListEnvironmentsResponse, type CreateRuntimeSnapshotRequest, type CreateRuntimeSnapshotResponse, type GetRuntimeSnapshotResponse, type ListRuntimeSnapshotsResponse, type CreateSpaceRequest, type CreateSpaceResponse, type SpacesForUserResponse, type CollaborationSessionResponse, type DeleteSpaceItemResponse, type GetSpaceItemResponse, type GetSpaceItemsResponse, type CreateNotebookRequest, type CreateNotebookResponse, type GetNotebookResponse, type UpdateNotebookRequest, type UpdateNotebookResponse, type CreateLexicalRequest, type CreateLexicalResponse, type GetLexicalResponse, type UpdateLexicalRequest, type UpdateLexicalResponse, type CreditsResponse, type CreateDatasourceResponse, type GetDatasourceResponse, type ListDatasourcesResponse, type UpdateDatasourceResponse, type CreateSecretRequest, type CreateSecretResponse, type GetSecretResponse, type ListSecretsResponse, type UpdateSecretRequest, type UpdateSecretResponse, type SpaceJSON, type NotebookJSON, type LexicalJSON, type RuntimeSnapshotJSON, HealthCheck, type HealthCheckJSON, type LoginRequest, type LoginResponse, type UserMeResponse, type MembershipsResponse, type WhoAmIResponse, type HealthzPingResponse, AuthenticationManager, type IUser, type IBaseUser, type ICell, type IDatasource, type IDatasourceVariant, type ICredits, type ICreditsReservation, type ISpaceItem, type ISurvey, type ISpace, type IBaseSpace, type IAnySpace, type ISpaceVariant, type IBaseTeam, type IAnyTeam, type IOrganization, type IAnyOrganization, type IBaseOrganization, type IRuntimeModel, type IRuntimePod, type IRuntimeType, type IRuntimeLocation, type IRuntimeCapabilities, type IRuntimeSnapshot, type IDatalayerEnvironment, type IResources, type ISnippet, type IRole, type IAssignment, type IContact, type ICourse, type IOrganizationMember, type IPage, type PageTagName, type PageTheme, type PageVariant, type ISecret, type ISecretVariant, type SecretData, type SecretJSON, type DatasourceData, type DatasourceJSON, type DatasourceType, type CreateDatasourceRequest, type UpdateDatasourceRequest, Datasource, type IIAMToken, type IIAMTokenVariant, type IDocument, type IBaseDocument, type IEnvironment, type IExercise, type ICode, type IHelp, type IInvite, type ILesson, type INotebook, type IBaseNotebook, type ISchool, type ITeam, type TeamMember, type IUserOnboarding, type IClient, type IOnboardingPosition, type IOnboardingTours, type ITour, type ITourStatus, type IUserSettings, type IDataset, type IUsage, type IItem, type IItemType, type Member, type Profile, type SpaceMember, type IContactEvent, type IContactIAMProvider, type IStudentItem, type Instructor, type IStudent, type IDean, type IUserEvent, type IIAMProviderLinked, type IContent, type AuthResult, type TokenValidationResult, type AuthOptions, type TokenStorage, type IRuntimeOptions, type IMultiServiceManager, type IRemoteServicesManager, type IEnvironmentsManager, type IRemoteRuntimesManager, type NavigationLinkProps, type IDatalayerCoreConfig, type IRuntimesConfiguration, type IIAMProviderName, } from './client';
15
15
  export { getEnvironments, createRuntime, getRuntimes, deleteRuntime, snapshotRuntime, getRuntimeSnapshots, loadRuntimeSnapshot, } from './stateful/runtimes/actions';
16
+ export * from './otel';
17
+ export * from './views';
package/lib/index.js CHANGED
@@ -26,3 +26,7 @@ User, Runtime, Environment, Snapshot, Space, Notebook, LexicalDTO, Secret, Credi
26
26
  AuthenticationManager, Datasource, } from './client';
27
27
  // Export commonly used functions directly for convenience
28
28
  export { getEnvironments, createRuntime, getRuntimes, deleteRuntime, snapshotRuntime, getRuntimeSnapshots, loadRuntimeSnapshot, } from './stateful/runtimes/actions';
29
+ // OTEL observability components, hooks, and types
30
+ export * from './otel';
31
+ // Reusable views (sign-in pages, etc.)
32
+ export * from './views';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * OtelLive – Full-featured observability dashboard that combines the
3
+ * search bar, signal list, timeline, span tree, and detail panel into
4
+ * a Logfire-inspired experience.
5
+ *
6
+ * Uses Primer React components for consistent theming.
7
+ *
8
+ * @module otel/OtelLive
9
+ */
10
+ import React from 'react';
11
+ import type { OtelLiveProps } from './types';
12
+ export declare const OtelLive: React.FC<OtelLiveProps>;
@@ -0,0 +1,354 @@
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
+ * OtelLive – Full-featured observability dashboard that combines the
8
+ * search bar, signal list, timeline, span tree, and detail panel into
9
+ * a Logfire-inspired experience.
10
+ *
11
+ * Uses Primer React components for consistent theming.
12
+ *
13
+ * @module otel/OtelLive
14
+ */
15
+ import { useState, useCallback, useMemo, useEffect, useRef, } from 'react';
16
+ import { Box, Text, Button, Label } from '@primer/react';
17
+ import { GitBranchIcon, ClockIcon } from '@primer/octicons-react';
18
+ import { coreStore } from '../state/substates/CoreState';
19
+ import { useOtelTraces, useOtelTrace, useOtelLogs, useOtelMetrics, useOtelServices, useOtelWebSocket, } from './hooks';
20
+ import { OtelSearchBar } from './OtelSearchBar';
21
+ import { OtelTimelineRangeSlider } from './OtelTimelineRangeSlider';
22
+ import { OtelTracesList } from './OtelTracesList';
23
+ import { OtelLogsList } from './OtelLogsList';
24
+ import { OtelMetricsList } from './OtelMetricsList';
25
+ import { OtelSpanDetail } from './OtelSpanDetail';
26
+ import { OtelTimeline } from './OtelTimeline';
27
+ import { OtelSpanTree } from './OtelSpanTree';
28
+ import { buildSpanTree } from './utils';
29
+ // ── Helpers ─────────────────────────────────────────────────────────
30
+ const BOTTOM_PANE_VIEWS = ['timeline', 'tree'];
31
+ const HISTOGRAM_BUCKETS = 60;
32
+ /** Extract timestamp from any signal record. */
33
+ function signalTs(signal, item) {
34
+ if (signal === 'traces')
35
+ return new Date(item.start_time).getTime();
36
+ return new Date(item.timestamp).getTime();
37
+ }
38
+ /** Build histogram buckets from a list of raw timestamps. */
39
+ function buildHistogram(timestamps, start, end, buckets) {
40
+ const range = end - start || 1;
41
+ const step = range / buckets;
42
+ const counts = new Array(buckets).fill(0);
43
+ for (const ts of timestamps) {
44
+ const idx = Math.min(Math.floor((ts - start) / step), buckets - 1);
45
+ if (idx >= 0)
46
+ counts[idx]++;
47
+ }
48
+ return counts.map((count, i) => ({
49
+ time: new Date(start + i * step + step / 2),
50
+ count,
51
+ }));
52
+ }
53
+ // ── OtelLive ────────────────────────────────────────────────────────
54
+ export const OtelLive = ({ baseUrl = coreStore.getState().configuration.otelRunUrl, wsBaseUrl, token, autoRefreshMs = 5000, defaultSignal = 'traces', limit = 200, onSignalRef, }) => {
55
+ // ── state ──
56
+ const [signal, setSignalState] = useState(() => {
57
+ try {
58
+ const match = document.cookie.match(/(?:^|;\s*)otel_signal=([^;]+)/);
59
+ if (match && ['traces', 'logs', 'metrics'].includes(match[1])) {
60
+ return match[1];
61
+ }
62
+ }
63
+ catch {
64
+ // ignore
65
+ }
66
+ return defaultSignal;
67
+ });
68
+ const setSignal = (s) => {
69
+ try {
70
+ document.cookie = `otel_signal=${s};path=/;max-age=31536000`;
71
+ }
72
+ catch {
73
+ // ignore
74
+ }
75
+ setSignalState(s);
76
+ };
77
+ // Expose signal setter to parent so external controls (e.g. generate
78
+ // buttons) can navigate to the right tab.
79
+ useEffect(() => {
80
+ onSignalRef?.(setSignal);
81
+ }, [onSignalRef]);
82
+ const [service, setService] = useState('');
83
+ const [query, setQuery] = useState('');
84
+ const [selectedSpan, setSelectedSpan] = useState(null);
85
+ const [selectedLogIdx, setSelectedLogIdx] = useState(null);
86
+ const [bottomPane, setBottomPane] = useState(null);
87
+ const [rangeStart, setRangeStart] = useState(null);
88
+ const [rangeEnd, setRangeEnd] = useState(null);
89
+ // Refs so the bounds-change effect can read current range without making
90
+ // rangeStart/rangeEnd deps (which would cause infinite loops).
91
+ const rangeStartRef = useRef(null);
92
+ const rangeEndRef = useRef(null);
93
+ rangeStartRef.current = rangeStart;
94
+ rangeEndRef.current = rangeEnd;
95
+ const prevBoundsRef = useRef(null);
96
+ // ── data hooks ──
97
+ const { traces, loading: tracesLoading, refetch: refetchTraces, } = useOtelTraces({
98
+ baseUrl,
99
+ token,
100
+ limit,
101
+ serviceName: service || undefined,
102
+ autoRefreshMs,
103
+ });
104
+ const { logs, loading: logsLoading, refetch: refetchLogs, } = useOtelLogs({
105
+ baseUrl,
106
+ token,
107
+ limit,
108
+ serviceName: service || undefined,
109
+ autoRefreshMs,
110
+ });
111
+ const { metrics, loading: metricsLoading, refetch: refetchMetrics, } = useOtelMetrics({
112
+ baseUrl,
113
+ token,
114
+ limit,
115
+ serviceName: service || undefined,
116
+ autoRefreshMs,
117
+ });
118
+ const { services } = useOtelServices({ baseUrl, token });
119
+ // ── WebSocket live updates ──
120
+ // When a WS message arrives for a signal, refetch the corresponding hook
121
+ // so the data stays fresh without polling.
122
+ const wsCallbacks = useMemo(() => ({
123
+ onTraces: () => void refetchTraces(),
124
+ onLogs: () => void refetchLogs(),
125
+ onMetrics: () => void refetchMetrics(),
126
+ }), [refetchTraces, refetchLogs, refetchMetrics]);
127
+ const { connected: wsConnected } = useOtelWebSocket({
128
+ baseUrl: wsBaseUrl ?? baseUrl,
129
+ token,
130
+ callbacks: wsCallbacks,
131
+ });
132
+ // trace-detail fetch (when a span is selected)
133
+ const { spans: traceSpans } = useOtelTrace({
134
+ baseUrl,
135
+ token,
136
+ traceId: selectedSpan?.trace_id ?? '',
137
+ });
138
+ // Build tree from traceSpans for bottom pane
139
+ const spanTree = useMemo(() => traceSpans && traceSpans.length > 0 ? buildSpanTree(traceSpans) : [], [traceSpans]);
140
+ // ── Timeline range slider data ──
141
+ const allTimestamps = useMemo(() => {
142
+ const ts = [];
143
+ if (traces)
144
+ for (const s of traces)
145
+ ts.push(signalTs('traces', s));
146
+ if (logs)
147
+ for (const l of logs)
148
+ ts.push(signalTs('logs', l));
149
+ if (metrics)
150
+ for (const m of metrics)
151
+ ts.push(signalTs('metrics', m));
152
+ // Drop invalid timestamps (NaN) so the slider doesn't collapse
153
+ return ts.filter(t => Number.isFinite(t) && t > 0);
154
+ }, [traces, logs, metrics]);
155
+ const timelineBounds = useMemo(() => {
156
+ if (allTimestamps.length === 0)
157
+ return null;
158
+ const min = Math.min(...allTimestamps);
159
+ const max = Math.max(...allTimestamps);
160
+ // Add a small padding (2 %) so edge items aren't clipped
161
+ const pad = Math.max((max - min) * 0.02, 1000);
162
+ return { start: new Date(min - pad), end: new Date(max + pad) };
163
+ }, [allTimestamps]);
164
+ const histogram = useMemo(() => {
165
+ if (!timelineBounds || allTimestamps.length === 0)
166
+ return undefined;
167
+ return buildHistogram(allTimestamps, timelineBounds.start.getTime(), timelineBounds.end.getTime(), HISTOGRAM_BUCKETS);
168
+ }, [allTimestamps, timelineBounds]);
169
+ // Auto-adapt slider range when data bounds change.
170
+ // Behaviour:
171
+ // – First load (rangeStart is null): initialise to the full range.
172
+ // – Subsequent updates: if the user's range handle was at the previous
173
+ // bounds edge (within 600 ms), advance it to the new edge so the view
174
+ // "follows" live data. If the handle was moved inward, leave it alone.
175
+ useEffect(() => {
176
+ if (!timelineBounds)
177
+ return;
178
+ const prev = prevBoundsRef.current;
179
+ prevBoundsRef.current = {
180
+ start: timelineBounds.start.getTime(),
181
+ end: timelineBounds.end.getTime(),
182
+ };
183
+ // First load
184
+ if (!prev ||
185
+ rangeStartRef.current === null ||
186
+ rangeEndRef.current === null) {
187
+ setRangeStart(timelineBounds.start);
188
+ setRangeEnd(timelineBounds.end);
189
+ return;
190
+ }
191
+ const STICKY_THRESHOLD = 600; // ms – how close to the edge counts as "at edge"
192
+ if (rangeEndRef.current.getTime() >= prev.end - STICKY_THRESHOLD) {
193
+ setRangeEnd(timelineBounds.end);
194
+ }
195
+ if (rangeStartRef.current.getTime() <= prev.start + STICKY_THRESHOLD) {
196
+ setRangeStart(timelineBounds.start);
197
+ }
198
+ }, [timelineBounds?.start.getTime(), timelineBounds?.end.getTime()]);
199
+ // Reset selection on signal change
200
+ useEffect(() => {
201
+ setSelectedSpan(null);
202
+ setSelectedLogIdx(null);
203
+ setBottomPane(null);
204
+ }, [signal]);
205
+ // ── handlers ──
206
+ const handleRefresh = useCallback(() => {
207
+ if (signal === 'traces')
208
+ void refetchTraces();
209
+ if (signal === 'logs')
210
+ void refetchLogs();
211
+ if (signal === 'metrics')
212
+ void refetchMetrics();
213
+ }, [signal, refetchTraces, refetchLogs, refetchMetrics]);
214
+ const handleSpanSelect = useCallback((span) => {
215
+ setSelectedSpan(span);
216
+ setSelectedLogIdx(null);
217
+ }, []);
218
+ const handleLogSelect = useCallback((log, idx) => {
219
+ setSelectedLogIdx(idx);
220
+ setSelectedSpan(null);
221
+ }, []);
222
+ const handleCloseDetail = useCallback(() => {
223
+ setSelectedSpan(null);
224
+ setSelectedLogIdx(null);
225
+ }, []);
226
+ const handleRangeChange = useCallback((start, end) => {
227
+ setRangeStart(start);
228
+ setRangeEnd(end);
229
+ }, []);
230
+ const toggleBottomPane = useCallback((pane) => setBottomPane(cur => (cur === pane ? null : pane)), []);
231
+ const hasDetail = (signal === 'traces' && selectedSpan !== null) ||
232
+ (signal === 'logs' && selectedLogIdx !== null);
233
+ // ── Time-range-filtered data ──
234
+ const isRangeActive = rangeStart !== null &&
235
+ rangeEnd !== null &&
236
+ timelineBounds !== null &&
237
+ (rangeStart.getTime() > timelineBounds.start.getTime() + 100 ||
238
+ rangeEnd.getTime() < timelineBounds.end.getTime() - 100);
239
+ const filteredTraces = useMemo(() => {
240
+ const list = filterSpans(traces ?? [], query);
241
+ if (!isRangeActive || !rangeStart || !rangeEnd)
242
+ return list;
243
+ const s = rangeStart.getTime();
244
+ const e = rangeEnd.getTime();
245
+ return list.filter(t => {
246
+ const ts = new Date(t.start_time).getTime();
247
+ return ts >= s && ts <= e;
248
+ });
249
+ }, [traces, query, isRangeActive, rangeStart, rangeEnd]);
250
+ const filteredLogs = useMemo(() => {
251
+ const list = filterLogs(logs ?? [], query);
252
+ if (!isRangeActive || !rangeStart || !rangeEnd)
253
+ return list;
254
+ const s = rangeStart.getTime();
255
+ const e = rangeEnd.getTime();
256
+ return list.filter(l => {
257
+ const ts = new Date(l.timestamp).getTime();
258
+ return ts >= s && ts <= e;
259
+ });
260
+ }, [logs, query, isRangeActive, rangeStart, rangeEnd]);
261
+ const filteredMetrics = useMemo(() => {
262
+ const list = metrics ?? [];
263
+ if (!isRangeActive || !rangeStart || !rangeEnd)
264
+ return list;
265
+ const s = rangeStart.getTime();
266
+ const e = rangeEnd.getTime();
267
+ return list.filter(m => {
268
+ const ts = new Date(m.timestamp).getTime();
269
+ return ts >= s && ts <= e;
270
+ });
271
+ }, [metrics, isRangeActive, rangeStart, rangeEnd]);
272
+ return (_jsxs(Box, { sx: {
273
+ display: 'flex',
274
+ flexDirection: 'column',
275
+ flex: 1,
276
+ minHeight: 0,
277
+ height: '100%',
278
+ color: 'fg.default',
279
+ bg: 'canvas.default',
280
+ border: '1px solid',
281
+ borderColor: 'border.default',
282
+ borderRadius: 2,
283
+ overflow: 'hidden',
284
+ }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0 }, children: [_jsx(Box, { sx: { flex: 1 }, children: _jsx(OtelSearchBar, { signal: signal, onSignalChange: setSignal, services: services ?? [], selectedService: service, onServiceChange: setService, query: query, onQueryChange: setQuery, onRefresh: handleRefresh, loading: signal === 'traces'
285
+ ? tracesLoading
286
+ : signal === 'logs'
287
+ ? logsLoading
288
+ : metricsLoading }) }), _jsx(Box, { sx: { pr: 2, flexShrink: 0 }, children: _jsx(Label, { variant: wsConnected ? 'success' : 'secondary', size: "small", children: wsConnected ? '● Live' : '○ Polling' }) })] }), timelineBounds && rangeStart && rangeEnd && (_jsx(Box, { sx: {
289
+ px: 3,
290
+ pt: 2,
291
+ pb: 1,
292
+ borderBottom: '1px solid',
293
+ borderColor: 'border.default',
294
+ bg: 'canvas.subtle',
295
+ flexShrink: 0,
296
+ position: 'relative',
297
+ zIndex: 0,
298
+ }, children: _jsx(OtelTimelineRangeSlider, { timelineStart: timelineBounds.start, timelineEnd: timelineBounds.end, selectedStart: rangeStart, selectedEnd: rangeEnd, onRangeChange: handleRangeChange, histogram: histogram, height: 48, tickCount: 6 }) })), _jsxs(Box, { sx: { display: 'flex', flex: 1, minHeight: 0, overflow: 'hidden' }, children: [_jsxs(Box, { sx: {
299
+ display: 'flex',
300
+ flexDirection: 'column',
301
+ flex: hasDetail ? '0 0 55%' : '1 1 100%',
302
+ minHeight: 0,
303
+ overflow: 'hidden',
304
+ }, children: [signal === 'traces' && (_jsx(OtelTracesList, { spans: filteredTraces, loading: tracesLoading, selectedSpanId: selectedSpan?.span_id, onSelectSpan: handleSpanSelect })), signal === 'logs' && (_jsx(OtelLogsList, { logs: filteredLogs, loading: logsLoading, selectedLogIndex: selectedLogIdx, onSelectLog: handleLogSelect })), signal === 'metrics' && (_jsx(OtelMetricsList, { metrics: filteredMetrics, loading: metricsLoading }))] }), hasDetail && (_jsxs(Box, { sx: {
305
+ flex: '0 0 45%',
306
+ minHeight: 0,
307
+ overflow: 'auto',
308
+ borderLeft: '1px solid',
309
+ borderColor: 'border.default',
310
+ }, children: [signal === 'traces' && selectedSpan && (_jsx(OtelSpanDetail, { span: selectedSpan, traceSpans: traceSpans ?? undefined, onClose: handleCloseDetail })), signal === 'logs' && selectedLogIdx !== null && logs && (_jsxs(Box, { sx: { p: 3 }, children: [_jsxs(Box, { sx: {
311
+ display: 'flex',
312
+ justifyContent: 'space-between',
313
+ mb: 3,
314
+ }, children: [_jsx(Text, { sx: { fontWeight: 'bold', fontSize: 2 }, children: "Log Detail" }), _jsx(Button, { size: "small", variant: "invisible", onClick: handleCloseDetail, children: "\u2715" })] }), _jsx(Box, { as: "pre", sx: {
315
+ m: 0,
316
+ fontSize: 1,
317
+ fontFamily: 'mono',
318
+ whiteSpace: 'pre-wrap',
319
+ wordBreak: 'break-word',
320
+ }, children: JSON.stringify(logs[selectedLogIdx], null, 2) })] }))] }))] }), signal === 'traces' && selectedSpan && spanTree.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Box, { sx: {
321
+ display: 'flex',
322
+ gap: 1,
323
+ px: 3,
324
+ py: 1,
325
+ bg: 'canvas.subtle',
326
+ borderTop: '1px solid',
327
+ borderColor: 'border.default',
328
+ flexShrink: 0,
329
+ }, children: [_jsx(Button, { size: "small", variant: bottomPane === 'timeline' ? 'primary' : 'invisible', leadingVisual: ClockIcon, onClick: () => toggleBottomPane('timeline'), children: "Timeline" }), _jsx(Button, { size: "small", variant: bottomPane === 'tree' ? 'primary' : 'invisible', leadingVisual: GitBranchIcon, onClick: () => toggleBottomPane('tree'), children: "Span Tree" })] }), bottomPane && (_jsxs(Box, { sx: {
330
+ height: 260,
331
+ overflow: 'auto',
332
+ borderTop: '1px solid',
333
+ borderColor: 'border.default',
334
+ flexShrink: 0,
335
+ }, children: [bottomPane === 'timeline' && (_jsx(OtelTimeline, { spans: traceSpans ?? [], selectedSpanId: selectedSpan?.span_id, onSelectSpan: handleSpanSelect })), bottomPane === 'tree' && (_jsx(OtelSpanTree, { spans: spanTree, selectedSpanId: selectedSpan?.span_id, onSelectSpan: handleSpanSelect, defaultExpandDepth: 3 }))] }))] }))] }));
336
+ };
337
+ // ── Client-side filter helpers ──────────────────────────────────────
338
+ function filterSpans(spans, q) {
339
+ if (!q.trim())
340
+ return spans;
341
+ const lq = q.toLowerCase();
342
+ return spans.filter(s => s.span_name.toLowerCase().includes(lq) ||
343
+ s.service_name.toLowerCase().includes(lq) ||
344
+ (s.otel_scope_name ?? '').toLowerCase().includes(lq) ||
345
+ (s.status_message ?? '').toLowerCase().includes(lq));
346
+ }
347
+ function filterLogs(logs, q) {
348
+ if (!q.trim())
349
+ return logs;
350
+ const lq = q.toLowerCase();
351
+ return logs.filter(l => l.body.toLowerCase().includes(lq) ||
352
+ l.service_name.toLowerCase().includes(lq) ||
353
+ l.severity_text.toLowerCase().includes(lq));
354
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * OtelLogsList – Tabular log-records view with severity colour coding,
3
+ * expandable body/attributes, and trace correlation links.
4
+ *
5
+ * Uses Primer React components for consistent theming.
6
+ *
7
+ * @module otel/OtelLogsList
8
+ */
9
+ import React from 'react';
10
+ import type { OtelLogsListProps } from './types';
11
+ export declare const OtelLogsList: React.FC<OtelLogsListProps>;
@@ -0,0 +1,137 @@
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
+ * OtelLogsList – Tabular log-records view with severity colour coding,
8
+ * expandable body/attributes, and trace correlation links.
9
+ *
10
+ * Uses Primer React components for consistent theming.
11
+ *
12
+ * @module otel/OtelLogsList
13
+ */
14
+ import React, { useState } from 'react';
15
+ import { Box, Text, Label, Spinner } from '@primer/react';
16
+ import { Blankslate } from '@primer/react/experimental';
17
+ import { LogIcon } from '@primer/octicons-react';
18
+ import { formatTime, severityVariant } from './utils';
19
+ // ── helpers ─────────────────────────────────────────────────────────
20
+ /** Severity badge using Primer Label, centered in its grid cell. */
21
+ const Severity = ({ text }) => (_jsx(Box, { sx: { display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: _jsx(Label, { size: "small", variant: severityVariant(text), children: text }) }));
22
+ /** Expandable row detail for a single log record. */
23
+ const LogDetail = ({ log }) => (_jsxs(Box, { sx: {
24
+ gridColumn: '1 / -1',
25
+ bg: 'canvas.subtle',
26
+ borderBottom: '1px solid',
27
+ borderColor: 'border.default',
28
+ p: 3,
29
+ }, children: [_jsxs(Box, { sx: { mb: 2 }, children: [_jsx(Text, { sx: {
30
+ fontSize: 0,
31
+ fontWeight: 'bold',
32
+ color: 'fg.muted',
33
+ display: 'block',
34
+ mb: 1,
35
+ }, children: "Body" }), _jsx(Box, { as: "pre", sx: {
36
+ fontSize: 1,
37
+ fontFamily: 'mono',
38
+ whiteSpace: 'pre-wrap',
39
+ wordBreak: 'break-word',
40
+ m: 0,
41
+ bg: 'canvas.default',
42
+ border: '1px solid',
43
+ borderColor: 'border.default',
44
+ borderRadius: 2,
45
+ p: 2,
46
+ }, children: log.body })] }), log.trace_id && (_jsxs(Box, { sx: { 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: {
47
+ fontSize: 0,
48
+ fontWeight: 'bold',
49
+ color: 'fg.muted',
50
+ display: 'block',
51
+ mb: 1,
52
+ }, children: "Attributes" }), _jsx(Box, { sx: {
53
+ bg: 'canvas.default',
54
+ border: '1px solid',
55
+ borderColor: 'border.default',
56
+ borderRadius: 2,
57
+ p: 2,
58
+ }, children: Object.entries(log.attributes).map(([k, v]) => (_jsxs(Box, { sx: {
59
+ display: 'flex',
60
+ gap: 2,
61
+ py: 1,
62
+ borderBottom: '1px solid',
63
+ borderColor: 'border.muted',
64
+ }, children: [_jsx(Text, { sx: {
65
+ color: 'accent.fg',
66
+ fontSize: 0,
67
+ fontFamily: 'mono',
68
+ minWidth: 150,
69
+ }, children: k }), _jsx(Text, { sx: {
70
+ fontSize: 0,
71
+ fontFamily: 'mono',
72
+ wordBreak: 'break-word',
73
+ }, children: typeof v === 'string' ? v : JSON.stringify(v) })] }, k))) })] }))] }));
74
+ // ── Main component ──────────────────────────────────────────────────
75
+ export const OtelLogsList = ({ logs, loading, selectedLogIndex, onSelectLog, }) => {
76
+ const [expandedIdx, setExpandedIdx] = useState(null);
77
+ const colTemplate = '140px 80px 160px 1fr';
78
+ if (loading && logs.length === 0) {
79
+ return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', p: 5 }, children: _jsx(Spinner, { size: "medium" }) }));
80
+ }
81
+ if (!loading && logs.length === 0) {
82
+ return (_jsxs(Blankslate, { children: [_jsx(Blankslate.Visual, { children: _jsx(LogIcon, { size: 24 }) }), _jsx(Blankslate.Heading, { children: "No log records found" }), _jsx(Blankslate.Description, { children: "Send some log data first." })] }));
83
+ }
84
+ return (_jsxs(Box, { sx: { flex: 1, minHeight: 0, overflow: 'auto' }, children: [_jsx(Box, { sx: {
85
+ display: 'grid',
86
+ gridTemplateColumns: colTemplate,
87
+ bg: 'canvas.subtle',
88
+ borderBottom: '2px solid',
89
+ borderColor: 'border.default',
90
+ px: 3,
91
+ py: 1,
92
+ position: 'sticky',
93
+ top: 0,
94
+ zIndex: 1,
95
+ }, children: ['Time', 'Severity', 'Service', 'Body'].map(h => (_jsx(Text, { sx: {
96
+ fontSize: 0,
97
+ fontWeight: 'bold',
98
+ color: 'fg.muted',
99
+ textTransform: 'uppercase',
100
+ letterSpacing: '0.05em',
101
+ textAlign: h === 'Severity' ? 'center' : undefined,
102
+ }, children: h }, h))) }), logs.map((log, idx) => {
103
+ const selected = idx === selectedLogIndex;
104
+ const expanded = idx === expandedIdx;
105
+ return (_jsxs(React.Fragment, { children: [_jsxs(Box, { sx: {
106
+ display: 'grid',
107
+ gridTemplateColumns: colTemplate,
108
+ px: 3,
109
+ py: '5px',
110
+ borderBottom: '1px solid',
111
+ borderColor: 'border.muted',
112
+ cursor: 'pointer',
113
+ bg: selected
114
+ ? 'accent.subtle'
115
+ : expanded
116
+ ? 'canvas.subtle'
117
+ : 'canvas.default',
118
+ ':hover': {
119
+ bg: selected || expanded ? undefined : 'canvas.subtle',
120
+ },
121
+ }, onClick: () => {
122
+ setExpandedIdx(expanded ? null : idx);
123
+ onSelectLog?.(log, idx);
124
+ }, children: [_jsx(Text, { sx: { fontSize: 1, fontFamily: 'mono', color: 'fg.default' }, children: formatTime(log.timestamp) }), _jsx(Severity, { text: log.severity_text }), _jsx(Text, { sx: {
125
+ fontSize: 1,
126
+ color: 'fg.muted',
127
+ overflow: 'hidden',
128
+ textOverflow: 'ellipsis',
129
+ whiteSpace: 'nowrap',
130
+ }, children: log.service_name }), _jsx(Text, { sx: {
131
+ fontSize: 1,
132
+ overflow: 'hidden',
133
+ textOverflow: 'ellipsis',
134
+ whiteSpace: 'nowrap',
135
+ }, children: log.body })] }), expanded && _jsx(LogDetail, { log: log })] }, idx));
136
+ })] }));
137
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * OtelMetricsChart – Type-aware ECharts visualisation for OTEL metrics.
3
+ *
4
+ * Renders one chart section per `metric_type` (`sum`, `histogram`, `gauge`,
5
+ * and any other type present) with an appropriate chart style:
6
+ *
7
+ * - **sum** (counters): area-line chart with gradient fill
8
+ * - **histogram**: bar chart with grouped series
9
+ * - **gauge**: plain line chart (no area fill)
10
+ * - **other**: falls back to area-line
11
+ *
12
+ * @module otel/OtelMetricsChart
13
+ */
14
+ import React from 'react';
15
+ import type { OtelMetric } from './types';
16
+ export interface OtelMetricsChartProps {
17
+ metrics: OtelMetric[];
18
+ /** Height per chart panel in px. Default 240. */
19
+ height?: number;
20
+ }
21
+ export declare const OtelMetricsChart: React.FC<OtelMetricsChartProps>;
22
+ export default OtelMetricsChart;