From 05b9427bec11688640d9bf1969b5f86d7886d9ac Mon Sep 17 00:00:00 2001
From: Sandro Weber <webers@in.tum.de>
Date: Mon, 4 Jan 2021 15:17:02 +0000
Subject: [PATCH] Merged in experiment-services-categories (pull request #9)

Experiment services categories

* WIP experiment start service

* experiments are being launched

* some CORS issue for slurm monitor

* server status indicators

* Merge branch 'development' into experiment-services-categories

* test mock files separated, experiment list selection

* some docu

* some docu

* moving files, removed console out

* adjust test
---
 package-lock.json                             | 401 +++++++++++++++++-
 package.json                                  |  50 ++-
 .../experiment-list-element.css               |  12 +
 .../experiment-list-element.js                | 156 +++++--
 .../experiment-list/experiment-list.js        |   4 +-
 src/mocks/handlers.js                         |  87 +---
 src/mocks/mock_available-servers.json         |  16 +
 src/mocks/mock_experiments.json               |  67 +++
 .../__mocks__/authentication-service.js       |   6 +-
 src/services/authentication-service.js        |   8 +-
 src/services/error-handler-service.js         |  28 ++
 .../execution/experiment-execution-service.js | 180 ++++++++
 .../execution/experiment-server-service.js    | 207 +++++++++
 .../experiments/experiment-constants.js       |  11 +
 .../experiment-storage-service.test.js        |   5 +-
 .../storage}/experiment-storage-service.js    |  22 +-
 src/services/http-service.js                  |  17 +-
 src/services/nrp-analytics-service.js         |  59 +++
 src/services/proxy/data/endpoints.json        |   3 +
 src/services/proxy/nrp-user-service.js        |   4 +-
 src/services/roslib-service.js                |  79 ++++
 21 files changed, 1263 insertions(+), 159 deletions(-)
 create mode 100644 src/mocks/mock_available-servers.json
 create mode 100644 src/mocks/mock_experiments.json
 create mode 100644 src/services/error-handler-service.js
 create mode 100644 src/services/experiments/execution/experiment-execution-service.js
 create mode 100644 src/services/experiments/execution/experiment-server-service.js
 create mode 100644 src/services/experiments/experiment-constants.js
 rename src/services/{proxy/experiment-services => experiments/storage}/__tests__/experiment-storage-service.test.js (86%)
 rename src/services/{proxy/experiment-services => experiments/storage}/experiment-storage-service.js (81%)
 create mode 100644 src/services/nrp-analytics-service.js
 create mode 100644 src/services/roslib-service.js

diff --git a/package-lock.json b/package-lock.json
index ee7220b..5427a9f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1258,18 +1258,24 @@
     },
     "@fortawesome/fontawesome-svg-core": {
       "version": "1.2.32",
+      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.32.tgz",
+      "integrity": "sha512-XjqyeLCsR/c/usUpdWcOdVtWFVjPbDFBTQkn2fQRrWhhUoxriQohO2RWDxLyUM8XpD+Zzg5xwJ8gqTYGDLeGaQ==",
       "requires": {
         "@fortawesome/fontawesome-common-types": "^0.2.32"
       }
     },
     "@fortawesome/free-solid-svg-icons": {
       "version": "5.15.1",
+      "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.1.tgz",
+      "integrity": "sha512-EFMuKtzRMNbvjab/SvJBaOOpaqJfdSap/Nl6hst7CgrJxwfORR1drdTV6q1Ib/JVzq4xObdTDcT6sqTaXMqfdg==",
       "requires": {
         "@fortawesome/fontawesome-common-types": "^0.2.32"
       }
     },
     "@fortawesome/react-fontawesome": {
       "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.12.tgz",
+      "integrity": "sha512-kV6HtqotM3K4YIXlTVvomuIi6QgGCvYm++ImyEx2wwgmSppZ6kbbA29ASwjAUBD63j2OFU0yoxeXpZkjrrX0qQ==",
       "requires": {
         "prop-types": "^15.7.2"
       }
@@ -2190,6 +2196,8 @@
     },
     "@testing-library/jest-dom": {
       "version": "5.11.5",
+      "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.11.5.tgz",
+      "integrity": "sha512-XI+ClHR864i6p2kRCEyhvpVejuer+ObVUF4cjCvRSF88eOMIfqw7RoS9+qoRhyigGswMfT64L6Nt0Ufotxbwtg==",
       "requires": {
         "@babel/runtime": "^7.9.2",
         "@types/testing-library__jest-dom": "^5.9.1",
@@ -2272,6 +2280,8 @@
     },
     "@testing-library/react": {
       "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.1.0.tgz",
+      "integrity": "sha512-Nfz58jGzW0tgg3irmTB7sa02JLkLnCk+QN3XG6WiaGQYb0Qc4Ok00aujgjdxlIQWZHbb4Zj5ZOIeE9yKFSs4sA==",
       "requires": {
         "@babel/runtime": "^7.11.2",
         "@testing-library/dom": "^7.26.0"
@@ -2279,6 +2289,8 @@
     },
     "@testing-library/user-event": {
       "version": "12.1.10",
+      "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-12.1.10.tgz",
+      "integrity": "sha512-StlNdKHp2Rpb7yrny/5/CGpz8bR3jLa1Ge59ODGU6TmAhkrxSpvR6tCD1gaMFkkjEUWkmmye8BaXsZPcaiJ6Ug==",
       "requires": {
         "@babel/runtime": "^7.10.2"
       }
@@ -2870,6 +2882,11 @@
         "regex-parser": "^2.2.11"
       }
     },
+    "after": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+      "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
+    },
     "aggregate-error": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
@@ -3047,6 +3064,11 @@
         "function-bind": "^1.1.1"
       }
     },
+    "arraybuffer.slice": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
+      "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
+    },
     "arrify": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
@@ -3590,6 +3612,11 @@
       "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
       "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ=="
     },
+    "backo2": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+      "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
+    },
     "balanced-match": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@@ -3650,11 +3677,21 @@
         }
       }
     },
+    "base64-arraybuffer": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+      "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
+    },
     "base64-js": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
       "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
     },
+    "base64id": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
+      "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY="
+    },
     "batch": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
@@ -3668,6 +3705,14 @@
         "tweetnacl": "^0.14.3"
       }
     },
+    "better-assert": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+      "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+      "requires": {
+        "callsite": "1.0.0"
+      }
+    },
     "bfj": {
       "version": "7.0.2",
       "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz",
@@ -3689,6 +3734,11 @@
       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
       "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ=="
     },
+    "blob": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
+      "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
+    },
     "bluebird": {
       "version": "3.7.2",
       "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -4007,6 +4057,11 @@
         "caller-callsite": "^2.0.0"
       }
     },
+    "callsite": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+      "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA="
+    },
     "callsites": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -4060,6 +4115,11 @@
       "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
       "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
     },
+    "cbor-js": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/cbor-js/-/cbor-js-0.1.0.tgz",
+      "integrity": "sha1-yAzmEg84fo+qdDcN/aIdlluPx/k="
+    },
     "chalk": {
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -4328,11 +4388,21 @@
       "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
       "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs="
     },
+    "component-bind": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+      "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
+    },
     "component-emitter": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
       "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
     },
+    "component-inherit": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+      "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
+    },
     "compose-function": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz",
@@ -4576,6 +4646,15 @@
         "sha.js": "^2.4.8"
       }
     },
+    "cross-fetch": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.6.tgz",
+      "integrity": "sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ==",
+      "dev": true,
+      "requires": {
+        "node-fetch": "2.6.1"
+      }
+    },
     "cross-spawn": {
       "version": "6.0.5",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@@ -5457,6 +5536,105 @@
         "once": "^1.4.0"
       }
     },
+    "engine.io": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.3.2.tgz",
+      "integrity": "sha512-AsaA9KG7cWPXWHp5FvHdDWY3AMWeZ8x+2pUVLcn71qE5AtAzgGbxuclOytygskw8XGmiQafTmnI9Bix3uihu2w==",
+      "requires": {
+        "accepts": "~1.3.4",
+        "base64id": "1.0.0",
+        "cookie": "0.3.1",
+        "debug": "~3.1.0",
+        "engine.io-parser": "~2.1.0",
+        "ws": "~6.1.0"
+      },
+      "dependencies": {
+        "cookie": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+          "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+        },
+        "ws": {
+          "version": "6.1.4",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz",
+          "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==",
+          "requires": {
+            "async-limiter": "~1.0.0"
+          }
+        }
+      }
+    },
+    "engine.io-client": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.3.2.tgz",
+      "integrity": "sha512-y0CPINnhMvPuwtqXfsGuWE8BB66+B6wTtCofQDRecMQPYX3MYUZXFNKDhdrSe3EVjgOu4V3rxdeqN/Tr91IgbQ==",
+      "requires": {
+        "component-emitter": "1.2.1",
+        "component-inherit": "0.0.3",
+        "debug": "~3.1.0",
+        "engine.io-parser": "~2.1.1",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "ws": "~6.1.0",
+        "xmlhttprequest-ssl": "~1.5.4",
+        "yeast": "0.1.2"
+      },
+      "dependencies": {
+        "component-emitter": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+          "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+        },
+        "ws": {
+          "version": "6.1.4",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz",
+          "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==",
+          "requires": {
+            "async-limiter": "~1.0.0"
+          }
+        }
+      }
+    },
+    "engine.io-parser": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz",
+      "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==",
+      "requires": {
+        "after": "0.8.2",
+        "arraybuffer.slice": "~0.0.7",
+        "base64-arraybuffer": "0.1.5",
+        "blob": "0.0.5",
+        "has-binary2": "~1.0.2"
+      }
+    },
     "enhanced-resolve": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz",
