Fixed webapp build and error message.
[src/xds/xds-agent.git] / webapp / src / app / 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 } 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: ProjectType;
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 IAgentStatus {
95     WS_connected: boolean;
96 }
97
98
99 @Injectable()
100 export class XDSAgentService {
101
102     public XdsConfig$: Observable<IXDSConfig>;
103     public Status$: Observable<IAgentStatus>;
104     public ProjectState$ = <Subject<IXDSProjectConfig>>new Subject();
105     public CmdOutput$ = <Subject<ICmdOutput>>new Subject();
106     public CmdExit$ = <Subject<ICmdExit>>new Subject();
107
108     private baseUrl: string;
109     private wsUrl: string;
110     private _config = <IXDSConfig>{ servers: [] };
111     private _status = { WS_connected: false };
112
113     private configSubject = <BehaviorSubject<IXDSConfig>>new BehaviorSubject(this._config);
114     private statusSubject = <BehaviorSubject<IAgentStatus>>new BehaviorSubject(this._status);
115
116     private socket: SocketIOClient.Socket;
117
118     constructor( @Inject(DOCUMENT) private document: Document,
119         private http: HttpClient, private alert: AlertService) {
120
121         this.XdsConfig$ = this.configSubject.asObservable();
122         this.Status$ = this.statusSubject.asObservable();
123
124         const originUrl = this.document.location.origin;
125         this.baseUrl = originUrl + '/api/v1';
126
127         const re = originUrl.match(/http[s]?:\/\/([^\/]*)[\/]?/);
128         if (re === null || re.length < 2) {
129             console.error('ERROR: cannot determine Websocket url');
130         } else {
131             this.wsUrl = 'ws://' + re[1];
132             this._handleIoSocket();
133             this._RegisterEvents();
134         }
135     }
136
137     private _WSState(sts: boolean) {
138         this._status.WS_connected = sts;
139         this.statusSubject.next(Object.assign({}, this._status));
140
141         // Update XDS config including XDS Server list when connected
142         if (sts) {
143             this.getConfig().subscribe(c => {
144                 this._config = c;
145                 this.configSubject.next(
146                     Object.assign({ servers: [] }, this._config)
147                 );
148             });
149         }
150     }
151
152     private _handleIoSocket() {
153         this.socket = io(this.wsUrl, { transports: ['websocket'] });
154
155         this.socket.on('connect_error', (res) => {
156             this._WSState(false);
157             console.error('XDS Agent WebSocket Connection error !');
158         });
159
160         this.socket.on('connect', (res) => {
161             this._WSState(true);
162         });
163
164         this.socket.on('disconnection', (res) => {
165             this._WSState(false);
166             this.alert.error('WS disconnection: ' + res);
167         });
168
169         this.socket.on('error', (err) => {
170             console.error('WS error:', err);
171         });
172
173         this.socket.on('make:output', data => {
174             this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data));
175         });
176
177         this.socket.on('make:exit', data => {
178             this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
179         });
180
181         this.socket.on('exec:output', data => {
182             this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data));
183         });
184
185         this.socket.on('exec:exit', data => {
186             this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
187         });
188
189         // Events
190         // (project-add and project-delete events are managed by project.service)
191         this.socket.on('event:server-config', ev => {
192             if (ev && ev.data) {
193                 const cfg: IXDServerCfg = ev.data;
194                 const idx = this._config.servers.findIndex(el => el.id === cfg.id);
195                 if (idx >= 0) {
196                     this._config.servers[idx] = Object.assign({}, cfg);
197                 }
198                 this.configSubject.next(Object.assign({}, this._config));
199             }
200         });
201
202         this.socket.on('event:project-state-change', ev => {
203             if (ev && ev.data) {
204                 this.ProjectState$.next(Object.assign({}, ev.data));
205             }
206         });
207
208     }
209
210     /**
211     ** Events
212     ***/
213     addEventListener(ev: string, fn: Function): SocketIOClient.Emitter {
214         return this.socket.addEventListener(ev, fn);
215     }
216
217     /**
218     ** Misc / Version
219     ***/
220     getVersion(): Observable<IXDSVersions> {
221         return this._get('/version');
222     }
223
224     /***
225     ** Config
226     ***/
227     getConfig(): Observable<IXDSConfig> {
228         return this._get('/config');
229     }
230
231     setConfig(cfg: IXDSConfig): Observable<IXDSConfig> {
232         return this._post('/config', cfg);
233     }
234
235     setServerRetry(serverID: string, r: number) {
236         const svr = this._getServer(serverID);
237         if (!svr) {
238             return Observable.of([]);
239         }
240
241         svr.connRetry = r;
242         this.setConfig(this._config).subscribe(
243             newCfg => {
244                 this._config = newCfg;
245                 this.configSubject.next(Object.assign({}, this._config));
246             },
247             err => {
248                 this.alert.error(err);
249             }
250         );
251     }
252
253     setServerUrl(serverID: string, url: string) {
254         const svr = this._getServer(serverID);
255         if (!svr) {
256             return Observable.of([]);
257         }
258         svr.url = url;
259         this.setConfig(this._config).subscribe(
260             newCfg => {
261                 this._config = newCfg;
262                 this.configSubject.next(Object.assign({}, this._config));
263             },
264             err => {
265                 this.alert.error(err);
266             }
267         );
268     }
269
270     /***
271     ** SDKs
272     ***/
273     getSdks(serverID: string): Observable<ISdk[]> {
274         const svr = this._getServer(serverID);
275         if (!svr || !svr.connected) {
276             return Observable.of([]);
277         }
278
279         return this._get(svr.partialUrl + '/sdks');
280     }
281
282     /***
283     ** Projects
284     ***/
285     getProjects(): Observable<IXDSProjectConfig[]> {
286         return this._get('/projects');
287     }
288
289     addProject(cfg: IXDSProjectConfig): Observable<IXDSProjectConfig> {
290         return this._post('/projects', cfg);
291     }
292
293     deleteProject(id: string): Observable<IXDSProjectConfig> {
294         return this._delete('/projects/' + id);
295     }
296
297     syncProject(id: string): Observable<string> {
298         return this._post('/projects/sync/' + id, {});
299     }
300
301     /***
302     ** Exec
303     ***/
304     exec(prjID: string, dir: string, cmd: string, sdkid?: string, args?: string[], env?: string[]): Observable<any> {
305         return this._post('/exec',
306             {
307                 id: prjID,
308                 rpath: dir,
309                 cmd: cmd,
310                 sdkID: sdkid || '',
311                 args: args || [],
312                 env: env || [],
313             });
314     }
315
316     /**
317     ** Private functions
318     ***/
319
320     private _RegisterEvents() {
321         // Register to all existing events
322         this._post('/events/register', { 'name': 'event:all' })
323             .subscribe(
324             res => { },
325             error => {
326                 this.alert.error('ERROR while registering to all events: ' + error);
327             }
328             );
329     }
330
331     private _getServer(ID: string): IXDServerCfg {
332         const svr = this._config.servers.filter(item => item.id === ID);
333         if (svr.length < 1) {
334             return null;
335         }
336         return svr[0];
337     }
338
339     private _attachAuthHeaders(options?: any) {
340         options = options || {};
341         const headers = options.headers || new HttpHeaders();
342         // headers.append('Authorization', 'Basic ' + btoa('username:password'));
343         headers.append('Accept', 'application/json');
344         headers.append('Content-Type', 'application/json');
345         // headers.append('Access-Control-Allow-Origin', '*');
346
347         options.headers = headers;
348         return options;
349     }
350
351     private _get(url: string): Observable<any> {
352         return this.http.get(this.baseUrl + url, this._attachAuthHeaders())
353             .catch(this._decodeError);
354     }
355     private _post(url: string, body: any): Observable<any> {
356         return this.http.post(this.baseUrl + url, JSON.stringify(body), this._attachAuthHeaders())
357             .catch((error) => {
358                 return this._decodeError(error);
359             });
360     }
361     private _delete(url: string): Observable<any> {
362         return this.http.delete(this.baseUrl + url, this._attachAuthHeaders())
363             .catch(this._decodeError);
364     }
365
366     private _decodeError(err: any) {
367         let e: string;
368         if (err instanceof HttpErrorResponse) {
369             e = (err.error && err.error.error) ? err.error.error : err.message || 'Unknown error';
370         } else if (typeof err === 'object') {
371             if (err.statusText) {
372                 e = err.statusText;
373             } else if (err.error) {
374                 e = String(err.error);
375             } else {
376                 e = JSON.stringify(err);
377             }
378         } else {
379             e = err.message ? err.message : err.toString();
380         }
381         console.log('xdsagent.service - ERROR: ', e);
382         return Observable.throw(e);
383     }
384 }