UNPKG

@datalayer/core

Version:

[![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.io)

263 lines (262 loc) 12.7 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, useState } from 'react'; import { PathExt } from '@jupyterlab/coreutils'; import { PromiseDelegate } from '@lumino/coreutils'; import { ActionList, ActionMenu, Heading, IconButton, Spinner, TreeView, } from '@primer/react'; import { Box } from '@datalayer/primer-addons'; import { Blankslate, Dialog } from '@primer/react/experimental'; import { CounterClockWiseIcon } from '@datalayer/icons-react'; import { useIsMounted } from 'usehooks-ts'; import { useToast } from '../../hooks'; import { UploadIconButton } from '../buttons'; import { DirectoryItem, TreeItem, modelToView, } from './ContentsItems'; /** * The maximum upload size (in bytes) for notebook version < 5.1.0 */ export const LARGE_FILE_SIZE = 15 * 1024 * 1024; /** * The size (in bytes) of the biggest chunk we should upload at once. */ export const CHUNK_SIZE = 1024 * 1024; /** * Storage browser component. */ export function ContentsBrowser(props) { const { contents, localContents, documentRegistry } = props; const isMounted = useIsMounted(); const { trackAsyncTask } = useToast(); const [children, setChildren] = useState(null); const [isLoading, setIsLoading] = useState(true); const [selectedItem, setSelectedItem] = useState(null); const [contextMenuAnchor, setContextMenuAnchor] = useState(null); const [openDeleteConfirmation, setOpenDeleteConfirmation] = useState(false); const [copyToLocalConfirmation, setCopyToLocalConfirmation] = useState(false); const refresh = useCallback(() => { contents .get('') .then(model => { setIsLoading(false); setChildren(modelToView(model.content, documentRegistry)); }) .catch(reason => { setIsLoading(false); console.error(`Failed to fetch folder '' content for manager ${contents.serverSettings.appUrl}.`, reason); }); }, [contents]); useEffect(() => { refresh(); }, [refresh]); const upload = useCallback( /** * @param file File to upload */ async (file) => { const checkIsMounted = () => { if (!isMounted()) { return Promise.reject(`Failed to upload ${file.name}; StorageBrowser component is unmounted.`); } }; checkIsMounted(); const chunked = file.size > CHUNK_SIZE; const currentDirectory = selectedItem ? selectedItem.type === 'directory' ? selectedItem.path : PathExt.dirname(selectedItem.path) : ''; const path = currentDirectory ? PathExt.join(currentDirectory, file.name) : file.name; const name = file.name; const type = 'file'; const format = 'base64'; const uploadChunk = async (blob, chunk) => { checkIsMounted(); const reader = new FileReader(); reader.readAsDataURL(blob); await new Promise((resolve, reject) => { reader.onload = resolve; reader.onerror = event => reject(`Failed to upload "${file.name}":` + event); }); checkIsMounted(); // remove header https://stackoverflow.com/a/24289420/907060 const content = reader.result.split(',')[1]; const model = { type, format, name, chunk, content, }; return await contents.save(path, model); }; const toastOptions = { error: { message: reason => { const msg = `Failed to upload ${file.name}.`; console.error(msg, reason); return msg; }, }, pending: { message: `Uploading ${file.name}…` }, success: { message: () => `${file.name} uploaded.` }, }; if (chunked) { const task = new PromiseDelegate(); trackAsyncTask(task.promise, toastOptions); try { let finalModel; for (let start = 0; !finalModel; start += CHUNK_SIZE) { const end = start + CHUNK_SIZE; const lastChunk = end >= file.size; const chunk = lastChunk ? -1 : end / CHUNK_SIZE; const currentModel = await uploadChunk(file.slice(start, end), chunk); if (lastChunk) { finalModel = currentModel; task.resolve(finalModel); } } if (selectedItem) { selectedItem.refresh(); } else { refresh(); } return finalModel; } catch (error) { task.reject(error); throw error; } } else { const task = uploadChunk(file); trackAsyncTask(task, toastOptions); task.then(() => { if (selectedItem) { selectedItem.refresh(); } else { refresh(); } }); return task; } }, [contents, selectedItem, refresh]); const onContextMenu = useCallback((ref) => { if (contextMenuAnchor === ref) { setContextMenuAnchor(null); } else { setContextMenuAnchor(ref); } }, [contextMenuAnchor]); const onSelectDelete = useCallback(() => { setOpenDeleteConfirmation(true); }, []); const deleteItem = useCallback(() => { if (selectedItem) { const task = contents.delete(selectedItem.path); trackAsyncTask(task, { success: { message: () => `${selectedItem.path} deleted.` }, pending: { message: `Deleting ${selectedItem.path}…` }, error: { message: reason => { const msg = `Failed to delete ${selectedItem.path}.`; console.error(msg, reason); return msg; }, }, }); task.finally(() => { selectedItem.refresh(); }); } setOpenDeleteConfirmation(false); }, [contents, selectedItem]); const onSelectCopyToLocal = useCallback(() => { setCopyToLocalConfirmation(true); }, []); const copyToLocal = useCallback(() => { if (selectedItem && localContents) { contents.get(selectedItem.path).then(model => { const copyTask = localContents?.save(model.path, model); trackAsyncTask(copyTask, { success: { message: () => `${selectedItem.path} copied to local.` }, pending: { message: `Copying to local ${selectedItem.path}…` }, error: { message: reason => { const msg = `Failed to copy to local ${selectedItem.path}.`; console.error(msg, reason); return msg; }, }, }); copyTask.finally(() => { selectedItem.refresh(); }); setCopyToLocalConfirmation(false); }); } }, [localContents, selectedItem]); const onSelect = useCallback((item, refresh) => { setSelectedItem(item.path === selectedItem?.path ? null : { ...item, refresh }); }, [selectedItem]); return (_jsxs(Box, { sx: { display: 'grid', gridTemplateAreas: `"header" "content"` }, children: [_jsxs(Box, { sx: { gridArea: 'header', display: 'flex', alignItems: 'center' }, children: [_jsx(Heading, { as: "h4", sx: { fontSize: 'var(--text-title-size-small)', lineHeight: 'var(--text-title-lineHeight-medium)', fontWeight: 'var(--text-title-weight-medium)', flex: '1 1 auto', }, children: "Contents Browser" }), _jsxs(Box, { children: [_jsx(IconButton, { variant: "invisible", "aria-label": 'Refresh contents browser.', title: 'Refresh contents browser.', icon: CounterClockWiseIcon, onClick: refresh }), _jsx(UploadIconButton, { label: 'Upload a file', multiple: true, upload: upload })] })] }), isLoading ? (_jsx(Box, { sx: { gridArea: 'content', display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '40px', height: '100vh', }, children: _jsx(Spinner, {}) })) : (_jsx(Box, { sx: { gridArea: 'content' }, children: children ? (_jsxs(_Fragment, { children: [_jsx(TreeView, { children: children?.map(child => { return child.type === 'directory' ? (_jsx(DirectoryItem, { item: child, contents: contents, current: selectedItem, documentRegistry: documentRegistry, onContextMenu: onContextMenu, onSelect: onSelect }, child.name)) : (_jsx(TreeItem, { item: child, current: selectedItem?.path === child.path, onSelect: item => { onSelect(item, refresh); }, onContextMenu: onContextMenu }, child.name)); }) }), _jsx(ActionMenu, { anchorRef: contextMenuAnchor ?? undefined, open: contextMenuAnchor?.current !== null, onOpenChange: () => { setContextMenuAnchor(null); }, children: _jsx(ActionMenu.Overlay, { children: _jsxs(ActionList, { children: [_jsx(ActionList.Item, { title: "Delete the active item.", onSelect: onSelectDelete, children: "Delete\u2026" }), localContents && (_jsx(ActionList.Item, { title: "Copy the active item to the local drive.", onSelect: onSelectCopyToLocal, children: "Copy to local drive\u2026" }))] }) }) }), openDeleteConfirmation && (_jsx(Dialog, { title: _jsx("span", { style: { color: 'var(--fgColor-default)' }, children: "Confirm deletion" }), onClose: () => { setOpenDeleteConfirmation(false); }, footerButtons: [ { buttonType: 'default', content: 'Cancel', onClick: () => { setOpenDeleteConfirmation(false); }, }, { buttonType: 'danger', content: 'Delete', onClick: () => { deleteItem(); }, }, ], children: `Are you sure you want to delete ${selectedItem?.path}?` })), copyToLocalConfirmation && (_jsx(Dialog, { title: _jsx("span", { style: { color: 'var(--fgColor-default)' }, children: "Confirm copy to local" }), onClose: () => { setCopyToLocalConfirmation(false); }, footerButtons: [ { buttonType: 'default', content: 'Cancel', onClick: () => { setCopyToLocalConfirmation(false); }, }, { buttonType: 'danger', content: 'Copy to local', onClick: () => { copyToLocal(); }, }, ], children: `Are you sure you want to copy to local ${selectedItem?.path}?` }))] })) : (_jsx(Blankslate, { children: _jsx(Blankslate.Heading, { children: "No contents" }) })) }))] })); } export default ContentsBrowser;