@datalayer/agent-runtimes 1.0.28 → 1.0.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/index.js +16 -4
- package/lib/components/code-sandboxes/CodeSandboxEnvironmentSelect.d.ts +41 -0
- package/lib/components/code-sandboxes/CodeSandboxEnvironmentSelect.js +83 -0
- package/lib/components/code-sandboxes/CodeSandboxLauncher.d.ts +11 -0
- package/lib/components/code-sandboxes/CodeSandboxLauncher.js +96 -12
- package/lib/components/code-sandboxes/CodeSandboxNames.js +68 -23
- package/lib/components/code-sandboxes/CodeSandboxPicker.d.ts +14 -1
- package/lib/components/code-sandboxes/CodeSandboxPicker.js +155 -21
- package/lib/components/code-sandboxes/CodeSandboxUtils.d.ts +3 -0
- package/lib/components/code-sandboxes/CodeSandboxUtils.js +62 -11
- package/lib/components/code-sandboxes/NewCodeSandboxControls.js +2 -2
- package/lib/components/code-sandboxes/codeSandboxProviders.d.ts +29 -0
- package/lib/components/code-sandboxes/codeSandboxProviders.js +81 -0
- package/lib/components/code-sandboxes/index.d.ts +2 -0
- package/lib/components/code-sandboxes/index.js +2 -0
- package/lib/examples/AgentspecsExample.js +3 -3
- package/lib/identity/index.d.ts +1 -1
- package/lib/identity/index.js +1 -1
- package/lib/identity/types.d.ts +1 -1
- package/lib/identity/types.js +1 -1
- package/lib/models/CodeSandboxVariant.d.ts +45 -0
- package/lib/models/CodeSandboxVariant.js +65 -0
- package/lib/models/index.d.ts +1 -0
- package/lib/models/index.js +1 -0
- package/lib/runtimes/actions.js +14 -2
- package/lib/specs/envvars.d.ts +1 -1
- package/lib/specs/envvars.js +3 -3
- package/lib/specs/mcpServers.js +2 -2
- package/lib/state/substates/RuntimesState.js +4 -1
- package/package.json +1 -1
- package/scripts/gen_workers.py +1 -1
package/lib/api/index.js
CHANGED
|
@@ -22,9 +22,21 @@ export * as notifications from './notifications';
|
|
|
22
22
|
export * as output from './output';
|
|
23
23
|
export * from '../runtimes';
|
|
24
24
|
import { iamStore } from '@datalayer/core/lib/state';
|
|
25
|
+
import { runtimesStore } from '../state/substates';
|
|
25
26
|
import { listEnvironments } from './runtimes/environments';
|
|
26
27
|
import { listSnapshots, createSnapshot, deleteSnapshot, } from './runtimes/snapshots';
|
|
27
28
|
import { asCodeSandboxSnapshot, } from '../models/CodeSandboxSnapshot';
|
|
29
|
+
/*
|
|
30
|
+
* The runtimes URL of the PAGE's configuration, not the built-in default.
|
|
31
|
+
*
|
|
32
|
+
* Every function below takes an optional `baseUrl`, and the low-level API
|
|
33
|
+
* layer defaults it to the production URL. Callers all over the application
|
|
34
|
+
* pass nothing — so against a local plane they silently queried production,
|
|
35
|
+
* and the picker offered the environments of the wrong deployment.
|
|
36
|
+
*/
|
|
37
|
+
function resolveRuntimesUrl(baseUrl) {
|
|
38
|
+
return baseUrl ?? runtimesStore.getState().runtimesUrl ?? undefined;
|
|
39
|
+
}
|
|
28
40
|
function resolveToken(token) {
|
|
29
41
|
const resolved = token ?? iamStore.getState()?.token;
|
|
30
42
|
if (!resolved) {
|
|
@@ -33,14 +45,14 @@ function resolveToken(token) {
|
|
|
33
45
|
return resolved;
|
|
34
46
|
}
|
|
35
47
|
export async function getEnvironments(token, baseUrl) {
|
|
36
|
-
const response = await listEnvironments(resolveToken(token), baseUrl);
|
|
48
|
+
const response = await listEnvironments(resolveToken(token), resolveRuntimesUrl(baseUrl));
|
|
37
49
|
return (response.environments ?? []).map(environment => ({
|
|
38
50
|
...environment,
|
|
39
51
|
burningRate: environment.burningRate ?? environment.burning_rate,
|
|
40
52
|
}));
|
|
41
53
|
}
|
|
42
54
|
export async function getSandboxSnapshots(token, baseUrl) {
|
|
43
|
-
const response = await listSnapshots(resolveToken(token), baseUrl);
|
|
55
|
+
const response = await listSnapshots(resolveToken(token), resolveRuntimesUrl(baseUrl));
|
|
44
56
|
return (response.snapshots ?? []).map(snapshot => asCodeSandboxSnapshot(snapshot));
|
|
45
57
|
}
|
|
46
58
|
export async function createSandboxSnapshot(data, token, baseUrl) {
|
|
@@ -49,11 +61,11 @@ export async function createSandboxSnapshot(data, token, baseUrl) {
|
|
|
49
61
|
if (!('pod_name' in data)) {
|
|
50
62
|
return data;
|
|
51
63
|
}
|
|
52
|
-
const response = await createSnapshot(resolveToken(token), data, baseUrl);
|
|
64
|
+
const response = await createSnapshot(resolveToken(token), data, resolveRuntimesUrl(baseUrl));
|
|
53
65
|
return response.snapshot;
|
|
54
66
|
}
|
|
55
67
|
export async function deleteCodeSandboxSnapshot(snapshotId, token, baseUrl) {
|
|
56
|
-
return deleteSnapshot(resolveToken(token), snapshotId, baseUrl);
|
|
68
|
+
return deleteSnapshot(resolveToken(token), snapshotId, resolveRuntimesUrl(baseUrl));
|
|
57
69
|
}
|
|
58
70
|
export async function exportCodeSandboxSnapshot(..._args) {
|
|
59
71
|
throw new Error('exportCodeSandboxSnapshot is not implemented in @datalayer/agent-runtimes/api yet.');
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** One choice of the dropdown. */
|
|
2
|
+
export type ICodeSandboxEnvironmentOption = {
|
|
3
|
+
/** Identity of the choice, as the host knows it. */
|
|
4
|
+
key: string;
|
|
5
|
+
/** What it is called. */
|
|
6
|
+
title: string;
|
|
7
|
+
/** The environment name, shown as a label beside the title. */
|
|
8
|
+
name?: string;
|
|
9
|
+
/** Who runs it — Datalayer, Kaggle, Modal, the Jupyter Server… */
|
|
10
|
+
providerTitle?: string;
|
|
11
|
+
/** Where it runs: `local`, `remote`, `browser`. */
|
|
12
|
+
location?: string;
|
|
13
|
+
/** Credits per second, when it costs any. */
|
|
14
|
+
burningRate?: number;
|
|
15
|
+
/** The GPU the environment carries, when it carries one. */
|
|
16
|
+
gpu?: string | boolean;
|
|
17
|
+
/** The heading this choice is listed under, when the list is grouped. */
|
|
18
|
+
group?: string;
|
|
19
|
+
};
|
|
20
|
+
export type ICodeSandboxEnvironmentSelectProps = {
|
|
21
|
+
/** The choices, in the order they should read. */
|
|
22
|
+
options: ICodeSandboxEnvironmentOption[];
|
|
23
|
+
/** The `key` of the chosen one, if any. */
|
|
24
|
+
selectedKey?: string;
|
|
25
|
+
/** Called with the `key` of the choice made. */
|
|
26
|
+
onSelect: (key: string) => void;
|
|
27
|
+
/** Whether the control refuses to open. */
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
/** What the trigger reads when nothing is chosen. */
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Width of the overlay, `xlarge` by default.
|
|
33
|
+
*
|
|
34
|
+
* Wide because the entries carry their labels on the same line as their
|
|
35
|
+
* name, and a priced one — the credits it burns, the environment, the
|
|
36
|
+
* provider, where it runs — is nearly as wide as the row itself.
|
|
37
|
+
*/
|
|
38
|
+
overlayWidth?: 'small' | 'medium' | 'large' | 'xlarge' | 'auto';
|
|
39
|
+
};
|
|
40
|
+
export declare function CodeSandboxEnvironmentSelect(props: ICodeSandboxEnvironmentSelectProps): JSX.Element;
|
|
41
|
+
export default CodeSandboxEnvironmentSelect;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The one dropdown for choosing where code runs.
|
|
8
|
+
*
|
|
9
|
+
* The launcher asks it of a new sandbox and the picker asks it of an editor
|
|
10
|
+
* that needs one; both are the same question, and were drawn twice — a menu
|
|
11
|
+
* here, a column of radio buttons there. This is that control, once: the
|
|
12
|
+
* chosen entry reads on the trigger exactly as the entries read in the list,
|
|
13
|
+
* name to the left and labels to the right, so the closed control and the
|
|
14
|
+
* open one show the same thing.
|
|
15
|
+
*
|
|
16
|
+
* An `ActionMenu` rather than a `<select>`: what tells two entries apart is
|
|
17
|
+
* their labels — the environment name, the provider, the credits it burns —
|
|
18
|
+
* and no `<option>` can carry those.
|
|
19
|
+
*
|
|
20
|
+
* @module components/code-sandboxes/CodeSandboxEnvironmentSelect
|
|
21
|
+
*/
|
|
22
|
+
import { ActionList, ActionMenu, Box, Label, Text } from '@primer/react';
|
|
23
|
+
/**
|
|
24
|
+
* The labels of an option, on one line: cost, name, provider, where.
|
|
25
|
+
*
|
|
26
|
+
* The price is the widest of them — "0.0008 credits/second" outruns the
|
|
27
|
+
* name, the provider and the location together — so the room comes from the
|
|
28
|
+
* overlay rather than from a second line: see `overlayWidth`.
|
|
29
|
+
*/
|
|
30
|
+
function OptionLabels(props) {
|
|
31
|
+
const { option } = props;
|
|
32
|
+
return (_jsxs(Box, { as: "span", sx: {
|
|
33
|
+
display: 'inline-flex',
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
gap: 1,
|
|
36
|
+
whiteSpace: 'nowrap',
|
|
37
|
+
}, children: [option.burningRate ? (_jsxs(Label, { size: "small", variant: "sponsors", children: [option.burningRate, " credits/second"] })) : null, option.gpu ? (_jsx(Label, { size: "small", variant: "success", children: "GPU" })) : null, option.name ? _jsx(Label, { size: "small", children: option.name }) : null, option.providerTitle ? (_jsx(Label, { size: "small", variant: "accent", children: option.providerTitle })) : null, option.location ? (_jsx(Label, { size: "small", variant: "secondary", children: option.location })) : null] }));
|
|
38
|
+
}
|
|
39
|
+
export function CodeSandboxEnvironmentSelect(props) {
|
|
40
|
+
const { disabled, onSelect, options, overlayWidth = 'xlarge', placeholder = 'Select an environment', selectedKey, } = props;
|
|
41
|
+
const selected = options.find(option => option.key === selectedKey);
|
|
42
|
+
/*
|
|
43
|
+
* What costs credits leads.
|
|
44
|
+
*
|
|
45
|
+
* The sandboxes of the platform and of its providers are the ones worth
|
|
46
|
+
* choosing between — a kernel of this server is the fallback, always
|
|
47
|
+
* there and free — so they are read first rather than found under a list
|
|
48
|
+
* of local specifications. A stable partition: within each half the order
|
|
49
|
+
* given by the caller is kept.
|
|
50
|
+
*/
|
|
51
|
+
const ordered = [
|
|
52
|
+
...options.filter(option => option.burningRate),
|
|
53
|
+
...options.filter(option => !option.burningRate),
|
|
54
|
+
];
|
|
55
|
+
const grouped = ordered.some(option => option.group);
|
|
56
|
+
const groups = grouped
|
|
57
|
+
? Array.from(new Set(ordered.map(option => option.group ?? '')))
|
|
58
|
+
: [];
|
|
59
|
+
const rows = (subset) => subset.map(option => (_jsxs(ActionList.Item, { selected: option.key === selectedKey, onSelect: () => onSelect(option.key), children: [option.title, _jsx(ActionList.TrailingVisual, { children: _jsx(OptionLabels, { option: option }) })] }, option.key)));
|
|
60
|
+
return (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Button, { block: true, disabled: disabled || ordered.length === 0,
|
|
61
|
+
// The button centers its text by default; the trigger must read like
|
|
62
|
+
// the rows below it — name left, labels right.
|
|
63
|
+
sx: {
|
|
64
|
+
// The content of a Primer button is a grid whose middle track is
|
|
65
|
+
// content-sized, which hugs the labels against the title. The
|
|
66
|
+
// template areas stay "leadingVisual text trailingVisual": only
|
|
67
|
+
// that track changes, from content-sized to 1fr.
|
|
68
|
+
'& [data-component="buttonContent"]': {
|
|
69
|
+
flex: 1,
|
|
70
|
+
gridTemplateColumns: 'min-content minmax(0, 1fr) min-content',
|
|
71
|
+
},
|
|
72
|
+
'& [data-component="text"]': { width: '100%', textAlign: 'left' },
|
|
73
|
+
}, children: selected ? (_jsxs(Box, { as: "span", sx: {
|
|
74
|
+
display: 'flex',
|
|
75
|
+
width: '100%',
|
|
76
|
+
alignItems: 'center',
|
|
77
|
+
justifyContent: 'space-between',
|
|
78
|
+
gap: 2,
|
|
79
|
+
}, children: [_jsx(Text, { children: selected.title }), _jsx(OptionLabels, { option: selected })] })) : (placeholder) }), _jsx(ActionMenu.Overlay, { width: overlayWidth, children: _jsx(ActionList, { selectionVariant: "single", children: grouped
|
|
80
|
+
? groups.map(group => (_jsxs(ActionList.Group, { children: [group ? (_jsx(ActionList.GroupHeading, { children: group })) : null, rows(ordered.filter(option => (option.group ?? '') === group))] }, group || 'ungrouped')))
|
|
81
|
+
: rows(ordered) }) })] }));
|
|
82
|
+
}
|
|
83
|
+
export default CodeSandboxEnvironmentSelect;
|
|
@@ -46,6 +46,17 @@ export interface ICodeSandboxLauncherProps {
|
|
|
46
46
|
* Upgrade subscription URL
|
|
47
47
|
*/
|
|
48
48
|
upgradeSubscription?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Where "Add credits" navigates.
|
|
51
|
+
*
|
|
52
|
+
* ABSOLUTE, always: a relative route resolves against the CURRENT
|
|
53
|
+
* location — opened from `/…/code-sandboxes`, `usage` landed on
|
|
54
|
+
* `code-sandboxes/usage` and a `:sandboxId` route swallowed it as a
|
|
55
|
+
* sandbox called "usage". The default is the web application's `/plans`
|
|
56
|
+
* (which redirects to the plan overview); a nested surface passes its
|
|
57
|
+
* own (e.g. `/datalayer/jupyter-kernels/usage/plans`).
|
|
58
|
+
*/
|
|
59
|
+
usageRoute?: string;
|
|
49
60
|
/**
|
|
50
61
|
* Optional submit button label override.
|
|
51
62
|
*/
|
|
@@ -5,15 +5,18 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
5
5
|
*/
|
|
6
6
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
7
|
import { useIsMounted } from 'usehooks-ts';
|
|
8
|
-
import { Button, FormControl,
|
|
8
|
+
import { Button, FormControl, Spinner, Text, TextInput, ToggleSwitch, } from '@primer/react';
|
|
9
9
|
import { Dialog } from '@primer/react/experimental';
|
|
10
10
|
import { Box } from '@datalayer/primer-addons';
|
|
11
11
|
import { useJupyterReactStore } from '@datalayer/jupyter-react';
|
|
12
|
-
import { USAGE_ROUTE } from '@datalayer/core/lib/routes';
|
|
13
12
|
import { useNavigate } from '@datalayer/core/lib/hooks';
|
|
14
13
|
import { NO_RUNTIME_AVAILABLE_LABEL } from '@datalayer/core/lib/i18n';
|
|
14
|
+
import { isCodeSandboxProviderAvailable } from './codeSandboxProviders';
|
|
15
|
+
import { CodeSandboxVariant, codeSandboxVariantOf, codeSandboxVariantTitle, } from '../../models/CodeSandboxVariant';
|
|
16
|
+
import { CodeSandboxEnvironmentSelect, } from './CodeSandboxEnvironmentSelect';
|
|
15
17
|
import { getCodeSandboxGivenName, listCodeSandboxGivenNames, nextCodeSandboxGivenName, setCodeSandboxGivenName, } from './CodeSandboxNames';
|
|
16
18
|
import { iamStore, runtimesStore, useCoreStore, useIAMStore, } from '../../state/substates';
|
|
19
|
+
import { createRuntime as createRuntimeRecord } from '../../runtimes/actions';
|
|
17
20
|
import { createNotebook, sleep } from '@datalayer/core/lib/utils';
|
|
18
21
|
import { Markdown } from '@datalayer/core/lib/components/display';
|
|
19
22
|
import { Timer } from '@datalayer/core/lib/components/progress';
|
|
@@ -42,10 +45,20 @@ const NOT_AVAILABLE_RETRIES = 5;
|
|
|
42
45
|
* The launcher of a new code sandbox.
|
|
43
46
|
*/
|
|
44
47
|
export function CodeSandboxLauncher(props) {
|
|
45
|
-
const { dialogTitle, kernelSnapshot, manager, onSubmit, markdownParser, sanitizer, upgradeSubscription, submitLabel, startRuntime = true, } = props;
|
|
48
|
+
const { dialogTitle, kernelSnapshot, manager, onSubmit, markdownParser, sanitizer, upgradeSubscription, usageRoute = '/plans', submitLabel, startRuntime = true, } = props;
|
|
46
49
|
const hasExample = startRuntime === 'with-example';
|
|
47
50
|
const shouldStartRuntime = startRuntime !== 'defer';
|
|
48
51
|
const user = iamStore.getState().user;
|
|
52
|
+
/*
|
|
53
|
+
* Where a sandbox may be launched: every provider that can be reached.
|
|
54
|
+
*
|
|
55
|
+
* The environments of the platform are offered wherever the account can
|
|
56
|
+
* reach it — withholding them inside JupyterLab left `ai-agents-env` off
|
|
57
|
+
* the list and no way to launch a remote sandbox from there. The Jupyter
|
|
58
|
+
* Server is the provider that depends on where this runs, and it is gated
|
|
59
|
+
* below.
|
|
60
|
+
*/
|
|
61
|
+
const jupyterAvailable = isCodeSandboxProviderAvailable('local', runtimesStore.getState().multiServiceManager);
|
|
49
62
|
const environments = manager.environments.get();
|
|
50
63
|
/*
|
|
51
64
|
* The sandboxes of this Jupyter Server, offered beside those of the platform.
|
|
@@ -57,6 +70,16 @@ export function CodeSandboxLauncher(props) {
|
|
|
57
70
|
* prefixed, since a kernelspec and an environment are free to share a name.
|
|
58
71
|
*/
|
|
59
72
|
const localSpecs = useMemo(() => {
|
|
73
|
+
/*
|
|
74
|
+
* ...and only inside JupyterLab.
|
|
75
|
+
*
|
|
76
|
+
* The rule cuts both ways: the web application talks to no Jupyter Server
|
|
77
|
+
* of its own, so a kernelspec of one is not something it can start — the
|
|
78
|
+
* entries read as choices that lead nowhere.
|
|
79
|
+
*/
|
|
80
|
+
if (!jupyterAvailable) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
60
83
|
const specs = runtimesStore.getState().multiServiceManager?.local?.kernelspecs.specs
|
|
61
84
|
?.kernelspecs ?? {};
|
|
62
85
|
return Object.values(specs)
|
|
@@ -66,8 +89,29 @@ export function CodeSandboxLauncher(props) {
|
|
|
66
89
|
title: spec.display_name,
|
|
67
90
|
language: spec.language,
|
|
68
91
|
}));
|
|
69
|
-
}, []);
|
|
92
|
+
}, [jupyterAvailable]);
|
|
70
93
|
const LOCAL_PREFIX = 'local:';
|
|
94
|
+
/*
|
|
95
|
+
* The choices of the dropdown: what the platform and the external
|
|
96
|
+
* providers offer, then the kernelspecs of this Jupyter Server. Each says
|
|
97
|
+
* whose machine it is — two entries called "GPU" are told apart by nothing
|
|
98
|
+
* else.
|
|
99
|
+
*/
|
|
100
|
+
const environmentOptions = useMemo(() => [
|
|
101
|
+
...environments.map(spec => ({
|
|
102
|
+
key: spec.name,
|
|
103
|
+
title: spec.title || spec.name,
|
|
104
|
+
name: spec.name,
|
|
105
|
+
providerTitle: codeSandboxVariantTitle(codeSandboxVariantOf(spec?.owner)),
|
|
106
|
+
burningRate: spec.burning_rate,
|
|
107
|
+
})),
|
|
108
|
+
...localSpecs.map(spec => ({
|
|
109
|
+
key: `${LOCAL_PREFIX}${spec.name}`,
|
|
110
|
+
title: spec.title || spec.name,
|
|
111
|
+
name: spec.name,
|
|
112
|
+
providerTitle: codeSandboxVariantTitle(CodeSandboxVariant.JupyterServer),
|
|
113
|
+
})),
|
|
114
|
+
], [environments, localSpecs]);
|
|
71
115
|
const localSpecOf = (value) => value.startsWith(LOCAL_PREFIX)
|
|
72
116
|
? localSpecs.find(spec => spec.name === value.slice(LOCAL_PREFIX.length))
|
|
73
117
|
: undefined;
|
|
@@ -85,6 +129,18 @@ export function CodeSandboxLauncher(props) {
|
|
|
85
129
|
const jupyterReactStore = useJupyterReactStore();
|
|
86
130
|
const jupyterLabAdapter = jupyterReactStore.jupyterLabAdapter;
|
|
87
131
|
const [selection, setSelection] = useState((kernelSnapshot?.environment || environments[0]?.name) ?? '');
|
|
132
|
+
/*
|
|
133
|
+
* Something is always selected.
|
|
134
|
+
*
|
|
135
|
+
* With the environments of the platform withheld, the first choice of the
|
|
136
|
+
* list is a sandbox of this server — and nothing was selected at all, so the
|
|
137
|
+
* launcher opened on an empty dropdown that could not be submitted.
|
|
138
|
+
*/
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!selection && localSpecs.length) {
|
|
141
|
+
setSelection(`${LOCAL_PREFIX}${localSpecs[0].name}`);
|
|
142
|
+
}
|
|
143
|
+
}, [localSpecs, selection]);
|
|
88
144
|
const [timeLimit, setTimeLimit] = useState(10);
|
|
89
145
|
/*
|
|
90
146
|
* The name the sandbox is given, and what is offered for it.
|
|
@@ -125,12 +181,11 @@ export function CodeSandboxLauncher(props) {
|
|
|
125
181
|
const allowance = useNewCodeSandboxAllowance(burningRate);
|
|
126
182
|
const effectiveMaxMinutes = allowance.effectiveMaxMinutes;
|
|
127
183
|
const outOfCredits = shouldStartRuntime && allowance.outOfCredits;
|
|
128
|
-
const handleSelectionChange = useCallback((
|
|
129
|
-
|
|
130
|
-
setSelection(selection);
|
|
184
|
+
const handleSelectionChange = useCallback((value) => {
|
|
185
|
+
setSelection(value);
|
|
131
186
|
if (!hasCustomRuntimeName) {
|
|
132
|
-
const local = localSpecOf(
|
|
133
|
-
setRuntimeName(givenNameFor(local ?? environments.find(env => env.name ===
|
|
187
|
+
const local = localSpecOf(value);
|
|
188
|
+
setRuntimeName(givenNameFor(local ?? environments.find(env => env.name === value)));
|
|
134
189
|
}
|
|
135
190
|
}, [environments, givenNameFor, setSelection, hasCustomRuntimeName]);
|
|
136
191
|
const handleSubmitRuntime = useCallback(async () => {
|
|
@@ -191,7 +246,36 @@ export function CodeSandboxLauncher(props) {
|
|
|
191
246
|
desc.params['capabilities'] = ['user_storage'];
|
|
192
247
|
}
|
|
193
248
|
let success = true;
|
|
194
|
-
|
|
249
|
+
const provider = codeSandboxVariantOf(spec?.owner);
|
|
250
|
+
if (shouldStartRuntime && provider !== CodeSandboxVariant.Datalayer) {
|
|
251
|
+
/*
|
|
252
|
+
* An EXTERNAL environment — Kaggle, Modal. The platform RECORDS such
|
|
253
|
+
* a sandbox rather than running it: the provider runs it under the
|
|
254
|
+
* user's own credentials, and what comes back is a record with no
|
|
255
|
+
* pod, no ingress and no kernel. Waiting for one, as the branch
|
|
256
|
+
* below does, would time out and then delete the record as broken.
|
|
257
|
+
*/
|
|
258
|
+
success = false;
|
|
259
|
+
try {
|
|
260
|
+
await createRuntimeRecord({
|
|
261
|
+
environmentName: selection,
|
|
262
|
+
type: 'notebook',
|
|
263
|
+
givenName: runtimeName,
|
|
264
|
+
creditsLimit,
|
|
265
|
+
});
|
|
266
|
+
await manager.runtimesManager.refresh();
|
|
267
|
+
success = true;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
console.error('Failed to record the external sandbox.', error);
|
|
271
|
+
setFlashLevel('danger');
|
|
272
|
+
setError(_jsxs(Text, { children: ["The ", codeSandboxVariantTitle(provider), " sandbox could not be created."] }));
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
setWaitingForRuntime(false);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else if (shouldStartRuntime) {
|
|
195
279
|
success = false;
|
|
196
280
|
let availableTrial = 1;
|
|
197
281
|
let retryDelay = NOT_AVAILABLE_INIT_RETRY;
|
|
@@ -334,7 +418,7 @@ export function CodeSandboxLauncher(props) {
|
|
|
334
418
|
event.preventDefault();
|
|
335
419
|
handleSubmitRuntime();
|
|
336
420
|
}
|
|
337
|
-
}, children: [_jsxs(FormControl, { disabled: !!kernelSnapshot?.environment || environments.length === 0, children: [_jsx(FormControl.Label, { children: "Environment" }),
|
|
421
|
+
}, children: [_jsxs(FormControl, { disabled: !!kernelSnapshot?.environment || environments.length === 0, children: [_jsx(FormControl.Label, { children: "Environment" }), _jsx(CodeSandboxEnvironmentSelect, { disabled: !!kernelSnapshot?.environment || environments.length === 0, options: environmentOptions, selectedKey: selection, onSelect: handleSelectionChange }), _jsx(FormControl.Caption, { children: _jsx(_Fragment, { children: markdownParser ? (_jsx(Box, { sx: { img: { maxWidth: '100%' } }, children: _jsx(Markdown, { text: description, markdownParser: markdownParser, sanitizer: sanitizer }) })) : sanitizer ? (
|
|
338
422
|
// The description of an environment carries markup. With no
|
|
339
423
|
// parser to render it, the sanitizer of the application is
|
|
340
424
|
// enough to show it as what it is rather than as its tags.
|
|
@@ -351,7 +435,7 @@ export function CodeSandboxLauncher(props) {
|
|
|
351
435
|
// no time to reserve and no storage of the platform to mount.
|
|
352
436
|
withReservation: !!startRuntime && !isLocalSelection, withUserStorage: !isLocalSelection, burningRate: burningRate, timeLimit: timeLimit, onTimeChange: setTimeLimit, max: effectiveMaxMinutes, disabled: outOfCredits, error: outOfCredits ? 'You must add credits to your account.' : undefined, addCredits: navigate
|
|
353
437
|
? () => {
|
|
354
|
-
navigate(
|
|
438
|
+
navigate(usageRoute);
|
|
355
439
|
}
|
|
356
440
|
: undefined, userStorage: userStorage, onUserStorageToggle: () => setUserStorage(current => !current) }), _jsxs(FormControl, { sx: { paddingTop: '10px' }, children: [_jsx(FormControl.Label, { children: "Given Name" }), _jsx(TextInput, { name: "name", value: runtimeName, onChange: handleKernelNameChange, block: true })] }), hasExample &&
|
|
357
441
|
jupyterLabAdapter?.jupyterLab &&
|
|
@@ -32,25 +32,56 @@
|
|
|
32
32
|
*/
|
|
33
33
|
/** Where the names are kept, and under which key. */
|
|
34
34
|
const STORAGE_KEY = 'datalayer:code-sandbox-names';
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
/** When each name was written, so pruning can spare what is brand new. */
|
|
36
|
+
const STAMP_KEY = 'datalayer:code-sandbox-names:at';
|
|
37
|
+
/**
|
|
38
|
+
* How long a fresh name is safe from pruning.
|
|
39
|
+
*
|
|
40
|
+
* Pruning compares the names against a list of running kernels, and that list
|
|
41
|
+
* is a POLL: a sweep that was in flight while a sandbox was created and named
|
|
42
|
+
* finishes with a list from before it existed, and deleted the name the user
|
|
43
|
+
* had just typed — every surface then agreed on the renumbered default. A
|
|
44
|
+
* kernel identifier is never reused, so sparing a young name costs nothing:
|
|
45
|
+
* a truly dead one is collected on the sweeps after the grace.
|
|
46
|
+
*/
|
|
47
|
+
const PRUNE_GRACE_MS = 10 * 60 * 1000;
|
|
48
|
+
/** The fallback for a browser that refuses its storage: this page only. */
|
|
49
|
+
let memory = {};
|
|
50
|
+
let memoryStamps = {};
|
|
51
|
+
function read(key) {
|
|
41
52
|
try {
|
|
42
|
-
const raw = globalThis.localStorage?.getItem(
|
|
43
|
-
|
|
53
|
+
const raw = globalThis.localStorage?.getItem(key);
|
|
54
|
+
return raw ? JSON.parse(raw) : {};
|
|
44
55
|
}
|
|
45
56
|
catch {
|
|
46
|
-
|
|
47
|
-
names = {};
|
|
57
|
+
return undefined;
|
|
48
58
|
}
|
|
49
|
-
return names;
|
|
50
59
|
}
|
|
51
|
-
|
|
60
|
+
/*
|
|
61
|
+
* Read on every call, never cached in the module.
|
|
62
|
+
*
|
|
63
|
+
* A cached copy is a second truth: another instance of this module — another
|
|
64
|
+
* tab, another bundle holding its own copy — writes the storage, the cache
|
|
65
|
+
* here goes stale, and the next save writes the stale cache back over what
|
|
66
|
+
* the other instance stored. The maps are a handful of entries; reading them
|
|
67
|
+
* each time is nothing.
|
|
68
|
+
*/
|
|
69
|
+
function load() {
|
|
70
|
+
const stored = read(STORAGE_KEY);
|
|
71
|
+
return stored === undefined ? memory : stored;
|
|
72
|
+
}
|
|
73
|
+
function loadStamps() {
|
|
74
|
+
const stored = read(STAMP_KEY);
|
|
75
|
+
return stored === undefined
|
|
76
|
+
? memoryStamps
|
|
77
|
+
: stored;
|
|
78
|
+
}
|
|
79
|
+
function save(names, stamps) {
|
|
80
|
+
memory = names;
|
|
81
|
+
memoryStamps = stamps;
|
|
52
82
|
try {
|
|
53
|
-
globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(
|
|
83
|
+
globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(names));
|
|
84
|
+
globalThis.localStorage?.setItem(STAMP_KEY, JSON.stringify(stamps));
|
|
54
85
|
}
|
|
55
86
|
catch {
|
|
56
87
|
/* the names stay in memory */
|
|
@@ -71,14 +102,17 @@ export function getCodeSandboxGivenName(kernelId) {
|
|
|
71
102
|
* @param givenName What to call it; empty forgets the name
|
|
72
103
|
*/
|
|
73
104
|
export function setCodeSandboxGivenName(kernelId, givenName) {
|
|
74
|
-
const
|
|
105
|
+
const names = load();
|
|
106
|
+
const stamps = loadStamps();
|
|
75
107
|
if (givenName) {
|
|
76
|
-
|
|
108
|
+
names[kernelId] = asGivenName(givenName);
|
|
109
|
+
stamps[kernelId] = Date.now();
|
|
77
110
|
}
|
|
78
111
|
else {
|
|
79
|
-
delete
|
|
112
|
+
delete names[kernelId];
|
|
113
|
+
delete stamps[kernelId];
|
|
80
114
|
}
|
|
81
|
-
save();
|
|
115
|
+
save(names, stamps);
|
|
82
116
|
}
|
|
83
117
|
/**
|
|
84
118
|
* Drop the names of the sandboxes that are no longer running.
|
|
@@ -87,16 +121,27 @@ export function setCodeSandboxGivenName(kernelId, givenName) {
|
|
|
87
121
|
*/
|
|
88
122
|
export function pruneCodeSandboxGivenNames(runningKernelIds) {
|
|
89
123
|
const alive = new Set(runningKernelIds);
|
|
90
|
-
const
|
|
124
|
+
const names = load();
|
|
125
|
+
const stamps = loadStamps();
|
|
126
|
+
const now = Date.now();
|
|
91
127
|
let dropped = false;
|
|
92
|
-
Object.keys(
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
128
|
+
Object.keys(names).forEach(kernelId => {
|
|
129
|
+
if (alive.has(kernelId)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Not in the list is not proof of death: the list is a poll, and a
|
|
133
|
+
// sandbox named a moment ago may not be in it yet. The young are spared;
|
|
134
|
+
// see PRUNE_GRACE_MS.
|
|
135
|
+
const namedAt = stamps[kernelId];
|
|
136
|
+
if (typeof namedAt === 'number' && now - namedAt < PRUNE_GRACE_MS) {
|
|
137
|
+
return;
|
|
96
138
|
}
|
|
139
|
+
delete names[kernelId];
|
|
140
|
+
delete stamps[kernelId];
|
|
141
|
+
dropped = true;
|
|
97
142
|
});
|
|
98
143
|
if (dropped) {
|
|
99
|
-
save();
|
|
144
|
+
save(names, stamps);
|
|
100
145
|
}
|
|
101
146
|
}
|
|
102
147
|
/**
|
|
@@ -4,7 +4,7 @@ import { ITranslator } from '@jupyterlab/translation';
|
|
|
4
4
|
import { IRuntimeDesc } from '../../models';
|
|
5
5
|
import { type IMultiServiceManager } from '../../runtimes';
|
|
6
6
|
import type { CodeSandboxTransfer } from './CodeSandboxTransfer';
|
|
7
|
-
type IDisplayMode = 'menu' | 'radio';
|
|
7
|
+
type IDisplayMode = 'menu' | 'radio' | 'dropdown';
|
|
8
8
|
/**
|
|
9
9
|
* {@link CodeSandboxPicker} properties
|
|
10
10
|
*/
|
|
@@ -13,6 +13,19 @@ export interface ICodeSandboxPickerProps {
|
|
|
13
13
|
* The display mode.
|
|
14
14
|
*/
|
|
15
15
|
display: IDisplayMode;
|
|
16
|
+
/**
|
|
17
|
+
* Where the sandboxes that ALREADY RUN are offered, in the `dropdown`
|
|
18
|
+
* display.
|
|
19
|
+
*
|
|
20
|
+
* `separated`, the default: as radio buttons of their own, above a
|
|
21
|
+
* dropdown that then offers only the environments — so the dropdown reads
|
|
22
|
+
* as "start a new one" and the running sandboxes are all in sight, which
|
|
23
|
+
* is what picking one of them is usually about.
|
|
24
|
+
*
|
|
25
|
+
* `combined`: everything in the one dropdown, running sandboxes and
|
|
26
|
+
* environments alike, under their headings.
|
|
27
|
+
*/
|
|
28
|
+
sandboxLayout?: 'separated' | 'combined';
|
|
16
29
|
/**
|
|
17
30
|
* Additional actions items to be placed at the top of the picker
|
|
18
31
|
*/
|