summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
Diffstat (limited to 'webapp')
-rw-r--r--webapp/README.md45
-rw-r--r--webapp/assets/favicon.icobin0 -> 26463 bytes
-rw-r--r--webapp/assets/images/iot-graphx.jpgbin0 -> 113746 bytes
-rw-r--r--webapp/bs-config.json9
-rw-r--r--webapp/gulp.conf.js34
-rw-r--r--webapp/gulpfile.js123
-rw-r--r--webapp/package.json62
-rw-r--r--webapp/src/app/alert/alert.component.ts30
-rw-r--r--webapp/src/app/app.component.css17
-rw-r--r--webapp/src/app/app.component.html21
-rw-r--r--webapp/src/app/app.component.ts34
-rw-r--r--webapp/src/app/app.module.ts69
-rw-r--r--webapp/src/app/app.routing.ts19
-rw-r--r--webapp/src/app/build/build.component.css10
-rw-r--r--webapp/src/app/build/build.component.html50
-rw-r--r--webapp/src/app/build/build.component.ts120
-rw-r--r--webapp/src/app/common/alert.service.ts64
-rw-r--r--webapp/src/app/common/config.service.ts276
-rw-r--r--webapp/src/app/common/syncthing.service.ts342
-rw-r--r--webapp/src/app/common/xdsserver.service.ts216
-rw-r--r--webapp/src/app/config/config.component.css26
-rw-r--r--webapp/src/app/config/config.component.html73
-rw-r--r--webapp/src/app/config/config.component.ts123
-rw-r--r--webapp/src/app/home/home.component.ts62
-rw-r--r--webapp/src/app/main.ts6
-rw-r--r--webapp/src/app/projects/projectCard.component.ts63
-rw-r--r--webapp/src/app/projects/projectsListAccordion.component.ts26
-rw-r--r--webapp/src/index.html49
-rw-r--r--webapp/src/systemjs.config.js55
-rw-r--r--webapp/tsconfig.json17
-rw-r--r--webapp/tslint.json55
-rw-r--r--webapp/tslint.prod.json56
-rw-r--r--webapp/typings.json11
33 files changed, 2163 insertions, 0 deletions
diff --git a/webapp/README.md b/webapp/README.md
new file mode 100644
index 0000000..acee846
--- /dev/null
+++ b/webapp/README.md
@@ -0,0 +1,45 @@
+XDS Dashboard
+=============
+
+This is the web application dashboard for Cross Development System.
+
+## 1. Prerequisites
+
+*nodejs* must be installed on your system and the below global node packages must be installed:
+
+> sudo npm install -g gulp-cli
+
+## 2. Installing dependencies
+
+Install dependencies by running the following command:
+
+> npm install
+
+`node_modules` and `typings` directories will be created during the install.
+
+## 3. Building the project
+
+Build the project by running the following command:
+
+> npm run clean & npm run build
+
+`dist` directory will be created during the build
+
+## 4. Starting the application
+
+Start the application by running the following command:
+
+> npm start
+
+The application will be displayed in the browser.
+
+
+## TODO
+
+- Upgrade to angular 2.4.9 or 2.4.10 AND rxjs 5.2.0
+- Complete README + package.json
+- Add prod mode and use update gulpfile tslint: "./tslint/prod.json"
+- Generate a bundle minified file, using systemjs-builder or find a better way
+ http://stackoverflow.com/questions/35280582/angular2-too-many-file-requests-on-load
+- Add SASS support
+ http://foundation.zurb.com/sites/docs/sass.html \ No newline at end of file
diff --git a/webapp/assets/favicon.ico b/webapp/assets/favicon.ico
new file mode 100644
index 0000000..6bf5138
--- /dev/null
+++ b/webapp/assets/favicon.ico
Binary files differ
diff --git a/webapp/assets/images/iot-graphx.jpg b/webapp/assets/images/iot-graphx.jpg
new file mode 100644
index 0000000..6a2c428
--- /dev/null
+++ b/webapp/assets/images/iot-graphx.jpg
Binary files differ
diff --git a/webapp/bs-config.json b/webapp/bs-config.json
new file mode 100644
index 0000000..0041c6d
--- /dev/null
+++ b/webapp/bs-config.json
@@ -0,0 +1,9 @@
+{
+ "port": 8000,
+ "files": [
+ "dist/**/*.{html,htm,css,js}"
+ ],
+ "server": {
+ "baseDir": "dist"
+ }
+} \ No newline at end of file
diff --git a/webapp/gulp.conf.js b/webapp/gulp.conf.js
new file mode 100644
index 0000000..0de52f9
--- /dev/null
+++ b/webapp/gulp.conf.js
@@ -0,0 +1,34 @@
+"use strict";
+
+module.exports = {
+ prodMode: process.env.PRODUCTION || false,
+ outDir: "dist",
+ paths: {
+ tsSources: ["src/**/*.ts"],
+ srcDir: "src",
+ assets: ["assets/**"],
+ node_modules_libs: [
+ 'core-js/client/shim.min.js',
+ 'reflect-metadata/Reflect.js',
+ 'rxjs/**/*.js',
+ 'socket.io-client/dist/socket.io*.js',
+ 'systemjs/dist/system-polyfills.js',
+ 'systemjs/dist/system.src.js',
+ 'zone.js/dist/**',
+ '@angular/**/bundles/**',
+ 'ngx-cookie/bundles/**',
+ 'ngx-bootstrap/bundles/**',
+ 'bootstrap/dist/**',
+ 'moment/*.min.js',
+ 'font-awesome-animation/dist/font-awesome-animation.min.css',
+ 'font-awesome/css/font-awesome.min.css',
+ 'font-awesome/fonts/**'
+ ]
+ },
+ deploy: {
+ target_ip: 'ip',
+ username: "user",
+ //port: 6666,
+ dir: '/tmp/xds-server'
+ }
+} \ No newline at end of file
diff --git a/webapp/gulpfile.js b/webapp/gulpfile.js
new file mode 100644
index 0000000..0226380
--- /dev/null
+++ b/webapp/gulpfile.js
@@ -0,0 +1,123 @@
+"use strict";
+//FIXME in VSC/eslint or add to typings declare function require(v: string): any;
+
+// FIXME: Rework based on
+// https://github.com/iotbzh/app-framework-templates/blob/master/templates/hybrid-html5/gulpfile.js
+// AND
+// https://github.com/antonybudianto/angular-starter
+// and/or
+// https://github.com/smmorneau/tour-of-heroes/blob/master/gulpfile.js
+
+const gulp = require("gulp"),
+ gulpif = require('gulp-if'),
+ del = require("del"),
+ sourcemaps = require('gulp-sourcemaps'),
+ tsc = require("gulp-typescript"),
+ tsProject = tsc.createProject("tsconfig.json"),
+ tslint = require('gulp-tslint'),
+ gulpSequence = require('gulp-sequence'),
+ rsync = require('gulp-rsync'),
+ conf = require('./gulp.conf');
+
+
+var tslintJsonFile = "./tslint.json"
+if (conf.prodMode) {
+ tslintJsonFile = "./tslint.prod.json"
+}
+
+
+/**
+ * Remove output directory.
+ */
+gulp.task('clean', (cb) => {
+ return del([conf.outDir], cb);
+});
+
+/**
+ * Lint all custom TypeScript files.
+ */
+gulp.task('tslint', function() {
+ return gulp.src(conf.paths.tsSources)
+ .pipe(tslint({
+ formatter: 'verbose',
+ configuration: tslintJsonFile
+ }))
+ .pipe(tslint.report());
+});
+
+/**
+ * Compile TypeScript sources and create sourcemaps in build directory.
+ */
+gulp.task("compile", ["tslint"], function() {
+ var tsResult = gulp.src(conf.paths.tsSources)
+ .pipe(sourcemaps.init())
+ .pipe(tsProject());
+ return tsResult.js
+ .pipe(sourcemaps.write(".", { sourceRoot: '/src' }))
+ .pipe(gulp.dest(conf.outDir));
+});
+
+/**
+ * Copy all resources that are not TypeScript files into build directory.
+ */
+gulp.task("resources", function() {
+ return gulp.src(["src/**/*", "!**/*.ts"])
+ .pipe(gulp.dest(conf.outDir));
+});
+
+/**
+ * Copy all assets into build directory.
+ */
+gulp.task("assets", function() {
+ return gulp.src(conf.paths.assets)
+ .pipe(gulp.dest(conf.outDir + "/assets"));
+});
+
+/**
+ * Copy all required libraries into build directory.
+ */
+gulp.task("libs", function() {
+ return gulp.src(conf.paths.node_modules_libs,
+ { cwd: "node_modules/**" }) /* Glob required here. */
+ .pipe(gulp.dest(conf.outDir + "/lib"));
+});
+
+/**
+ * Watch for changes in TypeScript, HTML and CSS files.
+ */
+gulp.task('watch', function () {
+ gulp.watch([conf.paths.tsSources], ['compile']).on('change', function (e) {
+ console.log('TypeScript file ' + e.path + ' has been changed. Compiling.');
+ });
+ gulp.watch(["src/**/*.html", "src/**/*.css"], ['resources']).on('change', function (e) {
+ console.log('Resource file ' + e.path + ' has been changed. Updating.');
+ });
+});
+
+/**
+ * Build the project.
+ */
+gulp.task("build", ['compile', 'resources', 'libs', 'assets'], function() {
+ console.log("Building the project ...");
+});
+
+/**
+ * Deploy the project on another machine/container
+ */
+gulp.task('rsync', function () {
+ return gulp.src(conf.outDir)
+ .pipe(rsync({
+ root: conf.outDir,
+ username: conf.deploy.username,
+ hostname: conf.deploy.target_ip,
+ port: conf.deploy.port || null,
+ archive: true,
+ recursive: true,
+ compress: true,
+ progress: false,
+ incremental: true,
+ destination: conf.deploy.dir
+ }));
+});
+
+gulp.task('deploy', gulpSequence('build', 'rsync')); \ No newline at end of file
diff --git a/webapp/package.json b/webapp/package.json
new file mode 100644
index 0000000..ecc6a78
--- /dev/null
+++ b/webapp/package.json
@@ -0,0 +1,62 @@
+{
+ "name": "xds-server",
+ "version": "1.0.0",
+ "description": "XDS (Cross Development System) Server",
+ "scripts": {
+ "clean": "gulp clean",
+ "compile": "gulp compile",
+ "build": "gulp build",
+ "start": "concurrently --kill-others \"gulp watch\" \"lite-server\""
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/xds-server"
+ },
+ "author": "Sebastien Douheret [IoT.bzh]",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/xds-server/issues"
+ },
+ "dependencies": {
+ "@angular/common": "2.4.4",
+ "@angular/compiler": "2.4.4",
+ "@angular/core": "2.4.4",
+ "@angular/forms": "2.4.4",
+ "@angular/http": "2.4.4",
+ "@angular/platform-browser": "2.4.4",
+ "@angular/platform-browser-dynamic": "2.4.4",
+ "@angular/router": "3.4.4",
+ "@angular/upgrade": "2.4.4",
+ "@types/core-js": "0.9.35",
+ "@types/node": "7.0.5",
+ "@types/socket.io-client": "^1.4.29",
+ "bootstrap": "^3.3.7",
+ "core-js": "^2.4.1",
+ "font-awesome": "^4.7.0",
+ "font-awesome-animation": "0.0.10",
+ "ngx-bootstrap": "1.6.6",
+ "ngx-cookie": "^1.0.0",
+ "reflect-metadata": "^0.1.8",
+ "rxjs": "5.0.3",
+ "socket.io-client": "^1.7.3",
+ "socketio": "^1.0.0",
+ "systemjs": "0.20.0",
+ "zone.js": "^0.7.6"
+ },
+ "devDependencies": {
+ "concurrently": "^3.1.0",
+ "del": "^2.2.0",
+ "gulp": "^3.9.1",
+ "gulp-if": "2.0.2",
+ "gulp-rsync": "0.0.7",
+ "gulp-sequence": "^0.4.6",
+ "gulp-sourcemaps": "^1.9.1",
+ "gulp-tslint": "^7.0.1",
+ "gulp-typescript": "^3.1.3",
+ "lite-server": "^2.2.2",
+ "ts-node": "^1.7.2",
+ "tslint": "^4.0.2",
+ "typescript": "^2.2.1",
+ "typings": "^2.0.0"
+ }
+}
diff --git a/webapp/src/app/alert/alert.component.ts b/webapp/src/app/alert/alert.component.ts
new file mode 100644
index 0000000..e9d7629
--- /dev/null
+++ b/webapp/src/app/alert/alert.component.ts
@@ -0,0 +1,30 @@
+import { Component } from '@angular/core';
+import { Observable } from 'rxjs';
+
+import {AlertService, IAlert} from '../common/alert.service';
+
+@Component({
+ selector: 'app-alert',
+ template: `
+ <div style="width:80%; margin-left:auto; margin-right:auto;" *ngFor="let alert of (alerts$ | async)">
+ <alert *ngIf="alert.show" [type]="alert.type" [dismissible]="alert.dismissible" [dismissOnTimeout]="alert.dismissTimeout"
+ (onClose)="onClose(alert)">
+ <span [innerHtml]="alert.msg"></span>
+ </alert>
+ </div>
+ `
+})
+
+export class AlertComponent {
+
+ alerts$: Observable<IAlert[]>;
+
+ constructor(private alertSvr: AlertService) {
+ this.alerts$ = this.alertSvr.alerts;
+ }
+
+ onClose(al) {
+ this.alertSvr.del(al);
+ }
+
+}
diff --git a/webapp/src/app/app.component.css b/webapp/src/app/app.component.css
new file mode 100644
index 0000000..0ec4936
--- /dev/null
+++ b/webapp/src/app/app.component.css
@@ -0,0 +1,17 @@
+.navbar-inverse {
+ background-color: #330066;
+}
+
+.navbar-brand {
+ background: #330066;
+ color: white;
+ font-size: x-large;
+}
+
+.navbar-nav ul li a {
+ color: #fff;
+}
+
+.menu-text {
+ color: #fff;
+}
diff --git a/webapp/src/app/app.component.html b/webapp/src/app/app.component.html
new file mode 100644
index 0000000..ab792be
--- /dev/null
+++ b/webapp/src/app/app.component.html
@@ -0,0 +1,21 @@
+<nav class="navbar navbar-fixed-top navbar-inverse">
+ <div class="container-fluid">
+ <div class="navbar-header">
+ <a class="navbar-brand" href="#">Cross Development System Dashboard</a>
+ </div>
+
+ <div class="navbar-collapse collapse menu2">
+ <ul class="nav navbar-nav navbar-right">
+ <li><a routerLink="/build"><i class="fa fa-2x fa-play-circle" title="Open build page"></i></a></li>
+ <li><a routerLink="/config"><i class="fa fa-2x fa-cog" title="Open configuration page"></i></a></li>
+ <li><a routerLink="/home"><i class="fa fa-2x fa-home" title="Back to home page"></i></a></li>
+ </ul>
+ </div>
+ </div>
+</nav>
+
+<app-alert id="alert"></app-alert>
+
+<div style="margin:10px;">
+ <router-outlet></router-outlet>
+</div> \ No newline at end of file
diff --git a/webapp/src/app/app.component.ts b/webapp/src/app/app.component.ts
new file mode 100644
index 0000000..d0f9c6e
--- /dev/null
+++ b/webapp/src/app/app.component.ts
@@ -0,0 +1,34 @@
+import { Component, OnInit, OnDestroy } from "@angular/core";
+import { Router } from '@angular/router';
+//TODO import {TranslateService} from "ng2-translate";
+
+@Component({
+ selector: 'app',
+ templateUrl: './app/app.component.html',
+ styleUrls: ['./app/app.component.css']
+})
+
+export class AppComponent implements OnInit, OnDestroy {
+ private defaultLanguage: string = 'en';
+
+ // I initialize the app component.
+ //TODO constructor(private translate: TranslateService) {
+ constructor(public router: Router) {
+ }
+
+ ngOnInit() {
+
+ /* TODO
+ this.translate.addLangs(["en", "fr"]);
+ this.translate.setDefaultLang(this.defaultLanguage);
+
+ let browserLang = this.translate.getBrowserLang();
+ this.translate.use(browserLang.match(/en|fr/) ? browserLang : this.defaultLanguage);
+ */
+ }
+
+ ngOnDestroy(): void {
+ }
+
+
+}
diff --git a/webapp/src/app/app.module.ts b/webapp/src/app/app.module.ts
new file mode 100644
index 0000000..5c33e43
--- /dev/null
+++ b/webapp/src/app/app.module.ts
@@ -0,0 +1,69 @@
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { HttpModule } from "@angular/http";
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { CookieModule } from 'ngx-cookie';
+
+// Import bootstrap
+import { AlertModule } from 'ngx-bootstrap/alert';
+import { ModalModule } from 'ngx-bootstrap/modal';
+import { AccordionModule } from 'ngx-bootstrap/accordion';
+import { CarouselModule } from 'ngx-bootstrap/carousel';
+import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
+
+// Import the application components and services.
+import { Routing, AppRoutingProviders } from './app.routing';
+import { AppComponent } from "./app.component";
+import { AlertComponent } from './alert/alert.component';
+import { ConfigComponent } from "./config/config.component";
+import { ProjectCardComponent } from "./projects/projectCard.component";
+import { ProjectReadableTypePipe } from "./projects/projectCard.component";
+import { ProjectsListAccordionComponent } from "./projects/projectsListAccordion.component";
+import { HomeComponent } from "./home/home.component";
+import { BuildComponent } from "./build/build.component";
+import { XDSServerService } from "./common/xdsserver.service";
+import { SyncthingService } from "./common/syncthing.service";
+import { ConfigService } from "./common/config.service";
+import { AlertService } from './common/alert.service';
+
+
+
+@NgModule({
+ imports: [
+ BrowserModule,
+ HttpModule,
+ FormsModule,
+ ReactiveFormsModule,
+ Routing,
+ CookieModule.forRoot(),
+ AlertModule.forRoot(),
+ ModalModule.forRoot(),
+ AccordionModule.forRoot(),
+ CarouselModule.forRoot(),
+ BsDropdownModule.forRoot(),
+ ],
+ declarations: [
+ AppComponent,
+ AlertComponent,
+ HomeComponent,
+ BuildComponent,
+ ConfigComponent,
+ ProjectCardComponent,
+ ProjectReadableTypePipe,
+ ProjectsListAccordionComponent,
+ ],
+ providers: [
+ AppRoutingProviders,
+ {
+ provide: Window,
+ useValue: window
+ },
+ XDSServerService,
+ ConfigService,
+ SyncthingService,
+ AlertService
+ ],
+ bootstrap: [AppComponent]
+})
+export class AppModule {
+} \ No newline at end of file
diff --git a/webapp/src/app/app.routing.ts b/webapp/src/app/app.routing.ts
new file mode 100644
index 0000000..747727c
--- /dev/null
+++ b/webapp/src/app/app.routing.ts
@@ -0,0 +1,19 @@
+import {Routes, RouterModule} from "@angular/router";
+import {ModuleWithProviders} from "@angular/core";
+import {ConfigComponent} from "./config/config.component";
+import {HomeComponent} from "./home/home.component";
+import {BuildComponent} from "./build/build.component";
+
+
+const appRoutes: Routes = [
+ {path: '', redirectTo: 'home', pathMatch: 'full'},
+
+ {path: 'config', component: ConfigComponent, data: {title: 'Config'}},
+ {path: 'home', component: HomeComponent, data: {title: 'Home'}},
+ {path: 'build', component: BuildComponent, data: {title: 'Build'}}
+];
+
+export const AppRoutingProviders: any[] = [];
+export const Routing: ModuleWithProviders = RouterModule.forRoot(appRoutes, {
+ useHash: true
+});
diff --git a/webapp/src/app/build/build.component.css b/webapp/src/app/build/build.component.css
new file mode 100644
index 0000000..5bfc898
--- /dev/null
+++ b/webapp/src/app/build/build.component.css
@@ -0,0 +1,10 @@
+.vcenter {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.blocks .btn-primary {
+ margin-left: 5px;
+ margin-right: 5px;
+ border-radius: 4px !important;
+} \ No newline at end of file
diff --git a/webapp/src/app/build/build.component.html b/webapp/src/app/build/build.component.html
new file mode 100644
index 0000000..d2a8da6
--- /dev/null
+++ b/webapp/src/app/build/build.component.html
@@ -0,0 +1,50 @@
+<form [formGroup]="buildForm">
+ <div class="row">
+ <div class="col-xs-6">
+ <label>Project </label>
+ <div class="btn-group" dropdown *ngIf="curProject">
+ <button dropdownToggle type="button" class="btn btn-primary dropdown-toggle" style="width: 14em;">
+ {{curProject.label}} <span class="caret" style="float: right; margin-top: 8px;"></span>
+ </button>
+ <ul *dropdownMenu class="dropdown-menu" role="menu">
+ <li role="menuitem"><a class="dropdown-item" *ngFor="let prj of (config$ | async)?.projects" (click)="curProject=prj">
+ {{prj.label}}</a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="col-xs-6" style="padding-right: 3em;">
+ <div class="btn-group blocks pull-right">
+ <button class="btn btn-primary " (click)="make() " [disabled]="!confValid ">Build</button>
+ <button class="btn btn-primary " (click)="make('clean') " [disabled]="!confValid ">Clean</button>
+ </div>
+ </div>
+ </div>
+ &nbsp;
+ <div class="row ">
+ <div class="col-xs-8 pull-left ">
+ <label>Sub-directory</label>
+ <input type="text" style="width:70%;" formControlName="subpath">
+ </div>
+ </div>
+</form>
+
+<div style="margin-left: 2em; margin-right: 2em; ">
+ <div class="row ">
+ <div class="col-xs-12 ">
+ <button class="btn btn-link pull-right " (click)="reset() "><span class="fa fa-eraser " style="font-size:20px; "></span></button>
+ </div>
+ </div>
+
+ <div class="row ">
+ <div class="col-xs-12 text-center ">
+ <textarea rows="30 " style="width:100%; overflow-y: scroll; " #scrollOutput>{{ cmdOutput }}</textarea>
+ </div>
+ </div>
+
+ <div class="row ">
+ <div class="col-xs-12 ">
+ {{ cmdInfo }}
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/webapp/src/app/build/build.component.ts b/webapp/src/app/build/build.component.ts
new file mode 100644
index 0000000..e1076c5
--- /dev/null
+++ b/webapp/src/app/build/build.component.ts
@@ -0,0 +1,120 @@
+import { Component, AfterViewChecked, ElementRef, ViewChild, OnInit } from '@angular/core';
+import { Observable } from 'rxjs';
+import { FormControl, FormGroup, Validators, FormBuilder } from '@angular/forms';
+
+import 'rxjs/add/operator/scan';
+import 'rxjs/add/operator/startWith';
+
+import { XDSServerService, ICmdOutput } from "../common/xdsserver.service";
+import { ConfigService, IConfig, IProject } from "../common/config.service";
+import { AlertService, IAlert } from "../common/alert.service";
+
+@Component({
+ selector: 'build',
+ moduleId: module.id,
+ templateUrl: './build.component.html',
+ styleUrls: ['./build.component.css']
+})
+
+export class BuildComponent implements OnInit, AfterViewChecked {
+ @ViewChild('scrollOutput') private scrollContainer: ElementRef;
+
+ config$: Observable<IConfig>;
+
+ buildForm: FormGroup;
+ subpathCtrl = new FormControl("", Validators.required);
+
+ public cmdOutput: string;
+ public confValid: boolean;
+ public curProject: IProject;
+ public cmdInfo: string;
+
+ private startTime: Map<string, number> = new Map<string, number>();
+
+ // I initialize the app component.
+ constructor(private configSvr: ConfigService, private sdkSvr: XDSServerService,
+ private fb: FormBuilder, private alertSvr: AlertService
+ ) {
+ this.cmdOutput = "";
+ this.confValid = false;
+ this.cmdInfo = ""; // TODO: to be remove (only for debug)
+ this.buildForm = fb.group({ subpath: this.subpathCtrl });
+ }
+
+ ngOnInit() {
+ this.config$ = this.configSvr.conf;
+ this.config$.subscribe((cfg) => {
+ this.curProject = cfg.projects[0];
+
+ this.confValid = (cfg.projects.length && this.curProject.id != null);
+ });
+
+ // Command output data tunneling
+ this.sdkSvr.CmdOutput$.subscribe(data => {
+ this.cmdOutput += data.stdout + "\n";
+ });
+
+ // Command exit
+ this.sdkSvr.CmdExit$.subscribe(exit => {
+ if (this.startTime.has(exit.cmdID)) {
+ this.cmdInfo = 'Last command duration: ' + this._computeTime(this.startTime.get(exit.cmdID));
+ this.startTime.delete(exit.cmdID);
+ }
+
+ if (exit && exit.code !== 0) {
+ this.cmdOutput += "--- Command exited with code " + exit.code + " ---\n\n";
+ }
+ });
+
+ this._scrollToBottom();
+ }
+
+ ngAfterViewChecked() {
+ this._scrollToBottom();
+ }
+
+ reset() {
+ this.cmdOutput = '';
+ }
+
+ make(args: string) {
+ let prjID = this.curProject.id;
+
+ this.cmdOutput += this._outputHeader();
+
+ let t0 = performance.now();
+ this.cmdInfo = 'Start build of ' + prjID + ' at ' + t0;
+
+ this.sdkSvr.make(prjID, this.buildForm.value.subpath, args)
+ .subscribe(res => {
+ this.startTime.set(String(res.cmdID), t0);
+ },
+ err => {
+ this.cmdInfo = 'Last command duration: ' + this._computeTime(t0);
+ this.alertSvr.add({ type: "danger", msg: 'ERROR: ' + err });
+ });
+ }
+
+ private _scrollToBottom(): void {
+ try {
+ this.scrollContainer.nativeElement.scrollTop = this.scrollContainer.nativeElement.scrollHeight;
+ } catch (err) { }
+ }
+
+ private _computeTime(t0: number, t1?: number): string {
+ let enlap = Math.round((t1 || performance.now()) - t0);
+ if (enlap < 1000.0) {
+ return enlap.toFixed(2) + ' ms';
+ } else {
+ return (enlap / 1000.0).toFixed(3) + ' seconds';
+ }
+ }
+
+ private _outputHeader(): string {
+ return "--- " + new Date().toString() + " ---\n";
+ }
+
+ private _outputFooter(): string {
+ return "\n";
+ }
+} \ No newline at end of file
diff --git a/webapp/src/app/common/alert.service.ts b/webapp/src/app/common/alert.service.ts
new file mode 100644
index 0000000..710046f
--- /dev/null
+++ b/webapp/src/app/common/alert.service.ts
@@ -0,0 +1,64 @@
+import { Injectable, SecurityContext } from '@angular/core';
+import { DomSanitizer } from '@angular/platform-browser';
+import { Observable } from 'rxjs/Observable';
+import { Subject } from 'rxjs/Subject';
+
+
+export type AlertType = "danger" | "warning" | "info" | "success";
+
+export interface IAlert {
+ type: AlertType;
+ msg: string;
+ show?: boolean;
+ dismissible?: boolean;
+ dismissTimeout?: number; // close alert after this time (in seconds)
+ id?: number;
+}
+
+@Injectable()
+export class AlertService {
+ public alerts: Observable<IAlert[]>;
+
+ private _alerts: IAlert[];
+ private alertsSubject = <Subject<IAlert[]>>new Subject();
+ private uid = 0;
+ private defaultDissmissTmo = 5; // in seconds
+
+ constructor(private sanitizer: DomSanitizer) {
+ this.alerts = this.alertsSubject.asObservable();
+ this._alerts = [];
+ this.uid = 0;
+ }
+
+ public error(msg: string) {
+ this.add({ type: "danger", msg: msg, dismissible: true });
+ }
+
+ public warning(msg: string, dismissible?: boolean) {
+ this.add({ type: "warning", msg: msg, dismissible: true, dismissTimeout: (dismissible ? this.defaultDissmissTmo : 0) });
+ }
+
+ public info(msg: string) {
+ this.add({ type: "warning", msg: msg, dismissible: true, dismissTimeout: this.defaultDissmissTmo });
+ }
+
+ public add(al: IAlert) {
+ this._alerts.push({
+ show: true,
+ type: al.type,
+ msg: this.sanitizer.sanitize(SecurityContext.HTML, al.msg),
+ dismissible: al.dismissible || true,
+ dismissTimeout: (al.dismissTimeout * 1000) || 0,
+ id: this.uid,
+ });
+ this.uid += 1;
+ this.alertsSubject.next(this._alerts);
+ }
+
+ public del(al: IAlert) {
+ let idx = this._alerts.findIndex((a) => a.id === al.id);
+ if (idx > -1) {
+ this._alerts.splice(idx, 1);
+ }
+ }
+}
diff --git a/webapp/src/app/common/config.service.ts b/webapp/src/app/common/config.service.ts
new file mode 100644
index 0000000..67ee14c
--- /dev/null
+++ b/webapp/src/app/common/config.service.ts
@@ -0,0 +1,276 @@
+import { Injectable, OnInit } from '@angular/core';
+import { Http, Headers, RequestOptionsArgs, Response } from '@angular/http';
+import { Location } from '@angular/common';
+import { CookieService } from 'ngx-cookie';
+import { Observable } from 'rxjs/Observable';
+import { Subscriber } from 'rxjs/Subscriber';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+
+// Import RxJs required methods
+import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/observable/throw';
+import 'rxjs/add/operator/mergeMap';
+
+
+import { XDSServerService, IXDSConfigProject } from "../common/xdsserver.service";
+import { SyncthingService, ISyncThingProject, ISyncThingStatus } from "../common/syncthing.service";
+import { AlertService, IAlert } from "../common/alert.service";
+
+export enum ProjectType {
+ NATIVE = 1,
+ SYNCTHING = 2
+}
+
+export interface INativeProject {
+ // TODO
+}
+
+export interface IProject {
+ id?: string;
+ label: string;
+ path: string;
+ type: ProjectType;
+ remotePrjDef?: INativeProject | ISyncThingProject;
+ localPrjDef?: any;
+ isExpanded?: boolean;
+ visible?: boolean;
+}
+
+export interface ILocalSTConfig {
+ ID: string;
+ URL: string;
+ retry: number;
+ tilde: string;
+}
+
+export interface IConfig {
+ xdsServerURL: string;
+ projectsRootDir: string;
+ projects: IProject[];
+ localSThg: ILocalSTConfig;
+}
+
+@Injectable()
+export class ConfigService {
+
+ public conf: Observable<IConfig>;
+
+ private confSubject: BehaviorSubject<IConfig>;
+ private confStore: IConfig;
+ private stConnectObs = null;
+
+ constructor(private _window: Window,
+ private cookie: CookieService,
+ private sdkSvr: XDSServerService,
+ private stSvr: SyncthingService,
+ private alert: AlertService,
+ ) {
+ this.load();
+ this.confSubject = <BehaviorSubject<IConfig>>new BehaviorSubject(this.confStore);
+ this.conf = this.confSubject.asObservable();
+
+ // force to load projects
+ this.loadProjects();
+ }
+
+ // Load config
+ load() {
+ // Try to retrieve previous config from cookie
+ let cookConf = this.cookie.getObject("xds-config");
+ if (cookConf != null) {
+ this.confStore = <IConfig>cookConf;
+ } else {
+ // Set default config
+ this.confStore = {
+ xdsServerURL: this._window.location.origin + '/api/v1',
+ projectsRootDir: "",
+ projects: [],
+ localSThg: {
+ ID: null,
+ URL: "http://localhost:8384",
+ retry: 10, // 10 seconds
+ tilde: "",
+ }
+ };
+ }
+ }
+
+ // Save config into cookie
+ save() {
+ // Notify subscribers
+ this.confSubject.next(Object.assign({}, this.confStore));
+
+ // Don't save projects in cookies (too big!)
+ let cfg = this.confStore;
+ delete(cfg.projects);
+ this.cookie.putObject("xds-config", cfg);
+ }
+
+ loadProjects() {
+ // Remove previous subscriber if existing
+ if (this.stConnectObs) {
+ try {
+ this.stConnectObs.unsubscribe();
+ } catch (err) { }
+ this.stConnectObs = null;
+ }
+
+ // First setup connection with local SyncThing
+ let retry = this.confStore.localSThg.retry;
+ let url = this.confStore.localSThg.URL;
+ this.stConnectObs = this.stSvr.connect(retry, url).subscribe((sts) => {
+ this.confStore.localSThg.ID = sts.ID;
+ this.confStore.localSThg.tilde = sts.tilde;
+ if (this.confStore.projectsRootDir === "") {
+ this.confStore.projectsRootDir = sts.tilde;
+ }
+
+ // Rebuild projects definition from local and remote syncthing
+ this.confStore.projects = [];
+
+ this.sdkSvr.getProjects().subscribe(remotePrj => {
+ this.stSvr.getProjects().subscribe(localPrj => {
+ remotePrj.forEach(rPrj => {
+ let lPrj = localPrj.filter(item => item.id === rPrj.id);
+ if (lPrj.length > 0) {
+ let pp: IProject = {
+ id: rPrj.id,
+ label: rPrj.label,
+ path: rPrj.path,
+ type: ProjectType.SYNCTHING, // FIXME support other types
+ remotePrjDef: Object.assign({}, rPrj),
+ localPrjDef: Object.assign({}, lPrj[0]),
+ };
+ this.confStore.projects.push(pp);
+ }
+ });
+ this.confSubject.next(Object.assign({}, this.confStore));
+ }), error => this.alert.error('Could not load initial state of local projects.');
+ }), error => this.alert.error('Could not load initial state of remote projects.');
+
+ }, error => this.alert.error(error));
+ }
+
+ set syncToolURL(url: string) {
+ this.confStore.localSThg.URL = url;
+ this.save();
+ }
+
+ set syncToolRetry(r: number) {
+ this.confStore.localSThg.retry = r;
+ this.save();
+ }
+
+ set projectsRootDir(p: string) {
+ if (p.charAt(0) === '~') {
+ p = this.confStore.localSThg.tilde + p.substring(1);
+ }
+ this.confStore.projectsRootDir = p;
+ this.save();
+ }
+
+ getLabelRootName(): string {
+ let id = this.confStore.localSThg.ID;
+ if (!id || id === "") {
+ return null;
+ }
+ return id.slice(0, 15);
+ }
+
+ addProject(prj: IProject) {
+ // Substitute tilde with to user home path
+ prj.path = prj.path.trim();
+ if (prj.path.charAt(0) === '~') {
+ prj.path = this.confStore.localSThg.tilde + prj.path.substring(1);
+
+ // Must be a full path (on Linux or Windows)
+ } else if (!((prj.path.charAt(0) === '/') ||
+ (prj.path.charAt(1) === ':' && (prj.path.charAt(2) === '\\' || prj.path.charAt(2) === '/')))) {
+ prj.path = this.confStore.projectsRootDir + '/' + prj.path;
+ }
+
+ if (prj.id == null) {
+ // FIXME - must be done on server side
+ let prefix = this.getLabelRootName() || new Date().toISOString();
+ let splath = prj.path.split('/');
+ prj.id = prefix + "_" + splath[splath.length - 1];
+ }
+
+ if (this._getProjectIdx(prj.id) !== -1) {
+ this.alert.warning("Project already exist (id=" + prj.id + ")", true);
+ return;
+ }
+
+ // TODO - support others project types
+ if (prj.type !== ProjectType.SYNCTHING) {
+ this.alert.error('Project type not supported yet (type: ' + prj.type + ')');
+ return;
+ }
+
+ let sdkPrj: IXDSConfigProject = {
+ id: prj.id,
+ label: prj.label,
+ path: prj.path,
+ hostSyncThingID: this.confStore.localSThg.ID,
+ };
+
+ // Send config to XDS server
+ let newPrj = prj;
+ this.sdkSvr.addProject(sdkPrj)
+ .subscribe(resStRemotePrj => {
+ newPrj.remotePrjDef = resStRemotePrj;
+
+ // FIXME REWORK local ST config
+ // move logic to server side tunneling-back by WS
+
+ // Now setup local config
+ let stLocPrj: ISyncThingProject = {
+ id: sdkPrj.id,
+ label: sdkPrj.label,
+ path: sdkPrj.path,
+ remoteSyncThingID: resStRemotePrj.builderSThgID
+ };
+
+ // Set local Syncthing config
+ this.stSvr.addProject(stLocPrj)
+ .subscribe(resStLocalPrj => {
+ newPrj.localPrjDef = resStLocalPrj;
+
+ // FIXME: maybe reduce subject to only .project
+ //this.confSubject.next(Object.assign({}, this.confStore).project);
+ this.confStore.projects.push(Object.assign({}, newPrj));
+ this.confSubject.next(Object.assign({}, this.confStore));
+ },
+ err => {
+ this.alert.error("Configuration local ERROR: " + err);
+ });
+ },
+ err => {
+ this.alert.error("Configuration remote ERROR: " + err);
+ });
+ }
+
+ deleteProject(prj: IProject) {
+ let idx = this._getProjectIdx(prj.id);
+ if (idx === -1) {
+ throw new Error("Invalid project id (id=" + prj.id + ")");
+ }
+ this.sdkSvr.deleteProject(prj.id)
+ .subscribe(res => {
+ this.stSvr.deleteProject(prj.id)
+ .subscribe(res => {
+ this.confStore.projects.splice(idx, 1);
+ }, err => {
+ this.alert.error("Delete local ERROR: " + err);
+ });
+ }, err => {
+ this.alert.error("Delete remote ERROR: " + err);
+ });
+ }
+
+ private _getProjectIdx(id: string): number {
+ return this.confStore.projects.findIndex((item) => item.id === id);
+ }
+
+} \ No newline at end of file
diff --git a/webapp/src/app/common/syncthing.service.ts b/webapp/src/app/common/syncthing.service.ts
new file mode 100644
index 0000000..c8b0193
--- /dev/null
+++ b/webapp/src/app/common/syncthing.service.ts
@@ -0,0 +1,342 @@
+import { Injectable } from '@angular/core';
+import { Http, Headers, RequestOptionsArgs, Response } from '@angular/http';
+import { Location } from '@angular/common';
+import { Observable } from 'rxjs/Observable';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+
+// Import RxJs required methods
+import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/observable/throw';
+import 'rxjs/add/observable/of';
+import 'rxjs/add/observable/timer';
+import 'rxjs/add/operator/retryWhen';
+
+export interface ISyncThingProject {
+ id: string;
+ path: string;
+ remoteSyncThingID: string;
+ label?: string;
+}
+
+export interface ISyncThingStatus {
+ ID: string;
+ baseURL: string;
+ connected: boolean;
+ tilde: string;
+ rawStatus: any;
+}
+
+// Private interfaces of Syncthing
+const ISTCONFIG_VERSION = 19;
+
+interface ISTFolderDeviceConfiguration {
+ deviceID: string;
+ introducedBy: string;
+}
+interface ISTFolderConfiguration {
+ id: string;
+ label: string;
+ path: string;
+ type?: number;
+ devices?: ISTFolderDeviceConfiguration[];
+ rescanIntervalS?: number;
+ ignorePerms?: boolean;
+ autoNormalize?: boolean;
+ minDiskFreePct?: number;
+ versioning?: { type: string; params: string[] };
+ copiers?: number;
+ pullers?: number;
+ hashers?: number;
+ order?: number;
+ ignoreDelete?: boolean;
+ scanProgressIntervalS?: number;
+ pullerSleepS?: number;
+ pullerPauseS?: number;
+ maxConflicts?: number;
+ disableSparseFiles?: boolean;
+ disableTempIndexes?: boolean;
+ fsync?: boolean;
+ paused?: boolean;
+}
+
+interface ISTDeviceConfiguration {
+ deviceID: string;
+ name?: string;
+ address?: string[];
+ compression?: string;
+ certName?: string;
+ introducer?: boolean;
+ skipIntroductionRemovals?: boolean;
+ introducedBy?: string;
+ paused?: boolean;
+ allowedNetwork?: string[];
+}
+
+interface ISTGuiConfiguration {
+ enabled: boolean;
+ address: string;
+ user?: string;
+ password?: string;
+ useTLS: boolean;
+ apiKey?: string;
+ insecureAdminAccess?: boolean;
+ theme: string;
+ debugging: boolean;
+ insecureSkipHostcheck?: boolean;
+}
+
+interface ISTOptionsConfiguration {
+ listenAddresses: string[];
+ globalAnnounceServer: string[];
+ // To be completed ...
+}
+
+interface ISTConfiguration {
+ version: number;
+ folders: ISTFolderConfiguration[];
+ devices: ISTDeviceConfiguration[];
+ gui: ISTGuiConfiguration;
+ options: ISTOptionsConfiguration;
+ ignoredDevices: string[];
+}
+
+// Default settings
+const DEFAULT_GUI_PORT = 8384;
+const DEFAULT_GUI_API_KEY = "1234abcezam";
+
+
+@Injectable()
+export class SyncthingService {
+
+ public Status$: Observable<ISyncThingStatus>;
+
+ private baseRestUrl: string;
+ private apikey: string;
+ private localSTID: string;
+ private stCurVersion: number;
+ private _status: ISyncThingStatus = {
+ ID: null,
+ baseURL: "",
+ connected: false,
+ tilde: "",
+ rawStatus: null,
+ };
+ private statusSubject = <BehaviorSubject<ISyncThingStatus>>new BehaviorSubject(this._status);
+
+ constructor(private http: Http, private _window: Window) {
+ this._status.baseURL = 'http://localhost:' + DEFAULT_GUI_PORT;
+ this.baseRestUrl = this._status.baseURL + '/rest';
+ this.apikey = DEFAULT_GUI_API_KEY;
+ this.stCurVersion = -1;
+
+ this.Status$ = this.statusSubject.asObservable();
+ }
+
+ connect(retry: number, url?: string): Observable<ISyncThingStatus> {
+ if (url) {
+ this._status.baseURL = url;
+ this.baseRestUrl = this._status.baseURL + '/rest';
+ }
+ this._status.connected = false;
+ this._status.ID = null;
+ return this.getStatus(retry);
+ }
+
+ getID(retry?: number): Observable<string> {
+ if (this._status.ID != null) {
+ return Observable.of(this._status.ID);
+ }
+ return this.getStatus(retry).map(sts => sts.ID);
+ }
+
+ getStatus(retry?: number): Observable<ISyncThingStatus> {
+
+ if (retry == null) {
+ retry = 3600; // 1 hour
+ }
+ return this._get('/system/status')
+ .map((status) => {
+ this._status.ID = status["myID"];
+ this._status.tilde = status["tilde"];
+ this._status.connected = true;
+ console.debug('ST local ID', this._status.ID);
+
+ this._status.rawStatus = status;
+
+ return this._status;
+ })
+ .retryWhen((attempts) => {
+ let count = 0;
+ return attempts.flatMap(error => {
+ if (++count >= retry) {
+ return this._handleError(error);
+ } else {
+ return Observable.timer(count * 1000);
+ }
+ });
+ });
+ }
+
+ getProjects(): Observable<ISTFolderConfiguration[]> {
+ return this._getConfig()
+ .map((conf) => conf.folders);
+ }
+
+ addProject(prj: ISyncThingProject): Observable<ISTFolderConfiguration> {
+ return this.getID()
+ .flatMap(() => this._getConfig())
+ .flatMap((stCfg) => {
+ let newDevID = prj.remoteSyncThingID;
+
+ // Add new Device if needed
+ let dev = stCfg.devices.filter(item => item.deviceID === newDevID);
+ if (dev.length <= 0) {
+ stCfg.devices.push(
+ {
+ deviceID: newDevID,
+ name: "Builder_" + newDevID.slice(0, 15),
+ address: ["dynamic"],
+ }
+ );
+ }
+
+ // Add or update Folder settings
+ let label = prj.label || "";
+ let folder: ISTFolderConfiguration = {
+ id: prj.id,
+ label: label,
+ path: prj.path,
+ devices: [{ deviceID: newDevID, introducedBy: "" }],
+ autoNormalize: true,
+ };
+
+ let idx = stCfg.folders.findIndex(item => item.id === prj.id);
+ if (idx === -1) {
+ stCfg.folders.push(folder);
+ } else {
+ let newFld = Object.assign({}, stCfg.folders[idx], folder);
+ stCfg.folders[idx] = newFld;
+ }
+
+ // Set new config
+ return this._setConfig(stCfg);
+ })
+ .flatMap(() => this._getConfig())
+ .map((newConf) => {
+ let idx = newConf.folders.findIndex(item => item.id === prj.id);
+ return newConf.folders[idx];
+ });
+ }
+
+ deleteProject(id: string): Observable<ISTFolderConfiguration> {
+ let delPrj: ISTFolderConfiguration;
+ return this._getConfig()
+ .flatMap((conf: ISTConfiguration) => {
+ let idx = conf.folders.findIndex(item => item.id === id);
+ if (idx === -1) {
+ throw new Error("Cannot delete project: not found");
+ }
+ delPrj = Object.assign({}, conf.folders[idx]);
+ conf.folders.splice(idx, 1);
+ return this._setConfig(conf);
+ })
+ .map(() => delPrj);
+ }
+
+ /*
+ * --- Private functions ---
+ */
+ private _getConfig(): Observable<ISTConfiguration> {
+ return this._get('/system/config');
+ }
+
+ private _setConfig(cfg: ISTConfiguration): Observable<any> {
+ return this._post('/system/config', cfg);
+ }
+
+ private _attachAuthHeaders(options?: any) {
+ options = options || {};
+ let headers = options.headers || new Headers();
+ // headers.append('Authorization', 'Basic ' + btoa('username:password'));
+ headers.append('Accept', 'application/json');
+ headers.append('Content-Type', 'application/json');
+ if (this.apikey !== "") {
+ headers.append('X-API-Key', this.apikey);
+
+ }
+ options.headers = headers;
+ return options;
+ }
+
+ private _checkAlive(): Observable<boolean> {
+ return this.http.get(this.baseRestUrl + '/system/version', this._attachAuthHeaders())
+ .map((r) => this._status.connected = true)
+ .repeatWhen
+ .catch((err) => {
+ this._status.connected = false;
+ throw new Error("Syncthing local daemon not responding (url=" + this._status.baseURL + ")");
+ });
+ }
+
+ private _getAPIVersion(): Observable<number> {
+ if (this.stCurVersion !== -1) {
+ return Observable.of(this.stCurVersion);
+ }
+
+ return this._checkAlive()
+ .flatMap(() => this.http.get(this.baseRestUrl + '/system/config', this._attachAuthHeaders()))
+ .map((res: Response) => {
+ let conf: ISTConfiguration = res.json();
+ this.stCurVersion = (conf && conf.version) || -1;
+ return this.stCurVersion;
+ })
+ .catch(this._handleError);
+ }
+
+ private _checkAPIVersion(): Observable<number> {
+ return this._getAPIVersion().map(ver => {
+ if (ver !== ISTCONFIG_VERSION) {
+ throw new Error("Unsupported Syncthing version api (" + ver +
+ " != " + ISTCONFIG_VERSION + ") !");
+ }
+ return ver;
+ });
+ }
+
+ private _get(url: string): Observable<any> {
+ return this._checkAPIVersion()
+ .flatMap(() => this.http.get(this.baseRestUrl + url, this._attachAuthHeaders()))
+ .map((res: Response) => res.json())
+ .catch(this._handleError);
+ }
+
+ private _post(url: string, body: any): Observable<any> {
+ return this._checkAPIVersion()
+ .flatMap(() => this.http.post(this.baseRestUrl + url, JSON.stringify(body), this._attachAuthHeaders()))
+ .map((res: Response) => {
+ if (res && res.status && res.status === 200) {
+ return res;
+ }
+ throw new Error(res.toString());
+
+ })
+ .catch(this._handleError);
+ }
+
+ private _handleError(error: Response | any) {
+ // In a real world app, you might use a remote logging infrastructure
+ let errMsg: string;
+ if (this._status) {
+ this._status.connected = false;
+ }
+ if (error instanceof Response) {
+ const body = error.json() || 'Server error';
+ const err = body.error || JSON.stringify(body);
+ errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
+ } else {
+ errMsg = error.message ? error.message : error.toString();
+ }
+ return Observable.throw(errMsg);
+ }
+}
diff --git a/webapp/src/app/common/xdsserver.service.ts b/webapp/src/app/common/xdsserver.service.ts
new file mode 100644
index 0000000..fd2e32a
--- /dev/null
+++ b/webapp/src/app/common/xdsserver.service.ts
@@ -0,0 +1,216 @@
+import { Injectable } from '@angular/core';
+import { Http, Headers, RequestOptionsArgs, Response } from '@angular/http';
+import { Location } from '@angular/common';
+import { Observable } from 'rxjs/Observable';
+import { Subject } from 'rxjs/Subject';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+import * as io from 'socket.io-client';
+
+import { AlertService } from './alert.service';
+
+// Import RxJs required methods
+import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/observable/throw';
+import 'rxjs/add/operator/mergeMap';
+
+
+export interface IXDSConfigProject {
+ id: string;
+ path: string;
+ hostSyncThingID: string;
+ label?: string;
+}
+
+interface IXDSBuilderConfig {
+ ip: string;
+ port: string;
+ syncThingID: string;
+}
+
+interface IXDSFolderConfig {
+ id: string;
+ label: string;
+ path: string;
+ type: number;
+ syncThingID: string;
+ builderSThgID?: string;
+ status?: string;
+}
+
+interface IXDSConfig {
+ version: number;
+ builder: IXDSBuilderConfig;
+ folders: IXDSFolderConfig[];
+}
+
+export interface ISdkMessage {
+ wsID: string;
+ msgType: string;
+ data: any;
+}
+
+export interface ICmdOutput {
+ cmdID: string;
+ timestamp: string;
+ stdout: string;
+ stderr: string;
+}
+
+export interface ICmdExit {
+ cmdID: string;
+ timestamp: string;
+ code: number;
+ error: string;
+}
+
+export interface IServerStatus {
+ WS_connected: boolean;
+
+}
+
+const FOLDER_TYPE_CLOUDSYNC = 2;
+
+@Injectable()
+export class XDSServerService {
+
+ public CmdOutput$ = <Subject<ICmdOutput>>new Subject();
+ public CmdExit$ = <Subject<ICmdExit>>new Subject();
+ public Status$: Observable<IServerStatus>;
+
+ private baseUrl: string;
+ private wsUrl: string;
+ private _status = { WS_connected: false };
+ private statusSubject = <BehaviorSubject<IServerStatus>>new BehaviorSubject(this._status);
+
+
+ private socket: SocketIOClient.Socket;
+
+ constructor(private http: Http, private _window: Window, private alert: AlertService) {
+
+ this.Status$ = this.statusSubject.asObservable();
+
+ this.baseUrl = this._window.location.origin + '/api/v1';
+ let re = this._window.location.origin.match(/http[s]?:\/\/([^\/]*)[\/]?/);
+ if (re === null || re.length < 2) {
+ console.error('ERROR: cannot determine Websocket url');
+ } else {
+ this.wsUrl = 'ws://' + re[1];
+ this._handleIoSocket();
+ }
+ }
+
+ private _WSState(sts: boolean) {
+ this._status.WS_connected = sts;
+ this.statusSubject.next(Object.assign({}, this._status));
+ }
+
+ private _handleIoSocket() {
+ this.socket = io(this.wsUrl, { transports: ['websocket'] });
+
+ this.socket.on('connect_error', (res) => {
+ this._WSState(false);
+ console.error('WS Connect_error ', res);
+ });
+
+ this.socket.on('connect', (res) => {
+ this._WSState(true);
+ });
+
+ this.socket.on('disconnection', (res) => {
+ this._WSState(false);
+ this.alert.error('WS disconnection: ' + res);
+ });
+
+ this.socket.on('error', (err) => {
+ console.error('WS error:', err);
+ });
+
+ this.socket.on('make:output', data => {
+ this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data));
+ });
+
+ this.socket.on('make:exit', data => {
+ this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
+ });
+
+ }
+
+ getProjects(): Observable<IXDSFolderConfig[]> {
+ return this._get('/folders');
+ }
+
+ addProject(cfg: IXDSConfigProject): Observable<IXDSFolderConfig> {
+ let folder: IXDSFolderConfig = {
+ id: cfg.id || null,
+ label: cfg.label || "",
+ path: cfg.path,
+ type: FOLDER_TYPE_CLOUDSYNC,
+ syncThingID: cfg.hostSyncThingID
+ };
+ return this._post('/folder', folder);
+ }
+
+ deleteProject(id: string): Observable<IXDSFolderConfig> {
+ return this._delete('/folder/' + id);
+ }
+
+ exec(cmd: string, args?: string[], options?: any): Observable<any> {
+ return this._post('/exec',
+ {
+ cmd: cmd,
+ args: args || []
+ });
+ }
+
+ make(prjID: string, dir: string, args: string): Observable<any> {
+ return this._post('/make', { id: prjID, rpath: dir, args: args });
+ }
+
+
+ private _attachAuthHeaders(options?: any) {
+ options = options || {};
+ let headers = options.headers || new Headers();
+ // headers.append('Authorization', 'Basic ' + btoa('username:password'));
+ headers.append('Accept', 'application/json');
+ headers.append('Content-Type', 'application/json');
+ // headers.append('Access-Control-Allow-Origin', '*');
+
+ options.headers = headers;
+ return options;
+ }
+
+ private _get(url: string): Observable<any> {
+ return this.http.get(this.baseUrl + url, this._attachAuthHeaders())
+ .map((res: Response) => res.json())
+ .catch(this._decodeError);
+ }
+ private _post(url: string, body: any): Observable<any> {
+ return this.http.post(this.baseUrl + url, JSON.stringify(body), this._attachAuthHeaders())
+ .map((res: Response) => res.json())
+ .catch((error) => {
+ return this._decodeError(error);
+ });
+ }
+ private _delete(url: string): Observable<any> {
+ return this.http.delete(this.baseUrl + url, this._attachAuthHeaders())
+ .map((res: Response) => res.json())
+ .catch(this._decodeError);
+ }
+
+ private _decodeError(err: any) {
+ let e: string;
+ if (typeof err === "object") {
+ if (err.statusText) {
+ e = err.statusText;
+ } else if (err.error) {
+ e = String(err.error);
+ } else {
+ e = JSON.stringify(err);
+ }
+ } else {
+ e = err.json().error || 'Server error';
+ }
+ return Observable.throw(e);
+ }
+}
diff --git a/webapp/src/app/config/config.component.css b/webapp/src/app/config/config.component.css
new file mode 100644
index 0000000..f480857
--- /dev/null
+++ b/webapp/src/app/config/config.component.css
@@ -0,0 +1,26 @@
+.fa-size-x2 {
+ font-size: 20px;
+}
+
+h2 {
+ font-family: sans-serif;
+ font-variant: small-caps;
+ font-size: x-large;
+}
+
+th span {
+ font-weight: 100;
+}
+
+th label {
+ font-weight: 100;
+ margin-bottom: 0;
+}
+
+tr.info>th {
+ vertical-align: middle;
+}
+
+tr.info>td {
+ vertical-align: middle;
+} \ No newline at end of file
diff --git a/webapp/src/app/config/config.component.html b/webapp/src/app/config/config.component.html
new file mode 100644
index 0000000..45b0e14
--- /dev/null
+++ b/webapp/src/app/config/config.component.html
@@ -0,0 +1,73 @@
+<div class="panel panel-default">
+ <div class="panel-heading clearfix">
+ <h2 class="panel-title pull-left">Global Configuration</h2>
+ <div class="pull-right">
+ <span class="fa fa-fw fa-exchange fa-size-x2" [style.color]="((severStatus$ | async)?.WS_connected)?'green':'red'"></span>
+ </div>
+ </div>
+ <div class="panel-body">
+ <div class="row">
+ <div class="col-xs-12">
+ <table class="table table-condensed">
+ <tbody>
+ <tr [ngClass]="{'info': (localSTStatus$ | async)?.connected, 'danger': !(localSTStatus$ | async)?.connected}">
+ <th><label>Local Sync-tool URL</label></th>
+ <td> <input type="text" [(ngModel)]="syncToolUrl"></td>
+ <td>
+ <button class="btn btn-link" (click)="syncToolRestartConn()"><span class="fa fa-refresh fa-size-x2"></span></button>
+ </td>
+ </tr>
+ <tr class="info">
+ <th><label>Local Sync-tool connection retry</label></th>
+ <td> <input type="text" [(ngModel)]="syncToolRetry" (ngModelChange)="showApplyBtn['retry'] = true"></td>
+ <td>
+ <button *ngIf="showApplyBtn['retry']" class="btn btn-primary btn-xs" (click)="submitGlobConf('retry')">APPLY</button>
+ </td>
+ </tr>
+ <tr class="info">
+ <th><label>Local Projects root directory</label></th>
+ <td> <input type="text" [(ngModel)]="projectsRootDir" (ngModelChange)="showApplyBtn['rootDir'] = true"></td>
+ <td>
+ <button *ngIf="showApplyBtn['rootDir']" class="btn btn-primary btn-xs" (click)="submitGlobConf('rootDir')">APPLY</button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div class="panel panel-default">
+ <div class="panel-heading">
+ <h2 class="panel-title">Projects Configuration</h2>
+ </div>
+ <div class="panel-body">
+ <form [formGroup]="addProjectForm" (ngSubmit)="onSubmit()">
+ <div class="row ">
+ <div class="col-xs-2">
+ <button class="btn btn-primary" type="submit" [disabled]="!addProjectForm.valid"><i class="fa fa-plus"></i>&nbsp;New Folder</button>
+ </div>
+
+ <div class="col-xs-6">
+ <label>Folder Path </label>
+ <input type="text" style="width:70%;" formControlName="path" placeholder="myProject">
+ </div>
+ <div class="col-xs-4">
+ <label>Label </label>
+ <input type="text" formControlName="label" (keyup)="onKeyLabel($event)">
+ </div>
+ </div>
+ </form>
+
+ <div class="row col-xs-12">
+ <projects-list-accordion [projects]="(config$ | async).projects"></projects-list-accordion>
+ </div>
+ </div>
+</div>
+
+
+<!-- only for debug -->
+<div *ngIf="false" class="row">
+ {{config$ | async | json}}
+</div> \ No newline at end of file
diff --git a/webapp/src/app/config/config.component.ts b/webapp/src/app/config/config.component.ts
new file mode 100644
index 0000000..681c296
--- /dev/null
+++ b/webapp/src/app/config/config.component.ts
@@ -0,0 +1,123 @@
+import { Component, OnInit } from "@angular/core";
+import { Observable } from 'rxjs/Observable';
+import { FormControl, FormGroup, Validators, FormBuilder } from '@angular/forms';
+
+// Import RxJs required methods
+import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/filter';
+import 'rxjs/add/operator/debounceTime';
+
+import { ConfigService, IConfig, IProject, ProjectType } from "../common/config.service";
+import { XDSServerService, IServerStatus } from "../common/xdsserver.service";
+import { SyncthingService, ISyncThingStatus } from "../common/syncthing.service";
+import { AlertService } from "../common/alert.service";
+
+@Component({
+ templateUrl: './app/config/config.component.html',
+ styleUrls: ['./app/config/config.component.css']
+})
+
+// Inspired from https://embed.plnkr.co/jgDTXknPzAaqcg9XA9zq/
+// and from http://plnkr.co/edit/vCdjZM?p=preview
+
+export class ConfigComponent implements OnInit {
+
+ config$: Observable<IConfig>;
+ severStatus$: Observable<IServerStatus>;
+ localSTStatus$: Observable<ISyncThingStatus>;
+
+ curProj: number;
+ userEditedLabel: boolean = false;
+
+ // TODO replace by reactive FormControl + add validation
+ syncToolUrl: string;
+ syncToolRetry: string;
+ projectsRootDir: string;
+ showApplyBtn = { // Used to show/hide Apply buttons
+ "retry": false,
+ "rootDir": false,
+ };
+
+ addProjectForm: FormGroup;
+ pathCtrl = new FormControl("", Validators.required);
+
+
+ constructor(
+ private configSvr: ConfigService,
+ private sdkSvr: XDSServerService,
+ private stSvr: SyncthingService,
+ private alert: AlertService,
+ private fb: FormBuilder
+ ) {
+ // FIXME implement multi project support
+ this.curProj = 0;
+ this.addProjectForm = fb.group({
+ path: this.pathCtrl,
+ label: ["", Validators.nullValidator],
+ });
+ }
+
+ ngOnInit() {
+ this.config$ = this.configSvr.conf;
+ this.severStatus$ = this.sdkSvr.Status$;
+ this.localSTStatus$ = this.stSvr.Status$;
+
+ // Bind syncToolUrl to baseURL
+ this.config$.subscribe(cfg => {
+ this.syncToolUrl = cfg.localSThg.URL;
+ this.syncToolRetry = String(cfg.localSThg.retry);
+ this.projectsRootDir = cfg.projectsRootDir;
+ });
+
+ // Auto create label name
+ this.pathCtrl.valueChanges
+ .debounceTime(100)
+ .filter(n => n)
+ .map(n => "Project_" + n.split('/')[0])
+ .subscribe(value => {
+ if (value && !this.userEditedLabel) {
+ this.addProjectForm.patchValue({ label: value });
+ }
+ });
+ }
+
+ onKeyLabel(event: any) {
+ this.userEditedLabel = (this.addProjectForm.value.label !== "");
+ }
+
+ submitGlobConf(field: string) {
+ switch (field) {
+ case "retry":
+ let re = new RegExp('^[0-9]+$');
+ let rr = parseInt(this.syncToolRetry, 10);
+ if (re.test(this.syncToolRetry) && rr >= 0) {
+ this.configSvr.syncToolRetry = rr;
+ } else {
+ this.alert.warning("Not a valid number", true);
+ }
+ break;
+ case "rootDir":
+ this.configSvr.projectsRootDir = this.projectsRootDir;
+ break;
+ default:
+ return;
+ }
+ this.showApplyBtn[field] = false;
+ }
+
+ syncToolRestartConn() {
+ this.configSvr.syncToolURL = this.syncToolUrl;
+ this.configSvr.loadProjects();
+ }
+
+ onSubmit() {
+ let formVal = this.addProjectForm.value;
+
+ this.configSvr.addProject({
+ label: formVal['label'],
+ path: formVal['path'],
+ type: ProjectType.SYNCTHING,
+ });
+ }
+
+} \ No newline at end of file
diff --git a/webapp/src/app/home/home.component.ts b/webapp/src/app/home/home.component.ts
new file mode 100644
index 0000000..1df277f
--- /dev/null
+++ b/webapp/src/app/home/home.component.ts
@@ -0,0 +1,62 @@
+import { Component, OnInit } from '@angular/core';
+
+export interface ISlide {
+ img?: string;
+ imgAlt?: string;
+ hText?: string;
+ pText?: string;
+ btn?: string;
+ btnHref?: string;
+}
+
+@Component({
+ selector: 'home',
+ moduleId: module.id,
+ template: `
+ <style>
+ .wide img {
+ width: 98%;
+ }
+ h1, h2, h3, h4, p {
+ color: #330066;
+ }
+
+ </style>
+ <div class="wide">
+ <carousel [interval]="carInterval" [(activeSlide)]="activeSlideIndex">
+ <slide *ngFor="let sl of slides; let index=index">
+ <img [src]="sl.img" [alt]="sl.imgAlt">
+ <div class="carousel-caption" *ngIf="sl.hText">
+ <h2>{{ sl.hText }}</h2>
+ <p>{{ sl.pText }}</p>
+ </div>
+ </slide>
+ </carousel>
+ </div>
+ `
+})
+
+export class HomeComponent {
+
+ public carInterval: number = 2000;
+
+ // FIXME SEB - Add more slides and info
+ public slides: ISlide[] = [
+ {
+ img: 'assets/images/iot-graphx.jpg',
+ imgAlt: "iot graphx image",
+ hText: "Welcome to XDS Dashboard !",
+ pText: "X(cross) Development System allows developers to easily cross-compile applications.",
+ },
+ {
+ //img: 'assets/images/beige.jpg',
+ //imgAlt: "beige image",
+ img: 'assets/images/iot-graphx.jpg',
+ imgAlt: "iot graphx image",
+ hText: "Create, Build, Deploy, Enjoy !",
+ pText: "TODO...",
+ }
+ ];
+
+ constructor() { }
+} \ No newline at end of file
diff --git a/webapp/src/app/main.ts b/webapp/src/app/main.ts
new file mode 100644
index 0000000..1f68ccc
--- /dev/null
+++ b/webapp/src/app/main.ts
@@ -0,0 +1,6 @@
+import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
+import {AppModule} from './app.module';
+
+const platform = platformBrowserDynamic();
+
+platform.bootstrapModule(AppModule); \ No newline at end of file
diff --git a/webapp/src/app/projects/projectCard.component.ts b/webapp/src/app/projects/projectCard.component.ts
new file mode 100644
index 0000000..010b476
--- /dev/null
+++ b/webapp/src/app/projects/projectCard.component.ts
@@ -0,0 +1,63 @@
+import { Component, Input, Pipe, PipeTransform } from '@angular/core';
+import { ConfigService, IProject, ProjectType } from "../common/config.service";
+
+@Component({
+ selector: 'project-card',
+ template: `
+ <div class="row">
+ <div class="col-xs-12">
+ <div class="text-right" role="group">
+ <button class="btn btn-link" (click)="delete(project)"><span class="fa fa-trash fa-size-x2"></span></button>
+ </div>
+ </div>
+ </div>
+
+ <table class="table table-striped">
+ <tbody>
+ <tr>
+ <th><span class="fa fa-fw fa-id-badge"></span>&nbsp;<span>Project ID</span></th>
+ <td>{{ project.id }}</td>
+ </tr>
+ <tr>
+ <th><span class="fa fa-fw fa-folder-open-o"></span>&nbsp;<span>Folder path</span></th>
+ <td>{{ project.path}}</td>
+ </tr>
+ <tr>
+ <th><span class="fa fa-fw fa-exchange"></span>&nbsp;<span>Synchronization type</span></th>
+ <td>{{ project.type | readableType }}</td>
+ </tr>
+
+ </tbody>
+ </table >
+ `,
+ styleUrls: ['./app/config/config.component.css']
+})
+
+export class ProjectCardComponent {
+
+ @Input() project: IProject;
+
+ constructor(private configSvr: ConfigService) {
+ }
+
+
+ delete(prj: IProject) {
+ this.configSvr.deleteProject(prj);
+ }
+
+}
+
+// Remove APPS. prefix if translate has failed
+@Pipe({
+ name: 'readableType'
+})
+
+export class ProjectReadableTypePipe implements PipeTransform {
+ transform(type: ProjectType): string {
+ switch (+type) {
+ case ProjectType.NATIVE: return "Native";
+ case ProjectType.SYNCTHING: return "Cloud (Syncthing)";
+ default: return String(type);
+ }
+ }
+} \ No newline at end of file
diff --git a/webapp/src/app/projects/projectsListAccordion.component.ts b/webapp/src/app/projects/projectsListAccordion.component.ts
new file mode 100644
index 0000000..bea3f0f
--- /dev/null
+++ b/webapp/src/app/projects/projectsListAccordion.component.ts
@@ -0,0 +1,26 @@
+import { Component, Input } from "@angular/core";
+
+import { IProject } from "../common/config.service";
+
+@Component({
+ selector: 'projects-list-accordion',
+ template: `
+ <accordion>
+ <accordion-group #group *ngFor="let prj of projects">
+ <div accordion-heading>
+ {{ prj.label }}
+ <i class="pull-right float-xs-right fa"
+ [ngClass]="{'fa-chevron-down': group.isOpen, 'fa-chevron-right': !group.isOpen}"></i>
+ </div>
+ <project-card [project]="prj"></project-card>
+ </accordion-group>
+ </accordion>
+ `
+})
+export class ProjectsListAccordionComponent {
+
+ @Input() projects: IProject[];
+
+}
+
+
diff --git a/webapp/src/index.html b/webapp/src/index.html
new file mode 100644
index 0000000..33e5efd
--- /dev/null
+++ b/webapp/src/index.html
@@ -0,0 +1,49 @@
+<html>
+
+<head>
+ <title>
+ XDS Dashboard
+ </title>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+
+ <link rel="icon" type="image/x-icon" href="assets/favicon.ico">
+
+ <!-- TODO cleanup
+ <link rel="stylesheet" href="lib/foundation-sites/dist/css/foundation.min.css">
+ -->
+ <link <link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
+
+ <link rel="stylesheet" href="lib/font-awesome/css/font-awesome.min.css">
+ <link rel="stylesheet" href="lib/font-awesome-animation/dist/font-awesome-animation.min.css">
+
+ <!-- 1. Load libraries -->
+ <!-- Polyfill(s) for older browsers -->
+ <script src="lib/core-js/client/shim.min.js"></script>
+
+ <script src="lib/zone.js/dist/zone.js"></script>
+ <script src="lib/reflect-metadata/Reflect.js"></script>
+ <script src="lib/systemjs/dist/system.src.js"></script>
+
+ <!-- 2. Configure SystemJS -->
+ <script src="systemjs.config.js"></script>
+ <script>
+ System.import('app')
+ .then(null, console.error.bind(console));
+ </script>
+
+ <script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
+
+</head>
+
+<!-- 3. Display the application -->
+
+<body style="padding-top: 70px;"> <!-- padding needed due to fixed navbar -->
+ <app>
+ <div style="text-align:center; position:absolute; top:50%; width:100%; transform:translate(0,-50%);">
+ Loading...
+ <i class="fa fa-spinner fa-spin fa-fw"></i>
+ </div>
+ </app>
+</body>
+
+</html> \ No newline at end of file
diff --git a/webapp/src/systemjs.config.js b/webapp/src/systemjs.config.js
new file mode 100644
index 0000000..e6139b0
--- /dev/null
+++ b/webapp/src/systemjs.config.js
@@ -0,0 +1,55 @@
+(function (global) {
+ System.config({
+ paths: {
+ // paths serve as alias
+ 'npm:': 'lib/'
+ },
+ // map tells the System loader where to look for things
+ map: {
+ // our app is within the app folder
+ app: 'app',
+ // angular bundles
+ '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
+ '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
+ '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
+ '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
+ '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
+ '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
+ '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
+ '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
+ 'ngx-cookie': 'npm:ngx-cookie/bundles/ngx-cookie.umd.js',
+ // ng2-bootstrap
+ 'moment': 'npm:moment',
+ 'ngx-bootstrap/alert': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ 'ngx-bootstrap/modal': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ 'ngx-bootstrap/accordion': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ 'ngx-bootstrap/carousel': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ 'ngx-bootstrap/dropdown': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ // other libraries
+ 'rxjs': 'npm:rxjs',
+ 'socket.io-client': 'npm:socket.io-client/dist/socket.io.min.js'
+ },
+ // packages tells the System loader how to load when no filename and/or no extension
+ packages: {
+ app: {
+ main: './main.js',
+ defaultExtension: 'js'
+ },
+ rxjs: {
+ defaultExtension: 'js'
+ },
+ "socket.io-client": {
+ defaultExtension: 'js'
+ },
+ 'ngx-bootstrap': {
+ format: 'cjs',
+ main: 'bundles/ng2-bootstrap.umd.js',
+ defaultExtension: 'js'
+ },
+ 'moment': {
+ main: 'moment.js',
+ defaultExtension: 'js'
+ }
+ }
+ });
+})(this); \ No newline at end of file
diff --git a/webapp/tsconfig.json b/webapp/tsconfig.json
new file mode 100644
index 0000000..4c37259
--- /dev/null
+++ b/webapp/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "outDir": "dist/app",
+ "target": "es5",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "removeComments": false,
+ "noImplicitAny": false
+ },
+ "exclude": [
+ "gulpfile.ts",
+ "node_modules"
+ ]
+} \ No newline at end of file
diff --git a/webapp/tslint.json b/webapp/tslint.json
new file mode 100644
index 0000000..15969a4
--- /dev/null
+++ b/webapp/tslint.json
@@ -0,0 +1,55 @@
+{
+ "rules": {
+ "class-name": true,
+ "curly": true,
+ "eofline": false,
+ "forin": true,
+ "indent": [
+ true,
+ 4
+ ],
+ "label-position": true,
+ "max-line-length": [
+ true,
+ 140
+ ],
+ "no-arg": true,
+ "no-bitwise": true,
+ "no-console": [
+ true,
+ "info",
+ "time",
+ "timeEnd",
+ "trace"
+ ],
+ "no-construct": true,
+ "no-debugger": true,
+ "no-duplicate-variable": true,
+ "no-empty": false,
+ "no-eval": true,
+ "no-string-literal": false,
+ "no-trailing-whitespace": true,
+ "no-use-before-declare": true,
+ "one-line": [
+ true,
+ "check-open-brace",
+ "check-catch",
+ "check-else",
+ "check-whitespace"
+ ],
+ "radix": true,
+ "semicolon": true,
+ "triple-equals": [
+ true,
+ "allow-null-check"
+ ],
+ "variable-name": false,
+ "whitespace": [
+ true,
+ "check-branch",
+ "check-decl",
+ "check-operator",
+ "check-separator"
+ ]
+ }
+}
diff --git a/webapp/tslint.prod.json b/webapp/tslint.prod.json
new file mode 100644
index 0000000..aa64c7f
--- /dev/null
+++ b/webapp/tslint.prod.json
@@ -0,0 +1,56 @@
+{
+ "rules": {
+ "class-name": true,
+ "curly": true,
+ "eofline": false,
+ "forin": true,
+ "indent": [
+ true,
+ 4
+ ],
+ "label-position": true,
+ "max-line-length": [
+ true,
+ 140
+ ],
+ "no-arg": true,
+ "no-bitwise": true,
+ "no-console": [
+ true,
+ "debug",
+ "info",
+ "time",
+ "timeEnd",
+ "trace"
+ ],
+ "no-construct": true,
+ "no-debugger": true,
+ "no-duplicate-variable": true,
+ "no-empty": false,
+ "no-eval": true,
+ "no-string-literal": false,
+ "no-trailing-whitespace": true,
+ "no-use-before-declare": true,
+ "one-line": [
+ true,
+ "check-open-brace",
+ "check-catch",
+ "check-else",
+ "check-whitespace"
+ ],
+ "radix": true,
+ "semicolon": true,
+ "triple-equals": [
+ true,
+ "allow-null-check"
+ ],
+ "variable-name": false,
+ "whitespace": [
+ true,
+ "check-branch",
+ "check-decl",
+ "check-operator",
+ "check-separator"
+ ]
+ }
+}
diff --git a/webapp/typings.json b/webapp/typings.json
new file mode 100644
index 0000000..23c6a41
--- /dev/null
+++ b/webapp/typings.json
@@ -0,0 +1,11 @@
+{
+ "dependencies": {},
+ "devDependencies": {},
+ "globalDependencies": {
+ "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654",
+ "socket.io-client": "registry:dt/socket.io-client#1.4.4+20160317120654"
+ },
+ "globalDevDependencies": {
+ "jasmine": "registry:dt/jasmine#2.2.0+20160505161446"
+ }
+}