From bc1cb820d1ff4c52ad739049388ad18a492bad67 Mon Sep 17 00:00:00 2001 From: Lenin Alevski Date: Thu, 24 Mar 2022 00:11:29 -0700 Subject: [PATCH] Multiple files upload refactor (#1755) - failed uploaded objects progress bar shows in red color - fixed bug in where failed uploaded objects cannot be removed from listed objects in ObjectManager - display delete button for failed upload objects - display setErrorSnackMessage component after done uploading all objects with number of failed objects - fixed race condition bug during multiple objects upload, now we are using Promise.allSettled to handle synchronization between uploads Signed-off-by: Lenin Alevski --- .../Objects/ListObjects/ListObjects.tsx | 240 +++++++++--------- .../Common/ObjectManager/ObjectHandled.tsx | 6 +- .../ProgressBarWrapper/ProgressBarWrapper.tsx | 10 +- .../screens/Console/ObjectBrowser/reducers.ts | 2 +- 4 files changed, 133 insertions(+), 125 deletions(-) diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx index 1c021892a..69947f51f 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx @@ -763,135 +763,135 @@ const ListObjects = ({ path: string, folderPath: string ) => { - if (files.length > 0) { - openList(); - let nextFile = files.pop(); - let uploadPromise = (file: File) => { - return new Promise((resolve, reject) => { - let uploadUrl = `api/v1/buckets/${bucketName}/objects/upload`; - const fileName = file.name; - const blobFile = new Blob([file], { type: file.type }); + let uploadPromise = (file: File) => { + return new Promise((resolve, reject) => { + let uploadUrl = `api/v1/buckets/${bucketName}/objects/upload`; + const fileName = file.name; + const blobFile = new Blob([file], { type: file.type }); - let encodedPath = ""; - const relativeFolderPath = - get(file, "webkitRelativePath", "") !== "" - ? get(file, "webkitRelativePath", "") - : folderPath; + let encodedPath = ""; + const relativeFolderPath = + get(file, "webkitRelativePath", "") !== "" + ? get(file, "webkitRelativePath", "") + : folderPath; - if (path !== "" || relativeFolderPath !== "") { - const finalFolderPath = relativeFolderPath - .split("/") - .slice(0, -1) - .join("/"); + if (path !== "" || relativeFolderPath !== "") { + const finalFolderPath = relativeFolderPath + .split("/") + .slice(0, -1) + .join("/"); - encodedPath = encodeFileName( - `${path}${finalFolderPath}${ - !finalFolderPath.endsWith("/") ? "/" : "" - }` - ); - } - - if (encodedPath !== "") { - uploadUrl = `${uploadUrl}?prefix=${encodedPath}`; - } - - const identity = encodeFileName( - `${bucketName}-${encodedPath}-${new Date().getTime()}-${Math.random()}` + encodedPath = encodeFileName( + `${path}${finalFolderPath}${ + !finalFolderPath.endsWith("/") ? "/" : "" + }` ); + } - setNewObject({ - bucketName, - done: false, - instanceID: identity, - percentage: 0, - prefix: `${decodeFileName(encodedPath)}${fileName}`, - type: "upload", - waitingForFile: false, - }); + if (encodedPath !== "") { + uploadUrl = `${uploadUrl}?prefix=${encodedPath}`; + } - let xhr = new XMLHttpRequest(); - xhr.open("POST", uploadUrl, true); + const identity = encodeFileName( + `${bucketName}-${encodedPath}-${new Date().getTime()}-${Math.random()}` + ); - const areMultipleFiles = files.length > 1; - const errorMessage = `An error occurred while uploading the file${ - areMultipleFiles ? "s" : "" - }.`; - const okMessage = `Object${ - areMultipleFiles ? "s" : `` - } uploaded successfully.`; - - xhr.withCredentials = false; - xhr.onload = function (event) { - if ( - xhr.status === 401 || - xhr.status === 403 || - xhr.status === 400 || - xhr.status === 500 - ) { - if (xhr.response) { - const err = JSON.parse(xhr.response); - setSnackBarMessage(err.detailedMessage); - } else { - setSnackBarMessage(errorMessage); - } - } - if (xhr.status === 413) { - setSnackBarMessage("Error - File size too large"); - } - if (xhr.status === 200) { - completeObject(identity); - if (files.length === 0) { - setSnackBarMessage(okMessage); - } - } - resolve(xhr.status); - if (files.length > 0) { - let nFile = files.pop(); - if (nFile) { - return uploadPromise(nFile); - } - } - }; - - xhr.upload.addEventListener("error", (event) => { - setSnackBarMessage(errorMessage); - }); - - xhr.upload.addEventListener("progress", (event) => { - const progress = Math.floor((event.loaded * 100) / event.total); - - updateProgress(identity, progress); - }); - - xhr.onerror = () => { - setSnackBarMessage(errorMessage); - reject(errorMessage); - }; - xhr.onloadend = () => { - if (files.length === 0) { - setLoading(true); - } - }; - - const formData = new FormData(); - if (file.size !== undefined) { - formData.append(file.size.toString(), blobFile, fileName); - - xhr.send(formData); - } + setNewObject({ + bucketName, + done: false, + instanceID: identity, + percentage: 0, + prefix: `${decodeFileName(encodedPath)}${fileName}`, + type: "upload", + waitingForFile: false, }); - }; - if (nextFile) { - uploadPromise(nextFile!) - .then(() => { - console.info("done uploading file"); - }) - .catch((err) => { - console.error("error uploading file,", err); - }); - } + let xhr = new XMLHttpRequest(); + xhr.open("POST", uploadUrl, true); + + const areMultipleFiles = files.length > 1; + let errorMessage = `An error occurred while uploading the file${ + areMultipleFiles ? "s" : "" + }.`; + + const errorMessages: any = { + 413: "Error - File size too large", + }; + + xhr.withCredentials = false; + xhr.onload = function (event) { + // resolve promise only when HTTP code is ok + if (xhr.status >= 200 && xhr.status < 300) { + completeObject(identity); + resolve({ status: xhr.status }); + } else { + // reject promise if there was a server error + if (errorMessages[xhr.status]) { + errorMessage = errorMessages[xhr.status]; + } else if (xhr.response) { + try { + const err = JSON.parse(xhr.response); + errorMessage = err.detailedMessage; + } catch (e) { + errorMessage = "something went wrong"; + } + } + reject({ status: xhr.status, message: errorMessage }); + } + }; + + xhr.upload.addEventListener("error", (event) => { + reject(errorMessage); + return; + }); + + xhr.upload.addEventListener("progress", (event) => { + const progress = Math.floor((event.loaded * 100) / event.total); + + updateProgress(identity, progress); + }); + + xhr.onerror = () => { + reject(errorMessage); + return; + }; + xhr.onloadend = () => { + if (files.length === 0) { + setLoading(true); + } + }; + + const formData = new FormData(); + if (file.size !== undefined) { + formData.append(file.size.toString(), blobFile, fileName); + xhr.send(formData); + } + }); + }; + + const uploadFilePromises: any = []; + // open object manager + openList(); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + uploadFilePromises.push(uploadPromise(file)); } + Promise.allSettled(uploadFilePromises).then((results: Array) => { + const errors = results.filter( + (result) => result.status === "rejected" + ); + if (errors.length > 0) { + const totalFiles = uploadFilePromises.length; + const successUploadedFiles = + uploadFilePromises.length - errors.length; + const err: ErrorResponseHandler = { + errorMessage: "There were some errors during file upload", + detailedError: `Uploaded files ${successUploadedFiles}/${totalFiles}`, + }; + console.log("upload results", results); + setErrorSnackMessage(err); + } + }); }; upload(files, bucketName, pathPrefix, folderPath); @@ -902,7 +902,7 @@ const ListObjects = ({ internalPaths, openList, setNewObject, - setSnackBarMessage, + setErrorSnackMessage, updateProgress, ] ); diff --git a/portal-ui/src/screens/Console/Common/ObjectManager/ObjectHandled.tsx b/portal-ui/src/screens/Console/Common/ObjectManager/ObjectHandled.tsx index 787b4da0d..7930c1b0b 100644 --- a/portal-ui/src/screens/Console/Common/ObjectManager/ObjectHandled.tsx +++ b/portal-ui/src/screens/Console/Common/ObjectManager/ObjectHandled.tsx @@ -120,7 +120,7 @@ const ObjectHandled = ({
@@ -129,9 +129,9 @@ const ObjectHandled = ({ deleteFromList(objectToDisplay.instanceID); }} className={`${classes.closeButton} hideOnProgress showOnHover`} - disabled={!objectToDisplay.done} + disabled={objectToDisplay.percentage !== 100} > - +
diff --git a/portal-ui/src/screens/Console/Common/ProgressBarWrapper/ProgressBarWrapper.tsx b/portal-ui/src/screens/Console/Common/ProgressBarWrapper/ProgressBarWrapper.tsx index a9622a06c..e5bd6a851 100644 --- a/portal-ui/src/screens/Console/Common/ProgressBarWrapper/ProgressBarWrapper.tsx +++ b/portal-ui/src/screens/Console/Common/ProgressBarWrapper/ProgressBarWrapper.tsx @@ -68,10 +68,18 @@ const ProgressBarWrapper = ({ withLabel, size = "regular", }: IProgressBarWrapper) => { + let color: any; + if (value === 100 && ready) { + color = "success"; + } else if (value === 100 && !ready) { + color = "error"; + } else { + color = "primary"; + } const propsComponent: LinearProgressProps = { variant: indeterminate && !ready ? "indeterminate" : "determinate", value: ready ? 100 : value, - color: ready ? "success" : "primary", + color: color, }; if (withLabel) { return ; diff --git a/portal-ui/src/screens/Console/ObjectBrowser/reducers.ts b/portal-ui/src/screens/Console/ObjectBrowser/reducers.ts index 6730aa213..1dbb88985 100644 --- a/portal-ui/src/screens/Console/ObjectBrowser/reducers.ts +++ b/portal-ui/src/screens/Console/ObjectBrowser/reducers.ts @@ -202,7 +202,7 @@ export function objectBrowserReducer( }; case OBJECT_MANAGER_CLEAN_LIST: const nonCompletedList = state.objectManager.objectsToManage.filter( - (item) => !item.done + (item) => item.percentage !== 100 ); return {