@datalayer/core 1.2.27 → 1.2.29

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 (80) hide show
  1. package/lib/api/evals/__tests__/external-definition.golden.json +110 -0
  2. package/lib/api/evals/client.d.ts +13 -1
  3. package/lib/api/evals/client.js +13 -0
  4. package/lib/api/evals/types.d.ts +168 -1
  5. package/lib/api/mcp/agentAccess.d.ts +10 -0
  6. package/lib/api/mcp/agentAccess.js +28 -0
  7. package/lib/api/mcp/generated.js +20 -0
  8. package/lib/api/mcp/index.d.ts +1 -0
  9. package/lib/api/mcp/index.js +1 -0
  10. package/lib/api/mcp/observability.d.ts +1 -1
  11. package/lib/api/mcp/observability.js +2 -0
  12. package/lib/api/mcp/operations.d.ts +28 -10
  13. package/lib/api/mcp/tasks.d.ts +9 -1
  14. package/lib/api/otel/firewall.d.ts +74 -0
  15. package/lib/api/otel/firewall.js +42 -0
  16. package/lib/api/otel/index.d.ts +1 -0
  17. package/lib/api/otel/index.js +1 -0
  18. package/lib/cli-login.js +2 -2
  19. package/lib/components/anonymous/AnonymousKeyExpired.js +15 -3
  20. package/lib/components/index.d.ts +1 -0
  21. package/lib/components/index.js +1 -0
  22. package/lib/components/mcp/ScopeList.js +10 -1
  23. package/lib/components/principal/PrincipalSwitcherMenu.js +4 -4
  24. package/lib/components/sharing/ShareAccessComponent.js +33 -8
  25. package/lib/components/toast/ToastPanel.d.ts +11 -0
  26. package/lib/components/toast/ToastPanel.js +42 -0
  27. package/lib/components/toast/index.d.ts +2 -0
  28. package/lib/components/toast/index.js +6 -0
  29. package/lib/components/toast/toastQueue.d.ts +35 -0
  30. package/lib/components/toast/toastQueue.js +47 -0
  31. package/lib/hooks/index.d.ts +1 -0
  32. package/lib/hooks/index.js +1 -0
  33. package/lib/hooks/useCache.d.ts +4 -2
  34. package/lib/hooks/useCache.js +13 -4
  35. package/lib/hooks/useDatalayer.js +12 -16
  36. package/lib/hooks/useKeptScroll.d.ts +6 -0
  37. package/lib/hooks/useKeptScroll.js +54 -0
  38. package/lib/hooks/useMcp.d.ts +2 -2
  39. package/lib/hooks/useMcp.js +5 -2
  40. package/lib/hooks/useNavigate.d.ts +16 -2
  41. package/lib/hooks/useNavigate.js +65 -5
  42. package/lib/hooks/useToast.d.ts +6 -0
  43. package/lib/hooks/useToast.js +40 -1
  44. package/lib/models/McpAgentAccess.d.ts +95 -0
  45. package/lib/models/McpAgentAccess.js +5 -0
  46. package/lib/models/Usage.d.ts +13 -0
  47. package/lib/models/Usage.js +5 -0
  48. package/lib/models/index.d.ts +1 -0
  49. package/lib/models/index.js +1 -0
  50. package/lib/otel/client/OtelClient.d.ts +56 -0
  51. package/lib/otel/client/OtelClient.js +25 -0
  52. package/lib/otel/client/index.d.ts +1 -1
  53. package/lib/otel/index.d.ts +1 -1
  54. package/lib/otel/views/OtelLive.js +3 -3
  55. package/lib/otel/views/OtelLiveSpans.js +2 -2
  56. package/lib/otel/views/OtelLogsList.js +3 -3
  57. package/lib/otel/views/OtelMetricsChart.d.ts +13 -0
  58. package/lib/otel/views/OtelMetricsChart.js +109 -26
  59. package/lib/otel/views/OtelSpanDetail.js +3 -3
  60. package/lib/otel/views/OtelSqlView.js +4 -4
  61. package/lib/otel/views/OtelSystemView.js +2 -2
  62. package/lib/otel/views/OtelTimelineRangeSlider.js +2 -2
  63. package/lib/otel/views/OtelTracesList.js +2 -2
  64. package/lib/state/substates/LayoutState.js +24 -3
  65. package/lib/state/substates/NavigationState.d.ts +11 -3
  66. package/lib/state/substates/NavigationState.js +48 -5
  67. package/lib/views/mcp/AuditLog.js +1 -1
  68. package/lib/views/mcp/ConnectedAgents.d.ts +1 -1
  69. package/lib/views/mcp/ConnectedAgents.js +13 -10
  70. package/lib/views/mcp/McpDashboard.d.ts +1 -1
  71. package/lib/views/mcp/McpDashboard.js +6 -6
  72. package/lib/views/mcp/McpHome.d.ts +3 -1
  73. package/lib/views/mcp/McpHome.js +19 -9
  74. package/lib/views/mcp/McpObservability.d.ts +22 -3
  75. package/lib/views/mcp/McpObservability.js +65 -17
  76. package/lib/views/mcp/Policies.js +1 -1
  77. package/lib/views/mcp/RunDetail.js +2 -2
  78. package/lib/views/mcp/types.d.ts +8 -4
  79. package/lib/views/otel/OtelHeader.js +2 -2
  80. package/package.json +2 -2
