6b344e1ac144da97e0df89bc0499ab17a4c439ce
[src/xds/xds-server.git] / webapp / src / app / services / config.service.ts
1 import { Injectable, OnInit } from '@angular/core';
2 import { Http, Headers, RequestOptionsArgs, Response } from '@angular/http';
3 import { Location } from '@angular/common';
4 import { CookieService } from 'ngx-cookie';
5 import { Observable } from 'rxjs/Observable';
6 import { Subscriber } from 'rxjs/Subscriber';
7 import { BehaviorSubject } from 'rxjs/BehaviorSubject';
8
9 // Import RxJs required methods
10 import 'rxjs/add/operator/map';
11 import 'rxjs/add/operator/catch';
12 import 'rxjs/add/observable/throw';
13 import 'rxjs/add/operator/mergeMap';
14
15
16 import { XDSServerService, IXDSConfigProject } from "../services/xdsserver.service";
17 import { XDSAgentService } from "../services/xdsagent.service";
18 import { SyncthingService, ISyncThingProject, ISyncThingStatus } from "../services/syncthing.service";
19 import { AlertService, IAlert } from "../services/alert.service";
20 import { UtilsService } from "../services/utils.service";
21
22 export enum ProjectType {
23     NATIVE = 1,
24     SYNCTHING = 2
25 }
26
27 export interface INativeProject {
28     // TODO
29 }
30
31 export interface IProject {
32     id?: string;
33     label: string;
34     path: string;
35     type: ProjectType;
36     remotePrjDef?: INativeProject | ISyncThingProject;
37     localPrjDef?: any;
38     isExpanded?: boolean;
39     visible?: boolean;
40     defaultSdkID?: string;
41 }
42
43 export interface IXDSAgentConfig {
44     URL: string;
45     retry: number;
46 }
47
48 export interface ILocalSTConfig {
49     ID: string;
50     URL: string;
51     retry: number;
52     tilde: string;
53 }
54
55 export interface IxdsAgentPackage {
56     os: string;
57     url: string;
58 }
59
60 export interface IConfig {
61     xdsServerURL: string;
62     xdsAgent: IXDSAgentConfig;
63     xdsAgentPackages: IxdsAgentPackage[];
64     projectsRootDir: string;
65     projects: IProject[];
66     localSThg: ILocalSTConfig;
67 }
68
69 @Injectable()
70 export class ConfigService {
71
72     public conf: Observable<IConfig>;
73
74     private confSubject: BehaviorSubject<IConfig>;
75     private confStore: IConfig;
76     private AgentConnectObs = null;
77     private stConnectObs = null;
78
79     constructor(private _window: Window,
80         private cookie: CookieService,
81         private xdsServerSvr: XDSServerService,
82         private xdsAgentSvr: XDSAgentService,
83         private stSvr: SyncthingService,
84         private alert: AlertService,
85         private utils: UtilsService,
86     ) {
87         this.load();
88         this.confSubject = <BehaviorSubject<IConfig>>new BehaviorSubject(this.confStore);
89         this.conf = this.confSubject.asObservable();
90
91         // force to load projects
92         this.loadProjects();
93     }
94
95     // Load config
96     load() {
97         // Try to retrieve previous config from cookie
98         let cookConf = this.cookie.getObject("xds-config");
99         if (cookConf != null) {
100             this.confStore = <IConfig>cookConf;
101         } else {
102             // Set default config
103             this.confStore = {
104                 xdsServerURL: this._window.location.origin + '/api/v1',
105                 xdsAgent: {
106                     URL: 'http://localhost:8000',
107                     retry: 10,
108                 },
109                 xdsAgentPackages: [],
110                 projectsRootDir: "",
111                 projects: [],
112                 localSThg: {
113                     ID: null,
114                     URL: "http://localhost:8384",
115                     retry: 10,    // 10 seconds
116                     tilde: "",
117                 }
118             };
119         }
120
121         // Update XDS Agent tarball url
122         this.xdsServerSvr.getXdsAgentInfo().subscribe(nfo => {
123             this.confStore.xdsAgentPackages = [];
124             nfo.tarballs && nfo.tarballs.forEach(el =>
125                 this.confStore.xdsAgentPackages.push({os: el.os, url: el.fileUrl})
126             );
127             this.confSubject.next(Object.assign({}, this.confStore));
128         });
129     }
130
131     // Save config into cookie
132     save() {
133         // Notify subscribers
134         this.confSubject.next(Object.assign({}, this.confStore));
135
136         // Don't save projects in cookies (too big!)
137         let cfg = Object.assign({}, this.confStore);
138         delete (cfg.projects);
139         this.cookie.putObject("xds-config", cfg);
140     }
141
142     loadProjects() {
143         // Setup connection with local XDS agent
144         if (this.AgentConnectObs) {
145             try {
146                 this.AgentConnectObs.unsubscribe();
147             } catch (err) { }
148             this.AgentConnectObs = null;
149         }
150
151         let cfg = this.confStore.xdsAgent;
152         this.AgentConnectObs = this.xdsAgentSvr.connect(cfg.retry, cfg.URL)
153             .subscribe((sts) => {
154                 //console.log("Agent sts", sts);
155                 // FIXME: load projects from local XDS Agent and
156                 //  not directly from local syncthing
157                 this._loadProjectFromLocalST();
158
159             }, error => {
160                 if (error.indexOf("XDS local Agent not responding") !== -1) {
161                     let msg = "<span><strong>" + error + "<br></strong>";
162                     msg += "You may need to download and execute XDS-Agent.<br>";
163
164                     let os = this.utils.getOSName(true);
165                     let zurl = this.confStore.xdsAgentPackages && this.confStore.xdsAgentPackages.filter(elem => elem.os === os);
166                     if (zurl && zurl.length) {
167                         msg += " Download XDS-Agent tarball for " + zurl[0].os + " host OS ";
168                         msg += "<a class=\"fa fa-download\" href=\"" +  zurl[0].url + "\" target=\"_blank\"></a>";
169                     }
170                     msg += "</span>";
171                     this.alert.error(msg);
172                 } else {
173                     this.alert.error(error);
174                 }
175             });
176     }
177
178     private _loadProjectFromLocalST() {
179         // Remove previous subscriber if existing
180         if (this.stConnectObs) {
181             try {
182                 this.stConnectObs.unsubscribe();
183             } catch (err) { }
184             this.stConnectObs = null;
185         }
186
187         // FIXME: move this code and all logic about syncthing inside XDS Agent
188         // Setup connection with local SyncThing
189         let retry = this.confStore.localSThg.retry;
190         let url = this.confStore.localSThg.URL;
191         this.stConnectObs = this.stSvr.connect(retry, url).subscribe((sts) => {
192             this.confStore.localSThg.ID = sts.ID;
193             this.confStore.localSThg.tilde = sts.tilde;
194             if (this.confStore.projectsRootDir === "") {
195                 this.confStore.projectsRootDir = sts.tilde;
196             }
197
198             // Rebuild projects definition from local and remote syncthing
199             this.confStore.projects = [];
200
201             this.xdsServerSvr.getProjects().subscribe(remotePrj => {
202                 this.stSvr.getProjects().subscribe(localPrj => {
203                     remotePrj.forEach(rPrj => {
204                         let lPrj = localPrj.filter(item => item.id === rPrj.id);
205                         if (lPrj.length > 0) {
206                             let pp: IProject = {
207                                 id: rPrj.id,
208                                 label: rPrj.label,
209                                 path: rPrj.path,
210                                 type: ProjectType.SYNCTHING,    // FIXME support other types
211                                 remotePrjDef: Object.assign({}, rPrj),
212                                 localPrjDef: Object.assign({}, lPrj[0]),
213                             };
214                             this.confStore.projects.push(pp);
215                         }
216                     });
217                     this.confSubject.next(Object.assign({}, this.confStore));
218                 }), error => this.alert.error('Could not load initial state of local projects.');
219             }), error => this.alert.error('Could not load initial state of remote projects.');
220
221         }, error => {
222             if (error.indexOf("Syncthing local daemon not responding") !== -1) {
223                 let msg = "<span><strong>" + error + "<br></strong>";
224                 msg += "Please check that local XDS-Agent is running.<br>";
225                 msg += "</span>";
226                 this.alert.error(msg);
227             } else {
228                 this.alert.error(error);
229             }
230         });
231     }
232
233     set syncToolURL(url: string) {
234         this.confStore.localSThg.URL = url;
235         this.save();
236     }
237
238     set xdsAgentRetry(r: number) {
239         this.confStore.localSThg.retry = r;
240         this.confStore.xdsAgent.retry = r;
241         this.save();
242     }
243
244     set xdsAgentUrl(url: string) {
245         this.confStore.xdsAgent.URL = url;
246         this.save();
247     }
248
249
250     set projectsRootDir(p: string) {
251         if (p.charAt(0) === '~') {
252             p = this.confStore.localSThg.tilde + p.substring(1);
253         }
254         this.confStore.projectsRootDir = p;
255         this.save();
256     }
257
258     getLabelRootName(): string {
259         let id = this.confStore.localSThg.ID;
260         if (!id || id === "") {
261             return null;
262         }
263         return id.slice(0, 15);
264     }
265
266     addProject(prj: IProject) {
267         // Substitute tilde with to user home path
268         prj.path = prj.path.trim();
269         if (prj.path.charAt(0) === '~') {
270             prj.path = this.confStore.localSThg.tilde + prj.path.substring(1);
271
272             // Must be a full path (on Linux or Windows)
273         } else if (!((prj.path.charAt(0) === '/') ||
274             (prj.path.charAt(1) === ':' && (prj.path.charAt(2) === '\\' || prj.path.charAt(2) === '/')))) {
275             prj.path = this.confStore.projectsRootDir + '/' + prj.path;
276         }
277
278         if (prj.id == null) {
279             // FIXME - must be done on server side
280             let prefix = this.getLabelRootName() || new Date().toISOString();
281             let splath = prj.path.split('/');
282             prj.id = prefix + "_" + splath[splath.length - 1];
283         }
284
285         if (this._getProjectIdx(prj.id) !== -1) {
286             this.alert.warning("Project already exist (id=" + prj.id + ")", true);
287             return;
288         }
289
290         // TODO - support others project types
291         if (prj.type !== ProjectType.SYNCTHING) {
292             this.alert.error('Project type not supported yet (type: ' + prj.type + ')');
293             return;
294         }
295
296         let sdkPrj: IXDSConfigProject = {
297             id: prj.id,
298             label: prj.label,
299             path: prj.path,
300             hostSyncThingID: this.confStore.localSThg.ID,
301             defaultSdkID: prj.defaultSdkID,
302         };
303
304         // Send config to XDS server
305         let newPrj = prj;
306         this.xdsServerSvr.addProject(sdkPrj)
307             .subscribe(resStRemotePrj => {
308                 newPrj.remotePrjDef = resStRemotePrj;
309
310                 // FIXME REWORK local ST config
311                 //  move logic to server side tunneling-back by WS
312
313                 // Now setup local config
314                 let stLocPrj: ISyncThingProject = {
315                     id: sdkPrj.id,
316                     label: sdkPrj.label,
317                     path: sdkPrj.path,
318                     remoteSyncThingID: resStRemotePrj.builderSThgID
319                 };
320
321                 // Set local Syncthing config
322                 this.stSvr.addProject(stLocPrj)
323                     .subscribe(resStLocalPrj => {
324                         newPrj.localPrjDef = resStLocalPrj;
325
326                         // FIXME: maybe reduce subject to only .project
327                         //this.confSubject.next(Object.assign({}, this.confStore).project);
328                         this.confStore.projects.push(Object.assign({}, newPrj));
329                         this.confSubject.next(Object.assign({}, this.confStore));
330                     },
331                     err => {
332                         this.alert.error("Configuration local ERROR: " + err);
333                     });
334             },
335             err => {
336                 this.alert.error("Configuration remote ERROR: " + err);
337             });
338     }
339
340     deleteProject(prj: IProject) {
341         let idx = this._getProjectIdx(prj.id);
342         if (idx === -1) {
343             throw new Error("Invalid project id (id=" + prj.id + ")");
344         }
345         this.xdsServerSvr.deleteProject(prj.id)
346             .subscribe(res => {
347                 this.stSvr.deleteProject(prj.id)
348                     .subscribe(res => {
349                         this.confStore.projects.splice(idx, 1);
350                     }, err => {
351                         this.alert.error("Delete local ERROR: " + err);
352                     });
353             }, err => {
354                 this.alert.error("Delete remote ERROR: " + err);
355             });
356     }
357
358     private _getProjectIdx(id: string): number {
359         return this.confStore.projects.findIndex((item) => item.id === id);
360     }
361
362 }