From 51ba86fa46b315db0305ec4238ae73f76674cf2e Mon Sep 17 00:00:00 2001 From: Cesar N Date: Fri, 18 Dec 2020 17:40:03 -0600 Subject: [PATCH] Add progress bar on UI (#526) --- portal-ui/src/actions.ts | 16 +++ portal-ui/src/reducer.ts | 14 +++ .../Objects/ListObjects/ListObjects.tsx | 113 ++++++++---------- .../Objects/ObjectDetails/ShareFile.tsx | 46 ++----- portal-ui/src/screens/Console/Console.tsx | 83 ++++++++++++- portal-ui/src/types.ts | 17 ++- 6 files changed, 187 insertions(+), 102 deletions(-) diff --git a/portal-ui/src/actions.ts b/portal-ui/src/actions.ts index 60f1965d4..fee7726ad 100644 --- a/portal-ui/src/actions.ts +++ b/portal-ui/src/actions.ts @@ -20,6 +20,8 @@ import { SERVER_IS_LOADING, SERVER_NEEDS_RESTART, USER_LOGGED, + SET_LOADING_PROGRESS, + SET_SNACK_BAR_MESSAGE, } from "./types"; export function userLoggedIn(loggedIn: boolean) { @@ -56,3 +58,17 @@ export function serverIsLoading(isLoading: boolean) { isLoading: isLoading, }; } + +export const setLoadingProgress = (progress: number) => { + return { + type: SET_LOADING_PROGRESS, + loadingProgress: progress, + }; +}; + +export const setSnackBarMessage = (message: string) => { + return { + type: SET_SNACK_BAR_MESSAGE, + snackBarMessage: message, + }; +}; diff --git a/portal-ui/src/reducer.ts b/portal-ui/src/reducer.ts index 9772c3d12..39876c4c9 100644 --- a/portal-ui/src/reducer.ts +++ b/portal-ui/src/reducer.ts @@ -22,6 +22,8 @@ import { SystemActionTypes, SystemState, USER_LOGGED, + SET_LOADING_PROGRESS, + SET_SNACK_BAR_MESSAGE, } from "./types"; const initialState: SystemState = { @@ -32,6 +34,8 @@ const initialState: SystemState = { sidebarOpen: true, serverNeedsRestart: false, serverIsLoading: false, + loadingProgress: 100, + snackBarMessage: "", }; export function systemReducer( @@ -65,6 +69,16 @@ export function systemReducer( ...state, serverIsLoading: action.isLoading, }; + case SET_LOADING_PROGRESS: + return { + ...state, + loadingProgress: action.loadingProgress, + }; + case SET_SNACK_BAR_MESSAGE: + return { + ...state, + snackBarMessage: action.snackBarMessage, + }; default: return state; } 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 bf72ccfcd..682deea8c 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 @@ -37,7 +37,6 @@ import { isNullOrUndefined } from "util"; import { Button, Input } from "@material-ui/core"; import * as reactMoment from "react-moment"; import { CreateIcon } from "../../../../../../icons"; -import Snackbar from "@material-ui/core/Snackbar"; import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs"; import get from "lodash/get"; import { withRouter } from "react-router-dom"; @@ -51,6 +50,10 @@ import { ObjectBrowserState, Route } from "../../../../ObjectBrowser/reducers"; import CreateFolderModal from "./CreateFolderModal"; import UploadFile from "../../../../../../icons/UploadFile"; import { download } from "../utils"; +import { + setLoadingProgress, + setSnackBarMessage, +} from "../../../../../../actions"; const commonIcon = { backgroundRepeat: "no-repeat", @@ -134,6 +137,8 @@ interface IListObjectsProps { setAllRoutes: (path: string) => any; routesList: Route[]; setLastAsFile: () => any; + setLoadingProgress: typeof setLoadingProgress; + setSnackBarMessage: typeof setSnackBarMessage; } interface ObjectBrowserReducer { @@ -147,6 +152,8 @@ const ListObjects = ({ setAllRoutes, routesList, setLastAsFile, + setLoadingProgress, + setSnackBarMessage, }: IListObjectsProps) => { const [records, setRecords] = useState([]); const [loading, setLoading] = useState(true); @@ -155,8 +162,6 @@ const ListObjects = ({ const [selectedObject, setSelectedObject] = useState(""); const [selectedBucket, setSelectedBucket] = useState(""); const [filterObjects, setFilterObjects] = useState(""); - const [openSnackbar, setOpenSnackbar] = useState(false); - const [snackBarMessage, setSnackbarMessage] = useState(""); useEffect(() => { const bucketName = match.params["bucket"]; @@ -185,26 +190,28 @@ const ListObjects = ({ }); }; - let extraPath = ""; - if (internalPaths) { - extraPath = `?prefix=${internalPaths}/`; - } + if (loading) { + let extraPath = ""; + if (internalPaths) { + extraPath = `?prefix=${internalPaths}/`; + } - api - .invoke("GET", `/api/v1/buckets/${bucketName}/objects${extraPath}`) - .then((res: BucketObjectsList) => { - setSelectedBucket(bucketName); - setRecords(res.objects || []); - // In case no objects were retrieved, We check if item is a file - if (!res.objects && extraPath !== "") { - verifyIfIsFile(); - return; - } - setLoading(false); - }) - .catch((err: any) => { - setLoading(false); - }); + api + .invoke("GET", `/api/v1/buckets/${bucketName}/objects${extraPath}`) + .then((res: BucketObjectsList) => { + setSelectedBucket(bucketName); + setRecords(res.objects || []); + // In case no objects were retrieved, We check if item is a file + if (!res.objects && extraPath !== "") { + verifyIfIsFile(); + return; + } + setLoading(false); + }) + .catch((err: any) => { + setLoading(false); + }); + } }, [loading, match, setLastAsFile]); useEffect(() => { @@ -218,7 +225,7 @@ const ListObjects = ({ setDeleteOpen(false); if (refresh) { - showSnackBarMessage(`Object '${selectedObject}' deleted successfully.`); + setSnackBarMessage(`Object '${selectedObject}' deleted successfully.`); setLoading(true); } }; @@ -227,16 +234,6 @@ const ListObjects = ({ setCreateFolderOpen(false); }; - const showSnackBarMessage = (text: string) => { - setSnackbarMessage(text); - setOpenSnackbar(true); - }; - - const closeSnackBar = () => { - setSnackbarMessage(""); - setOpenSnackbar(false); - }; - const upload = (e: any, bucketName: string, path: string) => { if (isNullOrUndefined(e) || isNullOrUndefined(e.target)) { return; @@ -252,31 +249,34 @@ const ListObjects = ({ xhr.open("POST", uploadUrl, true); xhr.withCredentials = false; - xhr.onload = function (event) { - // TODO: handle status - if (xhr.status === 401 || xhr.status === 403) { - showSnackBarMessage("An error occurred while uploading the file."); - } - if (xhr.status === 500) { - showSnackBarMessage("An error occurred while uploading the file."); + xhr.onload = function(event) { + if ( + xhr.status === 401 || + xhr.status === 403 || + xhr.status === 400 || + xhr.status === 500 + ) { + setSnackBarMessage("An error occurred while uploading the file."); } if (xhr.status === 200) { - showSnackBarMessage("Object uploaded successfully."); - setLoading(true); + setSnackBarMessage("Object uploaded successfully."); } }; xhr.upload.addEventListener("error", (event) => { - // TODO: handle error - showSnackBarMessage("An error occurred while uploading the file."); + setSnackBarMessage("An error occurred while uploading the file."); }); xhr.upload.addEventListener("progress", (event) => { - // TODO: handle progress with event.loaded, event.total + setLoadingProgress(Math.floor((event.loaded * 100) / event.total)); }); xhr.onerror = () => { - showSnackBarMessage("An error occurred while uploading the file."); + setSnackBarMessage("An error occurred while uploading the file."); + }; + xhr.onloadend = () => { + setLoading(true); + setLoadingProgress(100); }; const formData = new FormData(); @@ -346,23 +346,9 @@ const ListObjects = ({ path = `${splitPaths.slice(2).join("/")}/`; } - let file = e.target.files[0]; - showSnackBarMessage(`Uploading: ${file.name}`); upload(e, selectedBucket, path); }; - const snackBarAction = ( - - ); - const tableActions = [ { type: "view", onClick: openPath, sendOnlyId: true }, { type: "download", onClick: downloadObject }, @@ -419,11 +405,6 @@ const ListObjects = ({ onClose={closeAddFolderModal} /> )} - @@ -526,6 +507,8 @@ const mapDispatchToProps = { addRoute, setAllRoutes, setLastAsFile, + setLoadingProgress, + setSnackBarMessage, }; const connector = connect(mapStateToProps, mapDispatchToProps); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ShareFile.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ShareFile.tsx index 25f19c5e2..ae6efb2a6 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ShareFile.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ShareFile.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from "react"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import CopyToClipboard from "react-copy-to-clipboard"; -import Snackbar from "@material-ui/core/Snackbar"; import Grid from "@material-ui/core/Grid"; import Button from "@material-ui/core/Button"; import { modalBasic } from "../../../../Common/FormComponents/common/styleLibrary"; @@ -12,6 +11,8 @@ import api from "../../../../../../common/api"; import { IFileInfo } from "./types"; import PredefinedList from "../../../../Common/FormComponents/PredefinedList/PredefinedList"; import ErrorBlock from "../../../../../shared/ErrorBlock"; +import { setSnackBarMessage } from "../../../../../../actions"; +import { connect } from "react-redux"; const styles = (theme: Theme) => createStyles({ @@ -30,6 +31,7 @@ interface IShareFileProps { bucketName: string; dataObject: IFileInfo; closeModalAndRefresh: () => void; + setSnackBarMessage: typeof setSnackBarMessage; } const ShareFile = ({ @@ -38,24 +40,13 @@ const ShareFile = ({ closeModalAndRefresh, bucketName, dataObject, + setSnackBarMessage, }: IShareFileProps) => { const [shareURL, setShareURL] = useState(""); const [isLoadingFile, setIsLoadingFile] = useState(false); const [error, setError] = useState(""); const [selectedDate, setSelectedDate] = useState(""); const [dateValid, setDateValid] = useState(true); - const [openSnack, setOpenSnack] = useState(false); - const [snackBarMessage, setSnackbarMessage] = useState(""); - - const showSnackBarMessage = (text: string) => { - setSnackbarMessage(text); - setOpenSnack(true); - }; - - const closeSnackBar = () => { - setSnackbarMessage(""); - setOpenSnack(false); - }; const dateChanged = (newDate: string, isValid: boolean) => { setDateValid(isValid); @@ -115,27 +106,8 @@ const ShareFile = ({ } }, [dataObject, selectedDate, bucketName, dateValid, setShareURL]); - const snackBarAction = ( - - ); - return ( - {openSnack && ( - - )} } onClick={() => { - showSnackBarMessage("Share URL Copied to clipboard"); + setSnackBarMessage("Share URL Copied to clipboard"); }} disabled={shareURL === "" || isLoadingFile} > @@ -184,4 +156,10 @@ const ShareFile = ({ ); }; -export default withStyles(styles)(ShareFile); +const mapState = (state: IShareFileProps) => ({}); + +const connector = connect(mapState, { + setSnackBarMessage, +}); + +export default withStyles(styles)(connector(ShareFile)); diff --git a/portal-ui/src/screens/Console/Console.tsx b/portal-ui/src/screens/Console/Console.tsx index 94c468ba8..fdb9d415b 100644 --- a/portal-ui/src/screens/Console/Console.tsx +++ b/portal-ui/src/screens/Console/Console.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, useState, useEffect } from "react"; import clsx from "clsx"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { Button, LinearProgress } from "@material-ui/core"; @@ -29,6 +29,7 @@ import { serverIsLoading, serverNeedsRestart, setMenuOpen, + setSnackBarMessage, } from "../../actions"; import { ISessionResponse } from "./types"; import Buckets from "./Buckets/Buckets"; @@ -50,6 +51,7 @@ import Trace from "./Trace/Trace"; import Logs from "./Logs/Logs"; import Heal from "./Heal/Heal"; import Watch from "./Watch/Watch"; +import Snackbar from "@material-ui/core/Snackbar"; const drawerWidth = 245; @@ -57,6 +59,9 @@ const styles = (theme: Theme) => createStyles({ root: { display: "flex", + "& .MuiPaper-root": { + borderRadius: "0px 0px 15px 15px", + }, }, toolbar: { background: theme.palette.background.default, @@ -147,6 +152,34 @@ const styles = (theme: Theme) => lineHeight: "60px", textAlign: "center", }, + snackBar: { + backgroundColor: "#081F44", + fontWeight: "bold", + fontFamily: "Lato, sans-serif", + fontSize: 14, + padding: "0px 20px 0px 20px;", + + "& div": { + textAlign: "center", + padding: "6px 30px", + }, + }, + snackBarExternal: { + top: "-17px", + position: "absolute", + minWidth: "348px", + whiteSpace: "nowrap", + height: "33px", + }, + snackDiv: { + top: "17px", + left: "50%", + position: "absolute", + }, + progress: { + height: "3px", + backgroundColor: "#eaeaea", + }, }); interface IConsoleProps { @@ -159,6 +192,9 @@ interface IConsoleProps { serverNeedsRestart: typeof serverNeedsRestart; serverIsLoading: typeof serverIsLoading; session: ISessionResponse; + loadingProgress: number; + snackBarMessage: string; + setSnackBarMessage: typeof setSnackBarMessage; } const Console = ({ @@ -169,7 +205,12 @@ const Console = ({ serverNeedsRestart, serverIsLoading, session, + loadingProgress, + snackBarMessage, + setSnackBarMessage, }: IConsoleProps) => { + const [openSnackbar, setOpenSnackbar] = useState(false); + const restartServer = () => { serverIsLoading(true); api @@ -278,6 +319,20 @@ const Console = ({ ]; const allowedRoutes = routes.filter((route: any) => allowedPages[route.path]); + const closeSnackBar = () => { + setOpenSnackbar(false); + setSnackBarMessage(""); + }; + + useEffect(() => { + if (snackBarMessage === "") { + setOpenSnackbar(false); + return; + } + // Open SnackBar + setOpenSnackbar(true); + }, [snackBarMessage]); + return ( {session.status === "ok" ? ( @@ -302,7 +357,7 @@ const Console = ({ {isServerLoading ? ( The server is restarting. - + ) : ( @@ -321,6 +376,27 @@ const Console = ({ )} )} + {loadingProgress < 100 && ( + + )} +
+ { + closeSnackBar(); + }} + message={snackBarMessage} + autoHideDuration={5000} + className={classes.snackBarExternal} + ContentProps={{ + className: classes.snackBar, + }} + /> +
@@ -350,12 +426,15 @@ const mapState = (state: AppState) => ({ needsRestart: state.system.serverNeedsRestart, isServerLoading: state.system.serverIsLoading, session: state.console.session, + loadingProgress: state.system.loadingProgress, + snackBarMessage: state.system.snackBarMessage, }); const connector = connect(mapState, { setMenuOpen, serverNeedsRestart, serverIsLoading, + setSnackBarMessage, }); export default withStyles(styles)(connector(Console)); diff --git a/portal-ui/src/types.ts b/portal-ui/src/types.ts index 4bb022c40..765262961 100644 --- a/portal-ui/src/types.ts +++ b/portal-ui/src/types.ts @@ -22,6 +22,8 @@ export interface SystemState { userName: string; serverNeedsRestart: boolean; serverIsLoading: boolean; + loadingProgress: number; + snackBarMessage: string; } export const USER_LOGGED = "USER_LOGGED"; @@ -29,6 +31,8 @@ export const OPERATOR_MODE = "OPERATOR_MODE"; export const MENU_OPEN = "MENU_OPEN"; export const SERVER_NEEDS_RESTART = "SERVER_NEEDS_RESTART"; export const SERVER_IS_LOADING = "SERVER_IS_LOADING"; +export const SET_LOADING_PROGRESS = "SET_LOADING_PROGRESS"; +export const SET_SNACK_BAR_MESSAGE = "SET_SNACK_BAR_MESSAGE"; interface UserLoggedAction { type: typeof USER_LOGGED; @@ -54,10 +58,21 @@ interface ServerIsLoading { type: typeof SERVER_IS_LOADING; isLoading: boolean; } +interface SetLoadingProgress { + type: typeof SET_LOADING_PROGRESS; + loadingProgress: number; +} + +interface SetSnackBarMessage { + type: typeof SET_SNACK_BAR_MESSAGE; + snackBarMessage: string; +} export type SystemActionTypes = | UserLoggedAction | OperatorModeAction | SetMenuOpenAction | ServerNeedsRestartAction - | ServerIsLoading; + | ServerIsLoading + | SetLoadingProgress + | SetSnackBarMessage;