@datalayer/core 1.1.45 → 1.1.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/principal/Principal.js +109 -11
- package/lib/components/principal/PrincipalDetailsOverlay.d.ts +7 -1
- package/lib/components/principal/PrincipalDetailsOverlay.js +28 -8
- package/lib/config/Configuration.d.ts +18 -0
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.js +1 -0
- package/lib/hooks/usePrincipalCacheStore.d.ts +54 -0
- package/lib/hooks/usePrincipalCacheStore.js +94 -0
- package/lib/otel/client/OtelClient.d.ts +4 -0
- package/lib/otel/client/OtelClient.js +7 -5
- package/lib/otel/hooks/index.js +6 -6
- package/lib/otel/views/OtelLive.js +2 -2
- package/lib/routes/publicPaths.js +1 -0
- package/lib/state/substates/CoreState.d.ts +11 -0
- package/lib/state/substates/CoreState.js +18 -0
- package/lib/views/datasources/Datasources.d.ts +3 -1
- package/lib/views/datasources/Datasources.js +9 -9
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
13
13
|
import * as React from 'react';
|
|
14
14
|
import { Box } from '@datalayer/primer-addons';
|
|
15
15
|
import { useCache } from '../../hooks/index.js';
|
|
16
|
+
import { usePrincipalCacheStore, principalCacheKey, } from '../../hooks/usePrincipalCacheStore.js';
|
|
16
17
|
import { useIAMStore } from '../../state/index.js';
|
|
17
18
|
import { PrincipalAvatar } from './PrincipalAvatar.js';
|
|
18
19
|
import { PrincipalDetailsOverlay } from './PrincipalDetailsOverlay.js';
|
|
@@ -53,6 +54,14 @@ export const Principal = ({ principal, isAdmin = false, avatarSize = 20, gap = 2
|
|
|
53
54
|
const { user: authenticatedUser } = useIAMStore();
|
|
54
55
|
const isAnonymous = !authenticatedUser;
|
|
55
56
|
const normalizedUid = String(principal.uid || '').trim();
|
|
57
|
+
// In-memory principal cache: seed the display from a previously resolved
|
|
58
|
+
// snapshot (keyed by uid) for an instant paint, and write freshly resolved
|
|
59
|
+
// data back so later renders anywhere reuse it. Network hooks below still
|
|
60
|
+
// refresh on demand per their staleTime.
|
|
61
|
+
const cachedPrincipal = usePrincipalCacheStore(state => normalizedUid
|
|
62
|
+
? state.principals[principalCacheKey(principal.kind, normalizedUid)]
|
|
63
|
+
: undefined);
|
|
64
|
+
const upsertCachedPrincipal = usePrincipalCacheStore(state => state.upsertPrincipal);
|
|
56
65
|
const normalizedProvidedHandle = String(principal.handle || '').trim();
|
|
57
66
|
const normalizedProvidedAccountHandle = String(principal.accountHandle || '').trim();
|
|
58
67
|
const providedHandleIsUidPlaceholder = !!normalizedProvidedHandle &&
|
|
@@ -145,9 +154,31 @@ export const Principal = ({ principal, isAdmin = false, avatarSize = 20, gap = 2
|
|
|
145
154
|
: String(hydratedEntity?.displayName ||
|
|
146
155
|
hydratedEntity?.name ||
|
|
147
156
|
'').trim();
|
|
157
|
+
// Team-specific enrichment: parent organization + membership + visibility,
|
|
158
|
+
// falling back to the in-memory cache when the network data isn't ready yet.
|
|
159
|
+
const hydratedTeam = principal.kind === 'team' ? hydratedTeamQuery.data : undefined;
|
|
160
|
+
const teamMemberCount = hydratedTeam && Array.isArray(hydratedTeam.members)
|
|
161
|
+
? hydratedTeam.members.length
|
|
162
|
+
: cachedPrincipal?.memberCount;
|
|
163
|
+
const teamOrganizationHandleResolved = principal.kind === 'team'
|
|
164
|
+
? String(teamOrganizationQuery.data?.handle ||
|
|
165
|
+
teamOrganizationHandle ||
|
|
166
|
+
cachedPrincipal?.organizationHandle ||
|
|
167
|
+
'').trim() || undefined
|
|
168
|
+
: undefined;
|
|
169
|
+
const teamOrganizationNameResolved = principal.kind === 'team'
|
|
170
|
+
? String(teamOrganizationQuery.data?.displayName ||
|
|
171
|
+
teamOrganizationQuery.data?.name ||
|
|
172
|
+
cachedPrincipal?.organizationName ||
|
|
173
|
+
'').trim() || undefined
|
|
174
|
+
: undefined;
|
|
175
|
+
const resolvedIsPublic = typeof hydratedEntity?.public === 'boolean'
|
|
176
|
+
? hydratedEntity.public
|
|
177
|
+
: cachedPrincipal?.isPublic;
|
|
148
178
|
const resolvedPrincipal = {
|
|
149
179
|
...principal,
|
|
150
180
|
displayName: hydratedDisplayName ||
|
|
181
|
+
String(cachedPrincipal?.displayName || '').trim() ||
|
|
151
182
|
(() => {
|
|
152
183
|
const providedDisplayName = String(principal.displayName || '').trim();
|
|
153
184
|
const providedIsGenericPlaceholder = !providedDisplayName ||
|
|
@@ -164,30 +195,97 @@ export const Principal = ({ principal, isAdmin = false, avatarSize = 20, gap = 2
|
|
|
164
195
|
})(),
|
|
165
196
|
handle: (!providedHandleIsUidPlaceholder ? principal.handle : undefined) ||
|
|
166
197
|
String(hydratedEntity?.handle || '').trim() ||
|
|
198
|
+
cachedPrincipal?.handle ||
|
|
167
199
|
undefined,
|
|
168
|
-
accountHandle:
|
|
169
|
-
?
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
200
|
+
accountHandle: principal.kind === 'team'
|
|
201
|
+
? (!providedAccountHandleIsUidPlaceholder
|
|
202
|
+
? principal.accountHandle
|
|
203
|
+
: undefined) ||
|
|
204
|
+
teamOrganizationHandleResolved ||
|
|
205
|
+
cachedPrincipal?.accountHandle ||
|
|
206
|
+
undefined
|
|
207
|
+
: (!providedAccountHandleIsUidPlaceholder
|
|
208
|
+
? principal.accountHandle
|
|
209
|
+
: undefined) ||
|
|
210
|
+
String(hydratedEntity?.handle || '').trim() ||
|
|
211
|
+
cachedPrincipal?.accountHandle ||
|
|
212
|
+
undefined,
|
|
173
213
|
name: principal.name ||
|
|
174
214
|
String(hydratedEntity?.name || '').trim() ||
|
|
215
|
+
cachedPrincipal?.name ||
|
|
175
216
|
undefined,
|
|
176
217
|
description: principal.description ||
|
|
177
218
|
String(hydratedEntity?.description || '').trim() ||
|
|
219
|
+
cachedPrincipal?.description ||
|
|
220
|
+
undefined,
|
|
221
|
+
avatarUrl: principal.avatarUrl ||
|
|
222
|
+
hydratedEntity?.avatarUrl ||
|
|
223
|
+
cachedPrincipal?.avatarUrl ||
|
|
224
|
+
undefined,
|
|
225
|
+
firstName: principal.firstName ||
|
|
226
|
+
hydratedEntity?.firstName ||
|
|
227
|
+
cachedPrincipal?.firstName ||
|
|
228
|
+
undefined,
|
|
229
|
+
lastName: principal.lastName ||
|
|
230
|
+
hydratedEntity?.lastName ||
|
|
231
|
+
cachedPrincipal?.lastName ||
|
|
232
|
+
undefined,
|
|
233
|
+
email: principal.email ||
|
|
234
|
+
hydratedEntity?.email ||
|
|
235
|
+
cachedPrincipal?.email ||
|
|
236
|
+
undefined,
|
|
237
|
+
origin: principal.origin ||
|
|
238
|
+
hydratedEntity?.origin ||
|
|
239
|
+
cachedPrincipal?.origin ||
|
|
178
240
|
undefined,
|
|
179
|
-
avatarUrl: principal.avatarUrl || hydratedEntity?.avatarUrl || undefined,
|
|
180
|
-
firstName: principal.firstName || hydratedEntity?.firstName || undefined,
|
|
181
|
-
lastName: principal.lastName || hydratedEntity?.lastName || undefined,
|
|
182
|
-
email: principal.email || hydratedEntity?.email || undefined,
|
|
183
|
-
origin: principal.origin || hydratedEntity?.origin || undefined,
|
|
184
241
|
};
|
|
242
|
+
// Persist the freshly resolved snapshot so subsequent renders (here or in
|
|
243
|
+
// other views) paint instantly from memory. Refreshes still happen on demand
|
|
244
|
+
// through the underlying query hooks.
|
|
245
|
+
React.useEffect(() => {
|
|
246
|
+
if (!normalizedUid) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
upsertCachedPrincipal({
|
|
250
|
+
kind: principal.kind,
|
|
251
|
+
uid: normalizedUid,
|
|
252
|
+
// Only cache a real, resolved display name — never a uid/handle
|
|
253
|
+
// placeholder — so cached names stay authoritative.
|
|
254
|
+
displayName: hydratedDisplayName ||
|
|
255
|
+
String(cachedPrincipal?.displayName || '').trim() ||
|
|
256
|
+
undefined,
|
|
257
|
+
name: resolvedPrincipal.name,
|
|
258
|
+
description: resolvedPrincipal.description,
|
|
259
|
+
handle: resolvedPrincipal.handle,
|
|
260
|
+
accountHandle: resolvedPrincipal.accountHandle,
|
|
261
|
+
firstName: resolvedPrincipal.firstName,
|
|
262
|
+
lastName: resolvedPrincipal.lastName,
|
|
263
|
+
email: resolvedPrincipal.email,
|
|
264
|
+
origin: resolvedPrincipal.origin,
|
|
265
|
+
avatarUrl: resolvedPrincipal.avatarUrl,
|
|
266
|
+
organizationHandle: teamOrganizationHandleResolved,
|
|
267
|
+
organizationName: teamOrganizationNameResolved,
|
|
268
|
+
memberCount: teamMemberCount,
|
|
269
|
+
isPublic: resolvedIsPublic,
|
|
270
|
+
});
|
|
271
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
272
|
+
}, [
|
|
273
|
+
normalizedUid,
|
|
274
|
+
principal.kind,
|
|
275
|
+
hydratedEntity,
|
|
276
|
+
hydratedDisplayName,
|
|
277
|
+
teamOrganizationHandleResolved,
|
|
278
|
+
teamOrganizationNameResolved,
|
|
279
|
+
teamMemberCount,
|
|
280
|
+
resolvedIsPublic,
|
|
281
|
+
upsertCachedPrincipal,
|
|
282
|
+
]);
|
|
185
283
|
return (_jsxs(Box, { sx: {
|
|
186
284
|
display: 'inline-flex',
|
|
187
285
|
alignItems: 'center',
|
|
188
286
|
gap,
|
|
189
287
|
minWidth: 0,
|
|
190
288
|
...sx,
|
|
191
|
-
}, children: [_jsx(PrincipalAvatar, { kind: resolvedPrincipal.kind, avatarUrl: resolvedPrincipal.avatarUrl, alt: resolvedPrincipal.displayName, size: avatarSize, square: square }), _jsx(PrincipalDetailsOverlay, { kind: resolvedPrincipal.kind, uid: resolvedPrincipal.uid, displayName: resolvedPrincipal.displayName, name: resolvedPrincipal.name, description: resolvedPrincipal.description, handle: resolvedPrincipal.handle, accountHandle: resolvedPrincipal.accountHandle, firstName: resolvedPrincipal.firstName, lastName: resolvedPrincipal.lastName, email: resolvedPrincipal.email, origin: resolvedPrincipal.origin, avatarUrl: resolvedPrincipal.avatarUrl, isAdmin: isAdmin })] }));
|
|
289
|
+
}, children: [_jsx(PrincipalAvatar, { kind: resolvedPrincipal.kind, avatarUrl: resolvedPrincipal.avatarUrl, alt: resolvedPrincipal.displayName, size: avatarSize, square: square }), _jsx(PrincipalDetailsOverlay, { kind: resolvedPrincipal.kind, uid: resolvedPrincipal.uid, displayName: resolvedPrincipal.displayName, name: resolvedPrincipal.name, description: resolvedPrincipal.description, handle: resolvedPrincipal.handle, accountHandle: resolvedPrincipal.accountHandle, firstName: resolvedPrincipal.firstName, lastName: resolvedPrincipal.lastName, email: resolvedPrincipal.email, origin: resolvedPrincipal.origin, avatarUrl: resolvedPrincipal.avatarUrl, organizationName: teamOrganizationNameResolved, memberCount: teamMemberCount, isPublic: resolvedIsPublic, isAdmin: isAdmin })] }));
|
|
192
290
|
};
|
|
193
291
|
export default Principal;
|
|
@@ -12,6 +12,12 @@ export type PrincipalDetailsOverlayProps = {
|
|
|
12
12
|
email?: string;
|
|
13
13
|
origin?: string;
|
|
14
14
|
avatarUrl?: string;
|
|
15
|
+
/** Team-specific: parent organization display name. */
|
|
16
|
+
organizationName?: string;
|
|
17
|
+
/** Team-specific: number of members. */
|
|
18
|
+
memberCount?: number;
|
|
19
|
+
/** Team/organization visibility. */
|
|
20
|
+
isPublic?: boolean;
|
|
15
21
|
isAdmin?: boolean;
|
|
16
22
|
};
|
|
17
23
|
export declare function buildPrincipalProfilePath({ kind, uid, handle, accountHandle, isAdmin, }: {
|
|
@@ -21,5 +27,5 @@ export declare function buildPrincipalProfilePath({ kind, uid, handle, accountHa
|
|
|
21
27
|
accountHandle?: string;
|
|
22
28
|
isAdmin?: boolean;
|
|
23
29
|
}): string | null;
|
|
24
|
-
export declare function PrincipalDetailsOverlay({ kind, uid, displayName, name, description, handle, accountHandle, firstName, lastName, email, origin, avatarUrl, isAdmin, }: PrincipalDetailsOverlayProps): JSX.Element;
|
|
30
|
+
export declare function PrincipalDetailsOverlay({ kind, uid, displayName, name, description, handle, accountHandle, firstName, lastName, email, origin, avatarUrl, organizationName, memberCount, isPublic, isAdmin, }: PrincipalDetailsOverlayProps): JSX.Element;
|
|
25
31
|
export default PrincipalDetailsOverlay;
|
|
@@ -80,7 +80,7 @@ export function buildPrincipalProfilePath({ kind, uid, handle, accountHandle, is
|
|
|
80
80
|
}
|
|
81
81
|
return null;
|
|
82
82
|
}
|
|
83
|
-
export function PrincipalDetailsOverlay({ kind, uid, displayName, name, description, handle, accountHandle, firstName, lastName, email, origin, avatarUrl, isAdmin = false, }) {
|
|
83
|
+
export function PrincipalDetailsOverlay({ kind, uid, displayName, name, description, handle, accountHandle, firstName, lastName, email, origin, avatarUrl, organizationName, memberCount, isPublic, isAdmin = false, }) {
|
|
84
84
|
const navigate = useNavigate();
|
|
85
85
|
const normalizedDisplayName = normalize(displayName) ||
|
|
86
86
|
normalize(handle) ||
|
|
@@ -91,6 +91,8 @@ export function PrincipalDetailsOverlay({ kind, uid, displayName, name, descript
|
|
|
91
91
|
const normalizedName = normalize(name);
|
|
92
92
|
const normalizedDescription = normalize(description);
|
|
93
93
|
const normalizedOrigin = normalizeOriginLabel(origin);
|
|
94
|
+
const normalizedAccountHandle = normalize(accountHandle);
|
|
95
|
+
const normalizedOrganizationName = normalize(organizationName);
|
|
94
96
|
const resolvedHandle = normalizedHandle && normalizedHandle !== normalizedUid
|
|
95
97
|
? normalizedHandle
|
|
96
98
|
: '';
|
|
@@ -101,6 +103,20 @@ export function PrincipalDetailsOverlay({ kind, uid, displayName, name, descript
|
|
|
101
103
|
accountHandle,
|
|
102
104
|
isAdmin,
|
|
103
105
|
});
|
|
106
|
+
// For a team, extract the bare team handle (the last path segment) and the
|
|
107
|
+
// parent organization handle so we can present them separately.
|
|
108
|
+
const teamHandleOnly = kind === 'team' && resolvedHandle.includes('/')
|
|
109
|
+
? normalize(resolvedHandle.split('/').pop())
|
|
110
|
+
: resolvedHandle;
|
|
111
|
+
const teamOrganizationHandle = kind === 'team'
|
|
112
|
+
? normalizedAccountHandle ||
|
|
113
|
+
(resolvedHandle.includes('/')
|
|
114
|
+
? normalize(resolvedHandle.split('/')[0])
|
|
115
|
+
: '')
|
|
116
|
+
: '';
|
|
117
|
+
const organizationPath = teamOrganizationHandle
|
|
118
|
+
? `/${teamOrganizationHandle.replace(/^@+/, '')}`
|
|
119
|
+
: '';
|
|
104
120
|
return (_jsx(ThemeProvider, { children: _jsxs(ActionMenu, { children: [_jsx(ActionMenu.Anchor, { children: _jsx(Box, { as: "button", type: "button", sx: {
|
|
105
121
|
fontWeight: 'semibold',
|
|
106
122
|
color: 'accent.fg',
|
|
@@ -113,7 +129,9 @@ export function PrincipalDetailsOverlay({ kind, uid, displayName, name, descript
|
|
|
113
129
|
':hover': {
|
|
114
130
|
textDecoration: 'underline',
|
|
115
131
|
},
|
|
116
|
-
}, children: normalizedDisplayName }) }), _jsx(ActionMenu.Overlay, { width: "large", children: _jsxs(Box, { sx: { display: 'grid', gap: 3, p: 4, minWidth: 420 }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(PrincipalAvatar, { kind: kind, avatarUrl: avatarUrl, alt: normalizedDisplayName, size: 40 }), _jsxs(Box, { sx: { display: 'grid', gap: 0.5 }, children: [_jsx(Text, { sx: { fontWeight: 'semibold' }, children: normalizedDisplayName }),
|
|
132
|
+
}, children: normalizedDisplayName }) }), _jsx(ActionMenu.Overlay, { width: "large", children: _jsxs(Box, { sx: { display: 'grid', gap: 3, p: 4, minWidth: 420 }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(PrincipalAvatar, { kind: kind, avatarUrl: avatarUrl, alt: normalizedDisplayName, size: 40 }), _jsxs(Box, { sx: { display: 'grid', gap: 0.5 }, children: [_jsx(Text, { sx: { fontWeight: 'semibold' }, children: normalizedDisplayName }), kind === 'team' ? (teamHandleOnly ? (_jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: [displayHandleText(teamHandleOnly), teamOrganizationHandle
|
|
133
|
+
? ` · @${teamOrganizationHandle.replace(/^@+/, '')}`
|
|
134
|
+
: ''] })) : null) : resolvedHandle ? (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: displayHandleText(resolvedHandle) })) : null] })] }), _jsxs(Box, { sx: {
|
|
117
135
|
display: 'grid',
|
|
118
136
|
gridTemplateColumns: '110px 1fr',
|
|
119
137
|
gap: 1,
|
|
@@ -125,17 +143,19 @@ export function PrincipalDetailsOverlay({ kind, uid, displayName, name, descript
|
|
|
125
143
|
overflow: 'hidden',
|
|
126
144
|
textOverflow: 'ellipsis',
|
|
127
145
|
whiteSpace: 'nowrap',
|
|
128
|
-
}, children: normalizedOrigin })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "
|
|
146
|
+
}, children: normalizedOrigin })] })) : (_jsxs(_Fragment, { children: [kind === 'team' && teamHandleOnly ? (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Handle" }), _jsx(Text, { sx: { fontSize: 1 }, children: displayHandleText(teamHandleOnly) })] })) : null, kind === 'team' && teamOrganizationHandle ? (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Organization" }), _jsx(Text, { sx: { fontSize: 1 }, children: normalizedOrganizationName
|
|
147
|
+
? `${normalizedOrganizationName} (${displayHandleText(teamOrganizationHandle)})`
|
|
148
|
+
: displayHandleText(teamOrganizationHandle) })] })) : null, _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Name" }), _jsx(Text, { sx: { fontSize: 1 }, children: normalizedName || normalizedDisplayName }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Description" }), _jsx(Text, { sx: { fontSize: 1 }, children: normalizedDescription || 'N/A' }), kind === 'team' && typeof memberCount === 'number' ? (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Members" }), _jsxs(Text, { sx: { fontSize: 1 }, children: [memberCount, " ", memberCount === 1 ? 'member' : 'members'] })] })) : null, typeof isPublic === 'boolean' ? (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Visibility" }), _jsx(Label, { size: "small", variant: isPublic ? 'success' : 'secondary', sx: { justifySelf: 'start', width: 'fit-content' }, children: isPublic ? 'Public' : 'Private' })] })) : null, _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Origin" }), _jsx(Label, { size: "small", variant: "secondary", title: normalizedOrigin, sx: {
|
|
129
149
|
justifySelf: 'start',
|
|
130
150
|
width: 'fit-content',
|
|
131
151
|
maxWidth: '100%',
|
|
132
152
|
overflow: 'hidden',
|
|
133
153
|
textOverflow: 'ellipsis',
|
|
134
154
|
whiteSpace: 'nowrap',
|
|
135
|
-
}, children: normalizedOrigin })] }))] }),
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
155
|
+
}, children: normalizedOrigin })] }))] }), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'flex-end', gap: 2 }, children: [kind === 'team' && organizationPath ? (_jsx(Button, { size: "small", variant: "invisible", onClick: () => navigate(organizationPath), children: "View Organization" })) : null, _jsx(Button, { size: "small", onClick: () => {
|
|
156
|
+
if (targetPath) {
|
|
157
|
+
navigate(targetPath);
|
|
158
|
+
}
|
|
159
|
+
}, disabled: !targetPath, children: "View Profile" })] })] }) })] }) }));
|
|
140
160
|
}
|
|
141
161
|
export default PrincipalDetailsOverlay;
|
|
@@ -73,8 +73,26 @@ export type IDatalayerCoreConfig = {
|
|
|
73
73
|
mcpServersUrl: string;
|
|
74
74
|
/**
|
|
75
75
|
* OTEL (OpenTelemetry) API URL.
|
|
76
|
+
*
|
|
77
|
+
* This is the endpoint runtimes/agents export telemetry to. In the browser
|
|
78
|
+
* it is also the default endpoint used to *consume* (query) telemetry, unless
|
|
79
|
+
* {@link otelInUrl} is provided.
|
|
76
80
|
*/
|
|
77
81
|
otelUrl: string;
|
|
82
|
+
/**
|
|
83
|
+
* OTEL (OpenTelemetry) *consume* (read/query) API URL override.
|
|
84
|
+
*
|
|
85
|
+
* When set, the UI reads telemetry (metrics, traces, logs) from this URL
|
|
86
|
+
* instead of {@link otelUrl}. When empty/undefined, {@link otelUrl} is used.
|
|
87
|
+
*
|
|
88
|
+
* Primary use case: local development. When runtimes run in the cloud and
|
|
89
|
+
* export their telemetry to the production OTEL service, a local UI must fetch
|
|
90
|
+
* telemetry from production rather than from the local OTEL endpoint. Set this
|
|
91
|
+
* to e.g. `https://prod1.datalayer.run` while `otelUrl` stays local.
|
|
92
|
+
*
|
|
93
|
+
* Fed by the `DATALAYER_OTEL_IN_URL` environment variable at build/deploy time.
|
|
94
|
+
*/
|
|
95
|
+
otelInUrl?: string;
|
|
78
96
|
/**
|
|
79
97
|
* Growth API URL.
|
|
80
98
|
*/
|
package/lib/hooks/index.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export * from './useProjectStore';
|
|
|
28
28
|
export * from './useMobile';
|
|
29
29
|
export * from './useUsageRefreshStore';
|
|
30
30
|
export * from './usePrincipalStore';
|
|
31
|
+
export * from './usePrincipalCacheStore';
|
|
31
32
|
export * from './useBillingEntityStore';
|
|
32
33
|
export * from './useSelectedPrincipal';
|
|
33
34
|
export * from './useSelectedBillingEntity';
|
package/lib/hooks/index.js
CHANGED
|
@@ -33,6 +33,7 @@ export * from './useProjectStore.js';
|
|
|
33
33
|
export * from './useMobile.js';
|
|
34
34
|
export * from './useUsageRefreshStore.js';
|
|
35
35
|
export * from './usePrincipalStore.js';
|
|
36
|
+
export * from './usePrincipalCacheStore.js';
|
|
36
37
|
export * from './useBillingEntityStore.js';
|
|
37
38
|
export * from './useSelectedPrincipal.js';
|
|
38
39
|
export * from './useSelectedBillingEntity.js';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type PrincipalCacheKind = 'personal' | 'team' | 'organization';
|
|
2
|
+
/**
|
|
3
|
+
* A resolved principal snapshot kept in memory so repeated renders (across
|
|
4
|
+
* views) can paint instantly instead of waiting for a network round trip.
|
|
5
|
+
*
|
|
6
|
+
* Only meaningful fields are stored; partial snapshots are merged over time so
|
|
7
|
+
* the cache accumulates as much information as possible for a given principal.
|
|
8
|
+
*/
|
|
9
|
+
export type CachedPrincipal = {
|
|
10
|
+
kind: PrincipalCacheKind;
|
|
11
|
+
uid: string;
|
|
12
|
+
displayName?: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
handle?: string;
|
|
16
|
+
accountHandle?: string;
|
|
17
|
+
firstName?: string;
|
|
18
|
+
lastName?: string;
|
|
19
|
+
email?: string;
|
|
20
|
+
origin?: string;
|
|
21
|
+
avatarUrl?: string;
|
|
22
|
+
/** Team-specific: parent organization handle. */
|
|
23
|
+
organizationHandle?: string;
|
|
24
|
+
/** Team-specific: parent organization display name. */
|
|
25
|
+
organizationName?: string;
|
|
26
|
+
/** Team-specific: number of members. */
|
|
27
|
+
memberCount?: number;
|
|
28
|
+
/** Whether the principal (team/org) is public. */
|
|
29
|
+
isPublic?: boolean;
|
|
30
|
+
/** Timestamp (ms) of the last time this entry was updated. */
|
|
31
|
+
updatedAt?: number;
|
|
32
|
+
};
|
|
33
|
+
/** Build the cache key for a principal. */
|
|
34
|
+
export declare function principalCacheKey(kind: PrincipalCacheKind, uid: string): string;
|
|
35
|
+
type PrincipalCacheState = {
|
|
36
|
+
/** Cached principals keyed by `${kind}:${uid}`. */
|
|
37
|
+
principals: Record<string, CachedPrincipal>;
|
|
38
|
+
/**
|
|
39
|
+
* Read a cached principal snapshot, if present.
|
|
40
|
+
*/
|
|
41
|
+
getPrincipal: (kind: PrincipalCacheKind, uid: string) => CachedPrincipal | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Merge a (possibly partial) principal snapshot into the cache. Existing
|
|
44
|
+
* meaningful values are preserved when the incoming snapshot omits them, so
|
|
45
|
+
* information accumulates rather than being overwritten with blanks.
|
|
46
|
+
*/
|
|
47
|
+
upsertPrincipal: (entry: CachedPrincipal) => void;
|
|
48
|
+
/** Remove a single cached principal (e.g. to force a fresh resolution). */
|
|
49
|
+
clearPrincipal: (kind: PrincipalCacheKind, uid: string) => void;
|
|
50
|
+
/** Clear the entire cache. */
|
|
51
|
+
reset: () => void;
|
|
52
|
+
};
|
|
53
|
+
export declare const usePrincipalCacheStore: import("zustand").UseBoundStore<import("zustand").StoreApi<PrincipalCacheState>>;
|
|
54
|
+
export default usePrincipalCacheStore;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { create } from 'zustand';
|
|
6
|
+
/** Build the cache key for a principal. */
|
|
7
|
+
export function principalCacheKey(kind, uid) {
|
|
8
|
+
return `${kind}:${String(uid || '').trim()}`;
|
|
9
|
+
}
|
|
10
|
+
const MERGEABLE_KEYS = [
|
|
11
|
+
'displayName',
|
|
12
|
+
'name',
|
|
13
|
+
'description',
|
|
14
|
+
'handle',
|
|
15
|
+
'accountHandle',
|
|
16
|
+
'firstName',
|
|
17
|
+
'lastName',
|
|
18
|
+
'email',
|
|
19
|
+
'origin',
|
|
20
|
+
'avatarUrl',
|
|
21
|
+
'organizationHandle',
|
|
22
|
+
'organizationName',
|
|
23
|
+
'memberCount',
|
|
24
|
+
'isPublic',
|
|
25
|
+
];
|
|
26
|
+
function hasMeaningfulValue(value) {
|
|
27
|
+
if (value === undefined || value === null) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === 'string') {
|
|
31
|
+
return value.trim().length > 0;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
export const usePrincipalCacheStore = create((set, get) => ({
|
|
36
|
+
principals: {},
|
|
37
|
+
getPrincipal: (kind, uid) => {
|
|
38
|
+
const normalizedUid = String(uid || '').trim();
|
|
39
|
+
if (!normalizedUid) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return get().principals[principalCacheKey(kind, normalizedUid)];
|
|
43
|
+
},
|
|
44
|
+
upsertPrincipal: entry => {
|
|
45
|
+
const normalizedUid = String(entry.uid || '').trim();
|
|
46
|
+
if (!normalizedUid) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const key = principalCacheKey(entry.kind, normalizedUid);
|
|
50
|
+
const existing = get().principals[key];
|
|
51
|
+
const merged = {
|
|
52
|
+
...existing,
|
|
53
|
+
kind: entry.kind,
|
|
54
|
+
uid: normalizedUid,
|
|
55
|
+
};
|
|
56
|
+
for (const field of MERGEABLE_KEYS) {
|
|
57
|
+
const incoming = entry[field];
|
|
58
|
+
if (hasMeaningfulValue(incoming)) {
|
|
59
|
+
merged[field] = incoming;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
merged.updatedAt = Date.now();
|
|
63
|
+
// Skip the state update when no meaningful field changed to avoid
|
|
64
|
+
// needless re-renders of every mounted Principal.
|
|
65
|
+
if (existing) {
|
|
66
|
+
let changed = false;
|
|
67
|
+
for (const field of MERGEABLE_KEYS) {
|
|
68
|
+
if (merged[field] !== existing[field]) {
|
|
69
|
+
changed = true;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!changed) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
set(state => ({
|
|
78
|
+
principals: { ...state.principals, [key]: merged },
|
|
79
|
+
}));
|
|
80
|
+
},
|
|
81
|
+
clearPrincipal: (kind, uid) => {
|
|
82
|
+
const key = principalCacheKey(kind, String(uid || '').trim());
|
|
83
|
+
set(state => {
|
|
84
|
+
if (!state.principals[key]) {
|
|
85
|
+
return state;
|
|
86
|
+
}
|
|
87
|
+
const next = { ...state.principals };
|
|
88
|
+
delete next[key];
|
|
89
|
+
return { principals: next };
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
reset: () => set({ principals: {} }),
|
|
93
|
+
}));
|
|
94
|
+
export default usePrincipalCacheStore;
|
|
@@ -26,11 +26,15 @@ export interface FetchMetricsOptions {
|
|
|
26
26
|
serviceName?: string;
|
|
27
27
|
metricName?: string;
|
|
28
28
|
limit?: number;
|
|
29
|
+
/** Optional billing/account scope for account-scoped OTEL queries. */
|
|
30
|
+
accountUid?: string;
|
|
29
31
|
}
|
|
30
32
|
export interface FetchMetricTotalOptions {
|
|
31
33
|
serviceName?: string;
|
|
32
34
|
limit?: number;
|
|
33
35
|
fallbackWithoutService?: boolean;
|
|
36
|
+
/** Optional billing/account scope for account-scoped OTEL queries. */
|
|
37
|
+
accountUid?: string;
|
|
34
38
|
}
|
|
35
39
|
/**
|
|
36
40
|
* Typed HTTP client for all OTEL service endpoints.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* const { data: spans } = await client.fetchTraces({ limit: 50 });
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
19
|
+
import { getOtelConsumeUrl } from '../../state/substates/CoreState.js';
|
|
20
20
|
import { resolveOtelAuth } from '../auth.js';
|
|
21
21
|
// ── Fetch helper ────────────────────────────────────────────────────────────
|
|
22
22
|
async function otelFetch(url, token, userUid, options) {
|
|
@@ -125,8 +125,7 @@ export class OtelClient {
|
|
|
125
125
|
token;
|
|
126
126
|
userUid;
|
|
127
127
|
constructor(options) {
|
|
128
|
-
this.baseUrl =
|
|
129
|
-
options.baseUrl ?? coreStore.getState().configuration.otelUrl;
|
|
128
|
+
this.baseUrl = options.baseUrl ?? getOtelConsumeUrl();
|
|
130
129
|
const auth = resolveOtelAuth(options.token, options.userUid);
|
|
131
130
|
this.token = auth.token;
|
|
132
131
|
this.userUid = auth.userUid;
|
|
@@ -169,6 +168,8 @@ export class OtelClient {
|
|
|
169
168
|
params.set('service_name', options.serviceName);
|
|
170
169
|
if (options.metricName)
|
|
171
170
|
params.set('metric_name', options.metricName);
|
|
171
|
+
if (options.accountUid)
|
|
172
|
+
params.set('account_uid', options.accountUid);
|
|
172
173
|
const resp = await otelFetch(`${this.baseUrl}/api/otel/v1/metrics/?${params}`, this.token, this.userUid);
|
|
173
174
|
const rows = (Array.isArray(resp) ? resp : resp.data) ?? [];
|
|
174
175
|
const normalized = Array.isArray(rows)
|
|
@@ -178,17 +179,18 @@ export class OtelClient {
|
|
|
178
179
|
}
|
|
179
180
|
/** Sum all values for one metric name, with optional service filter. */
|
|
180
181
|
async fetchMetricTotal(metricName, options = {}) {
|
|
181
|
-
const { serviceName, limit = 500, fallbackWithoutService = true } = options;
|
|
182
|
+
const { serviceName, limit = 500, fallbackWithoutService = true, accountUid, } = options;
|
|
182
183
|
const filtered = await this.fetchMetrics({
|
|
183
184
|
metricName,
|
|
184
185
|
serviceName,
|
|
185
186
|
limit,
|
|
187
|
+
accountUid,
|
|
186
188
|
});
|
|
187
189
|
const filteredTotal = filtered.data.reduce((sum, row) => sum + Number(row.value || 0), 0);
|
|
188
190
|
if (filteredTotal > 0 || !serviceName || !fallbackWithoutService) {
|
|
189
191
|
return filteredTotal;
|
|
190
192
|
}
|
|
191
|
-
const unfiltered = await this.fetchMetrics({ metricName, limit });
|
|
193
|
+
const unfiltered = await this.fetchMetrics({ metricName, limit, accountUid });
|
|
192
194
|
return unfiltered.data.reduce((sum, row) => sum + Number(row.value || 0), 0);
|
|
193
195
|
}
|
|
194
196
|
// ── Services ──────────────────────────────────────────────────────────────
|
package/lib/otel/hooks/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @module otel/hooks
|
|
9
9
|
*/
|
|
10
10
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
11
|
-
import {
|
|
11
|
+
import { getOtelConsumeUrl } from '../../state/substates/CoreState.js';
|
|
12
12
|
import { buildOtelWebSocketUrl, resolveOtelAuth } from '../auth.js';
|
|
13
13
|
// ── Global 401 handler ──────────────────────────────────────────────
|
|
14
14
|
let _onUnauthorized = null;
|
|
@@ -189,7 +189,7 @@ function normalizeMetric(raw) {
|
|
|
189
189
|
// ── useOtelTraces ───────────────────────────────────────────────────
|
|
190
190
|
/** Fetch a list of traces / spans from the OTEL service. */
|
|
191
191
|
export function useOtelTraces(options) {
|
|
192
|
-
const { token, baseUrl =
|
|
192
|
+
const { token, baseUrl = getOtelConsumeUrl(), serviceName, limit = 50, autoRefreshMs, } = options;
|
|
193
193
|
const [traces, setTraces] = useState([]);
|
|
194
194
|
const [loading, setLoading] = useState(true);
|
|
195
195
|
const [error, setError] = useState(null);
|
|
@@ -246,7 +246,7 @@ export function useOtelTrace(options) {
|
|
|
246
246
|
// ── useOtelLogs ─────────────────────────────────────────────────────
|
|
247
247
|
/** Fetch log records from the OTEL service. */
|
|
248
248
|
export function useOtelLogs(options) {
|
|
249
|
-
const { token, baseUrl =
|
|
249
|
+
const { token, baseUrl = getOtelConsumeUrl(), serviceName, severity, traceId, limit = 100, autoRefreshMs, } = options;
|
|
250
250
|
const [logs, setLogs] = useState([]);
|
|
251
251
|
const [loading, setLoading] = useState(true);
|
|
252
252
|
const [error, setError] = useState(null);
|
|
@@ -283,7 +283,7 @@ export function useOtelLogs(options) {
|
|
|
283
283
|
// ── useOtelMetrics ──────────────────────────────────────────────────
|
|
284
284
|
/** Fetch metrics from the OTEL service. */
|
|
285
285
|
export function useOtelMetrics(options) {
|
|
286
|
-
const { token, baseUrl =
|
|
286
|
+
const { token, baseUrl = getOtelConsumeUrl(), serviceName, metricName, limit = 50, autoRefreshMs, } = options;
|
|
287
287
|
const [metrics, setMetrics] = useState([]);
|
|
288
288
|
const [loading, setLoading] = useState(true);
|
|
289
289
|
const [error, setError] = useState(null);
|
|
@@ -318,7 +318,7 @@ export function useOtelMetrics(options) {
|
|
|
318
318
|
// ── useOtelServices ─────────────────────────────────────────────────
|
|
319
319
|
/** Fetch list of observed service names. */
|
|
320
320
|
export function useOtelServices(options) {
|
|
321
|
-
const { token, baseUrl =
|
|
321
|
+
const { token, baseUrl = getOtelConsumeUrl() } = options;
|
|
322
322
|
const [services, setServices] = useState([]);
|
|
323
323
|
const [loading, setLoading] = useState(true);
|
|
324
324
|
useEffect(() => {
|
|
@@ -437,7 +437,7 @@ export function useOtelQuery(options) {
|
|
|
437
437
|
* @returns `{ connected, error, close }` – reactive connection state.
|
|
438
438
|
*/
|
|
439
439
|
export function useOtelWebSocket(options) {
|
|
440
|
-
const { baseUrl =
|
|
440
|
+
const { baseUrl = getOtelConsumeUrl(), token, userUid, autoReconnect = true, reconnectDelayMs = 3000, callbacks, } = options;
|
|
441
441
|
const [connected, setConnected] = useState(false);
|
|
442
442
|
const [error, setError] = useState(null);
|
|
443
443
|
const wsRef = useRef(null);
|
|
@@ -15,7 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
15
15
|
import { useState, useCallback, useMemo, useEffect, useRef, } from 'react';
|
|
16
16
|
import { Box, Text, Button, Label } from '@primer/react';
|
|
17
17
|
import { GitBranchIcon, ClockIcon } from '@primer/octicons-react';
|
|
18
|
-
import {
|
|
18
|
+
import { getOtelConsumeUrl } from '../../state/substates/CoreState.js';
|
|
19
19
|
import { useOtelTraces, useOtelTrace, useOtelLogs, useOtelMetrics, useOtelServices, useOtelWebSocket, } from '../hooks/index.js';
|
|
20
20
|
import { OtelSearchBar } from './OtelSearchBar.js';
|
|
21
21
|
import { OtelTimelineRangeSlider } from './OtelTimelineRangeSlider.js';
|
|
@@ -51,7 +51,7 @@ function buildHistogram(timestamps, start, end, buckets) {
|
|
|
51
51
|
}));
|
|
52
52
|
}
|
|
53
53
|
// ── OtelLive ────────────────────────────────────────────────────────
|
|
54
|
-
export const OtelLive = ({ baseUrl =
|
|
54
|
+
export const OtelLive = ({ baseUrl = getOtelConsumeUrl(), wsBaseUrl, token, autoRefreshMs = 5000, defaultSignal = 'traces', limit = 200, onSignalRef, }) => {
|
|
55
55
|
// ── state ──
|
|
56
56
|
const [signal, setSignalState] = useState(() => {
|
|
57
57
|
try {
|
|
@@ -22,4 +22,15 @@ export type DatalayerCoreState = {
|
|
|
22
22
|
export declare const coreStore: import("zustand").StoreApi<DatalayerCoreState>;
|
|
23
23
|
export declare function useCoreStore(): DatalayerCoreState;
|
|
24
24
|
export declare function useCoreStore<T>(selector: (state: DatalayerCoreState) => T): T;
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the OTEL base URL used to *consume* (read/query) telemetry.
|
|
27
|
+
*
|
|
28
|
+
* Prefers `otelInUrl` when set, otherwise falls back to `otelUrl`. This allows
|
|
29
|
+
* local development to fetch telemetry from a remote (e.g. production) OTEL
|
|
30
|
+
* service even when the local runtime exports telemetry to a different endpoint.
|
|
31
|
+
*
|
|
32
|
+
* Mirrors the `DATALAYER_OTEL_IN_URL` (consume) / `DATALAYER_OTEL_URL` (export)
|
|
33
|
+
* split on the backend/build side.
|
|
34
|
+
*/
|
|
35
|
+
export declare const getOtelConsumeUrl: () => string;
|
|
25
36
|
export default useCoreStore;
|
|
@@ -24,6 +24,10 @@ let initialConfiguration = {
|
|
|
24
24
|
aiInferenceUrl: 'https://prod1.datalayer.run',
|
|
25
25
|
mcpServersUrl: 'https://prod1.datalayer.run',
|
|
26
26
|
otelUrl: 'https://prod1.datalayer.run',
|
|
27
|
+
// Defaults to prod so telemetry is consumed from the production OTEL service
|
|
28
|
+
// even when runtimes export elsewhere (see `getOtelConsumeUrl` and the
|
|
29
|
+
// `DATALAYER_OTEL_IN_URL` env var). Override to '' to consume from `otelUrl`.
|
|
30
|
+
otelInUrl: 'https://prod1.datalayer.run',
|
|
27
31
|
growthUrl: 'https://prod1.datalayer.run',
|
|
28
32
|
inboundsUrl: 'https://prod1.datalayer.run',
|
|
29
33
|
successUrl: 'https://prod1.datalayer.run',
|
|
@@ -97,4 +101,18 @@ export const coreStore = createStore((set, get) => ({
|
|
|
97
101
|
export function useCoreStore(selector) {
|
|
98
102
|
return useStore(coreStore, selector);
|
|
99
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolve the OTEL base URL used to *consume* (read/query) telemetry.
|
|
106
|
+
*
|
|
107
|
+
* Prefers `otelInUrl` when set, otherwise falls back to `otelUrl`. This allows
|
|
108
|
+
* local development to fetch telemetry from a remote (e.g. production) OTEL
|
|
109
|
+
* service even when the local runtime exports telemetry to a different endpoint.
|
|
110
|
+
*
|
|
111
|
+
* Mirrors the `DATALAYER_OTEL_IN_URL` (consume) / `DATALAYER_OTEL_URL` (export)
|
|
112
|
+
* split on the backend/build side.
|
|
113
|
+
*/
|
|
114
|
+
export const getOtelConsumeUrl = () => {
|
|
115
|
+
const cfg = coreStore.getState().configuration;
|
|
116
|
+
return cfg.otelInUrl || cfg.otelUrl;
|
|
117
|
+
};
|
|
100
118
|
export default useCoreStore;
|
|
@@ -7,6 +7,8 @@ export type DatasourcesProps = {
|
|
|
7
7
|
principalUid?: string;
|
|
8
8
|
/** Optional principal kind used to scope datasource reads. */
|
|
9
9
|
principalKind?: 'personal' | 'organization' | 'team';
|
|
10
|
+
/** Show local inline spinner in empty state while loading. */
|
|
11
|
+
showInlineLoadingIndicator?: boolean;
|
|
10
12
|
};
|
|
11
|
-
export declare const Datasources: ({ newDatasourceRoute, datasourcesListRoute, principalUid, principalKind, }?: DatasourcesProps) => import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export declare const Datasources: ({ newDatasourceRoute, datasourcesListRoute, principalUid, principalKind, showInlineLoadingIndicator, }?: DatasourcesProps) => import("react/jsx-runtime").JSX.Element;
|
|
12
14
|
export default Datasources;
|
|
@@ -10,26 +10,26 @@ import { DatabaseIcon } from '@primer/octicons-react';
|
|
|
10
10
|
import { Box } from '@datalayer/primer-addons';
|
|
11
11
|
import { EditIcon } from '@datalayer/icons-react';
|
|
12
12
|
import { useCache, useNavigate } from '../../hooks/index.js';
|
|
13
|
-
const DatasourcesTable = ({ datasourcesListRoute, principalUid, principalKind, }) => {
|
|
13
|
+
const DatasourcesTable = ({ datasourcesListRoute, principalUid, principalKind, showInlineLoadingIndicator = true, }) => {
|
|
14
14
|
const { useDatasources } = useCache();
|
|
15
15
|
const datasourcesQuery = useDatasources({ principalUid, principalKind });
|
|
16
16
|
const navigate = useNavigate();
|
|
17
17
|
const [datasources, setDatasources] = useState([]);
|
|
18
|
-
const showInitialSpinner = datasources.length === 0
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
const showInitialSpinner = datasources.length === 0 &&
|
|
19
|
+
(datasourcesQuery.isLoading ||
|
|
20
|
+
datasourcesQuery.isFetching ||
|
|
21
|
+
!Array.isArray(datasourcesQuery.data));
|
|
22
22
|
useEffect(() => {
|
|
23
23
|
if (datasourcesQuery.data) {
|
|
24
24
|
setDatasources(datasourcesQuery.data || []);
|
|
25
25
|
}
|
|
26
26
|
}, [datasourcesQuery.data]);
|
|
27
|
-
return datasources.length === 0 ? (_jsx(Blankslate, { border: true, spacious: true, children: showInitialSpinner ? (_jsx(Box, { sx: {
|
|
27
|
+
return datasources.length === 0 ? (_jsx(Blankslate, { border: true, spacious: true, children: showInitialSpinner && showInlineLoadingIndicator ? (_jsx(Box, { sx: {
|
|
28
28
|
display: 'flex',
|
|
29
29
|
alignItems: 'center',
|
|
30
30
|
justifyContent: 'center',
|
|
31
31
|
minHeight: '40px',
|
|
32
|
-
}, children: _jsx(Spinner, {}) })) : (_jsxs(_Fragment, { children: [_jsx(Blankslate.Heading, { children: "Datasources" }), _jsx(Blankslate.Description, { children: _jsx(Text, { sx: { textAlign: 'center' }, children: "No Datasources found." }) })] })) })) : (_jsx(Table.Container, { children: _jsx(DataTable, { "aria-labelledby": "teams", "aria-describedby": "teams-subtitle", data: datasources, columns: [
|
|
32
|
+
}, children: _jsx(Spinner, {}) })) : showInitialSpinner ? (_jsxs(_Fragment, { children: [_jsx(Blankslate.Heading, { children: "Datasources" }), _jsx(Blankslate.Description, { children: _jsx(Text, { sx: { textAlign: 'center' }, children: "Loading datasources..." }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Blankslate.Heading, { children: "Datasources" }), _jsx(Blankslate.Description, { children: _jsx(Text, { sx: { textAlign: 'center' }, children: "No Datasources found." }) })] })) })) : (_jsx(Table.Container, { children: _jsx(DataTable, { "aria-labelledby": "teams", "aria-describedby": "teams-subtitle", data: datasources, columns: [
|
|
33
33
|
// @ts-ignore
|
|
34
34
|
{
|
|
35
35
|
header: 'Type',
|
|
@@ -54,7 +54,7 @@ const DatasourcesTable = ({ datasourcesListRoute, principalUid, principalKind, }
|
|
|
54
54
|
},
|
|
55
55
|
] }) }));
|
|
56
56
|
};
|
|
57
|
-
export const Datasources = ({ newDatasourceRoute = '/new/datasource', datasourcesListRoute, principalUid, principalKind, } = {}) => {
|
|
57
|
+
export const Datasources = ({ newDatasourceRoute = '/new/datasource', datasourcesListRoute, principalUid, principalKind, showInlineLoadingIndicator = true, } = {}) => {
|
|
58
58
|
const navigate = useNavigate();
|
|
59
59
|
return (_jsx(PageLayout, { containerWidth: "full", padding: "normal", style: { overflow: 'visible', minHeight: 'calc(100vh - 45px)' }, children: _jsx(PageLayout.Content, { children: _jsxs(Box, { sx: { maxWidth: 960, mx: 'auto', width: '100%' }, children: [_jsxs(Box, { sx: {
|
|
60
60
|
display: 'flex',
|
|
@@ -63,6 +63,6 @@ export const Datasources = ({ newDatasourceRoute = '/new/datasource', datasource
|
|
|
63
63
|
gap: 3,
|
|
64
64
|
flexWrap: 'wrap',
|
|
65
65
|
mb: 4,
|
|
66
|
-
}, children: [_jsxs(Box, { children: [_jsx(Heading, { as: "h2", sx: { fontSize: 3, mb: 1 }, children: "Datasources" }), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: "Configure external data providers available to your workspaces and agents." })] }), _jsx(Button, { size: "small", variant: "primary", leadingVisual: DatabaseIcon, onClick: e => navigate(newDatasourceRoute, e), children: "New datasource" })] }), _jsx(DatasourcesTable, { datasourcesListRoute: datasourcesListRoute, principalUid: principalUid, principalKind: principalKind })] }) }) }));
|
|
66
|
+
}, children: [_jsxs(Box, { children: [_jsx(Heading, { as: "h2", sx: { fontSize: 3, mb: 1 }, children: "Datasources" }), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: "Configure external data providers available to your workspaces and agents." })] }), _jsx(Button, { size: "small", variant: "primary", leadingVisual: DatabaseIcon, onClick: e => navigate(newDatasourceRoute, e), children: "New datasource" })] }), _jsx(DatasourcesTable, { datasourcesListRoute: datasourcesListRoute, principalUid: principalUid, principalKind: principalKind, showInlineLoadingIndicator: showInlineLoadingIndicator })] }) }) }));
|
|
67
67
|
};
|
|
68
68
|
export default Datasources;
|