@datalayer/core 1.2.28 → 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/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/principal/PrincipalSwitcherMenu.js +4 -4
- package/lib/components/sharing/ShareAccessComponent.js +33 -8
- package/lib/components/toast/ToastPanel.js +18 -7
- package/lib/components/toast/toastQueue.d.ts +2 -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/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/models/Usage.d.ts +13 -0
- package/lib/models/Usage.js +5 -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 +1 -1
- 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/McpObservability.d.ts +22 -3
- package/lib/views/mcp/McpObservability.js +65 -17
- package/lib/views/mcp/RunDetail.js +2 -2
- package/package.json +1 -1
|
@@ -39,7 +39,7 @@ export declare const MCP_DURABLE_SERVICE_NAME = "datalayer-durable";
|
|
|
39
39
|
*/
|
|
40
40
|
export declare const serviceNameFor: (metric: McpMetricName) => string;
|
|
41
41
|
/** The metric catalog `telemetry.py` owns; no ad-hoc counters. */
|
|
42
|
-
export declare const MCP_METRIC_CATALOG: readonly ["mcp.calls", "mcp.call.duration", "mcp.refusals", "mcp.limit.unenforced", "mcp.forwards", "mcp.workers", "mcp.worker_start_seconds", "mcp.bindings", "mcp.sandbox.lost", "mcp.tasks", "mcp.task.duration", "durable.step.duration", "durable.queue.wait", "durable.recoveries", "sandbox.launch_seconds", "mcp.audit.writes", "mcp.audit.write_failures", "mcp.dependency.duration", "mcp.dependency.timeouts", "mcp.dependency.ready", "mcp.readiness.failures", "mcp.audit.deleted", "mcp.jobs", "mcp.job.duration"];
|
|
42
|
+
export declare const MCP_METRIC_CATALOG: readonly ["mcp.calls", "mcp.call.duration", "mcp.refusals", "mcp.limit.unenforced", "mcp.forwards", "mcp.workers", "mcp.process.fds", "mcp.worker_start_seconds", "mcp.bindings", "mcp.sandbox.lost", "mcp.tasks", "mcp.task.duration", "durable.step.duration", "durable.queue.wait", "durable.recoveries", "sandbox.launch_seconds", "mcp.audit.writes", "mcp.audit.write_failures", "mcp.dependency.duration", "mcp.dependency.timeouts", "mcp.dependency.ready", "mcp.readiness.failures", "mcp.audit.deleted", "mcp.jobs", "mcp.job.duration"];
|
|
43
43
|
export type McpMetricName = (typeof MCP_METRIC_CATALOG)[number];
|
|
44
44
|
/** The four SLIs, and the catalog metric each is read from. */
|
|
45
45
|
export declare const MCP_SLI_METRICS: {
|
|
@@ -79,6 +79,8 @@ export const MCP_METRIC_CATALOG = [
|
|
|
79
79
|
'mcp.limit.unenforced',
|
|
80
80
|
'mcp.forwards',
|
|
81
81
|
'mcp.workers',
|
|
82
|
+
// A gateway replica: file descriptors open, and the limit (6 November 2026).
|
|
83
|
+
'mcp.process.fds',
|
|
82
84
|
'mcp.worker_start_seconds',
|
|
83
85
|
'mcp.bindings',
|
|
84
86
|
'mcp.sandbox.lost',
|
|
@@ -27,11 +27,17 @@ export interface McpWorkerList {
|
|
|
27
27
|
/** The replica that answered. */
|
|
28
28
|
replica?: string;
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
/** One of the newest runs the durable engine reports. */
|
|
31
|
+
export interface McpWorkflowRun {
|
|
32
|
+
uid: string;
|
|
33
|
+
workflow: string;
|
|
34
|
+
queue: string;
|
|
35
|
+
/** `running` (DBOS's pending and enqueued alike), `blocked`, `completed`, `failed` or `cancelled`. */
|
|
36
|
+
status: string;
|
|
37
|
+
taskUid?: string;
|
|
38
|
+
/** Unix seconds. */
|
|
39
|
+
startedAt?: number;
|
|
40
|
+
updatedAt?: number;
|
|
35
41
|
}
|
|
36
42
|
/** One periodic job on the replica that answered. */
|
|
37
43
|
export interface McpJob {
|
|
@@ -57,14 +63,26 @@ export interface McpJobSchedule {
|
|
|
57
63
|
holder: string;
|
|
58
64
|
jobs: McpJob[];
|
|
59
65
|
}
|
|
60
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* What the durable engine says about itself (`/operations/workflows`, the
|
|
68
|
+
* engine's `operations()` as the gateway forwards it): whether it is taking
|
|
69
|
+
* work, the queues it polls, the queues it was told to run that nobody serves,
|
|
70
|
+
* and its newest runs.
|
|
71
|
+
*/
|
|
61
72
|
export interface McpWorkflowsHealth {
|
|
62
73
|
engine: 'temporal' | 'dbos' | string;
|
|
63
|
-
|
|
74
|
+
/** Whether it takes work: launched, and its run listing readable. */
|
|
75
|
+
durable: boolean;
|
|
64
76
|
detail?: string | null;
|
|
65
|
-
queues
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
/** The queues declared on the engine, by name. */
|
|
78
|
+
queues: string[];
|
|
79
|
+
/** Queues this deployment was told to run that the catalogue does not have: work sent there is never done. */
|
|
80
|
+
queuesNobodyServes: string[];
|
|
81
|
+
workflows: string[];
|
|
82
|
+
/** Its newest runs (twenty), newest first. */
|
|
83
|
+
runs: McpWorkflowRun[];
|
|
84
|
+
databaseConfigured?: boolean;
|
|
85
|
+
applicationVersion?: string;
|
|
68
86
|
}
|
|
69
87
|
export declare const getGatewayVersion: (baseUrl?: string) => Promise<McpGatewayVersion>;
|
|
70
88
|
export declare const getGatewayHealth: (baseUrl?: string) => Promise<McpGatewayHealth>;
|
package/lib/api/mcp/tasks.d.ts
CHANGED
|
@@ -16,11 +16,19 @@ export interface McpTaskListFilters {
|
|
|
16
16
|
export declare const listTasks: (token: string, filters?: McpTaskListFilters, baseUrl?: string) => Promise<McpTaskList>;
|
|
17
17
|
export declare const getTask: (token: string, taskUid: string, baseUrl?: string) => Promise<McpTask>;
|
|
18
18
|
/** Stop a task that is still going. Idempotent: a finished task is answered as it is. */
|
|
19
|
-
export declare const cancelTask: (token: string, taskUid: string, baseUrl?: string) => Promise<
|
|
19
|
+
export declare const cancelTask: (token: string, taskUid: string, baseUrl?: string) => Promise<McpTaskCancelled>;
|
|
20
20
|
/** What the gateway says of an answer: that it was passed on, not the task. */
|
|
21
21
|
export type McpTaskAnswered = {
|
|
22
22
|
uid: string;
|
|
23
23
|
answered: boolean;
|
|
24
|
+
/** Set when the answer closed a run its engine no longer carried. */
|
|
25
|
+
status?: string;
|
|
26
|
+
};
|
|
27
|
+
/** What the gateway says of a cancel: whether it stopped, and the status. */
|
|
28
|
+
export type McpTaskCancelled = {
|
|
29
|
+
uid: string;
|
|
30
|
+
cancelled: boolean;
|
|
31
|
+
status?: string;
|
|
24
32
|
};
|
|
25
33
|
/**
|
|
26
34
|
* Answer a task that is `input_required`.
|
|
@@ -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
|
, {
|
|
@@ -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 &&
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Fragment, useSyncExternalStore } from 'react';
|
|
3
3
|
import { Button } from '@primer/react';
|
|
4
4
|
import { Box, SlidingPanel } from '@datalayer/primer-addons';
|
|
@@ -21,11 +21,22 @@ export const ToastPanel = () => {
|
|
|
21
21
|
// A new toast is a new panel: it slides in, and its own clock runs.
|
|
22
22
|
, { isOpen: Boolean(shown),
|
|
23
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?.actions?.length ? (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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'));
|
|
30
41
|
};
|
|
31
42
|
export default ToastPanel;
|
|
@@ -17,6 +17,8 @@ export type QueuedToast = {
|
|
|
17
17
|
id: string;
|
|
18
18
|
variant: ToastVariant;
|
|
19
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;
|
|
20
22
|
actions?: ToastAction[];
|
|
21
23
|
/** Milliseconds before it goes on its own; `false` stays until dismissed. */
|
|
22
24
|
autoClose: number | false;
|
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
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The nearest ancestor of `marker` that scrolls vertically, if any. */
|
|
2
|
+
export declare function scrollingAncestor(marker: HTMLElement | null): HTMLElement | null;
|
|
3
|
+
/** Where the sidebar kept under `key` was last scrolled to, if it was. */
|
|
4
|
+
export declare const keptScrollOf: (key: string) => number | undefined;
|
|
5
|
+
export declare function useKeptScroll<T extends HTMLElement = HTMLElement>(key: string): import("react").RefObject<T | null>;
|
|
6
|
+
export default useKeptScroll;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* A sidebar keeps its scroll across pages: an entry pressed far down the list
|
|
7
|
+
* leaves the list where it was, so the next one is still under the pointer.
|
|
8
|
+
*
|
|
9
|
+
* `useKeptScroll(key)` returns a ref for a small element placed inside the
|
|
10
|
+
* sidebar; the nearest ancestor that scrolls is the one kept. Its position is
|
|
11
|
+
* remembered under `key` while it scrolls, and given back when a sidebar with
|
|
12
|
+
* the same key is mounted again (a layout rendered anew by the next route).
|
|
13
|
+
* The panel must not be one `useNavigate` brings to the top: it is not marked
|
|
14
|
+
* `data-scroll-root`.
|
|
15
|
+
*
|
|
16
|
+
* @module hooks/useKeptScroll
|
|
17
|
+
*/
|
|
18
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
19
|
+
const kept = new Map();
|
|
20
|
+
const scrollsY = (element) => {
|
|
21
|
+
const overflow = window.getComputedStyle(element).overflowY;
|
|
22
|
+
return overflow === 'auto' || overflow === 'scroll';
|
|
23
|
+
};
|
|
24
|
+
/** The nearest ancestor of `marker` that scrolls vertically, if any. */
|
|
25
|
+
export function scrollingAncestor(marker) {
|
|
26
|
+
let element = marker?.parentElement ?? null;
|
|
27
|
+
while (element && !scrollsY(element)) {
|
|
28
|
+
element = element.parentElement;
|
|
29
|
+
}
|
|
30
|
+
return element;
|
|
31
|
+
}
|
|
32
|
+
/** Where the sidebar kept under `key` was last scrolled to, if it was. */
|
|
33
|
+
export const keptScrollOf = (key) => kept.get(key);
|
|
34
|
+
export function useKeptScroll(key) {
|
|
35
|
+
const ref = useRef(null);
|
|
36
|
+
useLayoutEffect(() => {
|
|
37
|
+
const panel = scrollingAncestor(ref.current);
|
|
38
|
+
if (!panel) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const saved = kept.get(key);
|
|
42
|
+
if (saved !== undefined) {
|
|
43
|
+
panel.scrollTop = saved;
|
|
44
|
+
}
|
|
45
|
+
const remember = () => kept.set(key, panel.scrollTop);
|
|
46
|
+
panel.addEventListener('scroll', remember, { passive: true });
|
|
47
|
+
return () => {
|
|
48
|
+
remember();
|
|
49
|
+
panel.removeEventListener('scroll', remember);
|
|
50
|
+
};
|
|
51
|
+
}, [key]);
|
|
52
|
+
return ref;
|
|
53
|
+
}
|
|
54
|
+
export default useKeptScroll;
|