@@ -0,0 +1,74 @@
1
+ /** One connection the firewall decided. */
2
+ export interface FirewallConnection {
3
+ /** Unix nanoseconds, which a JavaScript number holds only approximately. */
4
+ time_unix_nano: number;
5
+ decision: 'allow' | 'refuse';
6
+ /** The rule that decided, e.g. `spec:*`, `base:refused`, `spec:*_non-public`. */
7
+ rule: string;
8
+ /** `address:port`. */
9
+ destination: string;
10
+ /** The host the address was resolved from, when the firewall knows it. */
11
+ host: string;
12
+ port: number | null;
13
+ /** `tcp`, `udp` or `icmp`. */
14
+ protocol: string;
15
+ source: string;
16
+ /** Connections folded into this row, besides itself. */
17
+ repeated: number;
18
+ }
19
+ /** One apply of the runtime's rule. */
20
+ export interface FirewallApplied {
21
+ time_unix_nano: number;
22
+ /** `start`, `configure`, `refresh` or `apply`. */
23
+ reason: string;
24
+ revision: string;
25
+ /** The egress entries applied: hosts, CIDRs, `*` for any public address. */
26
+ egress: string[];
27
+ ingress: string[];
28
+ /** The base layer's allowances, as the firewall wrote them. */
29
+ base?: unknown;
30
+ /** How many rules were compiled. */
31
+ rules: number | null;
32
+ /** Hosts that resolved to nothing. */
33
+ unresolved: string[];
34
+ }
35
+ /** A runtime's firewall over a window. */
36
+ export interface RuntimeFirewall {
37
+ success: boolean;
38
+ runtime_uid: string;
39
+ window: {
40
+ since_unix_nano: number;
41
+ days: number;
42
+ };
43
+ /** Why the firewall did not take the runtime's rule, as its record says; empty when it holds. */
44
+ refused: string;
45
+ /** Latest first. None when the runtime has no firewall, or it has not reported yet. */
46
+ applied: FirewallApplied[];
47
+ /** The latest connections, latest first. */
48
+ connections: FirewallConnection[];
49
+ /** Over the whole window, each row counting itself and the connections folded into it. */
50
+ counts: {
51
+ allowed: number;
52
+ refused: number;
53
+ rows: number;
54
+ };
55
+ /** The window held more connection rows than were counted. */
56
+ truncated: boolean;
57
+ }
58
+ export interface RuntimeFirewallOptions {
59
+ /** How many days back, 1 to 30; 7 by default. */
60
+ days?: number;
61
+ /** How many connections to return, latest first; 200 by default. */
62
+ limit?: number;
63
+ }
64
+ /**
65
+ * A runtime's firewall: the rule applied, its latest connections and their counts.
66
+ *
67
+ * Answers 404 when the runtime is not the caller's (or no longer runs).
68
+ *
69
+ * @param token - Authentication token.
70
+ * @param runtimeUid - The runtime's uid.
71
+ * @param options - The window and how many connections.
72
+ * @param baseUrl - The OTEL service.
73
+ */
74
+ export declare const getRuntimeFirewall: (token: string, runtimeUid: string, options?: RuntimeFirewallOptions, baseUrl?: string) => Promise<RuntimeFirewall>;
@@ -0,0 +1,42 @@
1
+ /*
2
+ * Copyright (c) 2023-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * A runtime's network firewall, read back from Datalayer's observability.
7
+ *
8
+ * The runtime pod's `shield-firewall` sidecar (agent-shield's firewall)
9
+ * exports each rule it applies and each connection it allows or refuses as
10
+ * a span. The OTEL service reads them for the runtime's owner only: it asks
11
+ * the runtimes service for the runtime with the caller's own token first.
12
+ *
13
+ * @module api/otel/firewall
14
+ */
15
+ import { requestDatalayerAPI } from '../DatalayerApi.js';
16
+ import { API_BASE_PATHS, DEFAULT_SERVICE_URLS } from '../constants.js';
17
+ import { validateToken } from '../utils/validation.js';
18
+ /**
19
+ * A runtime's firewall: the rule applied, its latest connections and their counts.
20
+ *
21
+ * Answers 404 when the runtime is not the caller's (or no longer runs).
22
+ *
23
+ * @param token - Authentication token.
24
+ * @param runtimeUid - The runtime's uid.
25
+ * @param options - The window and how many connections.
26
+ * @param baseUrl - The OTEL service.
27
+ */
28
+ export const getRuntimeFirewall = async (token, runtimeUid, options = {}, baseUrl = DEFAULT_SERVICE_URLS.OTEL) => {
29
+ validateToken(token);
30
+ const params = new URLSearchParams();
31
+ if (options.days)
32
+ params.set('days', String(options.days));
33
+ if (options.limit)
34
+ params.set('limit', String(options.limit));
35
+ const queryString = params.toString();
36
+ const url = `${baseUrl}${API_BASE_PATHS.OTEL}/firewall/runtimes/${encodeURIComponent(runtimeUid)}${queryString ? `?${queryString}` : ''}`;
37
+ return requestDatalayerAPI({
38
+ url,
39
+ method: 'GET',
40
+ token,
41
+ });
42
+ };
@@ -11,3 +11,4 @@ export * as logs from './logs';
11
11
  export * as services from './services';
12
12
  export * as query from './query';
13
13
  export * as dashboards from './dashboards';
14
+ export * as firewall from './firewall';
@@ -15,3 +15,4 @@ export * as logs from './logs.js';
15
15
  export * as services from './services.js';
16
16
  export * as query from './query.js';
17
17
  export * as dashboards from './dashboards.js';
18
+ export * as firewall from './firewall.js';
package/lib/cli-login.js CHANGED
@@ -9,7 +9,7 @@ import { Button, Spinner, Text } from '@primer/react';
9
9
  import { ShieldLockIcon } from '@primer/octicons-react';
10
10
  import { Box, DatalayerThemeProvider, themeConfigs, createThemeStore, } from '@datalayer/primer-addons';
11
11
  import { SvgLinesLogo, SvgUsecasesHero } from '@datalayer/design';
12
- import { AppearanceControlsWithStore } from '@datalayer/primer-addons/lib/components/appearance';
12
+ import { AppearanceControls } from '@datalayer/primer-addons/lib/components/appearance';
13
13
  import { ConfettiSuccess } from './components/confetti/index.js';
14
14
  import { formatFriendlyHandle } from './utils/Handles.js';
15
15
  import { SignInSimple } from './views/iam/SignInSimple.js';
@@ -90,7 +90,7 @@ function SignInCLIApp() {
90
90
  setState('signin');
91
91
  });
