From 8bde755b1e12b7ff471372202a72d3e7244a184d Mon Sep 17 00:00:00 2001
From: Skander Hatira <skander.hatira@inrae.fr>
Date: Wed, 23 Mar 2022 17:47:35 +0100
Subject: [PATCH] adapting visualization and adding error handling

---
 package.json                                  |  2 +
 src/App.js                                    | 58 +++++++-----
 src/components/Comparisons/ComparisonTable.js | 10 +-
 src/components/Login/Login.js                 |  4 +-
 src/components/Table/Table.js                 | 41 ++++-----
 src/components/Tableau/DashLayout.js          | 36 +++++++-
 .../Visualization/VisualizationFill.js        | 91 ++++++++++++++-----
 src/hooks/useDownloads.js                     | 30 ++++++
 src/main.js                                   |  4 -
 yarn.lock                                     | 14 +++
 10 files changed, 205 insertions(+), 85 deletions(-)
 create mode 100644 src/hooks/useDownloads.js

diff --git a/package.json b/package.json
index 4d32d9b..2d5847b 100644
--- a/package.json
+++ b/package.json
@@ -130,8 +130,10 @@
     "electron-serve": "^1.1.0",
     "electron-squirrel-startup": "^1.0.0",
     "express": "^4.17.1",
+    "fast-folder-size": "^1.6.1",
     "fontsource-roboto": "^4.0.0",
     "fs-extra": "^9.1.0",
+    "get-folder-size": "^3.1.0",
     "got": "^11.8.2",
     "is-empty": "^1.2.0",
     "is-running": "^2.1.0",
diff --git a/src/App.js b/src/App.js
index b9a5d32..2ffade3 100644
--- a/src/App.js
+++ b/src/App.js
@@ -3,6 +3,8 @@ import { hot } from "react-hot-loader";
 import React from "react";
 import { HashRouter as Router, Route, Switch } from "react-router-dom";
 import { ProvideAuth } from "./hooks/useAuth";
+import { ProvideDownloads } from "./hooks/useDownloads";
+
 import { ProvideConfig } from "./hooks/useConfig";
 import Landing from "./components/Landing/Landing";
 import Register from "./components/Register/Register";