@@ -6197,6 +6375,11 @@
       "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
       "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
     },
+    "eventemitter2": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-4.1.2.tgz",
+      "integrity": "sha1-DhqEd6+CGm7zmVsxG/dMI6UkfxU="
+    },
     "eventemitter3": {
       "version": "4.0.7",
       "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -7162,6 +7345,26 @@
         "function-bind": "^1.1.1"
       }
     },
+    "has-binary2": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
+      "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
+      "requires": {
+        "isarray": "2.0.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+        }
+      }
+    },
+    "has-cors": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+      "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
+    },
     "has-flag": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@@ -7726,6 +7929,11 @@
       "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
       "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc="
     },
+    "indexof": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+      "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
+    },
     "infer-owner": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
@@ -8794,6 +9002,16 @@
         "jest-util": "^26.6.2"
       }
     },
+    "jest-fetch-mock": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz",
+      "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==",
+      "dev": true,
+      "requires": {
+        "cross-fetch": "^3.0.4",
+        "promise-polyfill": "^8.1.3"
+      }
+    },
     "jest-get-type": {
       "version": "26.3.0",
       "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz",
@@ -11022,6 +11240,11 @@
       "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
       "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
     },
+    "object-component": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
+      "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE="
+    },
     "object-copy": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
@@ -11360,6 +11583,22 @@
       "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
       "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="
     },
+    "parseqs": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+      "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
+    "parseuri": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+      "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
+      "requires": {
+        "better-assert": "~1.0.0"
+      }
+    },
     "parseurl": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -11552,6 +11791,11 @@
         }
       }
     },
+    "pngparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pngparse/-/pngparse-2.0.1.tgz",
+      "integrity": "sha1-hoUt5N40n077HoUudSVlXlrF37g="
+    },
     "pnp-webpack-plugin": {
       "version": "1.6.4",
       "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz",
@@ -12674,6 +12918,12 @@
       "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
       "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM="
     },
+    "promise-polyfill": {
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.0.tgz",
+      "integrity": "sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g==",
+      "dev": true
+    },
     "prompts": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz",
@@ -12860,6 +13110,8 @@
     },
     "react": {
       "version": "17.0.1",
+      "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz",
+      "integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==",
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1"
@@ -12880,6 +13132,8 @@
     },
     "react-bootstrap": {
       "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.4.0.tgz",
+      "integrity": "sha512-0BMzgeUAxH126v7VYDzIXbHxQVHSnniPVKpz9fblumdQpWaiElMnnzk+u8h8DoELX0nCXwPlcUzgXqmpncdc2Q==",
       "requires": {
         "@babel/runtime": "^7.4.2",
         "@restart/context": "^2.1.4",
@@ -12988,6 +13242,8 @@
     },
     "react-dom": {
       "version": "17.0.1",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz",
+      "integrity": "sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==",
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1",
@@ -13077,6 +13333,8 @@
     },
     "react-scripts": {
       "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-4.0.0.tgz",
+      "integrity": "sha512-icJ/ctwV5XwITUOupBP9TUVGdWOqqZ0H08tbJ1kVC5VpNWYzEZ3e/x8axhV15ZXRsixLo27snwQE7B6Zd9J2Tg==",
       "requires": {
         "@babel/core": "7.12.3",
         "@pmmmwh/react-refresh-webpack-plugin": "0.4.2",
@@ -13783,6 +14041,21 @@
         }
       }
     },
+    "roslib": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/roslib/-/roslib-1.1.0.tgz",
+      "integrity": "sha512-T1tZ3boTpOVvNrEscX1AZGieefD+W8kRilNZv8qHTESKo5KjYtighwp88KHsZbQQogVcROJWAUQVWok46DJJnQ==",
+      "requires": {
+        "cbor-js": "^0.1.0",
+        "eventemitter2": "^4.1.0",
+        "object-assign": "^4.0.1",
+        "pngparse": "^2.0.1",
+        "socket.io": "2.2.0",
+        "webworkify": "^1.5.0",
+        "ws": "^7.2.1",
+        "xmldom": "^0.1.19"
+      }
+    },
     "rsvp": {
       "version": "4.8.5",
       "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
@@ -14408,6 +14681,105 @@
         "kind-of": "^3.2.0"
       }
     },
+    "socket.io": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.2.0.tgz",
+      "integrity": "sha512-wxXrIuZ8AILcn+f1B4ez4hJTPG24iNgxBBDaJfT6MsyOhVYiTXWexGoPkd87ktJG8kQEcL/NBvRi64+9k4Kc0w==",
+      "requires": {
+        "debug": "~4.1.0",
+        "engine.io": "~3.3.1",
+        "has-binary2": "~1.0.2",
+        "socket.io-adapter": "~1.1.0",
+        "socket.io-client": "2.2.0",
+        "socket.io-parser": "~3.3.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        }
+      }
+    },
+    "socket.io-adapter": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz",
+      "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g=="
+    },
+    "socket.io-client": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.2.0.tgz",
+      "integrity": "sha512-56ZrkTDbdTLmBIyfFYesgOxsjcLnwAKoN4CiPyTVkMQj3zTUh0QAx3GbvIvLpFEOvQWu92yyWICxB0u7wkVbYA==",
+      "requires": {
+        "backo2": "1.0.2",
+        "base64-arraybuffer": "0.1.5",
+        "component-bind": "1.0.0",
+        "component-emitter": "1.2.1",
+        "debug": "~3.1.0",
+        "engine.io-client": "~3.3.1",
+        "has-binary2": "~1.0.2",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "object-component": "0.0.3",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "socket.io-parser": "~3.3.0",
+        "to-array": "0.1.4"
+      },
+      "dependencies": {
+        "component-emitter": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+          "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+        }
+      }
+    },
+    "socket.io-parser": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz",
+      "integrity": "sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ==",
+      "requires": {
+        "component-emitter": "~1.3.0",
+        "debug": "~3.1.0",
+        "isarray": "2.0.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "isarray": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+        }
+      }
+    },
     "sockjs": {
       "version": "0.3.20",
       "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz",
@@ -15267,6 +15639,11 @@
       "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
       "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE="
     },
+    "to-array": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
+      "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
+    },
     "to-arraybuffer": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
@@ -16058,7 +16435,9 @@
       }
     },
     "web-vitals": {
-      "version": "0.2.4"
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-0.2.4.tgz",
+      "integrity": "sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg=="
     },
     "webidl-conversions": {
       "version": "6.1.0",
@@ -16858,6 +17237,11 @@
       "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
       "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="
     },
+    "webworkify": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/webworkify/-/webworkify-1.5.0.tgz",
+      "integrity": "sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g=="
+    },
     "whatwg-encoding": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz",
@@ -17209,6 +17593,16 @@
       "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
       "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
     },
+    "xmldom": {
+      "version": "0.1.31",
+      "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz",
+      "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ=="
+    },
+    "xmlhttprequest-ssl": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
+      "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
+    },
     "xtend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -17285,6 +17679,11 @@
         }
       }
     },
+    "yeast": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+      "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
+    },
     "yocto-queue": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index d6199ec..69807f7 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,8 @@
     "react-dom": "^17.0.1",
     "react-router-dom": "5.2.0",
     "react-scripts": "4.0.0",
+    "roslib": "1.1.0",
+    "rxjs": "6.6.3",
     "ts-node": "^9.1.1",
     "web-vitals": "^0.2.4"
   },
@@ -26,17 +28,45 @@
     "eject": "react-scripts eject"
   },
   "eslintConfig": {
-    "extends": ["react-app", "react-app/jest"],
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ],
     "rules": {
-      "semi": [2, "always"],
-      "quotes": ["error", "single"],
-      "indent": ["error", 2],
-      "curly": ["error", "all"],
-      "block-spacing": ["error", "always"],
-      "brace-style": ["error", "stroustrup"],
-      "comma-dangle": ["error", "never"],
+      "semi": [
+        2,
+        "always"
+      ],
+      "quotes": [
+        "error",
+        "single"
+      ],
+      "indent": [
+        "error",
+        2
+      ],
+      "curly": [
+        "error",
+        "all"
+      ],
+      "block-spacing": [
+        "error",
+        "always"
+      ],
+      "brace-style": [
+        "error",
+        "stroustrup"
+      ],
+      "comma-dangle": [
+        "error",
+        "never"
+      ],
       "no-trailing-spaces": "error",
-      "array-bracket-newline": ["error", "consistent"]
+      "array-bracket-newline": [
+        "error",
+        "consistent"
+      ],
+      "max-len": ["error", { "code": 120 }]
     }
   },
   "browserslist": {
@@ -55,4 +85,4 @@
     "jest-fetch-mock": "^3.0.3",
     "msw": "^0.23.0"
   }
