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