Add Cross SDKs support (part 2)
[src/xds/xds-server.git] / webapp / src / app / common / 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 "../common/xdsserver.service";
17 import { SyncthingService, ISyncThingProject, ISyncThingStatus } from "../common/syncthing.service";
18 import { AlertService, IAlert } from "../common/alert.service";
19
20 export enum ProjectType {
21     NATIVE = 1,
22     SYNCTHING = 2
23 }
24
25 export interface INativeProject {
26     // TODO
27 }
28
29 export interface IProject {
30     id?: string;
31     label: string;
32     path: string;
33     type: ProjectType;
34     remotePrjDef?: INativeProject | ISyncThingProject;
35     localPrjDef?: any;
36     isExpanded?: boolean;
37     visible?: boolean;
38     defaultSdkID?: string;
39 }
40
41 export interface ILocalSTConfig {
42     ID: string;
43     URL: string;
44     retry: number;
45     tilde: string;
46 }
47
48 export interface IConfig {
49     xdsServerURL: string;
50     projectsRootDir: string;
51     projects: IProject[];
52     localSThg: ILocalSTConfig;
53 }
54
55 @Injectable()
56 export class ConfigService {
57
58     public conf: Observable<IConfig>;
59
60     private confSubject: BehaviorSubject<IConfig>;
61     private confStore: IConfig;
62     private stConnectObs = null;
63
64     constructor(private _window: Window,
65         private cookie: CookieService,
66         private sdkSvr: XDSServerService,
67         private stSvr: SyncthingService,
68         private alert: AlertService,
69     ) {
70         this.load();
71         this.confSubject = <BehaviorSubject<IConfig>>new BehaviorSubject(this.confStore);
72         this.conf = this.confSubject.asObservable();
73
74         // force to load projects
75         this.loadProjects();
76     }
77
78     // Load config
79     load() {
80         // Try to retrieve previous config from cookie
81         let cookConf = this.cookie.getObject("xds-config");
82         if (cookConf != null) {
83             this.confStore = <IConfig>cookConf;
84         } else {
85             // Set default config
86             this.confStore = {
87                 xdsServerURL: this._window.location.origin + '/api/v1',
88                 projectsRootDir: "",
89                 projects: [],
90                 localSThg: {
91                     ID: null,
92                     URL: "http://localhost:8384",
93                     retry: 10,    // 10 seconds
94                     tilde: "",
95                 }
96             };
97         }
98     }
99
100     // Save config into cookie
101     save() {
102         // Notify subscribers
103         this.confSubject.next(Object.assign({}, this.confStore));
104
105         // Don't save projects in cookies (too big!)
106         let cfg = this.confStore;
107         delete(cfg.projects);
108         this.cookie.putObject("xds-config", cfg);
109     }
110
111     loadProjects() {
112         // Remove previous subscriber if existing
113         if (this.stConnectObs) {
114             try {
115                 this.stConnectObs.unsubscribe();
116             } catch (err) { }
117             this.stConnectObs = null;
118         }
119
120         // First setup connection with local SyncThing
121         let retry = this.confStore.localSThg.retry;
122         let url = this.confStore.localSThg.URL;
123         this.stConnectObs = this.stSvr.connect(retry, url).subscribe((sts) => {
124             this.confStore.localSThg.ID = sts.ID;
125             this.confStore.localSThg.tilde = sts.tilde;
126             if (this.confStore.projectsRootDir === "") {
127                 this.confStore.projectsRootDir = sts.tilde;
128             }
129
130             // Rebuild projects definition from local and remote syncthing
131             this.confStore.projects = [];
132
133             this.sdkSvr.getProjects().subscribe(remotePrj => {
134                 this.stSvr.getProjects().subscribe(localPrj => {
135                     remotePrj.forEach(rPrj => {
136                         let lPrj = localPrj.filter(item => item.id === rPrj.id);
137                         if (lPrj.length > 0) {
138                             let pp: IProject = {
139                                 id: rPrj.id,
140                                 label: rPrj.label,
141                                 path: rPrj.path,
142                                 type: ProjectType.SYNCTHING,    // FIXME support other types
143                                 remotePrjDef: Object.assign({}, rPrj),
144                                 localPrjDef: Object.assign({}, lPrj[0]),
145                             };
146                             this.confStore.projects.push(pp);
147                         }
148                     });
149                     this.confSubject.next(Object.assign({}, this.confStore));
150                 }), error => this.alert.error('Could not load initial state of local projects.');
151             }), error => this.alert.error('Could not load initial state of remote projects.');
152
153         }, error => this.alert.error(error));
154     }
155
156     set syncToolURL(url: string) {
157         this.confStore.localSThg.URL = url;
158         this.save();
159     }
160
161     set syncToolRetry(r: number) {
162         this.confStore.localSThg.retry = r;
163         this.save();
164     }
165
166     set projectsRootDir(p: string) {
167         if (p.charAt(0) === '~') {
168             p = this.confStore.localSThg.tilde + p.substring(1);
169         }
170         this.confStore.projectsRootDir = p;
171         this.save();
172     }
173
174     getLabelRootName(): string {
175         let id = this.confStore.localSThg.ID;
176         if (!id || id === "") {
177             return null;
178         }
179         return id.slice(0, 15);
180     }
181
182     addProject(prj: IProject) {
183         // Substitute tilde with to user home path
184         prj.path = prj.path.trim();
185         if (prj.path.charAt(0) === '~') {
186             prj.path = this.confStore.localSThg.tilde + prj.path.substring(1);
187
188             // Must be a full path (on Linux or Windows)
189         } else if (!((prj.path.charAt(0) === '/') ||
190             (prj.path.charAt(1) === ':' && (prj.path.charAt(2) === '\\' || prj.path.charAt(2) === '/')))) {
191             prj.path = this.confStore.projectsRootDir + '/' + prj.path;
192         }
193
194         if (prj.id == null) {
195             // FIXME - must be done on server side
196             let prefix = this.getLabelRootName() || new Date().toISOString();
197             let splath = prj.path.split('/');
198             prj.id = prefix + "_" + splath[splath.length - 1];
199         }
200
201         if (this._getProjectIdx(prj.id) !== -1) {
202             this.alert.warning("Project already exist (id=" + prj.id + ")", true);
203             return;
204         }
205
206         // TODO - support others project types
207         if (prj.type !== ProjectType.SYNCTHING) {
208             this.alert.error('Project type not supported yet (type: ' + prj.type + ')');
209             return;
210         }
211
212         let sdkPrj: IXDSConfigProject = {
213             id: prj.id,
214             label: prj.label,
215             path: prj.path,
216             hostSyncThingID: this.confStore.localSThg.ID,
217             defaultSdkID: prj.defaultSdkID,
218         };
219
220         // Send config to XDS server
221         let newPrj = prj;
222         this.sdkSvr.addProject(sdkPrj)
223             .subscribe(resStRemotePrj => {
224                 newPrj.remotePrjDef = resStRemotePrj;
225
226                 // FIXME REWORK local ST config
227                 //  move logic to server side tunneling-back by WS
228
229                 // Now setup local config
230                 let stLocPrj: ISyncThingProject = {
231                     id: sdkPrj.id,
232                     label: sdkPrj.label,
233                     path: sdkPrj.path,
234                     remoteSyncThingID: resStRemotePrj.builderSThgID
235                 };
236
237                 // Set local Syncthing config
238                 this.stSvr.addProject(stLocPrj)
239                     .subscribe(resStLocalPrj => {
240                         newPrj.localPrjDef = resStLocalPrj;
241
242                         // FIXME: maybe reduce subject to only .project
243                         //this.confSubject.next(Object.assign({}, this.confStore).project);
244                         this.confStore.projects.push(Object.assign({}, newPrj));
245                         this.confSubject.next(Object.assign({}, this.confStore));
246                     },
247                     err => {
248                         this.alert.error("Configuration local ERROR: " + err);
249                     });
250             },
251             err => {
252                 this.alert.error("Configuration remote ERROR: " + err);
253             });
254     }
255
256     deleteProject(prj: IProject) {
257         let idx = this._getProjectIdx(prj.id);
258         if (idx === -1) {
259             throw new Error("Invalid project id (id=" + prj.id + ")");
260         }
261         this.sdkSvr.deleteProject(prj.id)
262             .subscribe(res => {
263                 this.stSvr.deleteProject(prj.id)
264                     .subscribe(res => {
265                         this.confStore.projects.splice(idx, 1);
266                     }, err => {
267                         this.alert.error("Delete local ERROR: " + err);
268                     });
269             }, err => {
270                 this.alert.error("Delete remote ERROR: " + err);
271             });
272     }
273
274     private _getProjectIdx(id: string): number {
275         return this.confStore.projects.findIndex((item) => item.id === id);
276     }
277
278 }