diff --git a/.gitignore b/.gitignore index dad6f4459a35afaef8598b5dac3f1f3104202a1c..72fc038c68e37795481943bec1f487767fa4d0a7 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,4 @@ Thumbs.db *.wmv src/config.json -coverage \ No newline at end of file +coverage diff --git a/package-lock.json b/package-lock.json index 8fec080b4db8eb3bad7954ba1de1f41bfcb3adf0..08e2fbb1ebb9eae559d2097f37b4779463c9840a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1896,9 +1896,9 @@ } }, "@popperjs/core": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.6.0.tgz", - "integrity": "sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw==" + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz", + "integrity": "sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==" }, "@restart/context": { "version": "2.1.4", @@ -2334,9 +2334,12 @@ } }, "@types/classnames": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.11.tgz", - "integrity": "sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==", + "requires": { + "classnames": "*" + } }, "@types/cookie": { "version": "0.4.0", @@ -2462,18 +2465,19 @@ "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==" }, "@types/react": { - "version": "16.14.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.3.tgz", - "integrity": "sha512-zPrXn03hmPYqh9DznqSFQsoRtrQ4aHgnZDO+hMGvsE/PORvDTdJCHQ6XvJV31ic+0LzF73huPFXUb++W6Kri0Q==", + "version": "16.14.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.5.tgz", + "integrity": "sha512-YRRv9DNZhaVTVRh9Wmmit7Y0UFhEVqXqCSw3uazRWMxa2x85hWQZ5BN24i7GXZbaclaLXEcodEeIHsjBA8eAMw==", "requires": { "@types/prop-types": "*", + "@types/scheduler": "*", "csstype": "^3.0.2" } }, "@types/react-transition-group": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz", - "integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz", + "integrity": "sha512-vIo69qKKcYoJ8wKCJjwSgCTM+z3chw3g18dkrDfVX665tMH7tmbDxEAnPdey4gTlwZz5QuHGzd+hul0OVZDqqQ==", "requires": { "@types/react": "*" } @@ -2486,6 +2490,11 @@ "@types/node": "*" } }, + "@types/scheduler": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz", + "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==" + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -4258,9 +4267,9 @@ } }, "classnames": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" }, "clean-css": { "version": "4.2.3", @@ -5055,9 +5064,9 @@ } }, "csstype": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", - "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==" + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", + "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==" }, "cyclist": { "version": "1.0.1", @@ -10501,9 +10510,9 @@ "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash-es": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.20.tgz", - "integrity": "sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, "lodash._reinterpolate": { "version": "3.0.0", diff --git a/public/index.html b/public/index.html index 8c1f6cb3cad46961473c1190c81023b095cbf282..dd172158746c1c6a64bd030dc00ee0ab7f45c857 100644 --- a/public/index.html +++ b/public/index.html @@ -3,6 +3,7 @@ <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> + <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous"> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta diff --git a/src/App.js b/src/App.js index 096f392f8a5c1ad32cca31e62b9446b59507f119..2fb35e283068de1e0c23590a711bea9af3853f4e 100644 --- a/src/App.js +++ b/src/App.js @@ -3,19 +3,23 @@ import React from 'react'; import { HashRouter, Switch, Route } from 'react-router-dom'; import EntryPage from './components/entry-page/entry-page'; +import ErrorDialog from './components/dialog/error-dialog.js'; import ExperimentOverview from './components/experiment-overview/experiment-overview'; import SimulationView from './components/simulation-view/simulation-view'; class App extends React.Component { render() { return ( - <HashRouter> - <Switch> - <Route path='/experiments-overview' component={ExperimentOverview} /> - <Route path='/simulation-view/:simID' component={SimulationView} /> - <Route path='/' component={EntryPage} /> - </Switch> - </HashRouter> + <div> + <ErrorDialog /> + <HashRouter> + <Switch> + <Route path='/experiments-overview' component={ExperimentOverview} /> + <Route path='/simulation-view/:simID' component={SimulationView} /> + <Route path='/' component={EntryPage} /> + </Switch> + </HashRouter> + </div> ); } } diff --git a/src/components/dialog/error-dialog.css b/src/components/dialog/error-dialog.css new file mode 100644 index 0000000000000000000000000000000000000000..f375b00076b3628c076a8277c57a62c9cac15b47 --- /dev/null +++ b/src/components/dialog/error-dialog.css @@ -0,0 +1,16 @@ +.modal-dialog-wrapper { + position: fixed; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + z-index: 999; +} + +.modal-header{ + background-color: rgb(241, 185, 185); + color: rgb(138, 41, 41); +} + +.details{ + clear: left; +} \ No newline at end of file diff --git a/src/components/dialog/error-dialog.js b/src/components/dialog/error-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..ee0dc909a18e53e514605e919f09e0550231e6c5 --- /dev/null +++ b/src/components/dialog/error-dialog.js @@ -0,0 +1,91 @@ +import React from 'react'; +import { Modal, Button } from 'react-bootstrap'; + +import ErrorHandlerService from '../../services/error-handler-service.js'; + +import './error-dialog.css'; + +class ErrorDialog extends React.Component{ + constructor(props) { + super(props); + this.state = { + error: undefined, + isErrorSourceDisplayed: false + }; + } + + async componentDidMount() { + ErrorHandlerService.instance.addListener( + ErrorHandlerService.EVENTS.ERROR, (error) => { + this.onError(error); + }); + } + + onError(error) { + this.setState({ + error: error + }); + } + + handleClose() { + this.setState({ + error: undefined, + isErrorSourceDisplayed: false + }); + } + + sourceDisplay() { + this.setState({ + isErrorSourceDisplayed: !this.state.isErrorSourceDisplayed + }); + } + + render(){ + const error = this.state.error; + return ( + <div> + {error? + <div className="modal-dialog-wrapper"> + <Modal.Dialog> + <Modal.Header className="modal-header"> + <h4>{error.type}</h4> + </Modal.Header> + <Modal.Body> + {error.message} + {this.state.isErrorSourceDisplayed + ? <div className="details"> + {!error.code && !error.data && !error.stack + ? <h6>No scary details</h6> + : null} + {this.state.error.code + ? <div><h6>Code</h6><pre>{this.state.error.code}</pre></div> + : null} + {this.state.error.data + ? <div><h6>Data</h6><pre>{this.state.error.data}</pre></div> + : null} + {this.state.error.stack + ? <div><h6>Stack Trace</h6><pre>{this.state.error.stack}</pre></div> + : null} + </div> + : null + } + </Modal.Body> + <Modal.Footer> + <div> + <Button variant="warning" onClick={() => this.handleClose()}> + <span className="glyphicon glyphicon-remove"></span> Close + </Button> + <Button variant="light" onClick={() => this.sourceDisplay()}> + {this.state.isErrorSourceDisplayed ? 'Hide' : 'Show'} scary details <span></span> + </Button> + </div> + </Modal.Footer> + </Modal.Dialog> + </div> + : null} + </div> + ); + } +} + +export default ErrorDialog; \ No newline at end of file diff --git a/src/components/experiment-list/experiment-list.css b/src/components/experiment-list/experiment-list.css index be1ab3c776ebb0ba1949c127d78177b43a552477..4eb57d703094f83aa7e2f18e273bf78002beda5d 100644 --- a/src/components/experiment-list/experiment-list.css +++ b/src/components/experiment-list/experiment-list.css @@ -4,18 +4,6 @@ li.nostyle { .experiment-list-wrapper { height: 100vh; - display: grid; - grid-template-rows: auto; - grid-template-columns: auto; - grid-template-areas: - "experiments"; -} - -.experiment-list { - text-align: left; - grid-area: experiments; - color: black; - background-color: white; } .no-items-notification { diff --git a/src/components/experiment-list/experiment-list.js b/src/components/experiment-list/experiment-list.js index f5c0850dc7183e1f127b2ee92bb2a4d496c96ab4..8581fceab98712af9faf094b3d42b37e3e423c64 100644 --- a/src/components/experiment-list/experiment-list.js +++ b/src/components/experiment-list/experiment-list.js @@ -8,22 +8,20 @@ export default class ExperimentList extends React.Component { render() { return ( <div className='experiment-list-wrapper'> - <div className='experiment-list'> - {this.props.experiments.length === 0 ? - <div className='no-items-notification'>List is currently empty ...</div> : - <ol> - {this.props.experiments.map(experiment => { - return ( - <li key={experiment.id || experiment.configuration.id} className='nostyle'> - <ExperimentListElement experiment={experiment} - availableServers={this.props.availableServers} - startingExperiment={this.props.startingExperiment} - selectExperimentOverviewTab={this.props.selectExperimentOverviewTab} /> - </li> - ); - })} - </ol>} - </div> + {this.props.experiments.length === 0 ? + <div className='no-items-notification'>List is currently empty ...</div> : + <ol> + {this.props.experiments.map(experiment => { + return ( + <li key={experiment.id || experiment.configuration.id} className='nostyle'> + <ExperimentListElement experiment={experiment} + availableServers={this.props.availableServers} + startingExperiment={this.props.startingExperiment} + selectExperimentOverviewTab={this.props.selectExperimentOverviewTab} /> + </li> + ); + })} + </ol>} </div> ); } diff --git a/src/components/experiment-list/import-experiment-buttons.css b/src/components/experiment-list/import-experiment-buttons.css index 429b61bbeb0046a0a3218f0b358a3fcd7cf82cd2..dd472ee2f4cf6ad496146ed054f1db57a9a2543c 100644 --- a/src/components/experiment-list/import-experiment-buttons.css +++ b/src/components/experiment-list/import-experiment-buttons.css @@ -12,7 +12,7 @@ z-index: 10001; } - #import-buttons { - vertical-align: middle; - margin: auto 0; + .import-button { + cursor: pointer; + height: 50%; } \ No newline at end of file diff --git a/src/components/experiment-list/import-experiment-buttons.js b/src/components/experiment-list/import-experiment-buttons.js index 8129213a7b674a8fd7fa8801d7e79a026147f865..6da73660ddf80f30f0059b1f833a2db71b958133 100644 --- a/src/components/experiment-list/import-experiment-buttons.js +++ b/src/components/experiment-list/import-experiment-buttons.js @@ -1,7 +1,6 @@ import React from 'react'; import { FaFolder, FaFileArchive, FaAudible } from 'react-icons/fa'; -import { ButtonGroup, Button } from 'react-bootstrap'; import ImportExperimentService from '../../services/experiments/files/import-experiment-service.js'; import ExperimentStorageService from '../../services/experiments/files/experiment-storage-service.js'; @@ -39,7 +38,7 @@ export default class ImportExperimentButtons extends React.Component { .importExperimentFolder(event) .then(async response => { this.setState({ - importFolderResponse : await response.json() + importFolderResponse : await response }); ExperimentStorageService.instance.getExperiments(true); }) @@ -94,14 +93,14 @@ export default class ImportExperimentButtons extends React.Component { {/* Import folder pop-up */} {this.state.importFolderResponse ? <div className="import-popup"> - <div variant="success"> + <div className="alert alert-success" role="alert"> <p>The experiment folder <b>{' ' + this.state.importFolderResponse.zipBaseFolderName}</b> has been succesfully imported as <b>{' ' + this.state.importFolderResponse.destFolderName}</b>. </p> </div> <div className="text-right"> - <Button variant="success" onClick={() => this.importFolderPopupClick()}>Got it!</Button> + <button className="btn btn-success" onClick={() => this.importFolderPopupClick()}>Got it!</button> </div> </div> : null @@ -110,7 +109,7 @@ export default class ImportExperimentButtons extends React.Component { {/* Import zip pop-up */} {this.state.importZipResponses ? <div className="import-popup"> - <div> + <div className="alert alert-success" role="alert"> <p>{this.state.importZipResponses.numberOfZips} successfully imported zip files.</p> </div> <p>The following experiments folders</p> @@ -118,7 +117,7 @@ export default class ImportExperimentButtons extends React.Component { <p>have been successfully imported as:</p> <p><b>{this.state.importZipResponses.destFolderName.join(', ')}.</b></p> <div className="text-right"> - <Button variant="success" onClick={() => this.importZipPopupClick()}>Got it!</Button> + <button className="btn btn-success" onClick={() => this.importZipPopupClick()}>Got it!</button> </div> </div> : null @@ -127,7 +126,7 @@ export default class ImportExperimentButtons extends React.Component { {/* Scan pop-up */} {this.state.scanStorageResponse ? <div className="import-popup"> - <div> + <div className="alert alert-success" role="alert"> <p>{this.state.scanStorageResponse.addedFoldersNumber} added folders, {' ' + this.state.scanStorageResponse.deletedFoldersNumber} deleted folders.</p> </div> @@ -141,7 +140,7 @@ export default class ImportExperimentButtons extends React.Component { ? this.state.scanStorageResponse.deletedFolders : 'none' }</b></p> <div className="text-right"> - <Button variant="success" onClick={() => this.scanStoragePopupClick()}>Got it!</Button> + <button className="btn btn-success" onClick={() => this.scanStoragePopupClick()}>Got it!</button> </div> </div> : null @@ -156,17 +155,19 @@ export default class ImportExperimentButtons extends React.Component { multiple accept='.zip' onChange={(event) => this.importZippedExperimentChange(event)}/> {!this.state.isImporting - ? <ButtonGroup role="group"> - <Button variant="outline-dark"> - <label htmlFor="folder"><FaFolder/> Import folder</label> - </Button> - <Button variant="outline-dark"> - <label htmlFor="zip"><FaFileArchive/> Import zip</label> - </Button > - <Button variant="outline-dark" onClick={() => this.scanStorageClick()}> + ? <div className="btn-group" role="group"> + <button type="button" className="btn btn-outline-dark"> + <label htmlFor="folder" className="import-button"> + <FaFolder/> Import folder + </label> + </button> + <button type="button" className="btn btn-outline-dark"> + <label htmlFor="zip" className="import-button"><FaFileArchive/> Import zip</label> + </button > + <button type="button" className="btn btn-outline-dark" onClick={() => this.scanStorageClick()}> <FaAudible/> Scan Storage - </Button> - </ButtonGroup> + </button> + </div> : null} </div> </div> diff --git a/src/components/user-menu/user-menu.js b/src/components/user-menu/user-menu.js index a287c8620d25ab87dcf6068bbf47e830b3bbebb4..efaea8c0ce86eb8a9059a3b9514c3b67a296cbd0 100644 --- a/src/components/user-menu/user-menu.js +++ b/src/components/user-menu/user-menu.js @@ -56,10 +56,11 @@ export default class UserMenu extends React.Component { onClick={this.onClickLogout} > <FaSignOutAlt className='user-icon' /> - Logout + Logout </Dropdown.Item> </Dropdown.Menu> </Dropdown> + </div> ); } diff --git a/src/index.js b/src/index.js index 33f3dd6b06a99231aa04113f545f3f2c34dd0955..ef2edf8ea3fc42258464231e29140c8723458c1e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; -import 'bootstrap/dist/css/bootstrap.min.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; diff --git a/src/mocks/handlers.js b/src/mocks/handlers.js index c6ca5a9f1ffd7464851ae1e501857b360306df38..829ef5dc8a47c4b9f0a42bfd810faa383c070bff 100644 --- a/src/mocks/handlers.js +++ b/src/mocks/handlers.js @@ -9,6 +9,7 @@ import MockServerConfig from './mock_server-config.json'; import MockUsers from './mock_users.json'; import MockSimulations from './mock_simulations.json'; import MockUserGroups from './mock_user-groups.json'; +import MockGDPR from './mock_gdpr.json'; import ImageAI from '../assets/images/Artificial_Intelligence_2.jpg'; @@ -48,7 +49,15 @@ export const handlers = [ rest.get(`${config.api.proxy.url}${endpoints.proxy.identity.me.groups.url}`, (req, res, ctx) => { return res(ctx.json(MockUserGroups)); }), + rest.get(`${config.api.proxy.url}${endpoints.proxy.identity.url}${endpoints.proxy.identity.gdpr.url}`, + (req, res, ctx) => { + return res(ctx.json(MockGDPR)); + }), rest.get(`${config.api.proxy.url}${endpoints.proxy.identity.url}/:userID`, (req, res, ctx) => { return res(ctx.json(MockUsers[1])); - }) + }), + rest.post(`${config.api.proxy.url}${endpoints.proxy.identity.url}${endpoints.proxy.identity.gdpr.url}`, + (req, res, ctx) => { + return res(ctx.json({'status':'success'})); + }) ]; \ No newline at end of file diff --git a/src/mocks/mock_gdpr.json b/src/mocks/mock_gdpr.json new file mode 100644 index 0000000000000000000000000000000000000000..5f7e96e64f6e6afdd5960f51cd34db5bb41f0548 --- /dev/null +++ b/src/mocks/mock_gdpr.json @@ -0,0 +1,3 @@ +{ + "gdpr": false +} \ No newline at end of file diff --git a/src/services/error-handler-service.js b/src/services/error-handler-service.js index 9a62e91d44dda85327f07733d89f962a716e43b6..026e9396a03fe5f02f74bab0cd252721fba8104c 100644 --- a/src/services/error-handler-service.js +++ b/src/services/error-handler-service.js @@ -1,11 +1,20 @@ +import { EventEmitter } from 'events'; + let _instance = null; const SINGLETON_ENFORCER = Symbol(); /** - * Service taking care of OIDC/FS authentication for NRP accounts + * Service that handles error retrieving from http and opening error dialog in App.js + * An Error object has 5 attributes among which 2 are required: + * - type: category (network ...) | required + * - message: details | required + * - code: error line | optional + * - data: related content | optional + * - stack: call stack | optional */ -class ErrorHandlerService { +class ErrorHandlerService extends EventEmitter { constructor(enforcer) { + super(); if (enforcer !== SINGLETON_ENFORCER) { throw new Error('Use ' + this.constructor.name + '.instance'); } @@ -19,20 +28,31 @@ class ErrorHandlerService { return _instance; } - displayServerHTTPError(error) { - //TODO: needs proper UI implementation - console.error(error); + // HTTP request error + networkError(error) { + error.type = 'Network Error'; + this.emit(ErrorHandlerService.EVENTS.ERROR, error); + } + + // Handling data error + dataError(error){ + error.type = 'Data Error'; + this.emit(ErrorHandlerService.EVENTS.ERROR, error); } - onErrorSimulationUpdate(error) { - //TODO: needs proper UI implementation - console.error(error); + startSimulationError(error) { + error.type = 'Start Simulation Error'; + this.emit(ErrorHandlerService.EVENTS.ERROR, error); } - displayError (error){ - //TODO: needs proper implementation - console.error(error.type + error.message); + updateSimulationError(error) { + error.type = 'Update Simulation Error'; + this.emit(ErrorHandlerService.EVENTS.ERROR, error); } } -export default ErrorHandlerService; +ErrorHandlerService.EVENTS = Object.freeze({ + ERROR: 'ERROR' +}); + +export default ErrorHandlerService; \ No newline at end of file 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 ad13ee9c15dd3df3f71f36def5dc930e3df7fad3..9492c9fa6d1d1a2ea97af49449db67f08df23e7d 100644 --- a/src/services/experiments/execution/__tests__/running-simulation-service.test.js +++ b/src/services/experiments/execution/__tests__/running-simulation-service.test.js @@ -41,11 +41,11 @@ test('initializes and gets the simulation resources', async () => { expect(resources).toBeDefined(); // failure case - jest.spyOn(ErrorHandlerService.instance, 'displayServerHTTPError').mockImplementation(() => { }); + jest.spyOn(ErrorHandlerService.instance, 'networkError').mockImplementation(() => { }); let simIDFailure = 0; - expect(ErrorHandlerService.instance.displayServerHTTPError).not.toHaveBeenCalled(); + expect(ErrorHandlerService.instance.networkError).not.toHaveBeenCalled(); resources = await RunningSimulationService.instance.initConfigFiles(serverBaseURL, simIDFailure); - expect(ErrorHandlerService.instance.displayServerHTTPError).toHaveBeenCalled(); + expect(ErrorHandlerService.instance.networkError).toHaveBeenCalled(); }); test('verifies whether a simulation is ready', async () => { @@ -139,7 +139,7 @@ test('register for ROS status information', () => { test('can retrieve the state of a simulation', async () => { let returnValueGET = undefined; - jest.spyOn(ErrorHandlerService.instance, 'displayServerHTTPError').mockImplementation(); + jest.spyOn(ErrorHandlerService.instance, 'networkError').mockImplementation(); jest.spyOn(RunningSimulationService.instance, 'httpRequestGET').mockImplementation(() => { if (RunningSimulationService.instance.httpRequestGET.mock.calls.length === 1) { returnValueGET = { state: EXPERIMENT_STATE.PAUSED }; // proper state msg @@ -161,12 +161,12 @@ test('can retrieve the state of a simulation', async () => { // call 2 => rejected simSate = await RunningSimulationService.instance.getState('test-url', 1); - expect(ErrorHandlerService.instance.displayServerHTTPError).toHaveBeenCalled(); + expect(ErrorHandlerService.instance.networkError).toHaveBeenCalled(); }); test('can set the state of a simulation', async () => { let returnValuePUT = undefined; - jest.spyOn(ErrorHandlerService.instance, 'onErrorSimulationUpdate').mockImplementation(); + jest.spyOn(ErrorHandlerService.instance, 'updateSimuationError').mockImplementation(); jest.spyOn(RunningSimulationService.instance, 'httpRequestPUT').mockImplementation(() => { if (RunningSimulationService.instance.httpRequestGET.mock.calls.length === 1) { returnValuePUT = {}; @@ -184,5 +184,5 @@ test('can set the state of a simulation', async () => { // call 2 => rejected returnValue = await RunningSimulationService.instance.updateState('test-url', 1, EXPERIMENT_STATE.PAUSED); - expect(ErrorHandlerService.instance.onErrorSimulationUpdate).toHaveBeenCalled(); + expect(ErrorHandlerService.instance.updateSimulationError).toHaveBeenCalled(); }); \ No newline at end of file diff --git a/src/services/experiments/execution/__tests__/server-resources-service.test.js b/src/services/experiments/execution/__tests__/server-resources-service.test.js index 2345da7fc8b4b3b23b967e6640d0142d8e4a7287..92ecf5b8b0ccf30ff1e1155aec4540ef8791a484 100644 --- a/src/services/experiments/execution/__tests__/server-resources-service.test.js +++ b/src/services/experiments/execution/__tests__/server-resources-service.test.js @@ -67,9 +67,9 @@ test('can get a server config', async () => { jest.spyOn(ServerResourcesService.instance, 'httpRequestGET').mockImplementation(() => { return Promise.reject(); }); - jest.spyOn(ErrorHandlerService.instance, 'displayServerHTTPError').mockImplementation(); + jest.spyOn(ErrorHandlerService.instance, 'networkError').mockImplementation(); config = await ServerResourcesService.instance.getServerConfig('test-server-id'); - expect(ErrorHandlerService.instance.displayServerHTTPError).toHaveBeenCalled(); + expect(ErrorHandlerService.instance.networkError).toHaveBeenCalled(); }); test('should stop polling updates when window is unloaded', async () => { diff --git a/src/services/experiments/execution/experiment-execution-service.js b/src/services/experiments/execution/experiment-execution-service.js index 924c6085fc7988b5b6d158f8390affcfb4ed4e76..0076fbf602d0a6d4ab7e959881ac7265aee4fa31 100644 --- a/src/services/experiments/execution/experiment-execution-service.js +++ b/src/services/experiments/execution/experiment-execution-service.js @@ -3,6 +3,7 @@ import _ from 'lodash'; //import NrpAnalyticsService from '../../nrp-analytics-service.js'; import ServerResourcesService from './server-resources-service.js'; import SimulationService from './running-simulation-service.js'; +import ErrorHandlerService from '../../error-handler-service'; import { HttpService } from '../../http-service.js'; import { EXPERIMENT_STATE } from '../experiment-constants.js'; @@ -88,9 +89,8 @@ class ExperimentExecutionService extends HttpService { profiler, progressCallback ).catch((failure) => { - if (failure.error && failure.error.data) { - //TODO: proper ErrorHandlerService callback - console.error('Failed to start simulation: ' + JSON.stringify(failure.error.data)); + if (failure.error) { + ErrorHandlerService.instance.startSimulationError(failure.error); } fatalErrorOccurred = fatalErrorOccurred || failure.isFatal; diff --git a/src/services/experiments/execution/running-simulation-service.js b/src/services/experiments/execution/running-simulation-service.js index caa3a1cf6d5067af4c9aef8cff60069d1c78af7d..754ccc8d96d54ab3e014630e03654ce5905ea010 100644 --- a/src/services/experiments/execution/running-simulation-service.js +++ b/src/services/experiments/execution/running-simulation-service.js @@ -44,7 +44,7 @@ class SimulationService extends HttpService { cachedConfigFiles = response.resources; } catch (error) { - ErrorHandlerService.instance.displayServerHTTPError(error); + ErrorHandlerService.instance.networkError(error); } return cachedConfigFiles; @@ -151,7 +151,7 @@ class SimulationService extends HttpService { return response; } catch (error) { - ErrorHandlerService.instance.displayServerHTTPError(error); + ErrorHandlerService.instance.networkError(error); } } @@ -168,7 +168,7 @@ class SimulationService extends HttpService { return response; } catch (error) { - ErrorHandlerService.instance.onErrorSimulationUpdate(error); + ErrorHandlerService.instance.updateSimulationError(error); } } } diff --git a/src/services/experiments/execution/server-resources-service.js b/src/services/experiments/execution/server-resources-service.js index 708a46bb5a601c105268f6b2bd4bd2e7fcb9a84e..8a2eb78c48f3f299c872187fbd239b18279c2003 100644 --- a/src/services/experiments/execution/server-resources-service.js +++ b/src/services/experiments/execution/server-resources-service.js @@ -79,7 +79,7 @@ class ServerResourcesService extends HttpService { .then(async (response) => { return await response.json(); }) - .catch(ErrorHandlerService.instance.displayServerHTTPError); + .catch(ErrorHandlerService.instance.networkError); } } diff --git a/src/services/experiments/files/experiment-storage-service.js b/src/services/experiments/files/experiment-storage-service.js index 9fdd009e4b5345b64d0487276089302886654850..a02538e76345527b61f35ea8e75d8b790b107553 100644 --- a/src/services/experiments/files/experiment-storage-service.js +++ b/src/services/experiments/files/experiment-storage-service.js @@ -3,6 +3,7 @@ import { EXPERIMENT_RIGHTS } from '../experiment-constants'; import endpoints from '../../proxy/data/endpoints.json'; import config from '../../../config.json'; +import ErrorHandlerService from '../../error-handler-service.js'; const storageURL = `${config.api.proxy.url}${endpoints.proxy.storage.url}`; const storageExperimentsURL = `${config.api.proxy.url}${endpoints.proxy.storage.experiments.url}`; @@ -68,13 +69,18 @@ class ExperimentStorageService extends HttpService { // move to experiment-configuration-service? async getExperiments(forceUpdate = false) { if (!this.experiments || forceUpdate) { - let experimentList = await (await this.httpRequestGET(storageExperimentsURL)).json(); - // filter out experiments with incomplete configuration (probably storage corruption) - experimentList = experimentList.filter(experiment => experiment.configuration.experimentFile); - this.sortExperiments(experimentList); - await this.fillExperimentDetails(experimentList); - this.experiments = experimentList; - this.emit(ExperimentStorageService.EVENTS.UPDATE_EXPERIMENTS, this.experiments); + try { + let experimentList = await (await this.httpRequestGET(storageExperimentsURL)).json(); + // filter out experiments with incomplete configuration (probably storage corruption) + experimentList = experimentList.filter(experiment => experiment.configuration.experimentFile); + this.sortExperiments(experimentList); + await this.fillExperimentDetails(experimentList); + this.experiments = experimentList; + this.emit(ExperimentStorageService.EVENTS.UPDATE_EXPERIMENTS, this.experiments); + } + catch (error) { + ErrorHandlerService.instance.networkError(error); + } } return this.experiments; diff --git a/src/services/experiments/files/import-experiment-service.js b/src/services/experiments/files/import-experiment-service.js index 7d00ffc80e75a1cff50debe545e829c2fc034983..45709388d357a983d7d0e58d343506a9d696aa00 100644 --- a/src/services/experiments/files/import-experiment-service.js +++ b/src/services/experiments/files/import-experiment-service.js @@ -1,9 +1,9 @@ import { HttpService } from '../../http-service.js'; -import ErrorHandlerService from '../../error-handler-service.js'; import JSZip from 'jszip'; import endpoints from '../../proxy/data/endpoints.json'; import config from '../../../config.json'; +import ErrorHandlerService from '../../error-handler-service.js'; const importExperimentURL = `${config.api.proxy.url}${endpoints.proxy.storage.importExperiment.url}`; const scanStorageURL = `${config.api.proxy.url}${endpoints.proxy.storage.scanStorage.url}`; @@ -69,7 +69,7 @@ export default class ImportExperimentService extends HttpService { async scanStorage() { return this.httpRequestPOST(scanStorageURL) .then(response => this.getScanStorageResponse(response)) - .catch(error => ErrorHandlerService.instance.displayError(error)); + .catch(error => ErrorHandlerService.instance.networkError(error)); } async zipExperimentFolder(event) { @@ -106,7 +106,7 @@ export default class ImportExperimentService extends HttpService { ) ) .catch(error => { - ErrorHandlerService.instance.displayError(error); + ErrorHandlerService.instance.dataError(error); return Promise.reject(error); }) ); @@ -115,7 +115,7 @@ export default class ImportExperimentService extends HttpService { return Promise.all(promises) .then(() => zip.generateAsync({ type: 'blob' })) .catch(error => { - ErrorHandlerService.instance.displayError(error); + ErrorHandlerService.instance.dataError(error); return Promise.reject(error); }); } @@ -123,8 +123,8 @@ export default class ImportExperimentService extends HttpService { async importExperimentFolder(event) { return this.zipExperimentFolder(event).then(async zipContent => { return this.httpRequestPOST(importExperimentURL, zipContent, options) - .catch(error => ErrorHandlerService.instance - .displayError(error) + .then(response => response.json()) + .catch(error => ErrorHandlerService.instance.networkError(error) ); }); } @@ -133,15 +133,7 @@ export default class ImportExperimentService extends HttpService { let files = event.target.files; let zipFiles = []; Array.from(files).forEach(file => { - if (file.type !== 'application/zip') { - ErrorHandlerService.instance.displayError({ - type : 'Import Error.', - message :`The file ${file.name} cannot be imported because it is not a zip file.` - }); - } - else { - zipFiles.push(file); - } + zipFiles.push(file); }); let promises = zipFiles.map(zipFile => { return new Promise(resolve => { @@ -159,7 +151,7 @@ export default class ImportExperimentService extends HttpService { zipContents.map(zipContent => this.httpRequestPOST(importExperimentURL, zipContent, options) .catch(error => { - ErrorHandlerService.instance.displayError(error); + ErrorHandlerService.instance.networkError(error); return Promise.reject(error); }) ) diff --git a/src/services/proxy/__tests__/nrp-user-service.test.js b/src/services/proxy/__tests__/nrp-user-service.test.js index 7561dcfb858bf5b20c18b2e2f84599821b8dc7a1..28c072900a91352e6fd515abef2fb0921e42f72f 100644 --- a/src/services/proxy/__tests__/nrp-user-service.test.js +++ b/src/services/proxy/__tests__/nrp-user-service.test.js @@ -71,4 +71,12 @@ test('can determine group membership for administrators', async () => { test('can retrieve cluster reservations', async () => { expect(await NrpUserService.instance.getReservation()).toBe(undefined); +}); + +test('can retrieve false gdpr status', async () => { + expect(await NrpUserService.instance.getGdpr()).toEqual({'gdpr': false}); +}); + +test('can set gdpr status', async () => { + expect(await NrpUserService.instance.setGdpr()).toEqual({'status':'success'}); }); \ No newline at end of file diff --git a/src/services/proxy/data/endpoints.json b/src/services/proxy/data/endpoints.json index 2ab2653147217a8d0853ad87705cd1b8d10fbcb0..aa5c143975c1d800ae711fe54b13a30784c8122c 100644 --- a/src/services/proxy/data/endpoints.json +++ b/src/services/proxy/data/endpoints.json @@ -16,6 +16,9 @@ "groups": { "url": "/identity/me/groups" } + }, + "gdpr": { + "url": "/gdpr" } }, "server": { @@ -29,11 +32,11 @@ "experiments": { "url": "/storage/experiments" }, - "importExperiment":{ + "importExperiment": { "url": "/storage/importExperiment" }, - "scanStorage":{ - "url":"/storage/scanStorage" + "scanStorage": { + "url": "/storage/scanStorage" } } } diff --git a/src/services/proxy/nrp-user-service.js b/src/services/proxy/nrp-user-service.js index 58d8ac224f694234dd68a099c7e247db597f9641..9801ef00e73d8fb9f06f8a02e86b0c82d1abb9b6 100644 --- a/src/services/proxy/nrp-user-service.js +++ b/src/services/proxy/nrp-user-service.js @@ -14,7 +14,7 @@ const PROXY_URL = config.api.proxy.url; const IDENTITY_BASE_URL = `${PROXY_URL}${endpoints.proxy.identity.url}`; const IDENTITY_ME_URL = `${PROXY_URL}${endpoints.proxy.identity.me.url}`; const IDENTITY_ME_GROUPS_URL = `${PROXY_URL}${endpoints.proxy.identity.me.groups.url}`; - +const GDPR_URL = `${IDENTITY_BASE_URL}${endpoints.proxy.identity.gdpr.url}`; /** * Service managing all data related to NRP users. */ @@ -112,6 +112,22 @@ class NrpUserService extends HttpService { getReservation() { return window.sessionStorage.getItem('clusterReservation'); } + + /** + * Get the GDPR status for current user. + * @returns {promise} Contains the GDPR status for the current user + */ + async getGdpr() { + return await (await this.httpRequestGET(GDPR_URL)).json(); + } + + /** + * Set the accepted GDPR status for current user. + * @returns {promise} Response for the current user + */ + async setGdpr() { + return await (await this.httpRequestPOST(GDPR_URL)).json(); + } } export default NrpUserService;