New dashboard look & feel
[src/xds/xds-agent.git] / webapp / src / app / @core-xds / services / xdsagent.service.ts
1 import { Injectable, Inject } from '@angular/core';
2 import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
3 import { DOCUMENT } from '@angular/common';
4 import { Observable } from 'rxjs/Observable';
5 import { Subject } from 'rxjs/Subject';
6 import { BehaviorSubject } from 'rxjs/BehaviorSubject';
7 import * as io from 'socket.io-client';
8
9 import { AlertService } from './alert.service';
10 import { ISdk } from './sdk.service';
11 import { ProjectType, ProjectTypeEnum } from './project.service';
12
13 // Import RxJs required methods
14 import 'rxjs/add/operator/map';
15 import 'rxjs/add/operator/catch';
16 import 'rxjs/add/observable/throw';
17 import 'rxjs/add/operator/mergeMap';
18 import 'rxjs/add/observable/of';
19 import 'rxjs/add/operator/retryWhen';
20
21
22 export interface IXDSConfigProject {
23   id: string;
24   path: string;
25   clientSyncThingID: string;
26   type: string;
27   label?: string;
28   defaultSdkID?: string;
29 }
30
31 interface IXDSBuilderConfig {
32   ip: string;
33   port: string;
34   syncThingID: string;
35 }
36
37 export interface IXDSProjectConfig {
38   id: string;
39   serverId: string;
40   label: string;
41   clientPath: string;
42   serverPath?: string;
43   type: ProjectTypeEnum;
44   status?: string;
45   isInSync?: boolean;
46   defaultSdkID: string;
47 }
48
49 export interface IXDSVer {
50   id: string;
51   version: string;
52   apiVersion: string;
53   gitTag: string;
54 }
55
56 export interface IXDSVersions {
57   client: IXDSVer;
58   servers: IXDSVer[];
59 }
60
61 export interface IXDServerCfg {
62   id: string;
63   url: string;
64   apiUrl?: string;
65   partialUrl?: string;
66   connRetry: number;
67   connected: boolean;
68 }
69
70 export interface IXDSConfig {
71   servers: IXDServerCfg[];
72 }
73
74 export interface ISdkMessage {
75   wsID: string;
76   msgType: string;
77   data: any;
78 }
79
80 export interface ICmdOutput {
81   cmdID: string;
82   timestamp: string;
83   stdout: string;
84   stderr: string;
85 }
86
87 export interface ICmdExit {
88   cmdID: string;
89   timestamp: string;
90   code: number;
91   error: string;
92 }
93
94 export interface IServerStatus {
95   id: string;
96   connected: boolean;
97 }
98
99 export interface IAgentStatus {
100   connected: boolean;
101   servers: IServerStatus[];
102 }
103
104
105 @Injectable()
106 export class XDSAgentService {
107
108   public XdsConfig$: Observable<IXDSConfig>;
109   public Status$: Observable<IAgentStatus>;
110   public ProjectState$ = <Subject<IXDSProjectConfig>>new Subject();
111   public CmdOutput$ = <Subject<ICmdOutput>>new Subject();
112   public CmdExit$ = <Subject<ICmdExit>>new Subject();
113
114   private baseUrl: string;
115   private wsUrl: string;
116   private _config = <IXDSConfig>{ servers: [] };
117   private _status = { connected: false, servers: [] };
118
119   private configSubject = <BehaviorSubject<IXDSConfig>>new BehaviorSubject(this._config);
120   private statusSubject = <BehaviorSubject<IAgentStatus>>new BehaviorSubject(this._status);
121
122   private socket: SocketIOClient.Socket;
123
124   constructor( @Inject(DOCUMENT) private document: Document,
125     private http: HttpClient, private alert: AlertService) {
126
127     this.XdsConfig$ = this.configSubject.asObservable();
128     this.Status$ = this.statusSubject.asObservable();
129
130     const originUrl = this.document.location.origin;
131     this.baseUrl = originUrl + '/api/v1';
132
133     const re = originUrl.match(/http[s]?:\/\/([^\/]*)[\/]?/);
134     if (re === null || re.length < 2) {
135       console.error('ERROR: cannot determine Websocket url');
136     } else {
137       this.wsUrl = 'ws://' + re[1];
138       this._handleIoSocket();
139       this._RegisterEvents();
140     }
141   }
142
143   private _NotifyXdsAgentState(sts: boolean) {
144     this._status.connected = sts;
145     this.statusSubject.next(Object.assign({}, this._status));
146
147     // Update XDS config including XDS Server list when connected
148     if (sts) {
149       this.getConfig().subscribe(c => {
150         this._config = c;
151         this._NotifyXdsServerState();
152         this.configSubject.next(Object.assign({ servers: [] }, this._config));
153       });
154     }
155   }
156
157   private _NotifyXdsServerState() {
158     this._status.servers = this._config.servers.map(svr => {
159       return { id: svr.id, connected: svr.connected };
160     });
161     this.statusSubject.next(Object.assign({}, this._status));
162   }
163
164   private _handleIoSocket() {
165     this.socket = io(this.wsUrl, { transports: ['websocket'] });
166
167     this.socket.on('connect_error', (res) => {
168       this._NotifyXdsAgentState(false);
169       console.error('XDS Agent WebSocket Connection error !');
170     });
171
172     this.socket.on('connect', (res) => {
173       this._NotifyXdsAgentState(true);
174     });
175
176     this.socket.on('disconnection', (res) => {
177       this._NotifyXdsAgentState(false);
178       this.alert.error('WS disconnection: ' + res);
179     });
180
181     this.socket.on('error', (err) => {
182       console.error('WS error:', err);
183     });
184
185     this.socket.on('make:output', data => {
186       this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data));
187     });
188
189     this.socket.on('make:exit', data => {
190       this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
191     });
192
193     this.socket.on('exec:output', data => {
194       this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data));
195     });
196
197     this.socket.on('exec:exit', data => {
198       this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
199     });
200
201     // Events
202     // (project-add and project-delete events are managed by project.service)
203     this.socket.on('event:server-config', ev => {
204       if (ev && ev.data) {
205         const cfg: IXDServerCfg = ev.data;
206         const idx = this._config.servers.findIndex(el => el.id === cfg.id);
207         if (idx >= 0) {
208           this._config.servers[idx] = Object.assign({}, cfg);
209           this._NotifyXdsServerState();
210         }
211         this.configSubject.next(Object.assign({}, this._config));
212       }
213     });
214
215     this.socket.on('event:project-state-change', ev => {
216       if (ev && ev.data) {
217         this.ProjectState$.next(Object.assign({}, ev.data));
218       }
219     });
220
221   }
222
223   /**
224   ** Events
225   ***/
226   addEventListener(ev: string, fn: Function): SocketIOClient.Emitter {
227     return this.socket.addEventListener(ev, fn);
228   }
229
230   /**
231   ** Misc / Version
232   ***/
233   getVersion(): Observable<IXDSVersions> {
234     return this._get('/version');
235   }
236
237   /***
238   ** Config
239   ***/
240   getConfig(): Observable<IXDSConfig> {
241     return this._get('/config');
242   }
243
244   setConfig(cfg: IXDSConfig): Observable<IXDSConfig> {
245     return this._post('/config', cfg);
246   }
247
248   setServerRetry(serverID: string, retry: number): Observable<IXDSConfig> {
249     const svr = this._getServer(serverID);
250     if (!svr) {
251       return Observable.throw('Unknown server ID');
252     }
253     if (retry < 0 || Number.isNaN(retry) || retry == null) {
254       return Observable.throw('Not a valid number');
255     }
256     svr.connRetry = retry;
257     return this._setConfig();
258   }
259
260   setServerUrl(serverID: string, url: string, retry: number): Observable<IXDSConfig> {
261     const svr = this._getServer(serverID);
262     if (!svr) {
263       return Observable.throw('Unknown server ID');
264     }
265     svr.connected = false;
266     svr.url = url;
267     if (!Number.isNaN(retry) && retry > 0) {
268       svr.connRetry = retry;
269     }
270     this._NotifyXdsServerState();
271     return this._setConfig();
272   }
273
274   private _setConfig(): Observable<IXDSConfig> {
275     return this.setConfig(this._config)
276       .map(newCfg => {
277         this._config = newCfg;
278         this.configSubject.next(Object.assign({}, this._config));
279         return this._config;
280       });
281   }
282
283   /***
284   ** SDKs
285   ***/
286   getSdks(serverID: string): Observable<ISdk[]> {
287     const svr = this._getServer(serverID);
288     if (!svr || !svr.connected) {
289       return Observable.of([]);
290     }
291
292     return this._get(svr.partialUrl + '/sdks');
293   }
294
295   /***
296   ** Projects
297   ***/
298   getProjects(): Observable<IXDSProjectConfig[]> {
299     return this._get('/projects');
300   }
301
302   addProject(cfg: IXDSProjectConfig): Observable<IXDSProjectConfig> {
303     return this._post('/projects', cfg);
304   }
305
306   deleteProject(id: string): Observable<IXDSProjectConfig> {
307     return this._delete('/projects/' + id);
308   }
309
310   syncProject(id: string): Observable<string> {
311     return this._post('/projects/sync/' + id, {});
312   }
313
314   /***
315   ** Exec
316   ***/
317   exec(prjID: string, dir: string, cmd: string, sdkid?: string, args?: string[], env?: string[]): Observable<any> {
318     return this._post('/exec',
319       {
320         id: prjID,
321         rpath: dir,
322         cmd: cmd,
323         sdkID: sdkid || '',
324         args: args || [],
325         env: env || [],
326       });
327   }
328
329   /**
330   ** Private functions
331   ***/
332
333   private _RegisterEvents() {
334     // Register to all existing events
335     this._post('/events/register', { 'name': 'event:all' })
336       .subscribe(
337       res => { },
338       error => {
339         this.alert.error('ERROR while registering to all events: ' + error);
340       }
341       );
342   }
343
344   private _getServer(ID: string): IXDServerCfg {
345     const svr = this._config.servers.filter(item => item.id === ID);
346     if (svr.length < 1) {
347       return null;
348     }
349     return svr[0];
350   }
351
352   private _attachAuthHeaders(options?: any) {
353     options = options || {};
354     const headers = options.headers || new HttpHeaders();
355     // headers.append('Authorization', 'Basic ' + btoa('username:password'));
356     headers.append('Accept', 'application/json');
357     headers.append('Content-Type', 'application/json');
358     // headers.append('Access-Control-Allow-Origin', '*');
359
360     options.headers = headers;
361     return options;
362   }
363
364   private _get(url: string): Observable<any> {
365     return this.http.get(this.baseUrl + url, this._attachAuthHeaders())
366       .catch(this._decodeError);
367   }
368   private _post(url: string, body: any): Observable<any> {
369     return this.http.post(this.baseUrl + url, JSON.stringify(body), this._attachAuthHeaders())
370       .catch((error) => {
371         return this._decodeError(error);
372       });
373   }
374   private _delete(url: string): Observable<any> {
375     return this.http.delete(this.baseUrl + url, this._attachAuthHeaders())
376       .catch(this._decodeError);
377   }
378
379   private _decodeError(err: any) {
380     let e: string;
381     if (err instanceof HttpErrorResponse) {
382       e = (err.error && err.error.error) ? err.error.error : err.message || 'Unknown error';
383     } else if (typeof err === 'object') {
384       if (err.statusText) {
385         e = err.statusText;
386       } else if (err.error) {
387         e = String(err.error);
388       } else {
389         e = JSON.stringify(err);
390       }
391     } else {
392       e = err.message ? err.message : err.toString();
393     }
394     console.log('xdsagent.service - ERROR: ', e);
395     return Observable.throw(e);
396   }
397 }