diff options
author | Sebastien Douheret <sebastien.douheret@iot.bzh> | 2017-09-25 14:15:16 +0200 |
---|---|---|
committer | Sebastien Douheret <sebastien.douheret@iot.bzh> | 2017-10-06 18:25:04 +0200 |
commit | 97ca1f277dc8b6973d6fa67add5593a9c395ce60 (patch) | |
tree | 761649d7771e8699a67567476c17fb2fa0e28e57 /webapp | |
parent | 12a20d0905b0d3e7e0f4c9ec8ee619f683256d71 (diff) |
Added webapp Dashboard + logic to interact with server.
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
Diffstat (limited to 'webapp')
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 Binary files differnew file mode 100644 index 0000000..6bf5138 --- /dev/null +++ b/webapp/assets/favicon.ico diff --git a/webapp/assets/images/iot-bzh-logo-small.png b/webapp/assets/images/iot-bzh-logo-small.png Binary files differnew file mode 100644 index 0000000..2c3b2ae --- /dev/null +++ b/webapp/assets/images/iot-bzh-logo-small.png diff --git a/webapp/assets/images/iot-graphx.jpg b/webapp/assets/images/iot-graphx.jpg Binary files differnew file mode 100644 index 0000000..74c640a --- /dev/null +++ b/webapp/assets/images/iot-graphx.jpg 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">×</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> <span>Project ID</span></th> + <td>{{ project.id }}</td> + </tr> + <tr> + <th><span class="fa fa-fw fa-exchange"></span> <span>Sharing type</span></th> + <td>{{ project.type | readableType }}</td> + </tr> + <tr> + <th><span class="fa fa-fw fa-folder-open-o"></span> <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> <span>Server path</span></th> + <td>{{ project.pathServer }}</td> + </tr> + <tr> + <th><span class="fa fa-fw fa-flag"></span> <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">×</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> <span>SDK ID</span></th> + <td>{{ sdk.id }}</td> + </tr> + <tr> + <th><span class="fa fa-fw fa-user"></span> <span>Profile</span></th> + <td>{{ sdk.profile }}</td> + </tr> + <tr> + <th><span class="fa fa-fw fa-tasks"></span> <span>Architecture</span></th> + <td>{{ sdk.arch }}</td> + </tr> + <tr> + <th><span class="fa fa-fw fa-code-fork"></span> <span>Version</span></th> + <td>{{ sdk.version }}</td> + </tr> + <tr> + <th><span class="fa fa-fw fa-folder-open-o"></span> <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" + } +} |