@datalayer/agent-runtimes 1.0.26 → 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 +190 -17
- package/lib/components/code-sandboxes/CodeSandboxNames.d.ts +47 -0
- package/lib/components/code-sandboxes/CodeSandboxNames.js +211 -0
- package/lib/components/code-sandboxes/CodeSandboxPicker.d.ts +14 -1
- package/lib/components/code-sandboxes/CodeSandboxPicker.js +203 -18
- package/lib/components/code-sandboxes/CodeSandboxTransfer.d.ts +6 -0
- package/lib/components/code-sandboxes/CodeSandboxUtils.d.ts +3 -0
- package/lib/components/code-sandboxes/CodeSandboxUtils.js +76 -20
- package/lib/components/code-sandboxes/NewCodeSandboxControls.d.ts +17 -0
- package/lib/components/code-sandboxes/NewCodeSandboxControls.js +4 -4
- 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 +3 -0
- package/lib/components/code-sandboxes/index.js +3 -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
|
*/
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/*
|
|
3
3
|
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
4
|
* Distributed under the terms of the Modified BSD License.
|
|
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';
|
|
15
|
-
import {
|
|
14
|
+
import { isCodeSandboxProviderAvailable } from './codeSandboxProviders';
|
|
15
|
+
import { CodeSandboxVariant, codeSandboxVariantOf, codeSandboxVariantTitle, } from '../../models/CodeSandboxVariant';
|
|
16
|
+
import { CodeSandboxEnvironmentSelect, } from './CodeSandboxEnvironmentSelect';
|
|
17
|
+
import { getCodeSandboxGivenName, listCodeSandboxGivenNames, nextCodeSandboxGivenName, setCodeSandboxGivenName, } from './CodeSandboxNames';
|
|
18
|
+
import { iamStore, runtimesStore, useCoreStore, useIAMStore, } from '../../state/substates';
|
|
19
|
+
import { createRuntime as createRuntimeRecord } from '../../runtimes/actions';
|
|
16
20
|
import { createNotebook, sleep } from '@datalayer/core/lib/utils';
|
|
17
21
|
import { Markdown } from '@datalayer/core/lib/components/display';
|
|
18
22
|
import { Timer } from '@datalayer/core/lib/components/progress';
|
|
@@ -41,11 +45,76 @@ const NOT_AVAILABLE_RETRIES = 5;
|
|
|
41
45
|
* The launcher of a new code sandbox.
|
|
42
46
|
*/
|
|
43
47
|
export function CodeSandboxLauncher(props) {
|
|
44
|
-
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;
|
|
45
49
|
const hasExample = startRuntime === 'with-example';
|
|
46
50
|
const shouldStartRuntime = startRuntime !== 'defer';
|
|
47
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);
|
|
48
62
|
const environments = manager.environments.get();
|
|
63
|
+
/*
|
|
64
|
+
* The sandboxes of this Jupyter Server, offered beside those of the platform.
|
|
65
|
+
*
|
|
66
|
+
* Inside JupyterLab a sandbox can be a kernel of the server, and the picker
|
|
67
|
+
* has always offered both — the launcher offered only the environments of
|
|
68
|
+
* the platform, so "Launch a Code Sandbox" could not launch the very kind
|
|
69
|
+
* that costs nothing and starts at once. The value of a local choice is
|
|
70
|
+
* prefixed, since a kernelspec and an environment are free to share a name.
|
|
71
|
+
*/
|
|
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
|
+
}
|
|
83
|
+
const specs = runtimesStore.getState().multiServiceManager?.local?.kernelspecs.specs
|
|
84
|
+
?.kernelspecs ?? {};
|
|
85
|
+
return Object.values(specs)
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.map(spec => ({
|
|
88
|
+
name: spec.name,
|
|
89
|
+
title: spec.display_name,
|
|
90
|
+
language: spec.language,
|
|
91
|
+
}));
|
|
92
|
+
}, [jupyterAvailable]);
|
|
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]);
|
|
115
|
+
const localSpecOf = (value) => value.startsWith(LOCAL_PREFIX)
|
|
116
|
+
? localSpecs.find(spec => spec.name === value.slice(LOCAL_PREFIX.length))
|
|
117
|
+
: undefined;
|
|
49
118
|
const { configuration } = useCoreStore();
|
|
50
119
|
const { refreshCredits } = useIAMStore();
|
|
51
120
|
let navigate;
|
|
@@ -60,8 +129,37 @@ export function CodeSandboxLauncher(props) {
|
|
|
60
129
|
const jupyterReactStore = useJupyterReactStore();
|
|
61
130
|
const jupyterLabAdapter = jupyterReactStore.jupyterLabAdapter;
|
|
62
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]);
|
|
63
144
|
const [timeLimit, setTimeLimit] = useState(10);
|
|
64
|
-
|
|
145
|
+
/*
|
|
146
|
+
* The name the sandbox is given, and what is offered for it.
|
|
147
|
+
*
|
|
148
|
+
* The environment says what to call one of its sandboxes; the sandboxes that
|
|
149
|
+
* already run say which of those names are taken — a second sandbox of an
|
|
150
|
+
* environment is offered that name numbered, exactly as an unnamed one is
|
|
151
|
+
* given one. Both go through `nextCodeSandboxGivenName`, so what is proposed
|
|
152
|
+
* here and what is assigned elsewhere never drift apart.
|
|
153
|
+
*/
|
|
154
|
+
const takenGivenNames = useCallback(() => [
|
|
155
|
+
...manager.runtimesManager
|
|
156
|
+
.get()
|
|
157
|
+
.map(runtime => runtime.given_name)
|
|
158
|
+
.filter(Boolean),
|
|
159
|
+
...listCodeSandboxGivenNames(),
|
|
160
|
+
], [manager]);
|
|
161
|
+
const givenNameFor = useCallback((spec) => nextCodeSandboxGivenName(spec?.runtime?.givenNameTemplate || spec?.title || '', takenGivenNames()), [takenGivenNames]);
|
|
162
|
+
const [runtimeName, setRuntimeName] = useState(() => givenNameFor(environments[0]));
|
|
65
163
|
// Whether the runtim name has been changed by the user or not
|
|
66
164
|
const [hasCustomRuntimeName, setHasCustomRuntimeName] = useState(false);
|
|
67
165
|
const [userStorage, setUserStorage] = useState(false);
|
|
@@ -75,6 +173,7 @@ export function CodeSandboxLauncher(props) {
|
|
|
75
173
|
refreshCredits();
|
|
76
174
|
}
|
|
77
175
|
}, [shouldStartRuntime]);
|
|
176
|
+
const isLocalSelection = selection.startsWith(LOCAL_PREFIX);
|
|
78
177
|
const spec = useMemo(() => environments.find(spec => spec.name === selection), [environments, selection]);
|
|
79
178
|
const description = spec?.description ?? '';
|
|
80
179
|
const burningRate = spec?.burning_rate ?? 1;
|
|
@@ -82,15 +181,52 @@ export function CodeSandboxLauncher(props) {
|
|
|
82
181
|
const allowance = useNewCodeSandboxAllowance(burningRate);
|
|
83
182
|
const effectiveMaxMinutes = allowance.effectiveMaxMinutes;
|
|
84
183
|
const outOfCredits = shouldStartRuntime && allowance.outOfCredits;
|
|
85
|
-
const handleSelectionChange = useCallback((
|
|
86
|
-
|
|
87
|
-
setSelection(selection);
|
|
184
|
+
const handleSelectionChange = useCallback((value) => {
|
|
185
|
+
setSelection(value);
|
|
88
186
|
if (!hasCustomRuntimeName) {
|
|
89
|
-
const
|
|
90
|
-
setRuntimeName(
|
|
187
|
+
const local = localSpecOf(value);
|
|
188
|
+
setRuntimeName(givenNameFor(local ?? environments.find(env => env.name === value)));
|
|
91
189
|
}
|
|
92
|
-
}, [setSelection, hasCustomRuntimeName]);
|
|
190
|
+
}, [environments, givenNameFor, setSelection, hasCustomRuntimeName]);
|
|
93
191
|
const handleSubmitRuntime = useCallback(async () => {
|
|
192
|
+
const localSpec = localSpecOf(selection);
|
|
193
|
+
if (localSpec) {
|
|
194
|
+
/*
|
|
195
|
+
* A sandbox of this Jupyter Server: started here, named here.
|
|
196
|
+
*
|
|
197
|
+
* Nothing to reserve, no credits to weigh, no pod to wait for — the
|
|
198
|
+
* kernel is started and the choice is handed back as any other, with
|
|
199
|
+
* the name the user gave it, which is the local answer to `given_name`.
|
|
200
|
+
*/
|
|
201
|
+
setError(undefined);
|
|
202
|
+
const kernels = runtimesStore.getState().multiServiceManager?.local?.kernels;
|
|
203
|
+
if (!kernels) {
|
|
204
|
+
setError(_jsx(_Fragment, { children: "The Jupyter Server of this page is not reachable." }));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
setWaitingForRuntime(true);
|
|
208
|
+
try {
|
|
209
|
+
const connection = await kernels.startNew({ name: localSpec.name });
|
|
210
|
+
setCodeSandboxGivenName(connection.id, runtimeName);
|
|
211
|
+
onSubmit({
|
|
212
|
+
kernelId: connection.id,
|
|
213
|
+
name: localSpec.name,
|
|
214
|
+
language: localSpec.language ?? '',
|
|
215
|
+
location: 'local',
|
|
216
|
+
displayName: getCodeSandboxGivenName(connection.id) ?? localSpec.title,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
catch (reason) {
|
|
220
|
+
console.warn('Failed to start a code sandbox of this server.', reason);
|
|
221
|
+
setError(_jsx(_Fragment, { children: "The code sandbox could not be started." }));
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
if (isMounted()) {
|
|
225
|
+
setWaitingForRuntime(false);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
94
230
|
if (selection) {
|
|
95
231
|
setError(undefined);
|
|
96
232
|
setWaitingForRuntime(shouldStartRuntime);
|
|
@@ -110,7 +246,36 @@ export function CodeSandboxLauncher(props) {
|
|
|
110
246
|
desc.params['capabilities'] = ['user_storage'];
|
|
111
247
|
}
|
|
112
248
|
let success = true;
|
|
113
|
-
|
|
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) {
|
|
114
279
|
success = false;
|
|
115
280
|
let availableTrial = 1;
|
|
116
281
|
let retryDelay = NOT_AVAILABLE_INIT_RETRY;
|
|
@@ -195,6 +360,8 @@ export function CodeSandboxLauncher(props) {
|
|
|
195
360
|
onSubmit(desc);
|
|
196
361
|
}
|
|
197
362
|
}
|
|
363
|
+
// `localSpecOf` closes over `localSpecs`, which is built once.
|
|
364
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
198
365
|
}, [
|
|
199
366
|
manager,
|
|
200
367
|
selection,
|
|
@@ -251,7 +418,7 @@ export function CodeSandboxLauncher(props) {
|
|
|
251
418
|
event.preventDefault();
|
|
252
419
|
handleSubmitRuntime();
|
|
253
420
|
}
|
|
254
|
-
}, children: [_jsxs(FormControl, { disabled: !!kernelSnapshot?.environment || environments.length === 0, children: [_jsx(FormControl.Label, { children: "Environment" }), _jsx(
|
|
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 ? (
|
|
255
422
|
// The description of an environment carries markup. With no
|
|
256
423
|
// parser to render it, the sanitizer of the application is
|
|
257
424
|
// enough to show it as what it is rather than as its tags.
|
|
@@ -260,11 +427,17 @@ export function CodeSandboxLauncher(props) {
|
|
|
260
427
|
} })) : (
|
|
261
428
|
// Neither a parser nor a sanitizer: the text of the markup,
|
|
262
429
|
// which is still better than the markup itself.
|
|
263
|
-
asPlainText(description)) }) })] }), _jsx(NewCodeSandboxControls
|
|
430
|
+
asPlainText(description)) }) })] }), _jsx(NewCodeSandboxControls
|
|
431
|
+
// A sandbox of this server costs nothing and starts at once: it has
|
|
432
|
+
// no time to reserve and no storage of the platform to mount.
|
|
433
|
+
, {
|
|
434
|
+
// A sandbox of this server costs nothing and starts at once: it has
|
|
435
|
+
// no time to reserve and no storage of the platform to mount.
|
|
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
|
|
264
437
|
? () => {
|
|
265
|
-
navigate(
|
|
438
|
+
navigate(usageRoute);
|
|
266
439
|
}
|
|
267
|
-
: undefined, userStorage: userStorage, onUserStorageToggle: () => setUserStorage(current => !current) }), _jsxs(FormControl, { sx: { paddingTop: '10px' }, children: [_jsx(FormControl.Label, { children: "
|
|
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 &&
|
|
268
441
|
jupyterLabAdapter?.jupyterLab &&
|
|
269
442
|
!configuration.whiteLabel && (_jsxs(FormControl, { sx: { paddingTop: '10px' }, children: [_jsx(FormControl.Label, { id: "open-example-label", children: "Open example notebook" }), _jsx(ToggleSwitch, { disabled: !environments.find(spec => spec.name === selection)?.example, checked: openExample, size: "small", onClick: handleSwitchClick, "aria-labelledby": "open-example-label" })] })), error && (_jsx(FlashClosable, { variant: flashLevel, actions: navigate && upgradeSubscription && flashLevel === 'warning' ? (_jsx(Button, { onClick: handleUpgrade, title: 'Upgrade your subscription.', children: "Upgrade" })) : undefined, children: error }))] }) }));
|
|
270
443
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The given name of a sandbox, when it has one.
|
|
3
|
+
*
|
|
4
|
+
* @param kernelId Kernel of the sandbox
|
|
5
|
+
*/
|
|
6
|
+
export declare function getCodeSandboxGivenName(kernelId?: string): string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Name a sandbox, or rename it.
|
|
9
|
+
*
|
|
10
|
+
* @param kernelId Kernel of the sandbox
|
|
11
|
+
* @param givenName What to call it; empty forgets the name
|
|
12
|
+
*/
|
|
13
|
+
export declare function setCodeSandboxGivenName(kernelId: string, givenName: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* Drop the names of the sandboxes that are no longer running.
|
|
16
|
+
*
|
|
17
|
+
* @param runningKernelIds The kernels that are still there
|
|
18
|
+
*/
|
|
19
|
+
export declare function pruneCodeSandboxGivenNames(runningKernelIds: Iterable<string>): void;
|
|
20
|
+
/**
|
|
21
|
+
* Every given name in use by a sandbox of a server or of the browser.
|
|
22
|
+
*/
|
|
23
|
+
export declare function listCodeSandboxGivenNames(): string[];
|
|
24
|
+
/**
|
|
25
|
+
* The next free name built on a base, numbered when the base is taken.
|
|
26
|
+
*
|
|
27
|
+
* "Python 3 (ipykernel)", then "Python 3 (ipykernel) 2", then 3 — one rule for
|
|
28
|
+
* the name a sandbox is given on first sight and for the name the launcher
|
|
29
|
+
* offers, so what the launcher proposes is what an unnamed one would get.
|
|
30
|
+
*
|
|
31
|
+
* @param base What to call it, before any numbering
|
|
32
|
+
* @param taken The names already in use
|
|
33
|
+
*/
|
|
34
|
+
export declare function nextCodeSandboxGivenName(base: string, taken: Iterable<string>): string;
|
|
35
|
+
/**
|
|
36
|
+
* The given name of a sandbox, giving it one when it has none.
|
|
37
|
+
*
|
|
38
|
+
* The name of what it runs, and a number when a sandbox of that kind is
|
|
39
|
+
* already named — the way two runtimes of one environment are told apart.
|
|
40
|
+
* Assigned once and kept: a name that changed with what else is running would
|
|
41
|
+
* be no name at all.
|
|
42
|
+
*
|
|
43
|
+
* @param kernelId Kernel of the sandbox
|
|
44
|
+
* @param specDisplayName What the kernelspec of the sandbox is called
|
|
45
|
+
*/
|
|
46
|
+
export declare function ensureCodeSandboxGivenName(kernelId: string, specDisplayName: string): string;
|
|
47
|
+
export default ensureCodeSandboxGivenName;
|