Skip to content
Snippets Groups Projects
Commit 943d658b authored by Antoine Detailleur's avatar Antoine Detailleur
Browse files

Merged development into NRRPLT-7930-remote-edition-of-experiment

parents 86cdfb68 abf635d3
No related branches found
No related tags found
No related merge requests found
Showing
with 192 additions and 63 deletions
...@@ -3,21 +3,25 @@ import React from 'react'; ...@@ -3,21 +3,25 @@ import React from 'react';
import { HashRouter, Switch, Route } from 'react-router-dom'; import { HashRouter, Switch, Route } from 'react-router-dom';
import EntryPage from './components/entry-page/entry-page.js'; import EntryPage from './components/entry-page/entry-page.js';
import ErrorDialog from './components/dialog/error-dialog.js';
import ExperimentOverview from './components/experiment-overview/experiment-overview.js'; import ExperimentOverview from './components/experiment-overview/experiment-overview.js';
class App extends React.Component { class App extends React.Component {
render() { render() {
return ( return(
<HashRouter> <div>
<Switch> <ErrorDialog />
<Route path='/experiments-overview'> <HashRouter>
<ExperimentOverview /> <Switch>
</Route> <Route path='/experiments-overview'>
<Route path='/'> <ExperimentOverview />
<EntryPage /> </Route>
</Route> <Route path='/'>
</Switch> <EntryPage />
</HashRouter> </Route>
</Switch>
</HashRouter>
</div>
); );
} }
} }
......
.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
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
...@@ -38,7 +38,7 @@ export default class ImportExperimentButtons extends React.Component { ...@@ -38,7 +38,7 @@ export default class ImportExperimentButtons extends React.Component {
.importExperimentFolder(event) .importExperimentFolder(event)
.then(async response => { .then(async response => {
this.setState({ this.setState({
importFolderResponse : await response.json() importFolderResponse : await response
}); });
ExperimentStorageService.instance.getExperiments(true); ExperimentStorageService.instance.getExperiments(true);
}) })
......
import { EventEmitter } from 'events';
let _instance = null; let _instance = null;
const SINGLETON_ENFORCER = Symbol(); 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) { constructor(enforcer) {
super();
if (enforcer !== SINGLETON_ENFORCER) { if (enforcer !== SINGLETON_ENFORCER) {
throw new Error('Use ' + this.constructor.name + '.instance'); throw new Error('Use ' + this.constructor.name + '.instance');
} }
...@@ -19,20 +28,31 @@ class ErrorHandlerService { ...@@ -19,20 +28,31 @@ class ErrorHandlerService {
return _instance; return _instance;
} }
displayServerHTTPError(error) { // HTTP request error
//TODO: needs proper UI implementation networkError(error) {
console.error(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) { startSimulationError(error) {
//TODO: needs proper UI implementation error.type = 'Start Simulation Error';
console.error(error); this.emit(ErrorHandlerService.EVENTS.ERROR, error);
} }
displayError (error){ updateSimulationError(error) {
//TODO: needs proper implementation error.type = 'Update Simulation Error';
console.error(error.type + error.message); this.emit(ErrorHandlerService.EVENTS.ERROR, error);
} }
} }
export default ErrorHandlerService; ErrorHandlerService.EVENTS = Object.freeze({
ERROR: 'ERROR'
});
export default ErrorHandlerService;
\ No newline at end of file
...@@ -41,11 +41,11 @@ test('initializes and gets the simulation resources', async () => { ...@@ -41,11 +41,11 @@ test('initializes and gets the simulation resources', async () => {
expect(resources).toBeDefined(); expect(resources).toBeDefined();
// failure case // failure case
jest.spyOn(ErrorHandlerService.instance, 'displayServerHTTPError').mockImplementation(() => { }); jest.spyOn(ErrorHandlerService.instance, 'networkError').mockImplementation(() => { });
let simIDFailure = 0; let simIDFailure = 0;
expect(ErrorHandlerService.instance.displayServerHTTPError).not.toHaveBeenCalled(); expect(ErrorHandlerService.instance.networkError).not.toHaveBeenCalled();
resources = await RunningSimulationService.instance.initConfigFiles(serverBaseURL, simIDFailure); 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 () => { test('verifies whether a simulation is ready', async () => {
...@@ -139,7 +139,7 @@ test('register for ROS status information', () => { ...@@ -139,7 +139,7 @@ test('register for ROS status information', () => {
test('can retrieve the state of a simulation', async () => { test('can retrieve the state of a simulation', async () => {
let returnValueGET = undefined; let returnValueGET = undefined;
jest.spyOn(ErrorHandlerService.instance, 'displayServerHTTPError').mockImplementation(); jest.spyOn(ErrorHandlerService.instance, 'networkError').mockImplementation();
jest.spyOn(RunningSimulationService.instance, 'httpRequestGET').mockImplementation(() => { jest.spyOn(RunningSimulationService.instance, 'httpRequestGET').mockImplementation(() => {
if (RunningSimulationService.instance.httpRequestGET.mock.calls.length === 1) { if (RunningSimulationService.instance.httpRequestGET.mock.calls.length === 1) {
returnValueGET = { state: EXPERIMENT_STATE.PAUSED }; // proper state msg returnValueGET = { state: EXPERIMENT_STATE.PAUSED }; // proper state msg
...@@ -161,12 +161,12 @@ test('can retrieve the state of a simulation', async () => { ...@@ -161,12 +161,12 @@ test('can retrieve the state of a simulation', async () => {
// call 2 => rejected // call 2 => rejected
simSate = await RunningSimulationService.instance.getState('test-url', 1); 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 () => { test('can set the state of a simulation', async () => {
let returnValuePUT = undefined; let returnValuePUT = undefined;
jest.spyOn(ErrorHandlerService.instance, 'onErrorSimulationUpdate').mockImplementation(); jest.spyOn(ErrorHandlerService.instance, 'updateSimuationError').mockImplementation();
jest.spyOn(RunningSimulationService.instance, 'httpRequestPUT').mockImplementation(() => { jest.spyOn(RunningSimulationService.instance, 'httpRequestPUT').mockImplementation(() => {
if (RunningSimulationService.instance.httpRequestGET.mock.calls.length === 1) { if (RunningSimulationService.instance.httpRequestGET.mock.calls.length === 1) {
returnValuePUT = {}; returnValuePUT = {};
...@@ -184,5 +184,5 @@ test('can set the state of a simulation', async () => { ...@@ -184,5 +184,5 @@ test('can set the state of a simulation', async () => {
// call 2 => rejected // call 2 => rejected
returnValue = await RunningSimulationService.instance.updateState('test-url', 1, EXPERIMENT_STATE.PAUSED); 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
...@@ -67,9 +67,9 @@ test('can get a server config', async () => { ...@@ -67,9 +67,9 @@ test('can get a server config', async () => {
jest.spyOn(ServerResourcesService.instance, 'httpRequestGET').mockImplementation(() => { jest.spyOn(ServerResourcesService.instance, 'httpRequestGET').mockImplementation(() => {
return Promise.reject(); return Promise.reject();
}); });
jest.spyOn(ErrorHandlerService.instance, 'displayServerHTTPError').mockImplementation(); jest.spyOn(ErrorHandlerService.instance, 'networkError').mockImplementation();
config = await ServerResourcesService.instance.getServerConfig('test-server-id'); 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 () => { test('should stop polling updates when window is unloaded', async () => {
......
...@@ -3,6 +3,7 @@ import _ from 'lodash'; ...@@ -3,6 +3,7 @@ import _ from 'lodash';
//import NrpAnalyticsService from '../../nrp-analytics-service.js'; //import NrpAnalyticsService from '../../nrp-analytics-service.js';
import ServerResourcesService from './server-resources-service.js'; import ServerResourcesService from './server-resources-service.js';
import SimulationService from './running-simulation-service.js'; import SimulationService from './running-simulation-service.js';
import ErrorHandlerService from '../../error-handler-service';
import { HttpService } from '../../http-service.js'; import { HttpService } from '../../http-service.js';
import { EXPERIMENT_STATE } from '../experiment-constants.js'; import { EXPERIMENT_STATE } from '../experiment-constants.js';
...@@ -88,9 +89,8 @@ class ExperimentExecutionService extends HttpService { ...@@ -88,9 +89,8 @@ class ExperimentExecutionService extends HttpService {
profiler, profiler,
progressCallback progressCallback
).catch((failure) => { ).catch((failure) => {
if (failure.error && failure.error.data) { if (failure.error) {
//TODO: proper ErrorHandlerService callback ErrorHandlerService.instance.startSimulationError(failure.error);
console.error('Failed to start simulation: ' + JSON.stringify(failure.error.data));
} }
fatalErrorOccurred = fatalErrorOccurred || failure.isFatal; fatalErrorOccurred = fatalErrorOccurred || failure.isFatal;
......
...@@ -44,7 +44,7 @@ class SimulationService extends HttpService { ...@@ -44,7 +44,7 @@ class SimulationService extends HttpService {
cachedConfigFiles = response.resources; cachedConfigFiles = response.resources;
} }
catch (error) { catch (error) {
ErrorHandlerService.instance.displayServerHTTPError(error); ErrorHandlerService.instance.networkError(error);
} }
return cachedConfigFiles; return cachedConfigFiles;
...@@ -151,7 +151,7 @@ class SimulationService extends HttpService { ...@@ -151,7 +151,7 @@ class SimulationService extends HttpService {
return response; return response;
} }
catch (error) { catch (error) {
ErrorHandlerService.instance.displayServerHTTPError(error); ErrorHandlerService.instance.networkError(error);
} }
} }
...@@ -168,7 +168,7 @@ class SimulationService extends HttpService { ...@@ -168,7 +168,7 @@ class SimulationService extends HttpService {
return response; return response;
} }
catch (error) { catch (error) {
ErrorHandlerService.instance.onErrorSimulationUpdate(error); ErrorHandlerService.instance.updateSimulationError(error);
} }
} }
} }
......
...@@ -79,7 +79,7 @@ class ServerResourcesService extends HttpService { ...@@ -79,7 +79,7 @@ class ServerResourcesService extends HttpService {
.then(async (response) => { .then(async (response) => {
return await response.json(); return await response.json();
}) })
.catch(ErrorHandlerService.instance.displayServerHTTPError); .catch(ErrorHandlerService.instance.networkError);
} }
} }
......
...@@ -3,6 +3,7 @@ import { EXPERIMENT_RIGHTS } from '../experiment-constants'; ...@@ -3,6 +3,7 @@ import { EXPERIMENT_RIGHTS } from '../experiment-constants';
import endpoints from '../../proxy/data/endpoints.json'; import endpoints from '../../proxy/data/endpoints.json';
import config from '../../../config.json'; import config from '../../../config.json';
import ErrorHandlerService from '../../error-handler-service.js';
const storageURL = `${config.api.proxy.url}${endpoints.proxy.storage.url}`; const storageURL = `${config.api.proxy.url}${endpoints.proxy.storage.url}`;
const storageExperimentsURL = `${config.api.proxy.url}${endpoints.proxy.storage.experiments.url}`; const storageExperimentsURL = `${config.api.proxy.url}${endpoints.proxy.storage.experiments.url}`;
...@@ -68,13 +69,18 @@ class ExperimentStorageService extends HttpService { ...@@ -68,13 +69,18 @@ class ExperimentStorageService extends HttpService {
// move to experiment-configuration-service? // move to experiment-configuration-service?
async getExperiments(forceUpdate = false) { async getExperiments(forceUpdate = false) {
if (!this.experiments || forceUpdate) { if (!this.experiments || forceUpdate) {
let experimentList = await (await this.httpRequestGET(storageExperimentsURL)).json(); try {
// filter out experiments with incomplete configuration (probably storage corruption) let experimentList = await (await this.httpRequestGET(storageExperimentsURL)).json();
experimentList = experimentList.filter(experiment => experiment.configuration.experimentFile); // filter out experiments with incomplete configuration (probably storage corruption)
this.sortExperiments(experimentList); experimentList = experimentList.filter(experiment => experiment.configuration.experimentFile);
await this.fillExperimentDetails(experimentList); this.sortExperiments(experimentList);
this.experiments = experimentList; await this.fillExperimentDetails(experimentList);
this.emit(ExperimentStorageService.EVENTS.UPDATE_EXPERIMENTS, this.experiments); this.experiments = experimentList;
this.emit(ExperimentStorageService.EVENTS.UPDATE_EXPERIMENTS, this.experiments);
}
catch (error) {
ErrorHandlerService.instance.networkError(error);
}
} }
return this.experiments; return this.experiments;
......
import { HttpService } from '../../http-service.js'; import { HttpService } from '../../http-service.js';
import ErrorHandlerService from '../../error-handler-service.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
import endpoints from '../../proxy/data/endpoints.json'; import endpoints from '../../proxy/data/endpoints.json';
import config from '../../../config.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 importExperimentURL = `${config.api.proxy.url}${endpoints.proxy.storage.importExperiment.url}`;
const scanStorageURL = `${config.api.proxy.url}${endpoints.proxy.storage.scanStorage.url}`; const scanStorageURL = `${config.api.proxy.url}${endpoints.proxy.storage.scanStorage.url}`;
...@@ -69,7 +69,7 @@ export default class ImportExperimentService extends HttpService { ...@@ -69,7 +69,7 @@ export default class ImportExperimentService extends HttpService {
async scanStorage() { async scanStorage() {
return this.httpRequestPOST(scanStorageURL) return this.httpRequestPOST(scanStorageURL)
.then(response => this.getScanStorageResponse(response)) .then(response => this.getScanStorageResponse(response))
.catch(error => ErrorHandlerService.instance.displayError(error)); .catch(error => ErrorHandlerService.instance.networkError(error));
} }
async zipExperimentFolder(event) { async zipExperimentFolder(event) {
...@@ -106,7 +106,7 @@ export default class ImportExperimentService extends HttpService { ...@@ -106,7 +106,7 @@ export default class ImportExperimentService extends HttpService {
) )
) )
.catch(error => { .catch(error => {
ErrorHandlerService.instance.displayError(error); ErrorHandlerService.instance.dataError(error);
return Promise.reject(error); return Promise.reject(error);
}) })
); );
...@@ -115,7 +115,7 @@ export default class ImportExperimentService extends HttpService { ...@@ -115,7 +115,7 @@ export default class ImportExperimentService extends HttpService {
return Promise.all(promises) return Promise.all(promises)
.then(() => zip.generateAsync({ type: 'blob' })) .then(() => zip.generateAsync({ type: 'blob' }))
.catch(error => { .catch(error => {
ErrorHandlerService.instance.displayError(error); ErrorHandlerService.instance.dataError(error);
return Promise.reject(error); return Promise.reject(error);
}); });
} }
...@@ -123,8 +123,8 @@ export default class ImportExperimentService extends HttpService { ...@@ -123,8 +123,8 @@ export default class ImportExperimentService extends HttpService {
async importExperimentFolder(event) { async importExperimentFolder(event) {
return this.zipExperimentFolder(event).then(async zipContent => { return this.zipExperimentFolder(event).then(async zipContent => {
return this.httpRequestPOST(importExperimentURL, zipContent, options) return this.httpRequestPOST(importExperimentURL, zipContent, options)
.catch(error => ErrorHandlerService.instance .then(response => response.json())
.displayError(error) .catch(error => ErrorHandlerService.instance.networkError(error)
); );
}); });
} }
...@@ -133,15 +133,7 @@ export default class ImportExperimentService extends HttpService { ...@@ -133,15 +133,7 @@ export default class ImportExperimentService extends HttpService {
let files = event.target.files; let files = event.target.files;
let zipFiles = []; let zipFiles = [];
Array.from(files).forEach(file => { Array.from(files).forEach(file => {
if (file.type !== 'application/zip') { zipFiles.push(file);
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);
}
}); });
let promises = zipFiles.map(zipFile => { let promises = zipFiles.map(zipFile => {
return new Promise(resolve => { return new Promise(resolve => {
...@@ -159,7 +151,7 @@ export default class ImportExperimentService extends HttpService { ...@@ -159,7 +151,7 @@ export default class ImportExperimentService extends HttpService {
zipContents.map(zipContent => zipContents.map(zipContent =>
this.httpRequestPOST(importExperimentURL, zipContent, options) this.httpRequestPOST(importExperimentURL, zipContent, options)
.catch(error => { .catch(error => {
ErrorHandlerService.instance.displayError(error); ErrorHandlerService.instance.networkError(error);
return Promise.reject(error); return Promise.reject(error);
}) })
) )
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment