summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
authorSebastien Douheret <sebastien.douheret@iot.bzh>2017-09-25 14:15:16 +0200
committerSebastien Douheret <sebastien.douheret@iot.bzh>2017-10-06 18:25:04 +0200
commit97ca1f277dc8b6973d6fa67add5593a9c395ce60 (patch)
tree761649d7771e8699a67567476c17fb2fa0e28e57 /webapp
parent12a20d0905b0d3e7e0f4c9ec8ee619f683256d71 (diff)
Added webapp Dashboard + logic to interact with server.
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
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-bzh-logo-small.pngbin0 -> 14449 bytes
-rw-r--r--webapp/assets/images/iot-graphx.jpgbin0 -> 138350 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.json63
-rw-r--r--webapp/src/app/alert/alert.component.ts30
-rw-r--r--webapp/src/app/app.component.css31
-rw-r--r--webapp/src/app/app.component.html30
-rw-r--r--webapp/src/app/app.component.ts37
-rw-r--r--webapp/src/app/app.module.ts93
-rw-r--r--webapp/src/app/app.routing.ts19
-rw-r--r--webapp/src/app/config/config.component.css35
-rw-r--r--webapp/src/app/config/config.component.html101
-rw-r--r--webapp/src/app/config/config.component.ts108
-rw-r--r--webapp/src/app/config/downloadXdsAgent.component.ts47
-rw-r--r--webapp/src/app/devel/build/build.component.css54
-rw-r--r--webapp/src/app/devel/build/build.component.html115
-rw-r--r--webapp/src/app/devel/build/build.component.ts223
-rw-r--r--webapp/src/app/devel/devel.component.css19
-rw-r--r--webapp/src/app/devel/devel.component.html40
-rw-r--r--webapp/src/app/devel/devel.component.ts35
-rw-r--r--webapp/src/app/home/home.component.ts81
-rw-r--r--webapp/src/app/main.ts6
-rw-r--r--webapp/src/app/projects/projectAddModal.component.css24
-rw-r--r--webapp/src/app/projects/projectAddModal.component.html54
-rw-r--r--webapp/src/app/projects/projectAddModal.component.ts147
-rw-r--r--webapp/src/app/projects/projectCard.component.ts91
-rw-r--r--webapp/src/app/projects/projectsListAccordion.component.ts39
-rw-r--r--webapp/src/app/sdks/sdkAddModal.component.html23
-rw-r--r--webapp/src/app/sdks/sdkAddModal.component.ts24
-rw-r--r--webapp/src/app/sdks/sdkCard.component.ts55
-rw-r--r--webapp/src/app/sdks/sdkSelectDropdown.component.ts48
-rw-r--r--webapp/src/app/sdks/sdksListAccordion.component.ts26
-rw-r--r--webapp/src/app/services/alert.service.ts66
-rw-r--r--webapp/src/app/services/config.service.ts178
-rw-r--r--webapp/src/app/services/project.service.ts199
-rw-r--r--webapp/src/app/services/sdk.service.ts54
-rw-r--r--webapp/src/app/services/syncthing.service.ts352
-rw-r--r--webapp/src/app/services/utils.service.ts33
-rw-r--r--webapp/src/app/services/xdsagent.service.ts401
-rw-r--r--webapp/src/index.html50
-rw-r--r--webapp/src/systemjs.config.js69
-rw-r--r--webapp/tsconfig.json18
-rw-r--r--webapp/tslint.json55
-rw-r--r--webapp/tslint.prod.json56
-rw-r--r--webapp/typings.json11
49 files changed, 3451 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-bzh-logo-small.png b/webapp/assets/images/iot-bzh-logo-small.png
new file mode 100644
index 0000000..2c3b2ae
--- /dev/null
+++ b/webapp/assets/images/iot-bzh-logo-small.png
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..74c640a
--- /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..2e8fa17
--- /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-system-bundle/*.min.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-agent'
+ }
+}
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..9c22f6b
--- /dev/null
+++ b/webapp/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "xds-dashboard",
+ "version": "1.0.0",
+ "description": "X (cross) Development System dashboard",
+ "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/iotbzh/xds-agent"
+ },
+ "author": "Sebastien Douheret [IoT.bzh]",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/iotbzh/xds-agent/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",
+ "rxjs-system-bundle": "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..672d7bf
--- /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 '../services/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)">
+ <div style="text-align:center;" [innerHtml]="alert.msg"></div>
+ </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..a47ad13
--- /dev/null
+++ b/webapp/src/app/app.component.css
@@ -0,0 +1,31 @@
+.navbar {
+ background-color: whitesmoke;
+}
+
+.navbar-brand {
+ font-size: x-large;
+ font-variant: small-caps;
+ color: #5a28a1;
+}
+
+a.navbar-brand {
+ margin-top: 5px;
+}
+
+
+.navbar-nav ul li a {
+ color: #fff;
+}
+
+.menu-text {
+ color: #fff;
+}
+
+#logo-iot {
+ padding: 0 2px;
+ height: 60px;
+}
+
+li>a {
+ color:#5a28a1;
+}
diff --git a/webapp/src/app/app.component.html b/webapp/src/app/app.component.html
new file mode 100644
index 0000000..a889b12
--- /dev/null
+++ b/webapp/src/app/app.component.html
@@ -0,0 +1,30 @@
+<nav class="navbar navbar-fixed-top">
+ <!-- navbar-inverse"> -->
+ <div class="container-fluid">
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#myNavbar"
+ [attr.aria-expanded]="!isCollapsed" (click)="isCollapsed = !isCollapsed;" [ngClass]="{'collapsed': isCollapsed}">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+
+ <img class="navbar-brand" id="logo-iot" src="assets/images/iot-bzh-logo-small.png">
+ <a class="navbar-brand" href="#">X(cross) Development System Dashboard</a>
+ </div>
+
+ <div class="collapse navbar-collapse" [ngClass]="{'in': !isCollapsed}" id="myNavbar">
+ <ul class="nav navbar-nav navbar-right">
+ <li><a routerLink="/config"><i class="fa fa-2x fa-cog" title="Open configuration page" (click)="isCollapsed=true;"></i></a></li>
+ <li><a routerLink="/devel"><i class="fa fa-2x fa-play-circle" title="Open build page" (click)="isCollapsed=true;"></i></a></li>
+ <li><a routerLink="/home"><i class="fa fa-2x fa-home" title="Back to home page" (click)="isCollapsed=true;"></i></a></li>
+ </ul>
+ </div>
+ </div>
+</nav>
+
+<app-alert id="alert"></app-alert>
+
+<div style="margin:10px;">
+ <router-outlet></router-outlet>
+</div>
diff --git a/webapp/src/app/app.component.ts b/webapp/src/app/app.component.ts
new file mode 100644
index 0000000..40cfb24
--- /dev/null
+++ b/webapp/src/app/app.component.ts
@@ -0,0 +1,37 @@
+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 {
+
+ isCollapsed: boolean = true;
+
+ 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..c3fd586
--- /dev/null
+++ b/webapp/src/app/app.module.ts
@@ -0,0 +1,93 @@
+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 { PopoverModule } from 'ngx-bootstrap/popover';
+import { CollapseModule } from 'ngx-bootstrap/collapse';
+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 { DlXdsAgentComponent, CapitalizePipe } from "./config/downloadXdsAgent.component";
+import { ProjectCardComponent } from "./projects/projectCard.component";
+import { ProjectReadableTypePipe } from "./projects/projectCard.component";
+import { ProjectsListAccordionComponent } from "./projects/projectsListAccordion.component";
+import { ProjectAddModalComponent} from "./projects/projectAddModal.component";
+import { SdkCardComponent } from "./sdks/sdkCard.component";
+import { SdksListAccordionComponent } from "./sdks/sdksListAccordion.component";
+import { SdkSelectDropdownComponent } from "./sdks/sdkSelectDropdown.component";
+import { SdkAddModalComponent} from "./sdks/sdkAddModal.component";
+
+import { HomeComponent } from "./home/home.component";
+import { DevelComponent } from "./devel/devel.component";
+import { BuildComponent } from "./devel/build/build.component";
+import { XDSAgentService } from "./services/xdsagent.service";
+import { ConfigService } from "./services/config.service";
+import { ProjectService } from "./services/project.service";
+import { AlertService } from './services/alert.service';
+import { UtilsService } from './services/utils.service';
+import { SdkService } from "./services/sdk.service";
+
+
+
+@NgModule({
+ imports: [
+ BrowserModule,
+ HttpModule,
+ FormsModule,
+ ReactiveFormsModule,
+ Routing,
+ CookieModule.forRoot(),
+ AlertModule.forRoot(),
+ ModalModule.forRoot(),
+ AccordionModule.forRoot(),
+ CarouselModule.forRoot(),
+ PopoverModule.forRoot(),
+ CollapseModule.forRoot(),
+ BsDropdownModule.forRoot(),
+ ],
+ declarations: [
+ AppComponent,
+ AlertComponent,
+ HomeComponent,
+ BuildComponent,
+ DevelComponent,
+ ConfigComponent,
+ DlXdsAgentComponent,
+ CapitalizePipe,
+ ProjectCardComponent,
+ ProjectReadableTypePipe,
+ ProjectsListAccordionComponent,
+ ProjectAddModalComponent,
+ SdkCardComponent,
+ SdksListAccordionComponent,
+ SdkSelectDropdownComponent,
+ SdkAddModalComponent,
+ ],
+ providers: [
+ AppRoutingProviders,
+ {
+ provide: Window,
+ useValue: window
+ },
+ XDSAgentService,
+ ConfigService,
+ ProjectService,
+ AlertService,
+ UtilsService,
+ SdkService,
+ ],
+ bootstrap: [AppComponent]
+})
+export class AppModule {
+}
diff --git a/webapp/src/app/app.routing.ts b/webapp/src/app/app.routing.ts
new file mode 100644
index 0000000..f0d808f
--- /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 {DevelComponent} from "./devel/devel.component";
+
+
+const appRoutes: Routes = [
+ {path: '', redirectTo: 'home', pathMatch: 'full'},
+
+ {path: 'config', component: ConfigComponent, data: {title: 'Config'}},
+ {path: 'home', component: HomeComponent, data: {title: 'Home'}},
+ {path: 'devel', component: DevelComponent, data: {title: 'Build & Deploy'}}
+];
+
+export const AppRoutingProviders: any[] = [];
+export const Routing: ModuleWithProviders = RouterModule.forRoot(appRoutes, {
+ useHash: true
+});
diff --git a/webapp/src/app/config/config.component.css b/webapp/src/app/config/config.component.css
new file mode 100644
index 0000000..6412f9a
--- /dev/null
+++ b/webapp/src/app/config/config.component.css
@@ -0,0 +1,35 @@
+.fa-big {
+ font-size: 20px;
+ font-weight: bold;
+}
+
+.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;
+}
+
+.panel-heading {
+ background: aliceblue;
+}
diff --git a/webapp/src/app/config/config.component.html b/webapp/src/app/config/config.component.html
new file mode 100644
index 0000000..4dbd238
--- /dev/null
+++ b/webapp/src/app/config/config.component.html
@@ -0,0 +1,101 @@
+<div class="panel panel-default">
+ <div class="panel-heading">
+ <h2 class="panel-title" (click)="gConfigIsCollapsed = !gConfigIsCollapsed">
+ Global Configuration
+ <div class="pull-right">
+ <span class="fa fa-fw fa-exchange fa-size-x2" [style.color]="((agentStatus$ | async)?.WS_connected)?'green':'red'"></span>
+
+ <button class="btn btn-link" (click)="gConfigIsCollapsed = !gConfigIsCollapsed; $event.stopPropagation()">
+ <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': gConfigIsCollapsed, 'fa-angle-double-right': !gConfigIsCollapsed}"></span>
+ </button>
+ </div>
+ </h2>
+ </div>
+ <div class="panel-body" [collapse]="gConfigIsCollapsed && xdsServerConnected">
+ <div class="row">
+ <div class="col-xs-12">
+ <table class="table table-condensed">
+ <tbody>
+ <tr [ngClass]="{'info': xdsServerConnected, 'danger': !xdsServerConnected}">
+ <th><label>XDS Server URL</label></th>
+ <td> <input type="text" [(ngModel)]="xdsServerUrl"></td>
+ <td style="white-space: nowrap">
+ <div class="btn-group">
+ <button class="btn btn-link" (click)="xdsAgentRestartConn()"><span class="fa fa-refresh fa-size-x2"></span></button>
+ <dl-xds-agent class="button"></dl-xds-agent>
+ </div>
+ </td>
+ </tr>
+ <tr class="info">
+ <th><label>XDS Server connection retry</label></th>
+ <td> <input type="text" [(ngModel)]="xdsServerRetry" (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" (click)="sdksIsCollapsed = !sdksIsCollapsed">
+ Cross SDKs
+ <div class="pull-right">
+ <button class="btn btn-link" (click)="childSdkModal.show(); $event.stopPropagation()"><span class="fa fa-plus fa-size-x2"></span></button>
+
+ <button class="btn btn-link" (click)="sdksIsCollapsed = !sdksIsCollapsed; $event.stopPropagation()">
+ <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': sdksIsCollapsed, 'fa-angle-double-right': !sdksIsCollapsed}"></span>
+ </button>
+ </div>
+ </h2>
+ </div>
+ <div class="panel-body" [collapse]="sdksIsCollapsed">
+ <div class="row col-xs-12">
+ <sdks-list-accordion [sdks]="(sdks$ | async)"></sdks-list-accordion>
+ </div>
+ </div>
+</div>
+
+<div class="panel panel-default">
+ <div class="panel-heading">
+ <h2 class="panel-title" (click)="projectsIsCollapsed = !projectsIsCollapsed; $event.stopPropagation()">
+ Projects
+ <div class="pull-right">
+ <button class="btn btn-link" (click)="childProjectModal.show(); $event.stopPropagation()"><span class="fa fa-plus fa-size-x2"></span></button>
+
+ <button class="btn btn-link" (click)="projectsIsCollapsed = !projectsIsCollapsed; $event.stopPropagation()">
+ <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': projectsIsCollapsed, 'fa-angle-double-right': !projectsIsCollapsed}"></span>
+ </button>
+ </div>
+ </h2>
+ </div>
+ <div class="panel-body" [collapse]="projectsIsCollapsed">
+ <div class="row col-xs-12">
+ <projects-list-accordion [projects]="(projects$ | async)"></projects-list-accordion>
+ </div>
+ </div>
+</div>
+
+<!-- Modals -->
+<project-add-modal #childProjectModal [title]="'Add a new project'" [server-id]=curServerID>
+</project-add-modal>
+<sdk-add-modal #childSdkModal [title]="'Add a new SDK'">
+</sdk-add-modal>
+
+<!-- only for debug -->
+<div *ngIf="false" class="row">
+ <pre>Config: {{config$ | async | json}}</pre>
+ <br>
+ <pre>Projects: {{projects$ | async | json}} </pre>
+</div>
diff --git a/webapp/src/app/config/config.component.ts b/webapp/src/app/config/config.component.ts
new file mode 100644
index 0000000..101596f
--- /dev/null
+++ b/webapp/src/app/config/config.component.ts
@@ -0,0 +1,108 @@
+import { Component, ViewChild, OnInit } from "@angular/core";
+import { Observable } from 'rxjs/Observable';
+import { FormControl, FormGroup, Validators, FormBuilder } from '@angular/forms';
+import { CollapseModule } from 'ngx-bootstrap/collapse';
+
+import { ConfigService, IConfig } from "../services/config.service";
+import { ProjectService, IProject } from "../services/project.service";
+import { XDSAgentService, IAgentStatus, IXDSConfig } from "../services/xdsagent.service";
+import { AlertService } from "../services/alert.service";
+import { ProjectAddModalComponent } from "../projects/projectAddModal.component";
+import { SdkService, ISdk } from "../services/sdk.service";
+import { SdkAddModalComponent } from "../sdks/sdkAddModal.component";
+
+@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 {
+ @ViewChild('childProjectModal') childProjectModal: ProjectAddModalComponent;
+ @ViewChild('childSdkModal') childSdkModal: SdkAddModalComponent;
+
+ config$: Observable<IConfig>;
+ projects$: Observable<IProject[]>;
+ sdks$: Observable<ISdk[]>;
+ agentStatus$: Observable<IAgentStatus>;
+
+ curProj: number;
+ curServer: number;
+ curServerID: string;
+ userEditedLabel: boolean = false;
+
+ gConfigIsCollapsed: boolean = true;
+ sdksIsCollapsed: boolean = true;
+ projectsIsCollapsed: boolean = false;
+
+ // TODO replace by reactive FormControl + add validation
+ xdsServerConnected: boolean = false;
+ xdsServerUrl: string;
+ xdsServerRetry: string;
+ projectsRootDir: string; // FIXME: should be remove when projectAddModal will always return full path
+ showApplyBtn = { // Used to show/hide Apply buttons
+ "retry": false,
+ "rootDir": false,
+ };
+
+ constructor(
+ private configSvr: ConfigService,
+ private projectSvr: ProjectService,
+ private xdsAgentSvr: XDSAgentService,
+ private sdkSvr: SdkService,
+ private alert: AlertService,
+ ) {
+ }
+
+ ngOnInit() {
+ this.config$ = this.configSvr.Conf$;
+ this.projects$ = this.projectSvr.Projects$;
+ this.sdks$ = this.sdkSvr.Sdks$;
+ this.agentStatus$ = this.xdsAgentSvr.Status$;
+
+ // FIXME support multiple servers
+ this.curServer = 0;
+
+ // Bind xdsServerUrl to baseURL
+ this.xdsAgentSvr.XdsConfig$.subscribe(cfg => {
+ if (!cfg || cfg.servers.length < 1) {
+ return;
+ }
+ let svr = cfg.servers[this.curServer];
+ this.curServerID = svr.id;
+ this.xdsServerConnected = svr.connected;
+ this.xdsServerUrl = svr.url;
+ this.xdsServerRetry = String(svr.connRetry);
+ this.projectsRootDir = ''; // SEB FIXME: add in go config? cfg.projectsRootDir;
+ });
+ }
+
+ submitGlobConf(field: string) {
+ switch (field) {
+ case "retry":
+ let re = new RegExp('^[0-9]+$');
+ let rr = parseInt(this.xdsServerRetry, 10);
+ if (re.test(this.xdsServerRetry) && rr >= 0) {
+ this.xdsAgentSvr.setServerRetry(this.curServerID, 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;
+ }
+
+ xdsAgentRestartConn() {
+ let url = this.xdsServerUrl;
+ this.xdsAgentSvr.setServerUrl(this.curServerID, url);
+ this.configSvr.loadProjects();
+ }
+
+}
diff --git a/webapp/src/app/config/downloadXdsAgent.component.ts b/webapp/src/app/config/downloadXdsAgent.component.ts
new file mode 100644
index 0000000..0b63e50
--- /dev/null
+++ b/webapp/src/app/config/downloadXdsAgent.component.ts
@@ -0,0 +1,47 @@
+import { Component, Input, Pipe, PipeTransform } from '@angular/core';
+
+@Component({
+ selector: 'dl-xds-agent',
+ template: `
+ <template #popTemplate>
+ <h3>Install xds-agent:</h3>
+ <ul>
+ <li>On Linux machine <a href="{{url_OS_Linux}}" target="_blank">
+ <span class="fa fa-external-link"></span></a></li>
+
+ <li>On Windows machine <a href="{{url_OS_Other}}" target="_blank"><span class="fa fa-external-link"></span></a></li>
+
+ <li>On MacOS machine <a href="{{url_OS_Other}}" target="_blank"><span class="fa fa-external-link"></span></a></li>
+ </ul>
+ <button type="button" class="btn btn-sm" (click)="pop.hide()"> Cancel </button>
+ </template>
+ <button type="button" class="btn btn-link fa fa-download fa-size-x2"
+ [popover]="popTemplate"
+ #pop="bs-popover"
+ placement="left">
+ </button>
+ `,
+ styles: [`
+ .fa-size-x2 {
+ font-size: 20px;
+ }
+ `]
+})
+
+export class DlXdsAgentComponent {
+
+ public url_OS_Linux = "https://en.opensuse.org/LinuxAutomotive#Installation_AGL_XDS";
+ public url_OS_Other = "https://github.com/iotbzh/xds-agent#how-to-install-on-other-platform";
+}
+
+@Pipe({
+ name: 'capitalize'
+})
+export class CapitalizePipe implements PipeTransform {
+ transform(value: string): string {
+ if (value) {
+ return value.charAt(0).toUpperCase() + value.slice(1);
+ }
+ return value;
+ }
+}
diff --git a/webapp/src/app/devel/build/build.component.css b/webapp/src/app/devel/build/build.component.css
new file mode 100644
index 0000000..695a89b
--- /dev/null
+++ b/webapp/src/app/devel/build/build.component.css
@@ -0,0 +1,54 @@
+.vcenter {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.blocks .btn-primary {
+ margin-left: 5px;
+ margin-right: 5px;
+ margin-top: 5px;
+ border-radius: 4px !important;
+}
+
+.table-center {
+ width: 80%;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.table-borderless>tbody>tr>td,
+.table-borderless>tbody>tr>th,
+.table-borderless>tfoot>tr>td,
+.table-borderless>tfoot>tr>th,
+.table-borderless>thead>tr>td,
+.table-borderless>thead>tr>th {
+ border: none;
+}
+
+.table-in-accordion>tbody>tr>th {
+ width: 30%
+}
+
+.btn-large {
+ width: 10em;
+}
+
+.fa-big {
+ font-size: 18px;
+ font-weight: bold;
+}
+
+.textarea-scroll {
+ width: 100%;
+ overflow-y: scroll;
+}
+
+h2 {
+ font-family: sans-serif;
+ font-variant: small-caps;
+ font-size: x-large;
+}
+
+.panel-heading {
+ background: aliceblue;
+}
diff --git a/webapp/src/app/devel/build/build.component.html b/webapp/src/app/devel/build/build.component.html
new file mode 100644
index 0000000..2bcd2c7
--- /dev/null
+++ b/webapp/src/app/devel/build/build.component.html
@@ -0,0 +1,115 @@
+<div class="panel panel-default">
+ <div class="panel-heading">
+ <h2 class="panel-title" (click)="buildIsCollapsed = !buildIsCollapsed">
+ Build
+ <div class="pull-right">
+ <button class="btn btn-link" (click)="buildIsCollapsed = !buildIsCollapsed; $event.stopPropagation()">
+ <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': buildIsCollapsed, 'fa-angle-double-right': !buildIsCollapsed}"></span>
+ </button>
+ </div>
+ </h2>
+ </div>
+ <div class="panel-body" [collapse]="buildIsCollapsed">
+ <form [formGroup]="buildForm">
+ <div class="col-xs-12">
+ <table class="table table-borderless table-center">
+ <tbody>
+ <tr>
+ <th>Cross SDK</th>
+ <td>
+ <!-- FIXME why not working ?
+ <sdk-select-dropdown [sdks]="(sdks$ | async)"></sdk-select-dropdown>
+ -->
+ <sdk-select-dropdown></sdk-select-dropdown>
+ </td>
+ </tr>
+ <tr>
+ <th>Project root path</th>
+ <td> <input type="text" disabled style="width:99%;" [value]="curProject && curProject.pathClient"></td>
+ </tr>
+ <tr>
+ <th>Sub-path</th>
+ <td> <input type="text" style="width:99%;" formControlName="subpath"> </td>
+ </tr>
+ <tr>
+ <td colspan="2">
+ <accordion>
+ <accordion-group #group>
+ <div accordion-heading>
+ Advanced Settings
+ <i class="pull-right float-xs-right fa" [ngClass]="{'fa-chevron-down': group.isOpen, 'fa-chevron-right': !group.isOpen}"></i>
+ </div>
+
+ <table class="table table-borderless table-in-accordion">
+ <tbody>
+ <tr>
+ <th>Clean Command</th>
+ <td> <input type="text" style="width:99%;" formControlName="cmdClean"> </td>
+ </tr>
+ <tr>
+ <th>Pre-Build Command</th>
+ <td> <input type="text" style="width:99%;" formControlName="cmdPrebuild"> </td>
+ </tr>
+ <tr>
+ <th>Build Command</th>
+ <td> <input type="text" style="width:99%;" formControlName="cmdBuild"> </td>
+ </tr>
+ <tr>
+ <th>Populate Command</th>
+ <td> <input type="text" style="width:99%;" formControlName="cmdPopulate"> </td>
+ </tr>
+ <tr>
+ <th>Env variables</th>
+ <td> <input type="text" style="width:99%;" formControlName="envVars"> </td>
+ </tr>
+ <tr *ngIf="debugEnable">
+ <th>Args variables</th>
+ <td> <input type="text" style="width:99%;" formControlName="cmdArgs"> </td>
+ </tr>
+ </tbody>
+ </table>
+ </accordion-group>
+ </accordion>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="row">
+ <div class="col-xs-12 text-center">
+ <div class="btn-group blocks">
+ <button class="btn btn-primary btn-large" (click)="clean()" [disabled]="!curProject ">Clean</button>
+ <button class="btn btn-primary btn-large" (click)="preBuild()" [disabled]="!curProject">Pre-Build</button>
+ <button class="btn btn-primary btn-large" (click)="build()" [disabled]="!curProject">Build</button>
+ <button class="btn btn-primary btn-large" (click)="populate()" [disabled]="!curProject ">Populate</button>
+ <button *ngIf="debugEnable" class="btn btn-primary btn-large" (click)="execCmd()" [disabled]="!curProject ">Execute command</button>
+ <button *ngIf="debugEnable" class="btn btn-primary btn-large" (click)="make()" [disabled]="!curProject ">Make</button>
+ </div>
+ </div>
+ </div>
+ </form>
+
+ <div style="margin-left: 2em; margin-right: 2em; ">
+ <div class="row ">
+ <div class="col-xs-10">
+ <div class="row ">
+ <div class="col-xs-4">
+ <label>Command Output</label>
+ </div>
+ <div class="col-xs-8" style="font-size:x-small; margin-top:5px;">
+ {{ cmdInfo }}
+ </div>
+ </div>
+ </div>
+ <div class="col-xs-2">
+ <button class="btn btn-link pull-right " (click)="reset() "><span class="fa fa-eraser fa-size-x2"></span></button>
+ </div>
+ </div>
+ <div class="row ">
+ <div class="col-xs-12 text-center ">
+ <textarea rows="20" class="textarea-scroll" #scrollOutput>{{ cmdOutput }}</textarea>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/webapp/src/app/devel/build/build.component.ts b/webapp/src/app/devel/build/build.component.ts
new file mode 100644
index 0000000..87df4e1
--- /dev/null
+++ b/webapp/src/app/devel/build/build.component.ts
@@ -0,0 +1,223 @@
+import { Component, AfterViewChecked, ElementRef, ViewChild, OnInit, Input } from '@angular/core';
+import { Observable } from 'rxjs';
+import { FormControl, FormGroup, Validators, FormBuilder } from '@angular/forms';
+import { CookieService } from 'ngx-cookie';
+
+import 'rxjs/add/operator/scan';
+import 'rxjs/add/operator/startWith';
+
+import { XDSAgentService, ICmdOutput } from "../../services/xdsagent.service";
+import { ProjectService, IProject } from "../../services/project.service";
+import { AlertService, IAlert } from "../../services/alert.service";
+import { SdkService } from "../../services/sdk.service";
+
+@Component({
+ selector: 'panel-build',
+ moduleId: module.id,
+ templateUrl: './build.component.html',
+ styleUrls: ['./build.component.css']
+})
+
+export class BuildComponent implements OnInit, AfterViewChecked {
+ @ViewChild('scrollOutput') private scrollContainer: ElementRef;
+
+ @Input() curProject: IProject;
+
+ public buildForm: FormGroup;
+ public subpathCtrl = new FormControl("", Validators.required);
+ public debugEnable: boolean = false;
+ public buildIsCollapsed: boolean = false;
+ public cmdOutput: string;
+ public cmdInfo: string;
+
+ private startTime: Map<string, number> = new Map<string, number>();
+
+ constructor(
+ private xdsSvr: XDSAgentService,
+ private fb: FormBuilder,
+ private alertSvr: AlertService,
+ private sdkSvr: SdkService,
+ private cookie: CookieService,
+ ) {
+ this.cmdOutput = "";
+ this.cmdInfo = ""; // TODO: to be remove (only for debug)
+ this.buildForm = fb.group({
+ subpath: this.subpathCtrl,
+ cmdClean: ["", Validators.nullValidator],
+ cmdPrebuild: ["", Validators.nullValidator],
+ cmdBuild: ["", Validators.nullValidator],
+ cmdPopulate: ["", Validators.nullValidator],
+ cmdArgs: ["", Validators.nullValidator],
+ envVars: ["", Validators.nullValidator],
+ });
+ }
+
+ ngOnInit() {
+ // Set default settings
+ // TODO save & restore values from cookies
+ this.buildForm.patchValue({
+ subpath: "",
+ cmdClean: "rm -rf build",
+ cmdPrebuild: "mkdir -p build && cd build && cmake ..",
+ cmdBuild: "cd build && make",
+ cmdPopulate: "cd build && make remote-target-populate",
+ cmdArgs: "",
+ envVars: "",
+ });
+
+ // Command output data tunneling
+ this.xdsSvr.CmdOutput$.subscribe(data => {
+ this.cmdOutput += data.stdout;
+ this.cmdOutput += data.stderr;
+ });
+
+ // Command exit
+ this.xdsSvr.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();
+
+ // only use for debug
+ this.debugEnable = (this.cookie.get("debug_build") === "1");
+ }
+
+ ngAfterViewChecked() {
+ this._scrollToBottom();
+ }
+
+ reset() {
+ this.cmdOutput = '';
+ }
+
+ clean() {
+ this._exec(
+ this.buildForm.value.cmdClean,
+ this.buildForm.value.subpath,
+ [],
+ this.buildForm.value.envVars);
+ }
+
+ preBuild() {
+ this._exec(
+ this.buildForm.value.cmdPrebuild,
+ this.buildForm.value.subpath,
+ [],
+ this.buildForm.value.envVars);
+ }
+
+ build() {
+ this._exec(
+ this.buildForm.value.cmdBuild,
+ this.buildForm.value.subpath,
+ [],
+ this.buildForm.value.envVars
+ );
+ }
+
+ populate() {
+ this._exec(
+ this.buildForm.value.cmdPopulate,
+ this.buildForm.value.subpath,
+ [], // args
+ this.buildForm.value.envVars
+ );
+ }
+
+ execCmd() {
+ this._exec(
+ this.buildForm.value.cmdArgs,
+ this.buildForm.value.subpath,
+ [],
+ this.buildForm.value.envVars
+ );
+ }
+
+ private _exec(cmd: string, dir: string, args: string[], env: string) {
+ if (!this.curProject) {
+ this.alertSvr.warning('No active project', true);
+ }
+
+ let prjID = this.curProject.id;
+
+ this.cmdOutput += this._outputHeader();
+
+ let sdkid = this.sdkSvr.getCurrentId();
+
+ // Detect key=value in env string to build array of string
+ let envArr = [];
+ env.split(';').forEach(v => envArr.push(v.trim()));
+
+ let t0 = performance.now();
+ this.cmdInfo = 'Start build of ' + prjID + ' at ' + t0;
+
+ this.xdsSvr.exec(prjID, dir, cmd, sdkid, args, envArr)
+ .subscribe(res => {
+ this.startTime.set(String(res.cmdID), t0);
+ },
+ err => {
+ this.cmdInfo = 'Last command duration: ' + this._computeTime(t0);
+ this.alertSvr.error('ERROR: ' + err);
+ });
+ }
+
+ make(args: string) {
+ if (!this.curProject) {
+ this.alertSvr.warning('No active project', true);
+ }
+
+ let prjID = this.curProject.id;
+
+ this.cmdOutput += this._outputHeader();
+
+ let sdkid = this.sdkSvr.getCurrentId();
+
+ let argsArr = args ? args.split(' ') : this.buildForm.value.cmdArgs.split(' ');
+
+ // Detect key=value in env string to build array of string
+ let envArr = [];
+ this.buildForm.value.envVars.split(';').forEach(v => envArr.push(v.trim()));
+
+ let t0 = performance.now();
+ this.cmdInfo = 'Start build of ' + prjID + ' at ' + t0;
+
+ this.xdsSvr.make(prjID, this.buildForm.value.subpath, sdkid, argsArr, envArr)
+ .subscribe(res => {
+ this.startTime.set(String(res.cmdID), t0);
+ },
+ err => {
+ this.cmdInfo = 'Last command duration: ' + this._computeTime(t0);
+ this.alertSvr.error('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";
+ }
+}
diff --git a/webapp/src/app/devel/devel.component.css b/webapp/src/app/devel/devel.component.css
new file mode 100644
index 0000000..4b03dcb
--- /dev/null
+++ b/webapp/src/app/devel/devel.component.css
@@ -0,0 +1,19 @@
+.table-center {
+ width: 60%;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.table-borderless>tbody>tr>td,
+.table-borderless>tbody>tr>th,
+.table-borderless>tfoot>tr>td,
+.table-borderless>tfoot>tr>th,
+.table-borderless>thead>tr>td,
+.table-borderless>thead>tr>th {
+ border: none;
+}
+
+a.dropdown-item.disabled {
+ pointer-events:none;
+ opacity:0.4;
+}
diff --git a/webapp/src/app/devel/devel.component.html b/webapp/src/app/devel/devel.component.html
new file mode 100644
index 0000000..cc62889
--- /dev/null
+++ b/webapp/src/app/devel/devel.component.html
@@ -0,0 +1,40 @@
+<div class="row">
+ <div class="col-md-8">
+ <table class="table table-borderless table-center">
+ <tbody>
+ <tr>
+ <th style="border: none;">Project</th>
+ <td>
+ <div class="btn-group" dropdown *ngIf="curPrj">
+ <button dropdownToggle type="button" class="btn btn-primary dropdown-toggle" style="width: 20em;">
+ {{curPrj.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 (Prjs$ | async)" [class.disabled]="!prj.isUsable"
+ (click)="curPrj=prj">{{prj.label}}</a>
+ </li>
+
+ </ul>
+ </div>
+ <span *ngIf="!curPrj" style="color:red; font-style: italic;">
+ No project detected, please create first a project using the configuration page.
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</div>
+
+<div class="row">
+ <!--<div class="col-md-8">-->
+ <div class="col-md-12">
+ <panel-build [curProject]=curPrj></panel-build>
+ </div>
+ <!-- TODO: disable for now
+ <div class="col-md-4">
+ <panel-deploy [curProject]=curPrj></panel-deploy>
+ </div>
+ -->
+</div>
diff --git a/webapp/src/app/devel/devel.component.ts b/webapp/src/app/devel/devel.component.ts
new file mode 100644
index 0000000..5c8b9f2
--- /dev/null
+++ b/webapp/src/app/devel/devel.component.ts
@@ -0,0 +1,35 @@
+import { Component } from '@angular/core';
+
+import { Observable } from 'rxjs';
+
+import { ProjectService, IProject } from "../services/project.service";
+
+@Component({
+ selector: 'devel',
+ moduleId: module.id,
+ templateUrl: './devel.component.html',
+ styleUrls: ['./devel.component.css'],
+})
+
+export class DevelComponent {
+
+ curPrj: IProject;
+ Prjs$: Observable<IProject[]>;
+
+ constructor(private projectSvr: ProjectService) {
+ }
+
+ ngOnInit() {
+ this.Prjs$ = this.projectSvr.Projects$;
+ this.Prjs$.subscribe((prjs) => {
+ // Select project if no one is selected or no project exists
+ if (this.curPrj && "id" in this.curPrj) {
+ this.curPrj = prjs.find(p => p.id === this.curPrj.id) || prjs[0];
+ } else if (this.curPrj == null) {
+ this.curPrj = prjs[0];
+ } else {
+ this.curPrj = null;
+ }
+ });
+ }
+}
diff --git a/webapp/src/app/home/home.component.ts b/webapp/src/app/home/home.component.ts
new file mode 100644
index 0000000..0e3c995
--- /dev/null
+++ b/webapp/src/app/home/home.component.ts
@@ -0,0 +1,81 @@
+import { Component, OnInit } from '@angular/core';
+
+export interface ISlide {
+ img?: string;
+ imgAlt?: string;
+ hText?: string;
+ hHtml?: string;
+ text?: string;
+ html?: string;
+ btn?: string;
+ btnHref?: string;
+}
+
+@Component({
+ selector: 'home',
+ moduleId: module.id,
+ template: `
+ <style>
+ .wide img {
+ width: 98%;
+ }
+ .carousel-item {
+ max-height: 90%;
+ }
+ h1, h2, h3, h4, p {
+ color: #330066;
+ }
+ .html-inner {
+ color: #330066;
+ }
+ h1 {
+ font-size: 4em;
+ }
+ p {
+ font-size: 2.5em;
+ }
+
+ </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">
+ <h1 *ngIf="sl.hText">{{ sl.hText }}</h1>
+ <h1 *ngIf="sl.hHtml" class="html-inner" [innerHtml]="sl.hHtml"></h1>
+ <p *ngIf="sl.text">{{ sl.text }}</p>
+ <div *ngIf="sl.html" class="html-inner" [innerHtml]="sl.html"></div>
+ </div>
+ </slide>
+ </carousel>
+ </div>
+ `
+})
+
+export class HomeComponent {
+
+ public carInterval: number = 4000;
+
+ // 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 !",
+ text: "X(cross) Development System allows developers to easily cross-compile applications.",
+ },
+ {
+ img: 'assets/images/iot-graphx.jpg',
+ imgAlt: "iot graphx image",
+ hText: "Create, Build, Deploy, Enjoy !",
+ },
+ {
+ img: 'assets/images/iot-graphx.jpg',
+ imgAlt: "iot graphx image",
+ hHtml: '<p>To Start: click on <i class="fa fa-cog" style="color:#9d9d9d;"></i> icon and add new folder</p>',
+ }
+ ];
+
+ 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/projectAddModal.component.css b/webapp/src/app/projects/projectAddModal.component.css
new file mode 100644
index 0000000..77f73a5
--- /dev/null
+++ b/webapp/src/app/projects/projectAddModal.component.css
@@ -0,0 +1,24 @@
+.table-borderless>tbody>tr>td,
+.table-borderless>tbody>tr>th,
+.table-borderless>tfoot>tr>td,
+.table-borderless>tfoot>tr>th,
+.table-borderless>thead>tr>td,
+.table-borderless>thead>tr>th {
+ border: none;
+}
+
+tr>th {
+ vertical-align: middle;
+}
+
+tr>td {
+ vertical-align: middle;
+}
+
+th label {
+ margin-bottom: 0;
+}
+
+td input {
+ width: 100%;
+}
diff --git a/webapp/src/app/projects/projectAddModal.component.html b/webapp/src/app/projects/projectAddModal.component.html
new file mode 100644
index 0000000..dc84985
--- /dev/null
+++ b/webapp/src/app/projects/projectAddModal.component.html
@@ -0,0 +1,54 @@
+<div bsModal #childProjectModal="bs-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel"
+ [config]="{backdrop: 'static'}" aria-hidden="true">
+ <div class="modal-dialog modal-lg">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title pull-left">{{title}}</h4>
+ <button type="button" class="close pull-right" aria-label="Close" (click)="hide()">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+
+ <form [formGroup]="addProjectForm" (ngSubmit)="onSubmit()">
+ <div class="modal-body">
+ <div class="row ">
+ <div class="col-xs-12">
+ <table class="table table-borderless">
+ <tbody>
+ <tr>
+ <th><label>Sharing Type </label></th>
+ <td><select class="form-control" formControlName="type">
+ <option *ngFor="let t of projectTypes" [value]="t.value">{{t.display}}
+ </option>
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <th><label for="select-local-path">Local Path </label></th>
+ <td><input type="text" id="select-local-path" formControlName="pathCli" placeholder="/tmp/myProject" (change)="onChangeLocalProject($event)"></td>
+ </tr>
+ <tr>
+ <th><label for="select-server-path">Server Path </label></th>
+ <td><input type="text" id="select-server-path" formControlName="pathSvr"></td>
+ </tr>
+ <tr>
+ <th><label for="select-label">Label </label></th>
+ <td><input type="text" formControlName="label" id="select-label" (keyup)="onKeyLabel($event)"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <div class="pull-left">
+ <button class="btn btn-default" (click)="cancelAction=true; hide()"> Cancel </button>
+ </div>
+ <div class="">
+ <button class="btn btn-primary" type="submit" [disabled]="!addProjectForm.valid">Add Folder</button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+</div>
diff --git a/webapp/src/app/projects/projectAddModal.component.ts b/webapp/src/app/projects/projectAddModal.component.ts
new file mode 100644
index 0000000..1584b5b
--- /dev/null
+++ b/webapp/src/app/projects/projectAddModal.component.ts
@@ -0,0 +1,147 @@
+import { Component, Input, ViewChild, OnInit } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { ModalDirective } from 'ngx-bootstrap/modal';
+import { FormControl, FormGroup, Validators, FormBuilder, ValidatorFn, AbstractControl } from '@angular/forms';
+
+// Import RxJs required methods
+import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/filter';
+import 'rxjs/add/operator/debounceTime';
+
+import { AlertService, IAlert } from "../services/alert.service";
+import { ProjectService, IProject, ProjectType, ProjectTypes } from "../services/project.service";
+
+
+@Component({
+ selector: 'project-add-modal',
+ templateUrl: './app/projects/projectAddModal.component.html',
+ styleUrls: ['./app/projects/projectAddModal.component.css']
+})
+export class ProjectAddModalComponent {
+ @ViewChild('childProjectModal') public childProjectModal: ModalDirective;
+ @Input() title?: string;
+ @Input('server-id') serverID: string;
+
+ cancelAction: boolean = false;
+ userEditedLabel: boolean = false;
+ projectTypes = ProjectTypes;
+
+ addProjectForm: FormGroup;
+ typeCtrl: FormControl;
+ pathCliCtrl: FormControl;
+ pathSvrCtrl: FormControl;
+
+ constructor(
+ private alert: AlertService,
+ private projectSvr: ProjectService,
+ private fb: FormBuilder
+ ) {
+ // Define types (first one is special/placeholder)
+ this.projectTypes.unshift({ value: ProjectType.UNSET, display: "--Select a type--" });
+
+ this.typeCtrl = new FormControl(this.projectTypes[0].value, Validators.pattern("[A-Za-z]+"));
+ this.pathCliCtrl = new FormControl("", Validators.required);
+ this.pathSvrCtrl = new FormControl({ value: "", disabled: true }, [Validators.required, Validators.minLength(1)]);
+
+ this.addProjectForm = fb.group({
+ type: this.typeCtrl,
+ pathCli: this.pathCliCtrl,
+ pathSvr: this.pathSvrCtrl,
+ label: ["", Validators.nullValidator],
+ });
+ }
+
+ ngOnInit() {
+ // Auto create label name
+ this.pathCliCtrl.valueChanges
+ .debounceTime(100)
+ .filter(n => n)
+ .map(n => {
+ let last = n.split('/');
+ let nm = n;
+ if (last.length > 0) {
+ nm = last.pop();
+ if (nm === "" && last.length > 0) {
+ nm = last.pop();
+ }
+ }
+ return "Project_" + nm;
+ })
+ .subscribe(value => {
+ if (value && !this.userEditedLabel) {
+ this.addProjectForm.patchValue({ label: value });
+ }
+ });
+
+ // Handle disabling of Server path
+ this.typeCtrl.valueChanges
+ .debounceTime(500)
+ .subscribe(valType => {
+ let dis = (valType === String(ProjectType.SYNCTHING));
+ this.pathSvrCtrl.reset({ value: "", disabled: dis });
+ });
+ }
+
+ show() {
+ this.cancelAction = false;
+ this.userEditedLabel = false;
+ this.childProjectModal.show();
+ }
+
+ hide() {
+ this.childProjectModal.hide();
+ }
+
+ onKeyLabel(event: any) {
+ this.userEditedLabel = (this.addProjectForm.value.label !== "");
+ }
+
+ /* FIXME: change input to file type
+ <td><input type="file" id="select-local-path" webkitdirectory
+ formControlName="pathCli" placeholder="myProject" (change)="onChangeLocalProject($event)"></td>
+
+ onChangeLocalProject(e) {
+ if e.target.files.length < 1 {
+ console.log('NO files');
+ }
+ let dir = e.target.files[0].webkitRelativePath;
+ console.log("files: " + dir);
+ let u = URL.createObjectURL(e.target.files[0]);
+ }
+ */
+ onChangeLocalProject(e) {
+ }
+
+ onSubmit() {
+ if (this.cancelAction) {
+ return;
+ }
+
+ let formVal = this.addProjectForm.value;
+
+ let type = formVal['type'].value;
+ this.projectSvr.Add({
+ serverId: this.serverID,
+ label: formVal['label'],
+ pathClient: formVal['pathCli'],
+ pathServer: formVal['pathSvr'],
+ type: formVal['type'],
+ // FIXME: allow to set defaultSdkID from New Project config panel
+ })
+ .subscribe(prj => {
+ this.alert.info("Project " + prj.label + " successfully created.");
+ this.hide();
+
+ // Reset Value for the next creation
+ this.addProjectForm.reset();
+ let selectedType = this.projectTypes[0].value;
+ this.addProjectForm.patchValue({ type: selectedType });
+
+ },
+ err => {
+ this.alert.error("Configuration ERROR: " + err, 60);
+ this.hide();
+ });
+ }
+
+}
diff --git a/webapp/src/app/projects/projectCard.component.ts b/webapp/src/app/projects/projectCard.component.ts
new file mode 100644
index 0000000..fdacba4
--- /dev/null
+++ b/webapp/src/app/projects/projectCard.component.ts
@@ -0,0 +1,91 @@
+import { Component, Input, Pipe, PipeTransform } from '@angular/core';
+import { ProjectService, IProject, ProjectType } from "../services/project.service";
+import { AlertService } from "../services/alert.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-exchange"></span>&nbsp;<span>Sharing type</span></th>
+ <td>{{ project.type | readableType }}</td>
+ </tr>
+ <tr>
+ <th><span class="fa fa-fw fa-folder-open-o"></span>&nbsp;<span>Local path</span></th>
+ <td>{{ project.pathClient }}</td>
+ </tr>
+ <tr *ngIf="project.pathServer && project.pathServer != ''">
+ <th><span class="fa fa-fw fa-folder-open-o"></span>&nbsp;<span>Server path</span></th>
+ <td>{{ project.pathServer }}</td>
+ </tr>
+ <tr>
+ <th><span class="fa fa-fw fa-flag"></span>&nbsp;<span>Status</span></th>
+ <td>{{ project.status }} - {{ project.isInSync ? "Up to Date" : "Out of Sync"}}
+ <button *ngIf="!project.isInSync" class="btn btn-link" (click)="sync(project)">
+ <span class="fa fa-refresh fa-size-x2"></span>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table >
+ `,
+ styleUrls: ['./app/config/config.component.css']
+})
+
+export class ProjectCardComponent {
+
+ @Input() project: IProject;
+
+ constructor(
+ private alert: AlertService,
+ private projectSvr: ProjectService
+ ) {
+ }
+
+ delete(prj: IProject) {
+ this.projectSvr.Delete(prj)
+ .subscribe(res => {
+ }, err => {
+ this.alert.error("Delete ERROR: " + err);
+ });
+ }
+
+ sync(prj: IProject) {
+ this.projectSvr.Sync(prj)
+ .subscribe(res => {
+ }, err => {
+ this.alert.error("ERROR: " + err);
+ });
+ }
+
+}
+
+// Remove APPS. prefix if translate has failed
+@Pipe({
+ name: 'readableType'
+})
+
+export class ProjectReadableTypePipe implements PipeTransform {
+ transform(type: ProjectType): string {
+ switch (type) {
+ case ProjectType.NATIVE_PATHMAP: return "Native (path mapping)";
+ case ProjectType.SYNCTHING: return "Cloud (Syncthing)";
+ default: return String(type);
+ }
+ }
+}
diff --git a/webapp/src/app/projects/projectsListAccordion.component.ts b/webapp/src/app/projects/projectsListAccordion.component.ts
new file mode 100644
index 0000000..210be5c
--- /dev/null
+++ b/webapp/src/app/projects/projectsListAccordion.component.ts
@@ -0,0 +1,39 @@
+import { Component, Input } from "@angular/core";
+
+import { IProject } from "../services/project.service";
+
+@Component({
+ selector: 'projects-list-accordion',
+ template: `
+ <style>
+ .fa.fa-exclamation-triangle {
+ margin-right: 2em;
+ color: red;
+ }
+ .fa.fa-refresh {
+ margin-right: 10px;
+ color: darkviolet;
+ }
+ </style>
+ <accordion>
+ <accordion-group #group *ngFor="let prj of projects">
+ <div accordion-heading>
+ {{ prj.label }}
+ <div class="pull-right">
+ <i *ngIf="prj.status == 'Syncing'" class="fa fa-refresh faa-spin animated"></i>
+ <i *ngIf="!prj.isInSync && prj.status != 'Syncing'" class="fa fa-exclamation-triangle"></i>
+ <i class="fa" [ngClass]="{'fa-chevron-down': group.isOpen, 'fa-chevron-right': !group.isOpen}"></i>
+ </div>
+ </div>
+ <project-card [project]="prj"></project-card>
+ </accordion-group>
+ </accordion>
+ `
+})
+export class ProjectsListAccordionComponent {
+
+ @Input() projects: IProject[];
+
+}
+
+
diff --git a/webapp/src/app/sdks/sdkAddModal.component.html b/webapp/src/app/sdks/sdkAddModal.component.html
new file mode 100644
index 0000000..2c07fca
--- /dev/null
+++ b/webapp/src/app/sdks/sdkAddModal.component.html
@@ -0,0 +1,23 @@
+<div bsModal #sdkChildModal="bs-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel"
+ aria-hidden="true">
+ <div class="modal-dialog modal-lg">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title pull-left">{{title}}</h4>
+ <button type="button" class="close pull-right" aria-label="Close" (click)="hideChildModal()">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <ng-content select=".modal-body"> </ng-content>
+ <i>Not available for now.</i>
+ </div>
+
+ <div class="modal-footer">
+ <div class="pull-left">
+ <button class="btn btn-default" (click)="hide()"> Cancel </button>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/webapp/src/app/sdks/sdkAddModal.component.ts b/webapp/src/app/sdks/sdkAddModal.component.ts
new file mode 100644
index 0000000..b6c8eb2
--- /dev/null
+++ b/webapp/src/app/sdks/sdkAddModal.component.ts
@@ -0,0 +1,24 @@
+import { Component, Input, ViewChild } from '@angular/core';
+import { ModalDirective } from 'ngx-bootstrap/modal';
+
+@Component({
+ selector: 'sdk-add-modal',
+ templateUrl: './app/sdks/sdkAddModal.component.html',
+})
+export class SdkAddModalComponent {
+ @ViewChild('sdkChildModal') public sdkChildModal: ModalDirective;
+
+ @Input() title?: string;
+
+ // TODO
+ constructor() {
+ }
+
+ show() {
+ this.sdkChildModal.show();
+ }
+
+ hide() {
+ this.sdkChildModal.hide();
+ }
+}
diff --git a/webapp/src/app/sdks/sdkCard.component.ts b/webapp/src/app/sdks/sdkCard.component.ts
new file mode 100644
index 0000000..3256a0b
--- /dev/null
+++ b/webapp/src/app/sdks/sdkCard.component.ts
@@ -0,0 +1,55 @@
+import { Component, Input } from '@angular/core';
+import { ISdk } from "../services/sdk.service";
+
+@Component({
+ selector: 'sdk-card',
+ template: `
+ <div class="row">
+ <div class="col-xs-12">
+ <div class="text-right" role="group">
+ <button disabled class="btn btn-link" (click)="delete(sdk)"><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>SDK ID</span></th>
+ <td>{{ sdk.id }}</td>
+ </tr>
+ <tr>
+ <th><span class="fa fa-fw fa-user"></span>&nbsp;<span>Profile</span></th>
+ <td>{{ sdk.profile }}</td>
+ </tr>
+ <tr>
+ <th><span class="fa fa-fw fa-tasks"></span>&nbsp;<span>Architecture</span></th>
+ <td>{{ sdk.arch }}</td>
+ </tr>
+ <tr>
+ <th><span class="fa fa-fw fa-code-fork"></span>&nbsp;<span>Version</span></th>
+ <td>{{ sdk.version }}</td>
+ </tr>
+ <tr>
+ <th><span class="fa fa-fw fa-folder-open-o"></span>&nbsp;<span>Sdk path</span></th>
+ <td>{{ sdk.path}}</td>
+ </tr>
+
+ </tbody>
+ </table >
+ `,
+ styleUrls: ['./app/config/config.component.css']
+})
+
+export class SdkCardComponent {
+
+ @Input() sdk: ISdk;
+
+ constructor() { }
+
+
+ delete(sdk: ISdk) {
+ // Not supported for now
+ }
+
+}
diff --git a/webapp/src/app/sdks/sdkSelectDropdown.component.ts b/webapp/src/app/sdks/sdkSelectDropdown.component.ts
new file mode 100644
index 0000000..a2fe37a
--- /dev/null
+++ b/webapp/src/app/sdks/sdkSelectDropdown.component.ts
@@ -0,0 +1,48 @@
+import { Component, Input } from "@angular/core";
+
+import { ISdk, SdkService } from "../services/sdk.service";
+
+@Component({
+ selector: 'sdk-select-dropdown',
+ template: `
+ <div class="btn-group" dropdown *ngIf="curSdk" >
+ <button dropdownToggle type="button" class="btn btn-primary dropdown-toggle" style="width: 20em;">
+ {{curSdk.name}} <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 sdk of sdks" (click)="select(sdk)">
+ {{sdk.name}}</a>
+ </li>
+ </ul>
+ </div>
+ `
+})
+export class SdkSelectDropdownComponent {
+
+ // FIXME investigate to understand why not working with sdks as input
+ // <sdk-select-dropdown [sdks]="(sdks$ | async)"></sdk-select-dropdown>
+ //@Input() sdks: ISdk[];
+ sdks: ISdk[];
+
+ curSdk: ISdk;
+
+ constructor(private sdkSvr: SdkService) { }
+
+ ngOnInit() {
+ this.curSdk = this.sdkSvr.getCurrent();
+ this.sdkSvr.Sdks$.subscribe((s) => {
+ if (s) {
+ this.sdks = s;
+ if (this.curSdk === null || s.indexOf(this.curSdk) === -1) {
+ this.sdkSvr.setCurrent(this.curSdk = s.length ? s[0] : null);
+ }
+ }
+ });
+ }
+
+ select(s) {
+ this.sdkSvr.setCurrent(this.curSdk = s);
+ }
+}
+
+
diff --git a/webapp/src/app/sdks/sdksListAccordion.component.ts b/webapp/src/app/sdks/sdksListAccordion.component.ts
new file mode 100644
index 0000000..9d5f7e9
--- /dev/null
+++ b/webapp/src/app/sdks/sdksListAccordion.component.ts
@@ -0,0 +1,26 @@
+import { Component, Input } from "@angular/core";
+
+import { ISdk } from "../services/sdk.service";
+
+@Component({
+ selector: 'sdks-list-accordion',
+ template: `
+ <accordion>
+ <accordion-group #group *ngFor="let sdk of sdks">
+ <div accordion-heading>
+ {{ sdk.name }}
+ <i class="pull-right float-xs-right fa"
+ [ngClass]="{'fa-chevron-down': group.isOpen, 'fa-chevron-right': !group.isOpen}"></i>
+ </div>
+ <sdk-card [sdk]="sdk"></sdk-card>
+ </accordion-group>
+ </accordion>
+ `
+})
+export class SdksListAccordionComponent {
+
+ @Input() sdks: ISdk[];
+
+}
+
+
diff --git a/webapp/src/app/services/alert.service.ts b/webapp/src/app/services/alert.service.ts
new file mode 100644
index 0000000..c3cae7a
--- /dev/null
+++ b/webapp/src/app/services/alert.service.ts
@@ -0,0 +1,66 @@
+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, dismissTime?: number) {
+ this.add({
+ type: "danger", msg: msg, dismissible: true, dismissTimeout: dismissTime
+ });
+ }
+
+ 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: "info", 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/services/config.service.ts b/webapp/src/app/services/config.service.ts
new file mode 100644
index 0000000..090df7b
--- /dev/null
+++ b/webapp/src/app/services/config.service.ts
@@ -0,0 +1,178 @@
+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 { XDSAgentService, IXDSProjectConfig } from "../services/xdsagent.service";
+import { AlertService, IAlert } from "../services/alert.service";
+import { UtilsService } from "../services/utils.service";
+
+export interface IConfig {
+ projectsRootDir: string;
+ //SEB projects: IProject[];
+}
+
+@Injectable()
+export class ConfigService {
+
+ public Conf$: Observable<IConfig>;
+
+ private confSubject: BehaviorSubject<IConfig>;
+ private confStore: IConfig;
+ // SEB cleanup private AgentConnectObs = null;
+ // SEB cleanup private stConnectObs = null;
+
+ constructor(private _window: Window,
+ private cookie: CookieService,
+ private xdsAgentSvr: XDSAgentService,
+ private alert: AlertService,
+ private utils: UtilsService,
+ ) {
+ 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 = {
+ projectsRootDir: "",
+ //projects: []
+ };
+ }
+ }
+
+ // Save config into cookie
+ save() {
+ // Notify subscribers
+ this.confSubject.next(Object.assign({}, this.confStore));
+
+ // Don't save projects in cookies (too big!)
+ let cfg = Object.assign({}, this.confStore);
+ this.cookie.putObject("xds-config", cfg);
+ }
+
+ loadProjects() {
+ /* SEB
+ // Setup connection with local XDS agent
+ if (this.AgentConnectObs) {
+ try {
+ this.AgentConnectObs.unsubscribe();
+ } catch (err) { }
+ this.AgentConnectObs = null;
+ }
+
+ let cfg = this.confStore.xdsAgent;
+ this.AgentConnectObs = this.xdsAgentSvr.connect(cfg.retry, cfg.URL)
+ .subscribe((sts) => {
+ //console.log("Agent sts", sts);
+ // FIXME: load projects from local XDS Agent and
+ // not directly from local syncthing
+ this._loadProjectFromLocalST();
+
+ }, error => {
+ if (error.indexOf("XDS local Agent not responding") !== -1) {
+ let url_OS_Linux = "https://en.opensuse.org/LinuxAutomotive#Installation_AGL_XDS";
+ let url_OS_Other = "https://github.com/iotbzh/xds-agent#how-to-install-on-other-platform";
+ let msg = `<span><strong>` + error + `<br></strong>
+ You may need to install and execute XDS-Agent: <br>
+ On Linux machine <a href="` + url_OS_Linux + `" target="_blank"><span
+ class="fa fa-external-link"></span></a>
+ <br>
+ On Windows machine <a href="` + url_OS_Other + `" target="_blank"><span
+ class="fa fa-external-link"></span></a>
+ <br>
+ On MacOS machine <a href="` + url_OS_Other + `" target="_blank"><span
+ class="fa fa-external-link"></span></a>
+ `;
+ this.alert.error(msg);
+ } else {
+ this.alert.error(error);
+ }
+ });
+ */
+ }
+
+ /* SEB
+ private _loadProjectFromLocalST() {
+ // Remove previous subscriber if existing
+ if (this.stConnectObs) {
+ try {
+ this.stConnectObs.unsubscribe();
+ } catch (err) { }
+ this.stConnectObs = null;
+ }
+
+ // FIXME: move this code and all logic about syncthing inside XDS Agent
+ // 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.xdsServerSvr.getProjects().subscribe(remotePrj => {
+ this.stSvr.getProjects().subscribe(localPrj => {
+ remotePrj.forEach(rPrj => {
+ let lPrj = localPrj.filter(item => item.id === rPrj.id);
+ if (lPrj.length > 0 || rPrj.type === ProjectType.NATIVE_PATHMAP) {
+ this._addProject(rPrj, true);
+ }
+ });
+ 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 => {
+ if (error.indexOf("Syncthing local daemon not responding") !== -1) {
+ let msg = "<span><strong>" + error + "<br></strong>";
+ msg += "Please check that local XDS-Agent is running.<br>";
+ msg += "</span>";
+ this.alert.error(msg);
+ } else {
+ this.alert.error(error);
+ }
+ });
+ }
+
+ set syncToolURL(url: string) {
+ this.confStore.localSThg.URL = url;
+ this.save();
+ }
+ */
+
+ set projectsRootDir(p: string) {
+ /* SEB
+ if (p.charAt(0) === '~') {
+ p = this.confStore.localSThg.tilde + p.substring(1);
+ }
+ */
+ this.confStore.projectsRootDir = p;
+ this.save();
+ }
+}
diff --git a/webapp/src/app/services/project.service.ts b/webapp/src/app/services/project.service.ts
new file mode 100644
index 0000000..53adc80
--- /dev/null
+++ b/webapp/src/app/services/project.service.ts
@@ -0,0 +1,199 @@
+import { Injectable, SecurityContext } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+
+import { XDSAgentService, IXDSProjectConfig } from "../services/xdsagent.service";
+
+export enum ProjectType {
+ UNSET = "",
+ NATIVE_PATHMAP = "PathMap",
+ SYNCTHING = "CloudSync"
+}
+
+export var ProjectTypes = [
+ { value: ProjectType.NATIVE_PATHMAP, display: "Path mapping" },
+ { value: ProjectType.SYNCTHING, display: "Cloud Sync" }
+];
+
+export var ProjectStatus = {
+ ErrorConfig: "ErrorConfig",
+ Disable: "Disable",
+ Enable: "Enable",
+ Pause: "Pause",
+ Syncing: "Syncing"
+};
+
+export interface IProject {
+ id?: string;
+ serverId: string;
+ label: string;
+ pathClient: string;
+ pathServer?: string;
+ type: ProjectType;
+ status?: string;
+ isInSync?: boolean;
+ isUsable?: boolean;
+ serverPrjDef?: IXDSProjectConfig;
+ isExpanded?: boolean;
+ visible?: boolean;
+ defaultSdkID?: string;
+}
+
+@Injectable()
+export class ProjectService {
+ public Projects$: Observable<IProject[]>;
+
+ private _prjsList: IProject[] = [];
+ private current: IProject;
+ private prjsSubject = <BehaviorSubject<IProject[]>>new BehaviorSubject(this._prjsList);
+
+ constructor(private xdsSvr: XDSAgentService) {
+ this.current = null;
+ this.Projects$ = this.prjsSubject.asObservable();
+
+ this.xdsSvr.getProjects().subscribe((projects) => {
+ this._prjsList = [];
+ projects.forEach(p => {
+ this._addProject(p, true);
+ });
+ this.prjsSubject.next(Object.assign([], this._prjsList));
+ });
+
+ // Update Project data
+ this.xdsSvr.ProjectState$.subscribe(prj => {
+ let i = this._getProjectIdx(prj.id);
+ if (i >= 0) {
+ // XXX for now, only isInSync and status may change
+ this._prjsList[i].isInSync = prj.isInSync;
+ this._prjsList[i].status = prj.status;
+ this._prjsList[i].isUsable = this._isUsableProject(prj);
+ this.prjsSubject.next(Object.assign([], this._prjsList));
+ }
+ });
+
+ // Add listener on create and delete project events
+ this.xdsSvr.addEventListener('event:project-add', (ev) => {
+ if (ev && ev.data && ev.data.id) {
+ this._addProject(ev.data);
+ } else {
+ console.log("Warning: received events with unknown data: ev=", ev);
+ }
+ });
+ this.xdsSvr.addEventListener('event:project-delete', (ev) => {
+ if (ev && ev.data && ev.data.id) {
+ let idx = this._prjsList.findIndex(item => item.id === ev.data.id);
+ if (idx === -1) {
+ console.log("Warning: received events on unknown project id: ev=", ev);
+ return;
+ }
+ this._prjsList.splice(idx, 1);
+ this.prjsSubject.next(Object.assign([], this._prjsList));
+ } else {
+ console.log("Warning: received events with unknown data: ev=", ev);
+ }
+ });
+
+ }
+
+ public setCurrent(s: IProject) {
+ this.current = s;
+ }
+
+ public getCurrent(): IProject {
+ return this.current;
+ }
+
+ public getCurrentId(): string {
+ if (this.current && this.current.id) {
+ return this.current.id;
+ }
+ return "";
+ }
+
+ Add(prj: IProject): Observable<IProject> {
+ let xdsPrj: IXDSProjectConfig = {
+ id: "",
+ serverId: prj.serverId,
+ label: prj.label || "",
+ clientPath: prj.pathClient.trim(),
+ serverPath: prj.pathServer,
+ type: prj.type,
+ defaultSdkID: prj.defaultSdkID,
+ };
+ // Send config to XDS server
+ return this.xdsSvr.addProject(xdsPrj)
+ .map(xdsPrj => this._convToIProject(xdsPrj));
+ }
+
+ Delete(prj: IProject): Observable<IProject> {
+ let idx = this._getProjectIdx(prj.id);
+ let delPrj = prj;
+ if (idx === -1) {
+ throw new Error("Invalid project id (id=" + prj.id + ")");
+ }
+ return this.xdsSvr.deleteProject(prj.id)
+ .map(res => { return delPrj; });
+ }
+
+ Sync(prj: IProject): Observable<string> {
+ let idx = this._getProjectIdx(prj.id);
+ if (idx === -1) {
+ throw new Error("Invalid project id (id=" + prj.id + ")");
+ }
+ return this.xdsSvr.syncProject(prj.id);
+ }
+
+ private _isUsableProject(p) {
+ return p && p.isInSync &&
+ (p.status === ProjectStatus.Enable) &&
+ (p.status !== ProjectStatus.Syncing);
+ }
+
+ private _getProjectIdx(id: string): number {
+ return this._prjsList.findIndex((item) => item.id === id);
+ }
+
+ private _convToIProject(rPrj: IXDSProjectConfig): IProject {
+ // Convert XDSFolderConfig to IProject
+ let pp: IProject = {
+ id: rPrj.id,
+ serverId: rPrj.serverId,
+ label: rPrj.label,
+ pathClient: rPrj.clientPath,
+ pathServer: rPrj.serverPath,
+ type: rPrj.type,
+ status: rPrj.status,
+ isInSync: rPrj.isInSync,
+ isUsable: this._isUsableProject(rPrj),
+ defaultSdkID: rPrj.defaultSdkID,
+ serverPrjDef: Object.assign({}, rPrj), // do a copy
+ };
+ return pp;
+ }
+
+ private _addProject(rPrj: IXDSProjectConfig, noNext?: boolean): IProject {
+
+ // Convert XDSFolderConfig to IProject
+ let pp = this._convToIProject(rPrj);
+
+ // add new project
+ this._prjsList.push(pp);
+
+ // sort project array
+ this._prjsList.sort((a, b) => {
+ if (a.label < b.label) {
+ return -1;
+ }
+ if (a.label > b.label) {
+ return 1;
+ }
+ return 0;
+ });
+
+ if (!noNext) {
+ this.prjsSubject.next(Object.assign([], this._prjsList));
+ }
+
+ return pp;
+ }
+}
diff --git a/webapp/src/app/services/sdk.service.ts b/webapp/src/app/services/sdk.service.ts
new file mode 100644
index 0000000..6d8a5f6
--- /dev/null
+++ b/webapp/src/app/services/sdk.service.ts
@@ -0,0 +1,54 @@
+import { Injectable, SecurityContext } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+
+import { XDSAgentService } from "../services/xdsagent.service";
+
+export interface ISdk {
+ id: string;
+ profile: string;
+ version: string;
+ arch: number;
+ path: string;
+}
+
+@Injectable()
+export class SdkService {
+ public Sdks$: Observable<ISdk[]>;
+
+ private _sdksList = [];
+ private current: ISdk;
+ private sdksSubject = <BehaviorSubject<ISdk[]>>new BehaviorSubject(this._sdksList);
+
+ constructor(private xdsSvr: XDSAgentService) {
+ this.current = null;
+ this.Sdks$ = this.sdksSubject.asObservable();
+
+ this.xdsSvr.XdsConfig$.subscribe(cfg => {
+ if (!cfg || cfg.servers.length < 1) {
+ return;
+ }
+ // FIXME support multiple server
+ //cfg.servers.forEach(svr => {
+ this.xdsSvr.getSdks(cfg.servers[0].id).subscribe((s) => {
+ this._sdksList = s;
+ this.sdksSubject.next(s);
+ });
+ });
+ }
+
+ public setCurrent(s: ISdk) {
+ this.current = s;
+ }
+
+ public getCurrent(): ISdk {
+ return this.current;
+ }
+
+ public getCurrentId(): string {
+ if (this.current && this.current.id) {
+ return this.current.id;
+ }
+ return "";
+ }
+}
diff --git a/webapp/src/app/services/syncthing.service.ts b/webapp/src/app/services/syncthing.service.ts
new file mode 100644
index 0000000..1561cbf
--- /dev/null
+++ b/webapp/src/app/services/syncthing.service.ts
@@ -0,0 +1,352 @@
+import { Injectable } from '@angular/core';
+/*
+import { Http, Headers, RequestOptionsArgs, Response } from '@angular/http';
+import { CookieService } from 'ngx-cookie';
+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;
+ serverSyncThingID: string;
+ label?: string;
+}
+
+export interface ISyncThingStatus {
+ ID: string;
+ baseURL: string;
+ connected: boolean;
+ connectionRetry: number;
+ tilde: string;
+ rawStatus: any;
+}
+
+// Private interfaces of Syncthing
+const ISTCONFIG_VERSION = 20;
+
+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";
+const DEFAULT_RESCAN_INTERV = 0; // 0: use syncthing-inotify to detect changes
+
+*/
+
+@Injectable()
+export class SyncthingService {
+
+ /* SEB A SUP
+ public Status$: Observable<ISyncThingStatus>;
+
+ private baseRestUrl: string;
+ private apikey: string;
+ private localSTID: string;
+ private stCurVersion: number;
+ private connectionMaxRetry: number;
+ private _status: ISyncThingStatus = {
+ ID: null,
+ baseURL: "",
+ connected: false,
+ connectionRetry: 0,
+ tilde: "",
+ rawStatus: null,
+ };
+ private statusSubject = <BehaviorSubject<ISyncThingStatus>>new BehaviorSubject(this._status);
+
+ constructor(private http: Http, private _window: Window, private cookie: CookieService) {
+ this._status.baseURL = 'http://localhost:' + DEFAULT_GUI_PORT;
+ this.baseRestUrl = this._status.baseURL + '/rest';
+ this.apikey = DEFAULT_GUI_API_KEY;
+ this.stCurVersion = -1;
+ this.connectionMaxRetry = 10; // 10 seconds
+
+ 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;
+ this._status.connectionRetry = 0;
+ this.connectionMaxRetry = retry || 3600; // 1 hour
+ return this.getStatus();
+ }
+
+ getID(): Observable<string> {
+ if (this._status.ID != null) {
+ return Observable.of(this._status.ID);
+ }
+ return this.getStatus().map(sts => sts.ID);
+ }
+
+ getStatus(): Observable<ISyncThingStatus> {
+ return this._get('/system/status')
+ .map((status) => {
+ this._status.ID = status["myID"];
+ this._status.tilde = status["tilde"];
+ console.debug('ST local ID', this._status.ID);
+
+ this._status.rawStatus = status;
+
+ return this._status;
+ });
+ }
+
+ 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.serverSyncThingID;
+
+ // 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 scanInterval = parseInt(this.cookie.get("st-rescanInterval"), 10) || DEFAULT_RESCAN_INTERV;
+ let folder: ISTFolderConfiguration = {
+ id: prj.id,
+ label: label,
+ path: prj.path,
+ devices: [{ deviceID: newDevID, introducedBy: "" }],
+ autoNormalize: true,
+ rescanIntervalS: scanInterval,
+ };
+
+ 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> {
+ if (this._status.connected) {
+ return Observable.of(true);
+ }
+
+ return this.http.get(this.baseRestUrl + '/system/version', this._attachAuthHeaders())
+ .map((r) => this._status.connected = true)
+ .retryWhen((attempts) => {
+ this._status.connectionRetry = 0;
+ return attempts.flatMap(error => {
+ this._status.connected = false;
+ if (++this._status.connectionRetry >= this.connectionMaxRetry) {
+ return Observable.throw("Syncthing local daemon not responding (url=" + this._status.baseURL + ")");
+ } else {
+ return Observable.timer(1000);
+ }
+ });
+ });
+ }
+
+ private _getAPIVersion(): Observable<number> {
+ if (this.stCurVersion !== -1) {
+ return Observable.of(this.stCurVersion);
+ }
+
+ return 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._checkAlive()
+ .flatMap(() => 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._checkAlive()
+ .flatMap(() => 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/services/utils.service.ts b/webapp/src/app/services/utils.service.ts
new file mode 100644
index 0000000..84b9ab6
--- /dev/null
+++ b/webapp/src/app/services/utils.service.ts
@@ -0,0 +1,33 @@
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class UtilsService {
+ constructor() { }
+
+ getOSName(lowerCase?: boolean): string {
+ var checkField = function (ff) {
+ if (ff.indexOf("Linux") !== -1) {
+ return "Linux";
+ } else if (ff.indexOf("Win") !== -1) {
+ return "Windows";
+ } else if (ff.indexOf("Mac") !== -1) {
+ return "MacOS";
+ } else if (ff.indexOf("X11") !== -1) {
+ return "UNIX";
+ }
+ return "";
+ };
+
+ let OSName = checkField(navigator.platform);
+ if (OSName === "") {
+ OSName = checkField(navigator.appVersion);
+ }
+ if (OSName === "") {
+ OSName = "Unknown OS";
+ }
+ if (lowerCase) {
+ return OSName.toLowerCase();
+ }
+ return OSName;
+ }
+} \ No newline at end of file
diff --git a/webapp/src/app/services/xdsagent.service.ts b/webapp/src/app/services/xdsagent.service.ts
new file mode 100644
index 0000000..e570399
--- /dev/null
+++ b/webapp/src/app/services/xdsagent.service.ts
@@ -0,0 +1,401 @@
+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 { ISdk } from './sdk.service';
+import { ProjectType} from "./project.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';
+import 'rxjs/add/observable/of';
+import 'rxjs/add/operator/retryWhen';
+
+
+export interface IXDSConfigProject {
+ id: string;
+ path: string;
+ clientSyncThingID: string;
+ type: string;
+ label?: string;
+ defaultSdkID?: string;
+}
+
+interface IXDSBuilderConfig {
+ ip: string;
+ port: string;
+ syncThingID: string;
+}
+
+export interface IXDSProjectConfig {
+ id: string;
+ serverId: string;
+ label: string;
+ clientPath: string;
+ serverPath?: string;
+ type: ProjectType;
+ status?: string;
+ isInSync?: boolean;
+ defaultSdkID: string;
+}
+
+export interface IXDSVer {
+ id: string;
+ version: string;
+ apiVersion: string;
+ gitTag: string;
+}
+
+export interface IXDSVersions {
+ client: IXDSVer;
+ servers: IXDSVer[];
+}
+
+export interface IXDServerCfg {
+ id: string;
+ url: string;
+ apiUrl: string;
+ partialUrl: string;
+ connRetry: number;
+ connected: boolean;
+}
+
+export interface IXDSConfig {
+ servers: IXDServerCfg[];
+}
+
+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 IAgentStatus {
+ WS_connected: boolean;
+}
+
+
+@Injectable()
+export class XDSAgentService {
+
+ public XdsConfig$: Observable<IXDSConfig>;
+ public Status$: Observable<IAgentStatus>;
+ public ProjectState$ = <Subject<IXDSProjectConfig>>new Subject();
+ public CmdOutput$ = <Subject<ICmdOutput>>new Subject();
+ public CmdExit$ = <Subject<ICmdExit>>new Subject();
+
+ private baseUrl: string;
+ private wsUrl: string;
+ private _config = <IXDSConfig>{ servers: [] };
+ private _status = { WS_connected: false };
+
+ private configSubject = <BehaviorSubject<IXDSConfig>>new BehaviorSubject(this._config);
+ private statusSubject = <BehaviorSubject<IAgentStatus>>new BehaviorSubject(this._status);
+
+ private socket: SocketIOClient.Socket;
+
+ constructor(private http: Http, private _window: Window, private alert: AlertService) {
+
+ this.XdsConfig$ = this.configSubject.asObservable();
+ 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();
+ this._RegisterEvents();
+ }
+ }
+
+ private _WSState(sts: boolean) {
+ this._status.WS_connected = sts;
+ this.statusSubject.next(Object.assign({}, this._status));
+
+ // Update XDS config including XDS Server list when connected
+ if (sts) {
+ this.getConfig().subscribe(c => {
+ this._config = c;
+ this.configSubject.next(
+ Object.assign({ servers: [] }, this._config)
+ );
+ });
+ }
+ }
+
+ private _handleIoSocket() {
+ this.socket = io(this.wsUrl, { transports: ['websocket'] });
+
+ this.socket.on('connect_error', (res) => {
+ this._WSState(false);
+ console.error('XDS Agent WebSocket Connection error !');
+ });
+
+ 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));
+ });
+
+ this.socket.on('exec:output', data => {
+ this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data));
+ });
+
+ this.socket.on('exec:exit', data => {
+ this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
+ });
+
+ // Events
+ // (project-add and project-delete events are managed by project.service)
+ this.socket.on('event:server-config', ev => {
+ if (ev && ev.data) {
+ let cfg: IXDServerCfg = ev.data;
+ let idx = this._config.servers.findIndex(el => el.id === cfg.id);
+ if (idx >= 0) {
+ this._config.servers[idx] = Object.assign({}, cfg);
+ }
+ this.configSubject.next(Object.assign({}, this._config));
+ }
+ });
+
+ this.socket.on('event:project-state-change', ev => {
+ if (ev && ev.data) {
+ this.ProjectState$.next(Object.assign({}, ev.data));
+ }
+ });
+
+ }
+
+ /**
+ ** Events
+ ***/
+ addEventListener(ev: string, fn: Function): SocketIOClient.Emitter {
+ return this.socket.addEventListener(ev, fn);
+ }
+
+ /**
+ ** Misc / Version
+ ***/
+ getVersion(): Observable<IXDSVersions> {
+ return this._get('/version');
+ }
+
+ /***
+ ** Config
+ ***/
+ getConfig(): Observable<IXDSConfig> {
+ return this._get('/config');
+ }
+
+ setConfig(cfg: IXDSConfig): Observable<IXDSConfig> {
+ return this._post('/config', cfg);
+ }
+
+ setServerRetry(serverID: string, r: number) {
+ let svr = this._getServer(serverID);
+ if (!svr) {
+ return Observable.of([]);
+ }
+
+ svr.connRetry = r;
+ this.setConfig(this._config).subscribe(
+ newCfg => {
+ this._config = newCfg;
+ this.configSubject.next(Object.assign({}, this._config));
+ },
+ err => {
+ this.alert.error(err);
+ }
+ );
+ }
+
+ setServerUrl(serverID: string, url: string) {
+ let svr = this._getServer(serverID);
+ if (!svr) {
+ return Observable.of([]);
+ }
+ svr.url = url;
+ this.setConfig(this._config).subscribe(
+ newCfg => {
+ this._config = newCfg;
+ this.configSubject.next(Object.assign({}, this._config));
+ },
+ err => {
+ this.alert.error(err);
+ }
+ );
+ }
+
+ /***
+ ** SDKs
+ ***/
+ getSdks(serverID: string): Observable<ISdk[]> {
+ let svr = this._getServer(serverID);
+ if (!svr || !svr.connected) {
+ return Observable.of([]);
+ }
+
+ return this._get(svr.partialUrl + '/sdks');
+ }
+
+ /***
+ ** Projects
+ ***/
+ getProjects(): Observable<IXDSProjectConfig[]> {
+ return this._get('/projects');
+ }
+
+ addProject(cfg: IXDSProjectConfig): Observable<IXDSProjectConfig> {
+ return this._post('/project', cfg);
+ }
+
+ deleteProject(id: string): Observable<IXDSProjectConfig> {
+ return this._delete('/project/' + id);
+ }
+
+ syncProject(id: string): Observable<string> {
+ return this._post('/project/sync/' + id, {});
+ }
+
+ /***
+ ** Exec
+ ***/
+ exec(prjID: string, dir: string, cmd: string, sdkid?: string, args?: string[], env?: string[]): Observable<any> {
+ return this._post('/exec',
+ {
+ id: prjID,
+ rpath: dir,
+ cmd: cmd,
+ sdkid: sdkid || "",
+ args: args || [],
+ env: env || [],
+ });
+ }
+
+ make(prjID: string, dir: string, sdkid?: string, args?: string[], env?: string[]): Observable<any> {
+ // SEB TODO add serverID
+ return this._post('/make',
+ {
+ id: prjID,
+ rpath: dir,
+ sdkid: sdkid,
+ args: args || [],
+ env: env || [],
+ });
+ }
+
+
+ /**
+ ** Private functions
+ ***/
+
+ private _RegisterEvents() {
+ // Register to all existing events
+ this._post('/events/register', { "name": "all" })
+ .subscribe(
+ res => { },
+ error => {
+ this.alert.error("ERROR while registering to all events: ", error);
+ }
+ );
+ }
+
+ private _getServer(ID: string): IXDServerCfg {
+ let svr = this._config.servers.filter(item => item.id === ID);
+ if (svr.length < 1) {
+ return null;
+ }
+ return svr[0];
+ }
+
+ 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 (err instanceof Response) {
+ const body = err.json() || 'Agent error';
+ e = body.error || JSON.stringify(body);
+ if (!e || e === "") {
+ e = `${err.status} - ${err.statusText || 'Unknown error'}`;
+ }
+ } else 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.message ? err.message : err.toString();
+ }
+ return Observable.throw(e);
+ }
+}
diff --git a/webapp/src/index.html b/webapp/src/index.html
new file mode 100644
index 0000000..290b4be
--- /dev/null
+++ b/webapp/src/index.html
@@ -0,0 +1,50 @@
+<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%);">
+ <img id="logo-iot" src="assets/images/iot-bzh-logo-small.png">
+ <br> Loading...
+ <i class="fa fa-spinner fa-spin fa-fw"></i>
+ </div>
+ </app>
+</body>
+
+</html>
diff --git a/webapp/src/systemjs.config.js b/webapp/src/systemjs.config.js
new file mode 100644
index 0000000..15c52ba
--- /dev/null
+++ b/webapp/src/systemjs.config.js
@@ -0,0 +1,69 @@
+(function (global) {
+ System.config({
+ paths: {
+ // paths serve as alias
+ 'npm:': 'lib/'
+ },
+ bundles: {
+ "npm:rxjs-system-bundle/Rx.system.min.js": [
+ "rxjs",
+ "rxjs/*",
+ "rxjs/operator/*",
+ "rxjs/observable/*",
+ "rxjs/scheduler/*",
+ "rxjs/symbol/*",
+ "rxjs/add/operator/*",
+ "rxjs/add/observable/*",
+ "rxjs/util/*"
+ ]
+ },
+ // 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/popover': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ 'ngx-bootstrap/dropdown': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ 'ngx-bootstrap/collapse': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ // other libraries
+ '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: false
+ },
+ 'socket.io-client': {
+ defaultExtension: 'js'
+ },
+ 'ngx-bootstrap': {
+ format: 'cjs',
+ main: 'bundles/ng2-bootstrap.umd.js',
+ defaultExtension: 'js'
+ },
+ 'moment': {
+ main: 'moment.js',
+ defaultExtension: 'js'
+ }
+ }
+ });
+})(this);
diff --git a/webapp/tsconfig.json b/webapp/tsconfig.json
new file mode 100644
index 0000000..9bad681
--- /dev/null
+++ b/webapp/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "outDir": "dist/app",
+ "target": "es5",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "removeComments": false,
+ "noImplicitAny": false,
+ "noStrictGenericChecks": true // better to switch to RxJS 5.4.2 ; workaround https://stackoverflow.com/questions/44810195/how-do-i-get-around-this-subject-incorrectly-extends-observable-error-in-types
+ },
+ "exclude": [
+ "gulpfile.ts",
+ "node_modules"
+ ]
+}
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"
+ }
+}