diff --git a/README.md b/README.md
index c68cb4b4fec901b9501f08ea8b2fd7dbce5533be..16b40bbf28344c5d2250a18237ec93bc06530467 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,6 @@
 
 NRP web-frontend 2.0 using React
 
-- created and built with Node v14.15.0
 
 ### Commands
 
diff --git a/package-lock.json b/package-lock.json
index 4a622054b67bd072dc2dd3536b7d13bbce6bfedb..085d53b6245f8d37a504c55ec286de6d71a37b4f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10346,6 +10346,11 @@
         }
       }
     },
+    "jquery": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
+      "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
+    },
     "js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -11399,6 +11404,21 @@
         "tslib": "^1.10.0"
       }
     },
+    "node": {
+      "version": "16.1.0",
+      "resolved": "https://registry.npmjs.org/node/-/node-16.1.0.tgz",
+      "integrity": "sha512-VCdVc3JI7WrGFwaentjwZsL936Gbzvi4k4y8pqWvml/7JM98oVY9mCv/WvzpPHr2NyZHXCngTiIa0Y26xufNSA==",
+      "dev": true,
+      "requires": {
+        "node-bin-setup": "^1.0.0"
+      }
+    },
+    "node-bin-setup": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/node-bin-setup/-/node-bin-setup-1.0.6.tgz",
+      "integrity": "sha512-uPIxXNis1CRbv1DwqAxkgBk5NFV3s7cMN/Gf556jSw6jBvV7ca4F9lRL/8cALcZecRibeqU+5dFYqFFmzv5a0Q==",
+      "dev": true
+    },
     "node-fetch": {
       "version": "2.6.1",
       "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
diff --git a/package.json b/package.json
index e4fa562d022406d00b3c4776c1d57fb97ef2c884..d8f260b2740c8660d6d4529f2fc8d8612e072cdc 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
     "@material-ui/lab": "4.0.0-alpha.57",
     "bootstrap": "4.5",
     "jszip": "3.2.0",
+    "jquery": "3.6.0",
     "react": "^17.0.1",
     "react-bootstrap": "1.4.0",
     "react-dom": "^17.0.1",
@@ -29,14 +30,15 @@
     "web-vitals": "^0.2.4"
   },
   "devDependencies": {
-    "jest-fetch-mock": "^3.0.3",
-    "jest-localstorage-mock": "2.4.6",
-    "msw": "^0.23.0",
     "@testing-library/jest-dom": "^5.11.5",
     "@testing-library/react": "^11.1.0",
     "@testing-library/user-event": "^12.1.10",
     "@typescript-eslint/parser": "4.14.2",
     "eslint": "7.19.0",
+    "jest-fetch-mock": "^3.0.3",
+    "jest-localstorage-mock": "2.4.6",
+    "msw": "^0.23.0",
+    "node": "16.1.0",
     "ts-node": "^9.1.1",
     "typescript": "4.1.3"
   },
diff --git a/src/App.js b/src/App.js
index 676465d36b760623aa6d05f08ea1b7ce948c82c4..8376431b189bcd3447684f2c77c62162d9e5549f 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 NotificationDialog from './components/dialog/notification-dialog.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 />
+        <NotificationDialog/>
         <HashRouter>
           <Switch>
             <Route path='/experiments-overview'>
diff --git a/src/components/dialog/error-dialog.css b/src/components/dialog/error-dialog.css
index 02e9b3a1eae2d741aa5f835cdf59ecc137e57c69..63c319bee51416bdc4d6ad21d6ab2765c3df81b4 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%;
@@ -7,6 +7,6 @@
 }
 
 .modal-header{
-  background-color: rgb(241, 185, 185);
+  background-color: rgb(238, 173, 173);
   color: rgb(138, 41, 41);
 }
