diff options
Diffstat (limited to 'webapp/src/app/pages/targets/terminals')
4 files changed, 366 insertions, 0 deletions
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(); + } + } + +} |