diff --git a/.gitignore b/.gitignore index 72fc038c68e37795481943bec1f487767fa4d0a7..9a0dbf0d00a130dd9010fb23e0dbf6703bbfa201 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ # Node artifact files node_modules/ -dist/ +build/ # Compiled Java class files *.class diff --git a/package-lock.json b/package-lock.json index 46e84c15df21c4c853dba58c0349d8197a78cb46..4a622054b67bd072dc2dd3536b7d13bbce6bfedb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1219,6 +1219,11 @@ "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz", "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==" }, + "@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, "@eslint/eslintrc": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz", @@ -1831,6 +1836,108 @@ } } }, + "@material-ui/core": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.3.tgz", + "integrity": "sha512-Adt40rGW6Uds+cAyk3pVgcErpzU/qxc7KBR94jFHBYretU4AtWZltYcNsbeMn9tXL86jjVL1kuGcIHsgLgFGRw==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/styles": "^4.11.3", + "@material-ui/system": "^4.11.3", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.11.2", + "@types/react-transition-group": "^4.2.0", + "clsx": "^1.0.4", + "hoist-non-react-statics": "^3.3.2", + "popper.js": "1.16.1-lts", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0", + "react-transition-group": "^4.4.0" + } + }, + "@material-ui/icons": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz", + "integrity": "sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==", + "requires": { + "@babel/runtime": "^7.4.4" + } + }, + "@material-ui/lab": { + "version": "4.0.0-alpha.57", + "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.57.tgz", + "integrity": "sha512-qo/IuIQOmEKtzmRD2E4Aa6DB4A87kmY6h0uYhjUmrrgmEAgbbw9etXpWPVXuRK6AGIQCjFzV6WO2i21m1R4FCw==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + } + }, + "@material-ui/styles": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.3.tgz", + "integrity": "sha512-HzVzCG+PpgUGMUYEJ2rTEmQYeonGh41BYfILNFb/1ueqma+p1meSdu4RX6NjxYBMhf7k+jgfHFTTz+L1SXL/Zg==", + "requires": { + "@babel/runtime": "^7.4.4", + "@emotion/hash": "^0.8.0", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "csstype": "^2.5.2", + "hoist-non-react-statics": "^3.3.2", + "jss": "^10.5.1", + "jss-plugin-camel-case": "^10.5.1", + "jss-plugin-default-unit": "^10.5.1", + "jss-plugin-global": "^10.5.1", + "jss-plugin-nested": "^10.5.1", + "jss-plugin-props-sort": "^10.5.1", + "jss-plugin-rule-value-function": "^10.5.1", + "jss-plugin-vendor-prefixer": "^10.5.1", + "prop-types": "^15.7.2" + }, + "dependencies": { + "csstype": { + "version": "2.6.16", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.16.tgz", + "integrity": "sha512-61FBWoDHp/gRtsoDkq/B1nWrCUG/ok1E3tUrcNbZjsE9Cxd9yzUirjS3+nAATB8U4cTtaQmAHbNndoFz5L6C9Q==" + } + } + }, + "@material-ui/system": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.11.3.tgz", + "integrity": "sha512-SY7otguNGol41Mu2Sg6KbBP1ZRFIbFLHGK81y4KYbsV2yIcaEPOmsCK6zwWlp+2yTV3J/VwT6oSBARtGIVdXPw==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "csstype": "^2.5.2", + "prop-types": "^15.7.2" + }, + "dependencies": { + "csstype": { + "version": "2.6.16", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.16.tgz", + "integrity": "sha512-61FBWoDHp/gRtsoDkq/B1nWrCUG/ok1E3tUrcNbZjsE9Cxd9yzUirjS3+nAATB8U4cTtaQmAHbNndoFz5L6C9Q==" + } + } + }, + "@material-ui/types": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==" + }, + "@material-ui/utils": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz", + "integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==", + "requires": { + "@babel/runtime": "^7.4.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -4886,6 +4993,15 @@ } } }, + "css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "requires": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, "css-what": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", @@ -7900,6 +8016,11 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" }, + "hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -8005,6 +8126,14 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, + "indefinite-observable": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/indefinite-observable/-/indefinite-observable-2.0.1.tgz", + "integrity": "sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ==", + "requires": { + "symbol-observable": "1.2.0" + } + }, "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -8329,6 +8458,11 @@ "is-extglob": "^2.1.1" } }, + "is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=" + }, "is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -10339,6 +10473,85 @@ "verror": "1.10.0" } }, + "jss": { + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.5.1.tgz", + "integrity": "sha512-hbbO3+FOTqVdd7ZUoTiwpHzKXIo5vGpMNbuXH1a0wubRSWLWSBvwvaq4CiHH/U42CmjOnp6lVNNs/l+Z7ZdDmg==", + "requires": { + "@babel/runtime": "^7.3.1", + "csstype": "^3.0.2", + "indefinite-observable": "^2.0.1", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-camel-case": { + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.5.1.tgz", + "integrity": "sha512-9+oymA7wPtswm+zxVti1qiowC5q7bRdCJNORtns2JUj/QHp2QPXYwSNRD8+D2Cy3/CEMtdJzlNnt5aXmpS6NAg==", + "requires": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.5.1" + } + }, + "jss-plugin-default-unit": { + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.5.1.tgz", + "integrity": "sha512-D48hJBc9Tj3PusvlillHW8Fz0y/QqA7MNmTYDQaSB/7mTrCZjt7AVRROExoOHEtd2qIYKOYJW3Jc2agnvsXRlQ==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.1" + } + }, + "jss-plugin-global": { + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.5.1.tgz", + "integrity": "sha512-jX4XpNgoaB8yPWw/gA1aPXJEoX0LNpvsROPvxlnYe+SE0JOhuvF7mA6dCkgpXBxfTWKJsno7cDSCgzHTocRjCQ==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.1" + } + }, + "jss-plugin-nested": { + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.5.1.tgz", + "integrity": "sha512-xXkWKOCljuwHNjSYcXrCxBnjd8eJp90KVFW1rlhvKKRXnEKVD6vdKXYezk2a89uKAHckSvBvBoDGsfZrldWqqQ==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.1", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-props-sort": { + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.5.1.tgz", + "integrity": "sha512-t+2vcevNmMg4U/jAuxlfjKt46D/jHzCPEjsjLRj/J56CvP7Iy03scsUP58Iw8mVnaV36xAUZH2CmAmAdo8994g==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.1" + } + }, + "jss-plugin-rule-value-function": { + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.5.1.tgz", + "integrity": "sha512-3gjrSxsy4ka/lGQsTDY8oYYtkt2esBvQiceGBB4PykXxHoGRz14tbCK31Zc6DHEnIeqsjMUGbq+wEly5UViStQ==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.1", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-vendor-prefixer": { + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.5.1.tgz", + "integrity": "sha512-cLkH6RaPZWHa1TqSfd2vszNNgxT1W0omlSjAd6hCFHp3KIocSrW21gaHjlMU26JpTHwkc+tJTCQOmE/O1A4FKQ==", + "requires": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.8", + "jss": "10.5.1" + } + }, "jsx-ast-utils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.1.0.tgz", @@ -11954,6 +12167,11 @@ "ts-pnp": "^1.1.6" } }, + "popper.js": { + "version": "1.16.1-lts", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", + "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==" + }, "portfinder": { "version": "1.0.28", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", @@ -15556,6 +15774,11 @@ "util.promisify": "~1.0.0" } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/package.json b/package.json index fef55b15322cae2989d9ebf1206936db583ca439..e4fa562d022406d00b3c4776c1d57fb97ef2c884 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "version": "0.1.0", "private": true, "dependencies": { + "@material-ui/core": "4.11.3", + "@material-ui/icons": "4.11.2", + "@material-ui/lab": "4.0.0-alpha.57", "bootstrap": "4.5", "jszip": "3.2.0", "react": "^17.0.1", diff --git a/src/components/experiment-files-viewer/experiment-files-viewer.css b/src/components/experiment-files-viewer/experiment-files-viewer.css new file mode 100644 index 0000000000000000000000000000000000000000..6469a06a9079ab846f365ea26fe8519b0bf045b4 --- /dev/null +++ b/src/components/experiment-files-viewer/experiment-files-viewer.css @@ -0,0 +1,136 @@ +.experiment-files-viewer-wrapper { + height: 100vh; + margin: 20px; + display: grid; + gap: 15px; + grid-template-rows: 110px auto; + grid-template-columns: 40% auto; + grid-template-areas: + "local-directory-picker selected-file-info" + "experiment-list experiment-files"; +} + +.grid-element { + border: 1px solid lightgray; +} + +.grid-element-header { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 5px 5px 5px 15px; + margin-bottom: 5px; + width: 100%; + background-color: #048cb4; + color: white; + font-size: 1.1em; + font-weight: bold; +} + +.grid-element-header-buttons { + font-size: 0.8em; + background-color: white; +} + +.local-directory-picker { + grid-area: local-directory-picker; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.elements-local-directory { + display: flex; + flex-direction: column; + padding: 10px; +} + +.local-directory-name { + padding: 0px 10px 0px 10px; + margin: 0px 10px 0px 10px; + border: 1px solid black; + min-width: 250px; +} + +.experiment-list { + grid-area: experiment-list; +} + +.experiment-files-list { + padding-left: 5px; +} + +.experiment-files-list li { + list-style-type: none; + margin: 2px 5px 2px 5px; + padding-left: 10px; + border-bottom: 1px solid lightgray; + display: flex; + align-items: center; + justify-content: space-between; +} + +.experiment-files-list li:hover { + background-color: lightblue; +} + +.experiments-li-selected { + border-left: 5px solid #048cb4; +} + +.experiments-li-disabled { + color: gray; +} + +.experiment-li-buttons { + justify-self: flex-end; + min-width: 76px; +} + +.experiment-files { + grid-area: experiment-files; +} + +.selected-file-info { + grid-area: selected-file-info; + padding: 10px; + overflow: auto; +} + +.fileinfo-group { + padding-bottom: 5px; +} + +.fileinfo-entry { + padding-left: 5px; +} + +.fileinfo-name { + font-weight: bold; +} + +.treeview { + flex-grow: 1; + max-width: 400; + padding-left: 10px; +} + +.experiment-file-local { + color: black; +} + +.file-local-changes { + color: darkorange; +} + +.file-dirty { + color: red; +} + +.file-local-only { + color: green; +} + +.file-server-only { + color: lightgray; +} \ No newline at end of file diff --git a/src/components/experiment-files-viewer/experiment-files-viewer.js b/src/components/experiment-files-viewer/experiment-files-viewer.js new file mode 100644 index 0000000000000000000000000000000000000000..222c1600500a988be4b9c8fbc92ad449d1c465bb --- /dev/null +++ b/src/components/experiment-files-viewer/experiment-files-viewer.js @@ -0,0 +1,279 @@ +import React from 'react'; +import { FaDownload, FaUpload, FaFolderOpen, FaTrash } from 'react-icons/fa'; +import { IoSyncCircleOutline, IoSyncCircleSharp } from 'react-icons/io5'; +import TreeView from '@material-ui/lab/TreeView'; +import TreeItem from '@material-ui/lab/TreeItem'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import ChevronRightIcon from '@material-ui/icons/ChevronRight'; + +import RemoteExperimentFilesService from '../../services/experiments/files/remote-experiment-files-service'; +//import ExperimentFilesTree from './experiment-files-tree'; + +import './experiment-files-viewer.css'; + +export default class ExperimentFilesViewer extends React.Component { + constructor() { + super(); + + this.state = { + selectedExperiment: undefined, + selectedFilepaths: undefined, + autoSync: RemoteExperimentFilesService.instance.autoSync + }; + } + + /** + * Handles select events on the file tree. + * @param {Event} event - select event + * @param {Array} nodeIds - tree node IDs, in this case we use file relative paths (= server file UUID) + */ + handleFileTreeSelect(event, nodeIds) { + this.setState({selectedFilepaths: nodeIds}); + } + + /** + * JSX for the file hierarchie of experiments. + * @param {Object} file - A file/folder with children to be displayed + * @returns {JSX} The JSX elements + */ + renderFileTree(file) { + let className = ''; + if (file.hasLocalChanges) { + className += ' file-local-changes'; + } + if (file.isOutOfSync) { + className += ' file-dirty'; + } + if (file.localOnly) { + className += ' file-local-only'; + } + if (!RemoteExperimentFilesService.instance.mapLocalFiles.has(file.relativePath)) { + className += ' file-server-only'; + } + className = className.trim(); + + return ( + <TreeItem key={file.relativePath} nodeId={file.relativePath} label={file.name} + className={className}> + {Array.isArray(file.children) ? file.children.map((subfile) => this.renderFileTree(subfile)) : null} + </TreeItem>); + } + + getExperimentsListItemClass(experiment) { + let className = ''; + if (this.state.selectedExperiment && this.state.selectedExperiment.id === experiment.id) { + className += ' experiments-li-selected'; + } + if (!RemoteExperimentFilesService.instance.mapFileInfos.has(experiment.uuid)) { + className += ' experiments-li-disabled'; + } + + return className.trim(); + } + + getInfoText() { + return (<div> + {this.state.selectedFilepaths && this.state.selectedFilepaths.map(filePath => { + let fileInfo = RemoteExperimentFilesService.instance.mapFileInfos.get(filePath); + if (fileInfo) { + return (<div key={fileInfo.relativePath} className="fileinfo-group"> + <div className="fileinfo-name">{fileInfo.name}</div> + {fileInfo.msgWarning && <div className="fileinfo-entry">{'Warning: ' + fileInfo.msgWarning}</div>} + {fileInfo.msgError && <div className="fileinfo-entry">{'Error: ' + fileInfo.msgError}</div>} + {fileInfo.localOnly && <div className="fileinfo-entry">{'File exists only locally.'}</div>} + {fileInfo.serverOnly && + <div className="fileinfo-entry">{'File exists only on server.'}</div>} + {fileInfo.hasLocalChanges && + <div className="fileinfo-entry">{'File has local changes not synced with server.'}</div>} + {fileInfo.isOutOfSync && + <div className="fileinfo-entry">{'File on server has newer changes.'}</div>} + </div>); + } + else { + return null; + } + })} + </div>); + } + + render() { + let selectedExperimentFiles = this.state.selectedExperiment ? + RemoteExperimentFilesService.instance.mapFileInfos.get(this.state.selectedExperiment.uuid) : undefined; + + return ( + <div> + {RemoteExperimentFilesService.instance.isSupported() ? + <div className='experiment-files-viewer-wrapper'> + {/* choose the local parent directory for experiment files */} + <div className='grid-element local-directory-picker'> + <div className='grid-element-header'> + <div>Local working directory</div> + <div className='grid-element-header-buttons'> + <button className='nrp-btn' + onClick={() => { + RemoteExperimentFilesService.instance.chooseLocalSyncDirectory(); + }} + title='This is your local directory to work in. All your experiment files will go here. + Usually, you would want to always pick the same folder. + If you have previously chosen a different folder, any changes made there will not be listed.' + > + <FaFolderOpen /> + </button> + </div> + + </div> + + <div className='elements-local-directory'> + <div></div> + <div className='local-directory-name'> + {RemoteExperimentFilesService.instance.localSyncDirectoryHandle ? + <span> + {RemoteExperimentFilesService.instance.localSyncDirectoryHandle.name} + </span> + : <span style={{color: 'gray'}}>Please choose local folder you want to work in.</span>} + </div> + + </div> + </div> + + {/* list of experiments */} + <div className='grid-element experiment-list'> + <div className='grid-element-header'> + <div>Experiments</div> + <div className='grid-element-header-buttons'> + <button className='nrp-btn' + onClick={() => { + RemoteExperimentFilesService.instance.toggleAutoSync(); + this.setState({autoSync: RemoteExperimentFilesService.instance.autoSync}); + }} + title={this.state.autoSync ? 'Auto sync: ON' : 'Auto sync: OFF'} + > + {RemoteExperimentFilesService.instance.autoSync ? + <IoSyncCircleSharp /> : <IoSyncCircleOutline />} + </button> + </div> + </div> + + <ol className='experiment-files-list'> + {this.props.experiments.map(experiment => { + let experimentServerFiles = RemoteExperimentFilesService.instance + .mapServerFiles.get(experiment.uuid); + let experimentLocalFiles = RemoteExperimentFilesService.instance.mapLocalFiles.get(experiment.uuid); + + return ( + <li key={experiment.id || experiment.configuration.id} + className={this.getExperimentsListItemClass(experiment)} + onClick={() => { + if (experimentLocalFiles) { + this.setState({ + selectedExperiment: experiment, + selectedFilepaths: undefined + }); + } + }}> + {experiment.configuration.name} + <div className='experiment-li-buttons'> + <button className='nrp-btn' + disabled={!RemoteExperimentFilesService.instance.localSyncDirectoryHandle + || RemoteExperimentFilesService.instance.autoSync} + onClick={() => { + RemoteExperimentFilesService.instance.downloadExperimentToLocalFS(experiment); + }} + title='Download all experiment files (will OVERWRITE unsaved local changes)' + > + <FaDownload /> + </button> + <button className='nrp-btn' + disabled={!experimentServerFiles + || !RemoteExperimentFilesService.instance.mapFileInfos.has(experiment.uuid) + || RemoteExperimentFilesService.instance.autoSync} + onClick={() => { + RemoteExperimentFilesService.instance.uploadExperimentFromLocalFS(experiment); + }} + title='Upload all experiment files' + > + <FaUpload /> + </button> + </div> + </li> + ); + })} + </ol> + </div> + + {/* file structure for selected experiment */} + <div className='grid-element experiment-files'> + <div className='grid-element-header'> + <div>Experiment Files</div> + <div className='grid-element-header-buttons'> + <button className='nrp-btn' title='Download selected' + disabled={!this.state.selectedFilepaths || this.state.selectedFilepaths.length === 0 + || RemoteExperimentFilesService.instance.autoSync} + onClick={() => + RemoteExperimentFilesService.instance.downloadExperimentFileList(this.state.selectedFilepaths)}> + <FaDownload /> + </button> + <button className='nrp-btn' title='Upload selected' + disabled={!this.state.selectedFilepaths || this.state.selectedFilepaths.length === 0 + || RemoteExperimentFilesService.instance.autoSync} + onClick={() => + RemoteExperimentFilesService.instance.uploadExperimentFileList(this.state.selectedFilepaths)}> + <FaUpload /> + </button> + <button className='nrp-btn' title='Delete selected' + disabled={!this.state.selectedFilepaths || this.state.selectedFilepaths.length === 0 + || RemoteExperimentFilesService.instance.autoSync} + onClick={() => + RemoteExperimentFilesService.instance.deleteExperimentFileList(this.state.selectedFilepaths)}> + <FaTrash /> + </button> + </div> + </div> + + <div> + {selectedExperimentFiles ? + <TreeView + multiSelect + className="treeview" + defaultCollapseIcon={<ExpandMoreIcon />} + defaultExpandIcon={<ChevronRightIcon />} + defaultExpanded={this.props.experiments.map(experiment => experiment.uuid)} + onNodeSelect={(event, nodeIds) => { + this.handleFileTreeSelect(event, nodeIds); + }} + > + {this.renderFileTree(selectedExperimentFiles)} + </TreeView> + : <span style={{margin: '20px'}}>Please select an experiment on the left first.</span> + } + </div> + </div> + + {/* info for selected file */} + <div className='grid-element selected-file-info'> + {this.getInfoText()} + </div> + </div> + + /* error notification for browser other than chrome */ + : <div> + File System Access API is a working draft and not supported by this browser at the moment. + Please try one of the supporting browsers. + <br /> + <a target='_blank' rel='noreferrer' + href='https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API'> + File System Access API + </a> + <br /> + <a target='_blank' rel='noreferrer' href='https://wicg.github.io/file-system-access/'> + W3C Draft + </a> + <br /> + <a target='_blank' rel='noreferrer' href='https://caniuse.com/?search=file%20system%20access'> + caniuse.com + </a> + </div>} + </div> + ); + } +} diff --git a/src/components/experiment-overview/experiment-overview.js b/src/components/experiment-overview/experiment-overview.js index afa1f1becfd18912770efcf1a67559d89472b2a9..43bc7ff7a7580d9f6e21c9921b06952316e014c7 100644 --- a/src/components/experiment-overview/experiment-overview.js +++ b/src/components/experiment-overview/experiment-overview.js @@ -10,6 +10,7 @@ import ExperimentExecutionService from '../../services/experiments/execution/exp import ImportExperimentButtons from '../experiment-list/import-experiment-buttons.js'; import ExperimentList from '../experiment-list/experiment-list.js'; import NrpHeader from '../nrp-header/nrp-header.js'; +import ExperimentFilesViewer from '../experiment-files-viewer/experiment-files-viewer.js'; import './experiment-overview.css'; @@ -144,7 +145,7 @@ export default class ExperimentOverview extends React.Component { </TabPanel> {/* Experiment Files */} <TabPanel> - <h2>"Experiment Files" tab coming soon ...</h2> + <ExperimentFilesViewer experiments={this.state.storageExperiments}/> </TabPanel> {/* Templates */} <TabPanel> diff --git a/src/components/main.css b/src/components/main.css index cb0a265943e91b921da9d548b419be8b956765b8..404747e79aae8232ef495bb6a300bceae38c1aa4 100644 --- a/src/components/main.css +++ b/src/components/main.css @@ -1,4 +1,9 @@ .nrp-btn { padding: 5px 10px 5px 10px; border: 1px solid lightgray; +} + +.nrp-btn-small { + width: 25px; + height: 25px; } \ No newline at end of file diff --git a/src/services/experiments/execution/__tests__/experiment-execution-service.test.js b/src/services/experiments/execution/__tests__/experiment-execution-service.test.js index f34a08dce1e9e5b5d9c118160e5c970872182e71..dbd8d11eb512ffe8701bf1a4cfd0a10d0b89c185 100644 --- a/src/services/experiments/execution/__tests__/experiment-execution-service.test.js +++ b/src/services/experiments/execution/__tests__/experiment-execution-service.test.js @@ -78,7 +78,6 @@ test('should go through the list of available servers when trying to start an ex MockAvailableServers.forEach(server => { expect(ServerResourcesService.instance.getServerConfig).toHaveBeenCalledWith(server.id); }); - expect(console.error).toHaveBeenCalled(); done(); }); }); diff --git a/src/services/experiments/execution/__tests__/running-simulation-service.test.js b/src/services/experiments/execution/__tests__/running-simulation-service.test.js index 9492c9fa6d1d1a2ea97af49449db67f08df23e7d..7af3b6e72eb1dbc0f5b53d53d0a9ffd960e99855 100644 --- a/src/services/experiments/execution/__tests__/running-simulation-service.test.js +++ b/src/services/experiments/execution/__tests__/running-simulation-service.test.js @@ -166,7 +166,7 @@ test('can retrieve the state of a simulation', async () => { test('can set the state of a simulation', async () => { let returnValuePUT = undefined; - jest.spyOn(ErrorHandlerService.instance, 'updateSimuationError').mockImplementation(); + jest.spyOn(ErrorHandlerService.instance, 'updateSimulationError').mockImplementation(); jest.spyOn(RunningSimulationService.instance, 'httpRequestPUT').mockImplementation(() => { if (RunningSimulationService.instance.httpRequestGET.mock.calls.length === 1) { returnValuePUT = {}; diff --git a/src/services/experiments/files/experiment-storage-service.js b/src/services/experiments/files/experiment-storage-service.js index a02538e76345527b61f35ea8e75d8b790b107553..0cf994dac08ab916701831c86cfe1e71e75fe1f8 100644 --- a/src/services/experiments/files/experiment-storage-service.js +++ b/src/services/experiments/files/experiment-storage-service.js @@ -147,25 +147,29 @@ class ExperimentStorageService extends HttpService { /** * Gets an experiment file from the storage. - * @param {string} experimentName - name of the experiment + * @param {string} experimentDirectoryPath - path of experiment folder + possibly subfolders * @param {string} filename - name of the file * @param {Boolean} byName - whether to check for the file by name or not * * @returns the file contents (as a request object) */ - async getFile(experimentName, filename, byName = false) { - const url = `${config.api.proxy.url}${endpoints.proxy.storage.url}/${experimentName}/${filename}?byname=${byName}`; + async getFile(experimentDirectoryPath, filename, byName = false) { + let directory = experimentDirectoryPath.replaceAll('/', '%2F'); + let file = filename.replaceAll('/', '%2F'); + const url = `${config.api.proxy.url}${endpoints.proxy.storage.url}/${directory}/${file}?byname=${byName}`; return this.httpRequestGET(url); } /** * Gets the list of the experiment files from the storage. - * @param {string} experimentName - name of the experiment + * @param {string} experimentDirectoryUUID - name of the experiment + * @param {string} subFolder - relative path to a subfolder from which to get files * * @returns {Array} the list of experiment files */ - async getExperimentFiles(experimentName) { - const url = `${config.api.proxy.url}${endpoints.proxy.storage.url}/${experimentName}`; + async getExperimentFiles(directoryPath) { + let directory = directoryPath.replaceAll('/', '%2F'); + let url = `${config.api.proxy.url}${endpoints.proxy.storage.url}/${directory}`; const files = await (await this.httpRequestGET(url)).json(); return files; } @@ -246,24 +250,27 @@ class ExperimentStorageService extends HttpService { * * @returns the request object containing the status code */ - async setFile(experimentName, filename, data, byname = true, contentType = 'text/plain') { - const url = new URL(`${config.api.proxy.url}${endpoints.proxy.storage.url}/${experimentName}/${filename}`); + async setFile(directoryPath, filename, data, byname = true, contentType = 'text/plain') { + let directory = directoryPath.replaceAll('/', '%2F'); + const url = new URL(`${config.api.proxy.url}${endpoints.proxy.storage.url}/${directory}/${filename}`); + //console.info(url); url.searchParams.append('byname', byname); let requestOptions = { ...this.POSTOptions, ...{ headers: { 'Content-Type': contentType } } }; + //console.info(requestOptions); if (contentType === 'text/plain') { - return this.httpRequestPOST(url, requestOptions, data); + return this.httpRequestPOST(url, data, requestOptions); } else if (contentType === 'application/json') { - return this.httpRequestPOST(url, requestOptions, JSON.stringify(data)); + return this.httpRequestPOST(url, JSON.stringify(data), requestOptions); } else if (contentType === 'application/octet-stream') { // placeholder for blob files where the data has to be transormed, // possibly to Uint8Array - return this.httpRequestPOST(url, requestOptions,/* new Uint8Array(data) */data); + return this.httpRequestPOST(url,/* new Uint8Array(data) */data, requestOptions); } else { return new Error('Content-Type for setFile request not specified,' + diff --git a/src/services/experiments/files/remote-experiment-files-service.js b/src/services/experiments/files/remote-experiment-files-service.js new file mode 100644 index 0000000000000000000000000000000000000000..c9625dfd3e824321a419847c877322edb857708e --- /dev/null +++ b/src/services/experiments/files/remote-experiment-files-service.js @@ -0,0 +1,467 @@ +import { HttpService } from '../../http-service.js'; +import ExperimentStorageService from './experiment-storage-service'; +import getMimeByExtension from '../../../utility/mime-type'; + +let _instance = null; +const SINGLETON_ENFORCER = Symbol(); + +const LOCALSTORAGE_KEY_FILE_INFO = 'NRP-remote-experiment-files_local-files'; +const FS_TYPE_FILE = 'file'; +const FS_TYPE_DIRECTORY = 'directory'; +const SERVER_FILE_TYPE_DIRECTORY = 'folder'; + +/** + * Provides functionality to mirror (up-/download) and manage experiment files locally. + */ +class RemoteExperimentFilesService extends HttpService { + constructor(enforcer) { + super(); + if (enforcer !== SINGLETON_ENFORCER) { + throw new Error('Use ' + this.constructor.name + '.instance'); + } + + this.localSyncDirectoryHandle = undefined; + this.mapLocalFiles = new Map(); + this.mapServerFiles = new Map(); + this.mapFileInfos = new Map(); + this.autoSync = false; + } + + static get instance() { + if (_instance == null) { + _instance = new RemoteExperimentFilesService(SINGLETON_ENFORCER); + } + + return _instance; + } + + isSupported() { + return window.showDirectoryPicker !== undefined && window.showDirectoryPicker !== null; + } + + toggleAutoSync() { + this.autoSync = !this.autoSync; + } + + async chooseLocalSyncDirectory() { + this.localSyncDirectoryHandle = await window.showDirectoryPicker(); + + if (this.localSyncDirectoryHandle) { + this.initLocalFileInfoFromLocalStorage(); + await this.updateFileLists(); + } + + this.intervalCheckLocalFiles = setInterval(async () => { + await this.updateFileLists(); + }, RemoteExperimentFilesService.CONSTANTS.INTERVAL_UPDATE_FILES); + } + + async updateFileLists() { + if (!this.localSyncDirectoryHandle) { + return; + } + + await this.updateServerFiles(); + + await this.updateLocalFiles(); + + await this.updateFileInfos(); + + this.saveLocalFileInfoToLocalStorage(); + } + + async updateLocalFiles() { + let updatedFilePaths = []; + + await this.traverseFilesystem(this.localSyncDirectoryHandle, + async (fileSystemHandle) => { + let fileRelativePath = await this.getRelativePathFromFSHandle(fileSystemHandle); + let file = await this.getOrCreateLocalFile(fileRelativePath, fileSystemHandle.kind, fileSystemHandle); + file && updatedFilePaths.push(file.relativePath); + } + ); + + // get rid of map entries that have been deleted in FS + for (let entry of this.mapLocalFiles) { + const relativePath = entry[0]; + if (!updatedFilePaths.includes(relativePath)) { + this.mapLocalFiles.delete(relativePath); + } + } + } + + /** + * + * @param {*} directoryHandle + * @param {*} callbackFile - callback function called with (fileRelativePath, fileSystemHandle) + * @returns + */ + async traverseFilesystem(directoryHandle, callbackFile) { + if (!directoryHandle) { + return; + } + + let traverseFolder = async (directoryHandle) => { + let iterator = directoryHandle.values(); + let result = await iterator.next(); + while (!result.done) { + let fileSystemHandle = result.value; + + callbackFile && await callbackFile(fileSystemHandle); + + if (fileSystemHandle.kind === 'directory') { + await traverseFolder(fileSystemHandle); + } + + result = await iterator.next(); + } + }; + await traverseFolder(directoryHandle); + } + + async getOrCreateLocalFile(relativePath, type, fileSystemHandle = undefined) { + if (!relativePath || relativePath.length === 0) { + return; + } + + let fileName = this.getFileNameFromRelativePath(relativePath); + if (fileName.charAt(0) === '.' || fileName.includes('.crswap')) { + return; + } + + this.getOrCreateFileInfo(relativePath, type); + let localFile = this.mapLocalFiles.get(relativePath); + if (!localFile) { + localFile = { + name: fileName, + relativePath: relativePath, + fileSystemHandle: fileSystemHandle + }; + this.mapLocalFiles.set(relativePath, localFile); + } + + localFile.fileSystemHandle = localFile.fileSystemHandle || fileSystemHandle; + if (!localFile.fileSystemHandle) { + let parentDirectory = undefined; + let lastIndexSlash = relativePath.lastIndexOf('/'); + if (lastIndexSlash && lastIndexSlash !== -1) { + let parentDirectoryPath = this.getParentDirectoryFromRelativePath(relativePath); + parentDirectory = await this.getOrCreateLocalFile(parentDirectoryPath, FS_TYPE_DIRECTORY); + } + let parentDirectoryHandle = parentDirectory ? parentDirectory.fileSystemHandle : this.localSyncDirectoryHandle; + + if (type === FS_TYPE_FILE) { + localFile.fileSystemHandle = await parentDirectoryHandle.getFileHandle(fileName, {create: true}); + } + else if (type === FS_TYPE_DIRECTORY) { + localFile.fileSystemHandle = await parentDirectoryHandle.getDirectoryHandle(fileName, {create: true}); + } + } + + return localFile; + } + + async updateServerFiles(forceUpdate = false) { + let newServerFilesMap = new Map(); + + let getServerDirectoryFiles = async (parentDirectory) => { + let serverFileList = await ExperimentStorageService.instance.getExperimentFiles(parentDirectory.uuid); + parentDirectory.children = serverFileList; + + for (let serverFile of serverFileList) { + newServerFilesMap.set(serverFile.uuid, serverFile); + if (!this.mapLocalFiles.has(serverFile.uuid)) { + this.getOrCreateFileInfo(serverFile.uuid, serverFile.type); + } + + try { + serverFile.parent = parentDirectory; + if (serverFile.type === SERVER_FILE_TYPE_DIRECTORY) { + await getServerDirectoryFiles(serverFile); + } + } + catch (error) { + console.error(error); + } + } + }; + + let experiments = await ExperimentStorageService.instance.getExperiments(forceUpdate); + for (let experiment of experiments) { + let serverExperiment = { + uuid: experiment.uuid, + name: experiment.configuration.name + }; + await getServerDirectoryFiles(serverExperiment); + + newServerFilesMap.set(experiment.uuid, serverExperiment); + } + + this.mapServerFiles = newServerFilesMap; + } + + async updateFileInfos() { + for (let keyValueEntry of this.mapFileInfos) { + const relativePath = keyValueEntry[0]; + let fileInfo = keyValueEntry[1]; + const localFile = this.mapLocalFiles.get(relativePath); + const serverFile = this.mapServerFiles.get(relativePath); + + if (!localFile) { + if (!serverFile) { + // local file has been deleted and is not present on server, remove from list + this.removeFileInfo(relativePath); + } + else { + // local file has been deleted, but is part of the experiment files on server + if (this.autoSync && fileInfo.type === FS_TYPE_FILE) { + await this.downloadExperimentFile(relativePath); + } + } + } + + if (localFile && localFile.fileSystemHandle && localFile.fileSystemHandle.kind === FS_TYPE_FILE) { + fileInfo.hasLocalChanges = await this.hasLocalChanges(relativePath); + fileInfo.isOutOfSync = this.isOutOfSync(relativePath); + fileInfo.localOnly = !this.mapServerFiles.has(relativePath); + fileInfo.serverOnly = !this.mapLocalFiles.has(relativePath); + + if (this.autoSync) { + if (fileInfo.hasLocalChanges || fileInfo.localOnly) { + await this.uploadExperimentFile(relativePath); + } + + if (fileInfo.isOutOfSync) { + await this.downloadExperimentFile(relativePath); + } + } + } + } + } + + getOrCreateFileInfo(relativePath, type) { + let fileName = this.getFileNameFromRelativePath(relativePath); + if (fileName.charAt(0) === '.' || fileName.includes('.crswap')) { + return; + } + + let fileInfo = this.mapFileInfos.get(relativePath); + if (!fileInfo) { + fileInfo = {}; + this.mapFileInfos.set(relativePath, fileInfo); + } + fileInfo.name = fileInfo.name || fileName; + fileInfo.type = fileInfo.type || type; + fileInfo.relativePath = fileInfo.relativePath || relativePath; + + let parentDirectoryPath = this.getParentDirectoryFromRelativePath(relativePath); + let parentDirectory = this.mapFileInfos.get(parentDirectoryPath); + if (!parentDirectory && parentDirectoryPath.length > 0) { + parentDirectory = this.getOrCreateFileInfo(parentDirectoryPath, FS_TYPE_DIRECTORY); + } + if (parentDirectory) { + fileInfo.parent = parentDirectory; + parentDirectory.children = parentDirectory.children || []; + if (!parentDirectory.children.includes(fileInfo)) { + parentDirectory.children.push(fileInfo); + } + } + + return fileInfo; + } + + removeFileInfo(relativePath) { + let file = this.mapFileInfos.get(relativePath); + if (!file) { + return; + } + + if (file.parent) { + file.parent.children = file.parent.children.filter(child => child.relativePath !== relativePath); + } + + this.mapFileInfos.delete(relativePath); + } + + initLocalFileInfoFromLocalStorage() { + this.mapFileInfos = new Map(JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_FILE_INFO))); + } + + saveLocalFileInfoToLocalStorage() { + let mapStorage = new Map(); + for (let keyValuePair of this.mapFileInfos) { + let relativePath = keyValuePair[0]; + let file = keyValuePair[1]; + mapStorage.set(relativePath, { + name: file.name, + relativePath: file.relativePath, + type: file.type, + dateSync: file.dateSync + }); + } + + localStorage.setItem(LOCALSTORAGE_KEY_FILE_INFO, JSON.stringify(Array.from(mapStorage.entries()))); + } + + async hasLocalChanges(relativePath) { + let fileInfo = this.mapFileInfos.get(relativePath); + let localFile = this.mapLocalFiles.get(relativePath); + if (!localFile || !localFile.fileSystemHandle || !fileInfo || !fileInfo.dateSync) { + return undefined; + } + return (fileInfo.dateSync < (await localFile.fileSystemHandle.getFile()).lastModified); + } + + isOutOfSync(relativePath) { + let fileInfo = this.mapFileInfos.get(relativePath); + let serverFile = this.mapServerFiles.get(relativePath); + return serverFile && fileInfo.dateSync && Date.parse(serverFile.modifiedOn) > fileInfo.dateSync; + } + + async downloadExperimentFile(relativeFilepath) { + let localFile = await this.getOrCreateLocalFile(relativeFilepath, FS_TYPE_FILE); + let fileInfo = this.mapFileInfos.get(relativeFilepath); + + let parentDirectoryPath = this.getParentDirectoryFromRelativePath(relativeFilepath); + let fileContent = await ExperimentStorageService.instance.getBlob(parentDirectoryPath, relativeFilepath, false); + let writable = await localFile.fileSystemHandle.createWritable(); + await writable.write(fileContent); + await writable.close(); + fileInfo.dateSync = (await localFile.fileSystemHandle.getFile()).lastModified; + } + + async downloadExperimentFileList(fileList) { + for (const filepath of fileList) { + await this.downloadExperimentFile(filepath); + } + } + + async downloadExperimentToLocalFS(experiment) { + if (!this.localSyncDirectoryHandle) { + return; + } + + let experimentRootDirectory = await this.getOrCreateLocalFile(experiment.id, FS_TYPE_DIRECTORY); + if (!experimentRootDirectory) { + return; + } + + let downloadFiles = async (parentDirectory) => { + let serverFileList = await ExperimentStorageService.instance.getExperimentFiles(parentDirectory.uuid); + parentDirectory.children = serverFileList; + + for (let serverFile of serverFileList) { + serverFile.parent = parentDirectory; + if (serverFile.type === FS_TYPE_FILE) { + await this.downloadExperimentFile(serverFile.uuid); + } + else if (serverFile.type === SERVER_FILE_TYPE_DIRECTORY) { + await this.getOrCreateLocalFile(serverFile.uuid, FS_TYPE_DIRECTORY); + await downloadFiles(serverFile); + } + } + }; + + let serverExperiment = { + uuid: experiment.uuid, + name: experiment.configuration.name + }; + await downloadFiles(serverExperiment); + + this.mapServerFiles.set(experiment.id, serverExperiment); + } + + async uploadExperimentFile(relativePath) { + let localFile = this.mapLocalFiles.get(relativePath); + let fileInfo = this.mapFileInfos.get(relativePath); + if (this.isOutOfSync(relativePath)) { + //TODO: error GUI + console.warn('WARNING! ' + fileInfo.name + ' has a newer version on the server, won\'t upload'); + fileInfo.msgError = 'Won\'t upload - file version on server is newer!'; + } + else { + let fileHandle = localFile && localFile.fileSystemHandle; + if (!fileHandle) { + console.warn('Could not upload ' + relativePath + ' - missing file handle.'); + return; + } + + let localFileData = await fileHandle.getFile(); + let fileExtension = fileInfo.name.substring(fileHandle.name.lastIndexOf('.') + 1); + let contentType = getMimeByExtension(fileExtension); + + let parentDirectoryPath = this.getParentDirectoryFromRelativePath(relativePath); + let response = await ExperimentStorageService.instance.setFile( + parentDirectoryPath, fileHandle.name, localFileData, true, contentType); + if (response.status === 200) { + fileInfo.dateSync = Date.now().valueOf(); + } + } + } + + async uploadExperimentFileList(fileList) { + for (const filepath of fileList) { + await this.uploadExperimentFile(filepath); + } + } + + uploadExperimentFromLocalFS(experiment) { + let uploadFolder = async (folder) => { + for (let file of folder.children) { + /*let localFile = this.mapLocalFiles.get(file.relativePath); + if (localFile && localFile.fileSystemHandle && localFile.fileSystemHandle.kind === FS_TYPE_FILE) { + await this.uploadExperimentFile(this.getRelativePathFromFSHandle(localFile.fileSystemHandle)); + } + else if (localFile && localFile.fileSystemHandle && localFile.type === FS_TYPE_DIRECTORY) { + uploadFolder(file); + }*/ + if (file.type === FS_TYPE_FILE) { + await this.uploadExperimentFile(file.relativePath); + } + else if (file.type === FS_TYPE_DIRECTORY) { + uploadFolder(file); + } + } + }; + + let localExperimentFiles = this.mapFileInfos.get(experiment.uuid); + uploadFolder(localExperimentFiles); + } + + deleteExperimentFile(relativePath) { + let experimentName = this.getExperimentNameFromRelativePath(relativePath); + let serverFile = this.mapServerFiles.get(relativePath); + ExperimentStorageService.instance.deleteEntity(experimentName, relativePath, true, serverFile.type); + } + + deleteExperimentFileList(fileList) { + for (const relativePath of fileList) { + this.deleteExperimentFile(relativePath); + } + } + + getExperimentNameFromRelativePath(relativePath) { + return relativePath.substring(0, relativePath.indexOf('/')); + } + + getFileNameFromRelativePath(relativePath) { + return relativePath.substring(relativePath.lastIndexOf('/') + 1); + } + + getParentDirectoryFromRelativePath(relativePath) { + return relativePath.substring(0, relativePath.lastIndexOf('/')); + } + + async getRelativePathFromFSHandle(fileSystemHandle) { + let filePathArray = await this.localSyncDirectoryHandle.resolve(fileSystemHandle); + let fileRelativePath = filePathArray.join('/'); + + return fileRelativePath; + } +} + +RemoteExperimentFilesService.CONSTANTS = Object.freeze({ + INTERVAL_UPDATE_FILES: 1000 +}); + +export default RemoteExperimentFilesService; diff --git a/src/utility/mime-type.js b/src/utility/mime-type.js new file mode 100644 index 0000000000000000000000000000000000000000..078f39cd639b57b95d3c8dbda024e8239d61a8fc --- /dev/null +++ b/src/utility/mime-type.js @@ -0,0 +1,43 @@ +/**---LICENSE-BEGIN - DO NOT CHANGE OR MOVE THIS HEADER + * This file is part of the Neurorobotics Platform software + * Copyright (C) 2014,2015,2016,2017 Human Brain Project + * https://www.humanbrainproject.eu + * + * The Human Brain Project is a European Commission funded project + * in the frame of the Horizon2020 FET Flagship plan. + * http://ec.europa.eu/programmes/horizon2020/en/h2020-section/fet-flagships + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * ---LICENSE-END**/ + +const MAP_MIME_TYPES = { + 'bibi': 'text/plain', + 'exc': 'text/plain', + 'ini': 'text/plain', + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'json': 'application/json', + 'png': 'image/png', + 'py': 'text/plain', + 'sdf': 'text/plain', + 'uis': 'text/plain' +}; + +export default function getMimeByExtension(fileExtension) { + if (MAP_MIME_TYPES.hasOwnProperty(fileExtension)) { + return MAP_MIME_TYPES[fileExtension]; + } + return undefined; +} \ No newline at end of file