\ No newline at end of file
diff --git a/src/components/dialog/error-dialog.js b/src/components/dialog/error-dialog.js
index 67746983eafb8e1ea2d304d4ec6f73e3ff134ee0..6c69fa7ef95c0a8c8a4be2ffbdc54302a35c04ed 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,16 @@ class ErrorDialog extends React.Component{
   }
 
   async componentDidMount() {
-    ErrorHandlerService.instance.addListener(
-      ErrorHandlerService.EVENTS.ERROR, (error) => {
-        this.onError(error);
-      });
+    this.onError = this.onError.bind(this);
+    DialogService.instance.addListener(
+      DialogService.EVENTS.ERROR, this.onError
+    );
+  }
+
+  componentWillUnmount() {
+    DialogService.instance.removeListener(
+      DialogService.EVENTS.ERROR, this.onError
+    );
   }
 
   onError(error) {
@@ -45,7 +51,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/notification-dialog.css b/src/components/dialog/notification-dialog.css
new file mode 100644
index 0000000000000000000000000000000000000000..b0ebdcec1e9cbf0955001ee874326c36633a4a3a
--- /dev/null
+++ b/src/components/dialog/notification-dialog.css
@@ -0,0 +1,25 @@
+.toast-notification-wrapper{
+  position: fixed;
+  bottom: 0;
+  right: 0;
+  z-index: 498;
+}
+
+.no-style{
+  list-style-type: none;
+}
+
+.toast-width{
+  max-width: none;
+  width: 350px;
+}
+
+.warning{
+  background-color:rgb(248, 248, 122);
+  color:rgb(126, 126, 45);
+}
+
+.info{
+  background-color: rgb(143, 143, 250);
+  color: rgb(48, 48, 134)
+}
diff --git a/src/components/dialog/notification-dialog.js b/src/components/dialog/notification-dialog.js
new file mode 100644
index 0000000000000000000000000000000000000000..26e0763f1ec2df8edc275bcfae201d5df72d29d2
--- /dev/null
+++ b/src/components/dialog/notification-dialog.js
@@ -0,0 +1,81 @@
+import React from 'react';
+import { Toast } from 'react-bootstrap';
+
+import DialogService from '../../services/dialog-service.js';
+
+import './notification-dialog.css';
+
+class NotificationDialog extends React.Component{
+  constructor(props){
+    super(props);
+    this.state = {
+      notifications: []
+    };
+  }
+
+  async componentDidMount() {
+    this.onNotification = this.onNotification.bind(this);
+    DialogService.instance.addListener(
+      DialogService.EVENTS.NOTIFICATION, this.onNotification
+    );
+  }
+
+  componentWillUnmount() {
+    DialogService.instance.removeListener(
+      DialogService.EVENTS.NOTIFICATION, this.onNotification
+    );
+  }
+
+  onNotification(notification) {
+    // avoid duplicates
+    var isIn = false;
+    this.state.notifications.forEach((notif) =>{
+      if (notification.type===notif.type && notification.message===notif.message){
+        isIn = true;
+      }
+    });
+    if (!isIn){
+      this.setState({
+        notifications: [...this.state.notifications, notification]
+      });
+    }
+  }
+
+  handleClose(index) {
+    var copy = [...this.state.notifications];
+    copy.splice(index, 1);
+    this.setState({
+      notifications: copy
+    });
+  }
+
+  render(){
+    let notifications = this.state.notifications;
+    return(
+      <div className='toast-notification-wrapper'>
+        {notifications.length!==0?
+          <ol>
+            {notifications.map((notification, index) => {
+              return (
+                <li key={index} className='no-style'>
+                  <Toast className='toast-width' onClose={(index) => this.handleClose(index)}
+                    delay={notification.type==='Warning'? 60000: 10000} autohide>
+                    <Toast.Header className={notification.type==='Warning'? 'warning': 'info'} >
+                      <strong className='mr-auto'>{notification.type}</strong>
+                    </Toast.Header>
+                    <Toast.Body>
+                      {notification.message}
+                    </Toast.Body>
+                  </Toast>
+                </li>
+              );
+            })}
+          </ol>
+          : null
+        }
+      </div>
+    );
+  }
+}
+
+export default NotificationDialog;
\ 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
index 222c1600500a988be4b9c8fbc92ad449d1c465bb..7accbc6b7f34e459a3f9baac908fd87bea7db936 100644
--- a/src/components/experiment-files-viewer/experiment-files-viewer.js
+++ b/src/components/experiment-files-viewer/experiment-files-viewer.js
@@ -99,7 +99,6 @@ export default class ExperimentFilesViewer extends React.Component {
   render() {
     let selectedExperimentFiles = this.state.selectedExperiment ?
       RemoteExperimentFilesService.instance.mapFileInfos.get(this.state.selectedExperiment.uuid) : undefined;
-
     return (
       <div>
         {RemoteExperimentFilesService.instance.isSupported() ?
diff --git a/src/components/experiment-list/experiment-list.css b/src/components/experiment-list/experiment-list.css
index 4eb57d703094f83aa7e2f18e273bf78002beda5d..0c37870a1e5a527454302288981f562662bedb70 100644
--- a/src/components/experiment-list/experiment-list.css
+++ b/src/components/experiment-list/experiment-list.css
@@ -1,4 +1,4 @@
-li.nostyle {
+.no-style {
     list-style-type: none;
 }
 
diff --git a/src/components/experiment-list/experiment-list.js b/src/components/experiment-list/experiment-list.js
index 8581fceab98712af9faf094b3d42b37e3e423c64..cb263408867e57fa3fde26e9d219f426efe3cf94 100644
--- a/src/components/experiment-list/experiment-list.js
+++ b/src/components/experiment-list/experiment-list.js
@@ -13,7 +13,7 @@ export default class ExperimentList extends React.Component {
           <ol>
             {this.props.experiments.map(experiment => {
               return (
-                <li key={experiment.id || experiment.configuration.id} className='nostyle'>
+                <li key={experiment.id || experiment.configuration.id} className='no-style'>
                   <ExperimentListElement experiment={experiment}
                     availableServers={this.props.availableServers}
                     startingExperiment={this.props.startingExperiment}
diff --git a/src/components/experiment-overview/experiment-overview.js b/src/components/experiment-overview/experiment-overview.js
index 43bc7ff7a7580d9f6e21c9921b06952316e014c7..efca5cc231cedc10837d2a24aa90503ce51f1d31 100644
--- a/src/components/experiment-overview/experiment-overview.js
+++ b/src/components/experiment-overview/experiment-overview.js
@@ -6,6 +6,7 @@ import ExperimentStorageService from '../../services/experiments/files/experimen
 import PublicExperimentsService from '../../services/experiments/files/public-experiments-service.js';
 import ExperimentServerService from '../../services/experiments/execution/server-resources-service.js';
 import ExperimentExecutionService from '../../services/experiments/execution/experiment-execution-service.js';
+import RemoteExperimentFilesService from '../../services/experiments/files/remote-experiment-files-service.js';
 
 import ImportExperimentButtons from '../experiment-list/import-experiment-buttons.js';
 import ExperimentList from '../experiment-list/experiment-list.js';
@@ -109,6 +110,13 @@ export default class ExperimentOverview extends React.Component {
     });
   }
 
+  onSelectTab(index, lastIndex){
+    this.setState({selectedTabIndex: index});
+    if (index===3 && lastIndex!==3){
+      RemoteExperimentFilesService.instance.notifyNotSupported();
+    }
+  }
+
   render() {
     return (
       <div className='experiment-overview-wrapper'>
@@ -118,7 +126,7 @@ export default class ExperimentOverview extends React.Component {
 
         <Tabs className="tabs-view" id="tabs-experiment-lists"
           selectedIndex={this.state.selectedTabIndex}
-          onSelect={(index) => this.setState({ selectedTabIndex: index })} >
+          onSelect={(index, lastIndex) => this.onSelectTab(index, lastIndex)} >
           <TabList>
             <Tab>My Experiments</Tab>
             <Tab>New Experiment</Tab>
diff --git a/src/mocks/handlers.js b/src/mocks/handlers.js
index 829ef5dc8a47c4b9f0a42bfd810faa383c070bff..01d5248a19a848b8ef55a9a58b97caf280a0f1df 100644
--- a/src/mocks/handlers.js
+++ b/src/mocks/handlers.js
@@ -10,6 +10,8 @@ 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 MockModels from './mock_models.json';
+import MockCustomModels from './mock_custom_models.json';
 
 import ImageAI from '../assets/images/Artificial_Intelligence_2.jpg';
 
@@ -58,6 +60,27 @@ export const handlers = [
   }),
   rest.post(`${config.api.proxy.url}${endpoints.proxy.identity.url}${endpoints.proxy.identity.gdpr.url}`,
     (req, res, ctx) => {
-      return res(ctx.json({'status':'success'}));
+      return res(ctx.json({ 'status': 'success' }));
+    }),
+  rest.get(`${config.api.proxy.url}${endpoints.proxy.models.url}/:modelType`,
+    (req, res, ctx) => {
+      return res(ctx.json(MockModels[0]));
+    }),
+  rest.post(`${config.api.proxy.url}${endpoints.proxy.models.url}/:modelType/:modelName`,
+    (req, res, ctx) => {
+      return res(ctx.json(MockCustomModels[2]));
+    }),
+  rest.get(`${config.api.proxy.url}${endpoints.proxy.storage.allCustomModels.url}/:modelType`,
+    (req, res, ctx) => {
+      return res(ctx.json(MockCustomModels[0]));
+    }),
+  rest.get(`${config.api.proxy.url}${endpoints.proxy.storage.userModels.url}/:modelType`,
+    (req, res, ctx) => {
+      return res(ctx.json(MockCustomModels[0]));
+    }),
+  rest.delete(`${config.api.proxy.url}${endpoints.proxy.storage.userModels.url}/:modelType/:modelName`,
+    (req, res, ctx) => {
+      return res(ctx.json(MockCustomModels[1]));
     })
+
 ];
\ No newline at end of file
diff --git a/src/mocks/mock_custom_models.json b/src/mocks/mock_custom_models.json
new file mode 100644
index 0000000000000000000000000000000000000000..3b312f522b3653e213da73a590223b99935ae14f
--- /dev/null
+++ b/src/mocks/mock_custom_models.json
@@ -0,0 +1,20 @@
+[
+    {
+        "name": "custom_hbp_clearpath_robotics_husky_a200",
+        "path": "robots/husky_model.zip",
+        "ownerName": "nrpuser",
+        "type": "robots",
+        "fileName": "husky_model.zip"
+    },
+    {
+        "name": "deleted_hbp_clearpath_robotics_husky_a200",
+        "ownerName": "nrpuser",
+        "type": "robots"
+    },
+    {
+        "name": "created_hbp_clearpath_robotics_husky_a200",
+        "ownerName": "nrpuser",
+        "type": "robots"
+    }
+
+]
\ No newline at end of file
diff --git a/src/mocks/mock_models.json b/src/mocks/mock_models.json
new file mode 100644
index 0000000000000000000000000000000000000000..2f2905b11a72d9128392831dcb72473bb215d1f4
--- /dev/null
+++ b/src/mocks/mock_models.json
@@ -0,0 +1,9 @@
+[
+    {
+        "name": "hbp_clearpath_robotics_husky_a200",
+        "path": "robots/husky_model.zip",
+        "ownerName": "nrpuser",
+        "type": "robots",
+        "fileName": "husky_model.zip"
+    }
+]
\ 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..1398e8d4f2555104e5742fc46cfa3aaaa684d524 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,37 @@ 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..3948e481b95a231826e39f3d8bf4fec30438c106 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 '../../../dialog-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..688b7075a9d903ab26fe4a7677ef25d3fa8798ae 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 '../../../dialog-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..4858fb3e08d5419b91f128323198b2d3112625a3 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 = () => {
+      DialogService.instance.progressNotification({message:'The experiment is loading'});
     };
 
     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 341e191e8dbfe51c5836397d79c329b6eaabcee2..e726c3a9c3dffcf6e1b8713634c4c9000275d842 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}`;
 
@@ -80,7 +80,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) {
@@ -117,7 +117,7 @@ export default class ImportExperimentService extends HttpService {
             )
           )
           .catch(error => {
-            ErrorHandlerService.instance.dataError(error);
+            DialogService.instance.dataError(error);
             return Promise.reject(error);
           })
       );
@@ -126,7 +126,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);
       });
   }
@@ -135,7 +135,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)
         );
     });
   }
@@ -162,7 +162,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);
             })
         )
diff --git a/src/services/experiments/files/remote-experiment-files-service.js b/src/services/experiments/files/remote-experiment-files-service.js
index c9625dfd3e824321a419847c877322edb857708e..7b85af51c074e92a6df2cfdcafec5eec66ae039d 100644
--- a/src/services/experiments/files/remote-experiment-files-service.js
+++ b/src/services/experiments/files/remote-experiment-files-service.js
@@ -1,6 +1,8 @@
 import { HttpService } from '../../http-service.js';
 import ExperimentStorageService from './experiment-storage-service';
 import getMimeByExtension from '../../../utility/mime-type';
+import DialogService from '../../dialog-service';
+import browserName from '../../../utility/browser-name';
 
 let _instance = null;
 const SINGLETON_ENFORCER = Symbol();
@@ -39,6 +41,14 @@ class RemoteExperimentFilesService extends HttpService {
     return window.showDirectoryPicker !== undefined && window.showDirectoryPicker !== null;
   }
 
+  notifyNotSupported() {
+    if (!this.isSupported()){
+      DialogService.instance.warningNotification({
+        message : 'The remote experiment file system is not supported on ' + browserName()
+      });
+    }
+  }
+
   toggleAutoSync() {
     this.autoSync = !this.autoSync;
   }
diff --git a/src/services/models/__tests__/models-storage-service.test.js b/src/services/models/__tests__/models-storage-service.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..ab2aeff297ab7de12f1240f7fd4159509b967ac5
--- /dev/null
+++ b/src/services/models/__tests__/models-storage-service.test.js
@@ -0,0 +1,70 @@
+/**
+ * @jest-environment jsdom
+*/
+import '@testing-library/jest-dom';
+import 'jest-fetch-mock';
+
+import ModelsStorageService from '../models-storage-service';
+
+test('makes sure that invoking the constructor fails with the right message', () => {
+  expect(() => {
+    new ModelsStorageService();
+  }).toThrow(Error);
+  expect(() => {
+    new ModelsStorageService();
+  }).toThrowError(Error('Use ModelsStorageService.instance'));
+});
+
+test('the service instance always refers to the same object', () => {
+  const instance1 = ModelsStorageService.instance;
+  const instance2 = ModelsStorageService.instance;
+  expect(instance1).toBe(instance2);
+});
+
+
+test('getTemplateModels function', async () => {
+
+  let modelsService = ModelsStorageService.instance;
+
+  // fetch template robots
+  let response = await modelsService.getTemplateModels(true, 'robots', false);
+  expect(response.name).toBe('hbp_clearpath_robotics_husky_a200');
+  expect(response.ownerName).toBe('nrpuser');
+  expect(response.type).toBe('robots');
+
+  // fetch custom robots
+  response = await modelsService.getTemplateModels(true, 'robots', true);
+  expect(response.name).toBe('custom_hbp_clearpath_robotics_husky_a200');
+  expect(response.ownerName).toBe('nrpuser');
+  expect(response.type).toBe('robots');
+});
+
+test('getCustomModelsByUser function', async () => {
+
+  let modelsService = ModelsStorageService.instance;
+
+  // fetch template robots
+  let response = await modelsService.getCustomModelsByUser('robots');
+  expect(response.name).toBe('custom_hbp_clearpath_robotics_husky_a200');
+  expect(response.ownerName).toBe('nrpuser');
+  expect(response.type).toBe('robots');
+
+});
+
+test('verifyModelType function', async () => {
+
+  let modelsService = ModelsStorageService.instance;
+  const expectedErrorPart = 'Error Type 400: Bad Request : The model type notRobots';
+  // fetch template robots
+  expect(() => modelsService.verifyModelType('notRobots')).toThrowError(expectedErrorPart);
+
+});
+
+test('setCustomModel function', async () => {
+  let modelsService = ModelsStorageService.instance;
+
+  let response = await modelsService.setCustomModel('robots', 'husky', 'fakeContent');
+  expect(response.name).toBe('created_hbp_clearpath_robotics_husky_a200');
+  expect(response.ownerName).toBe('nrpuser');
+  expect(response.type).toBe('robots');
+});
diff --git a/src/services/models/models-storage-service.js b/src/services/models/models-storage-service.js
new file mode 100644
index 0000000000000000000000000000000000000000..f9ae2bff1f24161f1d9e6912308873d4045b95ba
--- /dev/null
+++ b/src/services/models/models-storage-service.js
@@ -0,0 +1,145 @@
+import { HttpService } from '../http-service.js';
+
+import endpoints from '../proxy/data/endpoints.json';
+import config from '../../config.json';
+import ErrorHandlerService from '../error-handler-service';
+
+const storageModelsURL = `${config.api.proxy.url}${endpoints.proxy.models.url}`;
+const allCustomModelsURL = `${config.api.proxy.url}${endpoints.proxy.storage.allCustomModels.url}`;
+const userModelsURL = `${config.api.proxy.url}${endpoints.proxy.storage.userModels.url}`;
+
+let _instance = null;
+const SINGLETON_ENFORCER = Symbol();
+const availableModels = ['robots', 'brains', 'environments'];
+/**
+ * Service that manages the fetching and setting of custom and template
+ * models from the proxy.
+ */
+class ModelsStorageService extends HttpService {
+  constructor(enforcer) {
+    super();
+    if (enforcer !== SINGLETON_ENFORCER) {
+      throw new Error('Use ' + this.constructor.name + '.instance');
+    }
+  }
+
+  static get instance() {
+    if (_instance == null) {
+      _instance = new ModelsStorageService(SINGLETON_ENFORCER);
+    }
+
+    return _instance;
+  }
+
+  /**
+   * Retrieves the list of template or custom models from the proxy and stores
+   * them in the models class property. If the models are already
+   * there it just returns them, else does an HTTP request.
+   *
+   * @param {boolean} forceUpdate forces an update of the list
+   * @param {string} modelType one of the types
+   *                           ['robots', 'brains', 'environments']
+   * @param {boolean} allCustomModels if true fetch custom(user) models intead of templates
+   * @return models - the list of template models
+   */
+  async getTemplateModels(forceUpdate = false, modelType, allCustomModels = false) {
+    if (!this.models || forceUpdate) {
+      try {
+        this.verifyModelType(modelType);
+      }
+      catch (error) {
+        ErrorHandlerService.instance.dataError(error);
+      }
+
+      try {
+        const modelsWithTypeURL = allCustomModels ?
+          `${allCustomModelsURL}/${modelType}` :
+          `${storageModelsURL}/${modelType}`;
+        this.models = await (await this.httpRequestGET(modelsWithTypeURL)).json();
+      }
+      catch (error) {
+        ErrorHandlerService.instance.networkError(error);
+      }
+
+    }
+
+    return this.models;
+  }
+
+  /**
+   * Retrieves the list of custom models per user from the storage
+   *
+   * @param {string} modelType one of the types
+   *                           ['robots', 'brains', 'environments']
+   * @return models - the list of custom user models
+   */
+  async getCustomModelsByUser(modelType) {
+    try {
+      this.verifyModelType(modelType);
+      const customModelsURL = `${userModelsURL}/${modelType}`;
+      return (await this.httpRequestGET(customModelsURL)).json();
+    }
+    catch (error) {
+      ErrorHandlerService.instance.networkError(error);
+    }
+  }
+
+  /**
+   * Helper function that checks whether a specific type
+   * of model is in the list of available models.
+   *
+   * @param {string} modelType one of the types
+   *                           ['robots', 'brains', 'environments']
+   */
+  verifyModelType(modelType) {
+    if (!availableModels.includes(modelType)) {
+      throw new Error(
+        `Error Type 400: Bad Request : The model type ${modelType}
+        type that was requested is not one of brains, robots, environments.`);
+    }
+  }
+
+  /**
+    * Deletes a custom model from the storage
+    *
+    * @param {string} modelType one of the types
+    *                           ['robots', 'brains', 'environments']
+    * @param {string} modelName the name of the model to delete
+    * @return the response of the request
+    */
+
+  async deleteCustomModel(modelType, modelName) {
+    try {
+      this.verifyModelType(modelType);
+      const deleteCustomModelURL = `${userModelsURL}/${modelType}/${modelName}`;
+      return (await this.httpRequestDELETE(deleteCustomModelURL)).json();
+    }
+    catch (error) {
+      ErrorHandlerService.instance.dataError(error);
+    }
+  }
+
+  /**
+    * Sets a custom model to the storage
+    *
+    * @param {string} modelType one of the types
+    *                           ['robots', 'brains', 'environments']
+    * @param {string} modelName the name of the model to delete
+    * @param fileContent the data of the model to upload
+    * @return the response of the request
+    */
+  async setCustomModel(modelType, modelName, fileContent) {
+
+    try {
+      this.verifyModelType(modelType);
+      const setCustomModelURL = `${storageModelsURL}/${modelType}/${modelName}`;
+      return (await this.httpRequestPOST(setCustomModelURL, fileContent)).json();
+    }
+    catch (error) {
+      ErrorHandlerService.instance.networkError(error);
+    }
+  }
+}
+
+
+export default ModelsStorageService;
diff --git a/src/services/proxy/data/endpoints.json b/src/services/proxy/data/endpoints.json
index aa5c143975c1d800ae711fe54b13a30784c8122c..a2c1acd09cb0a2cd4a9ad8429d89db43003fe87c 100644
--- a/src/services/proxy/data/endpoints.json
+++ b/src/services/proxy/data/endpoints.json
@@ -9,6 +9,9 @@
         "experiments": {
             "url": "/experiments"
         },
+        "models": {
+            "url": "/models"
+        },
         "identity": {
             "url": "/identity",
             "me": {
@@ -37,6 +40,12 @@
             },
             "scanStorage": {
                 "url": "/storage/scanStorage"
+            },
+            "allCustomModels": {
+                "url": "/storage/models/all"
+            },
+            "userModels": {
+                "url": "/storage/models/user"
             }
         }
     }
diff --git a/src/utility/browser-name.js b/src/utility/browser-name.js
new file mode 100644
index 0000000000000000000000000000000000000000..af1274f248c922cbd27dd58a94f908d96325f1bf
--- /dev/null
+++ b/src/utility/browser-name.js
@@ -0,0 +1,37 @@
+
+function browserName(){
+  // CHROME
+  if (navigator.userAgent.indexOf('Chrome') !== -1 ) {
+    return 'Chrome';
+  }
+  // FIREFOX
+  else if (navigator.userAgent.indexOf('Firefox') !== -1 ) {
+    return 'Firefox';
+  }
+  // INTERNET EXPLORER
+  else if (navigator.userAgent.indexOf('MSIE') !== -1 ) {
+    return 'Internet Explorer';
+  }
+  // EDGE
+  else if (navigator.userAgent.indexOf('Edge') !== -1 ) {
+    return 'Edge';
+  }
+  // SAFARI
+  else if (navigator.userAgent.indexOf('Safari') !== -1 ) {
+    return 'Safari';
+  }
+  // OPERA
+  else if (navigator.userAgent.indexOf('Opera') !== -1 ) {
+    return 'Opera';
+  }
+  // YANDEX
+  else if (navigator.userAgent.indexOf('YaBrowser') !== -1 ) {
+    return 'YaBrowser';
+  }
+  // OTHER
+  else {
+    return 'Other';
+  }
+}
+
+export default browserName;
\ No newline at end of file