Added target and terminal support in Dashboard
[src/xds/xds-agent.git] / webapp / src / app / @core-xds / services / target.service.ts
diff --git a/webapp/src/app/@core-xds/services/target.service.ts b/webapp/src/app/@core-xds/services/target.service.ts
new file mode 100644 (file)
index 0000000..9c995ea
--- /dev/null
@@ -0,0 +1,285 @@
+/**
+* @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 { Injectable, SecurityContext, isDevMode } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { Subject } from 'rxjs/Subject';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+
+import { XDSAgentService, IXDSTargetConfig, IXDSTargetTerminal } from '../services/xdsagent.service';
+
+/* FIXME: syntax only compatible with TS>2.4.0
+export enum TargetTypeEnum {
+    UNSET = '',
+    STANDARD: 'standard',
+}
+*/
+export type TargetTypeEnum = '' | 'standard';
+export const TargetType = {
+  UNSET: <TargetTypeEnum>'',
+  STANDARD: <TargetTypeEnum>'standard',
+};
+
+export const TargetTypes = [
+  { value: TargetType.STANDARD, display: 'Standard' },
+];
+
+export const TargetStatus = {
+  ErrorConfig: 'ErrorConfig',
+  Disable: 'Disable',
+  Enable: 'Enable',
+};
+
+export type TerminalTypeEnum = '' | 'ssh';
+export const TerminalType = {
+  UNSET: <TerminalTypeEnum>'',
+  SSH: <TerminalTypeEnum>'ssh',
+};
+
+export interface ITarget extends IXDSTargetConfig {
+  isUsable?: boolean;
+}
+
+export interface ITerminal extends IXDSTargetTerminal {
+  targetID?: string;
+}
+
+export interface ITerminalOutput {
+  termID: string;
+  timestamp: string;
+  stdout: string;
+  stderr: string;
+}
+
+export interface ITerminalExit {
+  termID: string;
+  timestamp: string;
+  code: number;
+  error: string;
+}
+
+@Injectable()
+export class TargetService {
+  public targets$: Observable<ITarget[]>;
+  public curTarget$: Observable<ITarget>;
+  public terminalOutput$ = <Subject<ITerminalOutput>>new Subject();
+  public terminalExit$ = <Subject<ITerminalExit>>new Subject();
+
+  private _tgtsList: ITarget[] = [];
+  private tgtsSubject = <BehaviorSubject<ITarget[]>>new BehaviorSubject(this._tgtsList);
+  private _current: ITarget;
+  private curTgtSubject = <BehaviorSubject<ITarget>>new BehaviorSubject(this._current);
+  private curServerID;
+  private termSocket: SocketIOClient.Socket;
+
+  constructor(private xdsSvr: XDSAgentService) {
+    this._current = null;
+    this.targets$ = this.tgtsSubject.asObservable();
+    this.curTarget$ = this.curTgtSubject.asObservable();
+
+    this.xdsSvr.XdsConfig$.subscribe(cfg => {
+      if (!cfg || cfg.servers.length < 1) {
+        return;
+      }
+
+      // FIXME support multiple server
+      this.curServerID = cfg.servers[0].id;
+
+      // Load initial targets list
+      this.xdsSvr.getTargets(this.curServerID).subscribe((targets) => {
+        this._tgtsList = [];
+        targets.forEach(p => {
+          this._addTarget(p, true);
+        });
+
+        // TODO: get previous val from xds-config service / cookie
+        if (this._tgtsList.length > 0) {
+          this._current = this._tgtsList[0];
+          this.curTgtSubject.next(this._current);
+        }
+
+        this.tgtsSubject.next(this._tgtsList);
+      });
+    });
+
+    // Add listener on targets creation, deletion and change events
+    this.xdsSvr.onTargetAdd().subscribe(tgt => this._addTarget(tgt));
+    this.xdsSvr.onTargetDelete().subscribe(tgt => this._delTarget(tgt));
+    this.xdsSvr.onTargetChange().subscribe(tgt => this._updateTarget(tgt));
+
+    // Register events to forward terminal Output and Exit
+    this.xdsSvr.onSocketConnect().subscribe(socket => {
+      this.termSocket = socket;
+
+      // Handle terminal output
+      socket.on('term:output', data => {
+        const termOut = <ITerminalOutput>{
+          termID: data.termID,
+          timestamp: data.timestamp,
+          stdout: atob(data.stdout),
+          stderr: atob(data.stderr),
+        };
+        this.terminalOutput$.next(termOut);
+      });
+
+      // Handle terminal exit event
+      socket.on('term:exit', data => {
+        this.terminalExit$.next(Object.assign({}, <ITerminalExit>data));
+      });
+
+    });
+  }
+
+  setCurrent(p: ITarget): ITarget | undefined {
+    if (!p) {
+      this._current = null;
+      return undefined;
+    }
+    return this.setCurrentById(p.id);
+  }
+
+  setCurrentById(id: string): ITarget | undefined {
+    const p = this._tgtsList.find(item => item.id === id);
+    if (p) {
+      this._current = p;
+      this.curTgtSubject.next(this._current);
+    }
+    return this._current;
+  }
+
+  getCurrent(): ITarget {
+    return this._current;
+  }
+
+  getTargetById(id: string): ITarget | undefined {
+    const t = this._tgtsList.find(item => item.id === id);
+    return t;
+  }
+
+  add(tgt: ITarget): Observable<ITarget> {
+    return this.xdsSvr.addTarget(this.curServerID, tgt);
+  }
+
+  delete(tgt: ITarget): Observable<ITarget> {
+    const idx = this._getTargetIdx(tgt.id);
+    const delTgt = tgt;
+    if (idx === -1) {
+      throw new Error('Invalid target id (id=' + tgt.id + ')');
+    }
+    return this.xdsSvr.deleteTarget(this.curServerID, tgt.id)
+      .map(res => delTgt);
+  }
+
+  setSettings(tgt: ITarget): Observable<ITarget> {
+    return this.xdsSvr.updateTarget(this.curServerID, tgt);
+  }
+
+  terminalOpen(tgtID: string, termID: string, cfg?: IXDSTargetTerminal): Observable<IXDSTargetTerminal> {
+    if (termID === '' || termID === undefined) {
+      // create a new terminal when no termID is set
+      if (cfg === undefined) {
+        cfg = <IXDSTargetTerminal>{
+          name: 'ssh to ' + this.getTargetById(tgtID).name,
+          type: TerminalType.SSH,
+        };
+      }
+      return this.xdsSvr.createTerminalTarget(this.curServerID, tgtID, cfg)
+        .flatMap(res => {
+          return this.xdsSvr.openTerminalTarget(this.curServerID, tgtID, res.id);
+        });
+    } else {
+      return this.xdsSvr.openTerminalTarget(this.curServerID, tgtID, termID);
+    }
+  }
+
+  terminalClose(tgtID, termID: string): Observable<IXDSTargetTerminal> {
+    return this.xdsSvr.closeTerminalTarget(this.curServerID, tgtID, termID);
+  }
+
+  terminalWrite(data: string) {
+    if (this.termSocket) {
+      this.termSocket.emit('term:input', btoa(data));
+    }
+  }
+
+  terminalResize(tgtID, termID: string, cols, rows: number): Observable<IXDSTargetTerminal> {
+    return this.xdsSvr.resizeTerminalTarget(this.curServerID, tgtID, termID, cols, rows);
+  }
+
+  /***  Private functions  ***/
+
+  private _isUsableTarget(p) {
+    return p && (p.status === TargetStatus.Enable);
+  }
+
+  private _getTargetIdx(id: string): number {
+    return this._tgtsList.findIndex((item) => item.id === id);
+  }
+
+  private _addTarget(tgt: ITarget, noNext?: boolean): ITarget {
+
+    tgt.isUsable = this._isUsableTarget(tgt);
+
+    // add new target
+    this._tgtsList.push(tgt);
+
+    // sort target array
+    this._tgtsList.sort((a, b) => {
+      if (a.name < b.name) {
+        return -1;
+      }
+      if (a.name > b.name) {
+        return 1;
+      }
+      return 0;
+    });
+
+    if (!noNext) {
+      this.tgtsSubject.next(this._tgtsList);
+    }
+
+    return tgt;
+  }
+
+  private _delTarget(tgt: ITarget) {
+    const idx = this._tgtsList.findIndex(item => item.id === tgt.id);
+    if (idx === -1) {
+      if (isDevMode) {
+        /* tslint:disable:no-console */
+        console.log('Warning: Try to delete target unknown id: tgt=', tgt);
+      }
+      return;
+    }
+    const delId = this._tgtsList[idx].id;
+    this._tgtsList.splice(idx, 1);
+    if (delId === this._current.id) {
+      this.setCurrent(this._tgtsList[0]);
+    }
+    this.tgtsSubject.next(this._tgtsList);
+  }
+
+  private _updateTarget(tgt: ITarget) {
+    const i = this._getTargetIdx(tgt.id);
+    if (i >= 0) {
+      this._tgtsList[i].status = tgt.status;
+      this._tgtsList[i].isUsable = this._isUsableTarget(tgt);
+      this.tgtsSubject.next(this._tgtsList);
+    }
+  }
+
+}