diff --git a/src/App.js b/src/App.js index 676465d36b760623aa6d05f08ea1b7ce948c82c4..49ef483d5f200c0da5f6d8074d21606aa8ac7c59 100644 --- a/src/App.js +++ b/src/App.js @@ -4,6 +4,7 @@ import { HashRouter, Switch, Route } from 'react-router-dom'; import EntryPage from './components/entry-page/entry-page.js'; import ErrorDialog from './components/dialog/error-dialog.js'; +import ToastNotification from './components/dialog/toast-notification.js'; import ExperimentOverview from './components/experiment-overview/experiment-overview.js'; class App extends React.Component { @@ -11,6 +12,7 @@ class App extends React.Component { return( <div> <ErrorDialog /> + <ToastNotification/> <HashRouter> <Switch> <Route path='/experiments-overview'> diff --git a/src/components/dialog/error-dialog.css b/src/components/dialog/error-dialog.css index 02e9b3a1eae2d741aa5f835cdf59ecc137e57c69..b9f8a930181469c9e82ecdb54385ec6ffa5e6f8e 100644 --- a/src/components/dialog/error-dialog.css +++ b/src/components/dialog/error-dialog.css @@ -1,4 +1,4 @@ -.modal-dialog-wrapper { +.error-dialog-wrapper { position: fixed; width: 100%; height: 100%; diff --git a/src/components/dialog/error-dialog.js b/src/components/dialog/error-dialog.js index 67746983eafb8e1ea2d304d4ec6f73e3ff134ee0..d5a68975d105628967957365ce0e946ebb7c0c5a 100644 --- a/src/components/dialog/error-dialog.js +++ b/src/components/dialog/error-dialog.js @@ -1,7 +1,7 @@ import React from 'react'; import { Modal, Button } from 'react-bootstrap'; -import ErrorHandlerService from '../../services/error-handler-service.js'; +import DialogService from '../../services/dialog-service.js'; import './error-dialog.css'; @@ -15,10 +15,11 @@ class ErrorDialog extends React.Component{ } async componentDidMount() { - ErrorHandlerService.instance.addListener( - ErrorHandlerService.EVENTS.ERROR, (error) => { + DialogService.instance.addListener( + DialogService.EVENTS.ERROR, (error) => { this.onError(error); - }); + } + ); } onError(error) { @@ -45,7 +46,7 @@ class ErrorDialog extends React.Component{ return ( <div> {error? - <div className="modal-dialog-wrapper"> + <div className="error-dialog-wrapper"> <Modal.Dialog> <Modal.Header className="modal-header"> <h4>{error.type}</h4> diff --git a/src/components/dialog/toast-notification.js b/src/components/dialog/toast-notification.js new file mode 100644 index 0000000000000000000000000000000000000000..564b1379f888342bfc2e2fe62ebb1b360e2e5ac9 --- /dev/null +++ b/src/components/dialog/toast-notification.js @@ -0,0 +1,60 @@ +import { Toast } from 'bootstrap'; +import React from 'react' + +import DialogService from '../../services/dialog-service.js' + +class NotificationDialog extends React.Component{ + constructor(props){ + super(props); + this.state = { + notifications: [], + }; + } + + async componentDidMount() { + DialogService.instance.addListener( + DialogService.EVENTS.NOTIFICATION, (notification) => { + this.onNotification(notification); + } + ); + } + + onNotification(notification) { + this.setState({ + notifications: this.state.notifications.append(notification) + }); + } + + onClose(index) { + this.setState({ + notifications: notifications.splice(index, 1) + }); + } + + render(){ + return( + <div> + {this.state.notifications? + <ol> + {this.state.notifications.map(notification => { + return ( + <div className="toast-dialog-wrapper"> + <Toast.Dialog onClose={this.onClose(index)}> + <Toast.Header> + <h4>{notification.type}</h4> + </Toast.Header> + <Toast.Body> + {notification.message} + </Toast.Body> + </Toast.Dialog> + </div> + ); + }) + } + </ol> + : null + } + </div> + ) + } +} \ No newline at end of file diff --git a/src/services/error-handler-service.js b/src/services/dialog-service.js similarity index 54% rename from src/services/error-handler-service.js rename to src/services/dialog-service.js index 026e9396a03fe5f02f74bab0cd252721fba8104c..9a0f6b968480d45f6ca3d9470e27f33bcb8b51be 100644 --- a/src/services/error-handler-service.js +++ b/src/services/dialog-service.js @@ -12,7 +12,7 @@ const SINGLETON_ENFORCER = Symbol(); * - data: related content | optional * - stack: call stack | optional */ -class ErrorHandlerService extends EventEmitter { +class DialogService extends EventEmitter { constructor(enforcer) { super(); if (enforcer !== SINGLETON_ENFORCER) { @@ -22,7 +22,7 @@ class ErrorHandlerService extends EventEmitter { static get instance() { if (_instance == null) { - _instance = new ErrorHandlerService(SINGLETON_ENFORCER); + _instance = new DialogService(SINGLETON_ENFORCER); } return _instance; @@ -31,28 +31,38 @@ class ErrorHandlerService extends EventEmitter { // HTTP request error networkError(error) { error.type = 'Network Error'; - this.emit(ErrorHandlerService.EVENTS.ERROR, error); + this.emit(DialogService.EVENTS.ERROR, error); } // Handling data error dataError(error){ error.type = 'Data Error'; - this.emit(ErrorHandlerService.EVENTS.ERROR, error); + this.emit(DialogService.EVENTS.ERROR, error); } - startSimulationError(error) { - error.type = 'Start Simulation Error'; - this.emit(ErrorHandlerService.EVENTS.ERROR, error); + simulationError(error) { + error.type = 'Simulation Error'; + this.emit(DialogService.EVENTS.ERROR, error); } - updateSimulationError(error) { - error.type = 'Update Simulation Error'; - this.emit(ErrorHandlerService.EVENTS.ERROR, error); + progressNotification(notification) { + notification.type = 'Progress Status'; + this.emit(DialogService.EVENTS.NOTIFICATION, notification) } + + warningNotification(notification) { + notification.type = 'Warning' + this.emit(DialogService.EVENTS.NOTIFICATION, notification) + } + } -ErrorHandlerService.EVENTS = Object.freeze({ +DialogService.EVENTS = Object.freeze({ ERROR: 'ERROR' }); -export default ErrorHandlerService; \ No newline at end of file +DialogService.EVENTS = Object.freeze({ + NOTIFICATION: 'NOTIFICATION' +}) + +export default DialogService; \ 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 7af3b6e72eb1dbc0f5b53d53d0a9ffd960e99855..095c81384082acdce11f07dd4b2548a98d6ede46 100644 --- a/src/services/experiments/execution/__tests__/running-simulation-service.test.js +++ b/src/services/experiments/execution/__tests__/running-simulation-service.test.js @@ -8,7 +8,7 @@ import MockAvailableServers from '../../../../mocks/mock_available-servers.json' import MockSimulations from '../../../../mocks/mock_simulations.json'; import RunningSimulationService from '../running-simulation-service.js'; -import ErrorHandlerService from '../../../error-handler-service'; +import DialogService from '../../../error-handler-service'; import RoslibService from '../../../roslib-service'; import { EXPERIMENT_STATE } from '../../experiment-constants.js'; @@ -41,11 +41,11 @@ test('initializes and gets the simulation resources', async () => { expect(resources).toBeDefined(); // failure case - jest.spyOn(ErrorHandlerService.instance, 'networkError').mockImplementation(() => { }); + jest.spyOn(DialogService.instance, 'networkError').mockImplementation(() => { }); let simIDFailure = 0; - expect(ErrorHandlerService.instance.networkError).not.toHaveBeenCalled(); + expect(DialogService.instance.networkError).not.toHaveBeenCalled(); resources = await RunningSimulationService.instance.initConfigFiles(serverBaseURL, simIDFailure); - expect(ErrorHandlerService.instance.networkError).toHaveBeenCalled(); + expect(DialogService.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, 'networkError').mockImplementation(); + jest.spyOn(DialogService.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.networkError).toHaveBeenCalled(); + expect(DialogService.instance.networkError).toHaveBeenCalled(); }); test('can set the state of a simulation', async () => { let returnValuePUT = undefined; - jest.spyOn(ErrorHandlerService.instance, 'updateSimulationError').mockImplementation(); + jest.spyOn(DialogService.instance, 'simulationError').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.updateSimulationError).toHaveBeenCalled(); + expect(DialogService.instance.simulationError).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 92ecf5b8b0ccf30ff1e1155aec4540ef8791a484..6726f3f9f2f21fc0aa5f7b3dbaa0b4ecd9203fb9 100644 --- a/src/services/experiments/execution/__tests__/server-resources-service.test.js +++ b/src/services/experiments/execution/__tests__/server-resources-service.test.js @@ -7,7 +7,7 @@ import 'jest-fetch-mock'; import MockServerconfig from '../../../../mocks/mock_server-config.json'; import ServerResourcesService from '../../../../services/experiments/execution/server-resources-service'; -import ErrorHandlerService from '../../../error-handler-service'; +import DialogService from '../../../error-handler-service'; jest.setTimeout(10000); @@ -67,9 +67,9 @@ test('can get a server config', async () => { jest.spyOn(ServerResourcesService.instance, 'httpRequestGET').mockImplementation(() => { return Promise.reject(); }); - jest.spyOn(ErrorHandlerService.instance, 'networkError').mockImplementation(); + jest.spyOn(DialogService.instance, 'networkError').mockImplementation(); config = await ServerResourcesService.instance.getServerConfig('test-server-id'); - expect(ErrorHandlerService.instance.networkError).toHaveBeenCalled(); + expect(DialogService.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 0076fbf602d0a6d4ab7e959881ac7265aee4fa31..bcadae7e3f1088ed76f0453cc62b1d9622c52ff5 100644 --- a/src/services/experiments/execution/experiment-execution-service.js +++ b/src/services/experiments/execution/experiment-execution-service.js @@ -3,7 +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 DialogService from '../../dialog-service'; import { HttpService } from '../../http-service.js'; import { EXPERIMENT_STATE } from '../experiment-constants.js'; @@ -61,8 +61,8 @@ class ExperimentExecutionService extends HttpService { let brainProcesses = launchSingleMode ? 1 : experiment.configuration.brainProcesses; //TODO: placeholder, register actual progress callback later - let progressCallback = (msg) => { - console.info(msg); + let progressCallback = (notification) => { + DialogService.instance.progressNotification(notification); }; let launchOnNextServer = async () => { @@ -90,7 +90,7 @@ class ExperimentExecutionService extends HttpService { progressCallback ).catch((failure) => { if (failure.error) { - ErrorHandlerService.instance.startSimulationError(failure.error); + DialogService.instance.simulationError(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 754ccc8d96d54ab3e014630e03654ce5905ea010..2ef55859c3ec42302871b772c9d3c58a5641e68c 100644 --- a/src/services/experiments/execution/running-simulation-service.js +++ b/src/services/experiments/execution/running-simulation-service.js @@ -1,4 +1,4 @@ -import ErrorHandlerService from '../../error-handler-service.js'; +import DialogService from '../../dialog-service.js'; import RoslibService from '../../roslib-service.js'; import { HttpService } from '../../http-service.js'; import { EXPERIMENT_STATE } from '../experiment-constants.js'; @@ -44,7 +44,7 @@ class SimulationService extends HttpService { cachedConfigFiles = response.resources; } catch (error) { - ErrorHandlerService.instance.networkError(error); + DialogService.instance.networkError(error); } return cachedConfigFiles; @@ -151,7 +151,7 @@ class SimulationService extends HttpService { return response; } catch (error) { - ErrorHandlerService.instance.networkError(error); + DialogService.instance.networkError(error); } } @@ -168,7 +168,7 @@ class SimulationService extends HttpService { return response; } catch (error) { - ErrorHandlerService.instance.updateSimulationError(error); + DialogService.instance.simulationError(error); } } } diff --git a/src/services/experiments/execution/server-resources-service.js b/src/services/experiments/execution/server-resources-service.js index 8a2eb78c48f3f299c872187fbd239b18279c2003..8499b472a8619e251ef7769cfb28561b6704809e 100644 --- a/src/services/experiments/execution/server-resources-service.js +++ b/src/services/experiments/execution/server-resources-service.js @@ -1,4 +1,4 @@ -import ErrorHandlerService from '../../error-handler-service.js'; +import DialogService from '../../dialog-service.js'; import { HttpService } from '../../http-service.js'; import endpoints from '../../proxy/data/endpoints.json'; @@ -79,7 +79,7 @@ class ServerResourcesService extends HttpService { .then(async (response) => { return await response.json(); }) - .catch(ErrorHandlerService.instance.networkError); + .catch(DialogService.instance.networkError); } } diff --git a/src/services/experiments/files/experiment-storage-service.js b/src/services/experiments/files/experiment-storage-service.js index 0cf994dac08ab916701831c86cfe1e71e75fe1f8..8d6326388acea05cc17ed61d00daeb35c439b230 100644 --- a/src/services/experiments/files/experiment-storage-service.js +++ b/src/services/experiments/files/experiment-storage-service.js @@ -3,7 +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'; +import DialogService from '../../dialog-service.js'; const storageURL = `${config.api.proxy.url}${endpoints.proxy.storage.url}`; const storageExperimentsURL = `${config.api.proxy.url}${endpoints.proxy.storage.experiments.url}`; @@ -79,7 +79,7 @@ class ExperimentStorageService extends HttpService { this.emit(ExperimentStorageService.EVENTS.UPDATE_EXPERIMENTS, this.experiments); } catch (error) { - ErrorHandlerService.instance.networkError(error); + DialogService.instance.networkError(error); } } diff --git a/src/services/experiments/files/import-experiment-service.js b/src/services/experiments/files/import-experiment-service.js index 45709388d357a983d7d0e58d343506a9d696aa00..5ae1b74db8e043de07d94bd2cad534237542ca8a 100644 --- a/src/services/experiments/files/import-experiment-service.js +++ b/src/services/experiments/files/import-experiment-service.js @@ -3,7 +3,7 @@ import JSZip from 'jszip'; import endpoints from '../../proxy/data/endpoints.json'; import config from '../../../config.json'; -import ErrorHandlerService from '../../error-handler-service.js'; +import DialogService from '../../dialog-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.networkError(error)); + .catch(error => DialogService.instance.networkError(error)); } async zipExperimentFolder(event) { @@ -106,7 +106,7 @@ export default class ImportExperimentService extends HttpService { ) ) .catch(error => { - ErrorHandlerService.instance.dataError(error); + DialogService.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.dataError(error); + DialogService.instance.dataError(error); return Promise.reject(error); }); } @@ -124,7 +124,7 @@ export default class ImportExperimentService extends HttpService { return this.zipExperimentFolder(event).then(async zipContent => { return this.httpRequestPOST(importExperimentURL, zipContent, options) .then(response => response.json()) - .catch(error => ErrorHandlerService.instance.networkError(error) + .catch(error => DialogService.instance.networkError(error) ); }); } @@ -151,7 +151,7 @@ export default class ImportExperimentService extends HttpService { zipContents.map(zipContent => this.httpRequestPOST(importExperimentURL, zipContent, options) .catch(error => { - ErrorHandlerService.instance.networkError(error); + DialogService.instance.networkError(error); return Promise.reject(error); }) )