diff options
Diffstat (limited to 'webapp/src/app/pages/targets')
14 files changed, 1050 insertions, 0 deletions
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">×</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> + <span>Target ID</span> + </th> + <td>{{ target.id }}</td> + </tr> + <tr> + <th> + <span class="fa fa-fw fa-exchange"></span> + <span>Type</span> + </th> + <td>{{ target.type | readableType }}</td> + </tr> + <tr> + <th> + <span class="fa fa-fw fa-folder-open-o"></span> + <span>IP</span> + </th> + <td>{{ target.ip }}</td> + </tr> + <tr> + <th> + <span class="fa fa-fw fa-flag"></span> + <span>Status</span> + </th> + <td>{{ target.status }}</td> + </tr> + <tr> + <th> + <span class="fa fa-fw fa-folder-open-o"></span> + <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(); + } + } + +} |