[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