aboutsummaryrefslogtreecommitdiffstats
path: root/webapp/src/app/pages
diff options
context:
space:
mode:
Diffstat (limited to 'webapp/src/app/pages')
-rw-r--r--webapp/src/app/pages/pages-menu.ts11
-rw-r--r--webapp/src/app/pages/pages-routing.module.ts8
-rw-r--r--webapp/src/app/pages/pages.module.ts2
-rw-r--r--webapp/src/app/pages/projects/projects.module.ts1
-rw-r--r--webapp/src/app/pages/targets/settings/target-select-dropdown.component.ts57
-rw-r--r--webapp/src/app/pages/targets/target-add-modal/target-add-modal.component.html46
-rw-r--r--webapp/src/app/pages/targets/target-add-modal/target-add-modal.component.ts174
-rw-r--r--webapp/src/app/pages/targets/target-card/target-card.component.html62
-rw-r--r--webapp/src/app/pages/targets/target-card/target-card.component.scss41
-rw-r--r--webapp/src/app/pages/targets/target-card/target-card.component.ts87
-rw-r--r--webapp/src/app/pages/targets/targets.component.html26
-rw-r--r--webapp/src/app/pages/targets/targets.component.scss84
-rw-r--r--webapp/src/app/pages/targets/targets.component.ts60
-rw-r--r--webapp/src/app/pages/targets/targets.module.ts47
-rw-r--r--webapp/src/app/pages/targets/terminals/terminal.component.ts135
-rw-r--r--webapp/src/app/pages/targets/terminals/terminals.component.html32
-rw-r--r--webapp/src/app/pages/targets/terminals/terminals.component.scss84
-rw-r--r--webapp/src/app/pages/targets/terminals/terminals.component.ts115
18 files changed, 1070 insertions, 2 deletions
diff --git a/webapp/src/app/pages/pages-menu.ts b/webapp/src/app/pages/pages-menu.ts
index 1e4839d..86884bc 100644
--- a/webapp/src/app/pages/pages-menu.ts
+++ b/webapp/src/app/pages/pages-menu.ts
@@ -66,9 +66,18 @@ export const MENU_ITEMS: NbMenuItem[] = [
*/
},
{
- title: 'Boards',
+ title: 'Targets',
icon: 'fa fa-microchip',
+ link: '/pages/targets',
children: [
+ {
+ title: 'List',
+ link: '/pages/targets/list',
+ },
+ {
+ title: 'Terminal',
+ link: '/pages/targets/term',
+ },
],
},
{
diff --git a/webapp/src/app/pages/pages-routing.module.ts b/webapp/src/app/pages/pages-routing.module.ts
index 7eeccd0..655dea2 100644
--- a/webapp/src/app/pages/pages-routing.module.ts
+++ b/webapp/src/app/pages/pages-routing.module.ts
@@ -24,6 +24,8 @@ import { DashboardComponent } from './dashboard/dashboard.component';
import { ProjectsComponent } from './projects/projects.component';
import { SdksComponent } from './sdks/sdks.component';
import { SdkManagementComponent } from './sdks/sdk-management/sdk-management.component';
+import { TargetsComponent } from './targets/targets.component';
+import { TerminalsComponent } from './targets/terminals/terminals.component';
import { BuildComponent } from './build/build.component';
const routes: Routes = [{
@@ -45,6 +47,12 @@ const routes: Routes = [{
path: 'build',
component: BuildComponent,
}, {
+ path: 'targets/list',
+ component: TargetsComponent,
+ }, {
+ path: 'targets/term',
+ component: TerminalsComponent,
+ }, {
path: 'config',
loadChildren: './config/config.module#ConfigModule',
},
diff --git a/webapp/src/app/pages/pages.module.ts b/webapp/src/app/pages/pages.module.ts
index 42a9a84..55fe61a 100644
--- a/webapp/src/app/pages/pages.module.ts
+++ b/webapp/src/app/pages/pages.module.ts
@@ -26,6 +26,7 @@ import { DashboardModule } from './dashboard/dashboard.module';
import { BuildModule } from './build/build.module';
import { ProjectsModule } from './projects/projects.module';
import { SdksModule } from './sdks/sdks.module';
+import { TargetsModule } from './targets/targets.module';
import { PagesRoutingModule } from './pages-routing.module';
import { NotificationsComponent } from './notifications/notifications.component';
import { ThemeModule } from '../@theme/theme.module';
@@ -46,6 +47,7 @@ const PAGES_COMPONENTS = [
ProjectsModule,
SdksModule,
ToasterModule,
+ TargetsModule,
],
declarations: [
...PAGES_COMPONENTS,
diff --git a/webapp/src/app/pages/projects/projects.module.ts b/webapp/src/app/pages/projects/projects.module.ts
index 7c4b0a8..54255f8 100644
--- a/webapp/src/app/pages/projects/projects.module.ts
+++ b/webapp/src/app/pages/projects/projects.module.ts
@@ -23,7 +23,6 @@ import { ProjectsComponent } from './projects.component';
import { ProjectCardComponent, ProjectReadableTypePipe } from './project-card/project-card.component';
import { ProjectAddModalComponent } from './project-add-modal/project-add-modal.component';
-
@NgModule({
imports: [
ThemeModule,
diff --git a/webapp/src/app/pages/targets/settings/target-select-dropdown.component.ts b/webapp/src/app/pages/targets/settings/target-select-dropdown.component.ts
new file mode 100644
index 0000000..c124054
--- /dev/null
+++ b/webapp/src/app/pages/targets/settings/target-select-dropdown.component.ts
@@ -0,0 +1,57 @@
+/**
+* @license
+* Copyright (C) 2018 "IoT.bzh"
+* Author Sebastien Douheret <sebastien@iot.bzh>
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+import { Component, OnInit, Input } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+
+import { ITarget, TargetService } from '../../../@core-xds/services/target.service';
+
+@Component({
+ selector: 'xds-target-select-dropdown',
+ template: `
+ <div class="form-group row">
+ <label class="col-sm-3 form-control-label" style="margin-top: auto; margin-bottom: auto;">Target</label>
+ <div class="col-sm-9">
+ <select class="form-control" style="min-width: 10rem;" [(ngModel)]="curTgt" (click)="select()">
+ <option *ngFor="let tgt of targets$ | async" [ngValue]="tgt">{{ tgt.name }}</option>
+ </select>
+ </div>
+ </div>
+ `,
+})
+export class TargetSelectDropdownComponent implements OnInit {
+
+ targets$: Observable<ITarget[]>;
+ curTgt: ITarget;
+
+ constructor(private targetSvr: TargetService) { }
+
+ ngOnInit() {
+ this.curTgt = this.targetSvr.getCurrent();
+ this.targets$ = this.targetSvr.targets$;
+ this.targetSvr.curTarget$.subscribe(p => this.curTgt = p);
+ }
+
+ select() {
+ if (this.curTgt) {
+ this.targetSvr.setCurrentById(this.curTgt.id);
+ }
+ }
+}
+
+
diff --git a/webapp/src/app/pages/targets/target-add-modal/target-add-modal.component.html b/webapp/src/app/pages/targets/target-add-modal/target-add-modal.component.html
new file mode 100644
index 0000000..84424b4
--- /dev/null
+++ b/webapp/src/app/pages/targets/target-add-modal/target-add-modal.component.html
@@ -0,0 +1,46 @@
+<div class="modal-header">
+ <span>Add a new target</span>
+ <button class="close" aria-label="Close" (click)="closeModal()">
+ <span aria-hidden="true">&times;</span>
+ </button>
+</div>
+
+<div class="modal-body row">
+ <div class="col-12">
+ <form [formGroup]="addTargetForm" (ngSubmit)="onSubmit()">
+
+ <div class="form-group row">
+ <label for="sharing-type" class="col-sm-3 col-form-label">Target Type</label>
+ <div class="col-sm-9">
+ <select id="select-sharing-type" class="form-control" formControlName="type">
+ <option *ngFor="let t of targetTypes" [value]="t.value">{{t.display}}
+ </option>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label for="select-ip" class="col-sm-3 col-form-ip">IP or Name</label>
+ <div class="col-sm-9">
+ <input type="text" id="inputLabel" class="form-control" formControlName="ip" (keyup)="onKeyLabel($event)">
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <label for="select-name" class="col-sm-3 col-form-name">Name</label>
+ <div class="col-sm-9">
+ <input type="text" id="inputLabel" class="form-control" formControlName="name" (keyup)="onKeyLabel($event)">
+ </div>
+ </div>
+
+ </form>
+ </div>
+</div>
+<div class="modal-footer form-group">
+ <div class="col-12">
+ <div class="offset-sm-4 col-sm-6">
+ <button class="btn btn-md btn-secondary" (click)="cancelAction=true; closeModal()"> Cancel </button>
+ <button class="btn btn-md btn-primary" (click)="onSubmit()" [disabled]="!addTargetForm.valid">Add Folder</button>
+ </div>
+ </div>
+</div>
diff --git a/webapp/src/app/pages/targets/target-add-modal/target-add-modal.component.ts b/webapp/src/app/pages/targets/target-add-modal/target-add-modal.component.ts
new file mode 100644
index 0000000..fdcb048
--- /dev/null
+++ b/webapp/src/app/pages/targets/target-add-modal/target-add-modal.component.ts
@@ -0,0 +1,174 @@
+/**
+* @license
+* Copyright (C) 2018 "IoT.bzh"
+* Author Sebastien Douheret <sebastien@iot.bzh>
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+import { Component, ViewEncapsulation, Input, OnInit } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { FormControl, FormGroup, Validators, ValidationErrors, 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 '../../../@core-xds/services/alert.service';
+import { TargetService, ITarget, TargetType, TargetTypes } from '../../../@core-xds/services/target.service';
+import { XDSConfigService } from '../../../@core-xds/services/xds-config.service';
+
+
+@Component({
+ selector: 'xds-target-add-modal',
+ templateUrl: 'target-add-modal.component.html',
+ encapsulation: ViewEncapsulation.None,
+ styles: [`
+ .modal-xxl .modal-lg {
+ width: 90%;
+ max-width:1200px;
+ }
+ `],
+})
+export class TargetAddModalComponent implements OnInit {
+ // @Input('server-id') serverID: string;
+ private serverID: string;
+
+ cancelAction = false;
+ userEditedName = false;
+ targetTypes = Object.assign([], TargetTypes);
+
+ addTargetForm: FormGroup;
+ typeCtrl: FormControl;
+ ipCtrl: FormControl;
+
+ constructor(
+ private alert: AlertService,
+ private targetSvr: TargetService,
+ private XdsConfigSvr: XDSConfigService,
+ private fb: FormBuilder,
+ private activeModal: NgbActiveModal,
+ ) {
+ // Define types (first one is special/placeholder)
+ this.targetTypes.unshift({ value: TargetType.UNSET, display: '--Select a type--' });
+
+ // Select first type item (Standard) by default
+ this.typeCtrl = new FormControl(this.targetTypes[1].value, this.validatorTgtType.bind(this));
+ this.ipCtrl = new FormControl('', this.validatorIP.bind(this));
+
+ this.addTargetForm = fb.group({
+ type: this.typeCtrl,
+ ip: this.ipCtrl,
+ name: ['', Validators.nullValidator],
+ });
+ }
+
+ ngOnInit() {
+ // Update server ID
+ this.serverID = this.XdsConfigSvr.getCurServer().id;
+ this.XdsConfigSvr.onCurServer().subscribe(svr => this.serverID = svr.id);
+
+ // Auto create target name
+ this.ipCtrl.valueChanges
+ .debounceTime(100)
+ .filter(n => n)
+ .map(n => {
+ if (this._isIPstart(n)) {
+ return 'Target_' + n;
+ }
+// SEB PB
+ return n;
+ })
+ .subscribe(value => {
+ if (value && !this.userEditedName) {
+ this.addTargetForm.patchValue({ name: value });
+ }
+ });
+ }
+
+ closeModal() {
+ this.activeModal.close();
+ }
+
+ onKeyLabel(event: any) {
+ this.userEditedName = (this.addTargetForm.value.label !== '');
+ }
+
+ onChangeLocalTarget(e) {
+ }
+
+ onSubmit() {
+ if (this.cancelAction) {
+ return;
+ }
+
+ const formVal = this.addTargetForm.value;
+
+ this.targetSvr.add({
+ name: formVal['name'],
+ ip: formVal['ip'],
+ type: formVal['type'],
+ }).subscribe(
+ tgt => {
+ this.alert.info('Target ' + tgt.name + ' successfully created.');
+ this.closeModal();
+
+ // Reset Value for the next creation
+ this.addTargetForm.reset();
+ const selectedType = this.targetTypes[0].value;
+ this.addTargetForm.patchValue({ type: selectedType });
+
+ },
+ err => {
+ this.alert.error(err, 60);
+ this.closeModal();
+ },
+ );
+ }
+
+ private validatorTgtType(g: FormGroup): ValidationErrors | null {
+ return (g.value !== TargetType.UNSET) ? null : { validatorTgtType: { valid: false } };
+ }
+
+ private validatorIP(g: FormGroup): ValidationErrors | null {
+ const noValid = <ValidationErrors>{ validatorProjPath: { valid: false } };
+
+ if (g.value === '') {
+ return noValid;
+ }
+
+ if (this._isIPstart(g.value) && !this._isIPv4(g.value)) {
+ return noValid;
+ }
+
+ // Else accept any text / hostname
+ return null;
+ }
+
+ private _isIPstart(str) {
+ return /^(\d+)\./.test(str);
+ }
+
+ private _isIPv4(str) {
+ const ipv4Maybe = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
+ if (!ipv4Maybe.test(str)) {
+ return false;
+ }
+ const parts = str.split('.').sort(function (a, b) {
+ return a - b;
+ });
+ return (parts[3] <= 255);
+ }
+}
diff --git a/webapp/src/app/pages/targets/target-card/target-card.component.html b/webapp/src/app/pages/targets/target-card/target-card.component.html
new file mode 100644
index 0000000..7c921b1
--- /dev/null
+++ b/webapp/src/app/pages/targets/target-card/target-card.component.html
@@ -0,0 +1,62 @@
+<nb-card class="xds-targets">
+ <nb-card-header>
+
+ <div class="row">
+ <div class="col-12 col-md-8">
+ {{ target.name }}
+ </div>
+ <div class="col-6 col-md-4 text-right" role="group">
+ <button class="btn btn-outline-danger btn-tn btn-xds" (click)="delete(target)">
+ <span class="fa fa-trash fa-size-x2"></span>
+ </button>
+ </div>
+ </div>
+ </nb-card-header>
+
+ <nb-card-body>
+ <table class="table table-striped">
+ <tbody>
+ <tr>
+ <th>
+ <span class="fa fa-fw fa-id-badge"></span>&nbsp;
+ <span>Target ID</span>
+ </th>
+ <td>{{ target.id }}</td>
+ </tr>
+ <tr>
+ <th>
+ <span class="fa fa-fw fa-exchange"></span>&nbsp;
+ <span>Type</span>
+ </th>
+ <td>{{ target.type | readableType }}</td>
+ </tr>
+ <tr>
+ <th>
+ <span class="fa fa-fw fa-folder-open-o"></span>&nbsp;
+ <span>IP</span>
+ </th>
+ <td>{{ target.ip }}</td>
+ </tr>
+ <tr>
+ <th>
+ <span class="fa fa-fw fa-flag"></span>&nbsp;
+ <span>Status</span>
+ </th>
+ <td>{{ target.status }}</td>
+ </tr>
+ <tr>
+ <th>
+ <span class="fa fa-fw fa-folder-open-o"></span>&nbsp;
+ <span>Terminals</span>
+ </th>
+ <td>{{ target.terms.length }}</td>
+ </tr>
+
+ </tbody>
+ </table>
+ </nb-card-body>
+
+ <nb-card-footer>
+ <!-- <pre>{{target | json}}</pre> -->
+ </nb-card-footer>
+</nb-card>
diff --git a/webapp/src/app/pages/targets/target-card/target-card.component.scss b/webapp/src/app/pages/targets/target-card/target-card.component.scss
new file mode 100644
index 0000000..6ac8d11
--- /dev/null
+++ b/webapp/src/app/pages/targets/target-card/target-card.component.scss
@@ -0,0 +1,41 @@
+@import '~@nebular/theme/styles/global/bootstrap/buttons';
+
+.xds-project-card .icon {
+ padding: 0.75rem 0;
+ font-size: 1.75rem;
+}
+
+nb-card-body {
+ padding: 0;
+}
+
+nb-card-footer {
+ text-align: right;
+}
+
+.fa-size-x2 {
+ font-size: 20px;
+}
+
+th span {
+ font-weight: 100;
+}
+
+th label {
+ font-weight: 100;
+ margin-bottom: 0;
+}
+
+.btn-outline-danger.btn-xds {
+ color: #ff4c6a;
+ &:focus {
+ color: white;
+ }
+}
+
+.btn-outline-info.btn-xds {
+ color: #4ca6ff;
+ &:focus {
+ color: white;
+ }
+}
diff --git a/webapp/src/app/pages/targets/target-card/target-card.component.ts b/webapp/src/app/pages/targets/target-card/target-card.component.ts
new file mode 100644
index 0000000..6d43260
--- /dev/null
+++ b/webapp/src/app/pages/targets/target-card/target-card.component.ts
@@ -0,0 +1,87 @@
+/**
+* @license
+* Copyright (C) 2018 "IoT.bzh"
+* Author Sebastien Douheret <sebastien@iot.bzh>
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+import { Component, Input, Pipe, PipeTransform } from '@angular/core';
+import { TargetService, ITarget, TargetType, TargetTypeEnum, TargetTypes } from '../../../@core-xds/services/target.service';
+import { AlertService } from '../../../@core-xds/services/alert.service';
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
+import { ConfirmModalComponent, EType } from '../../confirm/confirm-modal/confirm-modal.component';
+import { find } from 'rxjs/operator/find';
+import { findIndex } from 'rxjs/operator/findIndex';
+
+@Component({
+ selector: 'xds-target-card',
+ styleUrls: ['./target-card.component.scss'],
+ templateUrl: './target-card.component.html',
+})
+export class TargetCardComponent {
+
+ // FIXME workaround of https://github.com/angular/angular-cli/issues/2034
+ // should be removed with angular 5
+ // @Input() target: ITarget;
+ @Input() target = <ITarget>null;
+
+ constructor(
+ private alert: AlertService,
+ private targetSvr: TargetService,
+ private modalService: NgbModal,
+ ) {
+ }
+
+ delete(tgt: ITarget) {
+
+ const modal = this.modalService.open(ConfirmModalComponent, {
+ size: 'lg',
+ backdrop: 'static',
+ container: 'nb-layout',
+ });
+ modal.componentInstance.title = 'Confirm SDK deletion';
+ modal.componentInstance.type = EType.YesNo;
+ modal.componentInstance.question = `
+ Do you <b>permanently delete '` + tgt.name + `'</b> target ?
+ <br><br>
+ <i><small>(Target ID: ` + tgt.id + ` )</small></i>`;
+
+ modal.result
+ .then(res => {
+ if (res === 'yes') {
+ this.targetSvr.delete(tgt).subscribe(
+ r => { },
+ err => this.alert.error('ERROR delete: ' + err),
+ );
+ }
+ });
+
+ }
+
+}
+
+// Make Target type human readable
+@Pipe({
+ name: 'readableType',
+})
+
+export class TargetReadableTypePipe implements PipeTransform {
+ transform(type: TargetTypeEnum): string {
+ const tt = TargetTypes.find(el => type === el.value);
+ if (tt) {
+ return tt.display;
+ }
+ return String(type);
+ }
+}
diff --git a/webapp/src/app/pages/targets/targets.component.html b/webapp/src/app/pages/targets/targets.component.html
new file mode 100644
index 0000000..a4fd894
--- /dev/null
+++ b/webapp/src/app/pages/targets/targets.component.html
@@ -0,0 +1,26 @@
+<div class="row">
+ <div class="col-12">
+ <nb-card-body>
+ <div class="col-9">
+ <nb-actions size="medium">
+ <nb-action>
+ <button (click)="add()">
+ <i class="nb-plus"></i>
+ <span>Add Target</span>
+ </button>
+ </nb-action>
+ </nb-actions>
+ </div>
+ <div class="col-3 right">
+ <nb-actions size="medium">
+ <nb-action>
+ <nb-search type="rotate-layout"></nb-search>
+ </nb-action>
+ </nb-actions>
+ </div>
+ </nb-card-body>
+ </div>
+ <div class="col-md-6 col-lg-6" *ngFor="let tgt of (targets$ | async)">
+ <xds-target-card [target]="tgt"></xds-target-card>
+ </div>
+</div>
diff --git a/webapp/src/app/pages/targets/targets.component.scss b/webapp/src/app/pages/targets/targets.component.scss
new file mode 100644
index 0000000..93ed0db
--- /dev/null
+++ b/webapp/src/app/pages/targets/targets.component.scss
@@ -0,0 +1,84 @@
+@import '~xterm/dist/xterm.css';
+@import '../../@theme/styles/themes';
+@import '~@nebular/theme/components/card/card.component.theme';
+@import '~bootstrap/scss/mixins/breakpoints';
+@import '~@nebular/theme/styles/global/bootstrap/breakpoints';
+@include nb-install-component() {
+ nb-card-body {
+ display: flex;
+ align-items: center;
+ }
+ .action-groups-header {
+ flex-basis: 20%;
+ color: nb-theme(card-header-fg-heading);
+ font-family: nb-theme(card-header-font-family);
+ font-size: nb-theme(card-header-font-size);
+ font-weight: nb-theme(card-header-font-weight);
+ }
+ .nb-actions {
+ flex-basis: 80%;
+ }
+ .right {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ order: 1;
+ flex-direction: row-reverse;
+ }
+ nb-actions > nb-action {
+ padding: 0;
+ }
+ nb-action {
+ i {
+ color: nb-theme(color-fg);
+ font-size: 2.5rem;
+ margin-right: 1rem;
+ }
+ span {
+ font-family: nb-theme(font-secondary);
+ font-weight: nb-theme(font-weight-bold);
+ color: nb-theme(color-fg-heading);
+ text-transform: uppercase;
+ }
+ button {
+ margin: 0 auto;
+ padding: 0;
+ cursor: pointer;
+ border: none;
+ background: none;
+ display: flex;
+ align-items: center;
+ &:focus {
+ box-shadow: none;
+ outline: none;
+ }
+ }
+ }
+ @include media-breakpoint-down(md) {
+ nb-actions nb-action {
+ padding: 0 0.75rem;
+ }
+ }
+ @include media-breakpoint-down(sm) {
+ nb-card-body {
+ padding: 1rem;
+ }
+ nb-action {
+ font-size: 0.75rem;
+ i {
+ font-size: 2rem;
+ margin-right: 0.5rem;
+ }
+ }
+ }
+ @include media-breakpoint-down(is) {
+ nb-action i {
+ font-size: 1.75rem;
+ margin: 0;
+ }
+ span {
+ display: none;
+ }
+ }
+}
diff --git a/webapp/src/app/pages/targets/targets.component.ts b/webapp/src/app/pages/targets/targets.component.ts
new file mode 100644
index 0000000..95abdea
--- /dev/null
+++ b/webapp/src/app/pages/targets/targets.component.ts
@@ -0,0 +1,60 @@
+/**
+* @license
+* Copyright (C) 2017-2018 "IoT.bzh"
+* Author Sebastien Douheret <sebastien@iot.bzh>
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+import { Component, OnInit } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { Subject } from 'rxjs/Subject';
+
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
+import { TargetAddModalComponent } from './target-add-modal/target-add-modal.component';
+
+import { TargetService, ITarget } from '../../@core-xds/services/target.service';
+import { AlertService } from '../../@core-xds/services/alert.service';
+
+@Component({
+ selector: 'xds-targets',
+ styleUrls: ['./targets.component.scss'],
+ templateUrl: './targets.component.html',
+})
+export class TargetsComponent implements OnInit {
+
+ public targets$: Observable<ITarget[]>;
+
+ protected curTargetID: string;
+
+ constructor(
+ private modalService: NgbModal,
+ private targetSvr: TargetService,
+ private alert: AlertService,
+ ) {
+ this.curTargetID = '';
+ }
+
+ ngOnInit() {
+ this.targets$ = this.targetSvr.targets$;
+ }
+
+ add() {
+ const activeModal = this.modalService.open(TargetAddModalComponent, {
+ size: 'lg',
+ windowClass: 'modal-xxl',
+ container: 'nb-layout',
+ });
+ }
+
+}
diff --git a/webapp/src/app/pages/targets/targets.module.ts b/webapp/src/app/pages/targets/targets.module.ts
new file mode 100644
index 0000000..8589bcd
--- /dev/null
+++ b/webapp/src/app/pages/targets/targets.module.ts
@@ -0,0 +1,47 @@
+/**
+* @license
+* Copyright (C) 2017-2018 "IoT.bzh"
+* Author Sebastien Douheret <sebastien@iot.bzh>
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+import { NgModule } from '@angular/core';
+import { ThemeModule } from '../../@theme/theme.module';
+
+import { TargetsComponent } from './targets.component';
+import { TerminalsComponent } from './terminals/terminals.component';
+import { TerminalComponent } from './terminals/terminal.component';
+import { TargetCardComponent, TargetReadableTypePipe } from './target-card/target-card.component';
+import { TargetAddModalComponent } from './target-add-modal/target-add-modal.component';
+import { TargetSelectDropdownComponent } from './settings/target-select-dropdown.component';
+
+
+@NgModule({
+ imports: [
+ ThemeModule,
+ ],
+ declarations: [
+ TargetsComponent,
+ TerminalsComponent,
+ TerminalComponent,
+ TargetCardComponent,
+ TargetAddModalComponent,
+ TargetReadableTypePipe,
+ TargetSelectDropdownComponent,
+ ],
+ entryComponents: [
+ TargetAddModalComponent,
+ ],
+})
+export class TargetsModule { }
diff --git a/webapp/src/app/pages/targets/terminals/terminal.component.ts b/webapp/src/app/pages/targets/terminals/terminal.component.ts
new file mode 100644
index 0000000..0478a08
--- /dev/null
+++ b/webapp/src/app/pages/targets/terminals/terminal.component.ts
@@ -0,0 +1,135 @@
+/**
+* @license
+* Copyright (C) 2018 "IoT.bzh"
+* Author Sebastien Douheret <sebastien@iot.bzh>
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+import { Component, ElementRef, ViewChild, Input, Output, HostListener, EventEmitter, AfterViewInit } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+
+import { Terminal } from 'xterm';
+import * as fit from 'xterm/lib/addons/fit/fit';
+
+export interface ITerminalFont {
+ fontFamily: string;
+ fontSize: string;
+ lineHeight: number;
+ charWidth: number;
+ charHeight: number;
+}
+
+
+@Component({
+ selector: 'xds-terminal',
+ styles: [],
+ template: `
+ <div #terminalContainer></div>
+ `,
+})
+export class TerminalComponent implements AfterViewInit {
+
+ private _xterm: Terminal;
+ private _initDone: boolean;
+
+ @ViewChild('terminalContainer') termContainer: ElementRef;
+
+ @Output() stdin = new EventEmitter<any>();
+ @Output() resize = new EventEmitter<{ cols: number, rows: number }>();
+
+
+ constructor() {
+ this._initDone = false;
+ Terminal.applyAddon(fit);
+
+ this._xterm = new Terminal({
+ cursorBlink: true,
+ // useStyle: true,
+ scrollback: 1000,
+ rows: 24,
+ cols: 80,
+ });
+ }
+
+ // getting the nativeElement only possible after view init
+ ngAfterViewInit() {
+
+ // this now finds the #terminal element
+ this._xterm.open(this.termContainer.nativeElement);
+
+ // the number of rows will determine the size of the terminal screen
+ (<any>this._xterm).fit();
+
+ // Bind input key
+ this._xterm.on('data', (data) => {
+ // console.log(data.charCodeAt(0));
+ this.stdin.emit(this._sanitizeInput(data));
+ return false;
+ });
+
+ this._initDone = true;
+ }
+
+ @Input('stdout')
+ set writeData(data) {
+ if (this._initDone && data !== undefined) {
+ this._xterm.write(data);
+ }
+ }
+
+ @Input('disable')
+ set disable(value: boolean) {
+ if (!this._initDone) {
+ return;
+ }
+
+ this._xterm.setOption('disableStdin', value);
+
+ if (value) {
+ this._xterm.blur();
+ } else {
+ this._xterm.focus();
+ }
+ this._resize();
+ }
+
+ @HostListener('window:resize', ['$event'])
+ onWindowResize(event) {
+ this._resize();
+ }
+
+ /*** Private functions ***/
+
+ private _sanitizeInput(d) {
+ // TODO sanitize ?
+ return d;
+ }
+
+ private _resize() {
+ const geom = fit.proposeGeometry(this._xterm);
+
+ // console.log('DEBUG cols ' + String(geom.cols) + ' rows ' + String(geom.rows));
+
+ if (geom.cols < 0 || geom.cols > 2000 || geom.rows < 0 || geom.rows > 2000) {
+ return;
+ }
+
+ // Update xterm size
+ this._xterm.resize(geom.cols, geom.rows);
+
+ // Send resize event to update remote terminal
+ this.resize.emit({ cols: geom.cols, rows: geom.rows });
+ }
+
+}
diff --git a/webapp/src/app/pages/targets/terminals/terminals.component.html b/webapp/src/app/pages/targets/terminals/terminals.component.html
new file mode 100644
index 0000000..8b78963
--- /dev/null
+++ b/webapp/src/app/pages/targets/terminals/terminals.component.html
@@ -0,0 +1,32 @@
+<div class="row">
+ <div class="col-12">
+ <nb-card-body>
+ <nb-actions size="medium">
+ <nb-action class="col-sm-6">
+ <xds-target-select-dropdown></xds-target-select-dropdown>
+ </nb-action>
+ <nb-action class="col-sm-3" [disabled]="curTarget==null">
+ <button (click)="openTerm()">
+ <i class="nb-layout-default"></i>
+ <span>Open Terminal</span>
+ </button>
+ </nb-action>
+ <nb-action class="col-sm-3" [disabled]="curTarget==null">
+ <button (click)="closeTerm()">
+ <i class="nb-close-circled"></i>
+ <span>Close Terminal</span>
+ </button>
+ </nb-action>
+ </nb-actions>
+ </nb-card-body>
+ </div>
+
+ <div class="col-12" *ngIf="!xTermDisable; else elseBlock">
+ <pre>Connected to {{curTarget?.name}}</pre>
+ </div>
+ <ng-template #elseBlock><pre> </pre></ng-template>
+
+ <div class="col-12">
+ <xds-terminal [(stdout)]="xTermStdout" (stdin)="onXTermData($event)" (resize)="onResize($event)" [disable]="xTermDisable"></xds-terminal>
+ </div>
+</div>
diff --git a/webapp/src/app/pages/targets/terminals/terminals.component.scss b/webapp/src/app/pages/targets/terminals/terminals.component.scss
new file mode 100644
index 0000000..3f12c78
--- /dev/null
+++ b/webapp/src/app/pages/targets/terminals/terminals.component.scss
@@ -0,0 +1,84 @@
+@import '~xterm/dist/xterm.css';
+@import '../../../@theme/styles/themes';
+@import '~@nebular/theme/components/card/card.component.theme';
+@import '~bootstrap/scss/mixins/breakpoints';
+@import '~@nebular/theme/styles/global/bootstrap/breakpoints';
+@include nb-install-component() {
+ nb-card-body {
+ display: flex;
+ align-items: center;
+ }
+ .action-groups-header {
+ flex-basis: 20%;
+ color: nb-theme(card-header-fg-heading);
+ font-family: nb-theme(card-header-font-family);
+ font-size: nb-theme(card-header-font-size);
+ font-weight: nb-theme(card-header-font-weight);
+ }
+ .nb-actions {
+ flex-basis: 80%;
+ }
+ .right {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ order: 1;
+ flex-direction: row-reverse;
+ }
+ nb-actions > nb-action {
+ padding: 0;
+ }
+ nb-action {
+ i {
+ color: nb-theme(color-fg);
+ font-size: 2.5rem;
+ margin-right: 1rem;
+ }
+ span {
+ font-family: nb-theme(font-secondary);
+ font-weight: nb-theme(font-weight-bold);
+ color: nb-theme(color-fg-heading);
+ text-transform: uppercase;
+ }
+ button {
+ margin: 0 auto;
+ padding: 0;
+ cursor: pointer;
+ border: none;
+ background: none;
+ display: flex;
+ align-items: center;
+ &:focus {
+ box-shadow: none;
+ outline: none;
+ }
+ }
+ }
+ @include media-breakpoint-down(md) {
+ nb-actions nb-action {
+ padding: 0 0.75rem;
+ }
+ }
+ @include media-breakpoint-down(sm) {
+ nb-card-body {
+ padding: 1rem;
+ }
+ nb-action {
+ font-size: 0.75rem;
+ i {
+ font-size: 2rem;
+ margin-right: 0.5rem;
+ }
+ }
+ }
+ @include media-breakpoint-down(is) {
+ nb-action i {
+ font-size: 1.75rem;
+ margin: 0;
+ }
+ span {
+ display: none;
+ }
+ }
+}
diff --git a/webapp/src/app/pages/targets/terminals/terminals.component.ts b/webapp/src/app/pages/targets/terminals/terminals.component.ts
new file mode 100644
index 0000000..306c759
--- /dev/null
+++ b/webapp/src/app/pages/targets/terminals/terminals.component.ts
@@ -0,0 +1,115 @@
+/**
+* @license
+* Copyright (C) 2017-2018 "IoT.bzh"
+* Author Sebastien Douheret <sebastien@iot.bzh>
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+import { Component, OnInit } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { Subject } from 'rxjs/Subject';
+import { Subscription } from 'rxjs/Subscription';
+
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { TargetService, TargetType, ITarget, ITerminal, TerminalType, ITerminalOutput } from '../../../@core-xds/services/target.service';
+import { AlertService } from '../../../@core-xds/services/alert.service';
+
+@Component({
+ selector: 'xds-terminals',
+ styleUrls: ['./terminals.component.scss'],
+ templateUrl: './terminals.component.html',
+})
+export class TerminalsComponent implements OnInit {
+
+ public curTarget: ITarget;
+ public xTermStdout: string;
+ public xTermDisable: boolean;
+
+ protected curTermID: string;
+
+ private termOut$: Subject<ITerminalOutput>;
+ private termSubs: Subscription;
+
+ constructor(
+ private modalService: NgbModal,
+ private targetSvr: TargetService,
+ private alert: AlertService,
+ ) {
+ this.xTermStdout = '';
+ this.xTermDisable = true;
+ this.curTarget = null;
+ this.curTermID = '';
+ }
+
+ ngOnInit() {
+ this.targetSvr.curTarget$.subscribe(p => this.curTarget = p);
+ }
+
+ openTerm() {
+ if (this.curTarget == null || this.curTarget.id === '') {
+ return;
+ }
+
+ // FIXME: don't always use 1st terminal
+ if (this.curTarget.terms.length > 0) {
+ this.curTermID = this.curTarget.terms[0].id;
+ }
+
+ this.targetSvr.terminalOpen(this.curTarget.id, this.curTermID)
+ .subscribe(
+ res => {
+ this.termOut$ = this.targetSvr.terminalOutput$;
+
+ this.termSubs = this.termOut$.subscribe(termOut => {
+ this.xTermStdout = termOut.stdout + termOut.stderr;
+ });
+
+ this.xTermDisable = false;
+ },
+ err => {
+ this.alert.error(err);
+ },
+ );
+ }
+
+ closeTerm() {
+ if (this.curTarget == null || this.curTarget.id === '' || this.curTermID === '') {
+ return;
+ }
+ this.targetSvr.terminalClose(this.curTarget.id, this.curTermID)
+ .subscribe(res => {
+ this.curTermID = '';
+ this.xTermStdout = '\r\n*** Terminal closed ***\n\n\r';
+ if (this.termSubs !== undefined) {
+ this.termSubs.unsubscribe();
+ this.termOut$ = undefined;
+ }
+ this.xTermDisable = true;
+ });
+ }
+
+ onXTermData(data: string) {
+ if (this.termOut$ !== undefined && !this.termOut$.closed) {
+ this.targetSvr.terminalWrite(data);
+ }
+ }
+
+ onResize(sz) {
+ if (this.termOut$ !== undefined && !this.termOut$.closed) {
+ this.targetSvr.terminalResize(this.curTarget.id, this.curTermID, sz.cols, sz.rows).subscribe();
+ }
+ }
+
+}