-}
+}
\ No newline at end of file
diff --git a/src/components/experiment-list/experiment-list-element.css b/src/components/experiment-list/experiment-list-element.css
index ec985d4..7383c3a 100644
--- a/src/components/experiment-list/experiment-list-element.css
+++ b/src/components/experiment-list/experiment-list-element.css
@@ -101,4 +101,16 @@
   margin-left: 10px;
   margin-top: 5px;
   margin-bottom: 5px;
+}
+
+.server-status-available {
+  background-color: #449d44;
+}
+
+.server-status-unavailable {
+  background-color: #d9534f;
+}
+
+.server-status-restricted {
+  background-color: #f0ad4e;
 }
\ No newline at end of file
diff --git a/src/components/experiment-list/experiment-list-element.js b/src/components/experiment-list/experiment-list-element.js
index b59bc8d..9ae86d2 100644
--- a/src/components/experiment-list/experiment-list-element.js
+++ b/src/components/experiment-list/experiment-list-element.js
@@ -1,31 +1,103 @@
 import React from 'react';
 import timeDDHHMMSS from '../../utility/time-filter.js';
-import ExperimentStorageService from '../../services/proxy/experiment-services/experiment-storage-service.js';
+import ExperimentStorageService from '../../services/experiments/storage/experiment-storage-service.js';
+import ExperimentExecutionService from '../../services/experiments/execution/experiment-execution-service.js';
 
 import './experiment-list-element.css';
