From 6069991405d445d21005f4d0a94a59300f9d3048 Mon Sep 17 00:00:00 2001 From: Alex <33497058+bexsoft@users.noreply.github.com> Date: Fri, 29 Apr 2022 22:54:12 -0500 Subject: [PATCH] Improvements for download / upload manager (#1933) - Changed styles on progress bars & items - Fixed some issues in error state & handling - Added cancel capability to objects - Added visual indicators when new objects are added to pool Signed-off-by: Benjamin Perez --- portal-ui/src/icons/CancelledIcon.tsx | 32 ++++ portal-ui/src/icons/DisabledIcon.tsx | 37 +---- portal-ui/src/icons/DownloadStatIcon.tsx | 16 +- portal-ui/src/icons/EnabledIcon.tsx | 33 +--- portal-ui/src/icons/ObjectManagerIcon.tsx | 31 ++-- portal-ui/src/icons/RemoveAllIcon.tsx | 37 +++++ portal-ui/src/icons/UploadStatIcon.tsx | 18 +- portal-ui/src/icons/index.ts | 2 + .../Objects/ListObjects/ListObjects.tsx | 73 ++++++--- .../Objects/ListObjects/ObjectDetailPanel.tsx | 41 +++-- .../ObjectDetails/VersionsNavigator.tsx | 41 +++-- .../Buckets/ListBuckets/Objects/utils.ts | 18 +- .../screens/Console/Common/IconsScreen.tsx | 12 ++ .../Common/ObjectManager/ObjectHandled.tsx | 154 +++++++++++++----- .../Common/ObjectManager/ObjectManager.tsx | 28 ++-- .../Console/Common/PageHeader/PageHeader.tsx | 70 +++++++- .../ProgressBarWrapper/ProgressBarWrapper.tsx | 60 +++++-- .../screens/Console/ObjectBrowser/actions.ts | 34 +++- .../screens/Console/ObjectBrowser/reducers.ts | 53 ++++++ .../screens/Console/ObjectBrowser/types.ts | 21 ++- 20 files changed, 585 insertions(+), 226 deletions(-) create mode 100644 portal-ui/src/icons/CancelledIcon.tsx create mode 100644 portal-ui/src/icons/RemoveAllIcon.tsx diff --git a/portal-ui/src/icons/CancelledIcon.tsx b/portal-ui/src/icons/CancelledIcon.tsx new file mode 100644 index 000000000..cca7502da --- /dev/null +++ b/portal-ui/src/icons/CancelledIcon.tsx @@ -0,0 +1,32 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as React from "react"; +import { SVGProps } from "react"; + +const CancelledIcon = (props: SVGProps) => ( + + + +); + +export default CancelledIcon; diff --git a/portal-ui/src/icons/DisabledIcon.tsx b/portal-ui/src/icons/DisabledIcon.tsx index 6c7cb65b8..02e173547 100644 --- a/portal-ui/src/icons/DisabledIcon.tsx +++ b/portal-ui/src/icons/DisabledIcon.tsx @@ -25,37 +25,12 @@ const DisabledIcon = (props: SVGProps) => { viewBox="0 0 16 16" {...props} > - - - - - - - - - - + + ); diff --git a/portal-ui/src/icons/DownloadStatIcon.tsx b/portal-ui/src/icons/DownloadStatIcon.tsx index 9b9c40f25..7a8bdcca4 100644 --- a/portal-ui/src/icons/DownloadStatIcon.tsx +++ b/portal-ui/src/icons/DownloadStatIcon.tsx @@ -25,21 +25,7 @@ const DownloadStatIcon = (props: SVGProps) => ( viewBox="0 0 256 256" {...props} > - - - - - - - - - - - - + ); diff --git a/portal-ui/src/icons/EnabledIcon.tsx b/portal-ui/src/icons/EnabledIcon.tsx index 6ea3d2d6c..4fb0565e0 100644 --- a/portal-ui/src/icons/EnabledIcon.tsx +++ b/portal-ui/src/icons/EnabledIcon.tsx @@ -25,37 +25,8 @@ const EnabledIcon = (props: SVGProps) => { viewBox="0 0 16 16" {...props} > - - - - - - - - - - + + ); diff --git a/portal-ui/src/icons/ObjectManagerIcon.tsx b/portal-ui/src/icons/ObjectManagerIcon.tsx index 0f81ba6b4..eea491a7c 100644 --- a/portal-ui/src/icons/ObjectManagerIcon.tsx +++ b/portal-ui/src/icons/ObjectManagerIcon.tsx @@ -26,19 +26,24 @@ const ObjectManagerIcon = (props: SVGProps) => { xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" > - - - - + + + + + + ); diff --git a/portal-ui/src/icons/RemoveAllIcon.tsx b/portal-ui/src/icons/RemoveAllIcon.tsx new file mode 100644 index 000000000..cac904865 --- /dev/null +++ b/portal-ui/src/icons/RemoveAllIcon.tsx @@ -0,0 +1,37 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as React from "react"; +import { SVGProps } from "react"; + +const ObjectManagerIcon = (props: SVGProps) => { + return ( + + + + + + + ); +}; + +export default ObjectManagerIcon; diff --git a/portal-ui/src/icons/UploadStatIcon.tsx b/portal-ui/src/icons/UploadStatIcon.tsx index 4f1f39609..d3f7acb5f 100644 --- a/portal-ui/src/icons/UploadStatIcon.tsx +++ b/portal-ui/src/icons/UploadStatIcon.tsx @@ -25,21 +25,9 @@ const UploadStatIcon = (props: SVGProps) => ( viewBox="0 0 256 256" {...props} > - - - - - - - - - - - - + ); diff --git a/portal-ui/src/icons/index.ts b/portal-ui/src/icons/index.ts index 485f6d58a..751d43407 100644 --- a/portal-ui/src/icons/index.ts +++ b/portal-ui/src/icons/index.ts @@ -186,3 +186,5 @@ export { default as EditTenantIcon } from "./EditTenantIcon"; export { default as SuccessIcon } from "./SuccessIcon"; export { default as NetworkGetIcon } from "./NetworkGetIcon"; export { default as NetworkPutIcon } from "./NetworkPutIcon"; +export { default as RemoveAllIcon } from "./RemoveAllIcon"; +export { default as CancelledIcon } from "./CancelledIcon"; 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 0174aa0e0..7459c30b7 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 @@ -52,7 +52,9 @@ import { import { Badge, Typography } from "@mui/material"; import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs"; import { + cancelObjectInList, completeObject, + failObject, openList, resetRewind, setLoadingObjectInfo, @@ -241,6 +243,8 @@ interface IListObjectsProps { setSelectedObjectView: typeof setSelectedObjectView; setLoadingObjectInfo: typeof setLoadingObjectInfo; setLoadingObjectsList: typeof setLoadingObjectsList; + failObject: typeof failObject; + cancelObjectInList: typeof cancelObjectInList; } function useInterval(callback: any, delay: number) { @@ -300,6 +304,8 @@ const ListObjects = ({ setSelectedObjectView, setLoadingObjectInfo, setLoadingObjectsList, + failObject, + cancelObjectInList, }: IListObjectsProps) => { const [records, setRecords] = useState([]); const [deleteMultipleOpen, setDeleteMultipleOpen] = useState(false); @@ -738,17 +744,7 @@ const ListObjects = ({ `${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}` ); - setNewObject({ - bucketName, - done: false, - instanceID: identityDownload, - percentage: 0, - prefix: object.name, - type: "download", - waitingForFile: true, - }); - - download( + const downloadCall = download( bucketName, encodeFileName(object.name), object.version_id, @@ -758,8 +754,29 @@ const ListObjects = ({ }, () => { completeObject(identityDownload); + }, + () => { + failObject(identityDownload); + }, + () => { + cancelObjectInList(identityDownload); } ); + + setNewObject({ + bucketName, + done: false, + instanceID: identityDownload, + percentage: 0, + prefix: object.name, + type: "download", + waitingForFile: true, + failed: false, + cancelled: false, + call: downloadCall, + }); + + downloadCall.send(); }; const openPath = (idElement: string) => { @@ -824,16 +841,6 @@ const ListObjects = ({ `${bucketName}-${encodedPath}-${new Date().getTime()}-${Math.random()}` ); - setNewObject({ - bucketName, - done: false, - instanceID: identity, - percentage: 0, - prefix: `${decodeFileName(encodedPath)}${fileName}`, - type: "upload", - waitingForFile: false, - }); - let xhr = new XMLHttpRequest(); xhr.open("POST", uploadUrl, true); @@ -864,12 +871,14 @@ const ListObjects = ({ errorMessage = "something went wrong"; } } + failObject(identity); reject({ status: xhr.status, message: errorMessage }); } }; xhr.upload.addEventListener("error", (event) => { reject(errorMessage); + failObject(identity); return; }); @@ -881,6 +890,7 @@ const ListObjects = ({ xhr.onerror = () => { reject(errorMessage); + failObject(identity); return; }; xhr.onloadend = () => { @@ -888,10 +898,27 @@ const ListObjects = ({ setLoadingObjectsList(true); } }; + xhr.onabort = () => { + cancelObjectInList(identity); + }; const formData = new FormData(); if (file.size !== undefined) { formData.append(file.size.toString(), blobFile, fileName); + + setNewObject({ + bucketName, + done: false, + instanceID: identity, + percentage: 0, + prefix: `${decodeFileName(encodedPath)}${fileName}`, + type: "upload", + waitingForFile: false, + failed: false, + cancelled: false, + call: xhr, + }); + xhr.send(formData); } }); @@ -934,6 +961,8 @@ const ListObjects = ({ setErrorSnackMessage, updateProgress, setLoadingObjectsList, + cancelObjectInList, + failObject, ] ); @@ -1485,6 +1514,7 @@ const mapDispatchToProps = { updateProgress, completeObject, openList, + failObject, setSearchObjects, setVersionsModeEnabled, setShowDeletedObjects, @@ -1493,6 +1523,7 @@ const mapDispatchToProps = { setSelectedObjectView, setLoadingObjectInfo, setLoadingObjectsList, + cancelObjectInList, }; const connector = connect(mapStateToProps, mapDispatchToProps); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx index c1337ec26..3b79e81f3 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx @@ -44,7 +44,9 @@ import { } from "../../../../../../common/utils"; import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions"; import { + cancelObjectInList, completeObject, + failObject, setLoadingObjectInfo, setLoadingVersions, setNewObject, @@ -137,6 +139,8 @@ interface IObjectDetailPanelProps { setLoadingObjectInfo: typeof setLoadingObjectInfo; setLoadingVersions: typeof setLoadingVersions; setSelectedVersion: typeof setSelectedVersion; + failObject: typeof failObject; + cancelObjectInList: typeof cancelObjectInList; } const emptyFile: IFileInfo = { @@ -170,6 +174,8 @@ const ObjectDetailPanel = ({ setLoadingObjectInfo, setLoadingVersions, setSelectedVersion, + failObject, + cancelObjectInList, }: IObjectDetailPanelProps) => { const [shareFileModalOpen, setShareFileModalOpen] = useState(false); const [retentionModalOpen, setRetentionModalOpen] = useState(false); @@ -294,17 +300,7 @@ const ObjectDetailPanel = ({ `${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}` ); - setNewObject({ - bucketName, - done: false, - instanceID: identityDownload, - percentage: 0, - prefix: object.name, - type: "download", - waitingForFile: true, - }); - - download( + const downloadCall = download( bucketName, internalPaths, object.version_id, @@ -314,8 +310,29 @@ const ObjectDetailPanel = ({ }, () => { completeObject(identityDownload); + }, + () => { + failObject(identityDownload); + }, + () => { + cancelObjectInList(identityDownload); } ); + + setNewObject({ + bucketName, + done: false, + instanceID: identityDownload, + percentage: 0, + prefix: object.name, + type: "download", + waitingForFile: true, + failed: false, + cancelled: false, + call: downloadCall, + }); + + downloadCall.send(); }; const closeDeleteModal = (closeAndReload: boolean) => { @@ -731,6 +748,8 @@ const mapDispatchToProps = { setLoadingObjectInfo, setLoadingVersions, setSelectedVersion, + failObject, + cancelObjectInList, }; const connector = connect(mapStateToProps, mapDispatchToProps); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/VersionsNavigator.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/VersionsNavigator.tsx index 1710aa22b..f626fcb43 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/VersionsNavigator.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/VersionsNavigator.tsx @@ -50,7 +50,9 @@ import { import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle"; import RestoreFileVersion from "./RestoreFileVersion"; import { + cancelObjectInList, completeObject, + failObject, setLoadingObjectInfo, setLoadingVersions, setNewObject, @@ -128,6 +130,8 @@ interface IVersionsNavigatorProps { setSelectedVersion: typeof setSelectedVersion; setLoadingVersions: typeof setLoadingVersions; setLoadingObjectInfo: typeof setLoadingObjectInfo; + failObject: typeof failObject; + cancelObjectInList: typeof cancelObjectInList; } const emptyFile: IFileInfo = { @@ -157,6 +161,8 @@ const VersionsNavigator = ({ setSelectedVersion, setLoadingVersions, setLoadingObjectInfo, + failObject, + cancelObjectInList, }: IVersionsNavigatorProps) => { const [shareFileModalOpen, setShareFileModalOpen] = useState(false); const [actualInfo, setActualInfo] = useState(null); @@ -227,17 +233,7 @@ const VersionsNavigator = ({ `${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}` ); - setNewObject({ - bucketName, - done: false, - instanceID: identityDownload, - percentage: 0, - prefix: object.name, - type: "download", - waitingForFile: true, - }); - - download( + const downloadCall = download( bucketName, internalPaths, object.version_id, @@ -247,8 +243,29 @@ const VersionsNavigator = ({ }, () => { completeObject(identityDownload); + }, + () => { + failObject(identityDownload); + }, + () => { + cancelObjectInList(identityDownload); } ); + + setNewObject({ + bucketName, + done: false, + instanceID: identityDownload, + percentage: 0, + prefix: object.name, + type: "download", + waitingForFile: true, + failed: false, + cancelled: false, + call: downloadCall, + }); + + downloadCall.send(); }; const onShareItem = (item: IFileInfo) => { @@ -517,6 +534,8 @@ const mapDispatchToProps = { setSelectedVersion, setLoadingVersions, setLoadingObjectInfo, + failObject, + cancelObjectInList, }; const connector = connect(mapStateToProps, mapDispatchToProps); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/utils.ts b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/utils.ts index c548b6c6b..fb42d1cde 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/utils.ts +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/utils.ts @@ -22,7 +22,9 @@ export const download = ( versionID: any, fileSize: number, progressCallback: (progress: number) => void, - completeCallback: () => void + completeCallback: () => void, + errorCallback: () => void, + abortCallback: () => void, ) => { const anchor = document.createElement("a"); document.body.appendChild(anchor); @@ -68,7 +70,19 @@ export const download = ( document.body.removeChild(link); } }; - req.send(); + req.onerror = () => { + if(errorCallback) { + errorCallback(); + } + }; + req.onabort = () => { + if(abortCallback) { + abortCallback(); + } + }; + //req.send(); + + return req; }; // Review file extension by name & returns the type of preview browser that can be used diff --git a/portal-ui/src/screens/Console/Common/IconsScreen.tsx b/portal-ui/src/screens/Console/Common/IconsScreen.tsx index 9504ef41e..82826b4c2 100644 --- a/portal-ui/src/screens/Console/Common/IconsScreen.tsx +++ b/portal-ui/src/screens/Console/Common/IconsScreen.tsx @@ -235,6 +235,12 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => { CallHomeFeatureIcon + + +
+ CancelledIcon +
+
@@ -847,6 +853,12 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => { RefreshIcon
+ + +
+ RemoveAllIcon +
+
diff --git a/portal-ui/src/screens/Console/Common/ObjectManager/ObjectHandled.tsx b/portal-ui/src/screens/Console/Common/ObjectManager/ObjectHandled.tsx index 79e1a794c..30191313d 100644 --- a/portal-ui/src/screens/Console/Common/ObjectManager/ObjectHandled.tsx +++ b/portal-ui/src/screens/Console/Common/ObjectManager/ObjectHandled.tsx @@ -21,7 +21,13 @@ import createStyles from "@mui/styles/createStyles"; import withStyles from "@mui/styles/withStyles"; import { IFileItem } from "../../ObjectBrowser/types"; import ProgressBarWrapper from "../ProgressBarWrapper/ProgressBarWrapper"; -import { DownloadStatIcon, UploadStatIcon } from "../../../../icons"; +import { + DisabledIcon, + DownloadStatIcon, + EnabledIcon, + UploadStatIcon, + CancelledIcon, +} from "../../../../icons"; import clsx from "clsx"; interface IObjectHandled { @@ -35,15 +41,15 @@ const styles = (theme: Theme) => container: { borderBottom: "#E2E2E2 1px solid", padding: "15px 5px", - margin: "0 15px", + margin: "0 30px", position: "relative", "& .showOnHover": { - opacity: 0, + opacity: 1, transitionDuration: "0.2s", }, "&.inProgress": { "& .hideOnProgress": { - visibility: "hidden", + //visibility: "hidden", }, }, "&:hover": { @@ -53,13 +59,19 @@ const styles = (theme: Theme) => }, }, headItem: { - color: "#868686", - fontSize: 12, + color: "#000", + fontSize: 14, + fontWeight: "bold", width: "100%", whiteSpace: "nowrap", textOverflow: "ellipsis", overflow: "hidden", }, + downloadHeader: { + display: "flex", + alignItems: "center", + width: "100%", + }, progressContainer: { marginTop: 5, }, @@ -71,42 +83,65 @@ const styles = (theme: Theme) => paddingTop: 5, marginRight: 5, "& svg": { - width: 20, - height: 20, + width: 16, + height: 16, }, }, - download: { - color: "rgb(113,200,150)", + completedSuccess: { + color: "#4CCB92", }, - upload: { - color: "rgb(66,127,172)", + inProgress: { + color: "#2781B0", + }, + completedError: { + color: "#C83B51", + }, + cancelledAction: { + color: "#FFBD62", }, closeIcon: { + backgroundColor: "#E9EDEE", + display: "block", + width: 18, + height: 18, + borderRadius: "100%", + "&:hover": { + backgroundColor: "#cecbcb", + }, "&::before": { width: 1, - height: 12, + height: 9, + top: "50%", content: "' '", position: "absolute", - transform: "rotate(45deg)", - borderLeft: "#9c9c9c 2px solid", + transform: "translate(-50%, -50%) rotate(45deg)", + borderLeft: "#000 2px solid", }, "&::after": { width: 1, - height: 12, + height: 9, + top: "50%", content: "' '", position: "absolute", - transform: "rotate(-45deg)", - borderLeft: "#9c9c9c 2px solid", + transform: "translate(-50%, -50%) rotate(-45deg)", + borderLeft: "#000 2px solid", }, }, closeButton: { backgroundColor: "transparent", border: 0, right: 0, + top: 5, + marginTop: 15, position: "absolute", }, fileName: { - width: 230, + width: 295, + }, + bucketName: { + fontSize: 12, + color: "#696969", + fontWeight: "normal", }, }); @@ -126,35 +161,74 @@ const ObjectHandled = ({
-
- {objectToDisplay.type === "download" ? ( - - ) : ( - - )} -
-
+ +
+ + {objectToDisplay.cancelled ? ( + + ) : ( + + {objectToDisplay.failed ? ( + + ) : ( + + {objectToDisplay.done ? ( + + ) : ( + + {objectToDisplay.type === "download" ? ( + + ) : ( + + )} + + )} + + )} + + )} + + + {prefix} + +
+
+ Bucket: {objectToDisplay.bucketName} -
- -
{prefix}
-
+
@@ -164,6 +238,8 @@ const ObjectHandled = ({ )} diff --git a/portal-ui/src/screens/Console/Common/ObjectManager/ObjectManager.tsx b/portal-ui/src/screens/Console/Common/ObjectManager/ObjectManager.tsx index 9f3558d1d..83a4b2963 100644 --- a/portal-ui/src/screens/Console/Common/ObjectManager/ObjectManager.tsx +++ b/portal-ui/src/screens/Console/Common/ObjectManager/ObjectManager.tsx @@ -23,7 +23,7 @@ import { Tooltip, IconButton } from "@mui/material"; import { AppState } from "../../../../store"; import { IFileItem } from "../../ObjectBrowser/types"; import { deleteFromList, cleanList } from "../../ObjectBrowser/actions"; -import { TrashIcon } from "../../../../icons"; +import { RemoveAllIcon } from "../../../../icons"; import ObjectHandled from "./ObjectHandled"; interface IObjectManager { @@ -38,12 +38,12 @@ const styles = (theme: Theme) => createStyles({ downloadContainer: { border: "#EAEDEE 1px solid", - boxShadow: "rgba(0, 0, 0, 0.08) 0 3px 10px", + boxShadow: "rgba(0, 0, 0, 0.08) 0 2px 10px", backgroundColor: "#fff", position: "absolute", - right: 0, - top: 80, - width: 300, + right: 20, + top: 60, + width: 400, overflowY: "hidden", overflowX: "hidden", borderRadius: 3, @@ -58,13 +58,13 @@ const styles = (theme: Theme) => }, }, title: { - fontSize: 14, + fontSize: 16, fontWeight: "bold", - textAlign: "center", - marginBottom: 5, - paddingBottom: 12, + textAlign: "left", + paddingBottom: 20, borderBottom: "#E2E2E2 1px solid", - margin: "15px 15px 5px 15px", + margin: "25px 30px 5px 30px", + color: "#000", }, actionsContainer: { overflowY: "auto", @@ -77,12 +77,12 @@ const styles = (theme: Theme) => }, cleanIcon: { position: "absolute", - right: 14, - top: 12, + right: 28, + top: 25, }, cleanButton: { "& svg": { - width: 20, + width: 25, }, }, }); @@ -110,7 +110,7 @@ const ObjectManager = ({ onClick={cleanList} className={classes.cleanButton} > - +
diff --git a/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx b/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx index 302e394d5..a5dd8f8f9 100644 --- a/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx +++ b/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { Fragment } from "react"; +import React, { Fragment, useEffect, useState } from "react"; import { Theme } from "@mui/material/styles"; import { connect } from "react-redux"; import Grid from "@mui/material/Grid"; @@ -26,7 +26,7 @@ import OperatorLogo from "../../../../icons/OperatorLogo"; import ConsoleLogo from "../../../../icons/ConsoleLogo"; import { IFileItem } from "../../ObjectBrowser/types"; import { toggleList } from "../../ObjectBrowser/actions"; -import { ObjectManagerIcon } from "../../../../icons"; +import { CircleIcon, ObjectManagerIcon } from "../../../../icons"; import { Box } from "@mui/material"; interface IPageHeader { @@ -35,10 +35,12 @@ interface IPageHeader { operatorMode?: boolean; label: any; actions?: any; - managerObjects?: IFileItem[]; + managerObjects: IFileItem[]; toggleList: typeof toggleList; middleComponent?: React.ReactNode; features: string[]; + managerOpen: boolean; + newItems: boolean; } const styles = (theme: Theme) => @@ -71,6 +73,31 @@ const styles = (theme: Theme) => justifyContent: "center", alignItems: "center", }, + indicator: { + position: "absolute", + display: "block", + width: 15, + height: 15, + top: 0, + right: 2, + marginTop: -16, + transitionDuration: "0.2s", + color: "#32C787", + "& svg": { + width: 10, + height: 10, + top: "50%", + left: "50%", + transitionDuration: "0.2s", + }, + "&.newItem": { + color: "#2781B0", + "& svg": { + width: 15, + height: 15, + }, + }, + }, }); const PageHeader = ({ @@ -83,7 +110,20 @@ const PageHeader = ({ toggleList, middleComponent, features, + managerOpen, + newItems, }: IPageHeader) => { + const [newObject, setNewObject] = useState(false); + + useEffect(() => { + if (managerObjects.length > 0 && !managerOpen) { + setNewObject(true); + setTimeout(() => { + setNewObject(false); + }, 300); + } + }, [managerObjects.length, managerOpen]); + if (features.includes("hide-menu")) { return ; } @@ -151,7 +191,29 @@ const PageHeader = ({ }} id="object-manager-toggle" size="large" + sx={{ + marginRight: "20px", + color: "#5E5E5E", + position: "relative", + border: "#E2E2E2 1px solid", + borderRadius: "3px", + width: "40px", + height: "40px", + backgroundColor: "#F8F8F8", + padding: 0, + "&>svg": { + width: "25px", + }, + }} > +
0 && newItems ? 1 : 0, + }} + > + +
)} @@ -165,6 +227,8 @@ const mapState = (state: AppState) => ({ operatorMode: state.system.operatorMode, managerObjects: state.objectBrowser.objectManager.objectsToManage, features: state.console.session.features, + managerOpen: state.objectBrowser.objectManager.managerOpen, + newItems: state.objectBrowser.objectManager.newItems, }); const mapDispatchToProps = { diff --git a/portal-ui/src/screens/Console/Common/ProgressBarWrapper/ProgressBarWrapper.tsx b/portal-ui/src/screens/Console/Common/ProgressBarWrapper/ProgressBarWrapper.tsx index e5bd6a851..12ea59c8f 100644 --- a/portal-ui/src/screens/Console/Common/ProgressBarWrapper/ProgressBarWrapper.tsx +++ b/portal-ui/src/screens/Console/Common/ProgressBarWrapper/ProgressBarWrapper.tsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React from "react"; +import React, { Fragment } from "react"; import { styled } from "@mui/material/styles"; import LinearProgress, { linearProgressClasses, @@ -28,6 +28,8 @@ interface IProgressBarWrapper { indeterminate?: boolean; withLabel?: boolean; size?: string; + error?: boolean; + cancelled?: boolean; } const BorderLinearProgress = styled(LinearProgress)(() => ({ @@ -48,14 +50,42 @@ const SmallBorderLinearProgress = styled(BorderLinearProgress)(() => ({ }, })); -function LinearProgressWithLabel(props: LinearProgressProps) { +function LinearProgressWithLabel( + props: { error: boolean; cancelled: boolean } & LinearProgressProps +) { + let color = "#000"; + let size = 18; + + if(props.error) { + color = "#C83B51" + size = 14 + } + + else if(props.cancelled) { + color = "#FFBD62" + size = 14 + } + return ( - + - - {`${Math.round(props.value || 0)}%`} + + {props.cancelled ? ( + "Cancelled" + ) : ( + + {props.error ? "Failed" : `${Math.round(props.value || 0)}%`} + + )} ); @@ -67,22 +97,32 @@ const ProgressBarWrapper = ({ indeterminate, withLabel, size = "regular", + error, + cancelled, }: IProgressBarWrapper) => { let color: any; - if (value === 100 && ready) { - color = "success"; - } else if (value === 100 && !ready) { + if (error) { color = "error"; + } else if (cancelled) { + color = "warning"; + }else if (value === 100 && ready) { + color = "success"; } else { color = "primary"; } const propsComponent: LinearProgressProps = { - variant: indeterminate && !ready ? "indeterminate" : "determinate", + variant: indeterminate && !ready && !cancelled ? "indeterminate" : "determinate", value: ready ? 100 : value, color: color, }; if (withLabel) { - return ; + return ( + + ); } if (size === "small") { return ; diff --git a/portal-ui/src/screens/Console/ObjectBrowser/actions.ts b/portal-ui/src/screens/Console/ObjectBrowser/actions.ts index 2c4e90fdd..0a91e6ae1 100644 --- a/portal-ui/src/screens/Console/ObjectBrowser/actions.ts +++ b/portal-ui/src/screens/Console/ObjectBrowser/actions.ts @@ -31,11 +31,13 @@ import { OBJECT_MANAGER_SET_SEARCH_OBJECT, OBJECT_MANAGER_TOGGLE_LIST, OBJECT_MANAGER_UPDATE_PROGRESS_OBJECT, + OBJECT_MANAGER_SET_LOADING, + OBJECT_MANAGER_ERROR_IN_OBJECT, + OBJECT_MANAGER_CANCEL_OBJECT, REWIND_RESET_REWIND, REWIND_SET_ENABLE, - IFileItem, BUCKET_BROWSER_SET_SELECTED_OBJECT, - OBJECT_MANAGER_SET_LOADING, + IFileItem, } from "./types"; export const setRewindEnable = ( @@ -90,6 +92,13 @@ export const completeObject = (instanceID: string) => { }; }; +export const failObject = (instanceID: string) => { + return { + type: OBJECT_MANAGER_ERROR_IN_OBJECT, + instanceID, + }; +}; + export const deleteFromList = (instanceID: string) => { return { type: OBJECT_MANAGER_DELETE_FROM_OBJECT_LIST, @@ -128,6 +137,20 @@ export const setSearchObjects = (searchString: string) => { }; }; +export const setLoadingObjectsList = (status: boolean) => { + return { + type: OBJECT_MANAGER_SET_LOADING, + status, + }; +}; + +export const cancelObjectInList = (instanceID: string) => { + return { + type: OBJECT_MANAGER_CANCEL_OBJECT, + instanceID, + }; +}; + export const setSearchVersions = (searchString: string) => { return { type: BUCKET_BROWSER_VERSIONS_SET_SEARCH, @@ -176,10 +199,3 @@ export const setSelectedObjectView = (object: string | null) => { object, }; }; - -export const setLoadingObjectsList = (status: boolean) => { - return { - type: OBJECT_MANAGER_SET_LOADING, - status, - }; -}; diff --git a/portal-ui/src/screens/Console/ObjectBrowser/reducers.ts b/portal-ui/src/screens/Console/ObjectBrowser/reducers.ts index 333c5ab6c..e38dcf863 100644 --- a/portal-ui/src/screens/Console/ObjectBrowser/reducers.ts +++ b/portal-ui/src/screens/Console/ObjectBrowser/reducers.ts @@ -37,6 +37,8 @@ import { ObjectBrowserActionTypes, BUCKET_BROWSER_SET_SELECTED_OBJECT, OBJECT_MANAGER_SET_LOADING, + OBJECT_MANAGER_ERROR_IN_OBJECT, + OBJECT_MANAGER_CANCEL_OBJECT, } from "./types"; const defaultRewind = { @@ -57,6 +59,7 @@ const initialState: ObjectBrowserState = { objectManager: { objectsToManage: [], managerOpen: false, + newItems: false, }, searchObjects: "", versionedFile: "", @@ -106,6 +109,7 @@ export function objectBrowserReducer( objectManager: { objectsToManage: cloneObjects, managerOpen: state.objectManager.managerOpen, + newItems: true, }, }; case OBJECT_MANAGER_UPDATE_PROGRESS_OBJECT: @@ -127,6 +131,7 @@ export function objectBrowserReducer( objectManager: { objectsToManage: copyManager, managerOpen: state.objectManager.managerOpen, + newItems: state.objectManager.newItems, }, }; case OBJECT_MANAGER_COMPLETE_OBJECT: @@ -149,6 +154,51 @@ export function objectBrowserReducer( objectManager: { objectsToManage: copyObject, managerOpen: state.objectManager.managerOpen, + newItems: state.objectManager.newItems, + }, + }; + case OBJECT_MANAGER_ERROR_IN_OBJECT: + const objectItems = [...state.objectManager.objectsToManage]; + + const objectToFail = state.objectManager.objectsToManage.findIndex( + (item) => item.instanceID === action.instanceID + ); + + if (objectToFail === -1) { + return { ...state }; + } + + objectItems[objectToFail].failed = true; + + return { + ...state, + objectManager: { + objectsToManage: objectItems, + managerOpen: state.objectManager.managerOpen, + newItems: state.objectManager.newItems, + }, + }; + case OBJECT_MANAGER_CANCEL_OBJECT: + const objectsListFind = [...state.objectManager.objectsToManage]; + + const objectToCancel = state.objectManager.objectsToManage.findIndex( + (item) => item.instanceID === action.instanceID + ); + + if (objectToCancel === -1) { + return { ...state }; + } + + objectsListFind[objectToCancel].cancelled = true; + objectsListFind[objectToCancel].done = true; + objectsListFind[objectToCancel].percentage = 0; + + return { + ...state, + objectManager: { + objectsToManage: objectsListFind, + managerOpen: state.objectManager.managerOpen, + newItems: state.objectManager.newItems, }, }; case OBJECT_MANAGER_DELETE_FROM_OBJECT_LIST: @@ -162,6 +212,7 @@ export function objectBrowserReducer( objectsToManage: notObject, managerOpen: notObject.length === 0 ? false : state.objectManager.managerOpen, + newItems: state.objectManager.newItems, }, }; case OBJECT_MANAGER_CLEAN_LIST: @@ -177,6 +228,7 @@ export function objectBrowserReducer( nonCompletedList.length === 0 ? false : state.objectManager.managerOpen, + newItems: false, }, }; case OBJECT_MANAGER_TOGGLE_LIST: @@ -185,6 +237,7 @@ export function objectBrowserReducer( objectManager: { ...state.objectManager, managerOpen: !state.objectManager.managerOpen, + newItems: false, }, }; case OBJECT_MANAGER_OPEN_LIST: diff --git a/portal-ui/src/screens/Console/ObjectBrowser/types.ts b/portal-ui/src/screens/Console/ObjectBrowser/types.ts index 022ec63f8..e410cb932 100644 --- a/portal-ui/src/screens/Console/ObjectBrowser/types.ts +++ b/portal-ui/src/screens/Console/ObjectBrowser/types.ts @@ -22,6 +22,7 @@ export const OBJECT_MANAGER_NEW_OBJECT = "OBJECT_MANAGER/NEW_OBJECT"; export const OBJECT_MANAGER_UPDATE_PROGRESS_OBJECT = "OBJECT_MANAGER/UPDATE_PROGRESS_OBJECT"; export const OBJECT_MANAGER_COMPLETE_OBJECT = "OBJECT_MANAGER/COMPLETE_OBJECT"; +export const OBJECT_MANAGER_ERROR_IN_OBJECT = "OBJECT_MANAGER/ERROR_IN_OBJECT"; export const OBJECT_MANAGER_DELETE_FROM_OBJECT_LIST = "OBJECT_MANAGER/DELETE_FROM_OBJECT_LIST"; export const OBJECT_MANAGER_CLEAN_LIST = "OBJECT_MANAGER/CLEAN_LIST"; @@ -30,6 +31,7 @@ export const OBJECT_MANAGER_OPEN_LIST = "OBJECT_MANAGER/OPEN_LIST"; export const OBJECT_MANAGER_CLOSE_LIST = "OBJECT_MANAGER/CLOSE_LIST"; export const OBJECT_MANAGER_SET_SEARCH_OBJECT = "OBJECT_MANAGER/SET_SEARCH_OBJECT"; +export const OBJECT_MANAGER_CANCEL_OBJECT = "OBJECT_MANAGER/CANCEL_OBJECT"; export const BUCKET_BROWSER_VERSIONS_MODE_ENABLED = "BUCKET_BROWSER/VERSIONS_MODE_ENABLED"; @@ -81,6 +83,7 @@ export interface ObjectBrowserReducer { export interface ObjectManager { objectsToManage: IFileItem[]; managerOpen: boolean; + newItems: boolean; } export interface IFileItem { @@ -91,6 +94,9 @@ export interface IFileItem { percentage: number; done: boolean; waitingForFile: boolean; + failed: boolean; + cancelled: boolean; + call?: XMLHttpRequest; } interface RewindSetEnabled { @@ -147,6 +153,12 @@ interface OMCloseList { type: typeof OBJECT_MANAGER_CLOSE_LIST; } +interface OMSetObjectError { + type: typeof OBJECT_MANAGER_ERROR_IN_OBJECT; + status: boolean; + instanceID: string; +} + interface SetSearchObjects { type: typeof OBJECT_MANAGER_SET_SEARCH_OBJECT; searchString: string; @@ -192,6 +204,11 @@ interface SetObjectManagerLoading { status: boolean; } +interface CancelObjectInManager { + type: typeof OBJECT_MANAGER_CANCEL_OBJECT; + instanceID: string; +} + export type ObjectBrowserActionTypes = | RewindSetEnabled | RewindReset @@ -204,6 +221,7 @@ export type ObjectBrowserActionTypes = | OMToggleList | OMOpenList | OMCloseList + | OMSetObjectError | SetSearchObjects | SetSearchVersions | SetSelectedversion @@ -212,4 +230,5 @@ export type ObjectBrowserActionTypes = | SetLoadingObjectInfo | SetObjectDetailsState | SetSelectedObject - | SetObjectManagerLoading; + | SetObjectManagerLoading + | CancelObjectInManager;