@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
|
@@ -0,0 +1,211 @@
|
|
|
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
|
+
/** 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) {
|
|
52
|
+
try {
|
|
53
|
+
const raw = globalThis.localStorage?.getItem(key);
|
|
54
|
+
return raw ? JSON.parse(raw) : {};
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
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;
|
|
82
|
+
try {
|
|
83
|
+
globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(names));
|
|
84
|
+
globalThis.localStorage?.setItem(STAMP_KEY, JSON.stringify(stamps));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
/* the names stay in memory */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The given name of a sandbox, when it has one.
|
|
92
|
+
*
|
|
93
|
+
* @param kernelId Kernel of the sandbox
|
|
94
|
+
*/
|
|
95
|
+
export function getCodeSandboxGivenName(kernelId) {
|
|
96
|
+
return kernelId ? load()[kernelId] : undefined;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Name a sandbox, or rename it.
|
|
100
|
+
*
|
|
101
|
+
* @param kernelId Kernel of the sandbox
|
|
102
|
+
* @param givenName What to call it; empty forgets the name
|
|
103
|
+
*/
|
|
104
|
+
export function setCodeSandboxGivenName(kernelId, givenName) {
|
|
105
|
+
const names = load();
|
|
106
|
+
const stamps = loadStamps();
|
|
107
|
+
if (givenName) {
|
|
108
|
+
names[kernelId] = asGivenName(givenName);
|
|
109
|
+
stamps[kernelId] = Date.now();
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
delete names[kernelId];
|
|
113
|
+
delete stamps[kernelId];
|
|
114
|
+
}
|
|
115
|
+
save(names, stamps);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Drop the names of the sandboxes that are no longer running.
|
|
119
|
+
*
|
|
120
|
+
* @param runningKernelIds The kernels that are still there
|
|
121
|
+
*/
|
|
122
|
+
export function pruneCodeSandboxGivenNames(runningKernelIds) {
|
|
123
|
+
const alive = new Set(runningKernelIds);
|
|
124
|
+
const names = load();
|
|
125
|
+
const stamps = loadStamps();
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
let dropped = false;
|
|
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;
|
|
138
|
+
}
|
|
139
|
+
delete names[kernelId];
|
|
140
|
+
delete stamps[kernelId];
|
|
141
|
+
dropped = true;
|
|
142
|
+
});
|
|
143
|
+
if (dropped) {
|
|
144
|
+
save(names, stamps);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* A name a sandbox can wear, out of whatever it was built from.
|
|
149
|
+
*
|
|
150
|
+
* A kernelspec is free to call itself with a picture — a laptop, a snake —
|
|
151
|
+
* and some do. A glyph makes a poor name: it says nothing about WHICH sandbox
|
|
152
|
+
* this is, it reads as a broken character in the fonts of the toolbar, and it
|
|
153
|
+
* is not what anyone would type. Pictures and their variation selectors are
|
|
154
|
+
* dropped, and a name left with nothing is replaced rather than shown empty.
|
|
155
|
+
*/
|
|
156
|
+
function asGivenName(base) {
|
|
157
|
+
const stripped = (base ?? '')
|
|
158
|
+
// Pictographs, their skin tones, their joiners and their selectors.
|
|
159
|
+
.replace(/[\u{1F000}-\u{1FAFF}\u{2190}-\u{2BFF}\u{FE00}-\u{FE0F}\u{200D}]/gu, ' ')
|
|
160
|
+
.replace(/\s+/g, ' ')
|
|
161
|
+
.trim();
|
|
162
|
+
return stripped || 'Code Sandbox';
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Every given name in use by a sandbox of a server or of the browser.
|
|
166
|
+
*/
|
|
167
|
+
export function listCodeSandboxGivenNames() {
|
|
168
|
+
return Object.values(load());
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The next free name built on a base, numbered when the base is taken.
|
|
172
|
+
*
|
|
173
|
+
* "Python 3 (ipykernel)", then "Python 3 (ipykernel) 2", then 3 — one rule for
|
|
174
|
+
* the name a sandbox is given on first sight and for the name the launcher
|
|
175
|
+
* offers, so what the launcher proposes is what an unnamed one would get.
|
|
176
|
+
*
|
|
177
|
+
* @param base What to call it, before any numbering
|
|
178
|
+
* @param taken The names already in use
|
|
179
|
+
*/
|
|
180
|
+
export function nextCodeSandboxGivenName(base, taken) {
|
|
181
|
+
const used = new Set(taken);
|
|
182
|
+
const stem = asGivenName(base);
|
|
183
|
+
let candidate = stem;
|
|
184
|
+
let ordinal = 1;
|
|
185
|
+
while (used.has(candidate)) {
|
|
186
|
+
ordinal += 1;
|
|
187
|
+
candidate = `${stem} ${ordinal}`;
|
|
188
|
+
}
|
|
189
|
+
return candidate;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The given name of a sandbox, giving it one when it has none.
|
|
193
|
+
*
|
|
194
|
+
* The name of what it runs, and a number when a sandbox of that kind is
|
|
195
|
+
* already named — the way two runtimes of one environment are told apart.
|
|
196
|
+
* Assigned once and kept: a name that changed with what else is running would
|
|
197
|
+
* be no name at all.
|
|
198
|
+
*
|
|
199
|
+
* @param kernelId Kernel of the sandbox
|
|
200
|
+
* @param specDisplayName What the kernelspec of the sandbox is called
|
|
201
|
+
*/
|
|
202
|
+
export function ensureCodeSandboxGivenName(kernelId, specDisplayName) {
|
|
203
|
+
const existing = getCodeSandboxGivenName(kernelId);
|
|
204
|
+
if (existing) {
|
|
205
|
+
return existing;
|
|
206
|
+
}
|
|
207
|
+
const candidate = nextCodeSandboxGivenName(specDisplayName, listCodeSandboxGivenNames());
|
|
208
|
+
setCodeSandboxGivenName(kernelId, candidate);
|
|
209
|
+
return candidate;
|
|
210
|
+
}
|
|
211
|
+
export default ensureCodeSandboxGivenName;
|
|
@@ -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
|
*/
|
|
@@ -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,17 +15,39 @@ 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';
|
|
19
|
+
import { codeSandboxVariantTitle } from '../../models/CodeSandboxVariant';
|
|
18
20
|
import { creditsLimitFor, NewCodeSandboxControls, useNewCodeSandboxAllowance, } from './NewCodeSandboxControls';
|
|
19
21
|
import { CodeSandboxVariables, useCodeSandboxVariablesTransfer, } from './CodeSandboxVariables';
|
|
22
|
+
import { CodeSandboxEnvironmentSelect, } from './CodeSandboxEnvironmentSelect';
|
|
20
23
|
/**
|
|
21
24
|
* Maximal runtime display name length after which it is trimmed.
|
|
22
25
|
*/
|
|
23
26
|
const RUNTIME_DISPLAY_NAME_MAX_LENGTH = 25;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the choice is a sandbox that ALREADY RUNS.
|
|
29
|
+
*
|
|
30
|
+
* A running one is named by its kernel, or — where it runs at an external
|
|
31
|
+
* provider and has no kernel of this platform — by its pod. An environment
|
|
32
|
+
* to start a new one in has neither. Told apart by the kernel alone, every
|
|
33
|
+
* Kaggle and Modal sandbox read as an environment: they were offered under
|
|
34
|
+
* "Assign a new Code Sandbox" and never among the ones already running.
|
|
35
|
+
*/
|
|
36
|
+
function isRunningSandbox(desc) {
|
|
37
|
+
return !!desc.kernelId || !!desc.podName;
|
|
38
|
+
}
|
|
39
|
+
/** How a sandbox is identified among the choices; see `dropdownDescs`. */
|
|
40
|
+
function codeSandboxDescKey(desc) {
|
|
41
|
+
// The pod stands in for the kernel of a sandbox that has none — the ones
|
|
42
|
+
// of an external provider. Without it two Modal sandboxes of the same
|
|
43
|
+
// environment answer to one key and the list shows a single row.
|
|
44
|
+
return `${desc.location}:${desc.kernelId ?? desc.podName ?? desc.name}`;
|
|
45
|
+
}
|
|
24
46
|
/**
|
|
25
47
|
* The picker of a code sandbox: what runs already, and what could run.
|
|
26
48
|
*/
|
|
27
49
|
export function CodeSandboxPicker(props) {
|
|
28
|
-
const { disabled, display, filterRuntime, multiServiceManager, onTransferChange, postActions, preActions, preference, sessionContext, translator, variant, withNewSandboxControls, withVariablesTransfer, } = props;
|
|
50
|
+
const { disabled, display, filterRuntime, multiServiceManager, onTransferChange, postActions, sandboxLayout = 'separated', preActions, preference, sessionContext, translator, variant, withNewSandboxControls, withVariablesTransfer, } = props;
|
|
29
51
|
/*
|
|
30
52
|
* The choice, held here when the host does not hold it.
|
|
31
53
|
*
|
|
@@ -42,6 +64,91 @@ export function CodeSandboxPicker(props) {
|
|
|
42
64
|
props.setRuntimeDesc?.(desc);
|
|
43
65
|
}, [isControlled, props.setRuntimeDesc]);
|
|
44
66
|
const [groupedRuntimeDescs, setGroupedRuntimeDescs] = useState(getGroupedCodeSandboxDescs(multiServiceManager, preference?.id, translator, filterRuntime, variant));
|
|
67
|
+
/*
|
|
68
|
+
* The same choices the radio column lists, as options of the dropdown.
|
|
69
|
+
*
|
|
70
|
+
* Keyed the way the radio column keys its rows — where the sandbox runs
|
|
71
|
+
* and which one it is — because a kernel already running and an
|
|
72
|
+
* environment to start one in are told apart by nothing else: two
|
|
73
|
+
* sandboxes of one environment share a name.
|
|
74
|
+
*/
|
|
75
|
+
const dropdownDescs = useMemo(() => {
|
|
76
|
+
const byKey = new Map();
|
|
77
|
+
Object.values(groupedRuntimeDescs ?? {}).forEach(descs => descs.forEach(desc => byKey.set(codeSandboxDescKey(desc), desc)));
|
|
78
|
+
return byKey;
|
|
79
|
+
}, [groupedRuntimeDescs]);
|
|
80
|
+
const dropdownOptions = useMemo(() => Object.entries(groupedRuntimeDescs ?? {}).flatMap(([group, descs]) => descs.map(desc => ({
|
|
81
|
+
key: codeSandboxDescKey(desc),
|
|
82
|
+
title: desc.displayName ?? desc.name,
|
|
83
|
+
name: desc.name,
|
|
84
|
+
providerTitle: desc.provider
|
|
85
|
+
? codeSandboxVariantTitle(desc.provider)
|
|
86
|
+
: undefined,
|
|
87
|
+
location: desc.location,
|
|
88
|
+
burningRate: desc.burningRate,
|
|
89
|
+
gpu: desc.gpu,
|
|
90
|
+
group,
|
|
91
|
+
}))), [groupedRuntimeDescs]);
|
|
92
|
+
/**
|
|
93
|
+
* The sandboxes as a column of radio buttons, group by group.
|
|
94
|
+
*
|
|
95
|
+
* Shared by the two layouts that show one: the whole list in the `radio`
|
|
96
|
+
* display, and the sandboxes that ALREADY RUN in the separated dropdown
|
|
97
|
+
* display, where the dropdown is left to the environments a new one is
|
|
98
|
+
* started in.
|
|
99
|
+
*/
|
|
100
|
+
/*
|
|
101
|
+
* A sandbox that already runs is told from an environment by its KERNEL:
|
|
102
|
+
* a running one has one, an environment to start one in has none. Read
|
|
103
|
+
* from the data rather than from the heading, which is translated.
|
|
104
|
+
*/
|
|
105
|
+
const runningEntries = useMemo(() => Object.entries(groupedRuntimeDescs ?? {})
|
|
106
|
+
.map(([group, descs]) => [
|
|
107
|
+
group,
|
|
108
|
+
descs.filter(isRunningSandbox),
|
|
109
|
+
])
|
|
110
|
+
.filter(([, descs]) => descs.length > 0), [groupedRuntimeDescs]);
|
|
111
|
+
const environmentOnlyOptions = useMemo(() => dropdownOptions.filter(option => {
|
|
112
|
+
const desc = dropdownDescs.get(option.key);
|
|
113
|
+
return !desc || !isRunningSandbox(desc);
|
|
114
|
+
}), [dropdownDescs, dropdownOptions]);
|
|
115
|
+
const radioSections = (entries) => (_jsx(RadioGroup, { name: "kernel-options", "aria-labelledby": "kernel-options", children: entries.map(([group, runtimeDescs]) => (_jsxs(Box, { children: [_jsx(Box, { as: "h4", sx: {
|
|
116
|
+
/*
|
|
117
|
+
* The type of a form label, not of a heading.
|
|
118
|
+
*
|
|
119
|
+
* "Assign an existing Code Sandbox" and "Assign a
|
|
120
|
+
* new Code Sandbox" name the two ways of answering
|
|
121
|
+
* one question, and the second is the label of the
|
|
122
|
+
* dropdown; set in the larger, heavier type of an
|
|
123
|
+
* `h4`, the first read as a section above it rather
|
|
124
|
+
* than as its pair. The values are the ones Primer
|
|
125
|
+
* gives `FormControl.Label` — taken as the variables
|
|
126
|
+
* it resolves rather than through the Primer scale,
|
|
127
|
+
* whose `semibold` the JupyterLab theme redefines.
|
|
128
|
+
*/
|
|
129
|
+
margin: 0,
|
|
130
|
+
marginBottom: 2,
|
|
131
|
+
color: 'fg.default',
|
|
132
|
+
fontSize: 'var(--text-body-size-medium, 0.875rem)',
|
|
133
|
+
fontWeight: 'var(--base-text-weight-semibold, 600)',
|
|
134
|
+
}, children: group }), runtimeDescs.map(k => {
|
|
135
|
+
return (
|
|
136
|
+
// A kernel identifies a runtime that already runs; an
|
|
137
|
+
// environment to start one in has none, and is named
|
|
138
|
+
// by where it runs and what it runs.
|
|
139
|
+
_jsxs(Box, { title: k.name, children: [_jsxs(FormControl, { children: [_jsx(Radio, { value: k.kernelId, onChange: () => {
|
|
140
|
+
setRuntimeDesc(k);
|
|
141
|
+
}, checked: (k.location === runtimeDesc?.location ||
|
|
142
|
+
(isRuntimeRemote(k.location) &&
|
|
143
|
+
isRuntimeRemote(runtimeDesc?.location ?? 'local'))) &&
|
|
144
|
+
(k.kernelId ?? k.name) ===
|
|
145
|
+
(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: {
|
|
146
|
+
ml: 2,
|
|
147
|
+
fontSize: 0,
|
|
148
|
+
color: 'fg.muted',
|
|
149
|
+
fontFamily: 'mono'
|
|
150
|
+
}, 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 }), k.provider && (_jsx(Label, { variant: "accent", sx: { marginLeft: 1 }, children: codeSandboxVariantTitle(k.provider) })), _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}`));
|
|
151
|
+
})] }, group))) }));
|
|
45
152
|
const trans = useMemo(() => (translator ?? nullTranslator).load('jupyterlab'), [translator]);
|
|
46
153
|
const [defaultSet, setDefaultSet] = useState(false);
|
|
47
154
|
const [remoteModelsReady, setRemoteModelsReady] = useState(!multiServiceManager.remote);
|
|
@@ -143,10 +250,30 @@ export function CodeSandboxPicker(props) {
|
|
|
143
250
|
}
|
|
144
251
|
}
|
|
145
252
|
}
|
|
253
|
+
else if (display === 'dropdown') {
|
|
254
|
+
/*
|
|
255
|
+
* Nothing assigned: the FIRST environment stands as the choice.
|
|
256
|
+
*
|
|
257
|
+
* A dropdown that opens on "Select an environment" reads as a
|
|
258
|
+
* question already answered — the control looks filled — and the
|
|
259
|
+
* dialog assigns nothing when it is accepted. A column of radio
|
|
260
|
+
* buttons shows its emptiness plainly and is left alone. The list
|
|
261
|
+
* must be settled first, or the choice would land on a local
|
|
262
|
+
* kernelspec and move when the remote environments arrive.
|
|
263
|
+
*/
|
|
264
|
+
if (!remoteModelsReady) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const first = Object.values(groupedRuntimeDescs).flat()[0];
|
|
268
|
+
if (first) {
|
|
269
|
+
setRuntimeDesc(first);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
146
272
|
}
|
|
147
273
|
setDefaultSet(true);
|
|
148
274
|
}, [
|
|
149
275
|
defaultSet,
|
|
276
|
+
display,
|
|
150
277
|
groupedRuntimeDescs,
|
|
151
278
|
remoteModelsReady,
|
|
152
279
|
sessionContext,
|
|
@@ -161,6 +288,18 @@ export function CodeSandboxPicker(props) {
|
|
|
161
288
|
*/
|
|
162
289
|
const { refreshCredits } = useIAMStore();
|
|
163
290
|
const [timeLimit, setTimeLimit] = useState(10);
|
|
291
|
+
/*
|
|
292
|
+
* The name a new sandbox is given.
|
|
293
|
+
*
|
|
294
|
+
* Offered from what was picked — the environment or the specification —
|
|
295
|
+
* numbered against the sandboxes that already carry that name, and written
|
|
296
|
+
* into the description so whoever creates the sandbox creates it named:
|
|
297
|
+
* `given_name` for a runtime of the platform, the name held beside a kernel
|
|
298
|
+
* of a server. Re-offered whenever another thing is picked, and left alone
|
|
299
|
+
* while the user edits it.
|
|
300
|
+
*/
|
|
301
|
+
const [givenName, setGivenName] = useState('');
|
|
302
|
+
const givenNameFor = useRef(undefined);
|
|
164
303
|
const [userStorage, setUserStorage] = useState(false);
|
|
165
304
|
const asksForNewSandbox = withNewSandboxControls || !!onTransferChange;
|
|
166
305
|
useEffect(() => {
|
|
@@ -174,6 +313,25 @@ export function CodeSandboxPicker(props) {
|
|
|
174
313
|
.find(env => env.name === runtimeDesc?.name)?.burning_rate;
|
|
175
314
|
const allowance = useNewCodeSandboxAllowance(resolvedBurningRate);
|
|
176
315
|
const isNewSandbox = !!runtimeDesc && !runtimeDesc.kernelId;
|
|
316
|
+
// Offer a name for what was just picked, and leave it alone afterwards: the
|
|
317
|
+
// key is the thing picked, so re-picking the same one keeps what was typed.
|
|
318
|
+
const pickedKey = isNewSandbox
|
|
319
|
+
? `${runtimeDesc?.location}:${runtimeDesc?.name}`
|
|
320
|
+
: undefined;
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
if (!pickedKey || givenNameFor.current === pickedKey) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
givenNameFor.current = pickedKey;
|
|
326
|
+
const taken = [
|
|
327
|
+
...(multiServiceManager.remote?.runtimesManager
|
|
328
|
+
.get()
|
|
329
|
+
.map(runtime => runtime.given_name)
|
|
330
|
+
.filter(Boolean) ?? []),
|
|
331
|
+
...listCodeSandboxGivenNames(),
|
|
332
|
+
];
|
|
333
|
+
setGivenName(nextCodeSandboxGivenName(runtimeDesc?.displayName ?? '', taken));
|
|
334
|
+
}, [multiServiceManager, pickedKey, runtimeDesc?.displayName]);
|
|
177
335
|
const effectiveMaxMinutes = runtimeDesc?.location === 'remote'
|
|
178
336
|
? allowance.effectiveMaxMinutes
|
|
179
337
|
: Math.max(1, allowance.maxFromCredits ?? -1);
|
|
@@ -217,6 +375,13 @@ export function CodeSandboxPicker(props) {
|
|
|
217
375
|
? `${runtimeDesc.location}-${runtimeDesc.name}`
|
|
218
376
|
: runtimeDesc.name,
|
|
219
377
|
id: runtimeDesc.kernelId,
|
|
378
|
+
// The name the sandbox is given, carried with the choice.
|
|
379
|
+
//
|
|
380
|
+
// Everything else about the pick travelled in this object and the
|
|
381
|
+
// name did not, so whoever created the sandbox had nothing to name
|
|
382
|
+
// it with and fell back to the kernelspec: the launcher honoured
|
|
383
|
+
// what was typed, the picker silently dropped it.
|
|
384
|
+
displayName: runtimeDesc.displayName,
|
|
220
385
|
creditsLimit,
|
|
221
386
|
capabilities: userStorage ? ['user_storage'] : undefined,
|
|
222
387
|
}
|
|
@@ -263,24 +428,37 @@ export function CodeSandboxPicker(props) {
|
|
|
263
428
|
candidateRuntimeDesc.name) ===
|
|
264
429
|
(runtimeDesc?.kernelId ?? runtimeDesc?.name), onSelect: () => {
|
|
265
430
|
setRuntimeDesc(candidateRuntimeDesc);
|
|
266
|
-
}, children: [_jsx(ActionList.LeadingVisual, { children: candidateRuntimeDesc.location === 'local' ? (_jsx(LaptopSimpleIcon, {})) : candidateRuntimeDesc.location === 'browser' ? (_jsx(BrowserIcon, {})) : (_jsx(CloudUploadIcon, {})) }), displayName + annotation.slice(0, 10)
|
|
267
|
-
|
|
431
|
+
}, children: [_jsx(ActionList.LeadingVisual, { children: candidateRuntimeDesc.location === 'local' ? (_jsx(LaptopSimpleIcon, {})) : candidateRuntimeDesc.location === 'browser' ? (_jsx(BrowserIcon, {})) : (_jsx(CloudUploadIcon, {})) }), displayName + annotation.slice(0, 10), _jsxs(ActionList.TrailingVisual, { children: [candidateRuntimeDesc.name && (_jsx(Label, { size: "small", variant: "secondary", children: candidateRuntimeDesc.name })), ' ', candidateRuntimeDesc
|
|
432
|
+
.provider && (_jsx(Label, { size: "small", variant: "accent", children: codeSandboxVariantTitle(candidateRuntimeDesc.provider) }))] })] }, candidateRuntimeDesc.name));
|
|
433
|
+
})] }, group))), !!postActions && (_jsxs(_Fragment, { children: [_jsx(ActionList.Divider, {}), postActions] }))] }) })] })) : display === 'dropdown' ? (
|
|
434
|
+
/*
|
|
435
|
+
* Section for the dropdown display.
|
|
436
|
+
*
|
|
437
|
+
* The same choices as the radio column, in the control the launcher
|
|
438
|
+
* uses — one question asked one way. A list of nine environments is
|
|
439
|
+
* nine rows of radio buttons before anything else on the dialog can
|
|
440
|
+
* be read; closed, it is one line.
|
|
441
|
+
*/
|
|
442
|
+
_jsxs(_Fragment, { children: [defaultSet && sandboxLayout === 'separated' &&
|
|
443
|
+
runningEntries.length > 0 &&
|
|
444
|
+
radioSections(runningEntries), defaultSet && (
|
|
445
|
+
// Room below: the controls of a new sandbox follow, and the
|
|
446
|
+
// toggle of the user storage sat against the dropdown.
|
|
447
|
+
_jsxs(FormControl, { sx: { marginBottom: 4 }, children: [_jsx(FormControl.Label, { children: sandboxLayout === 'separated'
|
|
448
|
+
? 'Assign a new Code Sandbox'
|
|
449
|
+
: 'Environment' }), _jsx(CodeSandboxEnvironmentSelect, { disabled: disabled, options: sandboxLayout === 'separated'
|
|
450
|
+
? environmentOnlyOptions
|
|
451
|
+
: dropdownOptions, selectedKey: runtimeDesc ? codeSandboxDescKey(runtimeDesc) : undefined, onSelect: key => {
|
|
452
|
+
const chosen = dropdownDescs.get(key);
|
|
453
|
+
if (chosen) {
|
|
454
|
+
setRuntimeDesc(chosen);
|
|
455
|
+
}
|
|
456
|
+
} })] })), !!postActions && _jsx(_Fragment, { children: postActions })] })) : (
|
|
268
457
|
/*
|
|
269
458
|
* Section for Radio display.
|
|
270
459
|
*/
|
|
271
|
-
_jsxs(_Fragment, { children: [defaultSet &&
|
|
272
|
-
|
|
273
|
-
// A kernel identifies a runtime that already runs; an
|
|
274
|
-
// environment to start one in has none, and is named
|
|
275
|
-
// by where it runs and what it runs.
|
|
276
|
-
_jsxs(Box, { title: k.name, children: [_jsxs(FormControl, { children: [_jsx(Radio, { value: k.kernelId, onChange: () => {
|
|
277
|
-
setRuntimeDesc(k);
|
|
278
|
-
}, checked: (k.location === runtimeDesc?.location ||
|
|
279
|
-
(isRuntimeRemote(k.location) &&
|
|
280
|
-
isRuntimeRemote(runtimeDesc?.location ?? 'local'))) &&
|
|
281
|
-
(k.kernelId ?? k.name) ===
|
|
282
|
-
(runtimeDesc?.kernelId ?? runtimeDesc?.name) }), _jsx(FormControl.Label, { children: _jsxs(Box, { display: "flex", children: [_jsx(Box, { children: k.displayName }), 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
|
-
})] }, group))) })), !!postActions && _jsx(_Fragment, { children: postActions })] })), withNewSandboxControls && isNewSandbox && (
|
|
460
|
+
_jsxs(_Fragment, { children: [defaultSet &&
|
|
461
|
+
radioSections(Object.entries(groupedRuntimeDescs ?? {})), !!postActions && _jsx(_Fragment, { children: postActions })] })), withNewSandboxControls && isNewSandbox && (
|
|
284
462
|
// The user asked for a NEW sandbox: the questions are the launcher's,
|
|
285
463
|
// asked through the launcher's own controls. A sandbox of this page
|
|
286
464
|
// or of this server reserves nothing, so only the time of a remote
|
|
@@ -291,6 +469,13 @@ export function CodeSandboxPicker(props) {
|
|
|
291
469
|
? 'You must add credits to your account.'
|
|
292
470
|
: timeLimit === 0
|
|
293
471
|
? 'You must set a time limit.'
|
|
294
|
-
: undefined,
|
|
472
|
+
: undefined, givenName: givenName, onGivenNameChange: value => {
|
|
473
|
+
setGivenName(value);
|
|
474
|
+
if (runtimeDesc) {
|
|
475
|
+
// The description is what the caller creates the sandbox from:
|
|
476
|
+
// the name travels with it, rather than beside it.
|
|
477
|
+
setRuntimeDesc({ ...runtimeDesc, displayName: value });
|
|
478
|
+
}
|
|
479
|
+
}, 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
480
|
}
|
|
296
481
|
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,8 +2,11 @@ import { SessionContext } from '@jupyterlab/apputils';
|
|
|
2
2
|
import { ITranslator } from '@jupyterlab/translation';
|
|
3
3
|
import { IMultiServiceManager } from '../../runtimes';
|
|
4
4
|
import { IRuntimeDesc } from '../../models';
|
|
5
|
+
import { CodeSandboxVariant } from '../../models/CodeSandboxVariant';
|
|
5
6
|
export type IDatalayerCodeSandboxDesc = IRuntimeDesc & {
|
|
6
7
|
gpu?: string;
|
|
8
|
+
/** The provider the environment belongs to — see `CodeSandboxVariant`. */
|
|
9
|
+
provider?: CodeSandboxVariant;
|
|
7
10
|
};
|
|
8
11
|
/**
|
|
9
12
|
* Create the grouped runtime descriptions.
|