@datalayer/agent-runtimes 1.0.28 → 1.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/api/index.js +16 -4
- package/lib/components/code-sandboxes/CodeSandboxEnvironmentSelect.d.ts +41 -0
- package/lib/components/code-sandboxes/CodeSandboxEnvironmentSelect.js +83 -0
- package/lib/components/code-sandboxes/CodeSandboxLauncher.d.ts +11 -0
- package/lib/components/code-sandboxes/CodeSandboxLauncher.js +96 -12
- package/lib/components/code-sandboxes/CodeSandboxNames.js +68 -23
- package/lib/components/code-sandboxes/CodeSandboxPicker.d.ts +14 -1
- package/lib/components/code-sandboxes/CodeSandboxPicker.js +155 -21
- package/lib/components/code-sandboxes/CodeSandboxUtils.d.ts +3 -0
- package/lib/components/code-sandboxes/CodeSandboxUtils.js +62 -11
- package/lib/components/code-sandboxes/NewCodeSandboxControls.js +2 -2
- package/lib/components/code-sandboxes/codeSandboxProviders.d.ts +29 -0
- package/lib/components/code-sandboxes/codeSandboxProviders.js +81 -0
- package/lib/components/code-sandboxes/index.d.ts +2 -0
- package/lib/components/code-sandboxes/index.js +2 -0
- package/lib/examples/AgentspecsExample.js +3 -3
- package/lib/identity/index.d.ts +1 -1
- package/lib/identity/index.js +1 -1
- package/lib/identity/types.d.ts +1 -1
- package/lib/identity/types.js +1 -1
- package/lib/models/CodeSandboxVariant.d.ts +45 -0
- package/lib/models/CodeSandboxVariant.js +65 -0
- package/lib/models/index.d.ts +1 -0
- package/lib/models/index.js +1 -0
- package/lib/runtimes/actions.js +14 -2
- package/lib/specs/envvars.d.ts +1 -1
- package/lib/specs/envvars.js +3 -3
- package/lib/specs/mcpServers.js +2 -2
- package/lib/state/substates/RuntimesState.js +4 -1
- package/package.json +1 -1
- package/scripts/gen_workers.py +1 -1
|
@@ -16,17 +16,38 @@ import { isRuntimeRemote, } from '../../runtimes';
|
|
|
16
16
|
import { useIAMStore } from '../../state/substates';
|
|
17
17
|
import { getGroupedCodeSandboxDescs, } from './CodeSandboxUtils';
|
|
18
18
|
import { listCodeSandboxGivenNames, nextCodeSandboxGivenName, } from './CodeSandboxNames';
|
|
19
|
+
import { codeSandboxVariantTitle } from '../../models/CodeSandboxVariant';
|
|
19
20
|
import { creditsLimitFor, NewCodeSandboxControls, useNewCodeSandboxAllowance, } from './NewCodeSandboxControls';
|
|
20
21
|
import { CodeSandboxVariables, useCodeSandboxVariablesTransfer, } from './CodeSandboxVariables';
|
|
22
|
+
import { CodeSandboxEnvironmentSelect, } from './CodeSandboxEnvironmentSelect';
|
|
21
23
|
/**
|
|
22
24
|
* Maximal runtime display name length after which it is trimmed.
|
|
23
25
|
*/
|
|
24
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
|
+
}
|
|
25
46
|
/**
|
|
26
47
|
* The picker of a code sandbox: what runs already, and what could run.
|
|
27
48
|
*/
|
|
28
49
|
export function CodeSandboxPicker(props) {
|
|
29
|
-
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;
|
|
30
51
|
/*
|
|
31
52
|
* The choice, held here when the host does not hold it.
|
|
32
53
|
*
|
|
@@ -43,6 +64,91 @@ export function CodeSandboxPicker(props) {
|
|
|
43
64
|
props.setRuntimeDesc?.(desc);
|
|
44
65
|
}, [isControlled, props.setRuntimeDesc]);
|
|
45
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))) }));
|
|
46
152
|
const trans = useMemo(() => (translator ?? nullTranslator).load('jupyterlab'), [translator]);
|
|
47
153
|
const [defaultSet, setDefaultSet] = useState(false);
|
|
48
154
|
const [remoteModelsReady, setRemoteModelsReady] = useState(!multiServiceManager.remote);
|
|
@@ -144,10 +250,30 @@ export function CodeSandboxPicker(props) {
|
|
|
144
250
|
}
|
|
145
251
|
}
|
|
146
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
|
+
}
|
|
147
272
|
}
|
|
148
273
|
setDefaultSet(true);
|
|
149
274
|
}, [
|
|
150
275
|
defaultSet,
|
|
276
|
+
display,
|
|
151
277
|
groupedRuntimeDescs,
|
|
152
278
|
remoteModelsReady,
|
|
153
279
|
sessionContext,
|
|
@@ -302,29 +428,37 @@ export function CodeSandboxPicker(props) {
|
|
|
302
428
|
candidateRuntimeDesc.name) ===
|
|
303
429
|
(runtimeDesc?.kernelId ?? runtimeDesc?.name), onSelect: () => {
|
|
304
430
|
setRuntimeDesc(candidateRuntimeDesc);
|
|
305
|
-
}, children: [_jsx(ActionList.LeadingVisual, { children: candidateRuntimeDesc.location === 'local' ? (_jsx(LaptopSimpleIcon, {})) : candidateRuntimeDesc.location === 'browser' ? (_jsx(BrowserIcon, {})) : (_jsx(CloudUploadIcon, {})) }), displayName + annotation.slice(0, 10)
|
|
306
|
-
|
|
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 })] })) : (
|
|
307
457
|
/*
|
|
308
458
|
* Section for Radio display.
|
|
309
459
|
*/
|
|
310
|
-
_jsxs(_Fragment, { children: [defaultSet &&
|
|
311
|
-
|
|
312
|
-
// A kernel identifies a runtime that already runs; an
|
|
313
|
-
// environment to start one in has none, and is named
|
|
314
|
-
// by where it runs and what it runs.
|
|
315
|
-
_jsxs(Box, { title: k.name, children: [_jsxs(FormControl, { children: [_jsx(Radio, { value: k.kernelId, onChange: () => {
|
|
316
|
-
setRuntimeDesc(k);
|
|
317
|
-
}, checked: (k.location === runtimeDesc?.location ||
|
|
318
|
-
(isRuntimeRemote(k.location) &&
|
|
319
|
-
isRuntimeRemote(runtimeDesc?.location ?? 'local'))) &&
|
|
320
|
-
(k.kernelId ?? k.name) ===
|
|
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}`));
|
|
327
|
-
})] }, 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 && (
|
|
328
462
|
// The user asked for a NEW sandbox: the questions are the launcher's,
|
|
329
463
|
// asked through the launcher's own controls. A sandbox of this page
|
|
330
464
|
// or of this server reserves nothing, so only the time of a remote
|
|
@@ -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.
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
6
6
|
const ASSIGN_NEW_RUNTIME_LABEL = 'Assign a new Code Sandbox';
|
|
7
7
|
import { ensureCodeSandboxGivenName } from './CodeSandboxNames';
|
|
8
|
+
import { isCodeSandboxProviderAvailable } from './codeSandboxProviders';
|
|
9
|
+
import { CodeSandboxVariant, codeSandboxVariantOf, } from '../../models/CodeSandboxVariant';
|
|
8
10
|
const ASSIGN_EXISTING_REMOTE_RUNTIME_LABEL = 'Assign an existing Remote Code Sandbox';
|
|
9
11
|
const ASSIGN_EXISTING_RUNTIME_LABEL = 'Assign an existing Code Sandbox';
|
|
10
12
|
/**
|
|
@@ -18,12 +20,25 @@ export function getGroupedCodeSandboxDescs(multiServiceManager, kernelId, transl
|
|
|
18
20
|
// identifier, the specification only names it. Only the environments to
|
|
19
21
|
// start a new runtime in come from the specifications, and they wait.
|
|
20
22
|
const specs = multiServiceManager.local.kernelspecs.specs;
|
|
21
|
-
|
|
23
|
+
/*
|
|
24
|
+
* The kernels of a Jupyter Server belong to the page that talks to one —
|
|
25
|
+
* inside JupyterLab. The web application's "local" manager points at
|
|
26
|
+
* whatever server its configuration names, and offering those kernels
|
|
27
|
+
* there lists sandboxes the user never started and cannot reason about.
|
|
28
|
+
* One rule for the whole file: the provider registry answers it, for the
|
|
29
|
+
* existing sandboxes here exactly as for the new-sandbox group below.
|
|
30
|
+
*/
|
|
31
|
+
const jupyterProviderAvailable = isCodeSandboxProviderAvailable('local', multiServiceManager);
|
|
32
|
+
const sessions = jupyterProviderAvailable
|
|
33
|
+
? multiServiceManager.local.sessions.running()
|
|
34
|
+
: [];
|
|
22
35
|
const kernels = {};
|
|
23
36
|
// Add the sessions.
|
|
24
37
|
const runningSessions = Array.from(sessions)
|
|
25
38
|
.filter(session => session.kernel && session.kernel.id !== kernelId)
|
|
26
|
-
|
|
39
|
+
// Annotated for the same reason as the chain below: what is pushed into
|
|
40
|
+
// this list includes sandboxes that carry a pod rather than a kernel.
|
|
41
|
+
.map((session) => {
|
|
27
42
|
const spec = specs?.kernelspecs[session.kernel.name];
|
|
28
43
|
return {
|
|
29
44
|
kernelId: session.kernel.id,
|
|
@@ -50,9 +65,11 @@ export function getGroupedCodeSandboxDescs(multiServiceManager, kernelId, transl
|
|
|
50
65
|
.filter(filterKernels);
|
|
51
66
|
// Add the running runtimes.
|
|
52
67
|
const listedAsSession = runningSessions.map(s => s.kernelId);
|
|
53
|
-
const runningKernels = Array.from(multiServiceManager.local.kernels.running())
|
|
68
|
+
const runningKernels = Array.from(jupyterProviderAvailable ? multiServiceManager.local.kernels.running() : [])
|
|
54
69
|
.filter(k => !listedAsSession.includes(k.id))
|
|
55
|
-
|
|
70
|
+
// Annotated, because the element type of the chain below is taken from
|
|
71
|
+
// this first link: a sandbox with no kernel joins it further down.
|
|
72
|
+
.map((k) => {
|
|
56
73
|
const spec = specs?.kernelspecs[k.name];
|
|
57
74
|
return {
|
|
58
75
|
kernelId: k.id,
|
|
@@ -63,15 +80,28 @@ export function getGroupedCodeSandboxDescs(multiServiceManager, kernelId, transl
|
|
|
63
80
|
};
|
|
64
81
|
})
|
|
65
82
|
.concat((multiServiceManager.remote?.runtimesManager.get() ?? [])
|
|
66
|
-
|
|
83
|
+
/*
|
|
84
|
+
* A sandbox with no KERNEL is still a sandbox that runs.
|
|
85
|
+
*
|
|
86
|
+
* The ones of an external provider — Kaggle, Modal — run at their
|
|
87
|
+
* provider and carry no kernel of this platform, so their `id` is
|
|
88
|
+
* empty. Asking for a truthy one dropped every one of them: they
|
|
89
|
+
* appeared in the table of the Code Sandboxes and nowhere in the
|
|
90
|
+
* picker, which is the one place a notebook can be pointed at them.
|
|
91
|
+
* What they always have is the pod that stands for them.
|
|
92
|
+
*/
|
|
93
|
+
.filter(k => (k.id || k.pod_name) && !listedAsSession.includes(k.id))
|
|
67
94
|
.map(runtime => {
|
|
68
95
|
const environment = multiServiceManager
|
|
69
96
|
.remote.environments.get()
|
|
70
97
|
.find(env => env.name === runtime.environment.name);
|
|
71
98
|
return {
|
|
72
|
-
|
|
99
|
+
// Empty for an external sandbox: it has none, and saying so is
|
|
100
|
+
// what keeps the picker from trying to connect to one.
|
|
101
|
+
kernelId: runtime.id || undefined,
|
|
73
102
|
name: environment.name,
|
|
74
103
|
language: environment.language,
|
|
104
|
+
provider: codeSandboxVariantOf(environment?.owner ?? runtime.environment?.owner),
|
|
75
105
|
displayName:
|
|
76
106
|
// The name it was GIVEN first: the title of the environment is
|
|
77
107
|
// what it runs, and every sandbox of that environment answers
|
|
@@ -101,7 +131,9 @@ export function getGroupedCodeSandboxDescs(multiServiceManager, kernelId, transl
|
|
|
101
131
|
// another — and a runtime listed twice would be picked twice.
|
|
102
132
|
const seen = new Set();
|
|
103
133
|
const distinctRunning = runningSessions.filter(desc => {
|
|
104
|
-
|
|
134
|
+
// A kernel-less sandbox is named by its pod; without it every external
|
|
135
|
+
// sandbox of one environment collapsed into a single entry.
|
|
136
|
+
const key = `${desc.location}:${desc.kernelId ?? desc.podName ?? desc.name}`;
|
|
105
137
|
if (seen.has(key)) {
|
|
106
138
|
return false;
|
|
107
139
|
}
|
|
@@ -117,7 +149,14 @@ export function getGroupedCodeSandboxDescs(multiServiceManager, kernelId, transl
|
|
|
117
149
|
kernels[key] = runningSessions;
|
|
118
150
|
}
|
|
119
151
|
// Environments.
|
|
120
|
-
|
|
152
|
+
/*
|
|
153
|
+
* The sandboxes of a Jupyter Server, which is a provider like the others.
|
|
154
|
+
*
|
|
155
|
+
* It is the one provider that only exists where the page is talking to such
|
|
156
|
+
* a server: inside JupyterLab. Elsewhere its kernels are somebody else's,
|
|
157
|
+
* and offering them promises what cannot be reached.
|
|
158
|
+
*/
|
|
159
|
+
const environments = Object.values(jupyterProviderAvailable ? (specs?.kernelspecs ?? {}) : {})
|
|
121
160
|
.filter(spec => !!spec)
|
|
122
161
|
.map(spec => ({
|
|
123
162
|
name: spec.name,
|
|
@@ -125,10 +164,21 @@ export function getGroupedCodeSandboxDescs(multiServiceManager, kernelId, transl
|
|
|
125
164
|
displayName: spec.display_name,
|
|
126
165
|
gpu: spec.resources?.['nvidia.com/gpu'],
|
|
127
166
|
location: 'local',
|
|
167
|
+
provider: CodeSandboxVariant.JupyterServer,
|
|
128
168
|
}))
|
|
129
169
|
.filter(filterKernels);
|
|
130
|
-
|
|
131
|
-
|
|
170
|
+
/*
|
|
171
|
+
* Every provider that can be used from here, and only those.
|
|
172
|
+
*
|
|
173
|
+
* A provider is a place a sandbox runs — the platform, this Jupyter Server,
|
|
174
|
+
* the browser — and each is offered when it is REACHABLE, not according to
|
|
175
|
+
* which application is asking. Withholding the platform inside JupyterLab
|
|
176
|
+
* took `ai-agents-env` off the list and left no way to launch a remote
|
|
177
|
+
* sandbox from there at all; the rule that was wanted is the other one, and
|
|
178
|
+
* it is stated where the Jupyter Server is read below.
|
|
179
|
+
*/
|
|
180
|
+
const remoteEnvironments = multiServiceManager.remote?.environments.get() ?? [];
|
|
181
|
+
environments.push(...remoteEnvironments
|
|
132
182
|
.map(spec => ({
|
|
133
183
|
name: spec.name,
|
|
134
184
|
language: spec.language,
|
|
@@ -136,8 +186,9 @@ export function getGroupedCodeSandboxDescs(multiServiceManager, kernelId, transl
|
|
|
136
186
|
location: 'remote',
|
|
137
187
|
gpu: spec.resources?.['nvidia.com/gpu'],
|
|
138
188
|
burningRate: spec.burning_rate,
|
|
189
|
+
provider: codeSandboxVariantOf(spec?.owner),
|
|
139
190
|
}))
|
|
140
|
-
.filter(filterKernels)
|
|
191
|
+
.filter(filterKernels));
|
|
141
192
|
environments.push(...Object.values(multiServiceManager.browser?.kernelspecs.specs?.kernelspecs ?? {})
|
|
142
193
|
.filter(spec => !!spec)
|
|
143
194
|
.map(spec => ({
|
|
@@ -69,8 +69,8 @@ export function creditsLimitFor(timeLimitMinutes, burningRate) {
|
|
|
69
69
|
export function NewCodeSandboxControls(props) {
|
|
70
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: [
|
|
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 })), 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
|
-
}, "aria-labelledby": "new-sandbox-user-storage-label" })] }))] }));
|
|
75
|
+
}, "aria-labelledby": "new-sandbox-user-storage-label" })] })), 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." })] }))] }));
|
|
76
76
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { IMultiServiceManager } from '../../api';
|
|
2
|
+
import type { IRuntimeLocation } from '../../models';
|
|
3
|
+
/** A place sandboxes run, as this page knows it. */
|
|
4
|
+
export type ICodeSandboxProvider = {
|
|
5
|
+
/** Identifier of the provider, which is where its sandboxes run. */
|
|
6
|
+
name: IRuntimeLocation;
|
|
7
|
+
/** What it is called, for a person. */
|
|
8
|
+
title: string;
|
|
9
|
+
/** Whether this page can reach it at all. */
|
|
10
|
+
available: boolean;
|
|
11
|
+
/** Why not, when it cannot. */
|
|
12
|
+
unavailableReason?: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Every provider this page knows, and whether it can be used.
|
|
16
|
+
*
|
|
17
|
+
* @param multiServiceManager The services of the page, which say what is
|
|
18
|
+
* reachable: the platform when the account has it, the Jupyter Server when
|
|
19
|
+
* the page is one, the browser when kernels run in it
|
|
20
|
+
*/
|
|
21
|
+
export declare function codeSandboxProviders(multiServiceManager: IMultiServiceManager | undefined): ICodeSandboxProvider[];
|
|
22
|
+
/**
|
|
23
|
+
* Whether one provider can be used from this page.
|
|
24
|
+
*
|
|
25
|
+
* @param location Where the sandboxes of that provider run
|
|
26
|
+
* @param multiServiceManager The services of the page
|
|
27
|
+
*/
|
|
28
|
+
export declare function isCodeSandboxProviderAvailable(location: IRuntimeLocation, multiServiceManager: IMultiServiceManager | undefined): boolean;
|
|
29
|
+
export default codeSandboxProviders;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The places a code sandbox can run, and which of them this page can use.
|
|
7
|
+
*
|
|
8
|
+
* A sandbox is not one kind of thing: it is a runtime of the platform, a
|
|
9
|
+
* kernel of a Jupyter Server, a kernel of the browser — and, on the Python
|
|
10
|
+
* side, a Kaggle session or a Modal container as well. Each of those is a
|
|
11
|
+
* PROVIDER, each ships the environments it offers (the platform ships
|
|
12
|
+
* `ai-agents-env`, Kaggle a CPU and a GPU session), and each is usable only
|
|
13
|
+
* where what it needs is at hand.
|
|
14
|
+
*
|
|
15
|
+
* That last rule was written three times over — in the picker, in the
|
|
16
|
+
* launcher, in the table of the sandboxes — and drifted every time it was
|
|
17
|
+
* touched: withholding the platform inside JupyterLab took `ai-agents-env`
|
|
18
|
+
* off the list and left no way to launch a remote sandbox there at all. It is
|
|
19
|
+
* stated once here, and the three read it.
|
|
20
|
+
*
|
|
21
|
+
* The same concept lives in `code_sandboxes.providers` on the Python side,
|
|
22
|
+
* where availability is decided by secrets rather than by services. Both
|
|
23
|
+
* answer the same question — which providers can be used from here — so the
|
|
24
|
+
* CLI, the web application and JupyterLab offer the same thing.
|
|
25
|
+
*
|
|
26
|
+
* @module components/code-sandboxes/codeSandboxProviders
|
|
27
|
+
*/
|
|
28
|
+
import { loadJupyterConfig } from '@datalayer/jupyter-react';
|
|
29
|
+
/**
|
|
30
|
+
* Every provider this page knows, and whether it can be used.
|
|
31
|
+
*
|
|
32
|
+
* @param multiServiceManager The services of the page, which say what is
|
|
33
|
+
* reachable: the platform when the account has it, the Jupyter Server when
|
|
34
|
+
* the page is one, the browser when kernels run in it
|
|
35
|
+
*/
|
|
36
|
+
export function codeSandboxProviders(multiServiceManager) {
|
|
37
|
+
/*
|
|
38
|
+
* The Jupyter Server is the provider bound to WHERE the page runs.
|
|
39
|
+
*
|
|
40
|
+
* Its kernels belong to the server the page is talking to. Outside
|
|
41
|
+
* JupyterLab there is no such server — what a manager reports as "local" is
|
|
42
|
+
* somebody else's — so offering them promises what cannot be reached.
|
|
43
|
+
*/
|
|
44
|
+
const insideJupyterLab = loadJupyterConfig().insideJupyterLab;
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
name: 'remote',
|
|
48
|
+
title: 'Datalayer',
|
|
49
|
+
available: Boolean(multiServiceManager?.remote),
|
|
50
|
+
unavailableReason: multiServiceManager?.remote
|
|
51
|
+
? undefined
|
|
52
|
+
: 'Sign in to Datalayer to launch a sandbox on the platform.'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'local',
|
|
56
|
+
title: 'Jupyter Server',
|
|
57
|
+
available: insideJupyterLab && Boolean(multiServiceManager?.local),
|
|
58
|
+
unavailableReason: insideJupyterLab
|
|
59
|
+
? undefined
|
|
60
|
+
: 'The kernels of a Jupyter Server are offered inside JupyterLab.'
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'browser',
|
|
64
|
+
title: 'Browser',
|
|
65
|
+
available: Boolean(multiServiceManager?.browser),
|
|
66
|
+
unavailableReason: multiServiceManager?.browser
|
|
67
|
+
? undefined
|
|
68
|
+
: 'No kernel runs in this browser.'
|
|
69
|
+
}
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether one provider can be used from this page.
|
|
74
|
+
*
|
|
75
|
+
* @param location Where the sandboxes of that provider run
|
|
76
|
+
* @param multiServiceManager The services of the page
|
|
77
|
+
*/
|
|
78
|
+
export function isCodeSandboxProviderAvailable(location, multiServiceManager) {
|
|
79
|
+
return Boolean(codeSandboxProviders(multiServiceManager).find(provider => provider.name === location)?.available);
|
|
80
|
+
}
|
|
81
|
+
export default codeSandboxProviders;
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export * from './CodeSandboxCellVariables';
|
|
13
13
|
export * from './CodeSandboxCellVariablesDialog';
|
|
14
|
+
export * from './CodeSandboxEnvironmentSelect';
|
|
14
15
|
export * from './CodeSandboxLauncher';
|
|
15
16
|
export * from './CodeSandboxPicker';
|
|
16
17
|
export * from './CodeSandboxPickerCell';
|
|
17
18
|
export * from './CodeSandboxReservationControl';
|
|
18
19
|
export * from './CodeSandboxTransfer';
|
|
19
20
|
export * from './CodeSandboxNames';
|
|
21
|
+
export * from './codeSandboxProviders';
|
|
20
22
|
export * from './CodeSandboxUtils';
|
|
21
23
|
export * from './CodeSandboxVariables';
|
|
22
24
|
export * from './NewCodeSandboxControls';
|
|
@@ -15,12 +15,14 @@
|
|
|
15
15
|
*/
|
|
16
16
|
export * from './CodeSandboxCellVariables';
|
|
17
17
|
export * from './CodeSandboxCellVariablesDialog';
|
|
18
|
+
export * from './CodeSandboxEnvironmentSelect';
|
|
18
19
|
export * from './CodeSandboxLauncher';
|
|
19
20
|
export * from './CodeSandboxPicker';
|
|
20
21
|
export * from './CodeSandboxPickerCell';
|
|
21
22
|
export * from './CodeSandboxReservationControl';
|
|
22
23
|
export * from './CodeSandboxTransfer';
|
|
23
24
|
export * from './CodeSandboxNames';
|
|
25
|
+
export * from './codeSandboxProviders';
|
|
24
26
|
export * from './CodeSandboxUtils';
|
|
25
27
|
export * from './CodeSandboxVariables';
|
|
26
28
|
export * from './NewCodeSandboxControls';
|
|
@@ -84,7 +84,7 @@ const GITHUB_CLIENT_ID = import.meta.env.VITE_GITHUB_CLIENT_ID || 'example-clien
|
|
|
84
84
|
// Kaggle API token - set via environment variable
|
|
85
85
|
// Get your token at: https://www.kaggle.com/settings/account (API section)
|
|
86
86
|
// Download kaggle.json and use the "key" value
|
|
87
|
-
const
|
|
87
|
+
const KAGGLE_API_TOKEN = import.meta.env.VITE_KAGGLE_API_TOKEN || '';
|
|
88
88
|
const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
89
89
|
const yamlScalar = (value) => {
|
|
90
90
|
if (value === null || value === undefined) {
|
|
@@ -252,11 +252,11 @@ const DEFAULT_IDENTITY_PROVIDERS = {
|
|
|
252
252
|
},
|
|
253
253
|
}
|
|
254
254
|
: {}),
|
|
255
|
-
...(
|
|
255
|
+
...(KAGGLE_API_TOKEN
|
|
256
256
|
? {
|
|
257
257
|
kaggle: {
|
|
258
258
|
type: 'token',
|
|
259
|
-
token:
|
|
259
|
+
token: KAGGLE_API_TOKEN,
|
|
260
260
|
displayName: 'Kaggle',
|
|
261
261
|
iconUrl: 'https://www.kaggle.com/static/images/favicon.ico',
|
|
262
262
|
},
|
package/lib/identity/index.d.ts
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
39
|
export type { OAuthProvider, OAuthToken, Identity, ProviderUserInfo, OAuthProviderConfig, AuthorizationRequest, AuthorizationCallback, IdentityState, IdentityActions, IdentityStore, AuthType, TokenProviderConfig, OAuthIdentityProviderConfig, TokenIdentityProviderConfig, IdentityProviderConfig, IdentityProvidersConfig, } from './types';
|
|
40
|
-
export { GITHUB_PROVIDER, GOOGLE_PROVIDER, KAGGLE_PROVIDER,
|
|
40
|
+
export { GITHUB_PROVIDER, GOOGLE_PROVIDER, KAGGLE_PROVIDER, KAGGLE_API_TOKEN_PROVIDER, } from './types';
|
|
41
41
|
export { generateCodeVerifier, generateCodeChallenge, generateState, generatePKCEPair, } from './pkce';
|
|
42
42
|
export { discoverAuthorizationServer, supportsDCR, registerClient, updateClientRegistration, deleteClientRegistration, getOrCreateDynamicClient, loadDynamicClient, saveDynamicClient, removeDynamicClient, getAllDynamicClients, clearAllDynamicClients, dynamicClientToProviderConfig, type AuthorizationServerMetadata, type ClientRegistrationRequest, type ClientRegistrationResponse, type ClientRegistrationError, type DynamicClient, } from './dcr';
|
|
43
43
|
export { useIdentityStore, useConnectedIdentities, useConnectedProviders, useIdentity as useIdentitySelector, useIsProviderConnected, usePendingAuthorization, useIdentityLoading, useIdentityError, configureBuiltinProviders, } from './identityStore';
|
package/lib/identity/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
export { GITHUB_PROVIDER, GOOGLE_PROVIDER, KAGGLE_PROVIDER,
|
|
5
|
+
export { GITHUB_PROVIDER, GOOGLE_PROVIDER, KAGGLE_PROVIDER, KAGGLE_API_TOKEN_PROVIDER, } from './types';
|
|
6
6
|
// PKCE utilities
|
|
7
7
|
export { generateCodeVerifier, generateCodeChallenge, generateState, generatePKCEPair, } from './pkce';
|
|
8
8
|
// Dynamic Client Registration (DCR)
|
package/lib/identity/types.d.ts
CHANGED
|
@@ -237,7 +237,7 @@ export interface TokenProviderConfig {
|
|
|
237
237
|
* Kaggle token-based provider configuration.
|
|
238
238
|
* Generate token at: https://www.kaggle.com/settings/account (API section)
|
|
239
239
|
*/
|
|
240
|
-
export declare const
|
|
240
|
+
export declare const KAGGLE_API_TOKEN_PROVIDER: TokenProviderConfig;
|
|
241
241
|
/**
|
|
242
242
|
* OAuth provider configuration for identity providers.
|
|
243
243
|
*/
|
package/lib/identity/types.js
CHANGED
|
@@ -63,7 +63,7 @@ export const KAGGLE_PROVIDER = {
|
|
|
63
63
|
* Kaggle token-based provider configuration.
|
|
64
64
|
* Generate token at: https://www.kaggle.com/settings/account (API section)
|
|
65
65
|
*/
|
|
66
|
-
export const
|
|
66
|
+
export const KAGGLE_API_TOKEN_PROVIDER = {
|
|
67
67
|
provider: 'kaggle',
|
|
68
68
|
displayName: 'Kaggle',
|
|
69
69
|
iconUrl: 'https://www.kaggle.com/static/images/favicon.ico',
|