@datalayer/core 1.0.22 → 1.0.23

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.
@@ -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: this.close, renderBody: this._renderBody, renderFooter: this._renderFooter, footerButtons: this.buttons.map((but, idx) => {
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
- this.setButton(this.buttons[idx]);
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
  };
@@ -46,6 +46,10 @@ export interface IRuntimeLauncherDialogProps {
46
46
  * Upgrade subscription URL
47
47
  */
48
48
  upgradeSubscription?: string;
49
+ /**
50
+ * Optional submit button label override.
51
+ */
52
+ submitLabel?: string;
49
53
  }
50
54
  /**
51
55
  * Start Runtime Launcher Dialog.
@@ -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(Math.min(credits?.available ?? 0, 10));
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 (startRuntime) {
64
+ if (shouldStartRuntime) {
64
65
  refreshCredits();
65
66
  }
66
- }, [startRuntime]);
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 max = Math.floor((credits?.available ?? 0) * creditsToMinutes);
72
- const outOfCredits = startRuntime && (!credits?.available || max < Number.EPSILON);
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(true);
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 (startRuntime && startRuntime !== 'defer') {
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) ? ('Launch') : ('Assign from the Environment'),
232
- disabled: waitingForRuntime || outOfCredits || timeLimit < Number.EPSILON,
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: max, 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 &&
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
  }
@@ -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: false, markdownParser: markdownParser, sanitizer: sanitizer }))] }));
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: true, 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(Math.min(credits?.available ?? 0, 10));
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 creditsLimit = selectedRuntimeDesc?.location === 'remote' &&
116
- selectedRuntimeDesc.burningRate
117
- ? Math.min(timeLimit, MAXIMAL_RUNTIME_TIME_RESERVATION_MINUTES) *
118
- selectedRuntimeDesc.burningRate *
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
- setValue(creditsLimit !== 0
122
- ? {
123
- runtime: selectedRuntimeDesc
124
- ? {
125
- environmentName: ['browser', 'remote'].includes(selectedRuntimeDesc.location)
126
- ? `${selectedRuntimeDesc.location}-${selectedRuntimeDesc.name}`
127
- : selectedRuntimeDesc.name,
128
- id: selectedRuntimeDesc.kernelId,
129
- creditsLimit,
130
- capabilities: userStorage ? ['user_storage'] : undefined,
131
- }
132
- : null,
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
- : new Error('Credits limit must be strictly positive.'));
136
- }, [selectedRuntimeDesc, userStorage, toTransfer, timeLimit]);
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 max = Math.floor((credits?.available ?? 0) / (selectedRuntimeDesc?.burningRate ?? -1) / 60.0);
139
- const outOfCredits = !credits?.available || max < Number.EPSILON;
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: max < 0 ? 1 : max, time: timeLimit, onTimeChange: setTimeLimit, error: outOfCredits && max >= 0
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: time, onChange: onTimeChange, disabled: disabled, label: "", displayValue: false }), _jsx(TextInput, { type: "number", step: "1", min: "1", max: max, disabled: disabled, value: Math.min(max, time).toFixed(2), onChange: event => {
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
  }
@@ -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, } from '../../api/index.js';
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/core",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",