Add XDS-agent tarball download feature
authorSebastien Douheret <sebastien.douheret@iot.bzh>
Mon, 22 May 2017 16:45:46 +0000 (18:45 +0200)
committerSebastien Douheret <sebastien.douheret@iot.bzh>
Mon, 22 May 2017 16:47:23 +0000 (18:47 +0200)
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
13 files changed:
.gitignore
lib/apiv1/agent.go [new file with mode: 0644]
lib/apiv1/apiv1.go
webapp/assets/xds-agent-tarballs/.gitkeep [new file with mode: 0644]
webapp/src/app/alert/alert.component.ts
webapp/src/app/app.module.ts
webapp/src/app/common/alert.service.ts
webapp/src/app/common/config.service.ts
webapp/src/app/common/utils.service.ts [new file with mode: 0644]
webapp/src/app/common/xdsagent.service.ts [new file with mode: 0644]
webapp/src/app/common/xdsserver.service.ts
webapp/src/app/config/config.component.html
webapp/src/app/config/config.component.ts

index 6602391..500d93b 100644 (file)
@@ -8,6 +8,7 @@ cmd/*/debug
 
 webapp/dist
 webapp/node_modules
+webapp/assets/xds-agent-tarballs/*.zip
 
 # private (prefixed by 2 underscores) directories or files
 __*/
diff --git a/lib/apiv1/agent.go b/lib/apiv1/agent.go
new file mode 100644 (file)
index 0000000..7434545
--- /dev/null
@@ -0,0 +1,37 @@
+package apiv1
+
+import (
+       "net/http"
+
+       "path/filepath"
+
+       "github.com/gin-gonic/gin"
+)
+
+type XDSAgentTarball struct {
+       OS      string `json:"os"`
+       FileURL string `json:"fileUrl"`
+}
+type XDSAgentInfo struct {
+       Tarballs []XDSAgentTarball `json:"tarballs"`
+}
+
+// getXdsAgentInfo : return various information about Xds Agent
+func (s *APIService) getXdsAgentInfo(c *gin.Context) {
+       // TODO: retrieve link dynamically by reading assets/xds-agent-tarballs
+       tarballDir := "assets/xds-agent-tarballs"
+       response := XDSAgentInfo{
+               Tarballs: []XDSAgentTarball{
+                       XDSAgentTarball{
+                               OS:      "linux",
+                               FileURL: filepath.Join(tarballDir, "xds-agent_linux-amd64-v0.0.1_3cdf92c.zip"),
+                       },
+                       XDSAgentTarball{
+                               OS:      "windows",
+                               FileURL: filepath.Join(tarballDir, "xds-agent_windows-386-v0.0.1_3cdf92c.zip"),
+                       },
+               },
+       }
+
+       c.JSON(http.StatusOK, response)
+}
index 7359266..2df8ea7 100644 (file)
@@ -34,6 +34,7 @@ func New(r *gin.Engine, sess *session.Sessions, cfg *xdsconfig.Config, mfolder *
        }
 
        s.apiRouter.GET("/version", s.getVersion)
+       s.apiRouter.GET("/xdsagent/info", s.getXdsAgentInfo)
 
        s.apiRouter.GET("/config", s.getConfig)
        s.apiRouter.POST("/config", s.setConfig)
diff --git a/webapp/assets/xds-agent-tarballs/.gitkeep b/webapp/assets/xds-agent-tarballs/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
index e9d7629..449506f 100644 (file)
@@ -9,7 +9,7 @@ import {AlertService, IAlert} from '../common/alert.service';
         <div style="width:80%; margin-left:auto; margin-right:auto;" *ngFor="let alert of (alerts$ | async)">
             <alert *ngIf="alert.show" [type]="alert.type" [dismissible]="alert.dismissible" [dismissOnTimeout]="alert.dismissTimeout"
             (onClose)="onClose(alert)">
