Added new SDKs management support v1.0.0-rc2
authorSebastien Douheret <sebastien.douheret@iot.bzh>
Thu, 21 Dec 2017 14:07:04 +0000 (15:07 +0100)
committerSebastien Douheret <sebastien.douheret@iot.bzh>
Fri, 22 Dec 2017 23:43:11 +0000 (00:43 +0100)
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
30 files changed:
.vscode/settings.json
glide.yaml
lib/agent/apiv1-sdks.go
lib/agent/apiv1.go
lib/agent/events.go
lib/agent/project-st.go
lib/agent/projects.go
lib/agent/xdsserver.go
lib/syncthing/st.go
lib/xaapiv1/events.go
lib/xaapiv1/sdks.go
webapp/src/app/@core-xds/services/sdk.service.ts
webapp/src/app/@core-xds/services/xdsagent.service.ts
webapp/src/app/pages/build/build-settings-modal/build-settings-modal.component.html
webapp/src/app/pages/build/build.component.ts
webapp/src/app/pages/confirm/confirm-modal/confirm-modal.component.ts
webapp/src/app/pages/pages-menu.ts
webapp/src/app/pages/pages-routing.module.ts
webapp/src/app/pages/projects/project-add-modal/project-add-modal.component.ts
webapp/src/app/pages/projects/projects.component.ts
webapp/src/app/pages/sdks/sdk-card/sdk-card.component.html
webapp/src/app/pages/sdks/sdk-card/sdk-card.component.ts
webapp/src/app/pages/sdks/sdk-management/sdk-install.component.ts [new file with mode: 0644]
webapp/src/app/pages/sdks/sdk-management/sdk-management.component.html [new file with mode: 0644]
webapp/src/app/pages/sdks/sdk-management/sdk-management.component.scss [new file with mode: 0644]
webapp/src/app/pages/sdks/sdk-management/sdk-management.component.ts [new file with mode: 0644]
webapp/src/app/pages/sdks/sdks.component.html
webapp/src/app/pages/sdks/sdks.component.scss
webapp/src/app/pages/sdks/sdks.component.ts
webapp/src/app/pages/sdks/sdks.module.ts

index 524a7d8..64ef9d6 100644 (file)
@@ -5,10 +5,8 @@
   "editor.tabSize": 2,
   // Insert spaces when pressing Tab. This setting is overriden based on the file contents when `editor.detectIndentation` is on.
   "editor.insertSpaces": true,
-
   // .jsbeautifyrc not at root directory but under webapp/
   "beautify.config": "webapp/.jsbeautifyrc",
-
   // Configure glob patterns for excluding files and folders.
   "files.exclude": {
     ".tmp": true,
   ],
   // Words to add to dictionary for a workspace.
   "cSpell.words": [
-    "apiv", "gonic", "devel", "csrffound", "Syncthing", "STID", "ISTCONFIG",
-    "socketio", "ldflags", "SThg", "stconfig", "Intf", "dismissible", "rpath",
-    "WSID", "sess", "IXDS", "golib", "xdsapi", "xdsconfig", "xdsserver",
-    "xdsagent", "nbsp", "Inot", "inotify", "cmdi", "sdkid", "Flds", "prjs",
-    "iosk", "CIFS", "IPROJECT", "unregister", "conv", "PATHMAP", "nospace",
-    "graphx", "Truthy", "darkviolet", "dwnl", "topnav", "leftbar", "urfave",
-    "unmarshall", "sebd", "priv", "evts", "gdbserver", "tabset", "pageview",
-    "subpath", "prebuild", "reflectme", "franciscocpg", "xsapiv", "xaapiv",
-    "Sillyf"
+    "apiv",
+    "gonic",
+    "devel",
+    "csrffound",
+    "Syncthing",
+    "STID",
+    "ISTCONFIG",
+    "socketio",
+    "ldflags",
+    "SThg",
+    "stconfig",
+    "Intf",
+    "dismissible",
+    "rpath",
+    "WSID",
+    "sess",
+    "IXDS",
+    "golib",
+    "xdsapi",
+    "xdsconfig",
+    "xdsserver",
+    "xdsagent",
+    "nbsp",
+    "Inot",
+    "inotify",
+    "cmdi",
+    "sdkid",
+    "Flds",
+    "prjs",
+    "iosk",
+    "CIFS",
+    "IPROJECT",
+    "unregister",
+    "conv",
+    "PATHMAP",
+    "nospace",
+    "graphx",
+    "Truthy",
+    "darkviolet",
+    "dwnl",
+    "topnav",
+    "leftbar",
+    "urfave",
+    "unmarshall",
+    "sebd",
+    "priv",
+    "evts",
+    "gdbserver",
+    "tabset",
+    "pageview",
+    "subpath",
+    "prebuild",
+    "reflectme",
+    "franciscocpg",
+    "xsapiv",
+    "xaapiv",
+    "Sillyf",
+    "tabindex",
+    "EVTSDK"
   ],
   // codelyzer
   "tslint.rulesDirectory": "./webapp/node_modules/codelyzer",
   "typescript.tsdk": "webapp/node_modules/typescript/lib",
   "tslint.configFile": "webapp/tslint.json"
-}
+}
\ No newline at end of file
index 65ea68f..a08c3f5 100644 (file)
@@ -26,7 +26,7 @@ import:
   subpackages:
   - golib/common
 - package: github.com/iotbzh/xds-server
-  version: v1.0.0-rc1
+  version: v1.0.0-rc2
   subpackages:
   - lib/xsapiv1
 - package: github.com/franciscocpg/reflectme
index 7a445ce..7d6342b 100644 (file)
 
 package agent
 
+import (
+       "encoding/json"
+       "fmt"
+
+       "github.com/iotbzh/xds-agent/lib/xaapiv1"
+       "github.com/iotbzh/xds-server/lib/xsapiv1"
+)
+
 // sdksPassthroughInit Declare passthrough routes for sdks
 func (s *APIService) sdksPassthroughInit(svr *XdsServer) error {
        svr.PassthroughGet("/sdks")
        svr.PassthroughGet("/sdks/:id")
+       svr.PassthroughPost("/sdks")
+       svr.PassthroughPost("/sdks/abortinstall")
+       svr.PassthroughDelete("/sdks/:id")
+
+       return nil
+}
+
+// sdksEventsForwardInit Register events forwarder for sdks
+func (s *APIService) sdksEventsForwardInit(svr *XdsServer) error {
+
+       if !svr.Connected {
+               return fmt.Errorf("Cannot register events: XDS Server %v not connected", svr.ID)
+       }
+
+       // Forward SDK events from XDS-server to client
+       if _, err := svr.EventOn(xsapiv1.EVTSDKInstall, "", s._sdkEventInstallCB); err != nil {
+               s.Log.Errorf("XDS Server EventOn '%s' failed: %v", xsapiv1.EVTSDKInstall, err)
+               return err
+       }
+
+       if _, err := svr.EventOn(xsapiv1.EVTSDKRemove, "", s._sdkEventRemoveCB); err != nil {
+               s.Log.Errorf("XDS Server EventOn '%s' failed: %v", xsapiv1.EVTSDKRemove, err)
+               return err
+       }
+
+       return nil
+}
+
+func (s *APIService) _sdkEventInstallCB(privD interface{}, data interface{}) error {
+       // assume that xsapiv1.SDKManagementMsg == xaapiv1.SDKManagementMsg
+       evt := xaapiv1.SDKManagementMsg{}
+       evtName := xaapiv1.EVTSDKInstall
+       d, err := json.Marshal(data)
+       if err != nil {
+               s.Log.Errorf("Cannot marshal XDS Server %s: err=%v", evtName, err)
+               return err
+       }
+       if err = json.Unmarshal(d, &evt); err != nil {
+               s.Log.Errorf("Cannot unmarshal XDS Server %s: err=%v", evtName, err)
+               return err
+       }
+
+       if err := s.events.Emit(evtName, evt, ""); err != nil {
+               s.Log.Warningf("Cannot notify %s (from server): %v", evtName, err)
+               return err
+       }
+       return nil
+}
+
+func (s *APIService) _sdkEventRemoveCB(privD interface{}, data interface{}) error {
+       // assume that xsapiv1.SDKManagementMsg == xaapiv1.SDKManagementMsg
+       evt := xaapiv1.SDKManagementMsg{}
+       evtName := xaapiv1.EVTSDKRemove
+       d, err := json.Marshal(data)
+       if err != nil {
+               s.Log.Errorf("Cannot marshal XDS Server %s: err=%v", evtName, err)
+               return err
+       }
+       if err = json.Unmarshal(d, &evt); err != nil {
+               s.Log.Errorf("Cannot unmarshal XDS Server %s: err=%v", evtName, err)
+               return err
+       }
 
+       if err := s.events.Emit(evtName, evt, ""); err != nil {
+               s.Log.Warningf("Cannot notify %s (from server): %v", evtName, err)
+               return err
+       }
        return nil
 }
