@datalayer/core
Version:
**Datalayer Core**
251 lines (250 loc) • 13.8 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
/*
* Copyright (c) 2023-2025 Datalayer, Inc.
* Distributed under the terms of the Modified BSD License.
*/
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useIsMounted } from 'usehooks-ts';
import { Button, FormControl, Select, Spinner, Text, TextInput, ToggleSwitch, Tooltip, IconButton } from '@primer/react';
import { Dialog } from '@primer/react/experimental';
import { AlertIcon } from '@primer/octicons-react';
import { Box } from "@datalayer/primer-addons";
import { USAGE_ROUTE } from '../../routes';
import { useNavigate } from '../../hooks';
import { NO_RUNTIME_AVAILABLE_LABEL } from '../../i18n';
import { iamStore, useCoreStore, useIAMStore, useRuntimesStore } from '../../state';
import { createNotebook, sleep } from '../../utils';
import { Markdown } from '../display';
import { Timer } from '../progress';
import { FlashClosable } from '../flashes';
import { RuntimeReservationControl, MAXIMAL_RUNTIME_TIME_RESERVATION_MINUTES } from './RuntimeReservationControl';
/**
* Initial time in milliseconds before retrying in case no kernels are available
*/
const NOT_AVAILABLE_INIT_RETRY = 10_000;
/**
* Number of trials in case of unavailable kernels
*/
const NOT_AVAILABLE_RETRIES = 5;
/**
* Start Remote Runtime Dialog.
*/
export function KernelLauncherDialog(props) {
const { dialogTitle, kernelSnapshot, manager, onSubmit, markdownParser, sanitizer, upgradeSubscription, startKernel = true } = props;
const hasExample = startKernel === 'with-example';
const user = iamStore.getState().user;
const environments = manager.environments.get();
const { configuration } = useCoreStore();
const { credits, refreshCredits } = useIAMStore();
let navigate;
try {
// eslint-disable-next-line react-hooks/rules-of-hooks
navigate = useNavigate();
}
catch (reason) {
// TODO when would this component be shown outside of a react-router? navigation is only available within a react-router.
console.warn(reason);
}
const { jupyterLabAdapter } = useRuntimesStore();
const [selection, setSelection] = useState((kernelSnapshot?.environment || environments[0]?.name) ?? '');
const [timeLimit, setTimeLimit] = useState(Math.min(credits?.available ?? 0, 10));
const [runtimeName, setRuntimeName] = useState(environments[0]?.kernel?.givenNameTemplate || environments[0]?.title || '');
// Whether the runtim name has been changed by the user or not
const [hasCustomRuntimeName, setHasCustomRuntimeName] = useState(false);
const [userStorage, setUserStorage] = useState(false);
const [openExample, setOpenExample] = useState(false);
const [waitingForRuntime, setWaitingForRuntime] = useState(false);
const [error, setError] = useState();
const [flashLevel, setFlashLevel] = useState('danger');
const isMounted = useIsMounted();
useEffect(() => {
if (startKernel) {
refreshCredits();
}
}, [startKernel]);
const spec = useMemo(() => environments.find(spec => spec.name === selection), [environments, selection]);
const description = spec?.description ?? '';
const burningRate = spec?.burning_rate ?? 1;
const creditsToMinutes = 1.0 / burningRate / 60.0;
const max = Math.floor((credits?.available ?? 0) * creditsToMinutes);
const outOfCredits = startKernel && (!credits?.available || max < Number.EPSILON);
const handleSelectionChange = useCallback((e) => {
const selection = e.target.value;
setSelection(selection);
if (!hasCustomRuntimeName) {
const spec = environments.find(env => env.name === selection);
setRuntimeName(spec?.kernel?.givenNameTemplate || spec?.title || '');
}
}, [setSelection, hasCustomRuntimeName]);
const handleSubmitKernel = useCallback(async () => {
if (selection) {
setError(undefined);
setWaitingForRuntime(true);
const spec = environments.find(s => s.name === selection);
const desc = {
name: selection,
language: spec?.language ?? '',
location: 'remote',
displayName: runtimeName ?? spec?.title,
};
const creditsLimit = Math.min(timeLimit, MAXIMAL_RUNTIME_TIME_RESERVATION_MINUTES) / creditsToMinutes;
desc.params = {};
if (startKernel === 'defer') {
desc.params['creditsLimit'] = creditsLimit;
}
if (userStorage) {
desc.params['capabilities'] = ['user_storage'];
}
let success = true;
if (startKernel && startKernel !== 'defer') {
success = false;
let availableTrial = 1;
let retryDelay = NOT_AVAILABLE_INIT_RETRY;
// Should return success status.
const startNewKernel = async () => {
try {
const connection = await manager.runtimesManager.startNew({
environmentName: selection,
type: 'notebook',
givenName: runtimeName,
creditsLimit: creditsLimit,
capabilities: userStorage
? ['user_storage']
: undefined,
snapshot: kernelSnapshot?.id
}, {
username: user?.handle,
handleComms: true
});
desc.kernelId = connection.id;
if (jupyterLabAdapter?.jupyterLab && hasExample && openExample) {
const example = environments.find(spec => spec.name === selection)?.example;
if (example) {
const options = {
kernelId: connection.id,
kernelName: connection.name
};
createNotebook({
app: jupyterLabAdapter.jupyterLab,
name: selection,
url: example,
options
});
}
}
// Close the connection as we are not using it.
connection.dispose();
}
catch (error) {
let msg = _jsx(Text, { children: "Failed to create the remote runtime\u2026" });
let level = 'danger';
let retry = false;
if (error.response?.status === 503) {
if (availableTrial++ <= NOT_AVAILABLE_RETRIES) {
retry = true;
msg = (_jsxs(Text, { children: ["The runtime you have requested is currently not available due to resource limitations. Leave this dialog open, new trial in ", _jsx(Timer, { duration: retryDelay * 0.001 }), ` (${availableTrial - 1}/${NOT_AVAILABLE_RETRIES}).`] }));
}
else {
msg = _jsx(Text, { children: NO_RUNTIME_AVAILABLE_LABEL });
}
level = 'warning';
}
else if (error.name === 'MaxRuntimesExceededError') {
msg = (_jsx(Text, { children: "You reached your remote runtime limits. Stop existing runtimes before starting new ones." }));
level = 'warning';
}
else if (error.name === 'RuntimeUnreachable') {
msg = (_jsx(Text, { children: "The runtime has been created but can not be accessed. Please contact your IT support team to report this issue." }));
}
setFlashLevel(level);
console.error(msg, error);
setError(msg);
if (retry) {
await sleep(retryDelay);
retryDelay *= 2;
if (isMounted()) {
return await startNewKernel();
}
}
return false;
}
finally {
setWaitingForRuntime(false);
}
return true;
};
// Start the kernel if the reservation succeeded.
success = await startNewKernel();
}
if (success && isMounted()) {
onSubmit(desc);
}
}
}, [
manager,
selection,
startKernel,
runtimeName,
onSubmit,
userStorage,
openExample,
jupyterLabAdapter,
timeLimit,
isMounted
]);
const handleUserStorageChange = useCallback((e) => {
e.preventDefault();
setUserStorage(!userStorage);
}, [userStorage]);
const handleSwitchClick = useCallback((e) => {
e.preventDefault();
setOpenExample(!openExample);
}, [openExample]);
const handleUpgrade = useCallback(() => {
if (upgradeSubscription) {
navigate?.(upgradeSubscription);
}
}, [navigate, upgradeSubscription]);
const handleKernelNameChange = useCallback((e) => {
if (typeof e.target.value === 'string') {
setRuntimeName(e.target.value);
setHasCustomRuntimeName(true);
}
}, []);
return (_jsx(Dialog, { title: dialogTitle || 'Launch a new Runtime', onClose: () => { onSubmit(undefined); }, footerButtons: [
{
buttonType: 'default',
onClick: () => { onSubmit(undefined); },
content: 'Cancel',
disabled: waitingForRuntime
},
{
buttonType: 'primary',
onClick: handleSubmitKernel,
content: waitingForRuntime ?
_jsx(Spinner, { size: "small" })
:
startKernel ?? true ? 'Launch' : 'Assign from the Environment',
disabled: waitingForRuntime || outOfCredits || timeLimit < Number.EPSILON,
autoFocus: true
}
], children: _jsxs(Box, { as: "form", onKeyDown: event => {
if (event.defaultPrevented) {
return;
}
if (event.key === 'Enter') {
event.preventDefault();
handleSubmitKernel();
}
}, children: [_jsxs(FormControl, { disabled: !!kernelSnapshot?.environment || environments.length === 0, children: [_jsx(FormControl.Label, { children: "Environment" }), _jsx(Select, { name: "environment", disabled: !!kernelSnapshot?.environment || environments.length === 0, value: selection, onChange: handleSelectionChange, block: true, children: environments.map(spec => (_jsxs(Select.Option, { value: spec.name, children: [spec.name, spec.title && (_jsxs(_Fragment, { children: [' - ', spec.title] }))] }, spec.name))) }), _jsx(FormControl.Caption, { children: _jsx(_Fragment, { children: markdownParser ? (_jsx(Box, { sx: { img: { maxWidth: '100%' } }, children: _jsx(Markdown, { text: description, markdownParser: markdownParser, sanitizer: sanitizer }) })) : (description) }) })] }), startKernel &&
_jsx(RuntimeReservationControl, { addCredits: navigate ?
() => { navigate(USAGE_ROUTE); }
:
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, { 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 })] }), _jsxs(FormControl, { sx: { paddingTop: '10px' }, children: [_jsx(FormControl.Label, { children: "Runtime name" }), _jsx(TextInput, { name: "name", value: runtimeName, onChange: handleKernelNameChange, block: true })] }), hasExample && jupyterLabAdapter?.jupyterLab && !configuration.whiteLabel &&
_jsxs(FormControl, { sx: { paddingTop: '10px' }, children: [_jsx(FormControl.Label, { children: "Open example notebook" }), _jsx(ToggleSwitch, { disabled: !environments.find(spec => spec.name === selection)?.example, checked: openExample, size: "small", onClick: handleSwitchClick })] }), error &&
_jsx(FlashClosable, { variant: flashLevel, actions: navigate && upgradeSubscription && flashLevel === 'warning' ?
_jsx(Button, { onClick: handleUpgrade, title: 'Upgrade your subscription.', children: "Upgrade" })
:
undefined, children: error })] }) }));
}