@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.
- package/lib/api/evals/__tests__/external-definition.golden.json +110 -0
- package/lib/api/evals/client.d.ts +13 -1
- package/lib/api/evals/client.js +13 -0
- package/lib/api/evals/types.d.ts +168 -1
- package/lib/api/mcp/agentAccess.d.ts +10 -0
- package/lib/api/mcp/agentAccess.js +28 -0
- package/lib/api/mcp/generated.js +20 -0
- package/lib/api/mcp/index.d.ts +1 -0
- package/lib/api/mcp/index.js +1 -0
- package/lib/api/mcp/observability.d.ts +1 -1
- package/lib/api/mcp/observability.js +2 -0
- package/lib/api/mcp/operations.d.ts +28 -10
- package/lib/api/mcp/tasks.d.ts +9 -1
- package/lib/api/otel/firewall.d.ts +74 -0
- package/lib/api/otel/firewall.js +42 -0
- package/lib/api/otel/index.d.ts +1 -0
- package/lib/api/otel/index.js +1 -0
- package/lib/cli-login.js +2 -2
- package/lib/components/anonymous/AnonymousKeyExpired.js +15 -3
- package/lib/components/index.d.ts +1 -0
- package/lib/components/index.js +1 -0
- package/lib/components/mcp/ScopeList.js +10 -1
- package/lib/components/principal/PrincipalSwitcherMenu.js +4 -4
- package/lib/components/sharing/ShareAccessComponent.js +33 -8
- package/lib/components/toast/ToastPanel.d.ts +11 -0
- package/lib/components/toast/ToastPanel.js +42 -0
- package/lib/components/toast/index.d.ts +2 -0
- package/lib/components/toast/index.js +6 -0
- package/lib/components/toast/toastQueue.d.ts +35 -0
- package/lib/components/toast/toastQueue.js +47 -0
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.js +1 -0
- package/lib/hooks/useCache.d.ts +4 -2
- package/lib/hooks/useCache.js +13 -4
- package/lib/hooks/useDatalayer.js +12 -16
- package/lib/hooks/useKeptScroll.d.ts +6 -0
- package/lib/hooks/useKeptScroll.js +54 -0
- package/lib/hooks/useMcp.d.ts +2 -2
- package/lib/hooks/useMcp.js +5 -2
- package/lib/hooks/useNavigate.d.ts +16 -2
- package/lib/hooks/useNavigate.js +65 -5
- package/lib/hooks/useToast.d.ts +6 -0
- package/lib/hooks/useToast.js +40 -1
- package/lib/models/McpAgentAccess.d.ts +95 -0
- package/lib/models/McpAgentAccess.js +5 -0
- package/lib/models/Usage.d.ts +13 -0
- package/lib/models/Usage.js +5 -0
- package/lib/models/index.d.ts +1 -0
- package/lib/models/index.js +1 -0
- package/lib/otel/client/OtelClient.d.ts +56 -0
- package/lib/otel/client/OtelClient.js +25 -0
- package/lib/otel/client/index.d.ts +1 -1
- package/lib/otel/index.d.ts +1 -1
- package/lib/otel/views/OtelLive.js +3 -3
- package/lib/otel/views/OtelLiveSpans.js +2 -2
- package/lib/otel/views/OtelLogsList.js +3 -3
- package/lib/otel/views/OtelMetricsChart.d.ts +13 -0
- package/lib/otel/views/OtelMetricsChart.js +109 -26
- package/lib/otel/views/OtelSpanDetail.js +3 -3
- package/lib/otel/views/OtelSqlView.js +4 -4
- package/lib/otel/views/OtelSystemView.js +2 -2
- package/lib/otel/views/OtelTimelineRangeSlider.js +2 -2
- package/lib/otel/views/OtelTracesList.js +2 -2
- package/lib/state/substates/LayoutState.js +24 -3
- package/lib/state/substates/NavigationState.d.ts +11 -3
- package/lib/state/substates/NavigationState.js +48 -5
- package/lib/views/mcp/AuditLog.js +1 -1
- package/lib/views/mcp/ConnectedAgents.d.ts +1 -1
- package/lib/views/mcp/ConnectedAgents.js +13 -10
- package/lib/views/mcp/McpDashboard.d.ts +1 -1
- package/lib/views/mcp/McpDashboard.js +6 -6
- package/lib/views/mcp/McpHome.d.ts +3 -1
- package/lib/views/mcp/McpHome.js +19 -9
- package/lib/views/mcp/McpObservability.d.ts +22 -3
- package/lib/views/mcp/McpObservability.js +65 -17
- package/lib/views/mcp/Policies.js +1 -1
- package/lib/views/mcp/RunDetail.js +2 -2
- package/lib/views/mcp/types.d.ts +8 -4
- package/lib/views/otel/OtelHeader.js +2 -2
- 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
|
+
};
|
package/lib/api/otel/index.d.ts
CHANGED
package/lib/api/otel/index.js
CHANGED
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 {
|
|
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(
|
|
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
|
-
: ''] }),
|
|
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
|
, {
|
package/lib/components/index.js
CHANGED
|
@@ -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",
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
510
|
+
const source = transport ?? requestUrl ?? null;
|
|
511
|
+
if (loadedSourceRef.current === source) {
|
|
500
512
|
return;
|
|
501
513
|
}
|
|
502
|
-
|
|
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,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
|
+
};
|
package/lib/hooks/index.d.ts
CHANGED
|
@@ -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';
|
package/lib/hooks/index.js
CHANGED
|
@@ -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';
|
package/lib/hooks/useCache.d.ts
CHANGED
|
@@ -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"
|
|
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>;
|
package/lib/hooks/useCache.js
CHANGED
|
@@ -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
|