+import ExperimentServerService from '../../services/experiments/execution/experiment-server-service.js';
+
+const CLUSTER_THRESHOLDS = {
+  UNAVAILABLE: 2,
+  AVAILABLE: 4
+};
+const SHORT_DESCRIPTION_LENGTH = 200;
 
 export default class ExperimentListElement extends React.Component {
   constructor(props) {
     super(props);
     this.state = {};
+
+    this.canLaunchExperiment = (this.props.experiment.private && this.props.experiment.owned) ||
+    !this.props.experiment.private;
+
+    this.wrapperRef = React.createRef();
+    this.handleClickOutside = this.handleClickOutside.bind(this);
   }
 
   async componentDidMount() {
     // retrieve the experiment thumbnail
-    let thumbnail = await ExperimentStorageService.instance.getThumbnail(this.props.experiment.name, this.props.experiment.configuration.thumbnail);
-    this.setState({thumbnail: URL.createObjectURL(thumbnail)});
+    let thumbnail = await ExperimentStorageService.instance.getThumbnail(
+      this.props.experiment.name,
+      this.props.experiment.configuration.thumbnail);
+    this.setState({ thumbnail: URL.createObjectURL(thumbnail) });
+
+    document.addEventListener('mousedown', this.handleClickOutside);
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener('mousedown', this.handleClickOutside);
+  }
+
+  handleClickOutside(event) {
+    if (this.wrapperRef && !this.wrapperRef.current.contains(event.target)) {
+      this.setState({selected: false});
+    }
+  }
+
+  getAvailabilityInfo() {
+    const experiment = this.props.experiment;
+    const clusterAvailability = ExperimentServerService.instance.getClusterAvailability();
+
+    let status;
+    if (clusterAvailability && clusterAvailability.free > CLUSTER_THRESHOLDS.AVAILABLE) {
+      status = 'Available';
+    }
+    else if (!experiment.availableServers || experiment.availableServers.length === 0) {
+      status = 'Unavailable';
+    }
+    else {
+      status = 'Restricted';
+    }
+
+    let cluster = `Cluster availability: ${clusterAvailability.free} / ${clusterAvailability.total}`;
+    let backends = `Backends: ${experiment.availableServers.length}`;
+
+    return `${status}\n${cluster}\n${backends}`;
+  }
+
+  getServerStatusClass() {
+    const experiment = this.props.experiment;
+    const clusterAvailability = ExperimentServerService.instance.getClusterAvailability();
+
+    let status = '';
+    if (clusterAvailability && clusterAvailability.free > CLUSTER_THRESHOLDS.AVAILABLE) {
+      status = 'server-status-available';
+    }
+    else if (!experiment.availableServers || experiment.availableServers.length === 0) {
+      status = 'server-status-unavailable';
+    }
+    else {
+      status = 'server-status-restricted';
+    }
+
+    return status;
   }
 
   render() {
     const exp = this.props.experiment;
     const config = this.props.experiment.configuration;
-    const pageState =this.props.pageState;
-    config.canLaunchExperiments = true;
+    const pageState = this.props.pageState;
+
     return (
-      <div className='list-entry-wrapper flex-container left-right' style={{position:'relative'}}>
-        <div className='list-entry-left' style={{position:'relative'}}>
+      <div className='list-entry-wrapper flex-container left-right'
+        style={{ position: 'relative' }}
+        onClick={() => this.setState({ selected: true})}
+        ref={this.wrapperRef}>
+
+        <div className='list-entry-left' style={{ position: 'relative' }}>
           <img className='entity-thumbnail' src={this.state.thumbnail} alt='' />
         </div>
+
         <div className='list-entry-middle flex-container up-down'>
           <div className='flex-container left-right title-line'>
             <div className='h4'>
@@ -34,57 +106,66 @@ export default class ExperimentListElement extends React.Component {
             <br />
           </div>
           <div>
-            {exp.configuration.description}
-            <br/>
+            {!this.state.selected && exp.configuration.description.length > SHORT_DESCRIPTION_LENGTH ?
+              exp.configuration.description.substr(0, SHORT_DESCRIPTION_LENGTH) + ' ...' :
+              exp.configuration.description}
+            <br />
           </div>
-          <div style={{position:'relative'}}>
+
+          {this.state.selected &&
+          <div style={{ position: 'relative' }} >
             <i>
               Timeout:
               {timeDDHHMMSS(exp.configuration.timeout)}
-              ({(exp.configuration.timeoutType==='simulation' ? 'simulation' : 'real')} time)
+              ({(exp.configuration.timeoutType === 'simulation' ? 'simulation' : 'real')} time)
             </i>
             <br />
             <i>
               Brain processes: {exp.configuration.brainProcesses}
             </i>
             <br />
-            <div style={{display:'flex'}}>
-              <i style={{marginTop: '4px'}}>Server status: </i>
-              <i className={{serverIcon: 1}} title='Restricted.'></i>
+            <div style={{ display: 'flex' }}>
+              <i style={{ marginTop: '4px' }}>Server status: </i>
+              <i className={'server-icon ' + this.getServerStatusClass()}
+                title={this.getAvailabilityInfo()}></i>
             </div>
-          </div>
-          <div className='list-entry-buttons flex-container' onClick={()=>{
+          </div>}
+
+          {this.state.selected &&
+          <div className='list-entry-buttons flex-container' onClick={() => {
             return exp.id === pageState.selected;
           }}>
             <div className='btn-group' role='group' >
-              {config.canLaunchExperiments && exp.joinableServers.length > 0 &&
-              exp.configuration.experimentFile && exp.configuration.bibiConfSr
-                ? <button onClick={()=>{
-                  return pageState.startingExperiment === exp.id;
+              {this.canLaunchExperiment && exp.availableServers.length > 0 &&
+                exp.configuration.experimentFile && exp.configuration.bibiConfSrc
+                ? <button onClick={() => {
+                  return ExperimentExecutionService.instance.startingExperiment === exp.id ||
+                    ExperimentExecutionService.instance.startNewExperiment(exp, false);
                 }}
-                disabled = {pageState.startingExperiment === exp.id || pageState.deletingExperiment}
+                disabled={pageState.startingExperiment === exp.id || pageState.deletingExperiment}
                 className='btn btn-default' >
                   <i className='fa fa-plus'></i> Launch
                 </button>
-                :null}
+                : null}
 
-              {config.canLaunchExperiments && exp.joinableServers.length === 0
-                ?<button className='btn btn-default disabled enable-tooltip'
+              {this.canLaunchExperiment && exp.availableServers.length === 0
+                ? <button className='btn btn-default disabled enable-tooltip'
                   title='Sorry, no available servers.'>
                   <i className='fa fa-plus'></i> Launch
                 </button>
                 : null}
 
-              {config.canLaunchExperiments && config.brainProcesses > 1 && exp.joinableServers.length > 0 &&
-              exp.configuration.experimentFile && exp.configuration.bibiConfSrc
+              {this.canLaunchExperiment && config.brainProcesses > 1 &&
+                exp.availableServers.length > 0 &&
+                exp.configuration.experimentFile && exp.configuration.bibiConfSrc
 
                 ? <button className='btn btn-default'>
                   <i className='fa fa-plus'></i> Launch in Single Process Mode
                 </button>
                 : null}
 
-              {config.canLaunchExperiments && exp.joinableServers.length > 1 &&
-                  exp.configuration.experimentFile && exp.configuration.bibiConfSrc
+              {this.canLaunchExperiment && exp.availableServers.length > 1 &&
+                exp.configuration.experimentFile && exp.configuration.bibiConfSrc
 
                 ? <button className='btn btn-default' >
                   <i className='fa fa-layer-group'></i> Launch Multiple
@@ -92,55 +173,56 @@ export default class ExperimentListElement extends React.Component {
                 : null}
 
               {/* isPrivateExperiment */}
-              {config.canLaunchExperiments
+              {this.canLaunchExperiment
                 ? <button className='btn btn-default'>
                   <i className='fa fa-times'></i> Delete
                 </button>
                 : null}
 
               {/* Records button */}
-              {config.canLaunchExperiments
+              {this.canLaunchExperiment
                 ? <button className='btn btn-default'>
                   <i className='fa fa-sign-in'></i> Recordings »
                 </button>
                 : null}
 
               {/* Export button */}
-              {config.canLaunchExperiments
+              {this.canLaunchExperiment
                 ? <button className='btn btn-default'>
                   <i className='fa fa-file-export'></i> Export
                 </button>
                 : null}
 
               {/* Join button */}
-              {config.canLaunchExperiments && exp.joinableServers.length > 0
+              {this.canLaunchExperiment && exp.joinableServers.length > 0
                 ? <button className='btn btn-default' >
                   <i className='fa fa-sign-in'></i> Simulations »
                 </button>
                 : null}
 
               {/* Clone button */}
-              {config.canCloneExperiments && (!exp.configuration.privateStorage || (exp.configuration.experimentFile && exp.configuration.bibiConfSrc))
+              {config.canCloneExperiments && (!exp.configuration.privateStorage ||
+                (exp.configuration.experimentFile && exp.configuration.bibiConfSrc))
                 ? <button className='btn btn-default'>
                   <i className='fa fa-pencil-alt'></i> Clone
                 </button>
                 : null}
 
               {/* Files button */}
-              {config.canLaunchExperiments
+              {this.canLaunchExperiment
                 ? <button className='btn btn-default' >
                   <i className='fa fa-list-alt'></i> Files
                 </button>
                 : null}
 
               {/* Shared button */}
-              {config.canLaunchExperiments
-                ? <button  className='btn btn-default'>
+              {this.canLaunchExperiment
+                ? <button className='btn btn-default'>
                   <i className='fas fa-share-alt'></i> Share
                 </button>
                 : null}
             </div>
-          </div>
+          </div>}
         </div>
       </div>
     );
diff --git a/src/components/experiment-list/experiment-list.js b/src/components/experiment-list/experiment-list.js
index a90c559..9087576 100644
--- a/src/components/experiment-list/experiment-list.js
+++ b/src/components/experiment-list/experiment-list.js
@@ -2,7 +2,7 @@ import React from 'react';
 import { Link } from 'react-router-dom';
 
 import UserMenu from '../user-menu/user-menu.js';
-import PrivateExperimentsService from '../../services/proxy/experiment-services/experiment-storage-service.js';
+import ExperimentStorageService from '../../services/experiments/storage/experiment-storage-service.js';
 
 import ExperimentListElement from './experiment-list-element.js';
 
@@ -19,7 +19,7 @@ export default class ExperimentList extends React.Component {
 
   async componentDidMount() {
     try {
-      const experiments = await PrivateExperimentsService.instance.getExperiments();
+      const experiments = await ExperimentStorageService.instance.getExperiments();
       this.setState({
         experiments: experiments
       });
diff --git a/src/mocks/handlers.js b/src/mocks/handlers.js
index 601f763..7c50248 100644
--- a/src/mocks/handlers.js
+++ b/src/mocks/handlers.js
@@ -2,91 +2,12 @@ import { rest } from 'msw';
 
 import config from '../config.json';
 import endpoints from '../services/proxy/data/endpoints';
+import MockExperiments from './mock_experiments.json';
+import MockAvailableServers from './mock_available-servers.json';
 
-const availableServers = [
-  {
-    'internalIp': 'http://localhost:8080',
-    'gzweb': {
-      'assets': 'http://localhost:8080/assets',
-      'nrp-services': 'http://localhost:8080',
-      'videoStreaming': 'http://localhost:8080/webstream/',
-      'websocket': 'ws://localhost:8080/gzbridge'
-    },
-    'rosbridge': {
-      'websocket': 'ws://localhost:8080/rosbridge'
-    },
-    'serverJobLocation': 'local',
-    'id': 'localhost'
-  }
-];
+const availableServers = MockAvailableServers;
+const experiments = MockExperiments;
 
-const experiments = [
-  {
-    'uuid': 'braitenberg_husky_holodeck_1_0_0',
-    'name': 'braitenberg_husky_holodeck_1_0_0',
-    'owned': true,
-    'joinableServers': [],
-    'configuration': {
-      'maturity': 'production',
-      'timeout': 840,
-      'timeoutType': 'real',
-      'name': 'Holodeck Husky Braitenberg experiment_changed_name',
-      'tags': [
-        'husky',
-        'robotics',
-        'holodeck',
-        'braitenberg'
-      ],
-      'thumbnail': 'ExDXMLExample.jpg',
-      'description': 'This experiment loads the Husky robot from Clearpath Robotics in the Holodeck environment.\n        If the user starts the experiment, the Braitenberg vehicle network is executed\n        and the robot will turn around itself in place, until the camera detects a red color. Then,\n        the robot will move towards the colored object. In this experiment, the user can interact\n        and change the color of both screens by clicking on them with the right mouse button.',
-      'cloneDate': '2019-11-19 16:35:55',
-      'cameraPose': [
-        5.056826,
-        -1.0210998,
-        2.6975987,
-        0,
-        0,
-        0.49999
-      ],
-      'experimentFile': '<ExD \n  xmlns:xsi=\'http://www.w3.org/2001/XMLSchema-instance\' \n  xmlns=\'http://schemas.humanbrainproject.eu/SP10/2014/ExDConfig\' xsi:schemaLocation=\'http://schemas.humanbrainproject.eu/SP10/2014/ExDConfig ../ExDConfFile.xsd\'>\n  <name>Holodeck Husky Braitenberg experiment_changed_name</name>\n  <thumbnail>ExDXMLExample.jpg</thumbnail>\n  <description>This experiment loads the Husky robot from Clearpath Robotics in the Holodeck environment.\n        If the user starts the experiment, the Braitenberg vehicle network is executed\n        and the robot will turn around itself in place, until the camera detects a red color. Then,\n        the robot will move towards the colored object. In this experiment, the user can interact\n        and change the color of both screens by clicking on them with the right mouse button.</description>\n  <tags>husky robotics holodeck braitenberg</tags>\n  <timeout>840</timeout>\n  <configuration type=\'3d-settings\' src=\'ExDXMLExample.ini\' />\n  <configuration type=\'brainvisualizer\' src=\'brainvisualizer.json\' />\n  <configuration type=\'user-interaction-settings\' src=\'ExDXMLExample.uis\' />\n  <maturity>production</maturity>\n  <environmentModel src=\'virtual_room.sdf\'>\n    <robotPose robotId=\'husky\' x=\'0.0\' y=\'0.0\' z=\'0.5\' roll=\'0.0\' pitch=\'-0.0\' yaw=\'3.14159265359\' />\n  </environmentModel>\n  <bibiConf src=\'bibi_configuration.bibi\' />\n  <cameraPose>\n    <cameraPosition x=\'5.056825994369357\' y=\'-1.0210998541555323\' z=\'2.697598759953974\' />\n    <cameraLookAt x=\'0\' y=\'0\' z=\'0.49999\' />\n  </cameraPose>\n  <cloneDate>2019-11-19T16:35:55</cloneDate>\n</ExD>',
-      'bibiConfSrc': 'bibi_configuration.bibi',
-      'visualModel': null,
-      'visualModelParams': []
-    },
-    'id': 'braitenberg_husky_holodeck_1_0_0',
-    'private': true
-  },
-  {
-    'uuid': 'template_new_0',
-    'name': 'template_new_0',
-    'owned': true,
-    'joinableServers': [],
-    'configuration': {
-      'maturity': 'production',
-      'timeout': 840,
-      'timeoutType': 'real',
-      'name': 'test',
-      'tags': [],
-      'thumbnail': 'TemplateNew.jpg',
-      'description': 'This new experiment is based on the models that you have selected. You are free to edit the description.',
-      'cloneDate': '2019-11-19 16:46:40',
-      'cameraPose': [
-        4.5,
-        0,
-        1.8,
-        0,
-        0,
-        0.6
-      ],
-      'experimentFile': '<ns1:ExD xmlns:ns1=\'http://schemas.humanbrainproject.eu/SP10/2014/ExDConfig\'>\n  <ns1:name>test</ns1:name>\n  <ns1:thumbnail>TemplateNew.jpg</ns1:thumbnail>\n  <ns1:description>This new experiment is based on the models that you have selected. You are free to edit the description.</ns1:description>\n  <ns1:timeout>840.0</ns1:timeout>\n  <ns1:configuration src=\'brainvisualizer.json\' type=\'brainvisualizer\'/>\n  <ns1:configuration src=\'TemplateNew.ini\' type=\'3d-settings\'/>\n  <ns1:configuration src=\'user-settings.uis\' type=\'user-interaction-settings\'/>\n  <ns1:maturity>production</ns1:maturity>\n  <ns1:environmentModel model=\'hbp_virtual_room\' src=\'virtual_room.sdf\'>\n    <ns1:robotPose pitch=\'0.0\' robotId=\'hbp_clearpath_robotics_husky_a200_0\' roll=\'0.0\' x=\'-0.662432968616\' y=\'-0.0223822146654\' yaw=\'0.0\' z=\'0.168474584818\'/>\n  </ns1:environmentModel>\n  <ns1:bibiConf src=\'bibi_configuration.bibi\'/>\n  <ns1:cameraPose>\n    <ns1:cameraPosition x=\'4.5\' y=\'0.0\' z=\'1.8\'/>\n    <ns1:cameraLookAt x=\'0.0\' y=\'0.0\' z=\'0.6\'/>\n  </ns1:cameraPose>\n  <ns1:cloneDate>2019-11-19T16:46:40</ns1:cloneDate>\n</ns1:ExD>\n',
-      'bibiConfSrc': 'bibi_configuration.bibi',
-      'visualModel': null,
-      'visualModelParams': []
-    },
-    'id': 'template_new_0',
-    'private': true
-  }
-];
 export const handlers = [
   rest.get(`${config.api.proxy.url}${endpoints.proxy.storage.experiments.url}`, (req, res, ctx) => {
     return res(
diff --git a/src/mocks/mock_available-servers.json b/src/mocks/mock_available-servers.json
new file mode 100644
index 0000000..ef023d1
--- /dev/null
+++ b/src/mocks/mock_available-servers.json
@@ -0,0 +1,16 @@
+[
+    {
+        "internalIp": "http://localhost:8080",
+        "gzweb": {
+            "assets": "http://localhost:8080/assets",
+            "nrp-services": "http://localhost:8080",
+            "videoStreaming": "http://localhost:8080/webstream/",
+            "websocket": "ws://localhost:8080/gzbridge"
+        },
+        "rosbridge": {
+            "websocket": "ws://localhost:8080/rosbridge"
+        },
+        "serverJobLocation": "local",
+        "id": "localhost"
+    }
+]
\ No newline at end of file
diff --git a/src/mocks/mock_experiments.json b/src/mocks/mock_experiments.json
new file mode 100644
index 0000000..5aba978
--- /dev/null
+++ b/src/mocks/mock_experiments.json
@@ -0,0 +1,67 @@
+[
+    {
+        "uuid": "braitenberg_husky_holodeck_1_0_0",
+        "name": "braitenberg_husky_holodeck_1_0_0",
+        "owned": true,
+        "joinableServers": [],
+        "configuration": {
+            "maturity": "production",
+            "timeout": 840,
+            "timeoutType": "real",
+            "name": "Holodeck Husky Braitenberg experiment_changed_name",
+            "tags": [
+                "husky",
+                "robotics",
+                "holodeck",
+                "braitenberg"
+            ],
+            "thumbnail": "ExDXMLExample.jpg",
+            "description": "This experiment loads the Husky robot from Clearpath Robotics in the Holodeck environment.\n        If the user starts the experiment, the Braitenberg vehicle network is executed\n        and the robot will turn around itself in place, until the camera detects a red color. Then,\n        the robot will move towards the colored object. In this experiment, the user can interact\n        and change the color of both screens by clicking on them with the right mouse button.",
+            "cloneDate": "2019-11-19 16:35:55",
+            "cameraPose": [
+                5.056826,
+                -1.0210998,
+                2.6975987,
+                0,
+                0,
+                0.49999
+            ],
+            "experimentFile": "<ExD \n  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" \n  xmlns=\"http://schemas.humanbrainproject.eu/SP10/2014/ExDConfig\" xsi:schemaLocation=\"http://schemas.humanbrainproject.eu/SP10/2014/ExDConfig ../ExDConfFile.xsd\">\n  <name>Holodeck Husky Braitenberg experiment_changed_name</name>\n  <thumbnail>ExDXMLExample.jpg</thumbnail>\n  <description>This experiment loads the Husky robot from Clearpath Robotics in the Holodeck environment.\n        If the user starts the experiment, the Braitenberg vehicle network is executed\n        and the robot will turn around itself in place, until the camera detects a red color. Then,\n        the robot will move towards the colored object. In this experiment, the user can interact\n        and change the color of both screens by clicking on them with the right mouse button.</description>\n  <tags>husky robotics holodeck braitenberg</tags>\n  <timeout>840</timeout>\n  <configuration type=\"3d-settings\" src=\"ExDXMLExample.ini\" />\n  <configuration type=\"brainvisualizer\" src=\"brainvisualizer.json\" />\n  <configuration type=\"user-interaction-settings\" src=\"ExDXMLExample.uis\" />\n  <maturity>production</maturity>\n  <environmentModel src=\"virtual_room.sdf\">\n    <robotPose robotId=\"husky\" x=\"0.0\" y=\"0.0\" z=\"0.5\" roll=\"0.0\" pitch=\"-0.0\" yaw=\"3.14159265359\" />\n  </environmentModel>\n  <bibiConf src=\"bibi_configuration.bibi\" />\n  <cameraPose>\n    <cameraPosition x=\"5.056825994369357\" y=\"-1.0210998541555323\" z=\"2.697598759953974\" />\n    <cameraLookAt x=\"0\" y=\"0\" z=\"0.49999\" />\n  </cameraPose>\n  <cloneDate>2019-11-19T16:35:55</cloneDate>\n</ExD>",
+            "bibiConfSrc": "bibi_configuration.bibi",
+            "visualModel": null,
+            "visualModelParams": []
+        },
+        "id": "braitenberg_husky_holodeck_1_0_0",
+        "private": true
+    },
+    {
+        "uuid": "template_new_0",
+        "name": "template_new_0",
+        "owned": true,
+        "joinableServers": [],
+        "configuration": {
+            "maturity": "production",
+            "timeout": 840,
+            "timeoutType": "real",
+            "name": "test",
+            "tags": [],
+            "thumbnail": "TemplateNew.jpg",
+            "description": "This new experiment is based on the models that you have selected. You are free to edit the description.",
+            "cloneDate": "2019-11-19 16:46:40",
+            "cameraPose": [
+                4.5,
+                0,
+                1.8,
+                0,
+                0,
+                0.6
+            ],
+            "experimentFile": "<ns1:ExD xmlns:ns1=\"http://schemas.humanbrainproject.eu/SP10/2014/ExDConfig\">\n  <ns1:name>test</ns1:name>\n  <ns1:thumbnail>TemplateNew.jpg</ns1:thumbnail>\n  <ns1:description>This new experiment is based on the models that you have selected. You are free to edit the description.</ns1:description>\n  <ns1:timeout>840.0</ns1:timeout>\n  <ns1:configuration src=\"brainvisualizer.json\" type=\"brainvisualizer\"/>\n  <ns1:configuration src=\"TemplateNew.ini\" type=\"3d-settings\"/>\n  <ns1:configuration src=\"user-settings.uis\" type=\"user-interaction-settings\"/>\n  <ns1:maturity>production</ns1:maturity>\n  <ns1:environmentModel model=\"hbp_virtual_room\" src=\"virtual_room.sdf\">\n    <ns1:robotPose pitch=\"0.0\" robotId=\"hbp_clearpath_robotics_husky_a200_0\" roll=\"0.0\" x=\"-0.662432968616\" y=\"-0.0223822146654\" yaw=\"0.0\" z=\"0.168474584818\"/>\n  </ns1:environmentModel>\n  <ns1:bibiConf src=\"bibi_configuration.bibi\"/>\n  <ns1:cameraPose>\n    <ns1:cameraPosition x=\"4.5\" y=\"0.0\" z=\"1.8\"/>\n    <ns1:cameraLookAt x=\"0.0\" y=\"0.0\" z=\"0.6\"/>\n  </ns1:cameraPose>\n  <ns1:cloneDate>2019-11-19T16:46:40</ns1:cloneDate>\n</ns1:ExD>\n",
+            "bibiConfSrc": "bibi_configuration.bibi",
+            "visualModel": null,
+            "visualModelParams": []
+        },
+        "id": "template_new_0",
+        "private": true
+    }
+]
\ No newline at end of file
diff --git a/src/services/__mocks__/authentication-service.js b/src/services/__mocks__/authentication-service.js
index 4223597..48c2c00 100644
--- a/src/services/__mocks__/authentication-service.js
+++ b/src/services/__mocks__/authentication-service.js
@@ -15,7 +15,8 @@ class AuthenticationService {
 
   /**
      * Checks if the current page URL contains access tokens.
-     * This happens when the successfully logging in at the proxy login page and being redirected back with the token info.
+     * This happens when the successfully logging in at the proxy login page and
+     * being redirected back with the token info.
      * Will automatically remove additional access info and present a clean URL after being redirected.
      */
   checkForNewTokenToStore() {
@@ -40,7 +41,8 @@ class AuthenticationService {
   /**
      * Opens the proxy's authentication page.
      *
-     * @param {*} url The URL of the authentication page. If not an absolute URL it is assumed to be a subpage of the proxy.
+     * @param {*} url The URL of the authentication page.
+     * If not an absolute URL it is assumed to be a subpage of the proxy.
      */
   openAuthenticationPage(url) {
 
diff --git a/src/services/authentication-service.js b/src/services/authentication-service.js
index 841b4d0..f69a04d 100644
--- a/src/services/authentication-service.js
+++ b/src/services/authentication-service.js
@@ -9,7 +9,7 @@ const SINGLETON_ENFORCER = Symbol();
 class AuthenticationService {
   constructor(enforcer) {
     if (enforcer !== SINGLETON_ENFORCER) {
-      throw new Error('Use AuthenticationService.instance');
+      throw new Error('Use ' + this.constructor.name + '.instance');
     }
 
     this.CLIENT_ID = config.auth.clientId;
@@ -29,7 +29,8 @@ class AuthenticationService {
 
   /**
    * Checks if the current page URL contains access tokens.
-   * This happens when the successfully logging in at the proxy login page and being redirected back with the token info.
+   * This happens when the successfully logging in at the proxy login page and
+   * being redirected back with the token info.
    * Will automatically remove additional access info and present a clean URL after being redirected.
    */
   checkForNewTokenToStore() {
@@ -83,7 +84,8 @@ class AuthenticationService {
   /**
    * Opens the proxy's authentication page.
    *
-   * @param {*} url The URL of the authentication page. If not an absolute URL it is assumed to be a subpage of the proxy.
+   * @param {*} url The URL of the authentication page.
+   * If not an absolute URL it is assumed to be a subpage of the proxy.
    */
   openAuthenticationPage(url) {
     this.clearStoredToken();
diff --git a/src/services/error-handler-service.js b/src/services/error-handler-service.js
new file mode 100644
index 0000000..8afc38f
--- /dev/null
+++ b/src/services/error-handler-service.js
@@ -0,0 +1,28 @@
+let _instance = null;
+const SINGLETON_ENFORCER = Symbol();
+
+/**
+ * Service taking care of OIDC/FS authentication for NRP accounts
+ */
+class ErrorHandlerService {
+  constructor(enforcer) {
+    if (enforcer !== SINGLETON_ENFORCER) {
+      throw new Error('Use ' + this.constructor.name + '.instance');
+    }
+  }
+
+  static get instance() {
+    if (_instance == null) {
+      _instance = new ErrorHandlerService(SINGLETON_ENFORCER);
+    }
+
+    return _instance;
+  }
+
+  displayServerHTTPError(error) {
+    //TODO: needs proper implementation
+    console.error(error);
+  }
+}
+
+export default ErrorHandlerService;
diff --git a/src/services/experiments/execution/experiment-execution-service.js b/src/services/experiments/execution/experiment-execution-service.js
new file mode 100644
index 0000000..e6ff4a1
--- /dev/null
+++ b/src/services/experiments/execution/experiment-execution-service.js
@@ -0,0 +1,180 @@
+import _ from 'lodash';
+
+import NrpAnalyticsService from '../../nrp-analytics-service.js';
+import ExperimentServerService from './experiment-server-service.js';
+import { HttpService } from '../../http-service.js';
+
+let _instance = null;
+const SINGLETON_ENFORCER = Symbol();
+
+/**
+ * Service handling the execution of experiments, e.g. starting/stopping simulations etc.
+ */
+class ExperimentExecutionService 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 ExperimentExecutionService(SINGLETON_ENFORCER);
+    }
+
+    return _instance;
+  }
+
+  /**
+   * Start a new simulation of an experiment, going through available servers.
+   *
+   * @param {object} experiment - experiment description
+   * @param {boolean} launchSingleMode - launch in single mode
+   * @param {object} reservation - server reservation
+   * @param {object} playbackRecording - a recording of a previous execution
+   * @param {*} profiler - a profiler option
+   */
+  startNewExperiment(
+    experiment,
+    launchSingleMode,
+    reservation,
+    playbackRecording,
+    profiler
+  ) {
+    NrpAnalyticsService.instance.eventTrack('Start', { category: 'Experiment' });
+    NrpAnalyticsService.instance.tickDurationEvent('Server-initialization');
+
+    this.startingExperiment = experiment;
+
+    let fatalErrorOccurred = false,
+      serversToTry = experiment.devServer
+        ? [experiment.devServer]
+        : experiment.availableServers.map(s => s.id);
+
+    let brainProcesses = launchSingleMode ? 1 : experiment.configuration.brainProcesses;
+
+    //TODO: placeholder, register actual progress callback later
+    let progressCallback = (msg) => {
+      console.info(msg);
+    };
+
+    let launchInNextServer = async () => {
+      let nextServer = serversToTry.splice(0, 1);
+      if (fatalErrorOccurred || !nextServer.length) {
+        //no more servers to retry, we have failed to start experiment
+        return Promise.reject(fatalErrorOccurred);
+      }
+
+      let server = nextServer[0];
+      let serverConfig = await ExperimentServerService.instance.getServerConfig(server);
+
+      return await this.launchExperimentOnServer(
+        experiment.id,
+        experiment.private,
+        brainProcesses,
+        server,
+        serverConfig,
+        reservation,
+        playbackRecording,
+        profiler,
+        progressCallback
+      ).catch((failure) => {
+        if (failure.error && failure.error.data) {
+          console.error('Failed to start simulation: ' + JSON.stringify(failure.error.data));
+        }
+        fatalErrorOccurred = fatalErrorOccurred || failure.isFatal;
+
+        return launchInNextServer();
+      });
+    };
+
+    return launchInNextServer();
+  };
+
+  /**
+   * Try launching an experiment on a specific server.
+   * @param {string} experimentID - ID of the experiment to launch
+   * @param {boolean} privateExperiment - whether the experiment is private or not
+   * @param {number} brainProcesses - number of brain processes to start with
+   * @param {string} server - server ID
+   * @param {object} serverConfiguration - configuration of server
+   * @param {object} reservation - server reservation
+   * @param {object} playbackRecording - recording
+   * @param {object} profiler - profiler option
+   * @param {function} progressCallback - a callback for progress updates
+   */
+  launchExperimentOnServer(
+    experimentID,
+    privateExperiment,
+    brainProcesses,
+    server,
+    serverConfiguration,
+    reservation,
+    playbackRecording,
+    profiler,
+    progressCallback
+  ) {
+    return new Promise((resolve, reject) => {
+      _.defer(() => {
+        //deferred.notify({ main: 'Create new Simulation...' });
+        progressCallback({ main: 'Create new Simulation...' });
+      }); //called once caller has the promise
+
+      let serverURL = serverConfiguration.gzweb['nrp-services'];
+      let serverJobLocation =
+        serverConfiguration.serverJobLocation || 'local';
+
+      let simInitData = {
+        gzserverHost: serverJobLocation,
+        private: privateExperiment,
+        experimentID: experimentID,
+        brainProcesses: brainProcesses,
+        reservation: reservation,
+        creationUniqueID: (Date.now() + Math.random()).toString(),
+        //ctxId: $stateParams.ctx, seems to not be used?
+        profiler: profiler
+      };
+
+      if (playbackRecording) {
+        simInitData.playbackPath = playbackRecording;
+      }
+
+      // Create a new simulation.
+      this.httpRequestPOST(serverURL + '/simulation', JSON.stringify(simInitData));
+      progressCallback({ main: 'Initialize Simulation...' });
+
+      // register for messages during initialization
+      ExperimentServerService.instance.registerForRosStatusInformation(
+        serverConfiguration.rosbridge.websocket,
+        progressCallback
+      );
+
+      ExperimentServerService.instance.simulationReady(serverURL, simInitData.creationUniqueID)
+        .then((simulation) => {
+          ExperimentServerService.instance.initConfigFiles(serverURL, simulation.simulationID)
+            .then(() => {
+              resolve(
+                'esv-private/experiment-view/' +
+                  server +
+                  '/' +
+                  experimentID +
+                  '/' +
+                  privateExperiment +
+                  '/' +
+                  simulation.simulationID
+              );
+              this.startingExperiment = undefined;
+            })
+            .catch((err) => {
+              reject(err);
+            });
+        })
+        .catch((err) => {
+          reject(err);
+        });
+    });
+  };
+}
+
+export default ExperimentExecutionService;
diff --git a/src/services/experiments/execution/experiment-server-service.js b/src/services/experiments/execution/experiment-server-service.js
new file mode 100644
index 0000000..8443c8d
--- /dev/null
+++ b/src/services/experiments/execution/experiment-server-service.js
@@ -0,0 +1,207 @@
+import _ from 'lodash';
+import {Subject, timer}from 'rxjs';
+import { switchMap, filter, map, multicast } from 'rxjs/operators';
+
+import ErrorHandlerService from '../../error-handler-service.js';
+import RoslibService from '../../roslib-service.js';
+import { HttpService } from '../../http-service.js';
+import { EXPERIMENT_STATE } from '../experiment-constants.js';
+
+import endpoints from '../../proxy/data/endpoints.json';
+import config from '../../../config.json';
+const proxyServerURL = `${config.api.proxy.url}${endpoints.proxy.server.url}`;
+const slurmMonitorURL = `${config.api.slurmmonitor.url}/api/v1/partitions/interactive`;
+
+let _instance = null;
+const SINGLETON_ENFORCER = Symbol();
+
+let rosConnections = new Map();
+const SLURM_MONITOR_POLL_INTERVAL = 5000;
+let clusterAvailability = { free: 'N/A', total: 'N/A' };
+
+/**
+ * Service handling server resources for simulating experiments.
+ */
+class ExperimentServerService extends HttpService {
+  constructor(enforcer) {
+    super();
+    if (enforcer !== SINGLETON_ENFORCER) {
+      throw new Error('Use ' + this.constructor.name + '.instance');
+    }
+
+    this.clusterAvailabilityObservable = timer(0, SLURM_MONITOR_POLL_INTERVAL)
+      .pipe(switchMap(() => {
+        try {
+          return this.httpRequestGET(slurmMonitorURL);
+        }
+        catch (error) {
+          _.once(error => {
+            if (error.status === -1) {
+              error = Object.assign(error, {
+                data: 'Could not probe vizualization cluster'
+              });
+            }
+            ErrorHandlerService.instance.displayServerHTTPError(error);
+          });
+        }
+      }))
+      .pipe(filter(e => e))
+      .pipe(map(({ free, nodes }) => ({ free, total: nodes[3] })))
+      .pipe(multicast(new Subject())).refCount();
+
+    this.startUpdates();
+  }
+
+  static get instance() {
+    if (_instance == null) {
+      _instance = new ExperimentServerService(SINGLETON_ENFORCER);
+    }
+
+    return _instance;
+  }
+
+  /**
+   * Start polling updates.
+   */
+  startUpdates() {
+    this.clusterAvailabilitySubscription = this.clusterAvailabilityObservable.subscribe(
+      availability => (clusterAvailability = availability)
+    );
+  }
+
+  /**
+   * Stop polling updates.
+   */
+  //TODO: find proper place to call
+  stopUpdates() {
+    this.clusterAvailabilitySubscription && this.clusterAvailabilitySubscription.unsubscribe();
+  }
+
+  /**
+   * Get available cluster server info.
+   * @returns {object} cluster availability info
+   */
+  getClusterAvailability() {
+    return clusterAvailability;
+  }
+
+  /**
+   * Get the server config for a given server ID.
+   * @param {string} serverID - ID of the server
+   * @returns {object} The server configuration
+   */
+  getServerConfig(serverID) {
+    return this.httpRequestGET(proxyServerURL + '/' + serverID)
+      .then(async (response) => {
+        return await response.json();
+      })
+      .catch(/*serverError.displayHTTPError*/ErrorHandlerService.instance.displayServerHTTPError);
+  }
+
+  /**
+   * Initialize config files on a server for a simulation.
+   * @param {string} serverBaseUrl - URL of the server
+   * @param {string} simulationID - ID of the simulation
+   * @returns {object} The initialized config files
+   */
+  async initConfigFiles(serverBaseUrl, simulationID) {
+    let cachedConfigFiles = undefined;
+    try {
+      let url = serverBaseUrl + '/simulation/' + simulationID + '/resources';
+      let response = await this.httpRequestGET(url);
+      cachedConfigFiles = await response.json().resources;
+    }
+    catch (error) {
+      ErrorHandlerService.instance.displayServerHTTPError(error);
+    }
+
+    return cachedConfigFiles;
+  }
+
+  /**
+   * Check whether a simulation on a server is ready to be started.
+   * @param {string} serverURL - URL of the server where simulation should be run
+   * @param {string} creationUniqueID - Unique ID generated while trying to launch experiment
+   * @returns {Promise} Whether simulation is ready to start
+   */
+  simulationReady(serverURL, creationUniqueID) {
+    return new Promise((resolve, reject) => {
+      let verifySimulation = () => {
+        setTimeout(() => {
+          this.httpRequestGET(serverURL + '/simulation').then(function(simulations) {
+            let continueVerify = true;
+
+            if (simulations.length > 0) {
+              let last = simulations.length - 1;
+              let state = simulations[last].state;
+
+              if (state === EXPERIMENT_STATE.PAUSED || state === EXPERIMENT_STATE.INITIALIZED) {
+                if (simulations[last].creationUniqueID === creationUniqueID) {
+                  continueVerify = false;
+                  resolve(simulations[last]);
+                }
+                else {
+                  reject();
+                }
+              }
+              else if (state === EXPERIMENT_STATE.HALTED || state === EXPERIMENT_STATE.FAILED) {
+                continueVerify = false;
+                reject();
+              }
+            }
+
+            if (continueVerify) {
+              verifySimulation();
+            }
+          }).catch(reject);
+        }, 1000);
+      };
+
+      verifySimulation();
+    });
+  };
+
+  /**
+   * Subscribe to status info topics.
+   * @param {string} rosbridgeWebsocket - ROS websocket URL
+   * @param {*} setProgressMessage - callback to be called with new status info
+   */
+  registerForRosStatusInformation(rosbridgeWebsocket, setProgressMessage) {
+    let destroyCurrentConnection = () => {
+      if (rosConnections.has(rosbridgeWebsocket)) {
+        let statusListener = rosConnections.get(rosbridgeWebsocket).statusListener;
+        // remove the progress bar callback only, unsubscribe terminates the rosbridge
+        // connection for any other subscribers on the status topic
+        statusListener.removeAllListeners();
+        rosConnections.delete(rosbridgeWebsocket);
+      }
+    };
+
+    destroyCurrentConnection();
+
+    let rosConnection = RoslibService.instance.getOrCreateConnectionTo(rosbridgeWebsocket);
+    let statusListener = RoslibService.instance.createStringTopic(
+      rosConnection,
+      config['ros-topics'].status
+    );
+    rosConnections.set(rosbridgeWebsocket, {rosConnection, statusListener});
+
+    statusListener.subscribe((data) => {
+      let message = JSON.parse(data.data);
+      if (message && message.progress) {
+        if (message.progress.done) {
+          destroyCurrentConnection();
+          setProgressMessage({ main: 'Simulation initialized.' });
+        }
+        else {
+          setProgressMessage({
+            main: message.progress.task,
+            sub: message.progress.subtask
+          });
+        }
+      }
+    });
+  };
+}
+
+export default ExperimentServerService;
diff --git a/src/services/experiments/experiment-constants.js b/src/services/experiments/experiment-constants.js
new file mode 100644
index 0000000..b09c414
--- /dev/null
+++ b/src/services/experiments/experiment-constants.js
@@ -0,0 +1,11 @@
+const EXPERIMENT_STATE = {
+  CREATED: 'created',
+  STARTED: 'started',
+  PAUSED: 'paused',
+  INITIALIZED: 'initialized',
+  HALTED: 'halted',
+  FAILED: 'failed',
+  STOPPED: 'stopped'
+};
+
+module.exports = {EXPERIMENT_STATE};
\ No newline at end of file
diff --git a/src/services/proxy/experiment-services/__tests__/experiment-storage-service.test.js b/src/services/experiments/storage/__tests__/experiment-storage-service.test.js
similarity index 86%
rename from src/services/proxy/experiment-services/__tests__/experiment-storage-service.test.js
rename to src/services/experiments/storage/__tests__/experiment-storage-service.test.js
index dbe6456..5a5d1be 100644
--- a/src/services/proxy/experiment-services/__tests__/experiment-storage-service.test.js
+++ b/src/services/experiments/storage/__tests__/experiment-storage-service.test.js
@@ -5,7 +5,7 @@ import '@testing-library/jest-dom';
 import 'jest-fetch-mock';
 
 import ExperimentStorageService from '../experiment-storage-service';
-import endpoints from '../../data/endpoints.json';
+import endpoints from '../../../proxy/data/endpoints.json';
 import config from '../../../../config.json';
 jest.mock('../../../authentication-service');
 
@@ -15,7 +15,8 @@ const experimentsUrl = `${config.api.proxy.url}${proxyEndpoint.storage.experimen
 test('fetches the list of experiments', async () => {
   jest.spyOn(ExperimentStorageService.instance, 'performRequest');
   const experiments = await ExperimentStorageService.instance.getExperiments();
-  expect(ExperimentStorageService.instance.performRequest).toHaveBeenCalledWith(experimentsUrl, ExperimentStorageService.instance.options);
+  expect(ExperimentStorageService.instance.performRequest)
+    .toHaveBeenCalledWith(experimentsUrl, ExperimentStorageService.instance.options);
   expect(experiments[0].name).toBe('braitenberg_husky_holodeck_1_0_0');
   expect(experiments[1].configuration.maturity).toBe('production');
   expect(experiments[1].availableServers[0].internalIp).toBe('http://localhost:8080');
diff --git a/src/services/proxy/experiment-services/experiment-storage-service.js b/src/services/experiments/storage/experiment-storage-service.js
similarity index 81%
rename from src/services/proxy/experiment-services/experiment-storage-service.js
rename to src/services/experiments/storage/experiment-storage-service.js
index bfff1d2..8cc5cd6 100644
--- a/src/services/proxy/experiment-services/experiment-storage-service.js
+++ b/src/services/experiments/storage/experiment-storage-service.js
@@ -1,8 +1,10 @@
-import endpoints from '../data/endpoints.json';
-import config from '../../../config.json';
-
 import { HttpService } from '../../http-service.js';
 
+import endpoints from '../../proxy/data/endpoints.json';
+import config from '../../../config.json';
+const storageExperimentsURL = `${config.api.proxy.url}${endpoints.proxy.storage.experiments.url}`;
+const availableServersURL = `${config.api.proxy.url}${endpoints.proxy.availableServers.url}`;
+
 let _instance = null;
 const SINGLETON_ENFORCER = Symbol();
 
@@ -12,11 +14,10 @@ const SINGLETON_ENFORCER = Symbol();
  */
 class ExperimentStorageService extends HttpService {
   constructor(enforcer) {
+    super();
     if (enforcer !== SINGLETON_ENFORCER) {
-      throw new Error('Use ExperimentStorageService.instance');
+      throw new Error('Use ' + this.constructor.name + '.instance');
     }
-
-    super();
   }
 
   static get instance() {
@@ -36,9 +37,7 @@ class ExperimentStorageService extends HttpService {
    */
   async getExperiments() {
     if (!this.experiments) {
-      const proxyEndpoint = endpoints.proxy;
-      const experimentsUrl = `${config.api.proxy.url}${proxyEndpoint.storage.experiments.url}`;
-      let response = await this.httpRequestGET(experimentsUrl);
+      let response = await this.httpRequestGET(storageExperimentsURL);
       this.experiments = await response.json();
       this.sortExperiments();
       await this.fillExperimentDetails();
@@ -55,7 +54,8 @@ class ExperimentStorageService extends HttpService {
    * @returns {Blob} image object
    */
   async getThumbnail(experimentName, thumbnailFilename) {
-    let url = config.api.proxy.url + endpoints.proxy.storage.url + '/' + experimentName + '/' + thumbnailFilename + '?byname=true';
+    let url = config.api.proxy.url + endpoints.proxy.storage.url +
+      '/' + experimentName + '/' + thumbnailFilename + '?byname=true';
     let response = await this.httpRequestGET(url);
     let image = await response.blob();
     return image;
@@ -78,8 +78,6 @@ class ExperimentStorageService extends HttpService {
   }
 
   async fillExperimentDetails() {
-    //TODO: needs to go into its own service
-    const availableServersURL = `${config.api.proxy.url}${endpoints.proxy.availableServers.url}`;
     let response = await this.httpRequestGET(availableServersURL);
     let availableServers = await response.json();
 
diff --git a/src/services/http-service.js b/src/services/http-service.js
index 1032438..0e1e89c 100644
--- a/src/services/http-service.js
+++ b/src/services/http-service.js
@@ -19,8 +19,11 @@ export class HttpService {
         'Content-Type': 'application/json',
         Referer: 'http://localhost:9000/'
       },
-      redirect: 'follow', // manual, *follow, error
-      referrerPolicy: 'no-referrer' // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
+      // redirect: manual, *follow, error
+      redirect: 'follow',
+      // referrerPolicy: no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin,
+      // strict-origin, strict-origin-when-cross-origin, unsafe-url
+      referrerPolicy: 'no-referrer'
       //body: JSON.stringify(data) // body data type must match "Content-Type" header
     };
   }
@@ -30,9 +33,13 @@ export class HttpService {
    * @param url - the url to perform the request
    * @param options - the http options object
    */
-  performRequest = async (url, options) => {
+  performRequest = async (url, options, data) => {
     // Add authorization header
     options.headers.Authorization = `Bearer ${AuthenticationService.instance.getStoredToken()}`;
+    if (data) {
+      console.info(options);
+      options.body = data;
+    }
 
     const response = await fetch(url, options);
 
@@ -66,12 +73,12 @@ export class HttpService {
    * Perform a POST http request to a url
    * @param url - the url to perform the request
    */
-  httpRequestPOST = (url) => {
+  httpRequestPOST = (url, data) => {
     // copy to avoid messing up the options object in case we need to reuse it
     const { ...postOptions } = this.options;
     postOptions.method = 'POST';
 
-    return this.performRequest(url, postOptions);
+    return this.performRequest(url, postOptions, data);
   };
 
   /**
diff --git a/src/services/nrp-analytics-service.js b/src/services/nrp-analytics-service.js
new file mode 100644
index 0000000..f00751e
--- /dev/null
+++ b/src/services/nrp-analytics-service.js
@@ -0,0 +1,59 @@
+import _ from 'lodash';
+
+import NrpUserService from './proxy/nrp-user-service.js';
+
+let _instance = null;
+const SINGLETON_ENFORCER = Symbol();
+
+let durationClocks = {};
+
+/**
+ * Service taking care of OIDC/FS authentication for NRP accounts
+ */
+class NrpAnalyticsService {
+  constructor(enforcer) {
+    if (enforcer !== SINGLETON_ENFORCER) {
+      throw new Error('Use ' + this.constructor.name + '.instance');
+    }
+  }
+
+  static get instance() {
+    if (_instance == null) {
+      _instance = new NrpAnalyticsService(SINGLETON_ENFORCER);
+    }
+
+    return _instance;
+  }
+
+  eventTrack(actionName, options) {
+    if (_.isObject(options) && _.isBoolean(options.value)) {
+      options.value = _.toInteger(options.value);
+    }
+    return NrpUserService.instance.getCurrentUser().then((user) => {
+      var extendedOptions = _.extend(options, {
+        label: user.displayName
+      });
+      //$analytics.eventTrack(actionName, extendedOptions);
+      console.error('implement $analytics.eventTrack(actionName, extendedOptions)');
+    });
+  }
+
+  tickDurationEvent(actionName) {
+    durationClocks[actionName] = Date.now();
+  }
+
+  durationEventTrack(actionName, options) {
+    if (_.isUndefined(durationClocks[actionName])) {
+      console.debug('Analytics duration: missing tick for action: ' + actionName);
+      return;
+    }
+    var duration = Date.now() - durationClocks[actionName];
+    var extendedOptions = _.extend(options, {
+      value: duration / 1000
+    });
+    this.eventTrack(actionName, extendedOptions);
+    delete durationClocks[actionName];
+  }
+}
+
+export default NrpAnalyticsService;
diff --git a/src/services/proxy/data/endpoints.json b/src/services/proxy/data/endpoints.json
index 8c1c84b..8900864 100644
--- a/src/services/proxy/data/endpoints.json
+++ b/src/services/proxy/data/endpoints.json
@@ -15,6 +15,9 @@
                 }
             }
         },
+        "server": {
+            "url": "/server"
+        },
         "storage": {
             "url": "/storage",
             "experiments": {
diff --git a/src/services/proxy/nrp-user-service.js b/src/services/proxy/nrp-user-service.js
index 4546ca4..e528d34 100644
--- a/src/services/proxy/nrp-user-service.js
+++ b/src/services/proxy/nrp-user-service.js
@@ -14,11 +14,11 @@ const SINGLETON_ENFORCER = Symbol();
  */
 class NrpUserService extends HttpService {
   constructor(enforcer) {
+    super();
     if (enforcer !== SINGLETON_ENFORCER) {
-      throw new Error('Use NrpUserService.instance');
+      throw new Error('Use ' + this.constructor.name + '.instance');
     }
 
-    super();
 
     this.PROXY_URL = config.api.proxy.url;
     this.IDENTITY_BASE_URL = `${this.PROXY_URL}${endpoints.proxy.identity.url}`;
diff --git a/src/services/roslib-service.js b/src/services/roslib-service.js
new file mode 100644
index 0000000..db24e1a
--- /dev/null
+++ b/src/services/roslib-service.js
@@ -0,0 +1,79 @@
+import * as ROSLIB from 'roslib';
+import _ from 'lodash';
+
+import AuthenticationService from './authentication-service.js';
+
+let _instance = null;
+const SINGLETON_ENFORCER = Symbol();
+
+/**
+ * Service taking care of OIDC/FS authentication for NRP accounts
+ */
+class RoslibService {
+  constructor(enforcer) {
+    if (enforcer !== SINGLETON_ENFORCER) {
+      throw new Error('Use ' + this.constructor.name + '.instance');
+    }
+  }
+
+  static get instance() {
+    if (_instance == null) {
+      _instance = new RoslibService(SINGLETON_ENFORCER);
+    }
+
+    return _instance;
+  }
+
+  /**
+   * Create a new connection or return an existing one to a ROS websocket.
+   * @param {string} url - URL of the ROS websocket to connect to
+   */
+  getOrCreateConnectionTo(url) {
+    url = url + '?token=' + AuthenticationService.instance.getStoredToken();
+
+    return new ROSLIB.Ros({ url: url });
+  };
+
+  /**
+   * Create a new ROSLIB.Topic.
+   * @param {object} connection - the ROSLIB.Ros connection
+   * @param {*} topicName - name of the topic
+   * @param {*} messageType - message type
+   * @param {*} additionalOptions - additional options to extend the ROSLIB.Topic with
+   */
+  createTopic(connection, topicName, messageType, additionalOptions) {
+    return new ROSLIB.Topic(
+      _.extend(
+        {
+          ros: connection,
+          name: topicName,
+          messageType: messageType
+        },
+        additionalOptions
+      )
+    );
+  };
+
+  createStringTopic(connection, topicName) {
+    return new ROSLIB.Topic({
+      ros: connection,
+      name: topicName,
+      messageType: 'std_msgs/String'
+    });
+  };
+
+  createService(connection, serviceName, additionalOptions) {
+    return new ROSLIB.Service(
+      _.extend(
+        {
+          ros: connection,
+          name: serviceName,
+          serviceType: serviceName
+        },
+        additionalOptions
+      )
+    );
+  };
+}
+
+export default RoslibService;
-- 
GitLab