From 19195e0cd02db98eeef2893aad718a1decd727a7 Mon Sep 17 00:00:00 2001 From: Daniel Valdivia Date: Mon, 22 Mar 2021 11:36:46 -0700 Subject: [PATCH] Interactive Feedback when list objects take a long time (#655) * Interactive Feedback when list objects take a long time * Remove cancel button Co-authored-by: Alex <33497058+bexsoft@users.noreply.github.com> --- .../Objects/ListObjects/ListObjects.tsx | 71 ++++++++++++++++++- .../Common/TableWrapper/TableWrapper.tsx | 19 ++--- 2 files changed, 79 insertions(+), 11 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 9dc79af4e..c23753d22 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 @@ -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, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import Grid from "@material-ui/core/Grid"; import TextField from "@material-ui/core/TextField"; @@ -33,7 +33,7 @@ import { searchField, } from "../../../../Common/FormComponents/common/styleLibrary"; import PageHeader from "../../../../Common/PageHeader/PageHeader"; -import { Button, Input } from "@material-ui/core"; +import { Button, Input, Typography } from "@material-ui/core"; import * as reactMoment from "react-moment"; import { CreateIcon } from "../../../../../../icons"; import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs"; @@ -150,6 +150,30 @@ interface IListObjectsProps { fileDownloadStarted: typeof fileDownloadStarted; } +function useInterval(callback: any, delay: number) { + const savedCallback = useRef(null); + + // Remember the latest callback. + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + // Set up the interval. + useEffect(() => { + function tick() { + if (savedCallback !== undefined && savedCallback.current) { + savedCallback.current(); + } + } + if (delay !== null) { + let id = setInterval(tick, delay); + return () => clearInterval(id); + } + }, [delay]); +} + +const defLoading = Loading...; + const ListObjects = ({ classes, match, @@ -171,6 +195,41 @@ const ListObjects = ({ const [selectedObject, setSelectedObject] = useState(""); const [selectedBucket, setSelectedBucket] = useState(""); const [filterObjects, setFilterObjects] = useState(""); + const [loadingPromise, setLoadingPromise] = useState | null>( + null + ); + const [loadingStartTime, setLoadingStartTime] = useState(0); + const [loadingMessage, setLoadingMessage] = useState( + defLoading + ); + + const updateMessage = () => { + let timeDelta = Date.now() - loadingStartTime; + + if (timeDelta / 1000 >= 6) { + setLoadingMessage( + + + This operation is taking longer than expected... ( + {Math.ceil(timeDelta / 1000)}s) + + + ); + } else if (timeDelta / 1000 >= 3) { + setLoadingMessage( + + This operation is taking longer than expected... + + ); + } + }; + + useInterval(() => { + // Your custom logic here + if (loading) { + updateMessage(); + } + }, 1000); useEffect(() => { const bucketName = match.params["bucket"]; @@ -206,7 +265,11 @@ const ListObjects = ({ extraPath = `?prefix=${internalPaths}/`; } - api + let currentTimestamp = Date.now() + 0; + setLoadingStartTime(currentTimestamp); + setLoadingMessage(defLoading); + + let p = api .invoke("GET", `/api/v1/buckets/${bucketName}/objects${extraPath}`) .then((res: BucketObjectsList) => { setSelectedBucket(bucketName); @@ -239,6 +302,7 @@ const ListObjects = ({ setLoading(false); setErrorSnackMessage(err); }); + setLoadingPromise(p); } }, [loading, match, setLastAsFile, setErrorSnackMessage]); @@ -558,6 +622,7 @@ const ListObjects = ({ }, ]} isLoading={loading} + loadingMessage={loadingMessage} entityName="Objects" idField="name" records={filteredRecords} diff --git a/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx b/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx index b51430d68..59e0cd09d 100644 --- a/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx +++ b/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx @@ -13,19 +13,19 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { useState, Fragment } from "react"; +import React, { Fragment, useState } from "react"; import get from "lodash/get"; import isString from "lodash/isString"; import { + Checkbox, + Grid, + IconButton, LinearProgress, Paper, - Grid, - Checkbox, - Typography, - IconButton, Popover, + Typography, } from "@material-ui/core"; -import { Table, Column, AutoSizer, InfiniteLoader } from "react-virtualized"; +import { AutoSizer, Column, InfiniteLoader, Table } from "react-virtualized"; import { createStyles, withStyles } from "@material-ui/core/styles"; import CircularProgress from "@material-ui/core/CircularProgress"; import ViewColumnIcon from "@material-ui/icons/ViewColumn"; @@ -43,11 +43,12 @@ import CheckboxWrapper from "../FormComponents/CheckboxWrapper/CheckboxWrapper"; interface ItemActions { type: string; - onClick?(valueToSend: any): any; to?: string; sendOnlyId?: boolean; hideButtonFunction?: (itemValue: any) => boolean; showLoaderFunction?: (itemValue: any) => boolean; + + onClick?(valueToSend: any): any; } interface IColumns { @@ -83,6 +84,7 @@ interface TableWrapperProps { onSelect?: (e: React.ChangeEvent) => any; idField: string; isLoading: boolean; + loadingMessage?: React.ReactNode; records: any[]; classes: any; entityName: string; @@ -491,6 +493,7 @@ const TableWrapper = ({ onSelect, records, isLoading, + loadingMessage = Loading..., entityName, selectedItems, idField, @@ -600,7 +603,7 @@ const TableWrapper = ({ {isLoading && ( - Loading... + {loadingMessage}