92
92
  }, [finalizeCliAuthentication]);
93
- const renderLayout = (content) => (_jsx(DatalayerThemeProvider, { colorMode: colorMode, theme: cfg.primerTheme, themeStyles: cfg.themeStyles, children: _jsxs(Box, { minHeight: "100vh", bg: "canvas.default", color: "fg.default", position: "relative", children: [_jsx(Box, { position: "fixed", top: 0, right: 0, zIndex: 20, p: 3, display: "flex", justifyContent: "flex-end", children: _jsx(AppearanceControlsWithStore, { useStore: useCliThemeStore }) }), content] }) }));
93
+ const renderLayout = (content) => (_jsx(DatalayerThemeProvider, { colorMode: colorMode, theme: cfg.primerTheme, themeStyles: cfg.themeStyles, children: _jsxs(Box, { minHeight: "100vh", bg: "canvas.default", color: "fg.default", position: "relative", children: [_jsx(Box, { position: "fixed", top: 0, right: 0, zIndex: 20, p: 3, display: "flex", justifyContent: "flex-end", children: _jsx(AppearanceControls, { layout: "inline", useStore: useCliThemeStore }) }), content] }) }));
94
94
  if (state === 'checking') {
95
95
  return renderLayout(_jsx(Box, { display: "flex", alignItems: "center", justifyContent: "center", minHeight: "100vh", children: _jsx(Spinner, {}) }));
96
96
  }
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback } from 'react';
3
- import { Text } from '@primer/react';
3
+ import { Button, Text } from '@primer/react';
4
4
  import { Box } from '@datalayer/primer-addons';
5
- import { KeyIcon } from '@primer/octicons-react';
5
+ import { KeyIcon, SyncIcon } from '@primer/octicons-react';
6
6
  import { SignInSimple } from '../../views/iam/SignInSimple.js';
7
7
  import { useCoreStore, iamStore } from '../../state/substates/index.js';