index 8ec26d2..d0e5a1c 100644 (file)
@@ -23,6 +23,7 @@ import (
 
        "github.com/gin-gonic/gin"
        "github.com/iotbzh/xds-agent/lib/xdsconfig"
+       "github.com/iotbzh/xds-server/lib/xsapiv1"
 )
 
 const apiBaseURL = "/api/v1"
@@ -129,11 +130,21 @@ func (s *APIService) AddXdsServer(cfg xdsconfig.XDSServerConf) (*XdsServer, erro
        // Add to map
        s.xdsServers[svr.ID] = svr
 
+       // Register event forwarder
+       s.sdksEventsForwardInit(svr)
+
        // Load projects
        if err == nil && svr.Connected {
                err = s.projects.Init(svr)
        }
 
+       // Registered to all events
+       if err == nil && svr.Connected {
+               if err = svr.EventRegister(xsapiv1.EVTAll, ""); err != nil {
+                       s.Log.Errorf("XDS Server %v - register all events error: %v", svr.ID, err)
+               }
+       }
+
        return svr, err
 }
 
index df7015a..678f116 100644 (file)
@@ -88,14 +88,14 @@ func (e *Events) UnRegister(evName, sessionID string) error {
 }
 
 // Emit Used to manually emit an event
-func (e *Events) Emit(evName string, data interface{},fromSid string) error {
+func (e *Events) Emit(evName string, data interface{}, fromSid string) error {
        var firstErr error
 
        if _, ok := e.eventsMap[evName]; !ok {
                return fmt.Errorf("Unsupported event type")
        }
 
-               e.LogSillyf("Emit Event %s: %v", evName, data)
+       e.LogSillyf("Emit Event %s: %v", evName, data)
 
        firstErr = nil
        evm := e.eventsMap[evName]
@@ -113,7 +113,7 @@ func (e *Events) Emit(evName string, data interface{},fromSid string) error {
                        Type:          evName,
                        Data:          data,
                }
-               e.Log.Debugf("Emit Event %s: %v", evName, sid)
+
                if err := (*so).Emit(evName, msg); err != nil {
                        e.Log.Errorf("WS Emit %v error : %v", evName, err)
                        if firstErr == nil {
index 8ac5f36..c92f5d4 100644 (file)
@@ -106,9 +106,8 @@ func (p *STProject) Setup(prj xaapiv1.ProjectConfig) (*xaapiv1.ProjectConfig, er
 
        // Register events to update folder status
        // Register to XDS Server events
-       p.server.EventOn(xsapiv1.EVTFolderStateChange, "", p._cbServerFolderChanged)
-       if err := p.server.EventRegister(xsapiv1.EVTFolderStateChange, svrPrj.ID); err != nil {
-               p.Log.Warningf("XDS Server EventRegister failed: %v", err)
+       if _, err := p.server.EventOn(xsapiv1.EVTFolderStateChange, "", p._cbServerFolderChanged); err != nil {
+               p.Log.Errorf("XDS Server EventOn '%s' failed: %v", xsapiv1.EVTFolderStateChange, err)
                return svrPrj, err
        }
 
index d6268fa..7393364 100644 (file)
@@ -75,6 +75,11 @@ func (p *Projects) Init(server *XdsServer) error {
                for _, prj := range xFlds {
                        newP := svr.FolderToProject(prj)
                        if _, err := p.createUpdate(newP, false, true); err != nil {
+                               // Don't consider that as an error (allow support config without CloudSync support)
+                               if p.Context.SThg == nil && strings.Contains(err.Error(), "Server doesn't support project type CloudSync") {
+                                       continue
+                               }
+
                                errMsg += "Error while creating project id " + prj.ID + ": " + err.Error() + "\n "
                                continue
                        }
index 32656cf..3ec6123 100644 (file)
@@ -21,7 +21,6 @@ import (
        "encoding/json"
        "fmt"
        "io"
-       "io/ioutil"
        "net/http"
        "strings"
        "sync"
@@ -239,8 +238,11 @@ func (xs *XdsServer) PassthroughPost(url string) {
        }
 
        xs.apiRouter.POST(url, func(c *gin.Context) {
-               bodyReq := []byte{}
-               n, err := c.Request.Body.Read(bodyReq)
+               var err error
+               var data interface{}
+
+               // Get raw body
+               body, err := c.GetRawData()
                if err != nil {
                        common.APIError(c, err.Error())
                        return
@@ -253,34 +255,80 @@ func (xs *XdsServer) PassthroughPost(url string) {
                }
 
                // Send Post request
-               body, err := json.Marshal(bodyReq[:n])
+               response, err := xs.client.HTTPPostWithRes(nURL, string(body))
                if err != nil {
-                       common.APIError(c, err.Error())
+                       goto httpError
+               }
+               if response.StatusCode != 200 {
+                       err = fmt.Errorf(response.Status)
                        return
                }
-
-               response, err := xs.client.HTTPPostWithRes(nURL, string(body))
+               err = json.Unmarshal(xs.client.ResponseToBArray(response), &data)
                if err != nil {
-                       common.APIError(c, err.Error())
-                       return
+                       goto httpError
                }
 
-               bodyRes, err := ioutil.ReadAll(response.Body)
+               c.JSON(http.StatusOK, data)
+               return
+
+               /* Handle error case */
+       httpError:
+               if strings.Contains(err.Error(), "connection refused") {
+                       xs._Disconnected()
+               }
+               common.APIError(c, err.Error())
+       })
+}
+
+// PassthroughDelete Used to declare a route that sends directly a Delete request to XDS Server
+func (xs *XdsServer) PassthroughDelete(url string) {
+       if xs.apiRouter == nil {
+               xs.Log.Errorf("apiRouter not set !")
+               return
+       }
+
+       xs.apiRouter.DELETE(url, func(c *gin.Context) {
+               var err error
+               var data interface{}
+
+               // Take care of param (eg. id in /projects/:id)
+               nURL := url
+               if strings.Contains(url, ":") {
+                       nURL = strings.TrimPrefix(c.Request.URL.Path, xs.APIURL)
+               }
+
+               // Send Post request
+               response, err := xs.client.HTTPDeleteWithRes(nURL)
                if err != nil {
-                       common.APIError(c, "Cannot read response body")
+                       goto httpError
+               }
+               if response.StatusCode != 200 {
+                       err = fmt.Errorf(response.Status)
                        return
                }
-               c.JSON(http.StatusOK, string(bodyRes))
+               err = json.Unmarshal(xs.client.ResponseToBArray(response), &data)
+               if err != nil {
+                       goto httpError
+               }
+
+               c.JSON(http.StatusOK, data)
+               return
+
+               /* Handle error case */
+       httpError:
+               if strings.Contains(err.Error(), "connection refused") {
+                       xs._Disconnected()
+               }
+               common.APIError(c, err.Error())
        })
 }
 
 // EventRegister Post a request to register to an XdsServer event
-func (xs *XdsServer) EventRegister(evName string, id string) error {
-       return xs.client.Post(
-               "/events/register",
+func (xs *XdsServer) EventRegister(evName string, filter string) error {
+       return xs.client.Post("/events/register",
                xsapiv1.EventRegisterArgs{
-                       Name:      evName,
-                       ProjectID: id,
+                       Name:   evName,
+                       Filter: filter,
                },
                nil)
 }
@@ -308,15 +356,15 @@ func (xs *XdsServer) EventOn(evName string, privData interface{}, f EventCB) (uu
                evn := evName
 
                err := xs.ioSock.On(evn, func(data interface{}) error {
-                               xs.sockEventsLock.Lock()
-                               sEvts := make([]*caller, len(xs.sockEvents[evn]))
-                               copy(sEvts, xs.sockEvents[evn])
-                               xs.sockEventsLock.Unlock()
-                               for _, c := range sEvts {
-                                       c.Func(c.PrivateData, data)
-                               }
-                               return nil
-                       })
+                       xs.sockEventsLock.Lock()
+                       sEvts := make([]*caller, len(xs.sockEvents[evn]))
+                       copy(sEvts, xs.sockEvents[evn])
+                       xs.sockEventsLock.Unlock()
+                       for _, c := range sEvts {
+                               c.Func(c.PrivateData, data)
+                       }
+                       return nil
+               })
                if err != nil {
                        return uuid.Nil, err
                }
@@ -493,6 +541,10 @@ func (xs *XdsServer) _Reconnect() error {
                // Reload projects list for this server
                err = xs.projects.Init(xs)
        }
+       if err == nil {
+               // Register again to all events
+               err = xs.EventRegister(xsapiv1.EVTAll, "")
+       }
        return err
 }
 
index c4b72c5..1ea3947 100644 (file)
@@ -188,7 +188,7 @@ func (s *SyncThing) startProc(exeName string, args []string, env []string, eChan
 
                cmdOut, err := cmd.StdoutPipe()
                if err != nil {
-                       return nil, fmt.Errorf("Pipe stdout error for : %s", err)
+                       return nil, fmt.Errorf("Pipe stdout error for : %v", err)
                }
 
                go io.Copy(outfile, cmdOut)
index 0ac08e8..16c2dd7 100644 (file)
@@ -45,6 +45,8 @@ const (
        EVTProjectAdd    = EventTypePrefix + "project-add"          // type EventMsg with Data type xaapiv1.ProjectConfig
        EVTProjectDelete = EventTypePrefix + "project-delete"       // type EventMsg with Data type xaapiv1.ProjectConfig
        EVTProjectChange = EventTypePrefix + "project-state-change" // type EventMsg with Data type xaapiv1.ProjectConfig
+       EVTSDKInstall    = EventTypePrefix + "sdk-install"          // type EventMsg with Data type xaapiv1.SDKManagementMsg
+       EVTSDKRemove     = EventTypePrefix + "sdk-remove"           // type EventMsg with Data type xaapiv1.SDKManagementMsg
 )
 
 // EVTAllList List of all supported events
@@ -53,6 +55,8 @@ var EVTAllList = []string{
        EVTProjectAdd,
        EVTProjectDelete,
        EVTProjectChange,
+       EVTSDKInstall,
+       EVTSDKRemove,
 }
 
 // EventMsg Event message send over Websocket, data format depend to Type (see DecodeXXX function)
@@ -92,3 +96,20 @@ func (e *EventMsg) DecodeProjectConfig() (ProjectConfig, error) {
        }
        return p, err
 }
+
+// DecodeSDKMsg Helper to decode Data field type SDKManagementMsg
+func (e *EventMsg) DecodeSDKMsg() (SDKManagementMsg, error) {
+       var err error
+       s := SDKManagementMsg{}
+       switch e.Type {
+       case EVTSDKInstall, EVTSDKRemove:
+               d := []byte{}
+               d, err = json.Marshal(e.Data)
+               if err == nil {
+                       err = json.Unmarshal(d, &s)
+               }
+       default:
+               err = fmt.Errorf("Invalid type")
+       }
+       return s, err
+}
index 2dceecf..589f748 100644 (file)
 
 package xaapiv1
 
+// SDK status definition
+const (
+       SdkStatusDisable      = "Disable"
+       SdkStatusNotInstalled = "Not Installed"
+       SdkStatusInstalling   = "Installing"
+       SdkStatusUninstalling = "Un-installing"
+       SdkStatusInstalled    = "Installed"
+)
+
 // SDK Define a cross tool chain used to build application
 type SDK struct {
-       ID      string `json:"id" binding:"required"`
-       Name    string `json:"name"`
-       Profile string `json:"profile"`
-       Version string `json:"version"`
-       Arch    string `json:"arch"`
-       Path    string `json:"path"`
+       ID          string `json:"id" binding:"required"`
+       Name        string `json:"name"`
+       Description string `json:"description"`
+       Profile     string `json:"profile"`
+       Version     string `json:"version"`
+       Arch        string `json:"arch"`
+       Path        string `json:"path"`
+       URL         string `json:"url"`
+       Status      string `json:"status"`
+       Date        string `json:"date"`
+       Size        string `json:"size"`
+       Md5sum      string `json:"md5sum"`
+       SetupFile   string `json:"setupFile"`
+       LastError   string `json:"lastError"`
+}
+
+// SDKInstallArgs JSON parameters of POST /sdks or /sdks/abortinstall commands
+type SDKInstallArgs struct {
+       ID       string `json:"id" binding:"required"` // install by ID (must be part of GET /sdks result)
+       Filename string `json:"filename"`              // install by using a file
+       Force    bool   `json:"force"`                 // force SDK install when already existing
+       Timeout  int    `json:"timeout"`               // 1800 == default 30 minutes
+}
+
+// SDKManagementMsg Message send during SDK installation or when installation is complete
+type SDKManagementMsg struct {
+       CmdID     string `json:"cmdID"`
+       Timestamp string `json:"timestamp"`
+       Sdk       SDK    `json:"sdk"`
+       Stdout    string `json:"stdout"`
+       Stderr    string `json:"stderr"`
+       Progress  int    `json:"progress"` // 0 = not started to 100% = complete
+       Exited    bool   `json:"exited"`
+       Code      int    `json:"code"`
+       Error     string `json:"error"`
 }
index 2d54d4e..8fa6ad2 100644 (file)
@@ -16,7 +16,7 @@
 * limitations under the License.
 */
 
-import { Injectable, SecurityContext } from '@angular/core';
+import { Injectable, SecurityContext, isDevMode } from '@angular/core';
 import { Observable } from 'rxjs/Observable';
 import { BehaviorSubject } from 'rxjs/BehaviorSubject';
 
@@ -25,60 +25,159 @@ import { XDSAgentService } from '../services/xdsagent.service';
 import 'rxjs/add/observable/throw';
 
 export interface ISdk {
-    id: string;
-    profile: string;
-    version: string;
-    arch: number;
-    path: string;
+  id: string;
+  name: string;
+  description: string;
+  profile: string;
+  version: string;
+  arch: string;
+  path: string;
+  url: string;
+  status: string;
+  date: string;
+  size: string;
+  md5sum: string;
+  setupFile: string;
+  lastError: string;
+}
+
+export interface ISdkManagementMsg {
+  cmdID: string;
+  timestamp: string;
+  sdk: ISdk;
+  stdout: string;
+  stderr: string;
+  progress: number;
+  exited: boolean;
+  code: number;
+  error: string;
 }
 
 @Injectable()
 export class SdkService {
-    public Sdks$: Observable<ISdk[]>;
-
-    private _sdksList = [];
-    private current: ISdk;
-    private sdksSubject = <BehaviorSubject<ISdk[]>>new BehaviorSubject(this._sdksList);
-
-    constructor(private xdsSvr: XDSAgentService) {
-        this.current = null;
-        this.Sdks$ = this.sdksSubject.asObservable();
-
-        this.xdsSvr.XdsConfig$.subscribe(cfg => {
-            if (!cfg || cfg.servers.length < 1) {
-                return;
-            }
-            // FIXME support multiple server
-            // cfg.servers.forEach(svr => {
-            this.xdsSvr.getSdks(cfg.servers[0].id).subscribe((s) => {
-                this._sdksList = s;
-                this.sdksSubject.next(s);
-            });
-        });
-    }
+  public Sdks$: Observable<ISdk[]>;
+  public curSdk$: Observable<ISdk>;
 
-    public setCurrent(s: ISdk) {
-        this.current = s;
-    }
+  private _sdksList = [];
+  private sdksSubject = <BehaviorSubject<ISdk[]>>new BehaviorSubject(this._sdksList);
+  private current: ISdk;
+  private curSdkSubject = <BehaviorSubject<ISdk>>new BehaviorSubject(this.current);
+  private curServerID;
 
-    public getCurrent(): ISdk {
-        return this.current;
-    }
+  constructor(private xdsSvr: XDSAgentService) {
+    this.current = null;
+    this.Sdks$ = this.sdksSubject.asObservable();
+    this.curSdk$ = this.curSdkSubject.asObservable();
 
-    public getCurrentId(): string {
-        if (this.current && this.current.id) {
-            return this.current.id;
+    this.xdsSvr.XdsConfig$.subscribe(cfg => {
+      if (!cfg || cfg.servers.length < 1) {
+        return;
+      }
+      // FIXME support multiple server
+      // cfg.servers.forEach(svr => {
+      this.curServerID = cfg.servers[0].id;
+      this.xdsSvr.getSdks(this.curServerID).subscribe((sdks) => {
+        this._sdksList = [];
+        sdks.forEach(s => {
+          this._addSdk(s, true);
+        });
+
+        // TODO: get previous val from xds-config service / cookie
+        if (this._sdksList.length > 0) {
+          this.current = this._sdksList[0];
+          this.curSdkSubject.next(this.current);
         }
-        return '';
+
+        this.sdksSubject.next(this._sdksList);
+      });
+    });
+
+    // Add listener on sdk creation, deletion and change events
+    this.xdsSvr.onSdkInstall().subscribe(evMgt => {
+      this._addSdk(evMgt.sdk);
+    });
+    this.xdsSvr.onSdkRemove().subscribe(evMgt => {
+      if (evMgt.sdk.status !== 'Not Installed') {
+        /* tslint:disable:no-console */
+        console.log('Error: received event:sdk-remove with invalid status: evMgt=', evMgt);
+        return;
+      }
+      this._delSdk(evMgt.sdk);
+    });
+
+  }
+
+  public setCurrent(s: ISdk) {
+    this.current = s;
+  }
+
+  public getCurrent(): ISdk {
+    return this.current;
+  }
+
+  public getCurrentId(): string {
+    if (this.current && this.current.id) {
+      return this.current.id;
     }
+    return '';
+  }
+
+  public install(sdk: ISdk): Observable<ISdk> {
+    return this.xdsSvr.installSdk(this.curServerID, sdk.id);
+  }
+
+  public onInstall(): Observable<ISdkManagementMsg> {
+    return this.xdsSvr.onSdkInstall();
+  }
+
+  public abortInstall(sdk: ISdk): Observable<ISdk> {
+    return this.xdsSvr.abortInstall(this.curServerID, sdk.id);
+  }
+
+  public remove(sdk: ISdk): Observable<ISdk> {
+    return this.xdsSvr.removeSdk(this.curServerID, sdk.id);
+  }
+
+  /** Private **/
 
-    public add(sdk: ISdk): Observable<ISdk> {
-      // TODO SEB
-      return Observable.throw('Not implement yet');
+  private _addSdk(sdk: ISdk, noNext?: boolean): ISdk {
+
+    // add new sdk
+    this._sdksList.push(sdk);
+
+    // sort sdk array
+    this._sdksList.sort((a, b) => {
+      if (a.name < b.name) {
+        return -1;
+      }
+      if (a.name > b.name) {
+        return 1;
+      }
+      return 0;
+    });
+
+    if (!noNext) {
+      this.sdksSubject.next(this._sdksList);
     }
 
-    public delete(sdk: ISdk): Observable<ISdk> {
-      // TODO SEB
-      return Observable.throw('Not implement yet');
+    return sdk;
+  }
+
+  private _delSdk(sdk: ISdk) {
+    const idx = this._sdksList.findIndex(item => item.id === sdk.id);
+    if (idx === -1) {
+      if (isDevMode) {
+        /* tslint:disable:no-console */
+        console.log('Warning: Try to delete sdk unknown id: sdk=', sdk);
+      }
+      return;
+    }
+    const delId = this._sdksList[idx].id;
+    this._sdksList.splice(idx, 1);
+    if (delId === this.current.id) {
+      this.setCurrent(this._sdksList[0]);
     }
+    this.sdksSubject.next(this._sdksList);
+  }
+
 }
index 94d3dec..9f8385a 100644 (file)
@@ -25,7 +25,7 @@ import { BehaviorSubject } from 'rxjs/BehaviorSubject';
 import * as io from 'socket.io-client';
 
 import { AlertService } from './alert.service';
-import { ISdk } from './sdk.service';
+import { ISdk, ISdkManagementMsg } from './sdk.service';
 import { ProjectType, ProjectTypeEnum } from './project.service';
 
 // Import RxJs required methods
@@ -34,7 +34,7 @@ import 'rxjs/add/operator/catch';
 import 'rxjs/add/observable/throw';
 import 'rxjs/add/operator/mergeMap';
 import 'rxjs/add/observable/of';
-import 'rxjs/add/operator/retryWhen';
+import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
 
 
 export interface IXDSConfigProject {
@@ -133,6 +133,9 @@ export class XDSAgentService {
   protected projectDel$ = new Subject<IXDSProjectConfig>();
   protected projectChange$ = new Subject<IXDSProjectConfig>();
 
+  protected sdkInstall$ = new Subject<ISdkManagementMsg>();
+  protected sdkRemove$ = new Subject<ISdkManagementMsg>();
+
   private baseUrl: string;
   private wsUrl: string;
   private httpSessionID: string;
@@ -249,7 +252,7 @@ export class XDSAgentService {
     this.socket.on('event:project-add', (ev) => {
       if (ev && ev.data && ev.data.id) {
         this.projectAdd$.next(Object.assign({}, ev.data));
-        if (ev.sessionID !== this.httpSessionID && ev.data.label) {
+        if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && ev.data.label) {
           this.alert.info('Project "' + ev.data.label + '" has been added by another tool.');
         }
       } else if (isDevMode) {
@@ -261,7 +264,7 @@ export class XDSAgentService {
     this.socket.on('event:project-delete', (ev) => {
       if (ev && ev.data && ev.data.id) {
         this.projectDel$.next(Object.assign({}, ev.data));
-        if (ev.sessionID !== this.httpSessionID && ev.data.label) {
+        if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && ev.data.label) {
           this.alert.info('Project "' + ev.data.label + '" has been deleted by another tool.');
         }
       } else if (isDevMode) {
@@ -273,10 +276,36 @@ export class XDSAgentService {
       if (ev && ev.data) {
         this.projectChange$.next(Object.assign({}, ev.data));
       } else if (isDevMode) {
-        console.log('Warning: received event:project-state-change with unknown data: ev=', ev);
+        console.log('Warning: received event:project-state-change with unkn220own data: ev=', ev);
+      }
+    });
+
+    this.socket.on('event:sdk-install', (ev) => {
+      if (ev && ev.data && ev.data.sdk) {
+        const evt = <ISdkManagementMsg>ev.data;
+        this.sdkInstall$.next(Object.assign({}, evt));
+
+        if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && evt.sdk.name) {
+          this.alert.info('SDK "' + evt.sdk.name + '" has been installed by another tool.');
+        }
+      } else if (isDevMode) {
+        /* tslint:disable:no-console */
+        console.log('Warning: received event:sdk-install with unknown data: ev=', ev);
       }
     });
 
+    this.socket.on('event:sdk-remove', (ev) => {
+      if (ev && ev.data && ev.data.sdk) {
+        const evt = <ISdkManagementMsg>ev.data;
+        this.sdkRemove$.next(Object.assign({}, evt));
+
+        if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && evt.sdk.name) {
+          this.alert.info('SDK "' + evt.sdk.name + '" has been removed by another tool.');
+        }
+      } else if (isDevMode) {
+        console.log('Warning: received event:sdk-remove with unknown data: ev=', ev);
+      }
+    });
   }
 
   /**
@@ -294,6 +323,14 @@ export class XDSAgentService {
     return this.projectChange$.asObservable();
   }
 
+  onSdkInstall(): Observable<ISdkManagementMsg> {
+    return this.sdkInstall$.asObservable();
+  }
+
+  onSdkRemove(): Observable<ISdkManagementMsg> {
+    return this.sdkRemove$.asObservable();
+  }
+
   /**
   ** Misc / Version
   ***/
@@ -355,10 +392,22 @@ export class XDSAgentService {
     if (!svr || !svr.connected) {
       return Observable.of([]);
     }
-
     return this._get(svr.partialUrl + '/sdks');
   }
 
+  installSdk(serverID: string, id: string, filename?: string, force?: boolean): Observable<ISdk> {
+    return this._post(this._getServerUrl(serverID) + '/sdks', { id: id, filename: filename, force: force });
+  }
+
+  abortInstall(serverID: string, id: string): Observable<ISdk> {
+    return this._post(this._getServerUrl(serverID) + '/sdks/abortinstall', { id: id });
+  }
+
+  removeSdk(serverID: string, id: string): Observable<ISdk> {
+    return this._delete(this._getServerUrl(serverID) + '/sdks/' + id);
+  }
+
+
   /***
   ** Projects
   ***/
@@ -420,6 +469,17 @@ export class XDSAgentService {
     return svr[0];
   }
 
+  private _getServerUrl(serverID: string): string | ErrorObservable {
+    const svr = this._getServer(serverID);
+    if (!svr || !svr.connected) {
+      if (isDevMode) {
+        console.log('ERROR: XDS Server unknown: serverID=' + serverID);
+      }
+      return Observable.throw('Cannot identify XDS Server');
+    }
+    return svr.partialUrl;
+  }
+
   private _attachAuthHeaders(options?: any) {
     options = options || {};
     const headers = options.headers || new HttpHeaders();
index 7dd2ec7..4890b91 100644 (file)
@@ -44,7 +44,7 @@
         </div>
       </div>
 
-      <div class="offset-sm-9 col-sm-9">
+      <div>
         <button class="btn btn-sm btn-hero-secondary" (click)="closeAction=false; resetDefault()">Reset settings</button>
       </div>
     </form>
index f937fc3..9a47c35 100644 (file)
@@ -105,9 +105,10 @@ export class BuildComponent implements OnInit, AfterViewChecked {
     if (!this.isSetupValid()) {
       return this.alertSvr.warning('Please select first a valid project.', true);
     }
-
-    const activeModal = this.modalService.open(BuildSettingsModalComponent, { size: 'lg', container: 'nb-layout' });
-    activeModal.componentInstance.modalHeader = 'Large Modal';
+    const modal = this.modalService.open(
+      BuildSettingsModalComponent,
+      { size: 'lg', container: 'nb-layout' },
+    );
   }
 
   execCmd(cmdName: string) {
@@ -134,7 +135,7 @@ export class BuildComponent implements OnInit, AfterViewChecked {
         cmd = this.curPrj.uiSettings.cmdPopulate;
         break;
       case 'exec':
-        if (this.curPrj.uiSettings.cmdArgs instanceof Array) Â {
+        if (this.curPrj.uiSettings.cmdArgs instanceof Array) {
           cmd = this.curPrj.uiSettings.cmdArgs.join(' ');
         } else {
           cmd = this.curPrj.uiSettings.cmdArgs;
index 3282d6b..b5b5db0 100644 (file)
@@ -24,6 +24,7 @@ export enum EType {
   YesNo = 1,
   OKCancel,
   OK,
+  Cancel,
 }
 
 @Component({
@@ -77,6 +78,10 @@ export class ConfirmModalComponent implements OnInit {
         this.textBtn = [ 'OK', '', '' ];
         break;
 
+      case EType.Cancel:
+        this.textBtn = [ '', 'Cancel', '' ];
+      break;
+
       case EType.OKCancel:
         this.textBtn = [ 'OK', 'Cancel', '' ];
       break;
index eb5462c..c212179 100644 (file)
@@ -56,6 +56,14 @@ export const MENU_ITEMS: NbMenuItem[] = [
     title: 'SDKs',
     icon: 'fa fa-file-archive-o',
     link: '/pages/sdks',
+    /*
+    children: [
+      {
+        title: 'SDKs Management',
+        link: '/pages/sdks/management',
+      },
+    ],
+    */
   },
   {
     title: 'Boards',
index beaae51..df28160 100644 (file)
@@ -23,6 +23,7 @@ import { PagesComponent } from './pages.component';
 import { DashboardComponent } from './dashboard/dashboard.component';
 import { ProjectsComponent } from './projects/projects.component';
 import { SdksComponent } from './sdks/sdks.component';
+import { SdkManagementComponent } from './sdks/sdk-management/sdk-management.component';
 import { BuildComponent } from './build/build.component';
 
 const routes: Routes = [{
@@ -37,6 +38,9 @@ const routes: Routes = [{
   }, {
     path: 'sdks',
     component: SdksComponent,
+  }, {
+    path: 'sdks/management',
+    component: SdkManagementComponent,
   }, {
     path: 'build',
     component: BuildComponent,
index adfc1d8..9c2f95f 100644 (file)
@@ -16,7 +16,7 @@
 * limitations under the License.
 */
 
-import { Component, Input, ViewChild, OnInit } from '@angular/core';
+import { Component, ViewEncapsulation, Input, OnInit } from '@angular/core';
 import { Observable } from 'rxjs/Observable';
 import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
 import { FormControl, FormGroup, Validators, ValidationErrors, FormBuilder, ValidatorFn, AbstractControl } from '@angular/forms';
@@ -34,6 +34,13 @@ import { XDSConfigService } from '../../../@core-xds/services/xds-config.service
 @Component({
   selector: 'xds-project-add-modal',
   templateUrl: 'project-add-modal.component.html',
+  encapsulation: ViewEncapsulation.None,
+  styles: [`
+    .modal-xxl .modal-lg {
+      width: 90%;
+      max-width:1200px;
+    }
+  `],
 })
 export class ProjectAddModalComponent implements OnInit {
   // @Input('server-id') serverID: string;
index 13f7a79..3d271e1 100644 (file)
@@ -45,7 +45,10 @@ export class ProjectsComponent implements OnInit {
   }
 
   add() {
-    const activeModal = this.modalService.open(ProjectAddModalComponent, { size: 'lg', container: 'nb-layout' });
-    activeModal.componentInstance.modalHeader = 'Large Modal';
+    const activeModal = this.modalService.open(ProjectAddModalComponent, {
+      size: 'lg',
+      windowClass: 'modal-xxl',
+      container: 'nb-layout',
+    });
   }
 }
index 0c2787c..2edc0d3 100644 (file)
@@ -3,10 +3,10 @@
 
     <div class="row">
       <div class="col-12 col-md-8">
-        {{ labelGet(sdk) }}
+        {{ sdk.name }}
       </div>
       <div class="col-6 col-md-4 text-right" role="group">
-        <button class="btn btn-outline-danger btn-tn btn-xds" (click)="delete(sdk)">
+        <button class="btn btn-outline-danger btn-tn btn-xds" (click)="remove(sdk)">
           <span class="fa fa-trash fa-size-x2"></span>
         </button>
       </div>
           </th>
           <td>{{ sdk.id }}</td>
         </tr>
+        <tr>
+          <th><span class="fa fa-fw fa-file-text-o"></span>&nbsp;<span>Description</span></th>
+          <td>{{ sdk.description }}</td>
+        </tr>
         <tr>
           <th><span class="fa fa-fw fa-user"></span>&nbsp;<span>Profile</span></th>
           <td>{{ sdk.profile }}</td>
-        </tr>            <tr>
-            <th><span class="fa fa-fw fa-tasks"></span>&nbsp;<span>Architecture</span></th>
-            <td>{{ sdk.arch }}</td>
         </tr>
         <tr>
-            <th><span class="fa fa-fw fa-code-fork"></span>&nbsp;<span>Version</span></th>
-            <td>{{ sdk.version }}</td>
+          <th><span class="fa fa-fw fa-tasks"></span>&nbsp;<span>Architecture</span></th>
+          <td>{{ sdk.arch }}</td>
+        </tr>
+        <tr>
+          <th><span class="fa fa-fw fa-code-fork"></span>&nbsp;<span>Version</span></th>
+          <td>{{ sdk.version }}</td>
         </tr>
         <tr>
-            <th><span class="fa fa-fw fa-folder-open-o"></span>&nbsp;<span>Sdk path</span></th>
-            <td>{{ sdk.path}}</td>
+          <th><span class="fa fa-fw fa-folder-open-o"></span>&nbsp;<span>Sdk path</span></th>
+          <td>{{ sdk.path}}</td>
         </tr>
       </tbody>
     </table>
index d41e2fb..997f01d 100644 (file)
@@ -20,6 +20,8 @@ import { Component, Input, Pipe, PipeTransform } from '@angular/core';
 import { SdkService, ISdk } from '../../../@core-xds/services/sdk.service';
 import { AlertService } from '../../../@core-xds/services/alert.service';
 
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
+import { ConfirmModalComponent, EType } from '../../confirm/confirm-modal/confirm-modal.component';
 
 @Component({
   selector: 'xds-sdk-card',
@@ -36,18 +38,32 @@ export class SdkCardComponent {
   constructor(
     private alert: AlertService,
     private sdkSvr: SdkService,
+    private modalService: NgbModal,
   ) {
   }
 
-  labelGet(sdk: ISdk) {
-    return sdk.profile + '-' + sdk.arch + '-' + sdk.version;
-  }
+  remove(sdk: ISdk) {
+    const modal = this.modalService.open(ConfirmModalComponent, {
+      size: 'lg',
+      backdrop: 'static',
+      container: 'nb-layout',
+    });
+    modal.componentInstance.title = 'Confirm SDK deletion';
+    modal.componentInstance.type = EType.YesNo;
+    modal.componentInstance.question = `
+    Do you <b>permanently remove '` + sdk.name + `'</b> SDK ?
+    <br><br>
+    <i><small>(SDK ID: ` + sdk.id + ` )</small></i>`;
 
-  delete(sdk: ISdk) {
-    this.sdkSvr.delete(sdk).subscribe(
-      res => { },
-      err => this.alert.error('ERROR delete: ' + err),
-    );
+    modal.result
+      .then(res => {
+        if (res === 'yes') {
+          this.sdkSvr.remove(sdk).subscribe(
+            r => { },
+            err => this.alert.error(err),
+          );
+        }
+      });
   }
 }
 
diff --git a/webapp/src/app/pages/sdks/sdk-management/sdk-install.component.ts b/webapp/src/app/pages/sdks/sdk-management/sdk-install.component.ts
new file mode 100644 (file)
index 0000000..c9c518f
--- /dev/null
@@ -0,0 +1,131 @@
+/**
+* @license
+* Copyright (C) 2017 "IoT.bzh"
+* Author Sebastien Douheret <sebastien@iot.bzh>
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*   http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+import { Component, OnInit, Input, ViewChild, AfterViewChecked, ElementRef } from '@angular/core';
+import { DomSanitizer } from '@angular/platform-browser';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { AlertService } from '../../../@core-xds/services/alert.service';
+import { SdkService, ISdk } from '../../../@core-xds/services/sdk.service';
+
+@Component({
+  selector: 'xds-sdk-install-modal',
+  template: `
+  <div tabindex="-1">
+    <div class="modal-header">
+      SDK installation
+    </div>
+
+    <div class="modal-body row">
+      <div class="col-12 text-center">
+        Installation of <b> {{ sdk?.name }} '</b> <span [innerHTML]="instStatus"></span>
+      </div>
+      <br>
+      <br>
+      <div class="col-12 text-center">
+        <textarea rows="20" class="textarea-scroll" #scrollOutput [innerHtml]="installOutput"></textarea>
+      </div>
+      <div class="col-12 text-center">
+        <button type="button" class="btn" tabindex="1"
+        [ngClass]="(btnName=='Cancel')?'btn-default':'btn-primary'"
+        (click)="onBtnClick()">{{ btnName }}</button>
+      </div>
+    </div>
+
+    <!-- <div *ngIf="footer!=''" class="modal-footer">
+      <div class="col-12 text-center">
+      </div>
+    </div> -->
+  </div>
+  `,
+  styles: [`
+    .btn {
+      margin-top: 2em;
+      min-width: 10em;
+    }
+    .textarea-scroll {
+      font-family: monospace;
+      width: 100%;
+      overflow-y: scroll;
+  `],
+})
+
+export class SdkInstallComponent implements OnInit {
+  @Input() sdk;
+  @ViewChild('scrollOutput') private scrollContainer: ElementRef;
+
+  constructor(
+    private modalRef: NgbActiveModal,
+    private sanitizer: DomSanitizer,
+    private alert: AlertService,
+    private sdkSvr: SdkService,
+  ) { }
+
+  onInstallSub: any;
+  installOutput = '';
+  btnName = 'Cancel';
+  instStatus = '';
+
+  ngOnInit() {
+    this.instStatus = 'in progress...';
+
+    this.onInstallSub = this.sdkSvr.onInstall().subscribe(ev => {
+      if (ev.exited) {
+        this.btnName = 'OK';
+        this.instStatus = '<font color="green"> Done. </font>';
+
+        if (ev.code === 0) {
+          this.alert.info('SDK ' + ev.sdk.name + ' successfully installed.');
+
+        } else {
+          if (ev.sdk.lastError !== '') {
+            this.alert.error(ev.sdk.lastError);
+          } else {
+            this.alert.error('SDK ' + ev.sdk.name + ' installation failed. ' + ev.error);
+          }
+        }
+
+      } else {
+        if (ev.stdout !== '') {
+          this.installOutput += ev.stdout;
+        }
+        if (ev.stderr !== '') {
+          this.installOutput += ev.stderr;
+        }
+        this._scrollToBottom();
+      }
+    });
+  }
+
+  onBtnClick(): void {
+    this.onInstallSub.unsubscribe();
+    if (this.btnName === 'Cancel') {
+      this.btnName = 'OK';
+      this.instStatus = '<b><font color="red"> ABORTED </font></b>';
+      this.sdkSvr.abortInstall(this.sdk).subscribe(r => { }, err => this.alert.error(err));
+    } else {
+      this.modalRef.close();
+    }
+  }
+
+  private _scrollToBottom(): void {
+    try {
+      this.scrollContainer.nativeElement.scrollTop = this.scrollContainer.nativeElement.scrollHeight;
+    } catch (err) { }
+  }
+}
diff --git a/webapp/src/app/pages/sdks/sdk-management/sdk-management.component.html b/webapp/src/app/pages/sdks/sdk-management/sdk-management.component.html
new file mode 100644 (file)
index 0000000..0587194
--- /dev/null
@@ -0,0 +1,26 @@
+<div class="row">
+  <div class="col-12">
+    <nb-card-body>
+      <div class="col-9">
+        <nb-actions size="medium">
+          <nb-action>
+            <a href="#/pages/sdks" class="btn" role="Button">
+            <i id="hack-i" class="fa fa-list-alt"></i>
+            <span id="hack-span">Switch to basic SDKs view</span>
+        </a>
+          </nb-action>
+        </nb-actions>
+      </div>
+    </nb-card-body>
+  </div>
+
+  <nb-card>
+    <nb-card-header>
+      <span>SDKs Management</span>
+    </nb-card-header>
+
+    <nb-card-body>
+      <ng2-smart-table [settings]="settings" [source]="sdks" (custom)="onCustom($event)"></ng2-smart-table>
+    </nb-card-body>
+  </nb-card>
+</div>
diff --git a/webapp/src/app/pages/sdks/sdk-management/sdk-management.component.scss b/webapp/src/app/pages/sdks/sdk-management/sdk-management.component.scss
new file mode 100644 (file)
index 0000000..0ba2741
--- /dev/null
@@ -0,0 +1,83 @@
+@import '../../../@theme/styles/themes';
+@import '~@nebular/theme/components/card/card.component.theme';
+// FIXME SEB: remove this ugly hack and use nb-theme
+#hack-i {
+  color: #a4abb3; // nb-theme(color-fg);
+  font-size: 1.5rem;
+  margin-right: 1rem;
+}
+
+#hack-span {
+  font-family: nb-theme(font-secondary);
+  font-weight: nb-theme(font-weight-bold);
+  color: #2a2a2a; // nb-theme(color-fg-heading);
+  text-transform: uppercase;
+}
+
+@include nb-install-component() {
+  nb-card-body {
+    display: flex;
+    align-items: center;
+  }
+  .action-groups-header {
+    flex-basis: 20%;
+    color: nb-theme(card-header-fg-heading);
+    font-family: nb-theme(card-header-font-family);
+    font-size: nb-theme(card-header-font-size);
+    font-weight: nb-theme(card-header-font-weight);
+  }
+  .nb-actions {
+    flex-basis: 80%;
+  }
+  .right {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+    order: 1;
+    flex-direction: row-reverse;
+  }
+  nb-actions > nb-action {
+    padding: 0;
+  }
+  nb-action {
+    i {
+      color: nb-theme(color-fg);
+      font-size: 1.5rem;
+      margin-right: 1rem;
+    }
+    span {
+      font-family: nb-theme(font-secondary);
+      font-weight: nb-theme(font-weight-bold);
+      color: nb-theme(color-fg-heading);
+      text-transform: uppercase;
+    }
+    button {
+      margin: 0 auto;
+      padding: 0;
+      cursor: pointer;
+      border: none;
+      background: none;
+      display: flex;
+      align-items: center;
+      &:focus {
+        box-shadow: none;
+        outline: none;
+      }
+    }
+  }
+}
+
+td.ng2-smart-actions {
+  height: auto !important;
+}
+
+nav.ng2-smart-pagination-nav {
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.ng2-smart-pagination .page-link.page-link-prev,
+.page-link.page-link-next {
+  font-size: 1em !important;
+}
diff --git a/webapp/src/app/pages/sdks/sdk-management/sdk-management.component.ts b/webapp/src/app/pages/sdks/sdk-management/sdk-management.component.ts
new file mode 100644 (file)
index 0000000..c885238
--- /dev/null
@@ -0,0 +1,168 @@
+/**
+* @license
+* Copyright (C) 2017 "IoT.bzh"
+* Author Sebastien Douheret <sebastien@iot.bzh>
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*   http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+import { Component, ViewEncapsulation, OnInit, isDevMode } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { LocalDataSource } from 'ng2-smart-table';
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
+import { ConfirmModalComponent, EType } from '../../confirm/confirm-modal/confirm-modal.component';
+import { SdkInstallComponent } from './sdk-install.component';
+
+import { AlertService } from '../../../@core-xds/services/alert.service';
+import { SdkService, ISdk } from '../../../@core-xds/services/sdk.service';
+import { ISdkMessage } from '../../../@core-xds/services/xdsagent.service';
+
+interface ISdkMgt extends ISdk {
+  link: string;
+  selected: boolean;
+}
+
+/*
+ * FIXME / TODO:
+ *  - support install of multi SDKs  (see settings.selectMode: 'multi')
+ *  - add Uninstall button (use delete)
+ *  - add (mouseover) to display description, date, size, ...
+ */
+
+@Component({
+  selector: 'xds-sdk-management',
+  templateUrl: 'sdk-management.component.html',
+  styleUrls: ['sdk-management.component.scss'],
+  encapsulation: ViewEncapsulation.None,
+})
+
+export class SdkManagementComponent implements OnInit {
+
+  sdks$: Observable<ISdk[]>;
+  sdks: ISdkMgt[];
+  source: LocalDataSource = new LocalDataSource();
+
+  settings = {
+    mode: 'external',
+    actions: {
+      add: false,
+      edit: false,
+      delete: false,  // TODO, add delete == uninstall
+      custom: [
+        { name: 'install', title: '<i class="nb-plus"></i>' },
+      ],
+    },
+    delete: {
+      deleteButtonContent: '<i class="nb-trash"></i>',
+      confirmDelete: true,
+    },
+    columns: {
+      name: { title: 'Name', editable: false },
+      profile: { title: 'Profile', editable: false, filter: {} },
+      arch: { title: 'Architecture', editable: false, filter: {} },
+      version: { title: 'Version', editable: false },
+      // TODO: add status when delete supported:
+      // status: { title: 'Status', editable: false },
+      link: { title: 'Link', editable: false, type: 'html', filter: false, width: '2%' },
+    },
+  };
+
+  constructor(
+    private alert: AlertService,
+    private sdkSvr: SdkService,
+    private modalService: NgbModal,
+  ) { }
+
+  ngOnInit() {
+    this.sdkSvr.Sdks$.subscribe(sdks => {
+      const profMap = {};
+      const archMap = {};
+      this.sdks = [];
+      sdks.forEach(s => {
+        // only display not installed SDK
+        if (s.status !== 'Not Installed') {
+          return;
+        }
+        profMap[s.profile] = s.profile;
+        archMap[s.arch] = s.arch;
+
+        const sm = <ISdkMgt>s;
+        sm.selected = false;
+        if (s.url !== '') {
+          sm.link = '<a href="' + s.url.substr(0, s.url.lastIndexOf('/')) + '" target="_blank" class="fa fa-external-link"></a>';
+        }
+        this.sdks.push(sm);
+
+      });
+
+      // Add text box filter for Profile and Arch columns
+      const profList = []; Object.keys(profMap).forEach(a => profList.push({ value: a, title: a }));
+      this.settings.columns.profile.filter = {
+        type: 'list',
+        config: { selectText: 'Select...', list: profList },
+      };
+
+      const archList = []; Object.keys(archMap).forEach(a => archList.push({ value: a, title: a }));
+      this.settings.columns.arch.filter = {
+        type: 'list',
+        config: { selectText: 'Select...', list: archList },
+      };
+
+      // update sources
+      this.source.load(this.sdks);
+
+    });
+  }
+
+  onCustom(event): void {
+    if (event.action === 'install') {
+      const sdk = <ISdkMgt>event.data;
+      const modal = this.modalService.open(ConfirmModalComponent, {
+        size: 'lg',
+        backdrop: 'static',
+        container: 'nb-layout',
+      });
+      modal.componentInstance.title = 'Confirm SDK installation';
+      modal.componentInstance.type = EType.YesNo;
+      modal.componentInstance.question = `
+      Please confirm installation of <b>` + sdk.name + `'</b> SDK ?<br>
+      <br>
+      <i><small>(size: ` + sdk.size + `, date: ` + sdk.date + `)</small></i>`;
+
+      modal.result.then(res => {
+        if (res === 'yes') {
+          // Request installation
+          this.sdkSvr.install(sdk).subscribe(r => { }, err => this.alert.error(err));
+
+          const modalInstall = this.modalService.open(SdkInstallComponent, {
+            size: 'lg',
+            backdrop: 'static',
+            container: 'nb-layout',
+          });
+          modalInstall.componentInstance.sdk = sdk;
+        }
+      });
+
+
+    } else if (event.action === 'uninstall') {
+      // TODO
+
+    } else {
+      /* tslint:disable:no-console */
+      if (isDevMode) {
+        console.error('onCustom: unknown event action: ', event);
+      }
+    }
+  }
+
+}
index 94c2501..9f9204f 100644 (file)
@@ -4,10 +4,10 @@
       <div class="col-9">
         <nb-actions size="medium">
           <nb-action>
-            <button (click)="add()">
-                <i class="nb-plus"></i>
-                <span>Add new SDK</span>
-            </button>
+            <a href="#/pages/sdks/management"  class="btn" role="Button">
+                <i class="fa fa-wrench"></i>
+                <span>SDKs Management</span>
+            </a>
           </nb-action>
         </nb-actions>
       </div>
       </div>
     </nb-card-body>
   </div>
-  <div class="col-md-6 col-lg-6" *ngFor="let sdk of (sdks$ | async)">
-    <xds-sdk-card [sdk]="sdk"></xds-sdk-card>
-  </div>
+
+  <ng-container *ngFor="let sdk of (sdks$ | async)">
+    <div class="col-md-6 col-lg-6" *ngIf="isVisible(sdk)">
+      <xds-sdk-card [sdk]="sdk"></xds-sdk-card>
+    </div>
+  </ng-container>
 </div>
index 92a8807..a125e8d 100644 (file)
@@ -31,7 +31,7 @@
   nb-action {
     i {
       color: nb-theme(color-fg);
-      font-size: 2.5rem;
+      font-size: 1.5rem;
       margin-right: 1rem;
     }
     span {
index f9e31a4..12d2f71 100644 (file)
@@ -20,7 +20,6 @@ import { Component, OnInit } from '@angular/core';
 import { Observable } from 'rxjs/Observable';
 
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
-// import { SdkAddModalComponent } from './sdk-add-modal/sdk-add-modal.component';
 
 import { SdkService, ISdk } from '../../@core-xds/services/sdk.service';
 
@@ -44,10 +43,7 @@ export class SdksComponent implements OnInit {
     this.sdks$ = this.sdkSvr.Sdks$;
   }
 
-  add() {
-    /* SEB TODO
-    const activeModal = this.modalService.open(SdkAddModalComponent, { size: 'lg', container: 'nb-layout' });
-    activeModal.componentInstance.modalHeader = 'Large Modal';
-    */
+  isVisible(sdk: ISdk) {
+    return sdk.status === 'Installed' || sdk.status === 'Installing';
   }
 }
index d717d86..17ede0e 100644 (file)
@@ -21,20 +21,23 @@ import { ThemeModule } from '../../@theme/theme.module';
 
 import { SdksComponent } from './sdks.component';
 import { SdkCardComponent } from './sdk-card/sdk-card.component';
-// import { SdkAddModalComponent } from './sdk-add-modal/sdk-add-modal.component';
-
+import { SdkManagementComponent } from './sdk-management/sdk-management.component';
+import { SdkInstallComponent } from './sdk-management/sdk-install.component';
+import { Ng2SmartTableModule } from 'ng2-smart-table';
 
 @NgModule({
   imports: [
     ThemeModule,
+    Ng2SmartTableModule,
   ],
   declarations: [
     SdksComponent,
     SdkCardComponent,
-    // SdkAddModalComponent,
+    SdkManagementComponent,
+    SdkInstallComponent,
   ],
   entryComponents: [
-    // SdkAddModalComponent,
+    SdkInstallComponent,
   ],
 })
 export class SdksModule { }