@@ -21,32 +23,38 @@ function App() {
   return (
     <ProvideAuth>
       <ProvideConfig>
-        <Router>
-          <div className="App">
-            <Switch>
-              <Route exact path="/" component={Landing} />
-              <Route exact path="/register" component={Register} />
-              <Route exact path="/login" component={Login} />
-              <PrivateRoute exact path="/newrun" component={RunBoard} />
-              <PrivateRoute exact path="/comparison" component={Comparisons} />
-              <PrivateRoute exact path="/machines" component={Profile} />
-              <PrivateRoute exact path="/profile" component={EditProfile} />
-              <PrivateRoute
-                exact
-                path="/newcomparison"
-                component={Comparison}
-              />
-              <PrivateRoute
-                exact
-                path="/visualization"
-                component={Visualization}
-              />
+        <ProvideDownloads>
+          <Router>
+            <div className="App">
+              <Switch>
+                <Route exact path="/" component={Landing} />
+                <Route exact path="/register" component={Register} />
+                <Route exact path="/login" component={Login} />
+                <PrivateRoute exact path="/newrun" component={RunBoard} />
+                <PrivateRoute
+                  exact
+                  path="/comparison"
+                  component={Comparisons}
+                />
+                <PrivateRoute exact path="/machines" component={Profile} />
+                <PrivateRoute exact path="/profile" component={EditProfile} />
+                <PrivateRoute
+                  exact
+                  path="/newcomparison"
+                  component={Comparison}
+                />
+                <PrivateRoute
+                  exact
+                  path="/visualization"
+                  component={Visualization}
+                />
 
-              <PrivateRoute exact path="/alignment" component={Dashboard} />
-              <Route component={NotFound} />
-            </Switch>
-          </div>
-        </Router>
+                <PrivateRoute exact path="/alignment" component={Dashboard} />
+                <Route component={NotFound} />
+              </Switch>
+            </div>
+          </Router>
+        </ProvideDownloads>
       </ProvideConfig>
     </ProvideAuth>
   );
diff --git a/src/components/Comparisons/ComparisonTable.js b/src/components/Comparisons/ComparisonTable.js
index 2e01200..c544920 100644
--- a/src/components/Comparisons/ComparisonTable.js
+++ b/src/components/Comparisons/ComparisonTable.js
@@ -324,8 +324,6 @@ export default function InteractiveList() {
     if (!fs.existsSync(bisepsTemp)) {
       fs.mkdirSync(bisepsTemp);
     }
-    const local = path.join(bisepsTemp, localPath);
-    const localtmp = local + "tmp";
     let sftp = new Client();
 
     let remotePath = `${row.remoteDir}/${filePath}`;
@@ -347,7 +345,7 @@ export default function InteractiveList() {
       .then(() => {
         return sftp.fastGet(
           remotePath.split(path.sep).join(path.posix.sep),
-          localtmp
+          path.join(bisepsTemp, localPath)
         );
       })
       .then((data) => {
@@ -355,12 +353,6 @@ export default function InteractiveList() {
         createBrowserWindow(path.join(bisepsTemp, localPath));
         sftp.end();
       })
-      .then(() => {
-        fs.rename(localtmp, local, function (err) {
-          if (err) console.log("ERROR: " + err);
-        });
-        sftp.end();
-      })
       .catch((err) => {
         console.log(err, "catch error");
         setErrors("File isn't ready yet");
diff --git a/src/components/Login/Login.js b/src/components/Login/Login.js
index f94e8d0..9ac0d51 100644
--- a/src/components/Login/Login.js
+++ b/src/components/Login/Login.js
@@ -132,10 +132,10 @@ const Login = () => {
             label="Password"
             autoComplete="current-password"
           />
-          <FormControlLabel
+          {/* <FormControlLabel
             control={<Checkbox value="remember" color="primary" />}
             label="Remember me"
-          />
+          /> */}
           <Button
             type="submit"
             fullWidth
diff --git a/src/components/Table/Table.js b/src/components/Table/Table.js
index 89ae33c..1824cbf 100644
--- a/src/components/Table/Table.js
+++ b/src/components/Table/Table.js
@@ -19,6 +19,8 @@ import CheckCircleIcon from "@material-ui/icons/CheckCircle";
 import ErrorIcon from "@material-ui/icons/Error";
 import Icon from "@material-ui/core/Icon";
 import { useAuth } from "../../hooks/useAuth";
+import { useDownloads } from "../../hooks/useDownloads";
+
 import TextField from "@material-ui/core/TextField";
 import Dialog from "@material-ui/core/Dialog";
 import DialogActions from "@material-ui/core/DialogActions";
@@ -33,7 +35,6 @@ import LockOpenIcon from "@material-ui/icons/LockOpen";
 import LockIcon from "@material-ui/icons/Lock";
 import IconButton from "@material-ui/core/IconButton";
 import GetAppIcon from "@material-ui/icons/GetApp";
-import LinearProgress from "@material-ui/core/LinearProgress";
 import CircularProgress from "@material-ui/core/CircularProgress";
 const { clipboard } = require("electron");
 const fs = require("fs");
@@ -87,8 +88,7 @@ export default function InteractiveList() {
   const [selectedRow, setSelectedRow] = useState({});
   const [refresh, setRefresh] = useState(0);
   const [successMessage, setSuccessMessage] = useState("");
-  const [progress, setProgress] = useState(0);
-  const [showPercent, setShowPercent] = useState(false);
+  const { loading, setLoading } = useDownloads();
 
   const [selected, setSelected] = useState([]);
 
@@ -140,17 +140,12 @@ export default function InteractiveList() {
         sftp.end();
       });
   };
-  const downloadCX = (row, sample, cx) => {
+  const downloadCX = async (row, sample, cx, idx) => {
+    console.log(idx);
     if (!fs.existsSync(bisepsTemp)) {
       fs.mkdirSync(bisepsTemp);
     }
-    setShowPercent(true);
-    const options = {
-      step: (step, chunk, total) => {
-        const percent = Math.floor((step / total) * 100);
-        setProgress(percent);
-      },
-    };
+
     let sftp = new Client();
     const local = path.join(bisepsTemp, path.basename(cx));
     const localtmp = local + ".tmp";
@@ -166,15 +161,16 @@ export default function InteractiveList() {
       })
       .then(async () => {
         if (!fs.existsSync(local)) {
-          return sftp.fastGet(cx, localtmp, options);
+          setLoading((prevState) => ({ ...prevState, [idx]: true }));
+          return sftp.fastGet(cx, localtmp);
         }
       })
       .then(() => {
+        console.log(loading);
         fs.rename(localtmp, local, function (err) {
           if (err) console.log("ERROR: " + err);
         });
-        setShowPercent(false);
-        setProgress(0);
+        setLoading((prevState) => ({ ...prevState, [idx]: false }));
         sftp.end();
       })
       .catch((err) => {
@@ -543,6 +539,7 @@ export default function InteractiveList() {
           data.map((row, idx) => {
             const reports = [];
             row.samples.map((sample) => {
+              console.log(sample);
               reports.push(
                 `${row.remoteDir}/results/${sample.samplePath}/methylation_extraction_bismark/${sample.samplePath}.deduplicated.CX_report.txt`
               );
@@ -705,15 +702,8 @@ export default function InteractiveList() {
                 </div>
                 <div className={classes.demo}>
                   <List dense={dense}>
-                    {showPercent && (
-                      <Box sx={{ width: "100%" }}>
-                        <LinearProgress
-                          variant="determinate"
-                          value={progress}
-                        />
-                      </Box>
-                    )}{" "}
-                    {row.samples.map((sample, idx) => {
+                    {row.samples.map((sample) => {
+                      const idx = `${sample._id}-align`;
                       const outdir = row.remote
                         ? `${row.remoteDir}`
                         : row.outdir;
@@ -765,6 +755,11 @@ export default function InteractiveList() {
                               aria-label="files"
                               onClick={() => downloadCX(row, sample, CX, idx)}
                             >
+                              {loading[idx] && (
+                                <Box sx={{ width: "100%" }}>
+                                  <CircularProgress />
+                                </Box>
+                              )}{" "}
                               <GetAppIcon
                                 style={{
                                   color: sampleExist ? "green" : "gray",
diff --git a/src/components/Tableau/DashLayout.js b/src/components/Tableau/DashLayout.js
index 28d1e4f..53b5b06 100644
--- a/src/components/Tableau/DashLayout.js
+++ b/src/components/Tableau/DashLayout.js
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useEffect, useState } from "react";
 import CssBaseline from "@material-ui/core/CssBaseline";
 import Drawer from "@material-ui/core/Drawer";
 import AppBar from "@material-ui/core/AppBar";
@@ -10,6 +10,8 @@ import IconButton from "@material-ui/core/IconButton";
 import MenuIcon from "@material-ui/icons/Menu";
 import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
 import ExitToAppIcon from "@material-ui/icons/ExitToApp";
+import DeleteForeverIcon from "@material-ui/icons/DeleteForever";
+
 import { mainListItems, secondaryListItems } from "./listItems";
 import { useAuth } from "../../hooks/useAuth";
 import { makeStyles } from "@material-ui/core/styles";
@@ -25,6 +27,14 @@ import Fade from "@material-ui/core/Fade";
 import ListItemIcon from "@material-ui/core/ListItemIcon";
 import AccountCircleIcon from "@material-ui/icons/AccountCircle";
 import EditIcon from "@material-ui/icons/Edit";
+import { useDownloads } from "../../hooks/useDownloads";
+
+const fs = require("fs");
+const path = require("path");
+const fastFolderSize = require("fast-folder-size");
+const homedir = require("os").homedir();
+const bisepsTemp = path.join(homedir, ".biseps", "tmp");
+
 const drawerWidth = 240;
 
 const useStyles = makeStyles((theme) => ({
@@ -125,11 +135,27 @@ function Copyright() {
 }
 const DashLayout = ({ Filling }) => {
   const classes = useStyles();
+  const { cache, setCache } = useDownloads();
 
   const fixedHeightPaper = clsx(classes.paper, classes.fixedHeight);
   const auth = useAuth();
   const { openDrawer, setOpenDrawer } = useConfig();
+  useEffect(() => {
+    console.log("cache is empty");
+    fastFolderSize(bisepsTemp, (err, bytes) => {
+      if (err) {
+        throw err;
+      }
 
+      setCache((bytes / 1e9).toFixed(2));
+    });
+  }, []);
+  const clearCache = () => {
+    fs.rmdirSync(bisepsTemp, {
+      recursive: true,
+    });
+    setCache(0);
+  };
   const handleDrawerOpen = () => {
     setOpenDrawer(true);
   };
@@ -234,6 +260,14 @@ const DashLayout = ({ Filling }) => {
                   </ListItemIcon>
                   <Typography variant="inherit">Logout </Typography>
                 </MenuItem>
+                <MenuItem onClick={clearCache} to="/profile">
+                  <ListItemIcon>
+                    <DeleteForeverIcon />
+                  </ListItemIcon>
+                  <Typography variant="inherit">
+                    Flush cache : {cache} Gb{" "}
+                  </Typography>
+                </MenuItem>
               </Menu>
             </>
           ) : (
diff --git a/src/components/Visualization/VisualizationFill.js b/src/components/Visualization/VisualizationFill.js
index eafec8f..2033a89 100644
--- a/src/components/Visualization/VisualizationFill.js
+++ b/src/components/Visualization/VisualizationFill.js
@@ -16,6 +16,10 @@ import Grid from "@material-ui/core/Grid";
 import Typography from "@material-ui/core/Typography";
 import IconButton from "@material-ui/core/IconButton";
 import Container from "@material-ui/core/Container";
+import CircularProgress from "@material-ui/core/CircularProgress";
+import { useDownloads } from "../../hooks/useDownloads";
+import Snackbar from "@material-ui/core/Snackbar";
+import MuiAlert from "@material-ui/lab/Alert";
 
 const handler = require("serve-handler");
 const { shell } = window.require("electron");
@@ -38,15 +42,20 @@ const useStyles = makeStyles((theme) => ({
     margin: theme.spacing(1),
   },
 }));
+function Alert(props) {
+  return <MuiAlert elevation={6} variant="filled" {...props} />;
+}
 
 export default function VisualizationFill() {
   const classes = useStyles();
   const [checked, setChecked] = useState([]);
   const [checkedComp, setCheckedComp] = useState([]);
-  const [progress, setProgress] = useState(0);
-  const [showPercent, setShowPercent] = useState(false);
   const [checkedTrack, setCheckedTrack] = useState([]);
   const [refresh, setRefresh] = useState(0);
+  const { loading, setLoading } = useDownloads();
+  const [openAlert, setOpenAlert] = useState(false);
+  const [errors, setErrors] = useState("");
+  const [successMessage, setSuccessMessage] = useState("");
 
   const { user } = useAuth();
 
@@ -85,7 +94,15 @@ export default function VisualizationFill() {
 
       setCheckedTrack(newChecked);
     };
-
+  const handleClose = () => {
+    setOpen(false);
+  };
+  const handleCloseAlert = () => {
+    setOpenAlert(false);
+  };
+  const handleOpenAlert = () => {
+    setOpenAlert(true);
+  };
   const handleToggleComp = (bed, bedtbi, associatedGenome, id) => () => {
     const currentIndex = checkedComp.findIndex((x) => x.id === id);
     const newChecked = [...checkedComp];
@@ -185,7 +202,7 @@ export default function VisualizationFill() {
       return false;
     }
   };
-  const downloadFiles = (row, tracks) => {
+  const downloadFiles = (row, tracks, idx) => {
     let sftp = new Client();
 
     console.log("download files");
@@ -193,13 +210,7 @@ export default function VisualizationFill() {
     if (!fs.existsSync(bisepsTemp)) {
       fs.mkdirSync(bisepsTemp);
     }
-    setShowPercent(true);
-    const options = {
-      step: (step, chunk, total) => {
-        const percent = Math.floor((step / total) * 100);
-        setProgress(percent);
-      },
-    };
+
     sftp
       .connect({
         host: row.machine.hostname,
@@ -219,11 +230,16 @@ export default function VisualizationFill() {
             !fs.existsSync(path.join(bisepsTemp, path.basename(tracks[track])))
           ) {
             try {
+              setLoading((prevState) => ({ ...prevState, [idx]: true }));
               await sftp.fastGet(
                 tracks[track].split(path.sep).join(path.posix.sep),
                 localtmp
               );
             } catch (err) {
+              setErrors(
+                "An unexpected error occurred, please make sure that this sample has been correctly processed"
+              );
+              handleOpenAlert();
               console.log(err);
             }
           }
@@ -232,6 +248,7 @@ export default function VisualizationFill() {
       })
       .then(() => {
         for (const track in tracks) {
+          setLoading((prevState) => ({ ...prevState, [idx]: false }));
           const local = path.join(bisepsTemp, path.basename(tracks[track]));
           const localtmp = local + ".tmp";
           fs.rename(localtmp, local, function (err) {
@@ -389,6 +406,19 @@ export default function VisualizationFill() {
   };
   return (
     <Container maxWidth="lg" className={classes.container} gutterbottom>
+      <Snackbar
+        anchorOrigin={{ vertical: "top", horizontal: "center" }}
+        open={openAlert}
+        autoHideDuration={10000}
+        onClose={handleCloseAlert}
+      >
+        <Alert
+          onClose={handleCloseAlert}
+          severity={errors && errors.length > 0 ? "error" : "success"}
+        >
+          {errors && errors.length > 0 ? `Error : ${errors}` : successMessage}
+        </Alert>
+      </Snackbar>
       <Grid container direction="column" alignItems="center" gutterBottom>
         <Box m={sessionStorage.Platform == "linux" ? 3 : 10}>
           {" "}
@@ -536,6 +566,7 @@ export default function VisualizationFill() {
                     `${associatedGenome}/${sample.samplePath}.deduplicated.bw`
                   )
                 );
+                const ident = `${sample._id}-viz`;
                 return (
                   <ListItem
                     key={`${sample._id}-${idx}`}
@@ -589,8 +620,13 @@ export default function VisualizationFill() {
                           edge="end"
                           disabled={sampleExist}
                           aria-label="files"
-                          onClick={() => downloadFiles(row, tracks)}
+                          onClick={() => downloadFiles(row, tracks, ident)}
                         >
+                          {loading[ident] && (
+                            <Box sx={{ width: "100%" }}>
+                              <CircularProgress />
+                            </Box>
+                          )}{" "}
                           <GetAppIcon
                             style={{
                               color: sampleExist ? "green" : "gray",
@@ -650,25 +686,23 @@ export default function VisualizationFill() {
                         bisepsTemp,
                         path.basename(bed)
                       );
+                      const fileDownloaded = fileExist(bedPathLocal);
                       const debExist = fileExist(
                         path.join(
                           user.user.jbPath,
-                          `${associatedGenome}/${comparison.id}-${context}.bed.gz`
+                          `${associatedGenome}/${comparison.id}-${context}-overallMethylation.bed.gz`
                         )
                       );
                       const tracks = [bed, bedtbi];
+                      const ident = `${comparison._id}-${context}-viz`;
+
                       return (
                         <ListItem
                           key={`${comparison._id}-${idx}-${context}`}
                           button
                           disabled={
                             fileExist(row.remote ? bedPathLocal : bed) &&
-                            !fileExist(
-                              path.join(
-                                user.user.jbPath,
-                                `${associatedGenome}/${comparison.id}-${context}.bed.gz`
-                              )
-                            ) &&
+                            !debExist &&
                             genomExist
                               ? false
                               : true
@@ -726,10 +760,25 @@ export default function VisualizationFill() {
                             {row.remote ? (
                               <IconButton
                                 edge="end"
+                                disabled={fileDownloaded}
                                 aria-label="files"
-                                onClick={() => downloadFiles(row, tracks)}
+                                onClick={() =>
+                                  downloadFiles(row, tracks, ident)
+                                }
                               >
-                                <GetAppIcon />
+                                <GetAppIcon
+                                  style={{
+                                    color: fileDownloaded ? "green" : "gray",
+                                  }}
+                                />
+                                {fileDownloaded
+                                  ? "Comparison files available"
+                                  : "Download comparison files"}
+                                {loading[ident] && (
+                                  <Box sx={{ width: "100%" }}>
+                                    <CircularProgress />
+                                  </Box>
+                                )}{" "}
                               </IconButton>
                             ) : (
                               ""
diff --git a/src/hooks/useDownloads.js b/src/hooks/useDownloads.js
new file mode 100644
index 0000000..7f80ff0
--- /dev/null
+++ b/src/hooks/useDownloads.js
@@ -0,0 +1,30 @@
+import React, { useState, useContext, createContext } from "react";
+
+const downloadContext = createContext();
+
+export const ProvideDownloads = ({ children }) => {
+  const config = useProvideDownloads();
+  return (
+    <downloadContext.Provider value={config}>
+      {children}
+    </downloadContext.Provider>
+  );
+};
+
+export const useDownloads = () => {
+  return useContext(downloadContext);
+};
+
+const useProvideDownloads = () => {
+  const initialState = {};
+
+  const [loading, setLoading] = useState(initialState);
+  const [cache, setCache] = useState(0);
+
+  return {
+    loading,
+    cache,
+    setCache,
+    setLoading,
+  };
+};
diff --git a/src/main.js b/src/main.js
index bd599eb..86d4855 100644
--- a/src/main.js
+++ b/src/main.js
@@ -242,10 +242,6 @@ app.on("ready", createWindow);
 // explicitly with Cmd + Q.
 app.on("window-all-closed", () => {
   if (process.platform !== "darwin") {
-    console.log(os.tmpdir());
-    fs.rmdirSync(bisepsTemp, {
-      recursive: true,
-    });
     if (process.platform !== "win32") {
       fs.unlinkSync(sock);
     }
diff --git a/yarn.lock b/yarn.lock
index d31f90b..6e3530c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7618,6 +7618,13 @@ fast-deep-equal@^3.1.1:
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
 
+fast-folder-size@^1.6.1:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/fast-folder-size/-/fast-folder-size-1.6.1.tgz#1dc1674842854032cf07a387ba77c66546c547eb"
+  integrity sha512-F3tRpfkAzb7TT2JNKaJUglyuRjRa+jelQD94s9OSqkfEeytLmupCqQiD+H2KoIXGtp4pB5m4zNmv5m2Ktcr+LA==
+  dependencies:
+    unzipper "^0.10.11"
+
 fast-glob@^2.2.6:
   version "2.2.7"
   resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
@@ -8183,6 +8190,13 @@ get-folder-size@^2.0.1:
     gar "^1.0.4"
     tiny-each-async "2.0.3"
 
+get-folder-size@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/get-folder-size/-/get-folder-size-3.1.0.tgz#96d39f7e1a0b2e30d13958e05373ebfa32bdfaa4"
+  integrity sha512-/I7q+x1HCd22IXP4+kp2Wkz8+au7VfNwNyMfM4Z0gwaTMs+dJ1ShXUWDGSWXi+rDU59MI/j7NBP7+kd7zejnPw==
+  dependencies:
+    gar "^1.0.4"
+
 get-installed-path@^2.0.3:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/get-installed-path/-/get-installed-path-2.1.1.tgz#a1f33dc6b8af542c9331084e8edbe37fe2634152"
-- 
GitLab