8
8
  export function AnonymousKeyExpired({ agentName, sandboxStillRuns = false, temporary = true, onSignedIn, onSignUp, }) {
@@ -35,7 +35,19 @@ export function AnonymousKeyExpired({ agentName, sandboxStillRuns = false, tempo
35
35
  ? `${agentName ?? 'The agent'} has used the time this key allows. Sign in to point your own agent at it, use your own data, and drop the time limit.`
36
36
  : `${agentName ?? 'The agent'} stopped because your session ran out.`, sandboxStillRuns
37
37
  ? ' Your notebook is still running in this page.'
38
- : ''] }), _jsx(SignInSimple
38
+ : ''] }), temporary && (
39
+ /*
40
+ Or another trial key (the user, 6 November 2026): a new page gets
41
+ a new one, so the demo can go on without an account. The button
42
+ reloads the browser rather than the chat, since the key is minted
43
+ as the page opens; what was said is gone with it, and the line
44
+ says so.
45
+ */
46
+ _jsxs(Box, { display: "flex", flexDirection: "column", alignItems: "center", gap: 2, mb: 3, "data-anonymous-key-refresh": "", children: [_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted', textAlign: 'center' }, children: "Or keep trying without an account: refreshing the page gives this demo a new key, and starts the conversation over." }), _jsx(Button, { leadingVisual: SyncIcon, onClick: () => {
47
+ if (typeof window !== 'undefined') {
48
+ window.location.reload();
49
+ }
50
+ }, children: "Refresh the page for a new key" })] })), _jsx(SignInSimple
39
51
  // The panel above has already said what happened and why; the form's
40
52
  // own hero would say it a second time, in a bigger font.
41
53
  , {
@@ -8,3 +8,4 @@ export * from './sharing';
8
8
  export * from './sparklines';
9
9
  export * from './time';
10
10
  export * from './timeline';
11
+ export * from './toast';
@@ -12,3 +12,4 @@ export * from './sharing/index.js';
12
12
  export * from './sparklines/index.js';
13
13
  export * from './time/index.js';
14
14
  export * from './timeline/index.js';
15
+ export * from './toast/index.js';
@@ -9,6 +9,15 @@ export const ScopeList = ({ scopes, max = 3 }) => {
9
9
  }
10
10
  const shown = scopes.slice(0, max);
11
11
  const rest = scopes.length - shown.length;
12
- return (_jsxs(Box, { display: "flex", gap: 1, flexWrap: "wrap", alignItems: "center", children: [shown.map(scope => (_jsx(Label, { size: "small", variant: "secondary", children: scope }, scope))), rest > 0 && (_jsxs(Text, { as: "span", title: scopes.join('\n'), "aria-label": `and ${rest} more: ${scopes.slice(max).join(', ')}`, sx: { fontSize: 0, color: 'fg.muted' }, children: ["+", rest] }))] }));
12
+ return (_jsxs(Box, { display: "flex", gap: 1, flexWrap: "wrap", alignItems: "center", minWidth: 0, maxWidth: "100%", children: [shown.map(scope => (
13
+ // A long scope is cut with an ellipsis and said in full on hover,
14
+ // never drawn past its cell into the next column.
15
+ _jsx(Label, { size: "small", variant: "secondary", title: scope, sx: {
16
+ maxWidth: '100%',
17
+ overflow: 'hidden',
18
+ textOverflow: 'ellipsis',
19
+ whiteSpace: 'nowrap',
20
+ display: 'inline-block',
21
+ }, children: scope }, scope))), rest > 0 && (_jsxs(Text, { as: "span", title: scopes.join('\n'), "aria-label": `and ${rest} more: ${scopes.slice(max).join(', ')}`, sx: { fontSize: 0, color: 'fg.muted' }, children: ["+", rest] }))] }));
13
22
  };
14
23
  export default ScopeList;
@@ -204,7 +204,7 @@ export function PrincipalSwitcherMenu({ maxLabelChars = 48, fullWidth = true, sh
204
204
  outline: '2px solid',
205
205
  outlineColor: 'accent.emphasis',
206
206
  outlineOffset: '2px',
207
- }, children: [_jsx(Box, { display: "inline-flex", alignItems: "center", color: "fg.muted", flexShrink: 0, children: selectedPrincipalKind === 'organization' ? (_jsx(PeopleIcon, { size: 16 })) : selectedPrincipalKind === 'team' ? (_jsx(OrganizationIcon, { size: 16 })) : (_jsx(PersonIcon, { size: 16 })) }), _jsx(Box, { minWidth: 0, flex: 1, children: _jsx(Text, { sx: {
207
+ }, children: [_jsx(Box, { display: "inline-flex", alignItems: "center", color: "fg.muted", flexShrink: 0, children: selectedPrincipalKind === 'organization' ? (_jsx(OrganizationIcon, { size: 16 })) : selectedPrincipalKind === 'team' ? (_jsx(PeopleIcon, { size: 16 })) : (_jsx(PersonIcon, { size: 16 })) }), _jsx(Box, { minWidth: 0, flex: 1, children: _jsx(Text, { sx: {
208
208
  display: 'block',
209
209
  color: 'accent.fg',
210
210
  fontWeight: 'semibold',
@@ -216,7 +216,7 @@ export function PrincipalSwitcherMenu({ maxLabelChars = 48, fullWidth = true, sh
216
216
  }, children: selectedPrincipalLabelClosed }) }), isPlatformAdmin && isCurrentUserPrincipal ? (_jsx(Box, { flexShrink: 0, display: "inline-flex", alignItems: "center", children: _jsx(AdminLabel, {}) })) : null, _jsx(Box, { display: "inline-flex", alignItems: "center", color: "fg.muted", flexShrink: 0, ml: isPlatformAdmin && isCurrentUserPrincipal ? 1 : 0, children: _jsx(TriangleDownIcon, { size: 12 }) })] })) : (_jsx(Button, { variant: "invisible", size: "small", "aria-label": "Switch principal", trailingVisual: TriangleDownIcon, sx: {
217
217
  maxWidth: fullWidth ? '100%' : ['200px', '260px', '360px'],
218
218
  width: fullWidth ? '100%' : 'auto',
219
- }, children: _jsxs(Box, { display: "inline-flex", alignItems: "center", gap: 1, minWidth: 0, children: [_jsx(Box, { display: "inline-flex", alignItems: "center", color: "fg.muted", flexShrink: 0, children: selectedPrincipalKind === 'organization' ? (_jsx(PeopleIcon, { size: 16 })) : selectedPrincipalKind === 'team' ? (_jsx(OrganizationIcon, { size: 16 })) : (_jsx(PersonIcon, { size: 16 })) }), _jsx(Box, { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", children: _jsx(Text, { sx: {
219
+ }, children: _jsxs(Box, { display: "inline-flex", alignItems: "center", gap: 1, minWidth: 0, children: [_jsx(Box, { display: "inline-flex", alignItems: "center", color: "fg.muted", flexShrink: 0, children: selectedPrincipalKind === 'organization' ? (_jsx(OrganizationIcon, { size: 16 })) : selectedPrincipalKind === 'team' ? (_jsx(PeopleIcon, { size: 16 })) : (_jsx(PersonIcon, { size: 16 })) }), _jsx(Box, { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", children: _jsx(Text, { sx: {
220
220
  color: 'accent.fg',
221
221
  fontWeight: 'semibold',
222
222
  }, children: selectedPrincipalLabelClosed }) }), isPlatformAdmin && isCurrentUserPrincipal ? (_jsx(AdminLabel, {})) : null] }) })) }), _jsx(ActionMenu.Overlay, { width: "large", children: _jsxs(ActionList, { selectionVariant: "single", children: [_jsxs(ActionList.Group, { children: [_jsx(ActionList.GroupHeading, { children: "Personal" }), _jsxs(ActionList.Item, { disabled: isCurrentUserPrincipal, selected: isCurrentUserPrincipal, sx: isCurrentUserPrincipal ? selectedItemSx : undefined, onSelect: () => {
@@ -242,7 +242,7 @@ export function PrincipalSwitcherMenu({ maxLabelChars = 48, fullWidth = true, sh
242
242
  if (organizationUid && organization.handle) {
243
243
  selectOrganization(organizationUid, organization.handle);
244
244
  }
245
- }, children: [_jsx(ActionList.LeadingVisual, { children: _jsx(PeopleIcon, {}) }), _jsxs(Box, { as: "span", title: displayHandleText(organization.handle), display: "block", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", children: [String(organization?.display_name ||
245
+ }, children: [_jsx(ActionList.LeadingVisual, { children: _jsx(OrganizationIcon, {}) }), _jsxs(Box, { as: "span", title: displayHandleText(organization.handle), display: "block", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", children: [String(organization?.display_name ||
246
246
  organization?.displayName ||
247
247
  organization?.name ||
248
248
  organization?.handle ||
@@ -258,7 +258,7 @@ export function PrincipalSwitcherMenu({ maxLabelChars = 48, fullWidth = true, sh
258
258
  return;
259
259
  }
260
260
  selectTeam(team, orgHandle);
261
- }, children: [_jsx(ActionList.LeadingVisual, { children: _jsx(OrganizationIcon, {}) }), _jsxs(Box, { as: "span", title: `${displayHandleText(orgHandle)}/${displayHandleText(team.handle, { withAt: false })}`, display: "block", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", children: [String(team.displayName ||
261
+ }, children: [_jsx(ActionList.LeadingVisual, { children: _jsx(PeopleIcon, {}) }), _jsxs(Box, { as: "span", title: `${displayHandleText(orgHandle)}/${displayHandleText(team.handle, { withAt: false })}`, display: "block", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", children: [String(team.displayName ||
262
262
  team.name ||
263
263
  team.handle ||
264
264
  'Team').trim(), _jsxs(Box, { as: "span", color: "fg.muted", children: [' ', "\u00B7 ", _jsx(DisplayHandle, { handle: orgHandle }), "/", _jsx(DisplayHandle, { handle: team.handle, withAt: false })] })] })] }, team.uid));
@@ -405,8 +405,18 @@ export function ShareAccessComponent({ isOpen, requestUrl, transport, resourceLa
405
405
  const [isSearchOverlayOpen, setIsSearchOverlayOpen] = useState(false);
406
406
  const [sharingAccessMessage, setSharingAccessMessage] = useState(null);
407
407
  const [isSharingAccessConfirmed, setIsSharingAccessConfirmed] = useState(false);
408
+ // Why the sharing could not be read, said where the sharing would be: a
409
+ // toast is gone in a few seconds and leaves the form disabled, unexplained.
410
+ const [sharingLoadError, setSharingLoadError] = useState(null);
408
411
  // ----- Refs -----
409
- const hasLoadedForOpenRef = useRef(false);
412
+ // What the sharing was last asked from (the transport, or the URL): a load
413
+ // is started once per source, and started again when the source changes or
414
+ // when the load in flight was dropped before it answered.
415
+ const loadedSourceRef = useRef(null);
416
+ const levelsRef = useRef(levels);
417
+ levelsRef.current = levels;
418
+ const resourceLabelRef = useRef(resourceLabel);
419
+ resourceLabelRef.current = resourceLabel;
410
420
  const hasHydratedSharingRef = useRef(false);
411
421
  const lastSavedSharingRef = useRef(null);
412
422
  const autoSaveTimerRef = useRef(null);
@@ -471,7 +481,8 @@ export function ShareAccessComponent({ isOpen, requestUrl, transport, resourceLa
471
481
  }, [isOpen, defaultAccessLevel]);
472
482
  useEffect(() => {
473
483
  if (!isOpen) {
474
- hasLoadedForOpenRef.current = false;
484
+ loadedSourceRef.current = null;
485
+ setSharingLoadError(null);
475
486
  hasHydratedSharingRef.current = false;
476
487
  lastSavedSharingRef.current = null;
477
488
  if (autoSaveTimerRef.current) {
@@ -496,15 +507,20 @@ export function ShareAccessComponent({ isOpen, requestUrl, transport, resourceLa
496
507
  setIsSharingAccessConfirmed(false);
497
508
  return;
498
509
  }
499
- if (hasLoadedForOpenRef.current) {
510
+ const source = transport ?? requestUrl ?? null;
511
+ if (loadedSourceRef.current === source) {
500
512
  return;
501
513
  }
502
- hasLoadedForOpenRef.current = true;
514
+ loadedSourceRef.current = source;
503
515
  let cancelled = false;
516
+ let settled = false;
517
+ const levels = levelsRef.current;
518
+ const resourceLabel = resourceLabelRef.current;
504
519
  const run = async () => {
505
520
  setIsLoading(true);
506
521
  setIsSharingAccessConfirmed(false);
507
522
  setSharingAccessMessage(null);
523
+ setSharingLoadError(null);
508
524
  try {
509
525
  const response = transport
510
526
  ? null
@@ -572,9 +588,11 @@ export function ShareAccessComponent({ isOpen, requestUrl, transport, resourceLa
572
588
  const message = error instanceof Error
573
589
  ? error.message
574
590
  : `Unable to load ${resourceLabel.toLowerCase()} sharing.`;
591
+ setSharingLoadError(message);
575
592
  enqueueToastRef.current(message, { variant: 'error' });
576
593
  }
577
594
  finally {
595
+ settled = true;
578
596
  if (!cancelled) {
579
597
  setIsLoading(false);
580
598
  }
@@ -583,16 +601,23 @@ export function ShareAccessComponent({ isOpen, requestUrl, transport, resourceLa
583
601
  void run();
584
602
  return () => {
585
603
  cancelled = true;
604
+ // A load dropped before it answered never clears "Loading…" by itself
605
+ // and its answer is thrown away: the next run has to ask again. Before,
606
+ // the once-per-open guard stayed set, so any re-render that re-ran this
607
+ // effect mid-flight (the space's name arriving, say) left the form
608
+ // disabled on "Loading current sharing settings…" for good.
609
+ if (!settled && loadedSourceRef.current === source) {
610
+ loadedSourceRef.current = null;
611
+ }
586
612
  };
613
+ // Only what decides the request: the label and the levels are read
614
+ // through refs, so a parent's re-render does not drop a load in flight.
587
615
  }, [
588
616
  isOpen,
589
617
  canRequest,
590
618
  requestUrl,
591
619
  transport,
592
620
  token,
593
- resourceLabel,
594
- resourceName,
595
- levels,
596
621
  mergePrincipalCacheEntry,
597
622
  ]);
598
623
  // ----- Fetch shareable principals on open -----
@@ -1215,7 +1240,7 @@ export function ShareAccessComponent({ isOpen, requestUrl, transport, resourceLa
1215
1240
  letterSpacing: 0.4,
1216
1241
  }, children: label }), _jsx(Box, { display: "grid", gap: 1, children: items.map(renderShareablePrincipalRow) })] }));
1217
1242
  };
1218
- const content = (_jsxs(Box, { p: 3, display: "flex", flexDirection: "column", gap: 3, children: [sharingAccessMessage && (_jsxs(Box, { px: 3, py: 3, borderRadius: 2, borderWidth: 1, borderStyle: "solid", borderColor: "attention.muted", bg: "attention.subtle", display: "grid", gap: 1, children: [_jsx(Text, { sx: { fontWeight: 600 }, children: "Sharing access is restricted" }), _jsx(Text, { sx: { color: 'fg.muted' }, children: sharingAccessMessage })] })), _jsxs(Box, { opacity: isReadOnly ? 0.6 : 1, pointerEvents: isReadOnly ? 'none' : 'auto', display: "flex", flexDirection: "column", gap: 3, "aria-disabled": isReadOnly, children: [_jsxs(Box, { px: 3, py: 2, borderRadius: 2, borderWidth: 1, borderStyle: "solid", borderColor: "border.default", bg: "canvas.subtle", display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 2, children: [_jsx(Box, { display: "grid", gap: 1, children: _jsxs(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: ["Share ", resourceName || `this ${resourceLabel.toLowerCase()}`, ". Pick a principal below \u2014 they will be granted the selected access level."] }) }), _jsxs(ActionMenu, { children: [_jsxs(Box, { display: "inline-flex", alignItems: "center", gap: 1, children: [_jsx(ActionMenu.Anchor, { children: _jsxs(Button, { variant: "default", size: "small", leadingVisual: KeyIcon, disabled: isSaving || isReadOnly, children: ["Access:", ' ', levelLabels[selectedAccessLevel] || selectedAccessLevel] }) }), _jsx(Button, { variant: "invisible", size: "small", leadingVisual: isExpanded ? ChevronUpIcon : ChevronDownIcon, onClick: () => setIsExpanded(previous => !previous), "aria-label": isExpanded
1243
+ const content = (_jsxs(Box, { p: 3, display: "flex", flexDirection: "column", gap: 3, children: [sharingAccessMessage && (_jsxs(Box, { px: 3, py: 3, borderRadius: 2, borderWidth: 1, borderStyle: "solid", borderColor: "attention.muted", bg: "attention.subtle", display: "grid", gap: 1, children: [_jsx(Text, { sx: { fontWeight: 600 }, children: "Sharing access is restricted" }), _jsx(Text, { sx: { color: 'fg.muted' }, children: sharingAccessMessage })] })), sharingLoadError && (_jsxs(Box, { role: "alert", px: 3, py: 3, borderRadius: 2, borderWidth: 1, borderStyle: "solid", borderColor: "danger.muted", bg: "danger.subtle", display: "grid", gap: 1, children: [_jsx(Text, { sx: { fontWeight: 600 }, children: "The sharing settings could not be loaded" }), _jsx(Text, { sx: { color: 'fg.muted' }, children: sharingLoadError })] })), _jsxs(Box, { opacity: isReadOnly ? 0.6 : 1, pointerEvents: isReadOnly ? 'none' : 'auto', display: "flex", flexDirection: "column", gap: 3, "aria-disabled": isReadOnly, children: [_jsxs(Box, { px: 3, py: 2, borderRadius: 2, borderWidth: 1, borderStyle: "solid", borderColor: "border.default", bg: "canvas.subtle", display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 2, children: [_jsx(Box, { display: "grid", gap: 1, children: _jsxs(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: ["Share ", resourceName || `this ${resourceLabel.toLowerCase()}`, ". Pick a principal below \u2014 they will be granted the selected access level."] }) }), _jsxs(ActionMenu, { children: [_jsxs(Box, { display: "inline-flex", alignItems: "center", gap: 1, children: [_jsx(ActionMenu.Anchor, { children: _jsxs(Button, { variant: "default", size: "small", leadingVisual: KeyIcon, disabled: isSaving || isReadOnly, children: ["Access:", ' ', levelLabels[selectedAccessLevel] || selectedAccessLevel] }) }), _jsx(Button, { variant: "invisible", size: "small", leadingVisual: isExpanded ? ChevronUpIcon : ChevronDownIcon, onClick: () => setIsExpanded(previous => !previous), "aria-label": isExpanded
1219
1244
  ? 'Shrink sharing details'
1220
1245
  : 'Expand sharing details', children: isExpanded ? 'Shrink details' : 'Expand details' })] }), _jsx(ActionMenu.Overlay, { width: "small", children: _jsx(ActionList, { selectionVariant: "single", children: levels.map(({ level, label }) => (_jsx(ActionList.Item, { selected: selectedAccessLevel === level, onSelect: () => setSelectedAccessLevel(level), children: label }, level))) }) })] })] }), !isExpanded && (_jsxs(Box, { px: 3, py: 2, borderRadius: 2, borderWidth: 1, borderStyle: "solid", borderColor: "border.default", bg: "canvas.default", display: "grid", gap: 1, children: [_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Sharing details are collapsed." }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: [aclEntries.length, " principal", aclEntries.length === 1 ? '' : 's', ' ', "currently granted access."] })] })), isExpanded && (_jsxs(_Fragment, { children: [_jsxs(Box, { px: 3, py: 2, borderRadius: 2, borderWidth: 1, borderStyle: "solid", borderColor: "border.default", bg: "canvas.default", display: "grid", gap: 1, children: [_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Owner" }), ownerPrincipals.length > 0 ? (_jsx(Box, { display: "grid", children: ownerPrincipals.map((ownerPrincipal, index) => (_jsx(Box, { py: 1, borderTopWidth: index === 0 ? 0 : 1, borderTopStyle: "solid", borderColor: "border.subtle", children: _jsx(OwnerPrincipalRow, { ownerPrincipal: ownerPrincipal, cache: principalCache, showAvatarSkeleton: ownerPrincipal.kind === 'personal' &&
1221
1246
  Boolean(hydratingUserUids[ownerPrincipal.uid]), isPlatformAdmin: isPlatformAdmin }) }, principalKey(ownerPrincipal.kind, ownerPrincipal.uid)))) })) : (_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Owner information is not available." }))] }), _jsxs(Box, { px: 3, py: 2, borderRadius: 2, borderWidth: 1, borderStyle: "solid", borderColor: "border.default", bg: "canvas.default", display: "grid", gap: 2, children: [_jsxs(Box, { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 2, children: [_jsx(Text, { sx: { fontSize: 1, fontWeight: 600 }, children: "Share with\u2026" }), isLoadingShareable && (_jsxs(Box, { display: "inline-flex", alignItems: "center", gap: 1, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Loading\u2026" })] }))] }), !isLoadingShareable &&
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Where `useToast` shows its toasts outside JupyterLab: primer-addons'
3
+ * sliding panel, one toast at a time, the next once it is gone. Mount it
4
+ * once, inside the application's theme. JupyterLab keeps its own
5
+ * notifications.
6
+ *
7
+ * @module components/toast/ToastPanel
8
+ */
9
+ import type { JSX } from 'react';
10
+ export declare const ToastPanel: () => JSX.Element;
11
+ export default ToastPanel;
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Fragment, useSyncExternalStore } from 'react';
3
+ import { Button } from '@primer/react';
4
+ import { Box, SlidingPanel } from '@datalayer/primer-addons';
5
+ import { toastQueue } from './toastQueue.js';
6
+ const BUTTON_VARIANTS = {
7
+ accent: 'primary',
8
+ link: 'invisible',
9
+ warn: 'danger',
10
+ default: 'default',
11
+ };
12
+ export const ToastPanel = () => {
13
+ const toasts = useSyncExternalStore(toastQueue.subscribe, toastQueue.snapshot, toastQueue.snapshot);
14
+ const shown = toasts[0];
15
+ const close = () => {
16
+ if (shown) {
17
+ toastQueue.dismiss(shown.id);
18
+ }
19
+ };
20
+ return (_jsx(SlidingPanel
21
+ // A new toast is a new panel: it slides in, and its own clock runs.
22
+ , { isOpen: Boolean(shown),
23
+ // A toast of several lines (a failed request's message and exception) says each on its own line.
24
+ message: (shown?.message ?? '').split('\n').map((line, index) => (_jsxs(Fragment, { children: [index > 0 ? _jsx("br", {}) : null, line] }, index))), variant: shown?.variant ?? 'default', position: "north", durationMs: shown && shown.autoClose !== false ? shown.autoClose : undefined, onDismiss: close, details: shown?.detail || shown?.actions?.length ? (_jsxs(_Fragment, { children: [shown?.detail ? (
25
+ // The technical detail: small, in monospace, whole, wrapped (never the message's type).
26
+ _jsx(Box, { as: "pre", "data-toast-detail": true, sx: {
27
+ m: 0,
28
+ mt: 1,
29
+ fontFamily: 'mono',
30
+ fontSize: 0,
31
+ whiteSpace: 'pre-wrap',
32
+ overflowWrap: 'anywhere',
33
+ maxHeight: '12rem',
34
+ overflowY: 'auto',
35
+ }, children: shown.detail })) : null, shown?.actions?.length ? (_jsx(Box, { display: "flex", gap: 2, flexWrap: "wrap", mt: 2, children: shown.actions.map(action => (_jsx(Button, { size: "small", title: action.caption ?? action.label, variant: BUTTON_VARIANTS[action.displayType ?? 'default'], onClick: (event) => {
36
+ action.callback(event.nativeEvent);
37
+ if (!event.nativeEvent.defaultPrevented) {
38
+ close();
39
+ }
40
+ }, children: action.label }, action.label))) })) : null] })) : undefined }, shown?.id ?? 'none'));
41
+ };
42
+ export default ToastPanel;
@@ -0,0 +1,2 @@
1
+ export * from './ToastPanel';
2
+ export * from './toastQueue';
@@ -0,0 +1,6 @@
1
+ /*
2
+ * Copyright (c) 2023-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export * from './ToastPanel.js';
6
+ export * from './toastQueue.js';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The toasts waiting to be shown outside JupyterLab, in the order they were
3
+ * asked for: `useToast` puts them here, `ToastPanel` shows the first in a
4
+ * sliding panel, and taking it away shows the next.
5
+ *
6
+ * @module components/toast/toastQueue
7
+ */
8
+ export type ToastVariant = 'info' | 'success' | 'warning' | 'error';
9
+ /** A button on a toast, as JupyterLab's notifications name one. */
10
+ export type ToastAction = {
11
+ label: string;
12
+ caption?: string;
13
+ displayType?: 'default' | 'accent' | 'warn' | 'link';
14
+ callback: (event: MouseEvent) => void;
15
+ };
16
+ export type QueuedToast = {
17
+ id: string;
18
+ variant: ToastVariant;
19
+ message: string;
20
+ /** A technical detail drawn under the message, small and in monospace (a request's status, a server's words). */
21
+ detail?: string;
22
+ actions?: ToastAction[];
23
+ /** Milliseconds before it goes on its own; `false` stays until dismissed. */
24
+ autoClose: number | false;
25
+ };
26
+ /** Info and success go on their own after five seconds; a warning or an error stays. */
27
+ export declare const defaultAutoClose: (variant: ToastVariant) => number | false;
28
+ export declare const toastQueue: {
29
+ subscribe(listener: () => void): () => void;
30
+ snapshot(): readonly QueuedToast[];
31
+ push(toast: Omit<QueuedToast, "id">): string;
32
+ update(id: string, patch: Partial<Omit<QueuedToast, "id">>): void;
33
+ /** Take one toast away, or every toast without an id. */
34
+ dismiss(id?: string): void;
35
+ };
@@ -0,0 +1,47 @@
1
+ /*
2
+ * Copyright (c) 2023-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ let toasts = [];
6
+ const listeners = new Set();
7
+ let next = 0;
8
+ const emit = () => {
9
+ for (const listener of listeners) {
10
+ listener();
11
+ }
12
+ };
13
+ /** Info and success go on their own after five seconds; a warning or an error stays. */
14
+ export const defaultAutoClose = (variant) => variant === 'info' || variant === 'success' ? 5000 : false;
15
+ export const toastQueue = {
16
+ subscribe(listener) {
17
+ listeners.add(listener);
18
+ return () => {
19
+ listeners.delete(listener);
20
+ };
21
+ },
22
+ snapshot() {
23
+ return toasts;
24
+ },
25
+ push(toast) {
26
+ // The same toast already waiting is not said twice: a page that asks
27
+ // twice, or two requests that fail alike, show it once.
28
+ const same = toasts.find(queued => queued.variant === toast.variant && queued.message === toast.message);
29
+ if (same) {
30
+ return same.id;
31
+ }
32
+ next += 1;
33
+ const id = `toast-${next}`;
34
+ toasts = [...toasts, { ...toast, id }];
35
+ emit();
36
+ return id;
37
+ },
38
+ update(id, patch) {
39
+ toasts = toasts.map(toast => toast.id === id ? { ...toast, ...patch } : toast);
40
+ emit();
41
+ },
42
+ /** Take one toast away, or every toast without an id. */
43
+ dismiss(id) {
44
+ toasts = id === undefined ? [] : toasts.filter(toast => toast.id !== id);
45
+ emit();
46
+ },
47
+ };
@@ -20,6 +20,7 @@ export * from './useLocation';
20
20
  export * from './useLocationHandles';
21
21
  export * from './useMcp';
22
22
  export * from './useNavigate';
23
+ export * from './useKeptScroll';
23
24
  export * from './useOrganizationPlugins';
24
25
  export * from './useParams';
25
26
  export * from './useOnClickOutside';
@@ -25,6 +25,7 @@ export * from './useLocation.js';
25
25
  export * from './useLocationHandles.js';
26
26
  export * from './useMcp.js';
27
27
  export * from './useNavigate.js';
28
+ export * from './useKeptScroll.js';
28
29
  export * from './useOrganizationPlugins.js';
29
30
  export * from './useParams.js';
30
31
  export * from './useOnClickOutside.js';
@@ -33,7 +33,7 @@
33
33
  * ```
34
34
  */
35
35
  import { QueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
36
- import { IAnyOrganization, IContact, IIAMProviderName, IIAMToken, IOrganization, ISchool, ISecret, IStudent, ITeam, IUser, IUserOnboarding, IUserSettings } from '../models';
36
+ import { IAnyOrganization, IContact, IIAMProviderName, IIAMToken, IOrganization, ISchool, ISecret, IStudent, ITeam, IUser, IUserOnboarding, IUserSettings, type UsageRuntimeEngine } from '../models';
37
37
  type IAnySpace = any;
38
38
  export type CacheProps = {
39
39
  loginRoute?: string;
@@ -635,7 +635,9 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
635
635
  accountUid?: string;
636
636
  } | Omit<UseQueryOptions<unknown[]>, "queryKey" | "queryFn">, options?: Omit<UseQueryOptions<unknown[]>, "queryKey" | "queryFn">) => import("@tanstack/react-query").UseQueryResult<unknown[], Error>;
637
637
  useUsagesForUser: (userId: string, options?: Omit<UseQueryOptions<unknown[]>, "queryKey" | "queryFn">) => import("@tanstack/react-query").UseQueryResult<unknown[], Error>;
638
- usePlatformUsages: (options?: Omit<UseQueryOptions<unknown[]>, "queryKey" | "queryFn">) => import("@tanstack/react-query").UseQueryResult<unknown[], Error>;
638
+ usePlatformUsages: (options?: Omit<UseQueryOptions<unknown[]>, "queryKey" | "queryFn">, filter?: {
639
+ runtimeEngine?: UsageRuntimeEngine;
640
+ }) => import("@tanstack/react-query").UseQueryResult<unknown[], Error>;
639
641
  useOrganization: (organizationId: string) => import("@tanstack/react-query").UseQueryResult<IOrganization, Error>;
640
642
  useOrganizationByHandle: (handle: string) => import("@tanstack/react-query").UseQueryResult<IOrganization | null, Error>;
641
643
  useOrganizationPublicProfileByHandle: (handle: string) => import("@tanstack/react-query").UseQueryResult<any, Error>;
@@ -37,7 +37,7 @@
37
37
  * ```
38
38
  */
39
39
  import { useQuery, useQueries, useMutation, useQueryClient, } from '@tanstack/react-query';
40
- import { BOOTSTRAP_USER_ONBOARDING, asContact, asInvite, asOrganization, asSecret, asTeam, asToken, asUser, } from '../models/index.js';
40
+ import { BOOTSTRAP_USER_ONBOARDING, asContact, asInvite, asOrganization, asSecret, asTeam, asToken, asUser, usageRuntimeEngineOf, } from '../models/index.js';
41
41
  import { asSpace } from './cacheConverters.js';
42
42
  import { parseDeckSpec } from './deckSpec.js';
43
43
  import { libraryAppFieldsOf } from './libraryApp.js';
@@ -6592,9 +6592,15 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
6592
6592
  /**
6593
6593
  * Get platform-wide usage statistics
6594
6594
  */
6595
- const usePlatformUsages = (options) => {
6595
+ const usePlatformUsages = (options,
6596
+ /**
6597
+ * Only the usage of one engine (`runtime_engine`), filtered by IAM:
6598
+ * `kagent` for the usage page's KAgent tab. Every record otherwise.
6599
+ */
6600
+ filter) => {
6601
+ const runtimeEngine = filter?.runtimeEngine;
6596
6602
  return useQuery({
6597
- queryKey: ['usage', 'platform'],
6603
+ queryKey: ['usage', 'platform', runtimeEngine ?? 'all'],
6598
6604
  queryFn: async () => {
6599
6605
  const parseUsageDate = (value) => {
6600
6606
  if (!value) {
@@ -6642,11 +6648,14 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
6642
6648
  usagePeriodLabel: u.usage_period_label,
6643
6649
  usagePeriodStart: parseUsageDate(u.usage_period_start),
6644
6650
  usagePeriodEnd: parseUsageDate(u.usage_period_end),
6651
+ runtimeEngine: usageRuntimeEngineOf(u.runtime_engine),
6645
6652
  metadata,
6646
6653
  };
6647
6654
  };
6648
6655
  const resp = await requestDatalayer({
6649
- url: `${configuration.iamUrl}/api/iam/v1/usage/platform`,
6656
+ url: `${configuration.iamUrl}/api/iam/v1/usage/platform${runtimeEngine
6657
+ ? `?runtime_engine=${encodeURIComponent(runtimeEngine)}`
6658
+ : ''}`,
6650
6659
  method: 'GET',
6651
6660
  });
6652
6661
  // Transform snake_case API response to camelCase IUsage interface