[CORD-1044] Migrating vTR Dashboard to the new GUI using gui-extensions

Change-Id: I33847766b790ffba2b9a9e9cfab9a7060734ce91
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a016a1e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.idea/
+xos/gui/node_modules/
+xos/gui/.tmp/
+xos/gui/typings/
+npm-debug.log
diff --git a/xos/gui/Dockerfile b/xos/gui/Dockerfile
new file mode 100644
index 0000000..b8a636c
--- /dev/null
+++ b/xos/gui/Dockerfile
@@ -0,0 +1,24 @@
+FROM xosproject/xos-gui-extension-builder
+
+# Set environment vars
+ENV CODE_SOURCE .
+ENV CODE_DEST /var/www
+ENV VHOST /var/www/dist
+
+# Add the app deps
+COPY ${CODE_SOURCE}/package.json ${CODE_DEST}/package.json
+COPY ${CODE_SOURCE}/typings.json ${CODE_DEST}/typings.json
+
+# Install Deps
+WORKDIR ${CODE_DEST}
+RUN npm install
+RUN npm run typings
+
+# Build the app
+COPY ${CODE_SOURCE}/conf ${CODE_DEST}/conf
+COPY ${CODE_SOURCE}/gulp_tasks ${CODE_DEST}/gulp_tasks
+COPY ${CODE_SOURCE}/src ${CODE_DEST}/src
+COPY ${CODE_SOURCE}/gulpfile.js ${CODE_DEST}/gulpfile.js
+COPY ${CODE_SOURCE}/tsconfig.json ${CODE_DEST}/tsconfig.json
+COPY ${CODE_SOURCE}/tslint.json ${CODE_DEST}/tslint.json
+RUN npm run build
diff --git a/xos/gui/README.md b/xos/gui/README.md
new file mode 100644
index 0000000..836b948
--- /dev/null
+++ b/xos/gui/README.md
@@ -0,0 +1,16 @@
+# vTR GUI
+
+This GUI provide the interface to perform a vTR test.
+
+## Platform install integration
+
+Having a profile deployed is required. To add extensions listed in your `profile-manifest` as:
+
+```
+enabled_gui_extensions:
+  - name: vtr
+    path: orchestration/xos_services/vtr/xos/gui
+```
+
+Execute: `ansible-playbook -i inventory/mock-rcord deploy-xos-gui-extensions-playbook.yml`
+_NOTE: remember to replate `inventory/**` with the actual `cord_profile` you are using_ 
\ No newline at end of file
diff --git a/xos/gui/conf/app/README.md b/xos/gui/conf/app/README.md
new file mode 100755
index 0000000..bdc361a
--- /dev/null
+++ b/xos/gui/conf/app/README.md
@@ -0,0 +1,47 @@
+# XOS-GUI Config
+
+### Note that the configurations defined in this folder are for development only, they are most likely to be overrided by a volume mount defined in `service-profile`
+
+## App Config
+
+This configuration will specifiy the rest API base url and the Websocket address.
+Here is it's structure:
+
+```
+angular.module('app')
+  .constant('AppConfig', {
+    apiEndpoint: '/spa/api',
+    websocketClient: '/'
+  });
+
+```
+
+## Style Config
+
+This configuration will contain branding information, such as title, logo and navigation items.
+Here is it's structure:
+
+```
+angular.module('app')
+  .constant('StyleConfig', {
+    projectName: 'CORD',
+    favicon: 'cord-favicon.png',
+    background: 'cord-bg.jpg',
+    payoff: 'Your VNF orchestrator',
+    logo: 'cord-logo.png',
+    routes: [
+        {
+            label: 'Slices',
+            state: 'xos.core.slices'
+        },
+        {
+            label: 'Instances',
+            state: 'xos.core.instances'
+        },
+        {
+            label: 'Nodes',
+            state: 'xos.core.nodes'
+        }
+    ]
+});
+```
\ No newline at end of file
diff --git a/xos/gui/conf/app/app.config.dev.js b/xos/gui/conf/app/app.config.dev.js
new file mode 100755
index 0000000..5c620d7
--- /dev/null
+++ b/xos/gui/conf/app/app.config.dev.js
@@ -0,0 +1,5 @@
+angular.module('app')
+  .constant('AppConfig', {
+    apiEndpoint: 'http://xos.dev:3000/api',
+    websocketClient: 'http://xos.dev:3000'
+  });
diff --git a/xos/gui/conf/app/app.config.local.ts b/xos/gui/conf/app/app.config.local.ts
new file mode 100755
index 0000000..a00155b
--- /dev/null
+++ b/xos/gui/conf/app/app.config.local.ts
@@ -0,0 +1,5 @@
+import {IAppConfig} from './interfaces';
+export const AppConfig: IAppConfig = {
+  apiEndpoint: 'http://localhost:4000/api',
+  websocketClient: 'http://localhost:4000/'
+};
diff --git a/xos/gui/conf/app/app.config.production.js b/xos/gui/conf/app/app.config.production.js
new file mode 100755
index 0000000..8f5bd5a
--- /dev/null
+++ b/xos/gui/conf/app/app.config.production.js
@@ -0,0 +1,5 @@
+angular.module('app')
+  .constant('AppConfig', {
+    apiEndpoint: '/spa/api',
+    websocketClient: '/'
+  });
diff --git a/xos/gui/conf/app/app.config.test.js b/xos/gui/conf/app/app.config.test.js
new file mode 100755
index 0000000..86cfcb1
--- /dev/null
+++ b/xos/gui/conf/app/app.config.test.js
@@ -0,0 +1,5 @@
+angular.module('app')
+  .constant('AppConfig', {
+      apiEndpoint: 'http://xos-test:3000/api',
+      websocketClient: 'http://xos-test:3000'
+  });
diff --git a/xos/gui/conf/app/style.config.cord.js b/xos/gui/conf/app/style.config.cord.js
new file mode 100755
index 0000000..6f7ebab
--- /dev/null
+++ b/xos/gui/conf/app/style.config.cord.js
@@ -0,0 +1,22 @@
+angular.module('app')
+  .constant('StyleConfig', {
+    projectName: 'CORD',
+    favicon: 'cord-favicon.png',
+    background: 'cord-bg.jpg',
+    payoff: 'Your VNF orchestrator',
+    logo: 'cord-logo.png',
+    routes: [
+        {
+            label: 'Slices',
+            state: 'xos.core.slices'
+        },
+        {
+            label: 'Instances',
+            state: 'xos.core.instances'
+        },
+        {
+            label: 'Nodes',
+            state: 'xos.core.nodes'
+        }
+    ]
+});
diff --git a/xos/gui/conf/app/style.config.opencloud.js b/xos/gui/conf/app/style.config.opencloud.js
new file mode 100755
index 0000000..9693c5d
--- /dev/null
+++ b/xos/gui/conf/app/style.config.opencloud.js
@@ -0,0 +1,14 @@
+angular.module('app')
+  .constant('StyleConfig', {
+    projectName: 'OpenCloud',
+    favicon: 'opencloud-favicon.png',
+    background: 'opencloud-bg.jpg',
+    payoff: 'Your OS resource manager',
+    logo: 'opencloud-logo.png',
+    routes: [
+        {
+            label: 'Slices',
+            state: 'xos.core.slices'
+        }
+    ]
+});
diff --git a/xos/gui/conf/browsersync-dist.conf.js b/xos/gui/conf/browsersync-dist.conf.js
new file mode 100755
index 0000000..fa45845
--- /dev/null
+++ b/xos/gui/conf/browsersync-dist.conf.js
@@ -0,0 +1,12 @@
+const conf = require('./gulp.conf');
+
+module.exports = function () {
+  return {
+    server: {
+      baseDir: [
+        conf.paths.dist
+      ]
+    },
+    open: false
+  };
+};
diff --git a/xos/gui/conf/browsersync.conf.js b/xos/gui/conf/browsersync.conf.js
new file mode 100755
index 0000000..430c94f
--- /dev/null
+++ b/xos/gui/conf/browsersync.conf.js
@@ -0,0 +1,25 @@
+const conf = require('./gulp.conf');
+const proxy = require('./proxy');
+
+module.exports = function () {
+  return {
+    server: {
+      baseDir: [
+        conf.paths.tmp,
+        conf.paths.src
+      ],
+      middleware: function (req, res, next) {
+        if (req.url.indexOf('xosapi') !== -1) {
+          proxy.api.web(req, res);
+        }
+        else if (req.url.indexOf('spa') !== -1 || req.url.indexOf('socket') !== -1) {
+          proxy.static.web(req, res);
+        }
+        else {
+          next();
+        }
+      }
+    },
+    open: false
+  };
+};
diff --git a/xos/gui/conf/gulp.conf.js b/xos/gui/conf/gulp.conf.js
new file mode 100755
index 0000000..f8b97d0
--- /dev/null
+++ b/xos/gui/conf/gulp.conf.js
@@ -0,0 +1,48 @@
+'use strict';
+
+/**
+ *  This file contains the variables used in other gulp files
+ *  which defines tasks
+ *  By design, we only put there very generic config values
+ *  which are used in several places to keep good readability
+ *  of the tasks
+ */
+
+const path = require('path');
+const gutil = require('gulp-util');
+
+exports.ngModule = 'app';
+
+/**
+ *  The main paths of your project handle these with care
+ */
+exports.paths = {
+  src: 'src',
+  dist: 'dist/extensions/vtr', // NOTE that 'sample' have to match the extension name provided in platform install
+  appConfig: 'conf/app',
+  tmp: '.tmp',
+  e2e: 'e2e',
+  tasks: 'gulp_tasks'
+};
+
+exports.path = {};
+for (const pathName in exports.paths) {
+  if (exports.paths.hasOwnProperty(pathName)) {
+    exports.path[pathName] = function pathJoin() {
+      const pathValue = exports.paths[pathName];
+      const funcArgs = Array.prototype.slice.call(arguments);
+      const joinArgs = [pathValue].concat(funcArgs);
+      return path.join.apply(this, joinArgs);
+    };
+  }
+}
+
+/**
+ *  Common implementation for an error handler of a Gulp plugin
+ */
+exports.errorHandler = function (title) {
+  return function (err) {
+    gutil.log(gutil.colors.red(`[${title}]`), err.toString());
+    this.emit('end');
+  };
+};
diff --git a/xos/gui/conf/karma-auto.conf.js b/xos/gui/conf/karma-auto.conf.js
new file mode 100755
index 0000000..a5ea084
--- /dev/null
+++ b/xos/gui/conf/karma-auto.conf.js
@@ -0,0 +1,61 @@
+const conf = require('./gulp.conf');
+const pkg = require('../package.json');
+
+module.exports = function (config) {
+  const configuration = {
+    basePath: '../',
+    singleRun: false,
+    autoWatch: true,
+    logLevel: 'INFO',
+    junitReporter: {
+      outputDir: 'test-reports'
+    },
+    browsers: [
+      'PhantomJS',
+      // 'Chrome'
+    ],
+    frameworks: [
+      'jasmine',
+      'es6-shim'
+    ],
+    files: [
+      'node_modules/es6-shim/es6-shim.js',
+      conf.path.src('index.spec.js'),
+      conf.path.src('**/*.html')
+    ],
+    preprocessors: {
+      [conf.path.src('index.spec.js')]: [
+        'webpack'
+      ],
+      [conf.path.src('**/*.html')]: [
+        'ng-html2js'
+      ]
+    },
+    ngHtml2JsPreprocessor: {
+      stripPrefix: `${conf.paths.src}/`
+    },
+    reporters: ['mocha', 'coverage'],
+    coverageReporter: {
+      type: 'html',
+      dir: 'coverage/'
+    },
+    webpack: require('./webpack-test.conf'),
+    webpackMiddleware: {
+      noInfo: true
+    },
+    plugins: [
+      require('karma-jasmine'),
+      require('karma-junit-reporter'),
+      require('karma-coverage'),
+      require('karma-phantomjs-launcher'),
+      require('karma-chrome-launcher'),
+      require('karma-phantomjs-shim'),
+      require('karma-ng-html2js-preprocessor'),
+      require('karma-webpack'),
+      require('karma-es6-shim'),
+      require('karma-mocha-reporter')
+    ]
+  };
+
+  config.set(configuration);
+};
diff --git a/xos/gui/conf/karma.conf.js b/xos/gui/conf/karma.conf.js
new file mode 100755
index 0000000..2db6132
--- /dev/null
+++ b/xos/gui/conf/karma.conf.js
@@ -0,0 +1,57 @@
+const conf = require('./gulp.conf');
+
+module.exports = function (config) {
+  const configuration = {
+    basePath: '../',
+    singleRun: true,
+    autoWatch: false,
+    logLevel: 'INFO',
+    junitReporter: {
+      outputDir: 'test-reports'
+    },
+    browsers: [
+      'PhantomJS'
+    ],
+    frameworks: [
+      'jasmine',
+      'es6-shim'
+    ],
+    files: [
+      'node_modules/es6-shim/es6-shim.js',
+      conf.path.src('index.spec.js'),
+      conf.path.src('**/*.html')
+    ],
+    preprocessors: {
+      [conf.path.src('index.spec.js')]: [
+        'webpack'
+      ],
+      [conf.path.src('**/*.html')]: [
+        'ng-html2js'
+      ]
+    },
+    ngHtml2JsPreprocessor: {
+      stripPrefix: `${conf.paths.src}/`
+    },
+    reporters: ['progress', 'coverage'],
+    coverageReporter: {
+      type: 'html',
+      dir: 'coverage/'
+    },
+    webpack: require('./webpack-test.conf'),
+    webpackMiddleware: {
+      noInfo: true
+    },
+    plugins: [
+      require('karma-jasmine'),
+      require('karma-junit-reporter'),
+      require('karma-coverage'),
+      require('karma-phantomjs-launcher'),
+      require('karma-phantomjs-shim'),
+      require('karma-ng-html2js-preprocessor'),
+      require('karma-webpack'),
+      require('karma-es6-shim')
+    ]
+  };
+
+  config.set(configuration);
+};
diff --git a/xos/gui/conf/proxy.js b/xos/gui/conf/proxy.js
new file mode 100644
index 0000000..c410f04
--- /dev/null
+++ b/xos/gui/conf/proxy.js
@@ -0,0 +1,28 @@
+const httpProxy = require('http-proxy');
+
+const apiProxy = httpProxy.createProxyServer({
+  target: 'http://192.168.46.100:9101'
+});
+
+const staticFilesProxy = httpProxy.createProxyServer({
+  target: 'http://192.168.46.100/spa'
+});
+
+apiProxy.on('error', (error, req, res) => {
+  res.writeHead(500, {
+    'Content-Type': 'text/plain'
+  });
+  console.error('[Proxy]', error);
+});
+
+staticFilesProxy.on('error', (error, req, res) => {
+  res.writeHead(500, {
+    'Content-Type': 'text/plain'
+  });
+  console.error('[Proxy]', error);
+});
+
+module.exports = {
+  api: apiProxy,
+  static: staticFilesProxy
+};
diff --git a/xos/gui/conf/webpack-dist.conf.js b/xos/gui/conf/webpack-dist.conf.js
new file mode 100755
index 0000000..91dd0a7
--- /dev/null
+++ b/xos/gui/conf/webpack-dist.conf.js
@@ -0,0 +1,103 @@
+const webpack = require('webpack');
+const conf = require('./gulp.conf');
+const path = require('path');
+
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
+const pkg = require('../package.json');
+const autoprefixer = require('autoprefixer');
+const BaseHrefWebpackPlugin = require('base-href-webpack-plugin').BaseHrefWebpackPlugin;
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+const env = process.env.NODE_ENV || 'production';
+const brand = process.env.BRAND || 'cord';
+
+module.exports = {
+  module: {
+    loaders: [
+      {
+        test: /.json$/,
+        loaders: [
+          'json'
+        ]
+      },
+      {
+        test: /\.(css|scss)$/,
+        loaders: ExtractTextPlugin.extract({
+          fallbackLoader: 'style',
+          loader: 'css?minimize!sass!postcss'
+        })
+      },
+      {
+        test: /\.ts$/,
+        exclude: /node_modules/,
+        loaders: [
+          'ng-annotate',
+          'ts'
+        ]
+      },
+      {
+        test: /.html$/,
+        loaders: [
+          'html?' + JSON.stringify({
+            attrs: ["img:src", "img:ng-src"]
+          })
+        ]
+      },
+      {
+        test: /\.(png|woff|woff2|eot|ttf|svg|jpg|gif|jpeg)$/,
+        loader: 'url-loader?limit=100000'
+      }
+    ]
+  },
+  plugins: [
+    new CopyWebpackPlugin([
+      { from: `./conf/app/app.config.${env}.js`, to: `app.config.js` },
+      { from: `./conf/app/style.config.${brand}.js`, to: `style.config.js` },
+    ]),
+    new webpack.optimize.OccurrenceOrderPlugin(),
+    new webpack.NoErrorsPlugin(),
+    new HtmlWebpackPlugin({
+      inject: true,
+      template: conf.path.src('index.html')
+    }),
+    new webpack.optimize.UglifyJsPlugin({
+      compress: {unused: true, dead_code: true, warnings: false}, // eslint-disable-line camelcase
+      mangle: false // NOTE mangling was breaking the build
+    }),
+    new ExtractTextPlugin('index-[contenthash].css'),
+    new webpack.optimize.CommonsChunkPlugin({name: 'vendor'}),
+    new webpack.ProvidePlugin({
+      $: "jquery",
+      jQuery: "jquery"
+    }),
+    new BaseHrefWebpackPlugin({
+      baseHref: '/spa/'
+    }),
+  ],
+  postcss: () => [autoprefixer],
+  output: {
+    path: path.join(process.cwd(), conf.paths.dist),
+    publicPath: "/spa/", // enable apache proxying on the head node
+    filename: '[name].js'
+  },
+  resolve: {
+    extensions: [
+      '',
+      '.webpack.js',
+      '.web.js',
+      '.js',
+      '.ts'
+    ]
+  },
+  entry: {
+    app: `./${conf.path.src('index')}`,
+    vendor: Object.keys(pkg.dependencies)
+  },
+  ts: {
+    configFileName: 'tsconfig.json'
+  },
+  tslint: {
+    configuration: require('../tslint.json')
+  }
+};
+
diff --git a/xos/gui/conf/webpack-test.conf.js b/xos/gui/conf/webpack-test.conf.js
new file mode 100755
index 0000000..a87e383
--- /dev/null
+++ b/xos/gui/conf/webpack-test.conf.js
@@ -0,0 +1,66 @@
+module.exports = {
+  module: {
+    preLoaders: [
+      {
+        test: /\.ts$/,
+        exclude: /node_modules/,
+        loader: 'tslint'
+      }
+    ],
+    loaders: [
+      {
+        test: /.json$/,
+        loaders: [
+          'json'
+        ]
+      },
+      {
+        test: /\.ts$/,
+        exclude: /node_modules/,
+        loaders: [
+          'ng-annotate',
+          'ts'
+        ]
+      },
+      {
+        test: /.html$/,
+        loaders: [
+          'html?' + JSON.stringify({
+            attrs: ["img:src", "img:ng-src"]
+          })
+        ]
+      },
+      {
+        test: /\.(css|scss)$/,
+        loaders: [
+          'style',
+          'css',
+          'sass',
+          'postcss'
+        ]
+      },
+      {
+        test: /\.(png|woff|woff2|eot|ttf|svg|jpg|gif|jpeg)$/,
+        loader: 'url-loader?limit=100000'
+      }
+    ]
+  },
+  plugins: [],
+  debug: true,
+  devtool: 'source-map',
+  resolve: {
+    extensions: [
+      '',
+      '.webpack.js',
+      '.web.js',
+      '.js',
+      '.ts'
+    ]
+  },
+  ts: {
+    configFileName: 'tsconfig.json'
+  },
+  tslint: {
+    configuration: require('../tslint.json')
+  }
+};
diff --git a/xos/gui/conf/webpack.conf.js b/xos/gui/conf/webpack.conf.js
new file mode 100755
index 0000000..544b2c5
--- /dev/null
+++ b/xos/gui/conf/webpack.conf.js
@@ -0,0 +1,99 @@
+const webpack = require('webpack');
+const conf = require('./gulp.conf');
+const path = require('path');
+
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const autoprefixer = require('autoprefixer');
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+const env = process.env.NODE_ENV || 'production';
+const brand = process.env.BRAND || 'cord';
+
+module.exports = {
+  module: {
+    preLoaders: [
+      {
+        test: /\.ts$/,
+        exclude: /node_modules/,
+        loader: 'tslint'
+      }
+    ],
+
+    loaders: [
+      {
+        test: /.json$/,
+        loaders: [
+          'json'
+        ]
+      },
+      {
+        test: /\.(css|scss)$/,
+        loaders: [
+          'style',
+          'css',
+          'sass',
+          'postcss'
+        ]
+      },
+      {
+        test: /\.ts$/,
+        exclude: /node_modules/,
+        loaders: [
+          'ng-annotate',
+          'ts'
+        ]
+      },
+      {
+        test: /.html$/,
+        loaders: [
+          'html?' + JSON.stringify({
+            attrs: ["img:src", "img:ng-src"]
+          })
+        ]
+      },
+      {
+        test: /\.(png|woff|woff2|eot|ttf|svg|jpg|gif|jpeg)$/,
+        loader: 'url-loader?limit=100000'
+      }
+    ]
+  },
+  plugins: [
+    new CopyWebpackPlugin([
+      { from: `./conf/app/app.config.${env}.js`, to: `app.config.js` },
+      { from: `./conf/app/style.config.${brand}.js`, to: `style.config.js` },
+    ]),
+    new webpack.optimize.OccurrenceOrderPlugin(),
+    new webpack.NoErrorsPlugin(),
+    new HtmlWebpackPlugin({
+      template: conf.path.src('index.html')
+    })
+  ],
+  postcss: () => [autoprefixer],
+  debug: true,
+  devtool: 'source-map',
+  output: {
+    path: path.join(process.cwd(), conf.paths.tmp),
+    filename: 'index.js'
+  },
+  resolve: {
+    extensions: [
+      '',
+      '.webpack.js',
+      '.web.js',
+      '.js',
+      '.ts'
+    ]
+  },
+  entry: `./${conf.path.src('index')}`,
+  ts: {
+    configFileName: 'tsconfig.json'
+  },
+  tslint: {
+    configuration: require('../tslint.json')
+  },
+  stats: {
+    colors: true,
+    modules: true,
+    reasons: true,
+    errorDetails: true
+  }
+};
diff --git a/xos/gui/gulp_tasks/browsersync.js b/xos/gui/gulp_tasks/browsersync.js
new file mode 100755
index 0000000..945a88d
--- /dev/null
+++ b/xos/gui/gulp_tasks/browsersync.js
@@ -0,0 +1,21 @@
+const gulp = require('gulp');
+const browserSync = require('browser-sync');
+const spa = require('browser-sync-spa');
+
+const browserSyncConf = require('../conf/browsersync.conf');
+const browserSyncDistConf = require('../conf/browsersync-dist.conf');
+
+browserSync.use(spa());
+
+gulp.task('browsersync', browserSyncServe);
+gulp.task('browsersync:dist', browserSyncDist);
+
+function browserSyncServe(done) {
+  browserSync.init(browserSyncConf());
+  done();
+}
+
+function browserSyncDist(done) {
+  browserSync.init(browserSyncDistConf());
+  done();
+}
diff --git a/xos/gui/gulp_tasks/karma.js b/xos/gui/gulp_tasks/karma.js
new file mode 100755
index 0000000..5b90572
--- /dev/null
+++ b/xos/gui/gulp_tasks/karma.js
@@ -0,0 +1,27 @@
+const path = require('path');
+
+const gulp = require('gulp');
+const karma = require('karma');
+
+gulp.task('karma:single-run', karmaSingleRun);
+gulp.task('karma:auto-run', karmaAutoRun);
+
+function karmaFinishHandler(done) {
+  return failCount => {
+    done(failCount ? new Error(`Failed ${failCount} tests.`) : null);
+  };
+}
+
+function karmaSingleRun(done) {
+  process.env.NODE_ENV = 'test';
+  const configFile = path.join(process.cwd(), 'conf', 'karma.conf.js');
+  const karmaServer = new karma.Server({configFile}, karmaFinishHandler(done));
+  karmaServer.start();
+}
+
+function karmaAutoRun(done) {
+  process.env.NODE_ENV = 'test';
+  const configFile = path.join(process.cwd(), 'conf', 'karma-auto.conf.js');
+  const karmaServer = new karma.Server({configFile}, karmaFinishHandler(done));
+  karmaServer.start();
+}
diff --git a/xos/gui/gulp_tasks/misc.js b/xos/gui/gulp_tasks/misc.js
new file mode 100755
index 0000000..d1e70ec
--- /dev/null
+++ b/xos/gui/gulp_tasks/misc.js
@@ -0,0 +1,38 @@
+const path = require('path');
+
+const gulp = require('gulp');
+const del = require('del');
+const filter = require('gulp-filter');
+const rename = require('gulp-rename');
+const replace = require('gulp-replace');
+
+const conf = require('../conf/gulp.conf');
+
+gulp.task('clean', clean);
+gulp.task('other', other);
+
+function clean() {
+  return del([`${conf.paths.dist}/*`, conf.paths.tmp]);
+}
+
+function other() {
+  const fileFilter = filter(file => file.stat.isFile());
+
+  return gulp.src([
+    path.join(conf.paths.src, '/**/*'),
+    path.join(`!${conf.paths.src}`, '/**/*.{scss,ts,html}')
+  ])
+    .pipe(fileFilter)
+    .pipe(gulp.dest(conf.paths.dist));
+}
+
+function other() {
+  const fileFilter = filter(file => file.stat.isFile());
+
+  return gulp.src([
+    path.join(conf.paths.src, '/**/*'),
+    path.join(`!${conf.paths.src}`, '/**/*.{scss,ts,html}')
+  ])
+    .pipe(fileFilter)
+    .pipe(gulp.dest(conf.paths.dist));
+}
diff --git a/xos/gui/gulp_tasks/webpack.js b/xos/gui/gulp_tasks/webpack.js
new file mode 100755
index 0000000..ec8e8b1
--- /dev/null
+++ b/xos/gui/gulp_tasks/webpack.js
@@ -0,0 +1,49 @@
+const gulp = require('gulp');
+const gutil = require('gulp-util');
+
+const webpack = require('webpack');
+const webpackConf = require('../conf/webpack.conf');
+const webpackDistConf = require('../conf/webpack-dist.conf');
+const gulpConf = require('../conf/gulp.conf');
+const browsersync = require('browser-sync');
+
+gulp.task('webpack:dev', done => {
+  webpackWrapper(false, webpackConf, done);
+});
+
+gulp.task('webpack:watch', done => {
+  webpackWrapper(true, webpackConf, done);
+});
+
+gulp.task('webpack:dist', done => {
+  process.env.NODE_ENV = 'production';
+  webpackWrapper(false, webpackDistConf, done);
+});
+
+function webpackWrapper(watch, conf, done) {
+  const webpackBundler = webpack(conf);
+
+  const webpackChangeHandler = (err, stats) => {
+    if (err) {
+      gulpConf.errorHandler('Webpack')(err);
+    }
+    gutil.log(stats.toString({
+      colors: true,
+      chunks: false,
+      hash: false,
+      version: false
+    }));
+    if (done) {
+      done();
+      done = null;
+    } else {
+      browsersync.reload();
+    }
+  };
+
+  if (watch) {
+    webpackBundler.watch(200, webpackChangeHandler);
+  } else {
+    webpackBundler.run(webpackChangeHandler);
+  }
+}
diff --git a/xos/gui/gulpfile.js b/xos/gui/gulpfile.js
new file mode 100755
index 0000000..541d6e4
--- /dev/null
+++ b/xos/gui/gulpfile.js
@@ -0,0 +1,29 @@
+const gulp = require('gulp');
+const HubRegistry = require('gulp-hub');
+const browserSync = require('browser-sync');
+
+const conf = require('./conf/gulp.conf');
+
+// Load some files into the registry
+const hub = new HubRegistry([conf.path.tasks('*.js')]);
+
+// Tell gulp to use the tasks just loaded
+gulp.registry(hub);
+
+gulp.task('build', gulp.series(gulp.parallel('other', 'webpack:dist')));
+gulp.task('test', gulp.series('karma:single-run'));
+gulp.task('test:auto', gulp.series('karma:auto-run'));
+gulp.task('serve', gulp.series('webpack:watch', 'watch', 'browsersync'));
+gulp.task('serve:dist', gulp.series('default', 'browsersync:dist'));
+gulp.task('default', gulp.series('clean', 'build'));
+gulp.task('watch', watch);
+
+function reloadBrowserSync(cb) {
+  browserSync.reload();
+  cb();
+}
+
+function watch(done) {
+  gulp.watch(conf.path.tmp('index.html'), reloadBrowserSync);
+  done();
+}
diff --git a/xos/gui/nginx.conf b/xos/gui/nginx.conf
new file mode 100755
index 0000000..5e7bc60
--- /dev/null
+++ b/xos/gui/nginx.conf
@@ -0,0 +1,19 @@
+server {
+    listen       4000;
+    server_name  localhost;
+
+    #charset koi8-r;
+    access_log  /var/log/nginx/log/xos-spa-gui.access.log  main;
+    error_log  /var/log/nginx/log/xos-spa-gui.error.log  debug;
+
+    location / {
+       root   /var/www/dist;
+       index  index.html index.htm;
+    }
+
+    # Redirect for FE config
+
+    location /spa/ {
+        rewrite ^/spa/(.*)$ /$1;
+    }
+}
\ No newline at end of file
diff --git a/xos/gui/package.json b/xos/gui/package.json
new file mode 100644
index 0000000..f9d893b
--- /dev/null
+++ b/xos/gui/package.json
@@ -0,0 +1,108 @@
+{
+  "version": "2.0.0",
+  "dependencies": {
+    "angular": "^1.5.0",
+    "angular-animate": "^1.6.0",
+    "angular-cookies": "^1.6.0",
+    "angular-resource": "^1.6.0",
+    "angular-toastr": "^2.1.1",
+    "angular-ui-bootstrap": "^2.3.1",
+    "angular-ui-router": "1.0.0-beta.1",
+    "bootstrap": "^3.3.7",
+    "http-proxy": "^1.16.2",
+    "jquery": "^3.1.1",
+    "lodash": "^4.17.2",
+    "pluralize": "^3.1.0",
+    "rxjs": "^5.0.1",
+    "socket.io-client": "^1.7.2"
+  },
+  "devDependencies": {
+    "angular-mocks": "^1.5.0-beta.2",
+    "autoprefixer": "^6.2.2",
+    "babel-eslint": "^6.0.2",
+    "babel-loader": "^6.2.0",
+    "babel-plugin-istanbul": "^2.0.1",
+    "base-href-webpack-plugin": "1.0.0",
+    "browser-sync": "^2.9.11",
+    "browser-sync-spa": "^1.0.3",
+    "copy-webpack-plugin": "^4.0.1",
+    "css-loader": "^0.23.1",
+    "del": "^2.0.2",
+    "es6-shim": "^0.35.0",
+    "eslint": "^3.2.2",
+    "eslint-config-angular": "^0.5.0",
+    "eslint-config-xo-space": "^0.12.0",
+    "eslint-loader": "^1.3.0",
+    "eslint-plugin-angular": "^1.3.0",
+    "eslint-plugin-babel": "^3.1.0",
+    "extract-text-webpack-plugin": "2.0.0-beta.3",
+    "file-loader": "^0.9.0",
+    "gulp": "gulpjs/gulp#4ed9a4a3275559c73a396eff7e1fde3824951ebb",
+    "gulp-angular-filesort": "^1.1.1",
+    "gulp-angular-templatecache": "^1.8.0",
+    "gulp-filter": "^4.0.0",
+    "gulp-htmlmin": "^1.3.0",
+    "gulp-hub": "frankwallis/gulp-hub#d461b9c700df9010d0a8694e4af1fb96d9f38bf4",
+    "gulp-insert": "^0.5.0",
+    "gulp-ng-annotate": "^1.1.0",
+    "gulp-rename": "^1.2.2",
+    "gulp-replace": "^0.5.4",
+    "gulp-sass": "^2.1.1",
+    "gulp-util": "^3.0.7",
+    "html-loader": "^0.4.3",
+    "html-webpack-plugin": "^2.9.0",
+    "jasmine": "^2.4.1",
+    "jasmine-jquery": "^2.1.1",
+    "json-loader": "^0.5.4",
+    "karma": "^1.3.0",
+    "karma-angular-filesort": "^1.0.0",
+    "karma-chrome-launcher": "^2.0.0",
+    "karma-coverage": "^1.1.1",
+    "karma-es6-shim": "^1.0.0",
+    "karma-jasmine": "^1.0.2",
+    "karma-junit-reporter": "^1.1.0",
+    "karma-mocha-reporter": "^2.2.1",
+    "karma-ng-html2js-preprocessor": "^0.2.0",
+    "karma-phantomjs-launcher": "^1.0.0",
+    "karma-phantomjs-shim": "^1.1.2",
+    "karma-webpack": "^1.7.0",
+    "ng-annotate-loader": "^0.0.10",
+    "node-sass": "^3.4.2",
+    "phantomjs-prebuilt": "^2.1.6",
+    "postcss-loader": "^0.8.0",
+    "sass-loader": "^3.1.2",
+    "style-loader": "^0.13.0",
+    "ts-loader": "^0.8.2",
+    "tslint": "^3.15.1",
+    "tslint-loader": "^2.1.0",
+    "typescript": "^2.0.2",
+    "typings": "^1.0.4",
+    "url-loader": "^0.5.7",
+    "webpack": "2.1.0-beta.20"
+  },
+  "scripts": {
+    "postinstall": "npm run typings",
+    "build": "gulp",
+    "start": "gulp serve",
+    "typings": "typings install",
+    "serve:dist": "gulp serve:dist",
+    "pretest": "npm run lint",
+    "test": "gulp test",
+    "test:auto": "gulp test:auto",
+    "config": "gulp config",
+    "lint": "tslint -c ./tslint.json 'src/**/*.ts'"
+  },
+  "eslintConfig": {
+    "globals": {
+      "expect": true
+    },
+    "root": true,
+    "env": {
+      "browser": true,
+      "jasmine": true
+    },
+    "extends": [
+      "xo-space/esnext"
+    ]
+  }
+}
diff --git a/xos/gui/src/app/components/vtr/vtr-dashboard.html b/xos/gui/src/app/components/vtr/vtr-dashboard.html
new file mode 100644
index 0000000..f089f4e
--- /dev/null
+++ b/xos/gui/src/app/components/vtr/vtr-dashboard.html
@@ -0,0 +1,109 @@
+<pre>{{vm.subscribers | json}}</pre>
+<div class="row">
+  <div class="col-xs-12">
+    <h1>vTR Dashboard</h1>
+    <p>Use this page to run test against your subscriber</p>
+  </div>
+</div>
+<form ng-submit="vm.runTest()">
+  <div class="row">
+    <div class="col-xs-12">
+      <label>Target:</label>
+    </div>
+    <div class="col-xs-12">
+      <select class="form-control" ng-model="vm.truckroll.target_id" ng-options="s.id as s.name for s in vm.subscribers"></select>
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-xs-12">
+      <label>Scope:</label>
+    </div>
+    <div class="col-xs-6">
+      <a 
+      ng-click="vm.truckroll.scope = 'container'"
+      ng-class="{'btn-default': vm.truckroll.scope !== 'container', 'btn-primary': vm.truckroll.scope === 'container'}"
+      class="btn btn-block"
+      >Container</a>
+    </div>
+    <div class="col-xs-6">
+      <a 
+      ng-click="vm.truckroll.scope = 'vm'"
+      ng-class="{'btn-default': vm.truckroll.scope !== 'vm', 'btn-primary': vm.truckroll.scope === 'vm'}"
+      class="btn btn-block"
+      >VM</a>
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-xs-12">
+      <label>Test:</label>
+    </div>
+    <div class="col-xs-4">
+      <a 
+      ng-click="vm.truckroll.test = 'ping'"
+      ng-class="{'btn-default': vm.truckroll.test !== 'ping', 'btn-primary': vm.truckroll.test === 'ping'}"
+      class="btn btn-block">Ping</a>
+    </div>
+    <div class="col-xs-4">
+      <a 
+      ng-click="vm.truckroll.test = 'traceroute'"
+      ng-class="{'btn-default': vm.truckroll.test !== 'traceroute', 'btn-primary': vm.truckroll.test === 'traceroute'}"
+      class="btn btn-block">Traceroute</a>
+    </div>
+    <div class="col-xs-4">
+      <a 
+      ng-click="vm.truckroll.test = 'tcpdump'"
+      ng-class="{'btn-default': vm.truckroll.test !== 'tcpdump', 'btn-primary': vm.truckroll.test === 'tcpdump'}"
+      class="btn btn-block">Tcp Dump</a>
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-xs-12">
+      <label>Argument:</label>
+    </div>
+    <div class="col-xs-12">
+      <input type="text" class="form-control" ng-model="vm.truckroll.argument" required />
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-xs-12" ng-show="!vm.loader">
+      <button class="btn btn-success btn-block">Run test</button>
+    </div>
+  </div>
+</form>
+<div class="row">
+  <div class="col-xs-12 animate-vertical" ng-show="vm.loader">
+    <div class="loader"></div>
+  </div>
+</div>
+<div class="row" ng-hide="!vm.truckroll.result_code">
+  <div class="col-xs-12">
+    <label>Result Code</label>
+  </div>
+  <div class="col-xs-12">
+    <pre>{{vm.truckroll.result_code}}</pre>
+  </div>
+</div>
+<div class="row" ng-hide="!vm.truckroll.result">
+  <div class="col-xs-12">
+    <label>Result:</label>
+  </div>
+  <div class="col-xs-12">
+    <pre>{{vm.truckroll.result}}</pre>
+  </div>
+</div>
+<div class="row" ng-hide="!vm.truckroll.backend_status">
+  <div class="col-xs-12">
+    <label>Backend Status</label>
+  </div>
+  <div class="col-xs-12">
+    <pre>{{vm.truckroll.backend_status}}</pre>
+  </div>
+</div>
+
+<div class="row" ng-show="vm.error">
+  <div class="col-xs-12">
+    <div class="alert alert-danger">
+      {{vm.error}}
+    </div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/xos/gui/src/app/components/vtr/vtr-dashboard.scss b/xos/gui/src/app/components/vtr/vtr-dashboard.scss
new file mode 100644
index 0000000..2ead2bf
--- /dev/null
+++ b/xos/gui/src/app/components/vtr/vtr-dashboard.scss
@@ -0,0 +1,5 @@
+xos-vtr-dashboard-component {
+  .row + .row {
+    margin-top: 20px;
+  }
+}
\ No newline at end of file
diff --git a/xos/gui/src/app/components/vtr/vtr-dashboard.ts b/xos/gui/src/app/components/vtr/vtr-dashboard.ts
new file mode 100644
index 0000000..3db80d1
--- /dev/null
+++ b/xos/gui/src/app/components/vtr/vtr-dashboard.ts
@@ -0,0 +1,97 @@
+import './vtr-dashboard.scss';
+import * as _ from 'lodash';
+import {subscribeOn} from 'rxjs/operator/subscribeOn';
+
+class VtrDashboardComponent {
+  static $inject = [
+    '$timeout',
+    'XosModelStore',
+    'XosVtrTruckroll'
+   ];
+
+  public subscribers = [];
+  public truckroll: any;
+  public loader: boolean;
+  public error: string;
+  private Truckroll;
+
+  constructor(
+    private $timeout: ng.ITimeoutService,
+    private XosModelStore: any,
+    private XosVtrTruckroll: any
+  ) {
+
+    this.Truckroll = this.XosVtrTruckroll.getResource();
+
+    // load subscribers
+    this.XosModelStore.query('Subscribers')
+      .subscribe(
+        res => {
+          this.subscribers = res;
+        }
+      );
+  }
+
+  public runTest() {
+
+    // clean previous tests
+    delete this.truckroll.id;
+    delete this.truckroll.result;
+    delete this.truckroll.is_synced;
+    delete this.truckroll.result_code;
+    delete this.truckroll.backend_status;
+    delete this.error;
+
+    this.truckroll.target_type_id = this.getSubscriberContentTypeId(this.truckroll.target_id);
+
+    const test = new this.Truckroll(this.truckroll);
+    this.loader = true;
+    test.$save()
+    .then((res) => {
+      this.waitForTest(res.id);
+    });
+  };
+
+  private getSubscriberContentTypeId(subscriberId: number) {
+    return _.find(this.subscribers, {id: subscriberId}).self_content_type_id;
+  }
+
+  private waitForTest(id: number) {
+
+        this.Truckroll.get({id: id}).$promise
+        .then((testResult, status) => {
+          // this is becasue error returning a string in an array
+          if (testResult[0] && testResult[0].length === 1) {
+            this.loader = false;
+            this.error = 'An error occurred, please try again later';
+            return;
+          }
+
+          // if error
+          // or
+          // if is synced
+          if (
+              testResult.backend_status.indexOf('2') >= 0 ||
+              (testResult.result_code && testResult.result_code.indexOf('2') >= 0) ||
+              testResult.is_synced
+            ) {
+            this.truckroll = angular.copy(testResult);
+            this.loader = false;
+            this.Truckroll.delete({id: id});
+          }
+          // else keep polling
+          else {
+            this.$timeout(() => {
+              this.waitForTest(id);
+            }, 2000);
+          }
+        });
+      };
+
+}
+
+export const xosVtrDashboardComponent: angular.IComponentOptions = {
+  template: require('./vtr-dashboard.html'),
+  controllerAs: 'vm',
+  controller: VtrDashboardComponent
+};
diff --git a/xos/gui/src/app/services/truckroll.resource.ts b/xos/gui/src/app/services/truckroll.resource.ts
new file mode 100644
index 0000000..1791021
--- /dev/null
+++ b/xos/gui/src/app/services/truckroll.resource.ts
@@ -0,0 +1,20 @@
+export class XosVtrTruckroll {
+
+  static $inject = [
+    '$resource',
+    'AppConfig'
+  ];
+
+  constructor(
+    private $resource: ng.resource.IResourceService,
+    private AppConfig: any
+  ) {
+
+  }
+
+  public getResource() {
+    return this.$resource(`${this.AppConfig.apiEndpoint}/vtr/vtrtenants/:id/`, { id: '@id' }, {
+      update: { method: 'PUT' }
+    });
+  }
+}
diff --git a/xos/gui/src/index.html b/xos/gui/src/index.html
new file mode 100644
index 0000000..33dcbd3
--- /dev/null
+++ b/xos/gui/src/index.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en" ng-app="xos-vtr-gui-extension">
+<head>
+  <meta charset="UTF-8">
+  <title>Document</title>
+  <link href="http://192.168.46.100/spa/loader.css" rel="stylesheet">
+  <link href="http://192.168.46.100/spa/app.css" rel="stylesheet">
+</head>
+<body>
+  <div ui-view></div>
+  <script src="http://192.168.46.100/spa/vendor.js"></script>
+  <script src="http://192.168.46.100/spa/app.js"></script>
+  <script src="http://192.168.46.100/spa/loader.js"></script>
+  <script src="http://192.168.46.100/spa/app.config.js"></script>
+  <script src="http://192.168.46.100/spa/style.config.js"></script>
+</body>
+</html>
diff --git a/xos/gui/src/index.ts b/xos/gui/src/index.ts
new file mode 100644
index 0000000..03d248c
--- /dev/null
+++ b/xos/gui/src/index.ts
@@ -0,0 +1,31 @@
+/// <reference path="../typings/index.d.ts" />
+import * as angular from 'angular';
+
+import 'angular-ui-router';
+import 'angular-resource';
+import 'angular-cookies';
+import routesConfig from './routes';
+import {xosVtrDashboardComponent} from './app/components/vtr/vtr-dashboard';
+import {XosVtrTruckroll} from './app/services/truckroll.resource';
+
+angular.module('xos-vtr-gui-extension', [
+    'ui.router',
+    'app'
+  ])
+  .config(routesConfig)
+  .service('XosVtrTruckroll', XosVtrTruckroll)
+  .component('xosVtrDashboardComponent', xosVtrDashboardComponent)
+  .run(function($log: ng.ILogService, XosNavigationService: any) {
+    $log.info('[xos-vtr-gui-extension] App is running');
+
+    XosNavigationService.add({
+      label: 'vTR',
+      state: 'xos.vtr',
+    });
+
+    XosNavigationService.add({
+      label: 'Dashboard',
+      state: 'xos.vtr.dashboard',
+      parent: 'xos.vtr'
+    });
+  });
diff --git a/xos/gui/src/routes.ts b/xos/gui/src/routes.ts
new file mode 100644
index 0000000..84abd47
--- /dev/null
+++ b/xos/gui/src/routes.ts
@@ -0,0 +1,16 @@
+export default routesConfig;
+
+function routesConfig($stateProvider: angular.ui.IStateProvider, $locationProvider: angular.ILocationProvider) {
+  $locationProvider.html5Mode(false).hashPrefix('');
+
+  $stateProvider
+  .state('xos.vtr', {
+      abstract: true,
+      template: '<div ui-view></div>'
+    })
+    .state('xos.vtr.dashboard', {
+      url: 'vtr/dashboard',
+      parent: 'xos.vtr',
+      component: 'xosVtrDashboardComponent'
+    });
+}
diff --git a/xos/gui/tsconfig.json b/xos/gui/tsconfig.json
new file mode 100644
index 0000000..c516fb7
--- /dev/null
+++ b/xos/gui/tsconfig.json
@@ -0,0 +1,19 @@
+{
+  "compilerOptions": {
+    "sourceMap": true,
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "removeComments": false,
+    "noImplicitAny": false
+  },
+  "compileOnSave": false,
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.tsx"
+  ],
+  "exclude": [
+    "typings/**",
+    "conf/app/**",
+    "node_modules"
+  ]
+}
diff --git a/xos/gui/tslint.json b/xos/gui/tslint.json
new file mode 100644
index 0000000..04d33e1
--- /dev/null
+++ b/xos/gui/tslint.json
@@ -0,0 +1,84 @@
+{
+  "rules": {
+    "ban": [true,
+      ["_", "extend"],
+      ["_", "isNull"],
+      ["_", "isDefined"]
+    ],
+    "class-name": true,
+    "comment-format": [true,
+      "check-space"
+    ],
+    "curly": true,
+    "eofline": true,
+    "forin": true,
+    "indent": [true, "spaces"],
+    "interface-name": true,
+    "jsdoc-format": true,
+    "label-position": true,
+    "label-undefined": true,
+    "max-line-length": [false, 140],
+    "member-ordering": [true,
+      "public-before-private",
+      "static-before-instance",
+      "variables-before-functions"
+    ],
+    "no-arg": true,
+    "no-bitwise": true,
+    "no-console": [true,
+      "debug",
+      "info",
+      "time",
+      "timeEnd",
+      "trace",
+      "log",
+      "error"
+    ],
+    "no-construct": true,
+    "no-constructor-vars": false,
+    "no-debugger": true,
+    "no-duplicate-key": true,
+    "no-duplicate-variable": true,
+    "no-empty": true,
+    "no-eval": true,
+    "no-string-literal": false,
+    "no-switch-case-fall-through": true,
+    "trailing-comma": true,
+    "no-trailing-whitespace": true,
+    "no-unused-expression": true,
+    "no-unused-variable": true,
+    "no-unreachable": true,
+    "no-use-before-declare": true,
+    "no-var-requires": true,
+    "one-line": [true,
+      "check-open-brace",
+      "check-catch",
+      "check-whitespace"
+    ],
+    "quotemark": [true, "single"],
+    "radix": true,
+    "semicolon": true,
+    "triple-equals": [true, "allow-null-check"],
+    "typedef": [true,
+      "callSignature",
+      "indexSignature",
+      "parameter",
+      "propertySignature",
+      "variableDeclarator"
+    ],
+    "typedef-whitespace": [true,
+      ["callSignature", "noSpace"],
+      ["catchClause", "noSpace"],
+      ["indexSignature", "space"]
+    ],
+    "use-strict": false,
+    "variable-name": false,
+    "whitespace": [true,
+      "check-branch",
+      "check-decl",
+      "check-operator",
+      "check-separator",
+      "check-type"
+    ]
+  }
+}
diff --git a/xos/gui/typings.json b/xos/gui/typings.json
new file mode 100644
index 0000000..a830275
--- /dev/null
+++ b/xos/gui/typings.json
@@ -0,0 +1,18 @@
+{
+  "globalDependencies": {
+    "angular": "registry:dt/angular#1.5.0+20161208205636",
+    "angular-cookies": "registry:dt/angular-cookies#1.4.0+20160317120654",
+    "angular-mocks": "github:DefinitelyTyped/DefinitelyTyped/angularjs/angular-mocks.d.ts#dc9dabe74a5be62613b17a3605309783a12ff28a",
+    "angular-resource": "registry:dt/angular-resource#1.5.0+20161114123626",
+    "angular-ui-router": "registry:dt/angular-ui-router#1.1.5+20160707113237",
+    "es6-shim": "registry:dt/es6-shim#0.31.2+20160602141504",
+    "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#dc9dabe74a5be62613b17a3605309783a12ff28a",
+    "jasmine-jquery": "registry:dt/jasmine-jquery#1.5.8+20161128184045",
+    "jquery": "registry:dt/jquery#1.10.0+20161119044246",
+    "require": "registry:dt/require#2.1.20+20160316155526",
+    "socket.io-client": "registry:dt/socket.io-client#1.4.4+20160317120654"
+  },
+  "dependencies": {
+    "angular-toastr": "registry:dt/angular-toastr#1.6.0+20160708003927"
+  }
+}
diff --git a/xos/gui/xos-sample-gui-extension.yaml b/xos/gui/xos-sample-gui-extension.yaml
new file mode 100644
index 0000000..2dcb037
--- /dev/null
+++ b/xos/gui/xos-sample-gui-extension.yaml
@@ -0,0 +1,15 @@
+tosca_definitions_version: tosca_simple_yaml_1_0
+
+description: Persiste xos-sample-gui-extension
+
+imports:
+   - custom_types/xos.yaml
+
+topology_template:
+  node_templates:
+
+    # UI Extension
+    xos-sample-gui-extension:
+      type: tosca.nodes.XOSGuiExtension
+      properties:
+        files: /spa/extensions/xos-sample-gui-extension/vendor.js, /spa/extensions/xos-sample-gui-extension/app.js