diff --git a/components/VFBMain.js b/components/VFBMain.js index e16e44f06..28ddfc53a 100644 --- a/components/VFBMain.js +++ b/components/VFBMain.js @@ -9,6 +9,7 @@ import VFBTermInfoWidget from './interface/VFBTermInfo/VFBTermInfo'; import Logo from '@geppettoengine/geppetto-client/components/interface/logo/Logo'; import Canvas from '@geppettoengine/geppetto-client/components/interface/3dCanvas/Canvas'; import QueryBuilder from '@geppettoengine/geppetto-client/components/interface/query/queryBuilder'; +import VFBDownloadContents from './interface/VFBDownloadContents/VFBDownloadContents'; import VFBUploader from './interface/VFBUploader/VFBUploader'; import HTMLViewer from '@geppettoengine/geppetto-ui/html-viewer/HTMLViewer'; import VFBListViewer from './interface/VFBListViewer/VFBListViewer'; @@ -52,6 +53,7 @@ class VFBMain extends React.Component { quickHelpVisible: undefined, UIUpdated: true, wireframeVisible: false, + downloadContentsVisible : true, uploaderContentsVisible : true }; @@ -488,6 +490,9 @@ class VFBMain extends React.Component { [buttonState]: !this.state[buttonState] }); break; + case 'downloadContentsVisible': + this.refs.downloadContentsRef?.openDialog(); + break; case 'quickHelpVisible': if (this.state[buttonState] === undefined) { this.setState({ @@ -527,6 +532,9 @@ class VFBMain extends React.Component { case 'triggerSetTermInfo': this.handlerInstanceUpdate(click.value[0]); break; + case 'downloadContentsVisible': + this.refs.downloadContentsRef?.openDialog(); + break; case 'uploaderContentsVisible': this.refs.uploaderContentsRef?.openDialog(); break; @@ -1752,7 +1760,10 @@ class VFBMain extends React.Component { searchConfiguration={this.searchConfiguration} datasourceConfiguration={this.datasourceConfiguration} /> + + + {this.htmlToolbarRender} ); diff --git a/components/configuration/VFBDownloadContents/configuration.json b/components/configuration/VFBDownloadContents/configuration.json new file mode 100644 index 000000000..e8898542a --- /dev/null +++ b/components/configuration/VFBDownloadContents/configuration.json @@ -0,0 +1,30 @@ +{ + "postURL":"https://zip.virtualflybrain.org/download", + "contentType": "application/json", + "zipName" : "VFB Files.zip", + "options" :{ + "obj": { + "label" : "OBJ" + }, + "swc": { + "label" : "SWC" + }, + "nrrd": { + "label" : "NRRD" + }, + "reference": { + "label" : "References" + } + }, + "text" : { + "title" : "Download Data", + "typesSubtitle" : "Please select the desired types", + "variablesSubtitle" : "Please select Variables:", + "noVariablesSubtitle" : "No loaded variables", + "errorMessage" : "Something went wrong... We were not able to download the data. Please try again.", + "noEntriesFound" : "No entries found for the types and variables selected.", + "cancelButton" : "Cancel", + "downloadButton" : "Download", + "tryAgainButton" : "Try Again" + } +} \ No newline at end of file diff --git a/components/configuration/VFBDownloadContents/nrrd.png b/components/configuration/VFBDownloadContents/nrrd.png new file mode 100644 index 000000000..beee12a5c Binary files /dev/null and b/components/configuration/VFBDownloadContents/nrrd.png differ diff --git a/components/configuration/VFBDownloadContents/obj.png b/components/configuration/VFBDownloadContents/obj.png new file mode 100644 index 000000000..fbcc914c7 Binary files /dev/null and b/components/configuration/VFBDownloadContents/obj.png differ diff --git a/components/configuration/VFBDownloadContents/reference.png b/components/configuration/VFBDownloadContents/reference.png new file mode 100644 index 000000000..b821fe7e1 Binary files /dev/null and b/components/configuration/VFBDownloadContents/reference.png differ diff --git a/components/configuration/VFBDownloadContents/swc.png b/components/configuration/VFBDownloadContents/swc.png new file mode 100644 index 000000000..3eb5d02b6 Binary files /dev/null and b/components/configuration/VFBDownloadContents/swc.png differ diff --git a/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js b/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js index a85df3499..1310a57c3 100644 --- a/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js +++ b/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js @@ -253,6 +253,14 @@ var toolbarMenu = { parameters: ["circuitBrowserVisible"] } }, + { + label: "Download Contents", + icon: "fa fa-download", + action: { + handlerAction: "downloadContentsVisible", + parameters: [] + } + }, { label: "NBLAST Uploader", icon: "fa fa-upload", diff --git a/components/interface/VFBDownloadContents/VFBDownloadContents.js b/components/interface/VFBDownloadContents/VFBDownloadContents.js new file mode 100644 index 000000000..aed98437b --- /dev/null +++ b/components/interface/VFBDownloadContents/VFBDownloadContents.js @@ -0,0 +1,563 @@ +import React from "react"; +import Button from "@material-ui/core/Button"; +import Grid from "@material-ui/core/Grid"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import Typography from "@material-ui/core/Typography"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import CircularProgress from '@material-ui/core/CircularProgress'; +import ChevronRightIcon from "@material-ui/icons/ChevronRight"; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; +import { Checkbox, Divider, IconButton } from "@material-ui/core"; +import Box from "@material-ui/core/Box"; +import { createMuiTheme, ThemeProvider } from "@material-ui/core/styles"; +import { withStyles } from "@material-ui/styles"; +import axios from "axios"; +import TreeView from "@material-ui/lab/TreeView"; +import TreeItem from "@material-ui/lab/TreeItem"; +import NRRDIcon from "../../configuration/VFBDownloadContents/nrrd.png"; +import OBJIcon from "../../configuration/VFBDownloadContents/obj.png"; +import SWCIcon from "../../configuration/VFBDownloadContents/swc.png"; +import ReferenceIcon from "../../configuration/VFBDownloadContents/reference.png"; +import CloseIcon from "@material-ui/icons/Close"; +import { connect } from "react-redux"; + +const iconsMap = { + obj: OBJIcon, + swc: SWCIcon, + reference: ReferenceIcon, + nrrd: NRRDIcon, +}; + +const ALL_INSTANCES = { id: "ALL_INSTANCES", name: "All Instances" }; + +const styles = theme => ({ + downloadButton: { backgroundColor: "#0AB7FE", color: "white !important" }, + downloadErrorButton: { backgroundColor: "#FCE7E7", color: "#E53935", border : "1px solid #E53935" }, + error: { color: "#E53935" }, + errorMessage: { wordWrap: "break-word" }, + downloadButtonText: { color: "white !important" }, + checkedBox: { borderColor: "#0AB7FE" }, + footer: { backgroundColor: "#EEF9FF" }, + errorFooter: { backgroundColor: "#FCE7E7" }, + listItemText: { fontSize: "1em" }, + customizedButton: { + position: "absolute", + left: "95%", + top: "2%", + backgroundColor: "#F5F5F5", + color: "gray", + }, + dialog: { + overflow: "unset", + margin: "0 auto", + }, + dialogContent: { overflow: "hidden" }, + checked: { "&$checked": { color: "#0AB7FE" } }, + "@global": { + ".MuiTreeItem-root.Mui-selected > .MuiTreeItem-content .MuiTreeItem-label": { backgroundColor: "white" }, + ".MuiTreeItem-root.Mui-selected > .MuiTreeItem-content .MuiTreeItem-label:hover, .MuiTreeItem-root.Mui-selected:focus > .MuiTreeItem-content .MuiTreeItem-label": { backgroundColor: "white" } + }, +}); + +const theme = createMuiTheme({ + typography: { + h2: { + fontSize: 22, + fontWeight: 400, + fontStyle: "normal", + lineHeight: "26.4px", + color: "#181818", + fontFamily: "Barlow", + }, + h5: { + fontSize: 11, + fontWeight: 500, + fontStyle: "normal", + lineHeight: "13.2px", + fontFamily: "Barlow", + color: "rgba(0, 0, 0, 0.54)", + }, + subtitle2: { + fontSize: 11, + fontWeight: 500, + fontStyle: "normal", + lineHeight: "13.2px", + fontFamily: "Barlow", + color: "rgba(0, 0, 0, 0.24)", + }, + error: { + fontSize: 11, + fontWeight: 500, + fontStyle: "normal", + lineHeight: "13.2px", + fontFamily: "Barlow", + color: "#E53935", + }, + button: { + fontSize: 11, + fontWeight: 600, + fontStyle: "normal", + lineHeight: "13.2px", + fontFamily: "Barlow", + color: "#0AB7FE", + }, + }, + Button: { + "&:hover": { + backgroundColor: "#0AB7FE", + boxShadow: "none", + }, + "&:active": { + boxShadow: "none", + backgroundColor: "#0AB7FE", + }, + }, +}); + +/** + * Component to download files contents + */ +class VFBDownloadContents extends React.Component { + constructor (props) { + super(props); + + this.state = { + open: false, + typesChecked: [], + downloadError: false, + downloading: false, + selectedVariables: [], + allVariablesSelectedFlag: false, + errorMessage : "" + }; + + this.configuration = require("../../configuration/VFBDownloadContents/configuration"); + this.configurationOptions = this.configuration.options; + this.handleCloseDialog = this.handleCloseDialog.bind(this); + this.openDialog = this.openDialog.bind(this); + this.handleTypeSelection = this.handleTypeSelection.bind(this); + this.handleDownload = this.handleDownload.bind(this); + this.extractVariableFileMeta = this.extractVariableFileMeta.bind(this); + this.getAllLoadedVariables = this.getAllLoadedVariables.bind(this); + this.requestZipDownload = this.requestZipDownload.bind(this); + this.getVariableById = this.getVariableById.bind(this); + this.toggleVariable = this.toggleVariable.bind(this); + this.variables = [ALL_INSTANCES]; + } + + handleCloseDialog () { + this.setState({ open: false }); + } + + openDialog () { + this.variables = this.getAllLoadedVariables(); + this.setState({ + open: true, + downloadError : false, + downloading : false, + downloadEnabled : this.state.typesChecked.length > 0 && this.state.selectedVariables.length > 0 + }); + } + + handleDownload () { + if ( this.state.downloading ) { + return; + } + + let json = { entries: [] }; + + this.state.selectedVariables.map( variable => { + const filemeta = this.extractVariableFileMeta(variable); + json.entries = json.entries.concat(filemeta); + }); + + json.entries.length > 0 ? this.requestZipDownload(json) : this.setState({ downloadError : true, errorMessage : this.configuration.text.noEntriesFound }); + } + + /** + * Extract filemeta from geppetto model, using variable id to find it + */ + extractVariableFileMeta (variable) { + let filemetaText = variable.filemeta?.values[0]?.value?.text; + filemetaText = filemetaText?.replace(/'/g, '"'); + + const filemetaObject = JSON.parse(filemetaText); + let filesArray = []; + + this.state.typesChecked.map( check => { + filemetaObject[check] + && filesArray.push({ + Url: filemetaObject[check]?.url, + ZipPath: filemetaObject[check]?.local, + }); + }); + + return filesArray; + } + + /** + * Get array of all loaded variables in application + */ + getAllLoadedVariables () { + let entities = GEPPETTO.ModelFactory.allPaths; + var visuals = []; + + for (var i = 0; i < entities.length; i++) { + if ( entities[i].metaType === "VisualType" || entities[i].metaType === "CompositeVisualType" ) { + const variable = entities[i]?.path?.split(".")[0]; + const instance = window.Instances[variable]; + const filemeta = instance[variable + "_meta"]?.variable?.types[0]?.filemeta; + visuals.push({ id: variable, name: instance?.name, filemeta: filemeta }); + } + } + + return visuals; + } + + /** + * Make axios call to download the zip + */ + requestZipDownload (jsonRequest) { + let self = this; + + this.setState({ downloading: true, downloadEnabled : false }); + // Axios HTTP Post request with post query + axios({ + method: "post", + url: this.configuration.postURL, + headers: { "content-type": this.configuration.contentType }, + data: jsonRequest, + responseType: "arraybuffer", + }) + .then(function (response) { + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", self.configuration.zipName); + document.body.appendChild(link); + link.click(); + setTimeout( + () => + self.setState({ + downloading: false, + open: false, + downloadEnabled : true + }), + 500 + ); + }) + .catch(function (error) { + self.setState({ + downloadError: true, + downloading: false, + errorMessage : this.props.classes.errorMessage + }); + }); + } + + /** + * Handle checkbox selection of different types to download + */ + handleTypeSelection (value) { + const currentIndex = this.state.typesChecked.indexOf(value); + const newTypesChecked = [...this.state.typesChecked]; + + if (currentIndex === -1) { + newTypesChecked.push(value); + } else { + newTypesChecked.splice(currentIndex, 1); + } + + this.setState({ typesChecked: newTypesChecked, downloadEnabled : newTypesChecked.length > 0 && this.state.selectedVariables.length > 0 }); + } + + /** + * Get variable by id, trigger by checkbox selection of variables + */ + getVariableById (nodes, id) { + let variablesMatched = []; + + if (id === ALL_INSTANCES.id) { + variablesMatched = nodes; + } else { + nodes.forEach(node => { + if (node.id === id) { + variablesMatched.push(node); + } + }); + } + + return variablesMatched; + } + + /** + * Toggle variable selection from checklist + */ + toggleVariable (checked, node) { + const allNode = this.getVariableById(this.variables, node.id); + let updatedVariables = checked + ? [...this.state.selectedVariables, ...allNode] + : this.state.selectedVariables.filter( + value => !allNode.find( node => node.id === value.id ) + ); + + updatedVariables = updatedVariables.filter((v, i) => updatedVariables.indexOf(v) === i); + + this.setState({ + selectedVariables: updatedVariables, + allVariablesSelectedFlag: updatedVariables.length > 0, + downloadEnabled : this.state.typesChecked.length > 0 && updatedVariables.length > 0 + }); + } + + render () { + let self = this; + const { idsMap } = this.props; + this.variables = this.getAllLoadedVariables(); + + return ( + + + + {this.configuration.text.title} + + + { !this.state.downloadError ? ( + + + + {this.configuration.text.typesSubtitle} + + + + {Object.keys(this.configurationOptions).map(key => { + const option = this.configurationOptions[key]; + const labelId = `checkbox-list-secondary-label-${key}`; + return ( + + + + + {`${option.label}`} + + self.handleTypeSelection(key)} + checked={this.state.typesChecked.indexOf(key) !== -1} + inputProps={{ "aria-labelledby": labelId }} + disabled={this.state.downloading} + disableRipple + className={self.props.classes.checked} + /> + + + ); + })} + + {this.variables.length > 0 ? ( + <> + + + {this.configuration.text.variablesSubtitle} + + + + } + defaultExpanded={[ALL_INSTANCES.id]} + defaultExpandIcon={} + > + e.stopPropagation()} + checked={self.state.allVariablesSelectedFlag} + onChange={event => + self.toggleVariable( + event.currentTarget.checked, + ALL_INSTANCES + ) + } + disableRipple + className={self.props.classes.checked} + /> + } + label={ + + {ALL_INSTANCES.name} + + } + key={ALL_INSTANCES.id} + /> + } + > + {this.variables.map(node => ( + e.stopPropagation()} + checked={self.state.selectedVariables.some( + item => item.id === node.id + )} + onChange={event => + self.toggleVariable( + event.currentTarget.checked, + node + ) + } + className={self.props.classes.checked} + /> + } + label={ + + {node.name} + + } + key={node.id} + /> + } + /> + ))} + + + + + ) : ( + + {this.configuration.text.noVariablesSubtitle} + + )} + + + ) + : ( + + + + + + + {this.state.errorMessage} + + + + ) + } + + + + + + { !this.state.downloadError ? ( + + + + + + + + + ) + : ( + + + + + + ) + } + + + + ); + } +} + +function mapStateToProps (state) { + return { + instanceDeleted : state.generals.ui.canvas.instanceDeleted, + instanceOnFocus : state.generals.instanceOnFocus, + idsMap : state.generals.idsMap, + idsList : state.generals.idsList + } +} + +export default connect(mapStateToProps, null, null, { forwardRef : true } )(withStyles(styles)(VFBDownloadContents)); \ No newline at end of file diff --git a/components/interface/VFBFocusTerm/VFBFocusTerm.js b/components/interface/VFBFocusTerm/VFBFocusTerm.js index 482347f9b..dd8a10262 100644 --- a/components/interface/VFBFocusTerm/VFBFocusTerm.js +++ b/components/interface/VFBFocusTerm/VFBFocusTerm.js @@ -568,6 +568,13 @@ class VFBFocusTerm extends React.Component { : } + + { + this.props.UIUpdateManager("downloadContentsVisible"); + }} /> +