@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,166 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Hook for fetching and managing projects.
|
|
7
|
+
*
|
|
8
|
+
* Projects are backed by the Spacer service as spaces with type_s="project".
|
|
9
|
+
* Projects persist forever — only agents can terminate.
|
|
10
|
+
*
|
|
11
|
+
* Free tier limits: max 3 projects per user.
|
|
12
|
+
*
|
|
13
|
+
* @module hooks/useProjects
|
|
14
|
+
*/
|
|
15
|
+
import { useMemo, useCallback } from 'react';
|
|
16
|
+
import { useCache } from './useCache';
|
|
17
|
+
/** The space type value used to identify project spaces in Solr */
|
|
18
|
+
export const PROJECT_SPACE_VARIANT = 'project';
|
|
19
|
+
/**
|
|
20
|
+
* Hook to fetch user's projects (spaces with type "project").
|
|
21
|
+
*
|
|
22
|
+
* Uses the spacer service's spaces endpoint filtered by type.
|
|
23
|
+
*/
|
|
24
|
+
export function useProjects() {
|
|
25
|
+
const { useUserSpaces } = useCache();
|
|
26
|
+
const { data: allSpaces, ...rest } = useUserSpaces();
|
|
27
|
+
// Filter to only project-type spaces
|
|
28
|
+
const projects = useMemo(() => {
|
|
29
|
+
if (!allSpaces)
|
|
30
|
+
return [];
|
|
31
|
+
return allSpaces
|
|
32
|
+
.filter((space) => space.variant === PROJECT_SPACE_VARIANT ||
|
|
33
|
+
space.type_s === PROJECT_SPACE_VARIANT)
|
|
34
|
+
.map((space) => ({
|
|
35
|
+
uid: space.uid,
|
|
36
|
+
id: space.id ?? space.uid,
|
|
37
|
+
handle: space.handle ?? space.handle_s,
|
|
38
|
+
name: space.name ?? space.name_t,
|
|
39
|
+
description: space.description ?? space.description_t ?? '',
|
|
40
|
+
createdAt: space.created_at ? new Date(space.created_at) : new Date(),
|
|
41
|
+
isPublic: space.public ?? space.public_b ?? false,
|
|
42
|
+
attachedAgentPodName: space.attached_agent_pod_name_s || undefined,
|
|
43
|
+
attachedAgentSpecId: space.attached_agent_spec_id_s || undefined,
|
|
44
|
+
}));
|
|
45
|
+
}, [allSpaces]);
|
|
46
|
+
return { data: projects, ...rest };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Hook to fetch a single project by UID.
|
|
50
|
+
*/
|
|
51
|
+
export function useProject(uid) {
|
|
52
|
+
const { useSpace } = useCache();
|
|
53
|
+
// useSpace requires a string – pass empty string when uid is undefined
|
|
54
|
+
// (the query is disabled when spaceId is falsy inside useCache)
|
|
55
|
+
const { data: space, ...rest } = useSpace(uid ?? '');
|
|
56
|
+
const project = useMemo(() => {
|
|
57
|
+
if (!space)
|
|
58
|
+
return undefined;
|
|
59
|
+
const s = space;
|
|
60
|
+
return {
|
|
61
|
+
uid: s.uid,
|
|
62
|
+
id: s.id ?? s.uid,
|
|
63
|
+
handle: s.handle ?? s.handle_s,
|
|
64
|
+
name: s.name ?? s.name_t,
|
|
65
|
+
description: s.description ?? s.description_t ?? '',
|
|
66
|
+
createdAt: s.created_at ? new Date(s.created_at) : new Date(),
|
|
67
|
+
isPublic: s.public ?? s.public_b ?? false,
|
|
68
|
+
attachedAgentPodName: s.attached_agent_pod_name_s || undefined,
|
|
69
|
+
attachedAgentSpecId: s.attached_agent_spec_id_s || undefined,
|
|
70
|
+
};
|
|
71
|
+
}, [space]);
|
|
72
|
+
return { data: project, ...rest };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Hook to create a new project.
|
|
76
|
+
* Creates a space with type "project" via the spacer service.
|
|
77
|
+
*/
|
|
78
|
+
export function useCreateProject() {
|
|
79
|
+
const { useCreateSpace } = useCache();
|
|
80
|
+
const createSpaceMutation = useCreateSpace();
|
|
81
|
+
const createProject = useCallback(async (request) => {
|
|
82
|
+
const spaceHandle = request.name
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
85
|
+
.replace(/^-|-$/g, '');
|
|
86
|
+
return createSpaceMutation.mutateAsync({
|
|
87
|
+
space: {
|
|
88
|
+
name: request.name,
|
|
89
|
+
description: request.description || '',
|
|
90
|
+
handle: spaceHandle || `project-${Date.now()}`,
|
|
91
|
+
variant: PROJECT_SPACE_VARIANT,
|
|
92
|
+
public: false,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}, [createSpaceMutation]);
|
|
96
|
+
return {
|
|
97
|
+
...createSpaceMutation,
|
|
98
|
+
createProject,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Hook to update a project (e.g. persist the attached agent pod name).
|
|
103
|
+
* Uses the spacer PUT endpoint via useUpdateSpace.
|
|
104
|
+
*/
|
|
105
|
+
export function useUpdateProject() {
|
|
106
|
+
const { useUpdateSpace } = useCache();
|
|
107
|
+
const updateSpaceMutation = useUpdateSpace();
|
|
108
|
+
/** Assign an agent runtime to the project */
|
|
109
|
+
const assignAgent = useCallback(async (project, agentPodName, agentSpecId) => {
|
|
110
|
+
return updateSpaceMutation.mutateAsync({
|
|
111
|
+
id: project.id,
|
|
112
|
+
name: project.name,
|
|
113
|
+
description: project.description,
|
|
114
|
+
attached_agent_pod_name_s: agentPodName,
|
|
115
|
+
attached_agent_spec_id_s: agentSpecId || '',
|
|
116
|
+
});
|
|
117
|
+
}, [updateSpaceMutation]);
|
|
118
|
+
/** Rename a project */
|
|
119
|
+
const renameProject = useCallback(async (project, newName) => {
|
|
120
|
+
return updateSpaceMutation.mutateAsync({
|
|
121
|
+
id: project.id,
|
|
122
|
+
name: newName,
|
|
123
|
+
description: project.description,
|
|
124
|
+
});
|
|
125
|
+
}, [updateSpaceMutation]);
|
|
126
|
+
/** Remove the agent assignment from the project */
|
|
127
|
+
const unassignAgent = useCallback(async (project) => {
|
|
128
|
+
return updateSpaceMutation.mutateAsync({
|
|
129
|
+
id: project.id,
|
|
130
|
+
name: project.name,
|
|
131
|
+
description: project.description,
|
|
132
|
+
attached_agent_pod_name_s: '',
|
|
133
|
+
attached_agent_spec_id_s: '',
|
|
134
|
+
});
|
|
135
|
+
}, [updateSpaceMutation]);
|
|
136
|
+
return {
|
|
137
|
+
...updateSpaceMutation,
|
|
138
|
+
assignAgent,
|
|
139
|
+
unassignAgent,
|
|
140
|
+
renameProject,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Hook to refresh the projects list.
|
|
145
|
+
*/
|
|
146
|
+
export function useRefreshProjects() {
|
|
147
|
+
const { useRefreshUserSpaces } = useCache();
|
|
148
|
+
return useRefreshUserSpaces();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Hook to delete a project (space) and all its contents.
|
|
152
|
+
*/
|
|
153
|
+
export function useDeleteProject() {
|
|
154
|
+
const { useDeleteSpace } = useCache();
|
|
155
|
+
return useDeleteSpace();
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Hook to fetch the default notebook and document UIDs for a project.
|
|
159
|
+
*
|
|
160
|
+
* This calls the spacer `GET /spaces/{uid}/default-items` endpoint which
|
|
161
|
+
* returns the UID of the first notebook and first document in the space.
|
|
162
|
+
*/
|
|
163
|
+
export function useProjectDefaultItems(projectUid) {
|
|
164
|
+
const { useSpaceDefaultItems } = useCache();
|
|
165
|
+
return useSpaceDefaultItems(projectUid);
|
|
166
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -5,7 +5,6 @@ export * from './collaboration';
|
|
|
5
5
|
export * from './services';
|
|
6
6
|
export * from './navigation';
|
|
7
7
|
export * from './hooks';
|
|
8
|
-
export * from './theme';
|
|
9
8
|
export { requestDatalayerAPI, RunResponseError, NetworkError, } from './api/DatalayerApi';
|
|
10
9
|
export type { IRequestDatalayerAPIOptions } from './api/DatalayerApi';
|
|
11
10
|
export { API_BASE_PATHS } from './api/constants';
|
|
@@ -14,3 +13,5 @@ export * as iamApi from './api/iam';
|
|
|
14
13
|
export * as spacerApi from './api/spacer';
|
|
15
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';
|
|
16
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
|
@@ -11,8 +11,6 @@ export * from './services';
|
|
|
11
11
|
// Export navigation before hooks to avoid conflicts
|
|
12
12
|
export * from './navigation';
|
|
13
13
|
export * from './hooks';
|
|
14
|
-
// Export Theme.
|
|
15
|
-
export * from './theme';
|
|
16
14
|
// Export APIs.
|
|
17
15
|
export { requestDatalayerAPI, RunResponseError, NetworkError, } from './api/DatalayerApi';
|
|
18
16
|
export { API_BASE_PATHS } from './api/constants';
|
|
@@ -28,3 +26,7 @@ User, Runtime, Environment, Snapshot, Space, Notebook, LexicalDTO, Secret, Credi
|
|
|
28
26
|
AuthenticationManager, Datasource, } from './client';
|
|
29
27
|
// Export commonly used functions directly for convenience
|
|
30
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';
|
package/lib/models/Page.d.ts
CHANGED
|
@@ -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>;
|