UNPKG

@datalayer/core

Version:
251 lines (250 loc) 13.8 kB
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 })] }) })); }