-                <span [innerHtml]="alert.msg"></span>
+                <div style="text-align:center;" [innerHtml]="alert.msg"></div>
             </alert>
         </div>
     `
index d4a6918..1abcf0c 100644 (file)
@@ -26,9 +26,11 @@ import { SdkSelectDropdownComponent } from "./sdks/sdkSelectDropdown.component";
 import { HomeComponent } from "./home/home.component";
 import { BuildComponent } from "./build/build.component";
 import { XDSServerService } from "./common/xdsserver.service";
+import { XDSAgentService } from "./common/xdsagent.service";
 import { SyncthingService } from "./common/syncthing.service";
 import { ConfigService } from "./common/config.service";
 import { AlertService } from './common/alert.service';
+import { UtilsService } from './common/utils.service';
 import { SdkService } from "./common/sdk.service";
 
 
@@ -67,9 +69,11 @@ import { SdkService } from "./common/sdk.service";
             useValue: window
         },
         XDSServerService,
+        XDSAgentService,
         ConfigService,
         SyncthingService,
         AlertService,
+        UtilsService,
         SdkService,
     ],
     bootstrap: [AppComponent]
index 710046f..9dab36a 100644 (file)
@@ -39,7 +39,7 @@ export class AlertService {
     }
 
     public info(msg: string) {
-        this.add({ type: "warning", msg: msg, dismissible: true, dismissTimeout: this.defaultDissmissTmo });
+        this.add({ type: "info", msg: msg, dismissible: true, dismissTimeout: this.defaultDissmissTmo });
     }
 
     public add(al: IAlert) {
index 201ee8b..a04ac13 100644 (file)
@@ -14,8 +14,10 @@ import 'rxjs/add/operator/mergeMap';
 
 
 import { XDSServerService, IXDSConfigProject } from "../common/xdsserver.service";
+import { XDSAgentService } from "../common/xdsagent.service";
 import { SyncthingService, ISyncThingProject, ISyncThingStatus } from "../common/syncthing.service";
 import { AlertService, IAlert } from "../common/alert.service";
+import { UtilsService } from "../common/utils.service";
 
 export enum ProjectType {
     NATIVE = 1,
@@ -38,6 +40,11 @@ export interface IProject {
     defaultSdkID?: string;
 }
 
+export interface IXDSAgentConfig {
+    URL: string;
+    retry: number;
+}
+
 export interface ILocalSTConfig {
     ID: string;
     URL: string;
@@ -47,6 +54,8 @@ export interface ILocalSTConfig {
 
 export interface IConfig {
     xdsServerURL: string;
+    xdsAgent: IXDSAgentConfig;
+    xdsAgentZipUrl: string;
     projectsRootDir: string;
     projects: IProject[];
     localSThg: ILocalSTConfig;
@@ -59,13 +68,17 @@ export class ConfigService {
 
     private confSubject: BehaviorSubject<IConfig>;
     private confStore: IConfig;
+    private AgentConnectObs = null;
     private stConnectObs = null;
+    private xdsAgentZipUrl = "";
 
     constructor(private _window: Window,
         private cookie: CookieService,
-        private sdkSvr: XDSServerService,
+        private xdsServerSvr: XDSServerService,
+        private xdsAgentSvr: XDSAgentService,
         private stSvr: SyncthingService,
         private alert: AlertService,
+        private utils: UtilsService,
     ) {
         this.load();
         this.confSubject = <BehaviorSubject<IConfig>>new BehaviorSubject(this.confStore);
@@ -85,6 +98,11 @@ export class ConfigService {
             // Set default config
             this.confStore = {
                 xdsServerURL: this._window.location.origin + '/api/v1',
+                xdsAgent: {
+                    URL: 'http://localhost:8000',
+                    retry: 10,
+                },
+                xdsAgentZipUrl: "",
                 projectsRootDir: "",
                 projects: [],
                 localSThg: {
@@ -95,6 +113,13 @@ export class ConfigService {
                 }
             };
         }
+
+        // Update XDS Agent tarball url
+        this.xdsServerSvr.getXdsAgentInfo().subscribe(nfo => {
+            let os = this.utils.getOSName(true);
+            let zurl = nfo.tarballs.filter(elem => elem.os === os);
+            this.confStore.xdsAgentZipUrl = zurl && zurl[0].fileUrl;
+        });
     }
 
     // Save config into cookie
@@ -104,11 +129,26 @@ export class ConfigService {
 
         // Don't save projects in cookies (too big!)
         let cfg = this.confStore;
-        delete(cfg.projects);
+        delete (cfg.projects);
         this.cookie.putObject("xds-config", cfg);
     }
 
     loadProjects() {
+        // Setup connection with local XDS agent
+        if (this.AgentConnectObs) {
+            try {
+                this.AgentConnectObs.unsubscribe();
+            } catch (err) { }
+            this.AgentConnectObs = null;
+        }
+
+        let cfg = this.confStore.xdsAgent;
+        this.AgentConnectObs = this.xdsAgentSvr.connect(cfg.retry, cfg.URL)
+            .subscribe((sts) => {
+                console.log("Agent sts", sts);
+            }, error => this.alert.error(error)
+            );
+
         // Remove previous subscriber if existing
         if (this.stConnectObs) {
             try {
@@ -117,7 +157,8 @@ export class ConfigService {
             this.stConnectObs = null;
         }
 
-        // First setup connection with local SyncThing
+        // FIXME: move this code and all logic about syncthing inside XDS Agent
+        // Setup connection with local SyncThing
         let retry = this.confStore.localSThg.retry;
         let url = this.confStore.localSThg.URL;
         this.stConnectObs = this.stSvr.connect(retry, url).subscribe((sts) => {
@@ -130,7 +171,7 @@ export class ConfigService {
             // Rebuild projects definition from local and remote syncthing
             this.confStore.projects = [];
 
-            this.sdkSvr.getProjects().subscribe(remotePrj => {
+            this.xdsServerSvr.getProjects().subscribe(remotePrj => {
                 this.stSvr.getProjects().subscribe(localPrj => {
                     remotePrj.forEach(rPrj => {
                         let lPrj = localPrj.filter(item => item.id === rPrj.id);
@@ -150,7 +191,18 @@ export class ConfigService {
                 }), error => this.alert.error('Could not load initial state of local projects.');
             }), error => this.alert.error('Could not load initial state of remote projects.');
 
-        }, error => this.alert.error(error));
+        }, error => {
+            if (error.indexOf("Syncthing local daemon not responding") !== -1) {
+                let msg = "<span><strong>" + error + "<br></strong>";
+                msg += "You may need to download and execute XDS-Agent.<br>";
+                msg += "<a class=\"fa fa-download\" href=\"" + this.confStore.xdsAgentZipUrl + "\" target=\"_blank\"></a>";
+                msg += " Download XDS-Agent tarball.";
+                msg += "</span>";
+                this.alert.error(msg);
+            } else {
+                this.alert.error(error);
+            }
+        });
     }
 
     set syncToolURL(url: string) {
@@ -158,11 +210,18 @@ export class ConfigService {
         this.save();
     }
 
-    set syncToolRetry(r: number) {
+    set xdsAgentRetry(r: number) {
         this.confStore.localSThg.retry = r;
+        this.confStore.xdsAgent.retry = r;
         this.save();
     }
 
+    set xdsAgentUrl(url: string) {
+        this.confStore.xdsAgent.URL = url;
+        this.save();
+    }
+
+
     set projectsRootDir(p: string) {
         if (p.charAt(0) === '~') {
             p = this.confStore.localSThg.tilde + p.substring(1);
@@ -219,7 +278,7 @@ export class ConfigService {
 
         // Send config to XDS server
         let newPrj = prj;
-        this.sdkSvr.addProject(sdkPrj)
+        this.xdsServerSvr.addProject(sdkPrj)
             .subscribe(resStRemotePrj => {
                 newPrj.remotePrjDef = resStRemotePrj;
 
@@ -258,7 +317,7 @@ export class ConfigService {
         if (idx === -1) {
             throw new Error("Invalid project id (id=" + prj.id + ")");
         }
-        this.sdkSvr.deleteProject(prj.id)
+        this.xdsServerSvr.deleteProject(prj.id)
             .subscribe(res => {
                 this.stSvr.deleteProject(prj.id)
                     .subscribe(res => {
diff --git a/webapp/src/app/common/utils.service.ts b/webapp/src/app/common/utils.service.ts
new file mode 100644 (file)
index 0000000..291ffd3
--- /dev/null
@@ -0,0 +1,23 @@
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class UtilsService {
+    constructor() { }
+
+    getOSName(lowerCase?: boolean): string {
+        let OSName = "Unknown OS";
+        if (navigator.appVersion.indexOf("Linux") !== -1) {
+            OSName = "Linux";
+        } else if (navigator.appVersion.indexOf("Win") !== -1) {
+            OSName = "Windows";
+        } else if (navigator.appVersion.indexOf("Mac") !== -1) {
+            OSName = "MacOS";
+        } else if (navigator.appVersion.indexOf("X11") !== -1) {
+            OSName = "UNIX";
+        }
+        if (lowerCase) {
+            return OSName.toLowerCase();
+        }
+        return OSName;
+    }
+}
\ No newline at end of file
diff --git a/webapp/src/app/common/xdsagent.service.ts b/webapp/src/app/common/xdsagent.service.ts
new file mode 100644 (file)
index 0000000..4d9aadc
--- /dev/null
@@ -0,0 +1,213 @@
+import { Injectable } from '@angular/core';
+import { Http, Headers, RequestOptionsArgs, Response } from '@angular/http';
+import { Location } from '@angular/common';
+import { Observable } from 'rxjs/Observable';
+import { Subject } from 'rxjs/Subject';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+import * as io from 'socket.io-client';
+
+import { AlertService } from './alert.service';
+
+
+// Import RxJs required methods
+import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/observable/throw';
+
+export interface IXDSVersion {
+    version: string;
+    apiVersion: string;
+    gitTag: string;
+
+}
+
+export interface IAgentStatus {
+    baseURL: string;
+    connected: boolean;
+    WS_connected: boolean;
+    connectionRetry: number;
+    version: string;
+}
+
+// Default settings
+const DEFAULT_PORT = 8010;
+const DEFAULT_API_KEY = "1234abcezam";
+const API_VERSION = "v1";
+
+@Injectable()
+export class XDSAgentService {
+    public Status$: Observable<IAgentStatus>;
+
+    private baseRestUrl: string;
+    private wsUrl: string;
+    private connectionMaxRetry: number;
+    private apikey: string;
+    private _status: IAgentStatus = {
+        baseURL: "",
+        connected: false,
+        WS_connected: false,
+        connectionRetry: 0,
+        version: "",
+    };
+    private statusSubject = <BehaviorSubject<IAgentStatus>>new BehaviorSubject(this._status);
+
+
+    private socket: SocketIOClient.Socket;
+
+    constructor(private http: Http, private _window: Window, private alert: AlertService) {
+
+        this.Status$ = this.statusSubject.asObservable();
+
+        this.apikey = DEFAULT_API_KEY; // FIXME Add dynamic allocated key
+        this._status.baseURL = 'http://localhost:' + DEFAULT_PORT;
+        this.baseRestUrl = this._status.baseURL + '/api/' + API_VERSION;
+        let re = this._window.location.origin.match(/http[s]?:\/\/([^\/]*)[\/]?/);
+        if (re === null || re.length < 2) {
+            console.error('ERROR: cannot determine Websocket url');
+        } else {
+            this.wsUrl = 'ws://' + re[1];
+        }
+    }
+
+    connect(retry: number, url?: string): Observable<IAgentStatus> {
+        if (url) {
+            this._status.baseURL = url;
+            this.baseRestUrl = this._status.baseURL + '/api/' + API_VERSION;
+        }
+        //FIXME [XDS-Agent]: not implemented yet, set always as connected
+        //this._status.connected = false;
+        this._status.connected = true;
+        this._status.connectionRetry = 0;
+        this.connectionMaxRetry = retry || 3600;   // 1 hour
+
+        // Init IO Socket connection
+        this._handleIoSocket();
+
+        // Get Version in order to check connection via a REST request
+        return this.getVersion()
+            .map((v) => {
+                this._status.version = v.version;
+                this.statusSubject.next(Object.assign({}, this._status));
+                return this._status;
+            });
+    }
+
+    public getVersion(): Observable<IXDSVersion> {
+        /*FIXME [XDS-Agent]: Not implemented for now
+        return this._get('/version');
+        */
+        return Observable.of({
+            version: "NOT_IMPLEMENTED",
+            apiVersion: "NOT_IMPLEMENTED",
+            gitTag: "NOT_IMPLEMENTED"
+        });
+    }
+
+    private _WSState(sts: boolean) {
+        this._status.WS_connected = sts;
+        this.statusSubject.next(Object.assign({}, this._status));
+    }
+
+    private _handleIoSocket() {
+        this.socket = io(this.wsUrl, { transports: ['websocket'] });
+
+        this.socket.on('connect_error', (res) => {
+            this._WSState(false);
+            console.error('WS Connect_error ', res);
+        });
+
+        this.socket.on('connect', (res) => {
+            this._WSState(true);
+        });
+
+        this.socket.on('disconnection', (res) => {
+            this._WSState(false);
+            this.alert.error('WS disconnection: ' + res);
+        });
+
+        this.socket.on('error', (err) => {
+            console.error('WS error:', err);
+        });
+
+    }
+
+    private _attachAuthHeaders(options?: any) {
+        options = options || {};
+        let headers = options.headers || new Headers();
+        // headers.append('Authorization', 'Basic ' + btoa('username:password'));
+        headers.append('Access-Control-Allow-Origin', '*');
+        headers.append('Accept', 'application/json');
+        headers.append('Content-Type', 'application/json');
+        if (this.apikey !== "") {
+            headers.append('X-API-Key', this.apikey);
+
+        }
+
+        options.headers = headers;
+        return options;
+    }
+
+    private _checkAlive(): Observable<boolean> {
+        if (this._status.connected) {
+            return Observable.of(true);
+        }
+
+        return this.http.get(this._status.baseURL, this._attachAuthHeaders())
+            .map((r) => this._status.connected = true)
+            .retryWhen((attempts) => {
+                this._status.connectionRetry = 0;
+                return attempts.flatMap(error => {
+                    this._status.connected = false;
+                    if (++this._status.connectionRetry >= this.connectionMaxRetry) {
+                        return Observable.throw("XDS local Agent not responding (url=" + this._status.baseURL + ")");
+                    } else {
+                        return Observable.timer(1000);
+                    }
+                });
+            });
+    }
+
+    private _get(url: string): Observable<any> {
+        return this._checkAlive()
+            .flatMap(() => this.http.get(this.baseRestUrl + url, this._attachAuthHeaders()))
+            .map((res: Response) => res.json())
+            .catch(this._decodeError);
+    }
+    private _post(url: string, body: any): Observable<any> {
+        return this._checkAlive()
+            .flatMap(() => this.http.post(this.baseRestUrl + url, JSON.stringify(body), this._attachAuthHeaders()))
+            .map((res: Response) => res.json())
+            .catch((error) => {
+                return this._decodeError(error);
+            });
+    }
+    private _delete(url: string): Observable<any> {
+        return this._checkAlive()
+            .flatMap(() => this.http.delete(this.baseRestUrl + url, this._attachAuthHeaders()))
+            .map((res: Response) => res.json())
+            .catch(this._decodeError);
+    }
+
+    private _decodeError(err: Response | any) {
+        let e: string;
+        if (this._status) {
+            this._status.connected = false;
+        }
+        if (typeof err === "object") {
+            if (err.statusText) {
+                e = err.statusText;
+            } else if (err.error) {
+                e = String(err.error);
+            } else {
+                e = JSON.stringify(err);
+            }
+        } else if (err instanceof Response) {
+            const body = err.json() || 'Server error';
+            const error = body.error || JSON.stringify(body);
+            e = `${err.status} - ${err.statusText || ''} ${error}`;
+        } else {
+            e = err.message ? err.message : err.toString();
+        }
+        return Observable.throw(e);
+    }
+}
index 6cd9ba3..49c2d37 100644 (file)
@@ -48,6 +48,15 @@ interface IXDSConfig {
     folders: IXDSFolderConfig[];
 }
 
+export interface IXDSAgentTarball {
+    os: string;
+    fileUrl: string;
+}
+
+export interface IXDSAgentInfo {
+    tarballs: IXDSAgentTarball[];
+}
+
 export interface ISdkMessage {
     wsID: string;
     msgType: string;
@@ -144,6 +153,10 @@ export class XDSServerService {
         return this._get('/sdks');
     }
 
+    getXdsAgentInfo(): Observable<IXDSAgentInfo> {
+        return this._get('/xdsagent/info');
+    }
+
     getProjects(): Observable<IXDSFolderConfig[]> {
         return this._get('/folders');
     }
index 2a3d322..77d90c5 100644 (file)
@@ -2,7 +2,7 @@
     <div class="panel-heading clearfix">
         <h2 class="panel-title pull-left">Global Configuration</h2>
         <div class="pull-right">
-            <span class="fa fa-fw fa-exchange fa-size-x2" [style.color]="((severStatus$ | async)?.WS_connected)?'green':'red'"></span>
+            <span class="fa fa-fw fa-exchange fa-size-x2" [style.color]="((serverStatus$ | async)?.WS_connected)?'green':'red'"></span>
         </div>
     </div>
     <div class="panel-body">
             <div class="col-xs-12">
                 <table class="table table-condensed">
                     <tbody>
+                        <!-- FIXME [XDS-Agent]
+                        <tr [ngClass]="{'info': (agentStatus$ | async)?.connected, 'danger': !(agentStatus$ | async)?.connected}">
+                        -->
                         <tr [ngClass]="{'info': (localSTStatus$ | async)?.connected, 'danger': !(localSTStatus$ | async)?.connected}">
-                            <th><label>Local Sync-tool URL</label></th>
-                            <td> <input type="text" [(ngModel)]="syncToolUrl"></td>
+                            <th><label>XDS local Agent URL</label></th>
+                            <td> <input type="text" [(ngModel)]="xdsAgentUrl"></td>
                             <td>
-                                <button class="btn btn-link" (click)="syncToolRestartConn()"><span class="fa fa-refresh fa-size-x2"></span></button>
+                                <button class="btn btn-link" (click)="xdsAgentRestartConn()"><span class="fa fa-refresh fa-size-x2"></span></button>
+                                <!-- FIXME [XDS-Agent]
+                                <button  *ngIf="!(agentStatus$ | async)?.connected"  -->
+                                <button  *ngIf="!(localSTStatus$ | async)?.connected"  class="btn btn-link"><a class="fa fa-download fa-size-x2" [href]="xdsAgentZipUrl" target="_blank"></a></button>
                             </td>
                         </tr>
                         <tr class="info">
-                            <th><label>Local Sync-tool connection retry</label></th>
-                            <td> <input type="text" [(ngModel)]="syncToolRetry" (ngModelChange)="showApplyBtn['retry'] = true"></td>
+                            <th><label>Local Agent connection retry</label></th>
+                            <td> <input type="text" [(ngModel)]="xdsAgentRetry" (ngModelChange)="showApplyBtn['retry'] = true"></td>
                             <td>
                                 <button *ngIf="showApplyBtn['retry']" class="btn btn-primary btn-xs" (click)="submitGlobConf('retry')">APPLY</button>
                             </td>
                         </tr>
+                        <tr [ngClass]="{'info': (localSTStatus$ | async)?.connected, 'danger': !(localSTStatus$ | async)?.connected}">
+                            <th><label>Local Sync-tool URL</label></th>
+                            <td> <input type="text" [(ngModel)]="syncToolUrl"></td>
+                            <td>
+                                <button class="btn btn-link" (click)="xdsAgentRestartConn()"><span class="fa fa-refresh fa-size-x2"></span></button>
+                            </td>
+                        </tr>
                         <tr class="info">
                             <th><label>Local Projects root directory</label></th>
                             <td> <input type="text" [(ngModel)]="projectsRootDir" (ngModelChange)="showApplyBtn['rootDir'] = true"></td>
index 745e9f6..1e1e9c2 100644 (file)
@@ -8,7 +8,8 @@ import 'rxjs/add/operator/filter';
 import 'rxjs/add/operator/debounceTime';
 
 import { ConfigService, IConfig, IProject, ProjectType } from "../common/config.service";
-import { XDSServerService, IServerStatus } from "../common/xdsserver.service";
+import { XDSServerService, IServerStatus, IXDSAgentInfo } from "../common/xdsserver.service";
+import { XDSAgentService, IAgentStatus } from "../common/xdsagent.service";
 import { SyncthingService, ISyncThingStatus } from "../common/syncthing.service";
 import { AlertService } from "../common/alert.service";
 import { ISdk, SdkService } from "../common/sdk.service";
@@ -25,15 +26,18 @@ export class ConfigComponent implements OnInit {
 
     config$: Observable<IConfig>;
     sdks$: Observable<ISdk[]>;
-    severStatus$: Observable<IServerStatus>;
+    serverStatus$: Observable<IServerStatus>;
+    agentStatus$: Observable<IAgentStatus>;
     localSTStatus$: Observable<ISyncThingStatus>;
 
     curProj: number;
     userEditedLabel: boolean = false;
+    xdsAgentZipUrl: string = "";
 
     // TODO replace by reactive FormControl + add validation
     syncToolUrl: string;
-    syncToolRetry: string;
+    xdsAgentUrl: string;
+    xdsAgentRetry: string;
     projectsRootDir: string;
     showApplyBtn = {    // Used to show/hide Apply buttons
         "retry": false,
@@ -46,7 +50,8 @@ export class ConfigComponent implements OnInit {
 
     constructor(
         private configSvr: ConfigService,
-        private xdsSvr: XDSServerService,
+        private xdsServerSvr: XDSServerService,
+        private xdsAgentSvr: XDSAgentService,
         private stSvr: SyncthingService,
         private sdkSvr: SdkService,
         private alert: AlertService,
@@ -63,14 +68,17 @@ export class ConfigComponent implements OnInit {
     ngOnInit() {
         this.config$ = this.configSvr.conf;
         this.sdks$ = this.sdkSvr.Sdks$;
-        this.severStatus$ = this.xdsSvr.Status$;
+        this.serverStatus$ = this.xdsServerSvr.Status$;
+        this.agentStatus$ = this.xdsAgentSvr.Status$;
         this.localSTStatus$ = this.stSvr.Status$;
 
-        // Bind syncToolUrl to baseURL
+        // Bind xdsAgentUrl to baseURL
         this.config$.subscribe(cfg => {
             this.syncToolUrl = cfg.localSThg.URL;
-            this.syncToolRetry = String(cfg.localSThg.retry);
+            this.xdsAgentUrl = cfg.xdsAgent.URL;
+            this.xdsAgentRetry = String(cfg.xdsAgent.retry);
             this.projectsRootDir = cfg.projectsRootDir;
+            this.xdsAgentZipUrl = cfg.xdsAgentZipUrl;
         });
 
         // Auto create label name
@@ -93,9 +101,9 @@ export class ConfigComponent implements OnInit {
         switch (field) {
             case "retry":
                 let re = new RegExp('^[0-9]+$');
-                let rr = parseInt(this.syncToolRetry, 10);
-                if (re.test(this.syncToolRetry) && rr >= 0) {
-                    this.configSvr.syncToolRetry = rr;
+                let rr = parseInt(this.xdsAgentRetry, 10);
+                if (re.test(this.xdsAgentRetry) && rr >= 0) {
+                    this.configSvr.xdsAgentRetry = rr;
                 } else {
                     this.alert.warning("Not a valid number", true);
                 }
@@ -109,8 +117,10 @@ export class ConfigComponent implements OnInit {
         this.showApplyBtn[field] = false;
     }
 
-    syncToolRestartConn() {
+    xdsAgentRestartConn() {
+        let aurl = this.xdsAgentUrl;
         this.configSvr.syncToolURL = this.syncToolUrl;
+        this.configSvr.xdsAgentUrl = aurl;
         this.configSvr.loadProjects();
     }