diff --git a/ui-admin/src/js/common/messages.js b/ui-admin/src/js/common/messages.js index 5bb2865196e60cd5b3e87363b0374dff1bc96004..ced5dd3f2394a9ec19a7c3c5cab38d0a4e0aabad 100644 --- a/ui-admin/src/js/common/messages.js +++ b/ui-admin/src/js/common/messages.js @@ -77,6 +77,7 @@ const keys = { DESCRIPTION: 'DESCRIPTION', DISALLOW_DELETION: 'DISALLOW_DELETION', DOWNLOAD: 'DOWNLOAD', + DOWNLOADS_NOT_SUPPORTED: 'DOWNLOADS_NOT_SUPPORTED', DYNAMIC_PROPERTY_PLUGIN: 'DYNAMIC_PROPERTY_PLUGIN', DYNAMIC_PROPERTY_PLUGINS: 'DYNAMIC_PROPERTY_PLUGINS', EDIT: 'EDIT', @@ -421,6 +422,7 @@ const messages_en = { [keys.DESCRIPTION]: 'Description', [keys.DISALLOW_DELETION]: 'Disallow Deletion', [keys.DOWNLOAD]: 'Download', + [keys.DOWNLOADS_NOT_SUPPORTED]: 'Downloads are not supported for this browser. The supported browsers are the desktop versions of Chrome, Edge and Opera.', [keys.DYNAMIC_PROPERTY_PLUGINS]: 'Dynamic Property Plugins', [keys.DYNAMIC_PROPERTY_PLUGIN]: 'Dynamic Property Plugin', [keys.EDIT]: 'Edit', diff --git a/ui-admin/src/js/components/database/data-browser/DataBrowser.jsx b/ui-admin/src/js/components/database/data-browser/DataBrowser.jsx index 5b994811c73b5db7c4a7678c7d8798ce3c963d9c..ca7830e87a9d0be95cda22b93f3f2840a065c7fa 100644 --- a/ui-admin/src/js/components/database/data-browser/DataBrowser.jsx +++ b/ui-admin/src/js/components/database/data-browser/DataBrowser.jsx @@ -12,7 +12,8 @@ import InfoPanel from '@src/js/components/database/data-browser/InfoPanel.jsx' import DataBrowserController from '@src/js/components/database/data-browser/DataBrowserController.js' import messages from '@src/js/common/messages.js' import InfoBar from '@src/js/components/database/data-browser/InfoBar.jsx' -import LoadingDialog from "@src/js/components/common/loading/LoadingDialog.jsx"; +import LoadingDialog from '@src/js/components/common/loading/LoadingDialog.jsx' +import ErrorDialog from '@src/js/components/common/error/ErrorDialog.jsx' const styles = theme => ({ columnFlexContainer: { @@ -261,7 +262,8 @@ class DataBrowser extends React.Component { freeSpace: -1, totalSpace: -1, loading: false, - progress: 0 + progress: 0, + errorMessage: null } this.zip = new JSZip() } @@ -313,6 +315,8 @@ class DataBrowser extends React.Component { async downloadFiles() { const { multiselectedFiles } = this.state const { id } = this.props + + // TODO: implement download by chunks const zipBlob = await this.prepareZipBlob(multiselectedFiles) this.downloadBlob(zipBlob, id) this.zip = new JSZip() @@ -336,11 +340,15 @@ class DataBrowser extends React.Component { } async downloadFile(file) { - try { - this.setState({ loading: true, progress: 0 }) - await this.controller.downloadAndSaveFile(file, this.updateProgress) - } finally { - this.setState({ loading: false, progress: 0 }) + if ('showSaveFilePicker' in window) { + try { + this.setState({ loading: true, progress: 0 }) + await this.controller.downloadAndSaveFile(file, this.updateProgress) + } finally { + this.setState({ loading: false, progress: 0 }) + } + } else { + this.openErrorDialog(messages.get(messages.DOWNLOADS_NOT_SUPPORTED)) } } @@ -435,6 +443,14 @@ class DataBrowser extends React.Component { this.fetchSpaceStatus() } + openErrorDialog(errorMessage) { + this.setState({ errorMessage }) + } + + closeErrorDialog() { + this.setState({ errorMessage: null }) + } + render() { const { classes, sessionToken, id } = this.props const { @@ -447,7 +463,8 @@ class DataBrowser extends React.Component { freeSpace, totalSpace, loading, - progress + progress, + errorMessage } = this.state return ([ @@ -583,7 +600,9 @@ class DataBrowser extends React.Component { </div> </div>, <LoadingDialog key='data-browser-loaging-dialog' variant='determinate' - value={progress} loading={loading} /> + value={progress} loading={loading} />, + <ErrorDialog open={!!errorMessage} error={errorMessage} + onClose={this.closeErrorDialog} /> ]) } } diff --git a/ui-admin/src/js/components/database/data-browser/DataBrowserController.js b/ui-admin/src/js/components/database/data-browser/DataBrowserController.js index a96e866681c48a33918c428314005f0331c4d6e1..693cd0da45d77d7435443247e66916d71e17e065 100644 --- a/ui-admin/src/js/components/database/data-browser/DataBrowserController.js +++ b/ui-admin/src/js/components/database/data-browser/DataBrowserController.js @@ -34,7 +34,7 @@ export default class DataBrowserController extends ComponentController { async free() { try { - return await openbis.free(this.owner, this.path); + return await openbis.free(this.owner, this.path) } catch (error) { if (error.message.includes('NoSuchFileException')) { return [] @@ -100,7 +100,7 @@ export default class DataBrowserController extends ComponentController { async copy(files, newLocation) { for (const file of files) { - await this._copy(file, newLocation); + await this._copy(file, newLocation) } if (this.gridController) { @@ -193,31 +193,17 @@ export default class DataBrowserController extends ComponentController { async _fileSliceToBinaryString(blob) { return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result); - reader.onerror = (error) => reject(error); - reader.readAsBinaryString(blob); - }); + const reader = new FileReader() + reader.onload = () => resolve(reader.result) + reader.onerror = (error) => reject(error) + reader.readAsBinaryString(blob) + }) } async _uploadChunk(source, offset, data) { return await openbis.write(this.owner, source, offset, data) } - async _arrayBufferToBase64(buffer) { - return new Promise((resolve, reject) => { - const blob = new Blob([buffer], {type: 'application/octet-stream'}) - const reader = new FileReader() - reader.onloadend = () => { - const dataUrl = reader.result - const base64String = dataUrl.split(',')[1] - resolve(base64String) - }; - reader.onerror = reject - reader.readAsDataURL(blob) - }); - } - async download(file) { let offset = 0 const dataArray = [] @@ -232,51 +218,38 @@ export default class DataBrowserController extends ComponentController { } async downloadAndSaveFile(file, onProgressUpdate) { - const fileHandle = await window.showSaveFilePicker() - const writable = await fileHandle.createWritable() - try { - let offset = 0; - - const size = file.size; - while (offset < size) { - const chunk = await this._download(file, offset) - await writable.write(chunk) - offset += CHUNK_SIZE - - const progress = Math.round((offset / size) * 100) - onProgressUpdate(Math.min(progress, 100)) + const fileHandle = await window.showSaveFilePicker( + { + startIn: 'downloads', + id: 'download-file-picker', + suggestedName: file.name + }) + const writable = await fileHandle.createWritable() + + try { + let offset = 0 + + const size = file.size + while (offset < size) { + const chunk = await this._download(file, offset) + await writable.write(chunk) + offset += CHUNK_SIZE + + const progress = Math.round((offset / size) * 100) + onProgressUpdate(Math.min(progress, 100)) + } + } finally { + onProgressUpdate(100) + await writable.close() + } + } catch (error) { + if (error.name !== 'AbortError') { + throw error } - } finally { - onProgressUpdate(100) - await writable.close() } } - // async downloadFile(file) { - // // Check if StreamSaver's service worker is correctly set up - // if (!navigator.serviceWorker.controller) { - // const registration = await navigator.serviceWorker.register('/sw.js'); // Path to your service worker file - // await navigator.serviceWorker.ready; // Wait for the service worker to be ready - // } - // - // const streamSaver = window.streamSaver - // streamSaver.mitm = 'https://cdn.jsdelivr.net/npm/streamsaver@2/mitm.html' - // const fileStream = streamSaver.createWriteStream(file.name); - // const writer = fileStream.getWriter(); - // - // let offset = 0; - // - // while (offset < file.size) { - // const chunk = await this._download(file, offset) - // const buffer = await chunk.arrayBuffer() - // await writer.write(new Uint8Array(buffer)) - // offset += CHUNK_SIZE - // } - // - // writer.close() - // } - async _download(file, offset) { const limit = Math.min(CHUNK_SIZE, file.size - offset) return await openbis.read(this.owner, file.path, offset, limit) @@ -286,10 +259,6 @@ export default class DataBrowserController extends ComponentController { return path && path[0] === '/' ? path.substring(1) : path } - handleUploadClick(event) { - console.log(event.target) - } - setPath(path) { this.path = path } diff --git a/ui-admin/src/js/components/database/data-browser/RightToolbar.jsx b/ui-admin/src/js/components/database/data-browser/RightToolbar.jsx index 44175aba4c3bcdfd38052de5d51c3ae263a8a132..58a1617e6368121c491dedbcca8501fbb7a8dd46 100644 --- a/ui-admin/src/js/components/database/data-browser/RightToolbar.jsx +++ b/ui-admin/src/js/components/database/data-browser/RightToolbar.jsx @@ -34,8 +34,8 @@ import UploadButton from '@src/js/components/database/data-browser/UploadButton. import FileIcon from '@material-ui/icons/InsertDriveFileOutlined' import FolderIcon from '@material-ui/icons/FolderOpen' import logger from '@src/js/common/logger.js' -import LoadingDialog from "@src/js/components/common/loading/LoadingDialog.jsx"; -import FileExistsDialog from "@src/js/components/common/dialog/FileExistsDialog.jsx"; +import LoadingDialog from '@src/js/components/common/loading/LoadingDialog.jsx' +import FileExistsDialog from '@src/js/components/common/dialog/FileExistsDialog.jsx' const color = 'default' const uploadButtonsColor = 'secondary'