@datalayer/core 1.0.22 → 1.0.24
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/README.md +5 -5
- package/lib/components/billing/BillableAccountSelect.js +40 -4
- package/lib/components/display/JupyterDialog.d.ts +8 -1
- package/lib/components/display/JupyterDialog.js +38 -5
- package/lib/components/runtimes/RuntimeLauncherDialog.d.ts +4 -0
- package/lib/components/runtimes/RuntimeLauncherDialog.js +36 -11
- package/lib/components/runtimes/RuntimePickerBase.js +18 -16
- package/lib/components/runtimes/RuntimePickerCell.js +1 -1
- package/lib/components/runtimes/RuntimePickerNotebook.js +89 -24
- package/lib/components/runtimes/RuntimeReservationControl.js +13 -4
- package/lib/hooks/useCache.d.ts +30 -0
- package/lib/hooks/useCache.js +204 -4
- package/lib/models/Page.d.ts +3 -0
- package/lib/state/substates/CoreState.js +1 -1
- package/lib/stateful/runtimes/actions.js +7 -1
- package/lib/utils/Name.d.ts +1 -0
- package/lib/utils/Name.js +8 -0
- package/lib/utils/Snapshot.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<a href="https://pypi.org/project/datalayer-core/"><img src="https://img.shields.io/pypi/v/datalayer-core.svg" alt="PyPI version"></img></a>
|
|
13
13
|
<a href="https://pypi.org/project/datalayer-core/"><img src="https://img.shields.io/pypi/pyversions/datalayer-core.svg" alt="Python versions"></img></a>
|
|
14
14
|
<a href="https://github.com/datalayer/core/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-BSD%203--Clause-blue.svg" alt="License"></img></a>
|
|
15
|
-
<a href="https://
|
|
15
|
+
<a href="https://datalayer.ai/docs/"><img src="https://img.shields.io/badge/docs-datalayer.app-blue" alt="Documentation"></img></a>
|
|
16
16
|
<a href="https://github.com/datalayer/core/actions/workflows/py-tests.yml"><img src="https://github.com/datalayer/core/actions/workflows/py-tests.yml/badge.svg" alt="Units Tests"></img></a><a href="https://github.com/datalayer/core/actions/workflows/ts-tests.yml"><img src="https://github.com/datalayer/core/actions/workflows/ts-tests.yml/badge.svg" alt="Units Tests"></img></a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
@@ -217,10 +217,10 @@ Key platform features accessible through this Client and CLI:
|
|
|
217
217
|
|
|
218
218
|
## Documentation
|
|
219
219
|
|
|
220
|
-
- **Command Line Interface (CLI)**: [https://
|
|
220
|
+
- **Command Line Interface (CLI)**: [https://datalayer.ai/docs/cli/](https://datalayer.ai/docs/cli/)
|
|
221
221
|
- **Core Python Client**: [core.datalayer.tech/python/](https://core.datalayer.tech/python/)
|
|
222
|
-
- **Platform Documentation**: [docs.datalayer.app](https://
|
|
223
|
-
- **API Reference**: [API documentation](https://
|
|
222
|
+
- **Platform Documentation**: [docs.datalayer.app](https://datalayer.ai/docs/)
|
|
223
|
+
- **API Reference**: [API documentation](https://datalayer.ai/docs/api/)
|
|
224
224
|
|
|
225
225
|
## Development
|
|
226
226
|
|
|
@@ -317,7 +317,7 @@ This project is licensed under the [BSD 3-Clause License](https://github.com/dat
|
|
|
317
317
|
|
|
318
318
|
## Support
|
|
319
319
|
|
|
320
|
-
- **Documentation**: [Datalayer Platform Documentation](https://
|
|
320
|
+
- **Documentation**: [Datalayer Platform Documentation](https://datalayer.ai/docs/)
|
|
321
321
|
- **Issues**: [GitHub Issues](https://github.com/datalayer/core/issues)
|
|
322
322
|
- **Community**: [Datalayer Platform](https://datalayer.app/)
|
|
323
323
|
|
|
@@ -12,7 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* only need to provide a value/onChange pair and optionally observe the full
|
|
13
13
|
* resolved account via `onSelectedAccountChange`.
|
|
14
14
|
*/
|
|
15
|
-
import { useCallback, useEffect, useMemo, Fragment } from 'react';
|
|
15
|
+
import { useCallback, useEffect, useMemo, useRef, Fragment } from 'react';
|
|
16
16
|
import { ActionList, ActionMenu, Button, Flash, FormControl, Label, Spinner, Text, } from '@primer/react';
|
|
17
17
|
import { OrganizationIcon, PeopleIcon, PersonIcon, } from '@primer/octicons-react';
|
|
18
18
|
import { Box } from '@datalayer/primer-addons';
|
|
@@ -196,6 +196,9 @@ export function BillableAccountSelect({ value, onChange, onSelectedAccountChange
|
|
|
196
196
|
: undefined;
|
|
197
197
|
if (fromCookie)
|
|
198
198
|
return fromCookie;
|
|
199
|
+
const personalEligible = eligibleBillable.find(a => a.accountType === 'user' && a.accountUid === personalAccountUid);
|
|
200
|
+
if (personalEligible)
|
|
201
|
+
return personalEligible;
|
|
199
202
|
const byPrincipal = selectedPrincipalUid
|
|
200
203
|
? eligibleBillable.find(account => {
|
|
201
204
|
if (account.accountUid !== selectedPrincipalUid)
|
|
@@ -209,9 +212,6 @@ export function BillableAccountSelect({ value, onChange, onSelectedAccountChange
|
|
|
209
212
|
: undefined;
|
|
210
213
|
if (byPrincipal)
|
|
211
214
|
return byPrincipal;
|
|
212
|
-
const personalEligible = eligibleBillable.find(a => a.accountType === 'user' && a.accountUid === personalAccountUid);
|
|
213
|
-
if (personalEligible)
|
|
214
|
-
return personalEligible;
|
|
215
215
|
const firstOrg = eligibleBillable.find(a => a.accountType === 'organization');
|
|
216
216
|
if (preferOrganizationDefault && firstOrg)
|
|
217
217
|
return firstOrg;
|
|
@@ -228,10 +228,45 @@ export function BillableAccountSelect({ value, onChange, onSelectedAccountChange
|
|
|
228
228
|
writeBillableAccountCookie(accountUid);
|
|
229
229
|
onChange(accountUid);
|
|
230
230
|
}, [onChange]);
|
|
231
|
+
// Apply persisted selection (cookie) once accounts are resolved. Runs once
|
|
232
|
+
// per mount so users can still change selection afterwards; falls back to
|
|
233
|
+
// personal user account when the stored uid is unknown/ineligible.
|
|
234
|
+
const initialSelectionAppliedRef = useRef(false);
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (isLoading)
|
|
237
|
+
return;
|
|
238
|
+
if (initialSelectionAppliedRef.current)
|
|
239
|
+
return;
|
|
240
|
+
if (eligibleBillable.length === 0 && accounts.length === 0)
|
|
241
|
+
return;
|
|
242
|
+
initialSelectionAppliedRef.current = true;
|
|
243
|
+
const storedAccount = storedBillableAccountUid
|
|
244
|
+
? eligibleBillable.find(a => a.accountUid === storedBillableAccountUid)
|
|
245
|
+
: undefined;
|
|
246
|
+
const personalAccount = eligibleBillable.find(a => a.accountType === 'user' && a.accountUid === personalAccountUid);
|
|
247
|
+
const target = storedAccount || personalAccount || preferredEligible;
|
|
248
|
+
if (!target)
|
|
249
|
+
return;
|
|
250
|
+
if (target.accountUid !== value) {
|
|
251
|
+
writeBillableAccountCookie(target.accountUid);
|
|
252
|
+
onChange(target.accountUid);
|
|
253
|
+
}
|
|
254
|
+
}, [
|
|
255
|
+
isLoading,
|
|
256
|
+
accounts,
|
|
257
|
+
eligibleBillable,
|
|
258
|
+
storedBillableAccountUid,
|
|
259
|
+
personalAccountUid,
|
|
260
|
+
preferredEligible,
|
|
261
|
+
value,
|
|
262
|
+
onChange,
|
|
263
|
+
]);
|
|
231
264
|
// Auto-select a sensible default when current value is empty/ineligible.
|
|
232
265
|
useEffect(() => {
|
|
233
266
|
if (isLoading)
|
|
234
267
|
return;
|
|
268
|
+
if (!initialSelectionAppliedRef.current)
|
|
269
|
+
return;
|
|
235
270
|
if (!preferredEligible) {
|
|
236
271
|
if (value)
|
|
237
272
|
onChange('');
|
|
@@ -239,6 +274,7 @@ export function BillableAccountSelect({ value, onChange, onSelectedAccountChange
|
|
|
239
274
|
}
|
|
240
275
|
const current = accounts.find(a => a.accountUid === value);
|
|
241
276
|
if (!current || !current.isEligible) {
|
|
277
|
+
writeBillableAccountCookie(preferredEligible.accountUid);
|
|
242
278
|
onChange(preferredEligible.accountUid);
|
|
243
279
|
}
|
|
244
280
|
}, [isLoading, preferredEligible, accounts, value, onChange]);
|
|
@@ -33,6 +33,11 @@ export interface IDialogWrapperOptions<T> {
|
|
|
33
33
|
* The top level text for the dialog.
|
|
34
34
|
*/
|
|
35
35
|
title: string;
|
|
36
|
+
/**
|
|
37
|
+
* Optional async hook called before an accept button closes the dialog.
|
|
38
|
+
* Return false to keep the dialog open.
|
|
39
|
+
*/
|
|
40
|
+
onWillAccept: (result: Dialog.IResult<T>) => Promise<boolean | void> | boolean | void;
|
|
36
41
|
}
|
|
37
42
|
/**
|
|
38
43
|
* A primer dialog mimicking the JupyterLab dialog interface
|
|
@@ -45,6 +50,8 @@ export declare class JupyterDialog<T> extends ReactWidget {
|
|
|
45
50
|
protected buttons: Dialog.IButton[];
|
|
46
51
|
protected host: HTMLElement;
|
|
47
52
|
protected dialogTitle?: string;
|
|
53
|
+
protected onWillAccept?: (result: Dialog.IResult<T>) => Promise<boolean | void> | boolean | void;
|
|
54
|
+
private _pendingButtonIndex;
|
|
48
55
|
private _closing;
|
|
49
56
|
private _result;
|
|
50
57
|
/**
|
|
@@ -60,7 +67,7 @@ export declare class JupyterDialog<T> extends ReactWidget {
|
|
|
60
67
|
* @returns a promise that resolves with the result of the dialog.
|
|
61
68
|
*/
|
|
62
69
|
launch(): Promise<Dialog.IResult<T>>;
|
|
63
|
-
protected handleButton: (idx: number) => void
|
|
70
|
+
protected handleButton: (idx: number) => Promise<void>;
|
|
64
71
|
protected setButton: (button: Dialog.IButton) => void;
|
|
65
72
|
protected setChecked: (c: boolean) => void;
|
|
66
73
|
protected setValue: (v: T | Error) => void;
|
|
@@ -9,7 +9,7 @@ import { ReactWidget } from '@jupyterlab/ui-components';
|
|
|
9
9
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
10
10
|
import { Widget } from '@lumino/widgets';
|
|
11
11
|
import { FocusKeys } from '@primer/behaviors';
|
|
12
|
-
import { Checkbox, FormControl, useFocusZone } from '@primer/react';
|
|
12
|
+
import { Checkbox, FormControl, Spinner, useFocusZone } from '@primer/react';
|
|
13
13
|
import { Dialog as PrimerDialog, } from '@primer/react/experimental';
|
|
14
14
|
import { JupyterReactTheme } from '@datalayer/jupyter-react';
|
|
15
15
|
function DialogFooter(props) {
|
|
@@ -33,6 +33,8 @@ export class JupyterDialog extends ReactWidget {
|
|
|
33
33
|
buttons;
|
|
34
34
|
host;
|
|
35
35
|
dialogTitle;
|
|
36
|
+
onWillAccept;
|
|
37
|
+
_pendingButtonIndex = null;
|
|
36
38
|
_closing = new PromiseDelegate();
|
|
37
39
|
_result = {
|
|
38
40
|
button: null,
|
|
@@ -52,6 +54,7 @@ export class JupyterDialog extends ReactWidget {
|
|
|
52
54
|
Dialog.okButton(),
|
|
53
55
|
];
|
|
54
56
|
this.dialogTitle = options.title;
|
|
57
|
+
this.onWillAccept = options.onWillAccept;
|
|
55
58
|
}
|
|
56
59
|
_renderBody = (props) => (_jsx(PrimerDialog.Body, { children: createElement(this.body, {
|
|
57
60
|
...props,
|
|
@@ -61,7 +64,11 @@ export class JupyterDialog extends ReactWidget {
|
|
|
61
64
|
render() {
|
|
62
65
|
// TODO title color is enforced for JupyterLab.
|
|
63
66
|
// This may be fixed in the jupyter-react theme (Primer generates h1 for the dialog title).
|
|
64
|
-
return (_jsx(JupyterReactTheme, { children: _jsx(PrimerDialog, { title: _jsx("span", { style: { color: 'var(--fgColor-default)' }, children: this.dialogTitle }), onClose:
|
|
67
|
+
return (_jsx(JupyterReactTheme, { children: _jsx(PrimerDialog, { title: _jsx("span", { style: { color: 'var(--fgColor-default)' }, children: this.dialogTitle }), onClose: () => {
|
|
68
|
+
if (this._pendingButtonIndex === null) {
|
|
69
|
+
this.close();
|
|
70
|
+
}
|
|
71
|
+
}, renderBody: this._renderBody, renderFooter: this._renderFooter, footerButtons: this.buttons.map((but, idx) => {
|
|
65
72
|
const footerButton = {
|
|
66
73
|
buttonType: but.displayType === 'default'
|
|
67
74
|
? but.accept
|
|
@@ -71,8 +78,9 @@ export class JupyterDialog extends ReactWidget {
|
|
|
71
78
|
onClick: () => {
|
|
72
79
|
this.handleButton(idx);
|
|
73
80
|
},
|
|
74
|
-
content: but.label,
|
|
81
|
+
content: this._pendingButtonIndex === idx ? (_jsx(Spinner, { size: "small" })) : (but.label),
|
|
75
82
|
'aria-label': but.ariaLabel,
|
|
83
|
+
disabled: this._pendingButtonIndex !== null,
|
|
76
84
|
autoFocus: but.accept,
|
|
77
85
|
};
|
|
78
86
|
return footerButton;
|
|
@@ -88,8 +96,30 @@ export class JupyterDialog extends ReactWidget {
|
|
|
88
96
|
await this._closing.promise;
|
|
89
97
|
return this._result;
|
|
90
98
|
}
|
|
91
|
-
handleButton = (idx) => {
|
|
92
|
-
|
|
99
|
+
handleButton = async (idx) => {
|
|
100
|
+
if (this._pendingButtonIndex !== null) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const button = this.buttons[idx];
|
|
104
|
+
this.setButton(button);
|
|
105
|
+
if (button.accept && this.onWillAccept) {
|
|
106
|
+
this._pendingButtonIndex = idx;
|
|
107
|
+
this.update();
|
|
108
|
+
try {
|
|
109
|
+
const shouldClose = await this.onWillAccept(this._result);
|
|
110
|
+
if (shouldClose === false) {
|
|
111
|
+
this._pendingButtonIndex = null;
|
|
112
|
+
this.update();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
this._pendingButtonIndex = null;
|
|
118
|
+
this.update();
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
this._pendingButtonIndex = null;
|
|
93
123
|
this.close();
|
|
94
124
|
};
|
|
95
125
|
setButton = (button) => {
|
|
@@ -107,6 +137,9 @@ export class JupyterDialog extends ReactWidget {
|
|
|
107
137
|
}
|
|
108
138
|
};
|
|
109
139
|
close = () => {
|
|
140
|
+
if (this._pendingButtonIndex !== null) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
110
143
|
Widget.detach(this);
|
|
111
144
|
this._closing.resolve();
|
|
112
145
|
};
|
|
@@ -31,8 +31,9 @@ const NOT_AVAILABLE_RETRIES = 5;
|
|
|
31
31
|
* Start Runtime Launcher Dialog.
|
|
32
32
|
*/
|
|
33
33
|
export function RuntimeLauncherDialog(props) {
|
|
34
|
-
const { dialogTitle, kernelSnapshot, manager, onSubmit, markdownParser, sanitizer, upgradeSubscription, startRuntime = true, } = props;
|
|
34
|
+
const { dialogTitle, kernelSnapshot, manager, onSubmit, markdownParser, sanitizer, upgradeSubscription, submitLabel, startRuntime = true, } = props;
|
|
35
35
|
const hasExample = startRuntime === 'with-example';
|
|
36
|
+
const shouldStartRuntime = startRuntime !== 'defer';
|
|
36
37
|
const user = iamStore.getState().user;
|
|
37
38
|
const environments = manager.environments.get();
|
|
38
39
|
const { configuration } = useCoreStore();
|
|
@@ -49,7 +50,7 @@ export function RuntimeLauncherDialog(props) {
|
|
|
49
50
|
const jupyterReactStore = useJupyterReactStore();
|
|
50
51
|
const jupyterLabAdapter = jupyterReactStore.jupyterLabAdapter;
|
|
51
52
|
const [selection, setSelection] = useState((kernelSnapshot?.environment || environments[0]?.name) ?? '');
|
|
52
|
-
const [timeLimit, setTimeLimit] = useState(
|
|
53
|
+
const [timeLimit, setTimeLimit] = useState(10);
|
|
53
54
|
const [runtimeName, setRuntimeName] = useState(environments[0]?.runtime?.givenNameTemplate || environments[0]?.title || '');
|
|
54
55
|
// Whether the runtim name has been changed by the user or not
|
|
55
56
|
const [hasCustomRuntimeName, setHasCustomRuntimeName] = useState(false);
|
|
@@ -60,16 +61,36 @@ export function RuntimeLauncherDialog(props) {
|
|
|
60
61
|
const [flashLevel, setFlashLevel] = useState('danger');
|
|
61
62
|
const isMounted = useIsMounted();
|
|
62
63
|
useEffect(() => {
|
|
63
|
-
if (
|
|
64
|
+
if (shouldStartRuntime) {
|
|
64
65
|
refreshCredits();
|
|
65
66
|
}
|
|
66
|
-
}, [
|
|
67
|
+
}, [shouldStartRuntime]);
|
|
67
68
|
const spec = useMemo(() => environments.find(spec => spec.name === selection), [environments, selection]);
|
|
68
69
|
const description = spec?.description ?? '';
|
|
69
70
|
const burningRate = spec?.burning_rate ?? 1;
|
|
70
71
|
const creditsToMinutes = 1.0 / burningRate / 60.0;
|
|
71
|
-
const
|
|
72
|
-
|
|
72
|
+
const includedRuns = user?.subscription?.usage?.included_runs ??
|
|
73
|
+
user?.subscription?.included_runs;
|
|
74
|
+
const currentRuns = user?.subscription?.usage?.current_runs ??
|
|
75
|
+
user?.subscription?.current_runs ??
|
|
76
|
+
user?.subscription?.used_runs;
|
|
77
|
+
const hasKnownRunAllowance = typeof includedRuns === 'number';
|
|
78
|
+
const hasRemainingRuns = hasKnownRunAllowance &&
|
|
79
|
+
typeof currentRuns === 'number' &&
|
|
80
|
+
includedRuns > 0 &&
|
|
81
|
+
currentRuns < includedRuns;
|
|
82
|
+
const hasKnownCredits = typeof credits?.available === 'number';
|
|
83
|
+
const maxFromCredits = hasKnownCredits
|
|
84
|
+
? Math.floor((credits.available ?? 0) * creditsToMinutes)
|
|
85
|
+
: 10;
|
|
86
|
+
const effectiveMaxMinutes = hasKnownCredits && hasKnownRunAllowance && !hasRemainingRuns
|
|
87
|
+
? Math.max(1, maxFromCredits)
|
|
88
|
+
: Math.max(10, maxFromCredits > 0 ? maxFromCredits : 0);
|
|
89
|
+
const outOfCredits = shouldStartRuntime &&
|
|
90
|
+
hasKnownCredits &&
|
|
91
|
+
hasKnownRunAllowance &&
|
|
92
|
+
!hasRemainingRuns &&
|
|
93
|
+
((credits.available ?? 0) <= 0 || maxFromCredits < Number.EPSILON);
|
|
73
94
|
const handleSelectionChange = useCallback((e) => {
|
|
74
95
|
const selection = e.target.value;
|
|
75
96
|
setSelection(selection);
|
|
@@ -81,7 +102,7 @@ export function RuntimeLauncherDialog(props) {
|
|
|
81
102
|
const handleSubmitRuntime = useCallback(async () => {
|
|
82
103
|
if (selection) {
|
|
83
104
|
setError(undefined);
|
|
84
|
-
setWaitingForRuntime(
|
|
105
|
+
setWaitingForRuntime(shouldStartRuntime);
|
|
85
106
|
const spec = environments.find(s => s.name === selection);
|
|
86
107
|
const desc = {
|
|
87
108
|
name: selection,
|
|
@@ -99,7 +120,7 @@ export function RuntimeLauncherDialog(props) {
|
|
|
99
120
|
desc.params['capabilities'] = ['user_storage'];
|
|
100
121
|
}
|
|
101
122
|
let success = true;
|
|
102
|
-
if (
|
|
123
|
+
if (shouldStartRuntime) {
|
|
103
124
|
success = false;
|
|
104
125
|
let availableTrial = 1;
|
|
105
126
|
let retryDelay = NOT_AVAILABLE_INIT_RETRY;
|
|
@@ -178,6 +199,9 @@ export function RuntimeLauncherDialog(props) {
|
|
|
178
199
|
success = await startNewKernel();
|
|
179
200
|
}
|
|
180
201
|
if (success && isMounted()) {
|
|
202
|
+
if (!shouldStartRuntime) {
|
|
203
|
+
setWaitingForRuntime(false);
|
|
204
|
+
}
|
|
181
205
|
onSubmit(desc);
|
|
182
206
|
}
|
|
183
207
|
}
|
|
@@ -191,6 +215,7 @@ export function RuntimeLauncherDialog(props) {
|
|
|
191
215
|
openExample,
|
|
192
216
|
jupyterLabAdapter,
|
|
193
217
|
timeLimit,
|
|
218
|
+
shouldStartRuntime,
|
|
194
219
|
isMounted,
|
|
195
220
|
]);
|
|
196
221
|
const handleUserStorageChange = useCallback((e) => {
|
|
@@ -228,8 +253,8 @@ export function RuntimeLauncherDialog(props) {
|
|
|
228
253
|
{
|
|
229
254
|
buttonType: 'primary',
|
|
230
255
|
onClick: handleSubmitRuntime,
|
|
231
|
-
content: waitingForRuntime ? (_jsx(Spinner, { size: "small" })) : (startRuntime ?? true) ?
|
|
232
|
-
disabled: waitingForRuntime ||
|
|
256
|
+
content: waitingForRuntime ? (_jsx(Spinner, { size: "small" })) : ((submitLabel ?? ((startRuntime ?? true) ? 'Launch' : 'Assign'))),
|
|
257
|
+
disabled: waitingForRuntime || !selection || outOfCredits,
|
|
233
258
|
autoFocus: true,
|
|
234
259
|
},
|
|
235
260
|
], children: _jsxs(Box, { as: "form", onKeyDown: event => {
|
|
@@ -244,7 +269,7 @@ export function RuntimeLauncherDialog(props) {
|
|
|
244
269
|
? () => {
|
|
245
270
|
navigate(USAGE_ROUTE);
|
|
246
271
|
}
|
|
247
|
-
: undefined, disabled: outOfCredits, label: 'Time reservation', max:
|
|
272
|
+
: undefined, disabled: outOfCredits, label: 'Time reservation', max: effectiveMaxMinutes, time: timeLimit, burningRate: burningRate, onTimeChange: setTimeLimit, error: outOfCredits ? 'You must add credits to your account.' : undefined })), !configuration.whiteLabel && (_jsxs(FormControl, { layout: "horizontal", children: [_jsxs(FormControl.Label, { id: "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, { checked: userStorage, size: "small", onClick: handleUserStorageChange, "aria-labelledby": "user-storage-label" })] })), _jsxs(FormControl, { sx: { paddingTop: '10px' }, children: [_jsx(FormControl.Label, { children: "Runtime name" }), _jsx(TextInput, { name: "name", value: runtimeName, onChange: handleKernelNameChange, block: true })] }), hasExample &&
|
|
248
273
|
jupyterLabAdapter?.jupyterLab &&
|
|
249
274
|
!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 }))] }) }));
|
|
250
275
|
}
|
|
@@ -85,24 +85,26 @@ export function RuntimePickerBase(props) {
|
|
|
85
85
|
// icon={() => <kernelIcon.react className="dla-Cell-runtime-icon" tag={'span'} />}
|
|
86
86
|
icon: () => (_jsx("span", { className: "dla-Cell-runtime-icon", children: _jsx(CpuIcon, {}) })), "aria-label": trans.__('Assign a Runtime to the Cell.'), title: trans.__('Assign a Runtime to the Cell.'), size: "small", variant: "invisible" }) })) : (_jsxs(ActionMenu.Button, { variant: "default", disabled: disabled || groupedRuntimeDescs === null, children: [_jsx(Text, { fontWeight: 'bold', children: trans.__('Runtime:') }), ' ' + (runtimeDesc?.displayName ?? trans.__('No Runtime'))] })), _jsx(ActionMenu.Overlay, { ...overlayProps, width: "medium", sx: { overflowY: 'auto' }, side: variant === 'cell' ? 'outside-left' : 'outside-right', children: _jsxs(ActionList, { selectionVariant: "single", children: [variant === 'cell' && (_jsxs(ActionList.Item, { selected: runtimeDesc === undefined, onSelect: () => {
|
|
87
87
|
setRuntimeDesc(undefined);
|
|
88
|
-
}, children: [preference?.location && (_jsx(ActionList.LeadingVisual, { children: preference.location === 'local' ? (_jsx(LaptopSimpleIcon, {})) : preference.location === 'browser' ? (_jsx(BrowserIcon, {})) : (_jsx(CloudUploadIcon, {})) })), trans.__('Assign the Notebook Runtime')] }, 'null')), !!preActions && preActions, Object.entries(groupedRuntimeDescs ?? {}).map(([group, runtimeDescs]) => (_jsxs(ActionList.Group, { children: [_jsx(ActionList.GroupHeading, { children: group }), runtimeDescs.map(
|
|
89
|
-
const annotation =
|
|
90
|
-
? ` - ${
|
|
91
|
-
:
|
|
92
|
-
? ` - ${
|
|
88
|
+
}, children: [preference?.location && (_jsx(ActionList.LeadingVisual, { children: preference.location === 'local' ? (_jsx(LaptopSimpleIcon, {})) : preference.location === 'browser' ? (_jsx(BrowserIcon, {})) : (_jsx(CloudUploadIcon, {})) })), trans.__('Assign the Notebook Runtime')] }, 'null')), !!preActions && preActions, Object.entries(groupedRuntimeDescs ?? {}).map(([group, runtimeDescs]) => (_jsxs(ActionList.Group, { children: [_jsx(ActionList.GroupHeading, { children: group }), runtimeDescs.map(candidateRuntimeDesc => {
|
|
89
|
+
const annotation = candidateRuntimeDesc.podName
|
|
90
|
+
? ` - ${candidateRuntimeDesc.podName.split('-', 2).reverse()[0]}`
|
|
91
|
+
: candidateRuntimeDesc.kernelId
|
|
92
|
+
? ` - ${candidateRuntimeDesc.kernelId}`
|
|
93
93
|
: '';
|
|
94
|
-
const fullDisplayName = (
|
|
95
|
-
const displayName = (
|
|
94
|
+
const fullDisplayName = (candidateRuntimeDesc.displayName ?? '') + annotation;
|
|
95
|
+
const displayName = (candidateRuntimeDesc.displayName?.length ?? 0) >
|
|
96
96
|
RUNTIME_DISPLAY_NAME_MAX_LENGTH
|
|
97
|
-
?
|
|
98
|
-
: (
|
|
99
|
-
return (_jsxs(ActionList.Item, { title: fullDisplayName, selected: (
|
|
100
|
-
|
|
97
|
+
? candidateRuntimeDesc.displayName.slice(0, RUNTIME_DISPLAY_NAME_MAX_LENGTH) + '…'
|
|
98
|
+
: (candidateRuntimeDesc.displayName ?? '');
|
|
99
|
+
return (_jsxs(ActionList.Item, { title: fullDisplayName, selected: (candidateRuntimeDesc.location ===
|
|
100
|
+
runtimeDesc?.location ||
|
|
101
|
+
(isRuntimeRemote(candidateRuntimeDesc.location) &&
|
|
101
102
|
isRuntimeRemote(runtimeDesc?.location ?? 'local'))) &&
|
|
102
|
-
(
|
|
103
|
+
(candidateRuntimeDesc.kernelId ??
|
|
104
|
+
candidateRuntimeDesc.name) ===
|
|
103
105
|
(runtimeDesc?.kernelId ?? runtimeDesc?.name), onSelect: () => {
|
|
104
|
-
setRuntimeDesc(
|
|
105
|
-
}, children: [_jsx(ActionList.LeadingVisual, { children:
|
|
106
|
+
setRuntimeDesc(candidateRuntimeDesc);
|
|
107
|
+
}, children: [_jsx(ActionList.LeadingVisual, { children: candidateRuntimeDesc.location === 'local' ? (_jsx(LaptopSimpleIcon, {})) : candidateRuntimeDesc.location === 'browser' ? (_jsx(BrowserIcon, {})) : (_jsx(CloudUploadIcon, {})) }), displayName + annotation.slice(0, 10)] }, candidateRuntimeDesc.name));
|
|
106
108
|
})] }, group))), !!postActions && (_jsxs(_Fragment, { children: [_jsx(ActionList.Divider, {}), postActions] }))] }) })] })) : (
|
|
107
109
|
/*
|
|
108
110
|
* Section for Radio display.
|
|
@@ -110,9 +112,9 @@ export function RuntimePickerBase(props) {
|
|
|
110
112
|
_jsxs(_Fragment, { children: [defaultSet && (_jsx(RadioGroup, { name: "kernel-options", "aria-labelledby": "kernel-options", children: Object.entries(groupedRuntimeDescs ?? {}).map(([group, runtimeDescs]) => (_jsxs(Box, { children: [_jsx(Box, { as: "h4", style: { marginTop: 0 }, children: group }), runtimeDescs.map(k => {
|
|
111
113
|
return (_jsxs(Box, { title: k.name, children: [_jsxs(FormControl, { children: [_jsx(Radio, { value: k.kernelId, onChange: () => {
|
|
112
114
|
setRuntimeDesc(k);
|
|
113
|
-
}, checked: (k.location ===
|
|
115
|
+
}, checked: (k.location === runtimeDesc?.location ||
|
|
114
116
|
(isRuntimeRemote(k.location) &&
|
|
115
|
-
isRuntimeRemote(
|
|
117
|
+
isRuntimeRemote(runtimeDesc?.location ?? 'local'))) &&
|
|
116
118
|
(k.kernelId ?? k.name) ===
|
|
117
119
|
(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.kernelId));
|
|
118
120
|
})] }, group))) })), !!postActions && _jsx(_Fragment, { children: postActions })] })) }));
|
|
@@ -99,6 +99,6 @@ export function RuntimePickerCell(props) {
|
|
|
99
99
|
const datalayerMeta = model.getMetadata('datalayer') ?? {};
|
|
100
100
|
return (_jsxs(_Fragment, { children: [_jsx(RuntimePickerBase, { display: "menu", filterRuntime: filterKernel, preference: preference, multiServiceManager: multiServiceManager, runtimeDesc: datalayerMeta.kernel ? datalayerMeta.kernel : undefined, setRuntimeDesc: setSelectedRuntimeDesc, variant: 'cell', preActions: _jsxs(ActionList.Item, { disabled: !multiServiceManager.remote, onSelect: setCell, selected: hasCellRuntime, title: !multiServiceManager.remote
|
|
101
101
|
? 'You are not connected with Datalayer.'
|
|
102
|
-
: 'Assign a new temporary Runtime on each Cell execution.', children: [_jsx(ActionList.LeadingVisual, { children: _jsx(CloudUploadIcon, {}) }), trans.__('Assign a Cell Runtime')] }), postActions: token || !logIn ? (_jsxs(_Fragment, { children: [RuntimeSnippetsFacade.supports(preference?.language ?? '') && (_jsx(ActionList.Item, { onSelect: openVariableDialog, disabled: !isForeign, title: trans.__('Define variables to transfer between the document kernel and the cell kernel.'), children: trans.__('Define Cell Variables Transfer') })), !configuration.whiteLabel && (_jsx(ActionList.Item, { onSelect: openSnippetDialog, disabled: snippets.length === 0, title: trans.__('Inject a code snippet at the end of the cell.'), children: trans.__('Inject Code Snippet') }))] })) : (_jsx(ActionList.Item, { onSelect: props.logIn, title: 'Connect to the Runtime provider.', children: _jsx(ExternalTokenSilentLogin, { message: "Connect to the Runtime provider" }) })), translator: translator }), isVariableDialogOpen && (_jsx(RuntimeCellVariablesDialog, { model: model, onClose: closeVariableDialog, preference: preference, sessionContext: sessionContext, translator: translator })), isSnippetDialogOpen && (_jsx(SnippetDialog, { language: language, model: model, snippets: snippets, onClose: closeSnippetDialog, markdownParser: markdownParser, sanitizer: sanitizer })), isKernelDialogOpen && (_jsx(RuntimeLauncherDialog, { manager: multiServiceManager.remote, onSubmit: onStartRemote, startRuntime:
|
|
102
|
+
: 'Assign a new temporary Runtime on each Cell execution.', children: [_jsx(ActionList.LeadingVisual, { children: _jsx(CloudUploadIcon, {}) }), trans.__('Assign a Cell Runtime')] }), postActions: token || !logIn ? (_jsxs(_Fragment, { children: [RuntimeSnippetsFacade.supports(preference?.language ?? '') && (_jsx(ActionList.Item, { onSelect: openVariableDialog, disabled: !isForeign, title: trans.__('Define variables to transfer between the document kernel and the cell kernel.'), children: trans.__('Define Cell Variables Transfer') })), !configuration.whiteLabel && (_jsx(ActionList.Item, { onSelect: openSnippetDialog, disabled: snippets.length === 0, title: trans.__('Inject a code snippet at the end of the cell.'), children: trans.__('Inject Code Snippet') }))] })) : (_jsx(ActionList.Item, { onSelect: props.logIn, title: 'Connect to the Runtime provider.', children: _jsx(ExternalTokenSilentLogin, { message: "Connect to the Runtime provider" }) })), translator: translator }), isVariableDialogOpen && (_jsx(RuntimeCellVariablesDialog, { model: model, onClose: closeVariableDialog, preference: preference, sessionContext: sessionContext, translator: translator })), isSnippetDialogOpen && (_jsx(SnippetDialog, { language: language, model: model, snippets: snippets, onClose: closeSnippetDialog, markdownParser: markdownParser, sanitizer: sanitizer })), isKernelDialogOpen && (_jsx(RuntimeLauncherDialog, { manager: multiServiceManager.remote, onSubmit: onStartRemote, startRuntime: 'defer', submitLabel: "Assign", markdownParser: markdownParser, sanitizer: sanitizer }))] }));
|
|
103
103
|
}
|
|
104
104
|
export default RuntimePickerCell;
|
|
@@ -21,9 +21,9 @@ import { RuntimePickerBase } from './RuntimePickerBase.js';
|
|
|
21
21
|
export function RuntimePickerNotebook(props) {
|
|
22
22
|
const { multiServiceManager, sessionContext, setValue, translator } = props;
|
|
23
23
|
const { configuration } = useCoreStore();
|
|
24
|
-
const { credits, refreshCredits, token } = useIAMStore();
|
|
24
|
+
const { credits, refreshCredits, token, user } = useIAMStore();
|
|
25
25
|
const [selectedRuntimeDesc, setSelectedRuntimeDesc] = useState();
|
|
26
|
-
const [timeLimit, setTimeLimit] = useState(
|
|
26
|
+
const [timeLimit, setTimeLimit] = useState(10);
|
|
27
27
|
const [userStorage, setUserStorage] = useState(false);
|
|
28
28
|
const [canTransferFrom, setTransferFrom] = useState(false);
|
|
29
29
|
const [canTransferTo, setTransferTo] = useState(false);
|
|
@@ -112,31 +112,96 @@ export function RuntimePickerNotebook(props) {
|
|
|
112
112
|
setUserStorage(!userStorage);
|
|
113
113
|
}, [userStorage]);
|
|
114
114
|
useEffect(() => {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
const resolvedBurningRate = selectedRuntimeDesc?.burningRate ??
|
|
116
|
+
multiServiceManager.remote?.environments
|
|
117
|
+
.get()
|
|
118
|
+
.find(env => env.name === selectedRuntimeDesc?.name)?.burning_rate;
|
|
119
|
+
const includedRuns = user?.subscription?.usage?.included_runs ??
|
|
120
|
+
user?.subscription?.included_runs;
|
|
121
|
+
const currentRuns = user?.subscription?.usage?.current_runs ??
|
|
122
|
+
user?.subscription?.current_runs ??
|
|
123
|
+
user?.subscription?.used_runs;
|
|
124
|
+
const hasKnownRunAllowance = typeof includedRuns === 'number';
|
|
125
|
+
const hasRemainingRuns = hasKnownRunAllowance &&
|
|
126
|
+
typeof currentRuns === 'number' &&
|
|
127
|
+
includedRuns > 0 &&
|
|
128
|
+
currentRuns < includedRuns;
|
|
129
|
+
const hasKnownCredits = typeof credits?.available === 'number';
|
|
130
|
+
const maxMinutes = selectedRuntimeDesc?.location === 'remote' && resolvedBurningRate
|
|
131
|
+
? Math.floor((credits?.available ?? 0) / resolvedBurningRate / 60.0)
|
|
132
|
+
: undefined;
|
|
133
|
+
const effectiveTimeLimit = selectedRuntimeDesc?.location === 'remote'
|
|
134
|
+
? Math.max(1, Math.min(timeLimit, maxMinutes && maxMinutes > 0 ? maxMinutes : 10))
|
|
135
|
+
: timeLimit;
|
|
136
|
+
const creditsLimit = selectedRuntimeDesc?.location === 'remote' && resolvedBurningRate
|
|
137
|
+
? Math.min(effectiveTimeLimit, MAXIMAL_RUNTIME_TIME_RESERVATION_MINUTES) *
|
|
138
|
+
resolvedBurningRate *
|
|
119
139
|
60
|
|
120
140
|
: undefined;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
selectedVariables: toTransfer,
|
|
141
|
+
const requiresRuntimeStart = !!selectedRuntimeDesc && !selectedRuntimeDesc.kernelId;
|
|
142
|
+
if (requiresRuntimeStart && selectedRuntimeDesc.location === 'remote') {
|
|
143
|
+
if (!resolvedBurningRate || !Number.isFinite(resolvedBurningRate)) {
|
|
144
|
+
setValue({ runtime: null, selectedVariables: toTransfer });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (hasKnownCredits &&
|
|
148
|
+
hasKnownRunAllowance &&
|
|
149
|
+
!hasRemainingRuns &&
|
|
150
|
+
(!creditsLimit || creditsLimit <= 0)) {
|
|
151
|
+
setValue({ runtime: null, selectedVariables: toTransfer });
|
|
152
|
+
return;
|
|
134
153
|
}
|
|
135
|
-
|
|
136
|
-
|
|
154
|
+
}
|
|
155
|
+
setValue({
|
|
156
|
+
runtime: selectedRuntimeDesc
|
|
157
|
+
? {
|
|
158
|
+
environmentName: ['browser', 'remote'].includes(selectedRuntimeDesc.location)
|
|
159
|
+
? `${selectedRuntimeDesc.location}-${selectedRuntimeDesc.name}`
|
|
160
|
+
: selectedRuntimeDesc.name,
|
|
161
|
+
id: selectedRuntimeDesc.kernelId,
|
|
162
|
+
creditsLimit,
|
|
163
|
+
capabilities: userStorage ? ['user_storage'] : undefined,
|
|
164
|
+
}
|
|
165
|
+
: null,
|
|
166
|
+
selectedVariables: toTransfer,
|
|
167
|
+
});
|
|
168
|
+
}, [
|
|
169
|
+
selectedRuntimeDesc,
|
|
170
|
+
userStorage,
|
|
171
|
+
toTransfer,
|
|
172
|
+
timeLimit,
|
|
173
|
+
multiServiceManager.remote,
|
|
174
|
+
credits?.available,
|
|
175
|
+
user,
|
|
176
|
+
]);
|
|
137
177
|
const { kernelPreference: { canStart }, } = sessionContext;
|
|
138
|
-
const
|
|
139
|
-
|
|
178
|
+
const resolvedBurningRate = selectedRuntimeDesc?.burningRate ??
|
|
179
|
+
multiServiceManager.remote?.environments
|
|
180
|
+
.get()
|
|
181
|
+
.find(env => env.name === selectedRuntimeDesc?.name)?.burning_rate;
|
|
182
|
+
const maxFromCredits = resolvedBurningRate
|
|
183
|
+
? Math.floor((credits?.available ?? 0) / resolvedBurningRate / 60.0)
|
|
184
|
+
: -1;
|
|
185
|
+
const includedRuns = user?.subscription?.usage?.included_runs ??
|
|
186
|
+
user?.subscription?.included_runs;
|
|
187
|
+
const currentRuns = user?.subscription?.usage?.current_runs ??
|
|
188
|
+
user?.subscription?.current_runs ??
|
|
189
|
+
user?.subscription?.used_runs;
|
|
190
|
+
const hasKnownRunAllowance = typeof includedRuns === 'number';
|
|
191
|
+
const hasRemainingRuns = hasKnownRunAllowance &&
|
|
192
|
+
typeof currentRuns === 'number' &&
|
|
193
|
+
includedRuns > 0 &&
|
|
194
|
+
currentRuns < includedRuns;
|
|
195
|
+
const hasKnownCredits = typeof credits?.available === 'number';
|
|
196
|
+
const effectiveMaxMinutes = selectedRuntimeDesc?.location === 'remote'
|
|
197
|
+
? hasKnownCredits && hasKnownRunAllowance && !hasRemainingRuns
|
|
198
|
+
? Math.max(1, maxFromCredits)
|
|
199
|
+
: Math.max(10, maxFromCredits > 0 ? maxFromCredits : 0)
|
|
200
|
+
: Math.max(1, maxFromCredits);
|
|
201
|
+
const outOfCredits = hasKnownCredits &&
|
|
202
|
+
hasKnownRunAllowance &&
|
|
203
|
+
!hasRemainingRuns &&
|
|
204
|
+
maxFromCredits < Number.EPSILON;
|
|
140
205
|
return (_jsxs(Box, { as: "form", className: "dla-Runtimes-picker", children: [_jsx(Box, { sx: { padding: 'var(--stack-padding-condensed) 0' }, children: _jsx(RuntimePickerBase, { display: "radio", disabled: canStart === false, preference: {
|
|
141
206
|
id: sessionContext.session?.id,
|
|
142
207
|
kernelDisplayName: sessionContext.kernelPreference.shouldStart
|
|
@@ -156,7 +221,7 @@ export function RuntimePickerNotebook(props) {
|
|
|
156
221
|
</Button>
|
|
157
222
|
*/
|
|
158
223
|
_jsx(_Fragment, {})) : (_jsx(ActionList.Item, { onSelect: props.logIn, title: 'Connect to Runtime provider.', children: _jsx(ExternalTokenSilentLogin, { message: "Connect to the Runtime provider" }) })) }) }), !selectedRuntimeDesc?.kernelId &&
|
|
159
|
-
selectedRuntimeDesc?.location === 'remote' && (_jsxs(_Fragment, { children: [_jsx(RuntimeReservationControl, { disabled: outOfCredits || selectedRuntimeDesc?.location !== 'remote', label: 'Time reservation', max:
|
|
224
|
+
selectedRuntimeDesc?.location === 'remote' && (_jsxs(_Fragment, { children: [_jsx(RuntimeReservationControl, { disabled: outOfCredits || selectedRuntimeDesc?.location !== 'remote', label: 'Time reservation', max: effectiveMaxMinutes, time: timeLimit, onTimeChange: setTimeLimit, error: outOfCredits && maxFromCredits >= 0
|
|
160
225
|
? 'You must add credits to your account.'
|
|
161
226
|
: timeLimit === 0
|
|
162
227
|
? 'You must set a time limit.'
|
|
@@ -18,7 +18,18 @@ export const MAXIMAL_RUNTIME_TIME_RESERVATION_MINUTES = 24 * 60;
|
|
|
18
18
|
*/
|
|
19
19
|
export function RuntimeReservationControl(props) {
|
|
20
20
|
const { addCredits, burningRate, disabled, error, label, max: maxProps, onTimeChange, time, } = props;
|
|
21
|
-
const max = Math.min(maxProps, MAXIMAL_RUNTIME_TIME_RESERVATION_MINUTES);
|
|
21
|
+
const max = Math.max(1, Math.min(maxProps, MAXIMAL_RUNTIME_TIME_RESERVATION_MINUTES));
|
|
22
|
+
const displayedTime = Number.isFinite(time)
|
|
23
|
+
? Math.min(max, Math.max(1, time))
|
|
24
|
+
: 1;
|
|
25
|
+
const handleTimeChange = (valueOrEvent) => {
|
|
26
|
+
const rawValue = typeof valueOrEvent === 'number'
|
|
27
|
+
? valueOrEvent
|
|
28
|
+
: parseFloat(valueOrEvent?.target?.value);
|
|
29
|
+
if (Number.isFinite(rawValue)) {
|
|
30
|
+
onTimeChange(Math.min(max, Math.max(1, rawValue)));
|
|
31
|
+
}
|
|
32
|
+
};
|
|
22
33
|
// Temporary workaround to not show disabled components.
|
|
23
34
|
const hidden = disabled;
|
|
24
35
|
return !hidden ? (_jsxs(FormControl, { disabled: disabled, sx: { paddingBottom: 'var(--stack-padding-condensed)' }, children: [_jsx(FormControl.Label, { children: label }), _jsxs(Box, { style: {
|
|
@@ -27,7 +38,5 @@ export function RuntimeReservationControl(props) {
|
|
|
27
38
|
gridTemplateColumns: 'max-content 1fr max-content max-content',
|
|
28
39
|
gridGap: 'var(--stack-gap-condensed)',
|
|
29
40
|
width: '100%',
|
|
30
|
-
}, children: [_jsx(Slider, { step: 1, min: 1, max: max, value:
|
|
31
|
-
onTimeChange(parseFloat(event.target.value));
|
|
32
|
-
} }), (max === 0 || max > Number.EPSILON) && (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["out of ", maxProps, " available minutes"] }), addCredits && (_jsx(IconButton, { icon: PlusIcon, "aria-label": "Add credits", onClick: () => addCredits() }))] }))] }), _jsxs(FormControl.Caption, { children: ["Maximum execution time that can be consumed by the runtime. It must be less than ", (MAXIMAL_RUNTIME_TIME_RESERVATION_MINUTES / 60).toFixed(0), ' ', "hours.", burningRate && (_jsxs(_Fragment, { children: [_jsx("br", {}), `With the current value, the runtime execution will consume at most ${(time * burningRate * 60).toFixed(2)} credits.`] }))] }), error && (_jsx(FormControl.Validation, { variant: "error", children: error }))] })) : (_jsx(_Fragment, {}));
|
|
41
|
+
}, children: [_jsx(Slider, { step: 1, min: 1, max: max, value: displayedTime, onChange: handleTimeChange, disabled: disabled, label: "", displayValue: false }), _jsx(TextInput, { type: "number", step: "1", min: "1", max: max, disabled: disabled, value: displayedTime.toFixed(2), onChange: handleTimeChange }), (max === 0 || max > Number.EPSILON) && (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["out of ", maxProps, " available minutes"] }), addCredits && (_jsx(IconButton, { icon: PlusIcon, "aria-label": "Add credits", onClick: () => addCredits() }))] }))] }), _jsxs(FormControl.Caption, { children: ["Maximum execution time that can be consumed by the runtime. It must be less than ", (MAXIMAL_RUNTIME_TIME_RESERVATION_MINUTES / 60).toFixed(0), ' ', "hours.", burningRate && (_jsxs(_Fragment, { children: [_jsx("br", {}), `With the current value, the runtime execution will consume at most ${(time * burningRate * 60).toFixed(2)} credits.`] }))] }), error && (_jsx(FormControl.Validation, { variant: "error", children: error }))] })) : (_jsx(_Fragment, {}));
|
|
33
42
|
}
|
package/lib/hooks/useCache.d.ts
CHANGED
|
@@ -233,6 +233,14 @@ export declare const queryKeys: {
|
|
|
233
233
|
readonly growth: {
|
|
234
234
|
readonly kpi: () => readonly ["growth", "kpi"];
|
|
235
235
|
};
|
|
236
|
+
readonly ray: {
|
|
237
|
+
readonly all: () => readonly ["ray"];
|
|
238
|
+
readonly clusters: () => readonly ["ray", "clusters"];
|
|
239
|
+
readonly cluster: (namespace: string, clusterName: string) => readonly ["ray", "cluster", string, string];
|
|
240
|
+
readonly jobs: (namespace: string, clusterName?: string) => readonly ["ray", "jobs", string, string];
|
|
241
|
+
readonly job: (namespace: string, jobName: string) => readonly ["ray", "job", string, string];
|
|
242
|
+
readonly logs: (namespace: string, jobName: string) => readonly ["ray", "job", string, string, "logs"];
|
|
243
|
+
};
|
|
236
244
|
readonly oauth2: {
|
|
237
245
|
readonly authorizationUrl: (queryArgs: Record<string, string>) => readonly ["oauth2", "authz", "url", Record<string, string>];
|
|
238
246
|
readonly authorizationLinkUrl: (queryArgs: Record<string, string>) => readonly ["oauth2", "authz", "url", "link", Record<string, string>];
|
|
@@ -899,6 +907,20 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
|
|
|
899
907
|
useUserSurveys: (userId: string, options?: Omit<UseQueryOptions<unknown>, "queryKey" | "queryFn">) => import("@tanstack/react-query").UseQueryResult<unknown, Error>;
|
|
900
908
|
useGrowthKPI: (options?: Omit<UseQueryOptions<unknown>, "queryKey" | "queryFn">) => import("@tanstack/react-query").UseQueryResult<unknown, Error>;
|
|
901
909
|
useGrowthContactsKPI: (options?: Omit<UseQueryOptions<unknown>, "queryKey" | "queryFn">) => import("@tanstack/react-query").UseQueryResult<unknown, Error>;
|
|
910
|
+
useRayClusters: () => import("@tanstack/react-query").UseQueryResult<{
|
|
911
|
+
clusters: any[];
|
|
912
|
+
namespaces: string[];
|
|
913
|
+
}, Error>;
|
|
914
|
+
useRayCluster: (namespace?: string, clusterName?: string) => import("@tanstack/react-query").UseQueryResult<any, Error>;
|
|
915
|
+
useCreateRayCluster: () => import("@tanstack/react-query").UseMutationResult<any, Error, Record<string, unknown>, unknown>;
|
|
916
|
+
useDeleteRayCluster: (namespace?: string) => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
|
|
917
|
+
useRayJobs: (namespace?: string, clusterName?: string) => import("@tanstack/react-query").UseQueryResult<any[], Error>;
|
|
918
|
+
useSubmitRayJob: (clusterName: string) => import("@tanstack/react-query").UseMutationResult<any, Error, Record<string, unknown>, unknown>;
|
|
919
|
+
useDeleteRayJob: (namespace?: string) => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
|
|
920
|
+
useRayJobLogs: (jobName: string, namespace?: string, tailLines?: number, enabled?: boolean) => import("@tanstack/react-query").UseQueryResult<{
|
|
921
|
+
logs: string;
|
|
922
|
+
status: string;
|
|
923
|
+
}, Error>;
|
|
902
924
|
queryKeys: {
|
|
903
925
|
readonly auth: {
|
|
904
926
|
readonly me: () => readonly ["auth", "me"];
|
|
@@ -1084,6 +1106,14 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
|
|
|
1084
1106
|
readonly growth: {
|
|
1085
1107
|
readonly kpi: () => readonly ["growth", "kpi"];
|
|
1086
1108
|
};
|
|
1109
|
+
readonly ray: {
|
|
1110
|
+
readonly all: () => readonly ["ray"];
|
|
1111
|
+
readonly clusters: () => readonly ["ray", "clusters"];
|
|
1112
|
+
readonly cluster: (namespace: string, clusterName: string) => readonly ["ray", "cluster", string, string];
|
|
1113
|
+
readonly jobs: (namespace: string, clusterName?: string) => readonly ["ray", "jobs", string, string];
|
|
1114
|
+
readonly job: (namespace: string, jobName: string) => readonly ["ray", "job", string, string];
|
|
1115
|
+
readonly logs: (namespace: string, jobName: string) => readonly ["ray", "job", string, string, "logs"];
|
|
1116
|
+
};
|
|
1087
1117
|
readonly oauth2: {
|
|
1088
1118
|
readonly authorizationUrl: (queryArgs: Record<string, string>) => readonly ["oauth2", "authz", "url", Record<string, string>];
|
|
1089
1119
|
readonly authorizationLinkUrl: (queryArgs: Record<string, string>) => readonly ["oauth2", "authz", "url", "link", Record<string, string>];
|
package/lib/hooks/useCache.js
CHANGED
|
@@ -305,6 +305,20 @@ export const queryKeys = {
|
|
|
305
305
|
growth: {
|
|
306
306
|
kpi: () => ['growth', 'kpi'],
|
|
307
307
|
},
|
|
308
|
+
// Ray (Runtimes)
|
|
309
|
+
ray: {
|
|
310
|
+
all: () => ['ray'],
|
|
311
|
+
clusters: () => [...queryKeys.ray.all(), 'clusters'],
|
|
312
|
+
cluster: (namespace, clusterName) => [...queryKeys.ray.all(), 'cluster', namespace, clusterName],
|
|
313
|
+
jobs: (namespace, clusterName) => [
|
|
314
|
+
...queryKeys.ray.all(),
|
|
315
|
+
'jobs',
|
|
316
|
+
namespace,
|
|
317
|
+
clusterName || 'all',
|
|
318
|
+
],
|
|
319
|
+
job: (namespace, jobName) => [...queryKeys.ray.all(), 'job', namespace, jobName],
|
|
320
|
+
logs: (namespace, jobName) => [...queryKeys.ray.job(namespace, jobName), 'logs'],
|
|
321
|
+
},
|
|
308
322
|
// OAuth2
|
|
309
323
|
oauth2: {
|
|
310
324
|
authorizationUrl: (queryArgs) => ['oauth2', 'authz', 'url', queryArgs],
|
|
@@ -2011,7 +2025,11 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
2011
2025
|
const principalUid = options?.principalUid;
|
|
2012
2026
|
const principalKind = options?.principalKind;
|
|
2013
2027
|
return useQuery({
|
|
2014
|
-
queryKey: [
|
|
2028
|
+
queryKey: [
|
|
2029
|
+
...queryKeys.datasources.all(),
|
|
2030
|
+
principalUid || 'self',
|
|
2031
|
+
principalKind || '',
|
|
2032
|
+
],
|
|
2015
2033
|
queryFn: async () => {
|
|
2016
2034
|
const resp = await requestDatalayer({
|
|
2017
2035
|
url: withAccountUidQuery(`${configuration.iamRunUrl}/api/iam/v1/datasources`, principalUid, principalKind),
|
|
@@ -2072,7 +2090,11 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
2072
2090
|
const principalUid = options?.principalUid;
|
|
2073
2091
|
const principalKind = options?.principalKind;
|
|
2074
2092
|
return useQuery({
|
|
2075
|
-
queryKey: [
|
|
2093
|
+
queryKey: [
|
|
2094
|
+
...queryKeys.secrets.all(),
|
|
2095
|
+
principalUid || 'self',
|
|
2096
|
+
principalKind || '',
|
|
2097
|
+
],
|
|
2076
2098
|
queryFn: async () => {
|
|
2077
2099
|
const resp = await requestDatalayer({
|
|
2078
2100
|
url: withAccountUidQuery(`${configuration.iamRunUrl}/api/iam/v1/secrets`, principalUid, principalKind),
|
|
@@ -2385,7 +2407,11 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
2385
2407
|
const principalUid = options?.principalUid;
|
|
2386
2408
|
const principalKind = options?.principalKind;
|
|
2387
2409
|
return useQuery({
|
|
2388
|
-
queryKey: [
|
|
2410
|
+
queryKey: [
|
|
2411
|
+
...queryKeys.datasources.detail(datasourceId),
|
|
2412
|
+
principalUid || 'self',
|
|
2413
|
+
principalKind || '',
|
|
2414
|
+
],
|
|
2389
2415
|
queryFn: async () => {
|
|
2390
2416
|
const resp = await requestDatalayer({
|
|
2391
2417
|
url: withAccountUidQuery(`${configuration.iamRunUrl}/api/iam/v1/datasources/${datasourceId}`, principalUid, principalKind),
|
|
@@ -2434,7 +2460,11 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
2434
2460
|
const principalUid = options?.principalUid;
|
|
2435
2461
|
const principalKind = options?.principalKind;
|
|
2436
2462
|
return useQuery({
|
|
2437
|
-
queryKey: [
|
|
2463
|
+
queryKey: [
|
|
2464
|
+
...queryKeys.secrets.detail(secretId),
|
|
2465
|
+
principalUid || 'self',
|
|
2466
|
+
principalKind || '',
|
|
2467
|
+
],
|
|
2438
2468
|
queryFn: async () => {
|
|
2439
2469
|
const resp = await requestDatalayer({
|
|
2440
2470
|
url: withAccountUidQuery(`${configuration.iamRunUrl}/api/iam/v1/secrets/${secretId}`, principalUid, principalKind),
|
|
@@ -6805,6 +6835,167 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
6805
6835
|
});
|
|
6806
6836
|
};
|
|
6807
6837
|
// ============================================================================
|
|
6838
|
+
// Ray (Runtimes) Hooks
|
|
6839
|
+
// ============================================================================
|
|
6840
|
+
const useRayClusters = () => {
|
|
6841
|
+
return useQuery({
|
|
6842
|
+
queryKey: queryKeys.ray.clusters(),
|
|
6843
|
+
queryFn: async () => {
|
|
6844
|
+
const resp = await requestDatalayer({
|
|
6845
|
+
url: `${configuration.runtimesRunUrl}/api/runtimes/v1/ray/clusters`,
|
|
6846
|
+
method: 'GET',
|
|
6847
|
+
});
|
|
6848
|
+
if (!resp.success) {
|
|
6849
|
+
throw new Error(resp.message || 'Failed to fetch Ray clusters');
|
|
6850
|
+
}
|
|
6851
|
+
return {
|
|
6852
|
+
clusters: asArray(resp.clusters),
|
|
6853
|
+
namespaces: asArray(resp.namespaces)
|
|
6854
|
+
.map(value => String(value))
|
|
6855
|
+
.filter(Boolean),
|
|
6856
|
+
};
|
|
6857
|
+
},
|
|
6858
|
+
...DEFAULT_QUERY_OPTIONS,
|
|
6859
|
+
refetchOnMount: true,
|
|
6860
|
+
enabled: Boolean(configuration.runtimesRunUrl),
|
|
6861
|
+
});
|
|
6862
|
+
};
|
|
6863
|
+
const useRayCluster = (namespace = 'default', clusterName = '') => {
|
|
6864
|
+
return useQuery({
|
|
6865
|
+
queryKey: queryKeys.ray.cluster(namespace, clusterName),
|
|
6866
|
+
queryFn: async () => {
|
|
6867
|
+
const resp = await requestDatalayer({
|
|
6868
|
+
url: `${configuration.runtimesRunUrl}/api/runtimes/v1/ray/clusters/${encodeURIComponent(clusterName)}?namespace=${encodeURIComponent(namespace)}`,
|
|
6869
|
+
method: 'GET',
|
|
6870
|
+
});
|
|
6871
|
+
if (!resp.success) {
|
|
6872
|
+
throw new Error(resp.message || 'Failed to fetch Ray cluster');
|
|
6873
|
+
}
|
|
6874
|
+
return resp.cluster || null;
|
|
6875
|
+
},
|
|
6876
|
+
...DEFAULT_QUERY_OPTIONS,
|
|
6877
|
+
enabled: Boolean(configuration.runtimesRunUrl) &&
|
|
6878
|
+
Boolean(namespace) &&
|
|
6879
|
+
Boolean(clusterName),
|
|
6880
|
+
});
|
|
6881
|
+
};
|
|
6882
|
+
const useCreateRayCluster = () => {
|
|
6883
|
+
return useMutation({
|
|
6884
|
+
mutationFn: async (payload) => {
|
|
6885
|
+
const resp = await requestDatalayer({
|
|
6886
|
+
url: `${configuration.runtimesRunUrl}/api/runtimes/v1/ray/clusters`,
|
|
6887
|
+
method: 'POST',
|
|
6888
|
+
body: payload,
|
|
6889
|
+
});
|
|
6890
|
+
if (resp?.success === false) {
|
|
6891
|
+
throw new Error(resp?.message || resp?.reason || 'Failed to create Ray cluster');
|
|
6892
|
+
}
|
|
6893
|
+
return resp;
|
|
6894
|
+
},
|
|
6895
|
+
onSuccess: () => {
|
|
6896
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.ray.all() });
|
|
6897
|
+
},
|
|
6898
|
+
});
|
|
6899
|
+
};
|
|
6900
|
+
const useDeleteRayCluster = (namespace = 'default') => {
|
|
6901
|
+
return useMutation({
|
|
6902
|
+
mutationFn: async (clusterName) => {
|
|
6903
|
+
return requestDatalayer({
|
|
6904
|
+
url: `${configuration.runtimesRunUrl}/api/runtimes/v1/ray/clusters/${encodeURIComponent(clusterName)}?namespace=${encodeURIComponent(namespace)}`,
|
|
6905
|
+
method: 'DELETE',
|
|
6906
|
+
});
|
|
6907
|
+
},
|
|
6908
|
+
onSuccess: () => {
|
|
6909
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.ray.all() });
|
|
6910
|
+
},
|
|
6911
|
+
});
|
|
6912
|
+
};
|
|
6913
|
+
const useRayJobs = (namespace = 'default', clusterName) => {
|
|
6914
|
+
return useQuery({
|
|
6915
|
+
queryKey: queryKeys.ray.jobs(namespace, clusterName),
|
|
6916
|
+
queryFn: async () => {
|
|
6917
|
+
const params = new URLSearchParams();
|
|
6918
|
+
params.set('namespace', namespace);
|
|
6919
|
+
if (clusterName) {
|
|
6920
|
+
params.set('cluster_name', clusterName);
|
|
6921
|
+
}
|
|
6922
|
+
const resp = await requestDatalayer({
|
|
6923
|
+
url: `${configuration.runtimesRunUrl}/api/runtimes/v1/ray/jobs?${params.toString()}`,
|
|
6924
|
+
method: 'GET',
|
|
6925
|
+
});
|
|
6926
|
+
if (!resp.success) {
|
|
6927
|
+
throw new Error(resp.message || 'Failed to fetch Ray jobs');
|
|
6928
|
+
}
|
|
6929
|
+
return asArray(resp.jobs);
|
|
6930
|
+
},
|
|
6931
|
+
...DEFAULT_QUERY_OPTIONS,
|
|
6932
|
+
enabled: Boolean(configuration.runtimesRunUrl),
|
|
6933
|
+
});
|
|
6934
|
+
};
|
|
6935
|
+
const useSubmitRayJob = (clusterName) => {
|
|
6936
|
+
return useMutation({
|
|
6937
|
+
mutationFn: async (payload) => {
|
|
6938
|
+
return requestDatalayer({
|
|
6939
|
+
url: `${configuration.runtimesRunUrl}/api/runtimes/v1/ray/clusters/${encodeURIComponent(clusterName)}/jobs`,
|
|
6940
|
+
method: 'POST',
|
|
6941
|
+
body: payload,
|
|
6942
|
+
});
|
|
6943
|
+
},
|
|
6944
|
+
onSuccess: () => {
|
|
6945
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.ray.all() });
|
|
6946
|
+
},
|
|
6947
|
+
});
|
|
6948
|
+
};
|
|
6949
|
+
const useDeleteRayJob = (namespace = 'default') => {
|
|
6950
|
+
return useMutation({
|
|
6951
|
+
mutationFn: async (jobName) => {
|
|
6952
|
+
return requestDatalayer({
|
|
6953
|
+
url: `${configuration.runtimesRunUrl}/api/runtimes/v1/ray/jobs/${encodeURIComponent(jobName)}?namespace=${encodeURIComponent(namespace)}`,
|
|
6954
|
+
method: 'DELETE',
|
|
6955
|
+
});
|
|
6956
|
+
},
|
|
6957
|
+
onSuccess: () => {
|
|
6958
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.ray.all() });
|
|
6959
|
+
},
|
|
6960
|
+
});
|
|
6961
|
+
};
|
|
6962
|
+
const useRayJobLogs = (jobName, namespace = 'default', tailLines = 200, enabled = true) => {
|
|
6963
|
+
return useQuery({
|
|
6964
|
+
queryKey: queryKeys.ray.logs(namespace, jobName),
|
|
6965
|
+
queryFn: async () => {
|
|
6966
|
+
const params = new URLSearchParams();
|
|
6967
|
+
params.set('namespace', namespace);
|
|
6968
|
+
params.set('tail_lines', String(tailLines));
|
|
6969
|
+
const resp = await requestDatalayer({
|
|
6970
|
+
url: `${configuration.runtimesRunUrl}/api/runtimes/v1/ray/jobs/${encodeURIComponent(jobName)}/logs?${params.toString()}`,
|
|
6971
|
+
method: 'GET',
|
|
6972
|
+
// Logs naturally 404 once a job is deleted; never surface a toast.
|
|
6973
|
+
notifyOnError: false,
|
|
6974
|
+
});
|
|
6975
|
+
if (!resp.success) {
|
|
6976
|
+
throw new Error(resp.message || 'Failed to fetch Ray job logs');
|
|
6977
|
+
}
|
|
6978
|
+
return {
|
|
6979
|
+
logs: String(resp.logs || ''),
|
|
6980
|
+
status: String(resp.status || ''),
|
|
6981
|
+
};
|
|
6982
|
+
},
|
|
6983
|
+
...DEFAULT_QUERY_OPTIONS,
|
|
6984
|
+
enabled: Boolean(configuration.runtimesRunUrl) && Boolean(jobName) && enabled,
|
|
6985
|
+
refetchOnMount: 'always',
|
|
6986
|
+
refetchInterval: query => {
|
|
6987
|
+
const status = String(query.state.data?.status || '').toLowerCase();
|
|
6988
|
+
const terminal = [
|
|
6989
|
+
'succeeded',
|
|
6990
|
+
'failed',
|
|
6991
|
+
'stopped',
|
|
6992
|
+
'completed',
|
|
6993
|
+
].includes(status);
|
|
6994
|
+
return terminal ? false : 3000;
|
|
6995
|
+
},
|
|
6996
|
+
});
|
|
6997
|
+
};
|
|
6998
|
+
// ============================================================================
|
|
6808
6999
|
// Return all methods grouped by category
|
|
6809
7000
|
// ============================================================================
|
|
6810
7001
|
return {
|
|
@@ -7093,6 +7284,15 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
|
|
|
7093
7284
|
useUserSurveys,
|
|
7094
7285
|
useGrowthKPI,
|
|
7095
7286
|
useGrowthContactsKPI,
|
|
7287
|
+
// Ray (Runtimes)
|
|
7288
|
+
useRayClusters,
|
|
7289
|
+
useRayCluster,
|
|
7290
|
+
useCreateRayCluster,
|
|
7291
|
+
useDeleteRayCluster,
|
|
7292
|
+
useRayJobs,
|
|
7293
|
+
useSubmitRayJob,
|
|
7294
|
+
useDeleteRayJob,
|
|
7295
|
+
useRayJobLogs,
|
|
7096
7296
|
// Query keys for manual operations
|
|
7097
7297
|
queryKeys,
|
|
7098
7298
|
// Upload
|
package/lib/models/Page.d.ts
CHANGED
|
@@ -29,6 +29,9 @@ export type IPage = {
|
|
|
29
29
|
screenCapture?: string;
|
|
30
30
|
creator?: IUser;
|
|
31
31
|
creatorId?: string;
|
|
32
|
+
selectedPrincipalUid?: string;
|
|
33
|
+
selectedPrincipalKind?: 'user' | 'organization' | 'team';
|
|
34
|
+
selectedOrganizationUid?: string;
|
|
32
35
|
kernelSnapshot?: ICodeSandboxSnapshot;
|
|
33
36
|
kernelSnapshotId?: string;
|
|
34
37
|
createdAt?: string;
|
|
@@ -40,7 +40,7 @@ let initialConfiguration = {
|
|
|
40
40
|
logoUrl: 'https://assets.datalayer.tech/datalayer-25.svg',
|
|
41
41
|
logoSquareUrl: 'https://assets.datalayer.tech/datalayer-square.png',
|
|
42
42
|
copyright: '© 2025 Datalayer, Inc',
|
|
43
|
-
docsUrl: 'https://
|
|
43
|
+
docsUrl: 'https://datalayer.ai/docs',
|
|
44
44
|
supportUrl: 'https://datalayer.ai/support',
|
|
45
45
|
termsUrl: 'https://datalayer.ai/terms',
|
|
46
46
|
pricingUrl: 'https://datalayer.ai/pricing',
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { URLExt } from '@jupyterlab/coreutils';
|
|
9
9
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
10
10
|
import { Upload } from 'tus-js-client';
|
|
11
|
-
import { requestDatalayerAPI
|
|
11
|
+
import { requestDatalayerAPI } from '../../api/index.js';
|
|
12
12
|
import { asCodeSandboxSnapshot } from '../../models/index.js';
|
|
13
13
|
import { iamStore, runtimesStore } from '../../state/index.js';
|
|
14
14
|
/**
|
|
@@ -30,6 +30,12 @@ export async function getEnvironments() {
|
|
|
30
30
|
*/
|
|
31
31
|
export async function createRuntime(options) {
|
|
32
32
|
const { externalToken, token } = iamStore.getState();
|
|
33
|
+
if (typeof options.creditsLimit !== 'number' ||
|
|
34
|
+
!Number.isFinite(options.creditsLimit) ||
|
|
35
|
+
options.creditsLimit <= 0) {
|
|
36
|
+
throw new Error(`Invalid runtime creditsLimit for environment ${options.environmentName}. ` +
|
|
37
|
+
'A positive number is required.');
|
|
38
|
+
}
|
|
33
39
|
const body = {
|
|
34
40
|
environment_name: options.environmentName,
|
|
35
41
|
type: options.type ?? 'notebook',
|
package/lib/utils/Name.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export declare const namesAsInitials: (firstName: string, lastName: string) => s
|
|
|
5
5
|
* Convert first and last names to a friendly name.
|
|
6
6
|
*/
|
|
7
7
|
export declare const toFriendlyName: (firstName?: string, lastName?: string) => string;
|
|
8
|
+
export declare const createRandomTimestampName: () => string;
|
package/lib/utils/Name.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
+
import { adjectives, animals, uniqueNamesGenerator, } from 'unique-names-generator';
|
|
6
|
+
const RANDOM_TIMESTAMP_NAME_CONFIGURATION = {
|
|
7
|
+
dictionaries: [adjectives, animals],
|
|
8
|
+
separator: '-',
|
|
9
|
+
};
|
|
5
10
|
export const asDisplayName = (givenName, familyName) => {
|
|
6
11
|
return givenName
|
|
7
12
|
? familyName
|
|
@@ -34,3 +39,6 @@ export const toFriendlyName = (firstName, lastName) => {
|
|
|
34
39
|
}
|
|
35
40
|
return '';
|
|
36
41
|
};
|
|
42
|
+
export const createRandomTimestampName = () => {
|
|
43
|
+
return `${uniqueNamesGenerator(RANDOM_TIMESTAMP_NAME_CONFIGURATION)}-${Date.now()}`;
|
|
44
|
+
};
|
package/lib/utils/Snapshot.js
CHANGED
|
@@ -6,7 +6,7 @@ import { adjectives, animals, uniqueNamesGenerator, } from 'unique-names-generat
|
|
|
6
6
|
/**
|
|
7
7
|
* Kernel snapshot description configuration.
|
|
8
8
|
*/
|
|
9
|
-
const
|
|
9
|
+
const CODE_SANDBOX_SNAPSHOT_DESCRIPTION_CONFIGURATION = {
|
|
10
10
|
dictionaries: [adjectives, animals],
|
|
11
11
|
separator: '-',
|
|
12
12
|
};
|
|
@@ -17,5 +17,5 @@ const KERNEL_SNAPSHOT_DESCRIPTION_CONFIGURATION = {
|
|
|
17
17
|
* @returns The kernel snapshot name
|
|
18
18
|
*/
|
|
19
19
|
export function createSandboxSnapshotName(suffix) {
|
|
20
|
-
return `${uniqueNamesGenerator(
|
|
20
|
+
return `${uniqueNamesGenerator(CODE_SANDBOX_SNAPSHOT_DESCRIPTION_CONFIGURATION)}-${suffix}`;
|
|
21
21
|
}
|