@datalayer/agent-runtimes 1.0.26 → 1.0.28
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/components/code-sandboxes/CodeSandboxLauncher.js +98 -9
- package/lib/components/code-sandboxes/CodeSandboxNames.d.ts +47 -0
- package/lib/components/code-sandboxes/CodeSandboxNames.js +166 -0
- package/lib/components/code-sandboxes/CodeSandboxPicker.js +54 -3
- package/lib/components/code-sandboxes/CodeSandboxTransfer.d.ts +6 -0
- package/lib/components/code-sandboxes/CodeSandboxUtils.js +14 -9
- package/lib/components/code-sandboxes/NewCodeSandboxControls.d.ts +17 -0
- package/lib/components/code-sandboxes/NewCodeSandboxControls.js +3 -3
- package/lib/components/code-sandboxes/index.d.ts +1 -0
- package/lib/components/code-sandboxes/index.js +1 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
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.
|
|
@@ -12,7 +12,8 @@ import { useJupyterReactStore } from '@datalayer/jupyter-react';
|
|
|
12
12
|
import { USAGE_ROUTE } from '@datalayer/core/lib/routes';
|
|
13
13
|
import { useNavigate } from '@datalayer/core/lib/hooks';
|
|
14
14
|
import { NO_RUNTIME_AVAILABLE_LABEL } from '@datalayer/core/lib/i18n';
|
|
15
|
-
import {
|
|
15
|
+
import { getCodeSandboxGivenName, listCodeSandboxGivenNames, nextCodeSandboxGivenName, setCodeSandboxGivenName, } from './CodeSandboxNames';
|
|
16
|
+
import { iamStore, runtimesStore, useCoreStore, useIAMStore, } from '../../state/substates';
|
|
16
17
|
import { createNotebook, sleep } from '@datalayer/core/lib/utils';
|
|
17
18
|
import { Markdown } from '@datalayer/core/lib/components/display';
|
|
18
19
|
import { Timer } from '@datalayer/core/lib/components/progress';
|
|
@@ -46,6 +47,30 @@ export function CodeSandboxLauncher(props) {
|
|
|
46
47
|
const shouldStartRuntime = startRuntime !== 'defer';
|
|
47
48
|
const user = iamStore.getState().user;
|
|
48
49
|
const environments = manager.environments.get();
|
|
50
|
+
/*
|
|
51
|
+
* The sandboxes of this Jupyter Server, offered beside those of the platform.
|
|
52
|
+
*
|
|
53
|
+
* Inside JupyterLab a sandbox can be a kernel of the server, and the picker
|
|
54
|
+
* has always offered both — the launcher offered only the environments of
|
|
55
|
+
* the platform, so "Launch a Code Sandbox" could not launch the very kind
|
|
56
|
+
* that costs nothing and starts at once. The value of a local choice is
|
|
57
|
+
* prefixed, since a kernelspec and an environment are free to share a name.
|
|
58
|
+
*/
|
|
59
|
+
const localSpecs = useMemo(() => {
|
|
60
|
+
const specs = runtimesStore.getState().multiServiceManager?.local?.kernelspecs.specs
|
|
61
|
+
?.kernelspecs ?? {};
|
|
62
|
+
return Object.values(specs)
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.map(spec => ({
|
|
65
|
+
name: spec.name,
|
|
66
|
+
title: spec.display_name,
|
|
67
|
+
language: spec.language,
|
|
68
|
+
}));
|
|
69
|
+
}, []);
|
|
70
|
+
const LOCAL_PREFIX = 'local:';
|
|
71
|
+
const localSpecOf = (value) => value.startsWith(LOCAL_PREFIX)
|
|
72
|
+
? localSpecs.find(spec => spec.name === value.slice(LOCAL_PREFIX.length))
|
|
73
|
+
: undefined;
|
|
49
74
|
const { configuration } = useCoreStore();
|
|
50
75
|
const { refreshCredits } = useIAMStore();
|
|
51
76
|
let navigate;
|
|
@@ -61,7 +86,24 @@ export function CodeSandboxLauncher(props) {
|
|
|
61
86
|
const jupyterLabAdapter = jupyterReactStore.jupyterLabAdapter;
|
|
62
87
|
const [selection, setSelection] = useState((kernelSnapshot?.environment || environments[0]?.name) ?? '');
|
|
63
88
|
const [timeLimit, setTimeLimit] = useState(10);
|
|
64
|
-
|
|
89
|
+
/*
|
|
90
|
+
* The name the sandbox is given, and what is offered for it.
|
|
91
|
+
*
|
|
92
|
+
* The environment says what to call one of its sandboxes; the sandboxes that
|
|
93
|
+
* already run say which of those names are taken — a second sandbox of an
|
|
94
|
+
* environment is offered that name numbered, exactly as an unnamed one is
|
|
95
|
+
* given one. Both go through `nextCodeSandboxGivenName`, so what is proposed
|
|
96
|
+
* here and what is assigned elsewhere never drift apart.
|
|
97
|
+
*/
|
|
98
|
+
const takenGivenNames = useCallback(() => [
|
|
99
|
+
...manager.runtimesManager
|
|
100
|
+
.get()
|
|
101
|
+
.map(runtime => runtime.given_name)
|
|
102
|
+
.filter(Boolean),
|
|
103
|
+
...listCodeSandboxGivenNames(),
|
|
104
|
+
], [manager]);
|
|
105
|
+
const givenNameFor = useCallback((spec) => nextCodeSandboxGivenName(spec?.runtime?.givenNameTemplate || spec?.title || '', takenGivenNames()), [takenGivenNames]);
|
|
106
|
+
const [runtimeName, setRuntimeName] = useState(() => givenNameFor(environments[0]));
|
|
65
107
|
// Whether the runtim name has been changed by the user or not
|
|
66
108
|
const [hasCustomRuntimeName, setHasCustomRuntimeName] = useState(false);
|
|
67
109
|
const [userStorage, setUserStorage] = useState(false);
|
|
@@ -75,6 +117,7 @@ export function CodeSandboxLauncher(props) {
|
|
|
75
117
|
refreshCredits();
|
|
76
118
|
}
|
|
77
119
|
}, [shouldStartRuntime]);
|
|
120
|
+
const isLocalSelection = selection.startsWith(LOCAL_PREFIX);
|
|
78
121
|
const spec = useMemo(() => environments.find(spec => spec.name === selection), [environments, selection]);
|
|
79
122
|
const description = spec?.description ?? '';
|
|
80
123
|
const burningRate = spec?.burning_rate ?? 1;
|
|
@@ -86,11 +129,49 @@ export function CodeSandboxLauncher(props) {
|
|
|
86
129
|
const selection = e.target.value;
|
|
87
130
|
setSelection(selection);
|
|
88
131
|
if (!hasCustomRuntimeName) {
|
|
89
|
-
const
|
|
90
|
-
setRuntimeName(
|
|
132
|
+
const local = localSpecOf(selection);
|
|
133
|
+
setRuntimeName(givenNameFor(local ?? environments.find(env => env.name === selection)));
|
|
91
134
|
}
|
|
92
|
-
}, [setSelection, hasCustomRuntimeName]);
|
|
135
|
+
}, [environments, givenNameFor, setSelection, hasCustomRuntimeName]);
|
|
93
136
|
const handleSubmitRuntime = useCallback(async () => {
|
|
137
|
+
const localSpec = localSpecOf(selection);
|
|
138
|
+
if (localSpec) {
|
|
139
|
+
/*
|
|
140
|
+
* A sandbox of this Jupyter Server: started here, named here.
|
|
141
|
+
*
|
|
142
|
+
* Nothing to reserve, no credits to weigh, no pod to wait for — the
|
|
143
|
+
* kernel is started and the choice is handed back as any other, with
|
|
144
|
+
* the name the user gave it, which is the local answer to `given_name`.
|
|
145
|
+
*/
|
|
146
|
+
setError(undefined);
|
|
147
|
+
const kernels = runtimesStore.getState().multiServiceManager?.local?.kernels;
|
|
148
|
+
if (!kernels) {
|
|
149
|
+
setError(_jsx(_Fragment, { children: "The Jupyter Server of this page is not reachable." }));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
setWaitingForRuntime(true);
|
|
153
|
+
try {
|
|
154
|
+
const connection = await kernels.startNew({ name: localSpec.name });
|
|
155
|
+
setCodeSandboxGivenName(connection.id, runtimeName);
|
|
156
|
+
onSubmit({
|
|
157
|
+
kernelId: connection.id,
|
|
158
|
+
name: localSpec.name,
|
|
159
|
+
language: localSpec.language ?? '',
|
|
160
|
+
location: 'local',
|
|
161
|
+
displayName: getCodeSandboxGivenName(connection.id) ?? localSpec.title,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (reason) {
|
|
165
|
+
console.warn('Failed to start a code sandbox of this server.', reason);
|
|
166
|
+
setError(_jsx(_Fragment, { children: "The code sandbox could not be started." }));
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
if (isMounted()) {
|
|
170
|
+
setWaitingForRuntime(false);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
94
175
|
if (selection) {
|
|
95
176
|
setError(undefined);
|
|
96
177
|
setWaitingForRuntime(shouldStartRuntime);
|
|
@@ -195,6 +276,8 @@ export function CodeSandboxLauncher(props) {
|
|
|
195
276
|
onSubmit(desc);
|
|
196
277
|
}
|
|
197
278
|
}
|
|
279
|
+
// `localSpecOf` closes over `localSpecs`, which is built once.
|
|
280
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
198
281
|
}, [
|
|
199
282
|
manager,
|
|
200
283
|
selection,
|
|
@@ -251,7 +334,7 @@ export function CodeSandboxLauncher(props) {
|
|
|
251
334
|
event.preventDefault();
|
|
252
335
|
handleSubmitRuntime();
|
|
253
336
|
}
|
|
254
|
-
}, children: [_jsxs(FormControl, { disabled: !!kernelSnapshot?.environment || environments.length === 0, children: [_jsx(FormControl.Label, { children: "Environment" }),
|
|
337
|
+
}, children: [_jsxs(FormControl, { disabled: !!kernelSnapshot?.environment || environments.length === 0, children: [_jsx(FormControl.Label, { children: "Environment" }), _jsxs(Select, { name: "environment", disabled: !!kernelSnapshot?.environment || environments.length === 0, value: selection, onChange: handleSelectionChange, block: true, children: [environments.map(spec => (_jsxs(Select.Option, { value: spec.name, children: [spec.name, spec.title && (_jsxs(_Fragment, { children: [' - ', spec.title] }))] }, spec.name))), localSpecs.map(spec => (_jsxs(Select.Option, { value: `${LOCAL_PREFIX}${spec.name}`, children: [spec.name, spec.title && _jsx(_Fragment, { children: ` - ${spec.title}` }), ' - Jupyter Server'] }, `${LOCAL_PREFIX}${spec.name}`)))] }), _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
338
|
// The description of an environment carries markup. With no
|
|
256
339
|
// parser to render it, the sanitizer of the application is
|
|
257
340
|
// enough to show it as what it is rather than as its tags.
|
|
@@ -260,11 +343,17 @@ export function CodeSandboxLauncher(props) {
|
|
|
260
343
|
} })) : (
|
|
261
344
|
// Neither a parser nor a sanitizer: the text of the markup,
|
|
262
345
|
// which is still better than the markup itself.
|
|
263
|
-
asPlainText(description)) }) })] }), _jsx(NewCodeSandboxControls
|
|
346
|
+
asPlainText(description)) }) })] }), _jsx(NewCodeSandboxControls
|
|
347
|
+
// A sandbox of this server costs nothing and starts at once: it has
|
|
348
|
+
// no time to reserve and no storage of the platform to mount.
|
|
349
|
+
, {
|
|
350
|
+
// A sandbox of this server costs nothing and starts at once: it has
|
|
351
|
+
// no time to reserve and no storage of the platform to mount.
|
|
352
|
+
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
353
|
? () => {
|
|
265
354
|
navigate(USAGE_ROUTE);
|
|
266
355
|
}
|
|
267
|
-
: undefined, userStorage: userStorage, onUserStorageToggle: () => setUserStorage(current => !current) }), _jsxs(FormControl, { sx: { paddingTop: '10px' }, children: [_jsx(FormControl.Label, { children: "
|
|
356
|
+
: 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
357
|
jupyterLabAdapter?.jupyterLab &&
|
|
269
358
|
!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
359
|
}
|
|
@@ -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;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/*
|
|
6
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
7
|
+
* Distributed under the terms of the Modified BSD License.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The given name of a code sandbox of a Jupyter Server or of the browser.
|
|
11
|
+
*
|
|
12
|
+
* A sandbox of the platform is named when it is launched — `given_name`, the
|
|
13
|
+
* field the launcher fills from the template of its environment — and it
|
|
14
|
+
* carries that name everywhere it is listed. A kernel of a Jupyter Server has
|
|
15
|
+
* no such field: the API gives its identifier and its kernelspec, and nothing
|
|
16
|
+
* that is the name of THIS sandbox. `python3` is what it runs, and reads the
|
|
17
|
+
* same on every one of them.
|
|
18
|
+
*
|
|
19
|
+
* The name was taken from the session instead, and that was the wrong place:
|
|
20
|
+
* a session is a standard Jupyter session, it ties a kernel to a document, it
|
|
21
|
+
* comes and goes as documents open and close, and several can name one kernel.
|
|
22
|
+
* A sandbox then wore the name of a document, changed name when another
|
|
23
|
+
* document took it, and kept the name of a document that had been closed.
|
|
24
|
+
*
|
|
25
|
+
* The given name is held here, by kernel, and given on first sight — from the
|
|
26
|
+
* kernelspec, which is what the launcher does for a runtime of the platform.
|
|
27
|
+
* It is kept in the browser: the kernels of a server outlive the page that
|
|
28
|
+
* lists them, so the name has to outlive it too, and the server has nowhere to
|
|
29
|
+
* put it.
|
|
30
|
+
*
|
|
31
|
+
* @module components/code-sandboxes/CodeSandboxNames
|
|
32
|
+
*/
|
|
33
|
+
/** Where the names are kept, and under which key. */
|
|
34
|
+
const STORAGE_KEY = 'datalayer:code-sandbox-names';
|
|
35
|
+
/** The names, by kernel identifier, as they were last written. */
|
|
36
|
+
let names;
|
|
37
|
+
function load() {
|
|
38
|
+
if (names) {
|
|
39
|
+
return names;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const raw = globalThis.localStorage?.getItem(STORAGE_KEY);
|
|
43
|
+
names = raw ? JSON.parse(raw) : {};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// A browser that refuses its storage keeps the names for this page only.
|
|
47
|
+
names = {};
|
|
48
|
+
}
|
|
49
|
+
return names;
|
|
50
|
+
}
|
|
51
|
+
function save() {
|
|
52
|
+
try {
|
|
53
|
+
globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(load()));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* the names stay in memory */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The given name of a sandbox, when it has one.
|
|
61
|
+
*
|
|
62
|
+
* @param kernelId Kernel of the sandbox
|
|
63
|
+
*/
|
|
64
|
+
export function getCodeSandboxGivenName(kernelId) {
|
|
65
|
+
return kernelId ? load()[kernelId] : undefined;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Name a sandbox, or rename it.
|
|
69
|
+
*
|
|
70
|
+
* @param kernelId Kernel of the sandbox
|
|
71
|
+
* @param givenName What to call it; empty forgets the name
|
|
72
|
+
*/
|
|
73
|
+
export function setCodeSandboxGivenName(kernelId, givenName) {
|
|
74
|
+
const stored = load();
|
|
75
|
+
if (givenName) {
|
|
76
|
+
stored[kernelId] = asGivenName(givenName);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
delete stored[kernelId];
|
|
80
|
+
}
|
|
81
|
+
save();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Drop the names of the sandboxes that are no longer running.
|
|
85
|
+
*
|
|
86
|
+
* @param runningKernelIds The kernels that are still there
|
|
87
|
+
*/
|
|
88
|
+
export function pruneCodeSandboxGivenNames(runningKernelIds) {
|
|
89
|
+
const alive = new Set(runningKernelIds);
|
|
90
|
+
const stored = load();
|
|
91
|
+
let dropped = false;
|
|
92
|
+
Object.keys(stored).forEach(kernelId => {
|
|
93
|
+
if (!alive.has(kernelId)) {
|
|
94
|
+
delete stored[kernelId];
|
|
95
|
+
dropped = true;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
if (dropped) {
|
|
99
|
+
save();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A name a sandbox can wear, out of whatever it was built from.
|
|
104
|
+
*
|
|
105
|
+
* A kernelspec is free to call itself with a picture — a laptop, a snake —
|
|
106
|
+
* and some do. A glyph makes a poor name: it says nothing about WHICH sandbox
|
|
107
|
+
* this is, it reads as a broken character in the fonts of the toolbar, and it
|
|
108
|
+
* is not what anyone would type. Pictures and their variation selectors are
|
|
109
|
+
* dropped, and a name left with nothing is replaced rather than shown empty.
|
|
110
|
+
*/
|
|
111
|
+
function asGivenName(base) {
|
|
112
|
+
const stripped = (base ?? '')
|
|
113
|
+
// Pictographs, their skin tones, their joiners and their selectors.
|
|
114
|
+
.replace(/[\u{1F000}-\u{1FAFF}\u{2190}-\u{2BFF}\u{FE00}-\u{FE0F}\u{200D}]/gu, ' ')
|
|
115
|
+
.replace(/\s+/g, ' ')
|
|
116
|
+
.trim();
|
|
117
|
+
return stripped || 'Code Sandbox';
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Every given name in use by a sandbox of a server or of the browser.
|
|
121
|
+
*/
|
|
122
|
+
export function listCodeSandboxGivenNames() {
|
|
123
|
+
return Object.values(load());
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The next free name built on a base, numbered when the base is taken.
|
|
127
|
+
*
|
|
128
|
+
* "Python 3 (ipykernel)", then "Python 3 (ipykernel) 2", then 3 — one rule for
|
|
129
|
+
* the name a sandbox is given on first sight and for the name the launcher
|
|
130
|
+
* offers, so what the launcher proposes is what an unnamed one would get.
|
|
131
|
+
*
|
|
132
|
+
* @param base What to call it, before any numbering
|
|
133
|
+
* @param taken The names already in use
|
|
134
|
+
*/
|
|
135
|
+
export function nextCodeSandboxGivenName(base, taken) {
|
|
136
|
+
const used = new Set(taken);
|
|
137
|
+
const stem = asGivenName(base);
|
|
138
|
+
let candidate = stem;
|
|
139
|
+
let ordinal = 1;
|
|
140
|
+
while (used.has(candidate)) {
|
|
141
|
+
ordinal += 1;
|
|
142
|
+
candidate = `${stem} ${ordinal}`;
|
|
143
|
+
}
|
|
144
|
+
return candidate;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The given name of a sandbox, giving it one when it has none.
|
|
148
|
+
*
|
|
149
|
+
* The name of what it runs, and a number when a sandbox of that kind is
|
|
150
|
+
* already named — the way two runtimes of one environment are told apart.
|
|
151
|
+
* Assigned once and kept: a name that changed with what else is running would
|
|
152
|
+
* be no name at all.
|
|
153
|
+
*
|
|
154
|
+
* @param kernelId Kernel of the sandbox
|
|
155
|
+
* @param specDisplayName What the kernelspec of the sandbox is called
|
|
156
|
+
*/
|
|
157
|
+
export function ensureCodeSandboxGivenName(kernelId, specDisplayName) {
|
|
158
|
+
const existing = getCodeSandboxGivenName(kernelId);
|
|
159
|
+
if (existing) {
|
|
160
|
+
return existing;
|
|
161
|
+
}
|
|
162
|
+
const candidate = nextCodeSandboxGivenName(specDisplayName, listCodeSandboxGivenNames());
|
|
163
|
+
setCodeSandboxGivenName(kernelId, candidate);
|
|
164
|
+
return candidate;
|
|
165
|
+
}
|
|
166
|
+
export default ensureCodeSandboxGivenName;
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
4
|
* Distributed under the terms of the Modified BSD License.
|
|
5
5
|
*/
|
|
6
|
-
import { useCallback, useEffect, useMemo, useState, } from 'react';
|
|
6
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
7
7
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
8
8
|
//import { kernelIcon } from '@jupyterlab/ui-components';
|
|
9
9
|
import { ActionList, ActionMenu, IconButton, Text, RadioGroup, Radio, FormControl, LabelGroup, Label, } from '@primer/react';
|
|
@@ -15,6 +15,7 @@ import { CreditsIndicator } from '../progress';
|
|
|
15
15
|
import { isRuntimeRemote, } from '../../runtimes';
|
|
16
16
|
import { useIAMStore } from '../../state/substates';
|
|
17
17
|
import { getGroupedCodeSandboxDescs, } from './CodeSandboxUtils';
|
|
18
|
+
import { listCodeSandboxGivenNames, nextCodeSandboxGivenName, } from './CodeSandboxNames';
|
|
18
19
|
import { creditsLimitFor, NewCodeSandboxControls, useNewCodeSandboxAllowance, } from './NewCodeSandboxControls';
|
|
19
20
|
import { CodeSandboxVariables, useCodeSandboxVariablesTransfer, } from './CodeSandboxVariables';
|
|
20
21
|
/**
|
|
@@ -161,6 +162,18 @@ export function CodeSandboxPicker(props) {
|
|
|
161
162
|
*/
|
|
162
163
|
const { refreshCredits } = useIAMStore();
|
|
163
164
|
const [timeLimit, setTimeLimit] = useState(10);
|
|
165
|
+
/*
|
|
166
|
+
* The name a new sandbox is given.
|
|
167
|
+
*
|
|
168
|
+
* Offered from what was picked — the environment or the specification —
|
|
169
|
+
* numbered against the sandboxes that already carry that name, and written
|
|
170
|
+
* into the description so whoever creates the sandbox creates it named:
|
|
171
|
+
* `given_name` for a runtime of the platform, the name held beside a kernel
|
|
172
|
+
* of a server. Re-offered whenever another thing is picked, and left alone
|
|
173
|
+
* while the user edits it.
|
|
174
|
+
*/
|
|
175
|
+
const [givenName, setGivenName] = useState('');
|
|
176
|
+
const givenNameFor = useRef(undefined);
|
|
164
177
|
const [userStorage, setUserStorage] = useState(false);
|
|
165
178
|
const asksForNewSandbox = withNewSandboxControls || !!onTransferChange;
|
|
166
179
|
useEffect(() => {
|
|
@@ -174,6 +187,25 @@ export function CodeSandboxPicker(props) {
|
|
|
174
187
|
.find(env => env.name === runtimeDesc?.name)?.burning_rate;
|
|
175
188
|
const allowance = useNewCodeSandboxAllowance(resolvedBurningRate);
|
|
176
189
|
const isNewSandbox = !!runtimeDesc && !runtimeDesc.kernelId;
|
|
190
|
+
// Offer a name for what was just picked, and leave it alone afterwards: the
|
|
191
|
+
// key is the thing picked, so re-picking the same one keeps what was typed.
|
|
192
|
+
const pickedKey = isNewSandbox
|
|
193
|
+
? `${runtimeDesc?.location}:${runtimeDesc?.name}`
|
|
194
|
+
: undefined;
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (!pickedKey || givenNameFor.current === pickedKey) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
givenNameFor.current = pickedKey;
|
|
200
|
+
const taken = [
|
|
201
|
+
...(multiServiceManager.remote?.runtimesManager
|
|
202
|
+
.get()
|
|
203
|
+
.map(runtime => runtime.given_name)
|
|
204
|
+
.filter(Boolean) ?? []),
|
|
205
|
+
...listCodeSandboxGivenNames(),
|
|
206
|
+
];
|
|
207
|
+
setGivenName(nextCodeSandboxGivenName(runtimeDesc?.displayName ?? '', taken));
|
|
208
|
+
}, [multiServiceManager, pickedKey, runtimeDesc?.displayName]);
|
|
177
209
|
const effectiveMaxMinutes = runtimeDesc?.location === 'remote'
|
|
178
210
|
? allowance.effectiveMaxMinutes
|
|
179
211
|
: Math.max(1, allowance.maxFromCredits ?? -1);
|
|
@@ -217,6 +249,13 @@ export function CodeSandboxPicker(props) {
|
|
|
217
249
|
? `${runtimeDesc.location}-${runtimeDesc.name}`
|
|
218
250
|
: runtimeDesc.name,
|
|
219
251
|
id: runtimeDesc.kernelId,
|
|
252
|
+
// The name the sandbox is given, carried with the choice.
|
|
253
|
+
//
|
|
254
|
+
// Everything else about the pick travelled in this object and the
|
|
255
|
+
// name did not, so whoever created the sandbox had nothing to name
|
|
256
|
+
// it with and fell back to the kernelspec: the launcher honoured
|
|
257
|
+
// what was typed, the picker silently dropped it.
|
|
258
|
+
displayName: runtimeDesc.displayName,
|
|
220
259
|
creditsLimit,
|
|
221
260
|
capabilities: userStorage ? ['user_storage'] : undefined,
|
|
222
261
|
}
|
|
@@ -279,7 +318,12 @@ export function CodeSandboxPicker(props) {
|
|
|
279
318
|
(isRuntimeRemote(k.location) &&
|
|
280
319
|
isRuntimeRemote(runtimeDesc?.location ?? 'local'))) &&
|
|
281
320
|
(k.kernelId ?? k.name) ===
|
|
282
|
-
(runtimeDesc?.kernelId ?? runtimeDesc?.name) }), _jsx(FormControl.Label, { children: _jsxs(Box, { display: "flex",
|
|
321
|
+
(runtimeDesc?.kernelId ?? runtimeDesc?.name) }), _jsx(FormControl.Label, { children: _jsxs(Box, { display: "flex", sx: { alignItems: 'baseline' }, children: [_jsx(Box, { children: k.displayName }), k.kernelId && (_jsx(Text, { sx: {
|
|
322
|
+
ml: 2,
|
|
323
|
+
fontSize: 0,
|
|
324
|
+
color: 'fg.muted',
|
|
325
|
+
fontFamily: 'mono'
|
|
326
|
+
}, title: k.kernelId, children: k.kernelId.slice(0, 8) })), k.kernelId && k.location === 'remote' && (_jsx(Box, { ml: 3, mt: 1, children: _jsx(CreditsIndicator, { kernelId: k.kernelId, serviceManager: multiServiceManager.remote }, "credits-indicator") }))] }) }), _jsx(FormControl.Caption, { children: _jsxs(LabelGroup, { sx: { marginTop: 1 }, children: [_jsx(Label, { variant: "secondary", children: k.name }), _jsx(Label, { variant: "secondary", sx: { marginLeft: 1 }, children: k.location }), k.burningRate && (_jsxs(Label, { variant: "sponsors", sx: { marginLeft: 1 }, children: [k.burningRate, " credits/second"] })), k.gpu && (_jsx(Label, { variant: "success", sx: { marginLeft: 1 }, children: "GPU" }))] }) })] }), _jsx(ActionList.Divider, {})] }, `${k.location}:${k.kernelId ?? k.name}`));
|
|
283
327
|
})] }, group))) })), !!postActions && _jsx(_Fragment, { children: postActions })] })), withNewSandboxControls && isNewSandbox && (
|
|
284
328
|
// The user asked for a NEW sandbox: the questions are the launcher's,
|
|
285
329
|
// asked through the launcher's own controls. A sandbox of this page
|
|
@@ -291,6 +335,13 @@ export function CodeSandboxPicker(props) {
|
|
|
291
335
|
? 'You must add credits to your account.'
|
|
292
336
|
: timeLimit === 0
|
|
293
337
|
? 'You must set a time limit.'
|
|
294
|
-
: undefined,
|
|
338
|
+
: undefined, givenName: givenName, onGivenNameChange: value => {
|
|
339
|
+
setGivenName(value);
|
|
340
|
+
if (runtimeDesc) {
|
|
341
|
+
// The description is what the caller creates the sandbox from:
|
|
342
|
+
// the name travels with it, rather than beside it.
|
|
343
|
+
setRuntimeDesc({ ...runtimeDesc, displayName: value });
|
|
344
|
+
}
|
|
345
|
+
}, userStorage: userStorage, onUserStorageToggle: () => setUserStorage(current => !current) })), variables.available && (_jsx(CodeSandboxVariables, { selectedVariables: variables.selected, setSelectVariable: variables.setSelected, transferVariables: variables.transfer, setTransferVariable: variables.setTransfer, kernelVariables: variables.variables, translator: translator }))] }));
|
|
295
346
|
}
|
|
296
347
|
export default CodeSandboxPicker;
|
|
@@ -2,9 +2,15 @@ import { type IRuntimeOptions } from '../../runtimes';
|
|
|
2
2
|
export interface CodeSandboxTransfer {
|
|
3
3
|
/**
|
|
4
4
|
* Selected Kernel.
|
|
5
|
+
*
|
|
6
|
+
* `displayName` is the name the user gave the sandbox in the picker, carried
|
|
7
|
+
* with the choice so whoever creates it can create it named — the kernels
|
|
8
|
+
* API of a server has no field for one, and a runtime of the platform takes
|
|
9
|
+
* it as its `given_name`.
|
|
5
10
|
*/
|
|
6
11
|
runtime: Partial<Omit<IRuntimeOptions, 'kernelType'> & {
|
|
7
12
|
id: string;
|
|
13
|
+
displayName: string;
|
|
8
14
|
}> | null;
|
|
9
15
|
/**
|
|
10
16
|
* List of selected variables
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
import { PathExt } from '@jupyterlab/coreutils';
|
|
6
5
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
7
|
-
const ASSIGN_NEW_RUNTIME_LABEL = 'Assign a new
|
|
8
|
-
|
|
9
|
-
const
|
|
6
|
+
const ASSIGN_NEW_RUNTIME_LABEL = 'Assign a new Code Sandbox';
|
|
7
|
+
import { ensureCodeSandboxGivenName } from './CodeSandboxNames';
|
|
8
|
+
const ASSIGN_EXISTING_REMOTE_RUNTIME_LABEL = 'Assign an existing Remote Code Sandbox';
|
|
9
|
+
const ASSIGN_EXISTING_RUNTIME_LABEL = 'Assign an existing Code Sandbox';
|
|
10
10
|
/**
|
|
11
11
|
* Create the grouped runtime descriptions.
|
|
12
12
|
*/
|
|
@@ -29,7 +29,7 @@ export function getGroupedCodeSandboxDescs(multiServiceManager, kernelId, transl
|
|
|
29
29
|
kernelId: session.kernel.id,
|
|
30
30
|
name: spec?.name ?? session.kernel.name,
|
|
31
31
|
language: spec?.language ?? '',
|
|
32
|
-
displayName: session.
|
|
32
|
+
displayName: ensureCodeSandboxGivenName(session.kernel.id, spec?.display_name ?? session.kernel.name),
|
|
33
33
|
location: 'local',
|
|
34
34
|
};
|
|
35
35
|
})
|
|
@@ -43,7 +43,7 @@ export function getGroupedCodeSandboxDescs(multiServiceManager, kernelId, transl
|
|
|
43
43
|
kernelId: session.kernel.id,
|
|
44
44
|
name: spec?.name ?? session.kernel.name,
|
|
45
45
|
language: spec?.language ?? '',
|
|
46
|
-
displayName: session.
|
|
46
|
+
displayName: ensureCodeSandboxGivenName(session.kernel.id, spec?.display_name ?? session.kernel.name),
|
|
47
47
|
location: 'browser',
|
|
48
48
|
};
|
|
49
49
|
}))
|
|
@@ -58,7 +58,7 @@ export function getGroupedCodeSandboxDescs(multiServiceManager, kernelId, transl
|
|
|
58
58
|
kernelId: k.id,
|
|
59
59
|
name: spec?.name ?? k.name,
|
|
60
60
|
language: spec?.language ?? '',
|
|
61
|
-
displayName: spec?.display_name ?? k.name,
|
|
61
|
+
displayName: ensureCodeSandboxGivenName(k.id, spec?.display_name ?? k.name),
|
|
62
62
|
location: 'local',
|
|
63
63
|
};
|
|
64
64
|
})
|
|
@@ -72,7 +72,12 @@ export function getGroupedCodeSandboxDescs(multiServiceManager, kernelId, transl
|
|
|
72
72
|
kernelId: runtime.id,
|
|
73
73
|
name: environment.name,
|
|
74
74
|
language: environment.language,
|
|
75
|
-
displayName:
|
|
75
|
+
displayName:
|
|
76
|
+
// The name it was GIVEN first: the title of the environment is
|
|
77
|
+
// what it runs, and every sandbox of that environment answers
|
|
78
|
+
// it — the launcher asks for a name precisely so two of them
|
|
79
|
+
// can be told apart here.
|
|
80
|
+
runtime.given_name || environment.title || environment.name,
|
|
76
81
|
location: 'remote',
|
|
77
82
|
podName: runtime.pod_name,
|
|
78
83
|
gpu: environment.resources?.['nvidia.com/gpu'],
|
|
@@ -86,7 +91,7 @@ export function getGroupedCodeSandboxDescs(multiServiceManager, kernelId, transl
|
|
|
86
91
|
kernelId: k.id,
|
|
87
92
|
name: spec?.name ?? k.name,
|
|
88
93
|
language: spec?.language ?? '',
|
|
89
|
-
displayName: spec?.display_name ?? k.name,
|
|
94
|
+
displayName: ensureCodeSandboxGivenName(k.id, spec?.display_name ?? k.name),
|
|
90
95
|
location: 'browser',
|
|
91
96
|
};
|
|
92
97
|
}))
|
|
@@ -34,6 +34,23 @@ export interface INewCodeSandboxControlsProps {
|
|
|
34
34
|
userStorage: boolean;
|
|
35
35
|
onUserStorageToggle: () => void;
|
|
36
36
|
storageDisabled?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The name the sandbox is given, when the caller asks for that question.
|
|
39
|
+
*
|
|
40
|
+
* A sandbox of the platform is named through `given_name`; one of a server
|
|
41
|
+
* or of the browser has the name held for it by `CodeSandboxNames`. The
|
|
42
|
+
* question is the same either way, so it is asked in one place.
|
|
43
|
+
*/
|
|
44
|
+
givenName?: string;
|
|
45
|
+
onGivenNameChange?: (givenName: string) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Whether the storage of the user is offered.
|
|
48
|
+
*
|
|
49
|
+
* A sandbox of this Jupyter Server runs on this machine, with the files of
|
|
50
|
+
* this machine: it reserves no time and mounts no storage of the platform,
|
|
51
|
+
* so neither question applies to it.
|
|
52
|
+
*/
|
|
53
|
+
withUserStorage?: boolean;
|
|
37
54
|
}
|
|
38
55
|
/**
|
|
39
56
|
* The two questions of a new sandbox: the time reservation, and the storage.
|
|
@@ -15,7 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
15
15
|
*
|
|
16
16
|
* @module components/code-sandboxes/NewCodeSandboxControls
|
|
17
17
|
*/
|
|
18
|
-
import { FormControl, ToggleSwitch, Tooltip,
|
|
18
|
+
import { FormControl, IconButton, TextInput, ToggleSwitch, Tooltip, } from '@primer/react';
|
|
19
19
|
import { AlertIcon } from '@primer/octicons-react';
|
|
20
20
|
import { useCoreStore, useIAMStore } from '../../state/substates';
|
|
21
21
|
import { CodeSandboxReservationControl, MAXIMAL_CODE_SANDBOX_TIME_RESERVATION_MINUTES, } from './CodeSandboxReservationControl';
|
|
@@ -67,9 +67,9 @@ export function creditsLimitFor(timeLimitMinutes, burningRate) {
|
|
|
67
67
|
* The storage is withheld on white-label deployments, as it always was.
|
|
68
68
|
*/
|
|
69
69
|
export function NewCodeSandboxControls(props) {
|
|
70
|
-
const { addCredits, burningRate, withReservation = true, disabled, error, max, onTimeChange, onUserStorageToggle, storageDisabled, timeLimit, userStorage, } = props;
|
|
70
|
+
const { addCredits, burningRate, withReservation = true, disabled, error, givenName, max, onGivenNameChange, onTimeChange, onUserStorageToggle, storageDisabled, timeLimit, userStorage, withUserStorage = true, } = props;
|
|
71
71
|
const { configuration } = useCoreStore();
|
|
72
|
-
return (_jsxs(_Fragment, { children: [withReservation && (_jsx(CodeSandboxReservationControl, { addCredits: addCredits, disabled: disabled, label: 'Time reservation', max: max, time: timeLimit, burningRate: burningRate, onTimeChange: onTimeChange, error: error })), !configuration.whiteLabel && (_jsxs(FormControl, { disabled: storageDisabled, layout: "horizontal", children: [_jsxs(FormControl.Label, { id: "new-sandbox-user-storage-label", children: ["User storage", _jsx(Tooltip, { text: "The runtime will be slower to start.", direction: "e", style: { marginLeft: 3 }, children: _jsx(IconButton, { icon: AlertIcon, "aria-label": "", variant: "invisible" }) })] }), _jsx(ToggleSwitch, { disabled: storageDisabled, checked: userStorage, size: "small", onClick: event => {
|
|
72
|
+
return (_jsxs(_Fragment, { children: [onGivenNameChange && (_jsxs(FormControl, { disabled: disabled, children: [_jsx(FormControl.Label, { children: "Given Name" }), _jsx(TextInput, { block: true, name: "given-name", value: givenName ?? '', onChange: event => onGivenNameChange(event.target.value) }), _jsx(FormControl.Caption, { children: "What this sandbox is called, wherever it is listed." })] })), withReservation && (_jsx(CodeSandboxReservationControl, { addCredits: addCredits, disabled: disabled, label: 'Time reservation', max: max, time: timeLimit, burningRate: burningRate, onTimeChange: onTimeChange, error: error })), withUserStorage && !configuration.whiteLabel && (_jsxs(FormControl, { disabled: storageDisabled, layout: "horizontal", children: [_jsxs(FormControl.Label, { id: "new-sandbox-user-storage-label", children: ["User storage", _jsx(Tooltip, { text: "The runtime will be slower to start.", direction: "e", style: { marginLeft: 3 }, children: _jsx(IconButton, { icon: AlertIcon, "aria-label": "", variant: "invisible" }) })] }), _jsx(ToggleSwitch, { disabled: storageDisabled, checked: userStorage, size: "small", onClick: event => {
|
|
73
73
|
event.preventDefault();
|
|
74
74
|
onUserStorageToggle();
|
|
75
75
|
}, "aria-labelledby": "new-sandbox-user-storage-label" })] }))] }));
|
|
@@ -16,6 +16,7 @@ export * from './CodeSandboxPicker';
|
|
|
16
16
|
export * from './CodeSandboxPickerCell';
|
|
17
17
|
export * from './CodeSandboxReservationControl';
|
|
18
18
|
export * from './CodeSandboxTransfer';
|
|
19
|
+
export * from './CodeSandboxNames';
|
|
19
20
|
export * from './CodeSandboxUtils';
|
|
20
21
|
export * from './CodeSandboxVariables';
|
|
21
22
|
export * from './NewCodeSandboxControls';
|
|
@@ -20,6 +20,7 @@ export * from './CodeSandboxPicker';
|
|
|
20
20
|
export * from './CodeSandboxPickerCell';
|
|
21
21
|
export * from './CodeSandboxReservationControl';
|
|
22
22
|
export * from './CodeSandboxTransfer';
|
|
23
|
+
export * from './CodeSandboxNames';
|
|
23
24
|
export * from './CodeSandboxUtils';
|
|
24
25
|
export * from './CodeSandboxVariables';
|
|
25
26
|
export * from './NewCodeSandboxControls';
|