New dashboard improvements.
authorSebastien Douheret <sebastien.douheret@iot.bzh>
Fri, 24 Nov 2017 00:14:30 +0000 (01:14 +0100)
committerSebastien Douheret <sebastien.douheret@iot.bzh>
Fri, 24 Nov 2017 00:37:24 +0000 (01:37 +0100)
- add build buttons
- add build settings support and backup into project clientData
- improved async alert
- fixed project dropdown

Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
49 files changed:
.vscode/settings.json
glide.yaml
lib/agent/apiv1-exec.go
lib/agent/apiv1-projects.go
lib/agent/apiv1.go
lib/agent/events.go
lib/agent/project-interface.go
lib/agent/project-pathmap.go
lib/agent/project-st.go
lib/agent/projects.go
lib/agent/sessions.go
lib/agent/xdsserver.go
lib/apiv1/events.go
lib/apiv1/projects.go
webapp/.stylelintrc.json
webapp/src/app/@core-xds/services/alert.service.spec.ts
webapp/src/app/@core-xds/services/alert.service.ts
webapp/src/app/@core-xds/services/build-settings.service.ts [new file with mode: 0644]
webapp/src/app/@core-xds/services/config.service.spec.ts
webapp/src/app/@core-xds/services/project.service.spec.ts
webapp/src/app/@core-xds/services/project.service.ts
webapp/src/app/@core-xds/services/xds-config.service.ts
webapp/src/app/@core-xds/services/xdsagent.service.ts
webapp/src/app/@theme/components/header/header.component.html
webapp/src/app/pages/build/build-settings-modal/build-settings-modal.component.html [new file with mode: 0644]
webapp/src/app/pages/build/build-settings-modal/build-settings-modal.component.ts [new file with mode: 0644]
webapp/src/app/pages/build/build.component.html
webapp/src/app/pages/build/build.component.scss
webapp/src/app/pages/build/build.component.spec.ts
webapp/src/app/pages/build/build.component.ts
webapp/src/app/pages/build/build.module.ts
webapp/src/app/pages/build/settings-modal/build-settings-modal.component.ts [new file with mode: 0644]
webapp/src/app/pages/build/settings/project-select-dropdown.component.ts
webapp/src/app/pages/config/config-xds/downloadXdsAgent.component.ts
webapp/src/app/pages/config/config.module.ts
webapp/src/app/pages/projects/project-add-modal/project-add-modal.component.html
webapp/src/app/pages/projects/project-add-modal/project-add-modal.component.ts
webapp/src/app/pages/projects/project-card/project-card.component.scss
webapp/src/app/pages/projects/project-card/project-card.component.ts
webapp/src/app/pages/projects/projects.component.html
webapp/src/app/pages/projects/projects.component.scss
webapp/src/app/pages/projects/projects.component.ts
webapp/src/app/pages/projects/projects.module.ts
webapp/src/app/pages/sdks/sdk-card/sdk-card.component.scss
webapp/src/app/pages/sdks/sdk-card/sdk-card.component.ts
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 4c9ba28..5d43dd0 100644 (file)
   ],
   // 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"
+    "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"
   ],
   // 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 3370e2d..1630276 100644 (file)
@@ -25,3 +25,5 @@ import:
   version: ^0.1.0
   subpackages:
   - golib/common
+- package: github.com/franciscocpg/reflectme
+  version: ^0.1.9
index c199267..3cb4d23 100644 (file)
@@ -5,15 +5,13 @@ import (
        "io/ioutil"
        "net/http"
 
+       "github.com/franciscocpg/reflectme"
        "github.com/gin-gonic/gin"
        "github.com/iotbzh/xds-agent/lib/apiv1"
        common "github.com/iotbzh/xds-common/golib"
        uuid "github.com/satori/go.uuid"
 )
 
-var execCmdID = 1
-var fwdFuncID []uuid.UUID
-
 // ExecCmd executes remotely a command
 func (s *APIService) execCmd(c *gin.Context) {
        s._execRequest("/exec", c)
@@ -81,6 +79,7 @@ func (s *APIService) _execRequest(cmd string, c *gin.Context) {
                apiv1.ExecInferiorOutEvent,
        }
 
+       var fwdFuncID []uuid.UUID
        for _, evName := range evtList {
                evN := evName
                fwdFunc := func(pData interface{}, evData interface{}) error {
@@ -92,6 +91,9 @@ func (s *APIService) _execRequest(cmd string, c *gin.Context) {
                                return nil
                        }
 
+                       // Add sessionID to event Data
+                       reflectme.SetField(evData, "sessionID", sid)
+
                        // Forward event to Client/Dashboard
                        (*so).Emit(evN, evData)
                        return nil
@@ -110,15 +112,17 @@ func (s *APIService) _execRequest(cmd string, c *gin.Context) {
                evN := apiv1.ExecExitEvent
                sid := pData.(string)
 
+               // Add sessionID to event Data
+               reflectme.SetField(evData, "sessionID", sid)
+
                // IO socket can be nil when disconnected
                so := s.sessions.IOSocketGet(sid)
-               if so == nil {
+               if so != nil {
+                       (*so).Emit(evN, evData)
+               } else {
                        s.Log.Infof("%s not emitted: WS closed (sid:%s)", evN, sid)
-                       return nil
                }
 
-               (*so).Emit(evN, evData)
-
                // cleanup listener
                for i, evName := range evtList {
                        svr.EventOff(evName, fwdFuncID[i])
index c835967..5784896 100644 (file)
@@ -39,7 +39,7 @@ func (s *APIService) addProject(c *gin.Context) {
 
        s.Log.Debugln("Add project config: ", cfgArg)
 
-       newFld, err := s.projects.Add(cfgArg)
+       newFld, err := s.projects.Add(cfgArg, s.sessions.GetID(c))
        if err != nil {
                common.APIError(c, err.Error())
                return
@@ -77,10 +77,34 @@ func (s *APIService) delProject(c *gin.Context) {
 
        s.Log.Debugln("Delete project id ", id)
 
-       delEntry, err := s.projects.Delete(id)
+       delEntry, err := s.projects.Delete(id, s.sessions.GetID(c))
        if err != nil {
                common.APIError(c, err.Error())
                return
        }
        c.JSON(http.StatusOK, delEntry)
 }
+
+// updateProject Update some field of a specific project
+func (s *APIService) updateProject(c *gin.Context) {
+       id, err := s.projects.ResolveID(c.Param("id"))
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       var cfgArg apiv1.ProjectConfig
+       if c.BindJSON(&cfgArg) != nil {
+               common.APIError(c, "Invalid arguments")
+               return
+       }
+
+       s.Log.Debugln("Update project id ", id)
+
+       upPrj, err := s.projects.Update(id, cfgArg, s.sessions.GetID(c))
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+       c.JSON(http.StatusOK, upPrj)
+}
index 3e742f5..36e5a54 100644 (file)
@@ -8,7 +8,7 @@ import (
        "github.com/iotbzh/xds-agent/lib/xdsconfig"
 )
 
-const apiBaseUrl = "/api/v1"
+const apiBaseURL = "/api/v1"
 
 // APIService .
 type APIService struct {
@@ -21,7 +21,7 @@ type APIService struct {
 func NewAPIV1(ctx *Context) *APIService {
        s := &APIService{
                Context:     ctx,
-               apiRouter:   ctx.webServer.router.Group(apiBaseUrl),
+               apiRouter:   ctx.webServer.router.Group(apiBaseURL),
                serverIndex: 0,
        }
 
@@ -34,6 +34,7 @@ func NewAPIV1(ctx *Context) *APIService {
 
        s.apiRouter.GET("/projects", s.getProjects)
        s.apiRouter.GET("/projects/:id", s.getProject)
+       s.apiRouter.PUT("/projects/:id", s.updateProject)
        s.apiRouter.POST("/projects", s.addProject)
        s.apiRouter.POST("/projects/sync/:id", s.syncProject)
        s.apiRouter.DELETE("/projects/:id", s.delProject)
@@ -80,7 +81,7 @@ func (s *APIService) AddXdsServer(cfg xdsconfig.XDSServerConf) (*XdsServer, erro
 
                // Create a new server object
                if cfg.APIBaseURL == "" {
-                       cfg.APIBaseURL = apiBaseUrl
+                       cfg.APIBaseURL = apiBaseURL
                }
                if cfg.APIPartialURL == "" {
                        cfg.APIPartialURL = "/server/" + strconv.Itoa(s.serverIndex)
index 9ff72ac..ccf8ddc 100644 (file)
@@ -71,7 +71,7 @@ func (e *Events) UnRegister(evName, sessionID string) error {
 }
 
 // Emit Used to manually emit an event
-func (e *Events) Emit(evName string, data interface{}) error {
+func (e *Events) Emit(evName string, data interface{},fromSid string) error {
        var firstErr error
 
        if _, ok := e.eventsMap[evName]; !ok {
@@ -93,9 +93,10 @@ func (e *Events) Emit(evName string, data interface{}) error {
                        continue
                }
                msg := apiv1.EventMsg{
-                       Time: time.Now().String(),
-                       Type: evName,
-                       Data: data,
+                       Time:          time.Now().String(),
+                       FromSessionID: fromSid,
+                       Type:          evName,
+                       Data:          data,
                }
                e.Log.Debugf("Emit Event %s: %v", evName, sid)
                if err := (*so).Emit(evName, msg); err != nil {
index c9e9ec5..0d6bb1a 100644 (file)
@@ -4,11 +4,12 @@ import "github.com/iotbzh/xds-agent/lib/apiv1"
 
 // IPROJECT Project interface
 type IPROJECT interface {
-       Add(cfg apiv1.ProjectConfig) (*apiv1.ProjectConfig, error)           // Add a new project
-       Delete() error                                                             // Delete a project
-       GetProject() *apiv1.ProjectConfig                                       // Get project public configuration
-       UpdateProject(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) // Update project configuration
-       GetServer() *XdsServer                                                     // Get XdsServer that holds this project
-       Sync() error                                                               // Force project files synchronization
-       IsInSync() (bool, error)                                                   // Check if project files are in-sync
+       Add(cfg apiv1.ProjectConfig) (*apiv1.ProjectConfig, error)          // Add a new project
+       Setup(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) // Local setup of the project
+       Delete() error                                                      // Delete a project
+       GetProject() *apiv1.ProjectConfig                                   // Get project public configuration
+       Update(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error)       // Update project configuration
+       GetServer() *XdsServer                                              // Get XdsServer that holds this project
+       Sync() error                                                        // Force project files synchronization
+       IsInSync() (bool, error)                                            // Check if project files are in-sync
 }
index 7a96e6e..3c87770 100644 (file)
@@ -69,7 +69,7 @@ func (p *PathMap) Add(cfg apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) {
        // Send request to create folder on XDS server side
        err = p.server.FolderAdd(fld, p.folder)
        if err != nil {
-               return nil, fmt.Errorf("Folders mapping verification failure.\n%v", err)
+               return nil, err
        }
 
        // 2nd part of sanity checker
@@ -98,16 +98,30 @@ func (p *PathMap) GetProject() *apiv1.ProjectConfig {
        return &prj
 }
 
-// UpdateProject Set project config
-func (p *PathMap) UpdateProject(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) {
+// Setup Setup local project config
+func (p *PathMap) Setup(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) {
        p.folder = p.server.ProjectToFolder(prj)
        np := p.GetProject()
-       if err := p.events.Emit(apiv1.EVTProjectChange, np); err != nil {
+       if err := p.events.Emit(apiv1.EVTProjectChange, np, ""); err != nil {
                return np, err
        }
        return np, nil
 }
 
+// Update Update some field of a project
+func (p *PathMap) Update(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) {
+       if p.folder.ID != prj.ID {
+               return nil, fmt.Errorf("Invalid id")
+       }
+
+       err := p.server.FolderUpdate(p.server.ProjectToFolder(prj), p.folder)
+       if err != nil {
+               return nil, err
+       }
+
+       return p.GetProject(), nil
+}
+
 // GetServer Get the XdsServer that holds this project
 func (p *PathMap) GetServer() *XdsServer {
        return p.server
index e2cd3cb..c4e8fce 100644 (file)
@@ -1,6 +1,8 @@
 package agent
 
 import (
+       "fmt"
+
        "github.com/iotbzh/xds-agent/lib/apiv1"
        st "github.com/iotbzh/xds-agent/lib/syncthing"
 )
@@ -56,8 +58,8 @@ func (p *STProject) Add(cfg apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) {
                p.Log.Errorf("Project ID in XDSServer and local ST differ: %s != %s", svrPrj.ID, locPrj.ID)
        }
 
-       // Use Update function to setup remains fields
-       return p.UpdateProject(*svrPrj)
+       // Use Setup function to setup remains fields
+       return p.Setup(*svrPrj)
 }
 
 // Delete a project
@@ -77,16 +79,16 @@ func (p *STProject) GetProject() *apiv1.ProjectConfig {
        return &prj
 }
 
-// UpdateProject Update project config
-func (p *STProject) UpdateProject(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) {
+// Setup Setup local project config
+func (p *STProject) Setup(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) {
        // Update folder
        p.folder = p.server.ProjectToFolder(prj)
        svrPrj := p.GetProject()
 
        // Register events to update folder status
        // Register to XDS Server events
-       p.server.EventOn("event:FolderStateChanged", "", p._cbServerFolderChanged)
-       if err := p.server.EventRegister("FolderStateChanged", svrPrj.ID); err != nil {
+       p.server.EventOn("event:folder-state-change", "", p._cbServerFolderChanged)
+       if err := p.server.EventRegister("folder-state-change", svrPrj.ID); err != nil {
                p.Log.Warningf("XDS Server EventRegister failed: %v", err)
                return svrPrj, err
        }
@@ -103,6 +105,21 @@ func (p *STProject) UpdateProject(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig
        return svrPrj, nil
 }
 
+// Update Update some field of a project
+func (p *STProject) Update(prj apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) {
+
+       if p.folder.ID != prj.ID {
+               return nil, fmt.Errorf("Invalid id")
+       }
+
+       err := p.server.FolderUpdate(p.server.ProjectToFolder(prj), p.folder)
+       if err != nil {
+               return nil, err
+       }
+
+       return p.GetProject(), nil
+}
+
 // GetServer Get the XdsServer that holds this project
 func (p *STProject) GetServer() *XdsServer {
        return p.server
@@ -142,7 +159,7 @@ func (p *STProject) _cbServerFolderChanged(pData interface{}, data interface{})
                p.folder.DataCloudSync.STSvrIsInSync = evt.Folder.IsInSync
                p.folder.DataCloudSync.STSvrStatus = evt.Folder.Status
 
-               if err := p.events.Emit(apiv1.EVTProjectChange, p.server.FolderToProject(*p.folder)); err != nil {
+               if err := p.events.Emit(apiv1.EVTProjectChange, p.server.FolderToProject(*p.folder), ""); err != nil {
                        p.Log.Warningf("Cannot notify project change (from server): %v", err)
                }
        }
@@ -181,7 +198,7 @@ func (p *STProject) _cbLocalSTEvents(ev st.Event, data *st.EventsCBData) {
                p.folder.DataCloudSync.STLocIsInSync = inSync
                p.folder.DataCloudSync.STLocStatus = sts
 
-               if err := p.events.Emit(apiv1.EVTProjectChange, p.server.FolderToProject(*p.folder)); err != nil {
+               if err := p.events.Emit(apiv1.EVTProjectChange, p.server.FolderToProject(*p.folder), ""); err != nil {
                        p.Log.Warningf("Cannot notify project change (local): %v", err)
                }
        }
index f089882..966c231 100644 (file)
@@ -6,6 +6,7 @@ import (
        "strings"
        "time"
 
+       "github.com/franciscocpg/reflectme"
        "github.com/iotbzh/xds-agent/lib/apiv1"
        "github.com/iotbzh/xds-agent/lib/syncthing"
        "github.com/syncthing/syncthing/lib/sync"
@@ -119,14 +120,14 @@ func (p *Projects) GetProjectArrUnsafe() []apiv1.ProjectConfig {
 }
 
 // Add adds a new folder
-func (p *Projects) Add(newF apiv1.ProjectConfig) (*apiv1.ProjectConfig, error) {
+func (p *Projects) Add(newF apiv1.ProjectConfig, fromSid string) (*apiv1.ProjectConfig, error) {
        prj, err := p.createUpdate(newF, true, false)
        if err != nil {
                return prj, err
        }
 
        // Notify client with event
-       if err := p.events.Emit(apiv1.EVTProjectAdd, *prj); err != nil {
+       if err := p.events.Emit(apiv1.EVTProjectAdd, *prj, fromSid); err != nil {
                p.Log.Warningf("Cannot notify project deletion: %v", err)
        }
 
@@ -190,7 +191,7 @@ func (p *Projects) createUpdate(newF apiv1.ProjectConfig, create bool, initial b
                }
        } else {
                // Just update project config
-               if newPrj, err = fld.UpdateProject(newF); err != nil {
+               if newPrj, err = fld.Setup(newF); err != nil {
                        newF.Status = apiv1.StatusErrorConfig
                        log.Printf("ERROR Updating project: %v\n", err)
                        return newPrj, err
@@ -217,7 +218,7 @@ func (p *Projects) createUpdate(newF apiv1.ProjectConfig, create bool, initial b
 }
 
 // Delete deletes a specific folder
-func (p *Projects) Delete(id string) (apiv1.ProjectConfig, error) {
+func (p *Projects) Delete(id, fromSid string) (apiv1.ProjectConfig, error) {
        var err error
 
        pjMutex.Lock()
@@ -238,7 +239,7 @@ func (p *Projects) Delete(id string) (apiv1.ProjectConfig, error) {
        delete(p.projects, id)
 
        // Notify client with event
-       if err := p.events.Emit(apiv1.EVTProjectDelete, *prj); err != nil {
+       if err := p.events.Emit(apiv1.EVTProjectDelete, *prj, fromSid); err != nil {
                p.Log.Warningf("Cannot notify project deletion: %v", err)
        }
 
@@ -262,3 +263,50 @@ func (p *Projects) IsProjectInSync(id string) (bool, error) {
        }
        return (*fc).IsInSync()
 }
+
+// Update Update some field of a project
+func (p *Projects) Update(id string, prj apiv1.ProjectConfig, fromSid string) (*apiv1.ProjectConfig, error) {
+
+       pjMutex.Lock()
+       defer pjMutex.Unlock()
+
+       fc, exist := p.projects[id]
+       if !exist {
+               return nil, fmt.Errorf("Unknown id")
+       }
+
+       // Copy current in a new object to change nothing in case of an error rises
+       newFld := apiv1.ProjectConfig{}
+       reflectme.Copy((*fc).GetProject(), &newFld)
+
+       // Only update some fields
+       dirty := false
+       for _, fieldName := range apiv1.ProjectConfigUpdatableFields {
+               valNew, err := reflectme.GetField(prj, fieldName)
+               if err == nil {
+                       valCur, err := reflectme.GetField(newFld, fieldName)
+                       if err == nil && valNew != valCur {
+                               err = reflectme.SetField(&newFld, fieldName, valNew)
+                               if err != nil {
+                                       return nil, err
+                               }
+                               dirty = true
+                       }
+               }
+       }
+
+       if !dirty {
+               return &newFld, nil
+       }
+
+       upPrj, err := (*fc).Update(newFld)
+       if err != nil {
+               return nil, err
+       }
+
+       // Notify client with event
+       if err := p.events.Emit(apiv1.EVTProjectChange, *upPrj, fromSid); err != nil {
+               p.Log.Warningf("Cannot notify project change: %v", err)
+       }
+       return upPrj, err
+}
index 7347480..3d8b0f4 100644 (file)
@@ -125,6 +125,14 @@ func (s *Sessions) Get(c *gin.Context) *ClientSession {
        return nil
 }
 
+// GetID returns the session or an empty string
+func (s *Sessions) GetID(c *gin.Context) string {
+       if sess := s.Get(c); sess != nil {
+               return sess.ID
+       }
+       return ""
+}
+
 // IOSocketGet Get socketio definition from sid
 func (s *Sessions) IOSocketGet(sid string) *socketio.Socket {
        s.mutex.Lock()
index 73a5bd9..7b03579 100644 (file)
@@ -64,9 +64,12 @@ type XdsBuilderConfig struct {
 type XdsFolderType string
 
 const (
-       XdsTypePathMap   = "PathMap"
+       // XdsTypePathMap Path Mapping folder type
+       XdsTypePathMap = "PathMap"
+       // XdsTypeCloudSync Cloud synchronization (AKA syncthing) folder type
        XdsTypeCloudSync = "CloudSync"
-       XdsTypeCifsSmb   = "CIFS"
+       // XdsTypeCifsSmb CIFS (AKA samba) folder type
+       XdsTypeCifsSmb = "CIFS"
 )
 
 // XdsFolderConfig XdsServer folder config
@@ -78,6 +81,8 @@ type XdsFolderConfig struct {
        Status     string        `json:"status"`
        IsInSync   bool          `json:"isInSync"`
        DefaultSdk string        `json:"defaultSdk"`
+       ClientData string        `json:"clientData"` // free form field that can used by client
+
        // Specific data depending on which Type is used
        DataPathMap   XdsPathMapConfig   `json:"dataPathMap,omitempty"`
        DataCloudSync XdsCloudSyncConfig `json:"dataCloudSync,omitempty"`
@@ -112,7 +117,7 @@ type XdsEventFolderChange struct {
        Folder XdsFolderConfig `json:"folder"`
 }
 
-// Event emitter callback
+// EventCB Event emitter callback
 type EventCB func(privData interface{}, evtData interface{}) error
 
 // caller Used to chain event listeners
@@ -241,6 +246,11 @@ func (xs *XdsServer) FolderSync(id string) error {
        return xs.client.HTTPPost("/folders/sync/"+id, "")
 }
 
+// FolderUpdate Send PUT request to update a folder
+func (xs *XdsServer) FolderUpdate(fld *XdsFolderConfig, resFld *XdsFolderConfig) error {
+       return xs.client.Put("/folders/"+fld.ID, fld, resFld)
+}
+
 // SetAPIRouterGroup .
 func (xs *XdsServer) SetAPIRouterGroup(r *gin.RouterGroup) {
        xs.apiRouter = r
@@ -334,7 +344,7 @@ func (xs *XdsServer) EventOn(evName string, privData interface{}, f EventCB) (uu
 
                // FIXME: use generic type: data interface{} instead of data XdsEventFolderChange
                var err error
-               if evName == "event:FolderStateChanged" {
+               if evName == "event:folder-state-change" {
                        err = xs.ioSock.On(evn, func(data XdsEventFolderChange) error {
                                xs.sockEventsLock.Lock()
                                sEvts := make([]*caller, len(xs.sockEvents[evn]))
@@ -400,6 +410,7 @@ func (xs *XdsServer) ProjectToFolder(pPrj apiv1.ProjectConfig) *XdsFolderConfig
        if pPrj.Type == XdsTypeCloudSync {
                stID, _ = xs.SThg.IDGet()
        }
+       // TODO: limit ClientData size and gzip it (see https://golang.org/pkg/compress/gzip/)
        fPrj := XdsFolderConfig{
                ID:         pPrj.ID,
                Label:      pPrj.Label,
@@ -408,6 +419,7 @@ func (xs *XdsServer) ProjectToFolder(pPrj apiv1.ProjectConfig) *XdsFolderConfig
                Status:     pPrj.Status,
                IsInSync:   pPrj.IsInSync,
                DefaultSdk: pPrj.DefaultSdk,
+               ClientData: pPrj.ClientData,
                DataPathMap: XdsPathMapConfig{
                        ServerPath: pPrj.ServerPath,
                },
@@ -457,6 +469,7 @@ func (xs *XdsServer) FolderToProject(fPrj XdsFolderConfig) apiv1.ProjectConfig {
                Status:     sts,
                IsInSync:   inSync,
                DefaultSdk: fPrj.DefaultSdk,
+               ClientData: fPrj.ClientData,
        }
        return pPrj
 }
@@ -628,7 +641,7 @@ func (xs *XdsServer) _NotifyState() {
                ConnRetry:  xs.ConnRetry,
                Connected:  xs.Connected,
        }
-       if err := xs.events.Emit(apiv1.EVTServerConfig, evSts); err != nil {
+       if err := xs.events.Emit(apiv1.EVTServerConfig, evSts, ""); err != nil {
                xs.Log.Warningf("Cannot notify XdsServer state change: %v", err)
        }
 }
index cdd0889..b2fda62 100644 (file)
@@ -40,9 +40,10 @@ var EVTAllList = []string{
 
 // EventMsg Event message send over Websocket, data format depend to Type (see DecodeXXX function)
 type EventMsg struct {
-       Time string      `json:"time"`
-       Type string      `json:"type"`
-       Data interface{} `json:"data"`
+       Time          string      `json:"time"`      // Timestamp
+       FromSessionID string      `json:"sessionID"` // Session ID of client that emits this event
+       Type          string      `json:"type"`      // Data type
+       Data          interface{} `json:"data"`      // Data
 }
 
 // DecodeServerCfg Helper to decode Data field type ServerCfg
index d76fa09..b1e64c8 100644 (file)
@@ -29,4 +29,10 @@ type ProjectConfig struct {
        Status     string      `json:"status"`
        IsInSync   bool        `json:"isInSync"`
        DefaultSdk string      `json:"defaultSdk"`
+       ClientData string      `json:"clientData"` // free form field that can used by client
+}
+
+// ProjectConfigUpdatableFields List fields that can be updated using Update function
+var ProjectConfigUpdatableFields = []string{
+       "Label", "DefaultSdk", "ClientData",
 }
index 9732970..8132883 100644 (file)
@@ -63,7 +63,7 @@
     "selector-pseudo-element-colon-notation": "double",
     "selector-pseudo-element-no-unknown": true,
     "selector-type-case": "lower",
-    "selector-max-id": 0,
+    "selector-max-id": 1,
 
     "no-missing-end-of-source-newline": true,
 
index b3d364c..2de2ac3 100644 (file)
@@ -3,13 +3,13 @@ import { TestBed, inject } from '@angular/core/testing';
 import { AlertService } from './alert.service';
 
 describe('AlertService', () => {
-    beforeEach(() => {
-        TestBed.configureTestingModule({
-            providers: [AlertService]
-        });
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [AlertService],
     });
+  });
 
-    it('should be created', inject([AlertService], (service: AlertService) => {
-        expect(service).toBeTruthy();
-    }));
+  it('should be created', inject([AlertService], (service: AlertService) => {
+    expect(service).toBeTruthy();
+  }));
 });
index c15e176..23a5e5d 100644 (file)
@@ -31,7 +31,7 @@ export class AlertService {
 
   public error(msg: string, dismissTime?: number) {
     this.add({
-      type: 'error', msg: msg, dismissible: true, dismissTimeout: dismissTime
+      type: 'error', msg: msg, dismissible: true, dismissTimeout: dismissTime,
     });
   }
 
diff --git a/webapp/src/app/@core-xds/services/build-settings.service.ts b/webapp/src/app/@core-xds/services/build-settings.service.ts
new file mode 100644 (file)
index 0000000..cb52ce3
--- /dev/null
@@ -0,0 +1,78 @@
+import { Injectable } from '@angular/core';
+import { CookieService } from 'ngx-cookie';
+import { Observable } from 'rxjs/Observable';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+
+export interface IBuildSettings {
+  subpath: string;
+  cmdClean: string;
+  cmdPrebuild: string;
+  cmdBuild: string;
+  cmdPopulate: string;
+  cmdArgs: string[];
+  envVars: string[];
+}
+
+@Injectable()
+export class BuildSettingsService {
+  public settings$: Observable<IBuildSettings>;
+
+  private settingsSubject: BehaviorSubject<IBuildSettings>;
+  private settingsStore: IBuildSettings;
+
+  constructor(
+    private cookie: CookieService,
+  ) {
+    this._load();
+  }
+
+  // Load build settings from cookie
+  private _load() {
+    // Try to retrieve previous config from cookie
+    const cookConf = this.cookie.getObject('xds-build-settings');
+    if (cookConf != null) {
+      this.settingsStore = <IBuildSettings>cookConf;
+    } else {
+      // Set default config
+      this.settingsStore = {
+        subpath: '',
+        cmdClean: 'rm -rf build && echo Done',
+        cmdPrebuild: 'mkdir -p build && cd build && cmake ..',
+        cmdBuild: 'cd build && make',
+        cmdPopulate: 'cd build && make remote-target-populate',
+        cmdArgs: [],
+        envVars: [],
+      };
+    }
+  }
+
+  // Save config into cookie
+  private _save() {
+    // Notify subscribers
+    this.settingsSubject.next(Object.assign({}, this.settingsStore));
+
+    const cfg = Object.assign({}, this.settingsStore);
+    this.cookie.putObject('xds-build-settings', cfg);
+  }
+
+  // Get whole config values
+  get(): IBuildSettings {
+    return this.settingsStore;
+  }
+
+  // Get whole config values
+  set(bs: IBuildSettings) {
+    this.settingsStore = bs;
+    this._save();
+  }
+
+  get subpath(): string {
+    return this.settingsStore.subpath;
+  }
+
+  set subpath(p: string) {
+    this.settingsStore.subpath = p;
+    this._save();
+  }
+
+}
index a20d4ba..f39b9d9 100644 (file)
@@ -5,7 +5,7 @@ import { ConfigService } from './config.service';
 describe('ConfigService', () => {
   beforeEach(() => {
     TestBed.configureTestingModule({
-      providers: [ConfigService]
+      providers: [ConfigService],
     });
   });
 
index b8edfc7..0924a73 100644 (file)
@@ -5,7 +5,7 @@ import { ProjectService } from './project.service';
 describe('ProjectService', () => {
   beforeEach(() => {
     TestBed.configureTestingModule({
-      providers: [ProjectService]
+      providers: [ProjectService],
     });
   });
 
index 8aeed80..94469fe 100644 (file)
@@ -1,4 +1,4 @@
-import { Injectable, SecurityContext } from '@angular/core';
+import { Injectable, SecurityContext, isDevMode } from '@angular/core';
 import { Observable } from 'rxjs/Observable';
 import { BehaviorSubject } from 'rxjs/BehaviorSubject';
 
@@ -15,12 +15,12 @@ export type ProjectTypeEnum = '' | 'PathMap' | 'CloudSync';
 export const ProjectType = {
   UNSET: '',
   NATIVE_PATHMAP: 'PathMap',
-  SYNCTHING: 'CloudSync'
+  SYNCTHING: 'CloudSync',
 };
 
 export const ProjectTypes = [
   { value: ProjectType.NATIVE_PATHMAP, display: 'Path mapping' },
-  { value: ProjectType.SYNCTHING, display: 'Cloud Sync' }
+  { value: ProjectType.SYNCTHING, display: 'Cloud Sync' },
 ];
 
 export const ProjectStatus = {
@@ -28,9 +28,18 @@ export const ProjectStatus = {
   Disable: 'Disable',
   Enable: 'Enable',
   Pause: 'Pause',
-  Syncing: 'Syncing'
+  Syncing: 'Syncing',
 };
 
+export interface IUISettings {
+  subpath: string;
+  cmdClean: string;
+  cmdPrebuild: string;
+  cmdBuild: string;
+  cmdPopulate: string;
+  cmdArgs: string[];
+  envVars: string[];
+}
 export interface IProject {
   id?: string;
   serverId: string;
@@ -45,95 +54,84 @@ export interface IProject {
   isExpanded?: boolean;
   visible?: boolean;
   defaultSdkID?: string;
+  uiSettings?: IUISettings;
 }
 
+const defaultUISettings: IUISettings = {
+  subpath: '',
+  cmdClean: 'rm -rf build && echo Done',
+  cmdPrebuild: 'mkdir -p build && cd build && cmake ..',
+  cmdBuild: 'cd build && make',
+  cmdPopulate: 'cd build && make remote-target-populate',
+  cmdArgs: [],
+  envVars: [],
+};
+
 @Injectable()
 export class ProjectService {
-  public Projects$: Observable<IProject[]>;
+  projects$: Observable<IProject[]>;
+  curProject$: Observable<IProject>;
 
   private _prjsList: IProject[] = [];
-  private current: IProject;
   private prjsSubject = <BehaviorSubject<IProject[]>>new BehaviorSubject(this._prjsList);
+  private _current: IProject;
+  private curPrjSubject = <BehaviorSubject<IProject>>new BehaviorSubject(this._current);
 
   constructor(private xdsSvr: XDSAgentService) {
-    this.current = null;
-    this.Projects$ = this.prjsSubject.asObservable();
+    this._current = null;
+    this.projects$ = this.prjsSubject.asObservable();
+    this.curProject$ = this.curPrjSubject.asObservable();
 
+    // Load initial projects list
     this.xdsSvr.getProjects().subscribe((projects) => {
       this._prjsList = [];
       projects.forEach(p => {
         this._addProject(p, true);
       });
-      this.prjsSubject.next(Object.assign([], this._prjsList));
-    });
 
-    // Update Project data
-    this.xdsSvr.ProjectState$.subscribe(prj => {
-      const i = this._getProjectIdx(prj.id);
-      if (i >= 0) {
-        // XXX for now, only isInSync and status may change
-        this._prjsList[i].isInSync = prj.isInSync;
-        this._prjsList[i].status = prj.status;
-        this._prjsList[i].isUsable = this._isUsableProject(prj);
-        this.prjsSubject.next(Object.assign([], this._prjsList));
+      // TODO: get previous val from xds-config service / cookie
+      if (this._prjsList.length > 0) {
+        this._current = this._prjsList[0];
+        this.curPrjSubject.next(this._current);
       }
-    });
 
-    // Add listener on create and delete project events
-    this.xdsSvr.addEventListener('event:project-add', (ev) => {
-      if (ev && ev.data && ev.data.id) {
-        this._addProject(ev.data);
-      } else {
-        console.log('Warning: received events with unknown data: ev=', ev);
-      }
-    });
-    this.xdsSvr.addEventListener('event:project-delete', (ev) => {
-      if (ev && ev.data && ev.data.id) {
-        const idx = this._prjsList.findIndex(item => item.id === ev.data.id);
-        if (idx === -1) {
-          console.log('Warning: received events on unknown project id: ev=', ev);
-          return;
-        }
-        this._prjsList.splice(idx, 1);
-        this.prjsSubject.next(Object.assign([], this._prjsList));
-      } else {
-        console.log('Warning: received events with unknown data: ev=', ev);
-      }
+      this.prjsSubject.next(this._prjsList);
     });
 
+    // Add listener on projects creation, deletion and change events
+    this.xdsSvr.onProjectAdd().subscribe(prj => this._addProject(prj));
+    this.xdsSvr.onProjectDelete().subscribe(prj => this._delProject(prj));
+    this.xdsSvr.onProjectChange().subscribe(prj => this._updateProject(prj));
   }
 
-  public setCurrent(s: IProject) {
-    this.current = s;
+  setCurrent(p: IProject): IProject | undefined {
+    if (!p) {
+      this._current = null;
+      return undefined;
+    }
+    return this.setCurrentById(p.id);
   }
 
-  public getCurrent(): IProject {
-    return this.current;
+  setCurrentById(id: string): IProject | undefined {
+    const p = this._prjsList.find(item => item.id === id);
+    if (p) {
+      this._current = p;
+      this.curPrjSubject.next(this._current);
+    }
+    return this._current;
   }
 
-  public getCurrentId(): string {
-    if (this.current && this.current.id) {
-      return this.current.id;
-    }
-    return '';
+  getCurrent(): IProject {
+    return this._current;
   }
 
-  Add(prj: IProject): Observable<IProject> {
-    const xdsPrj: IXDSProjectConfig = {
-      id: '',
-      serverId: prj.serverId,
-      label: prj.label || '',
-      clientPath: prj.pathClient.trim(),
-      serverPath: prj.pathServer,
-      type: prj.type,
-      defaultSdkID: prj.defaultSdkID,
-    };
+  add(prj: IProject): Observable<IProject> {
     // Send config to XDS server
-    return this.xdsSvr.addProject(xdsPrj)
+    return this.xdsSvr.addProject(this._convToIXdsProject(prj))
       .map(xp => this._convToIProject(xp));
   }
 
-  Delete(prj: IProject): Observable<IProject> {
+  delete(prj: IProject): Observable<IProject> {
     const idx = this._getProjectIdx(prj.id);
     const delPrj = prj;
     if (idx === -1) {
@@ -143,7 +141,7 @@ export class ProjectService {
       .map(res => delPrj);
   }
 
-  Sync(prj: IProject): Observable<string> {
+  sync(prj: IProject): Observable<string> {
     const idx = this._getProjectIdx(prj.id);
     if (idx === -1) {
       throw new Error('Invalid project id (id=' + prj.id + ')');
@@ -151,6 +149,17 @@ export class ProjectService {
     return this.xdsSvr.syncProject(prj.id);
   }
 
+  setSettings(prj: IProject): Observable<IProject> {
+    return this.xdsSvr.updateProject(this._convToIXdsProject(prj))
+      .map(xp => this._convToIProject(xp));
+  }
+
+  getDefaultSettings(): IUISettings {
+    return defaultUISettings;
+  }
+
+  /***  Private functions  ***/
+
   private _isUsableProject(p) {
     return p && p.isInSync &&
       (p.status === ProjectStatus.Enable) &&
@@ -161,7 +170,27 @@ export class ProjectService {
     return this._prjsList.findIndex((item) => item.id === id);
   }
 
+
+  private _convToIXdsProject(prj: IProject): IXDSProjectConfig {
+    const xPrj: IXDSProjectConfig = {
+      id: prj.id || '',
+      serverId: prj.serverId,
+      label: prj.label || '',
+      clientPath: prj.pathClient.trim(),
+      serverPath: prj.pathServer,
+      type: prj.type,
+      defaultSdkID: prj.defaultSdkID,
+      clientData: JSON.stringify(prj.uiSettings || defaultUISettings),
+    };
+    return xPrj;
+  }
+
   private _convToIProject(rPrj: IXDSProjectConfig): IProject {
+    let settings = defaultUISettings;
+    if (rPrj.clientData && rPrj.clientData !== '') {
+      settings = JSON.parse(rPrj.clientData);
+    }
+
     // Convert XDSFolderConfig to IProject
     const pp: IProject = {
       id: rPrj.id,
@@ -175,14 +204,15 @@ export class ProjectService {
       isUsable: this._isUsableProject(rPrj),
       defaultSdkID: rPrj.defaultSdkID,
       serverPrjDef: Object.assign({}, rPrj),  // do a copy
+      uiSettings: settings,
     };
     return pp;
   }
 
-  private _addProject(rPrj: IXDSProjectConfig, noNext?: boolean): IProject {
+  private _addProject(prj: IXDSProjectConfig, noNext?: boolean): IProject {
 
     // Convert XDSFolderConfig to IProject
-    const pp = this._convToIProject(rPrj);
+    const pp = this._convToIProject(prj);
 
     // add new project
     this._prjsList.push(pp);
@@ -199,9 +229,38 @@ export class ProjectService {
     });
 
     if (!noNext) {
-      this.prjsSubject.next(Object.assign([], this._prjsList));
+      this.prjsSubject.next(this._prjsList);
     }
 
     return pp;
   }
+
+  private _delProject(prj: IXDSProjectConfig) {
+    const idx = this._prjsList.findIndex(item => item.id === prj.id);
+    if (idx === -1) {
+      if (isDevMode) {
+        /* tslint:disable:no-console */
+        console.log('Warning: Try to delete project unknown id: prj=', prj);
+      }
+      return;
+    }
+    const delId = this._prjsList[idx].id;
+    this._prjsList.splice(idx, 1);
+    if (this._prjsList[idx].id === this._current.id) {
+      this.setCurrent(this._prjsList[0]);
+    }
+    this.prjsSubject.next(this._prjsList);
+  }
+
+  private _updateProject(prj: IXDSProjectConfig) {
+    const i = this._getProjectIdx(prj.id);
+    if (i >= 0) {
+      // XXX for now, only isInSync and status may change
+      this._prjsList[i].isInSync = prj.isInSync;
+      this._prjsList[i].status = prj.status;
+      this._prjsList[i].isUsable = this._isUsableProject(prj);
+      this.prjsSubject.next(this._prjsList);
+    }
+  }
+
 }
index 7559673..2f751a7 100644 (file)
@@ -47,7 +47,7 @@ export class XDSConfigService {
   }
 
   getCurServer(): IXDServerCfg {
-    return Object.assign({}, this._curServer);
+    return this._curServer;
   }
 
   setCurServer(svr: IXDServerCfg): Observable<IXDServerCfg> {
@@ -58,7 +58,7 @@ export class XDSConfigService {
         .map(cfg => this._updateCurServer())
         .catch(err => {
           this._curServer.connected = false;
-          this.curServer$.next(Object.assign({}, this._curServer));
+          this.curServer$.next(this._curServer);
           return Observable.throw(err);
         });
     } else {
@@ -66,7 +66,7 @@ export class XDSConfigService {
         return this.xdsAgentSvr.setServerRetry(curSvr.id, svr.connRetry)
           .map(cfg => this._updateCurServer())
           .catch(err => {
-            this.curServer$.next(Object.assign({}, this._curServer));
+            this.curServer$.next(this._curServer);
             return Observable.throw(err);
           });
       }
@@ -76,7 +76,7 @@ export class XDSConfigService {
 
   private _updateCurServer() {
     this._curServer = this._getCurServer();
-    this.curServer$.next(Object.assign({}, this._curServer));
+    this.curServer$.next(this._curServer);
   }
 
   private _getCurServer(url?: string): IXDServerCfg {
index 56e493f..06ca557 100644 (file)
@@ -1,4 +1,4 @@
-import { Injectable, Inject } from '@angular/core';
+import { Injectable, Inject, isDevMode } from '@angular/core';
 import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
 import { DOCUMENT } from '@angular/common';
 import { Observable } from 'rxjs/Observable';
@@ -44,6 +44,7 @@ export interface IXDSProjectConfig {
   status?: string;
   isInSync?: boolean;
   defaultSdkID: string;
+  clientData?: string;
 }
 
 export interface IXDSVer {
@@ -107,12 +108,16 @@ export class XDSAgentService {
 
   public XdsConfig$: Observable<IXDSConfig>;
   public Status$: Observable<IAgentStatus>;
-  public ProjectState$ = <Subject<IXDSProjectConfig>>new Subject();
   public CmdOutput$ = <Subject<ICmdOutput>>new Subject();
   public CmdExit$ = <Subject<ICmdExit>>new Subject();
 
+  protected projectAdd$ = new Subject<IXDSProjectConfig>();
+  protected projectDel$ = new Subject<IXDSProjectConfig>();
+  protected projectChange$ = new Subject<IXDSProjectConfig>();
+
   private baseUrl: string;
   private wsUrl: string;
+  private httpSessionID: string;
   private _config = <IXDSConfig>{ servers: [] };
   private _status = { connected: false, servers: [] };
 
@@ -130,14 +135,25 @@ export class XDSAgentService {
     const originUrl = this.document.location.origin;
     this.baseUrl = originUrl + '/api/v1';
 
-    const re = originUrl.match(/http[s]?:\/\/([^\/]*)[\/]?/);
-    if (re === null || re.length < 2) {
-      console.error('ERROR: cannot determine Websocket url');
-    } else {
-      this.wsUrl = 'ws://' + re[1];
-      this._handleIoSocket();
-      this._RegisterEvents();
-    }
+    // Retrieve Session ID / token
+    this.http.get(this.baseUrl + '/version', { observe: 'response' })
+      .subscribe(
+      resp => {
+        this.httpSessionID = resp.headers.get('xds-agent-sid');
+
+        const re = originUrl.match(/http[s]?:\/\/([^\/]*)[\/]?/);
+        if (re === null || re.length < 2) {
+          console.error('ERROR: cannot determine Websocket url');
+        } else {
+          this.wsUrl = 'ws://' + re[1];
+          this._handleIoSocket();
+          this._RegisterEvents();
+        }
+      },
+      err => {
+        /* tslint:disable:no-console */
+        console.error('ERROR while retrieving session id:', err);
+      });
   }
 
   private _NotifyXdsAgentState(sts: boolean) {
@@ -182,6 +198,8 @@ export class XDSAgentService {
       console.error('WS error:', err);
     });
 
+    // XDS Events decoding
+
     this.socket.on('make:output', data => {
       this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data));
     });
@@ -198,8 +216,6 @@ export class XDSAgentService {
       this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
     });
 
-    // Events
-    // (project-add and project-delete events are managed by project.service)
     this.socket.on('event:server-config', ev => {
       if (ev && ev.data) {
         const cfg: IXDServerCfg = ev.data;
@@ -212,19 +228,52 @@ 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) {
+          this.alert.info('Project "' + ev.data.label + '" has been added by another tool.');
+        }
+      } else if (isDevMode) {
+        /* tslint:disable:no-console */
+        console.log('Warning: received event:project-add with unknown data: ev=', ev);
+      }
+    });
+
+    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) {
+          this.alert.info('Project "' + ev.data.label + '" has been deleted by another tool.');
+        }
+      } else if (isDevMode) {
+        console.log('Warning: received event:project-delete with unknown data: ev=', ev);
+      }
+    });
+
     this.socket.on('event:project-state-change', ev => {
       if (ev && ev.data) {
-        this.ProjectState$.next(Object.assign({}, 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);
       }
     });
 
   }
 
   /**
-  ** Events
+  ** Events registration
   ***/
-  addEventListener(ev: string, fn: Function): SocketIOClient.Emitter {
-    return this.socket.addEventListener(ev, fn);
+  onProjectAdd(): Observable<IXDSProjectConfig> {
+    return this.projectAdd$.asObservable();
+  }
+
+  onProjectDelete(): Observable<IXDSProjectConfig> {
+    return this.projectDel$.asObservable();
+  }
+
+  onProjectChange(): Observable<IXDSProjectConfig> {
+    return this.projectChange$.asObservable();
   }
 
   /**
@@ -307,6 +356,10 @@ export class XDSAgentService {
     return this._delete('/projects/' + id);
   }
 
+  updateProject(cfg: IXDSProjectConfig): Observable<IXDSProjectConfig> {
+    return this._put('/projects/' + cfg.id, cfg);
+  }
+
   syncProject(id: string): Observable<string> {
     return this._post('/projects/sync/' + id, {});
   }
@@ -337,8 +390,8 @@ export class XDSAgentService {
       res => { },
       error => {
         this.alert.error('ERROR while registering to all events: ' + error);
-      }
-      );
+      },
+    );
   }
 
   private _getServer(ID: string): IXDServerCfg {
@@ -371,6 +424,12 @@ export class XDSAgentService {
         return this._decodeError(error);
       });
   }
+  private _put(url: string, body: any): Observable<any> {
+    return this.http.put(this.baseUrl + url, JSON.stringify(body), this._attachAuthHeaders())
+      .catch((error) => {
+        return this._decodeError(error);
+      });
+  }
   private _delete(url: string): Observable<any> {
     return this.http.delete(this.baseUrl + url, this._attachAuthHeaders())
       .catch(this._decodeError);
@@ -391,7 +450,10 @@ export class XDSAgentService {
     } else {
       e = err.message ? err.message : err.toString();
     }
-    console.log('xdsagent.service - ERROR: ', e);
+    /* tslint:disable:no-console */
+    if (isDevMode) {
+      console.log('xdsagent.service - ERROR: ', e);
+    }
     return Observable.throw(e);
   }
 }
index 5d5eff6..4fa66b8 100644 (file)
@@ -1,32 +1,37 @@
-<div class="header-container"
-     [class.left]="position === 'normal'"
-     [class.right]="position === 'inverse'">
+<div class="header-container" [class.left]="position === 'normal'" [class.right]="position === 'inverse'">
   <div class="logo-containter">
-    <a (click)="toggleSidebar()" href="#" class="navigation"><i class="nb-menu"></i></a>
-<!-- MODS_XDS
+    <a (click)="toggleSidebar()" href="#" class="navigation">
+      <i class="nb-menu"></i>
+    </a>
+    <!-- MODS_XDS
     <div class="logo" (click)="goToHome()">ngx-<span>admin</span></div>
 -->
-    <div class="logo" (click)="goToHome()">XDS <span>dashboard</span></div>
+    <div class="logo" (click)="goToHome()">XDS
+      <span>dashboard</span>
+    </div>
   </div>
-<!-- MODS_XDS
+  <!-- MODS_XDS
   <ngx-theme-switcher></ngx-theme-switcher>
 -->
 </div>
 
-<nb-actions
-  size="medium"
-  class="header-container"
-  [class.right]="position === 'normal'"
-  [class.left]="position === 'inverse'">
+<nb-actions size="medium" class="header-container" [class.right]="position === 'normal'" [class.left]="position === 'inverse'">
+  <!-- MODS_XDS
   <nb-action icon="nb-grid-b" class="toggle-layout" (click)="toggleSettings()"></nb-action>
+-->
   <nb-action>
     <nb-user [menu]="userMenu" [name]="user?.name" [picture]="user?.picture"></nb-user>
   </nb-action>
-  <nb-action class="control-item" disabled icon="nb-notifications"></nb-action>
-<!-- MODS_XDS
+  <nb-action class="control-item" disabled icon="nb-notifications">
+  </nb-action>
+  <nb-action icon="fa fa-question-circle-o">
+  </nb-action>
+
+  <!-- MODS_XDS
   <nb-action class="control-item" icon="nb-email"></nb-action>
--->
   <nb-action class="control-item">
     <nb-search type="rotate-layout" (click)="startSearch()"></nb-search>
   </nb-action>
+-->
+
 </nb-actions>
diff --git a/webapp/src/app/pages/build/build-settings-modal/build-settings-modal.component.html b/webapp/src/app/pages/build/build-settings-modal/build-settings-modal.component.html
new file mode 100644 (file)
index 0000000..7dd2ec7
--- /dev/null
@@ -0,0 +1,60 @@
+<div class="modal-header">
+  <span>Build Settings</span>
+  <button class="close" aria-label="Close" (click)="closeModal()">
+    <span aria-hidden="true">&times;</span>
+  </button>
+</div>
+
+<div class="modal-body row">
+  <div class="col-12">
+    <form [formGroup]="settingsProjectForm" (ngSubmit)="onSubmit()">
+
+      <div class="form-group row">
+        <label for="clean-cmd" class="col-sm-3 col-form-label">Clean command</label>
+        <div class="col-sm-9">
+          <input type="text" id="inputCleanCmd" class="form-control" formControlName="cmdClean">
+        </div>
+      </div>
+
+      <div class="form-group row">
+        <label for="prebuild-cmd" class="col-sm-3 col-form-label">Pre-Build command</label>
+        <div class="col-sm-9">
+          <input type="text" id="inputPrebuildCmd" class="form-control" formControlName="cmdPrebuild">
+        </div>
+      </div>
+
+      <div class="form-group row">
+        <label for="build-cmd" class="col-sm-3 col-form-label">Build command</label>
+        <div class="col-sm-9">
+          <input type="text" id="inputBuildCmd" class="form-control" formControlName="cmdBuild">
+        </div>
+      </div>
+
+      <div class="form-group row">
+        <label for="populate-cmd" class="col-sm-3 col-form-label">Populate command</label>
+        <div class="col-sm-9">
+          <input type="text" id="inputPopulateCmd" class="form-control" formControlName="cmdPopulate">
+        </div>
+      </div>
+
+      <div class="form-group row">
+        <label for="envvars-cmd" class="col-sm-3 col-form-label">Env variables</label>
+        <div class="col-sm-9">
+          <input type="text" id="inputEnvVars" class="form-control" formControlName="envVars">
+        </div>
+      </div>
+
+      <div class="offset-sm-9 col-sm-9">
+        <button class="btn btn-sm btn-hero-secondary" (click)="closeAction=false; resetDefault()">Reset settings</button>
+      </div>
+    </form>
+  </div>
+</div>
+<div class="modal-footer form-group">
+  <div class="col-12">
+    <div class="offset-sm-4 col-sm-6">
+      <button class="btn btn-md btn-secondary" (click)="closeAction=false; closeModal()"> Cancel </button>
+      <button class="btn btn-md btn-primary" [disabled]="!settingsProjectForm.valid" (click)="closeAction=true; onSubmit()">Update</button>
+    </div>
+  </div>
+</div>
diff --git a/webapp/src/app/pages/build/build-settings-modal/build-settings-modal.component.ts b/webapp/src/app/pages/build/build-settings-modal/build-settings-modal.component.ts
new file mode 100644 (file)
index 0000000..01c6d1e
--- /dev/null
@@ -0,0 +1,77 @@
+import { Component, 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';
+
+import { AlertService } from '../../../@core-xds/services/alert.service';
+import { ProjectService, IProject } from '../../../@core-xds/services/project.service';
+
+
+@Component({
+  selector: 'xds-build-settings-modal',
+  templateUrl: 'build-settings-modal.component.html',
+})
+
+export class BuildSettingsModalComponent implements OnInit {
+  // @Input('server-id') serverID: string;
+  private serverID: string;
+
+  closeAction = false;
+  userEditedLabel = false;
+
+  settingsProjectForm: FormGroup;
+  subpathCtrl = new FormControl('', Validators.nullValidator);
+
+  private curPrj: IProject;
+
+  constructor(
+    private alert: AlertService,
+    private projectSvr: ProjectService,
+    private fb: FormBuilder,
+    private activeModal: NgbActiveModal,
+  ) {
+    this.settingsProjectForm = fb.group({
+      subpath: this.subpathCtrl,
+      cmdClean: ['', Validators.required],
+      cmdPrebuild: ['', Validators.nullValidator],
+      cmdBuild: ['', Validators.required],
+      cmdPopulate: ['', Validators.nullValidator],
+      cmdArgs: ['', Validators.nullValidator],
+      envVars: ['', Validators.nullValidator],
+    });
+  }
+
+  ngOnInit() {
+    this.curPrj = this.projectSvr.getCurrent();
+    this.settingsProjectForm.patchValue(this.curPrj.uiSettings);
+  }
+
+  closeModal() {
+    this.activeModal.close();
+  }
+
+  resetDefault() {
+    this.settingsProjectForm.patchValue(this.projectSvr.getDefaultSettings());
+  }
+
+  onSubmit() {
+    if (!this.closeAction) {
+      return;
+    }
+
+    this.curPrj.uiSettings = this.settingsProjectForm.value;
+    this.projectSvr.setSettings(this.curPrj)
+    .subscribe(prj => {
+      this.alert.info('Settings of project "' + prj.label + '" successfully updated.');
+      this.closeModal();
+
+      // Reset Value for the next creation
+      this.settingsProjectForm.reset();
+    },
+    err => {
+      this.alert.error(err, 60);
+      this.closeModal();
+    });
+  }
+
+}
index a1ef62d..1ce9484 100644 (file)
       </nb-actions>
     </nb-card-body>
   </div>
-  <div class="col-md-12 col-lg-12 col-xxxl-6">
+  <div class="col-md-12 col-lg-12">
     <nb-card size="xlarge">
       <nb-tabset fullWidth>
 
-        <nb-tab tabTitle="Build">
+        <nb-tab tabTitle="Build" style="overflow: hidden;">
 
-          <div class="row" style="margin-top:1em;">
-            <!-- FIXME SEB
-            <button class="btn pull-right " (click)="reset() ">
-              <span class="fa fa-eraser fa-size-x2"></span>
-            </button>
-          -->
-            <div class="col-md-12 text-center ">
-              <textarea rows="20" class="textarea-scroll" #scrollOutput>{{ cmdOutput }}</textarea>
-            </div>
+          <div class="offset-md-10 col-md-2 right">
+            <i class="control-icon fa fa-eraser" (click)="resetOutput()">
+            </i>
+          </div>
+          <div class="col-md-12 text-center ">
+            <textarea rows="20" class="textarea-scroll" #scrollOutput>{{ cmdOutput }}</textarea>
           </div>
 
           <nb-card-body>
-              <nb-actions size="medium" fullWidth>
-                <nb-action (click)="clean()">
-                  <i class="fa fa-eraser"></i>
-                  <span>Clean</span>
-                </nb-action>
-                <nb-action (click)="preBuild()">
-                  <i class="nb-list"></i>
-                  <span>Pre-Build</span>
-                </nb-action>
-                <nb-action (click)="build()">
-                  <i class="fa fa-wrench"></i>
-                  <span>Build</span>
-                </nb-action>
-                <nb-action (click)="populate()">
-                  <i class="fa fa-send"></i>
-                  <span>Populate</span>
-                </nb-action>
-              </nb-actions>
-            </nb-card-body>
+            <nb-actions size="medium" fullWidth>
+              <nb-action (click)="settingsShow()">
+                <i class="fa fa-cog"></i>
+                <span>Settings</span>
+              </nb-action>
+
+              <nb-action>
+              </nb-action>
+
+              <nb-action (click)="clean()">
+                <i class="fa fa-eraser"></i>
+                <span>Clean</span>
+              </nb-action>
+              <nb-action (click)="preBuild()">
+                <i class="nb-list"></i>
+                <span>Pre-Build</span>
+              </nb-action>
+              <nb-action (click)="build()">
+                <i class="fa fa-wrench"></i>
+                <span>Build</span>
+              </nb-action>
+              <nb-action (click)="populate()">
+                <i class="fa fa-send"></i>
+                <span>Populate</span>
+              </nb-action>
+            </nb-actions>
+          </nb-card-body>
 
         </nb-tab>
 
index b256f66..5308f3f 100644 (file)
   }
 }
 
+.right {
+  display: flex;
+  flex-direction: row-reverse;
+  padding-right: 30px;
+}
+
 nb-action {
   i {
     font-size: 2rem;
index 016192c..a676a66 100644 (file)
@@ -8,7 +8,7 @@ describe('BuildComponent', () => {
 
   beforeEach(async(() => {
     TestBed.configureTestingModule({
-      declarations: [ BuildComponent ]
+      declarations: [ BuildComponent ],
     })
     .compileComponents();
   }));
index 5adb9bc..99b7e54 100644 (file)
@@ -1,11 +1,13 @@
 import { Component, ViewEncapsulation, AfterViewChecked, ElementRef, ViewChild, OnInit, Input } from '@angular/core';
 import { Observable } from 'rxjs/Observable';
 import { FormControl, FormGroup, Validators, FormBuilder } from '@angular/forms';
-import { CookieService } from 'ngx-cookie';
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
 
 import 'rxjs/add/operator/scan';
 import 'rxjs/add/operator/startWith';
 
+import { BuildSettingsModalComponent } from './build-settings-modal/build-settings-modal.component';
+
 import { XDSAgentService, ICmdOutput } from '../../@core-xds/services/xdsagent.service';
 import { ProjectService, IProject } from '../../@core-xds/services/project.service';
 import { AlertService, IAlert } from '../../@core-xds/services/alert.service';
@@ -26,9 +28,6 @@ export class BuildComponent implements OnInit, AfterViewChecked {
   //  @Input() curProject: IProject;
   @Input() curProject = <IProject>null;
 
-  public buildForm: FormGroup;
-  public subpathCtrl = new FormControl('', Validators.required);
-  public debugEnable = false;
   public buildIsCollapsed = false;
   public cmdOutput: string;
   public cmdInfo: string;
@@ -38,37 +37,16 @@ export class BuildComponent implements OnInit, AfterViewChecked {
   constructor(
     private prjSvr: ProjectService,
     private xdsSvr: XDSAgentService,
-    private fb: FormBuilder,
     private alertSvr: AlertService,
     private sdkSvr: SdkService,
-    private cookie: CookieService,
+    private modalService: NgbModal,
   ) {
     this.cmdOutput = '';
     this.cmdInfo = '';      // TODO: to be remove (only for debug)
-    this.buildForm = fb.group({
-      subpath: this.subpathCtrl,
-      cmdClean: ['', Validators.nullValidator],
-      cmdPrebuild: ['', Validators.nullValidator],
-      cmdBuild: ['', Validators.nullValidator],
-      cmdPopulate: ['', Validators.nullValidator],
-      cmdArgs: ['', Validators.nullValidator],
-      envVars: ['', Validators.nullValidator],
-    });
+
   }
 
   ngOnInit() {
-    // Set default settings
-    // TODO save & restore values from cookies
-    this.buildForm.patchValue({
-      subpath: '',
-      cmdClean: 'rm -rf build',
-      cmdPrebuild: 'mkdir -p build && cd build && cmake ..',
-      cmdBuild: 'cd build && make',
-      cmdPopulate: 'cd build && make remote-target-populate',
-      cmdArgs: '',
-      envVars: '',
-    });
-
     // Command output data tunneling
     this.xdsSvr.CmdOutput$.subscribe(data => {
       this.cmdOutput += data.stdout;
@@ -88,70 +66,77 @@ export class BuildComponent implements OnInit, AfterViewChecked {
     });
 
     this._scrollToBottom();
-
-    // only use for debug
-    this.debugEnable = (this.cookie.get('debug_build') === '1');
   }
 
   ngAfterViewChecked() {
     this._scrollToBottom();
   }
 
-  reset() {
+  resetOutput() {
     this.cmdOutput = '';
   }
 
+  settingsShow() {
+    const activeModal = this.modalService.open(BuildSettingsModalComponent, { size: 'lg', container: 'nb-layout' });
+    activeModal.componentInstance.modalHeader = 'Large Modal';
+  }
+
   clean() {
+    const curPrj = this.prjSvr.getCurrent();
     this._exec(
-      this.buildForm.value.cmdClean,
-      this.buildForm.value.subpath,
+      curPrj.uiSettings.cmdClean,
+      curPrj.uiSettings.subpath,
       [],
-      this.buildForm.value.envVars);
+      curPrj.uiSettings.envVars.join(' '));
   }
 
   preBuild() {
+    const curPrj = this.prjSvr.getCurrent();
     this._exec(
-      this.buildForm.value.cmdPrebuild,
-      this.buildForm.value.subpath,
+      curPrj.uiSettings.cmdPrebuild,
+      curPrj.uiSettings.subpath,
       [],
-      this.buildForm.value.envVars);
+      curPrj.uiSettings.envVars.join(' '));
   }
 
   build() {
+    const curPrj = this.prjSvr.getCurrent();
     this._exec(
-      this.buildForm.value.cmdBuild,
-      this.buildForm.value.subpath,
+      curPrj.uiSettings.cmdBuild,
+      curPrj.uiSettings.subpath,
       [],
-      this.buildForm.value.envVars
+      curPrj.uiSettings.envVars.join(' '),
     );
   }
 
   populate() {
+    const curPrj = this.prjSvr.getCurrent();
     this._exec(
-      this.buildForm.value.cmdPopulate,
-      this.buildForm.value.subpath,
+      curPrj.uiSettings.cmdPopulate,
+      curPrj.uiSettings.subpath,
       [], // args
-      this.buildForm.value.envVars
+      curPrj.uiSettings.envVars.join(' '),
     );
   }
 
   execCmd() {
+    const curPrj = this.prjSvr.getCurrent();
     this._exec(
-      this.buildForm.value.cmdArgs,
-      this.buildForm.value.subpath,
+      curPrj.uiSettings.cmdArgs.join(' '),
+      curPrj.uiSettings.subpath,
       [],
-      this.buildForm.value.envVars
+      curPrj.uiSettings.envVars.join(' '),
     );
   }
 
   private _exec(cmd: string, dir: string, args: string[], env: string) {
+    this.curProject = this.prjSvr.getCurrent();
+    const prjID = this.curProject.id;
+
     if (!this.curProject) {
-      this.alertSvr.warning('No active project', true);
+      return this.alertSvr.warning('No active project', true);
     }
 
-    // const prjID = this.curProject.id;
-    const prjID = this.prjSvr.getCurrent().id;
-
     this.cmdOutput += this._outputHeader();
 
     const sdkid = this.sdkSvr.getCurrentId();
index ac1dfab..34f05f2 100644 (file)
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
 import { ThemeModule } from '../../@theme/theme.module';
 
 import { BuildComponent } from './build.component';
+import { BuildSettingsModalComponent } from './build-settings-modal/build-settings-modal.component';
 import { ProjectSelectDropdownComponent } from './settings/project-select-dropdown.component';
 import { SdkSelectDropdownComponent } from './settings/sdk-select-dropdown.component';
 
@@ -11,10 +12,12 @@ import { SdkSelectDropdownComponent } from './settings/sdk-select-dropdown.compo
   ],
   declarations: [
     BuildComponent,
+    BuildSettingsModalComponent,
     ProjectSelectDropdownComponent,
     SdkSelectDropdownComponent,
   ],
   entryComponents: [
+    BuildSettingsModalComponent,
   ],
 })
 export class BuildModule { }
diff --git a/webapp/src/app/pages/build/settings-modal/build-settings-modal.component.ts b/webapp/src/app/pages/build/settings-modal/build-settings-modal.component.ts
new file mode 100644 (file)
index 0000000..fd1b904
--- /dev/null
@@ -0,0 +1,143 @@
+import { Component, Input, ViewChild, 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';
+
+// Import RxJs required methods
+import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/filter';
+import 'rxjs/add/operator/debounceTime';
+
+import { AlertService, IAlert } from '../../../@core-xds/services/alert.service';
+import { ProjectService, IProject, ProjectType, ProjectTypes } from '../../../@core-xds/services/project.service';
+import { XDSConfigService } from '../../../@core-xds/services/xds-config.service';
+
+
+@Component({
+  selector: 'xds-build-settings-modal',
+  templateUrl: 'build-settings-modal.component.html',
+  styleUrls: ['build-settings-modal.component.scss'],
+})
+export class BuildSettingsModalComponent implements OnInit {
+  // @Input('server-id') serverID: string;
+  private serverID: string;
+
+  cancelAction = false;
+  userEditedLabel = false;
+  projectTypes = ProjectTypes;
+
+  addProjectForm: FormGroup;
+  typeCtrl: FormControl;
+  pathCliCtrl: FormControl;
+  pathSvrCtrl: FormControl;
+
+  constructor(
+    private alert: AlertService,
+    private projectSvr: ProjectService,
+    private XdsConfigSvr: XDSConfigService,
+    private fb: FormBuilder,
+    private activeModal: NgbActiveModal,
+  ) {
+    // Define types (first one is special/placeholder)
+    this.projectTypes.unshift({ value: ProjectType.UNSET, display: '--Select a type--' });
+
+    this.typeCtrl = new FormControl(this.projectTypes[0].value, this.validatorProjType);
+    this.pathCliCtrl = new FormControl('', this.validatorProjPath);
+    this.pathSvrCtrl = new FormControl({ value: '', disabled: true }, this.validatorProjPath);
+
+    this.addProjectForm = fb.group({
+      type: this.typeCtrl,
+      pathCli: this.pathCliCtrl,
+      pathSvr: this.pathSvrCtrl,
+      label: ['', Validators.nullValidator],
+    });
+  }
+
+
+  ngOnInit() {
+    // Update server ID
+    this.serverID = this.XdsConfigSvr.getCurServer().id;
+    this.XdsConfigSvr.onCurServer().subscribe(svr => this.serverID = svr.id);
+
+    // Auto create label name
+    this.pathCliCtrl.valueChanges
+      .debounceTime(100)
+      .filter(n => n)
+      .map(n => {
+        const last = n.split('/');
+        let nm = n;
+        if (last.length > 0) {
+          nm = last.pop();
+          if (nm === '' && last.length > 0) {
+            nm = last.pop();
+          }
+        }
+        return 'Project_' + nm;
+      })
+      .subscribe(value => {
+        if (value && !this.userEditedLabel) {
+          this.addProjectForm.patchValue({ label: value });
+        }
+      });
+
+    // Handle disabling of Server path
+    this.typeCtrl.valueChanges
+      .debounceTime(500)
+      .subscribe(valType => {
+        const dis = (valType === String(ProjectType.SYNCTHING));
+        this.pathSvrCtrl.reset({ value: '', disabled: dis });
+      });
+  }
+
+  closeModal() {
+    this.activeModal.close();
+  }
+
+  onKeyLabel(event: any) {
+    this.userEditedLabel = (this.addProjectForm.value.label !== '');
+  }
+
+  onChangeLocalProject(e) {
+  }
+
+  onSubmit() {
+    if (this.cancelAction) {
+      return;
+    }
+
+    const formVal = this.addProjectForm.value;
+
+    const type = formVal['type'].value;
+    this.projectSvr.add({
+      serverId: this.serverID,
+      label: formVal['label'],
+      pathClient: formVal['pathCli'],
+      pathServer: formVal['pathSvr'],
+      type: formVal['type'],
+      // FIXME: allow to set defaultSdkID from New Project config panel
+    })
+      .subscribe(prj => {
+        this.alert.info('Project ' + prj.label + ' successfully created.');
+        this.closeModal();
+
+        // Reset Value for the next creation
+        this.addProjectForm.reset();
+        const selectedType = this.projectTypes[0].value;
+        this.addProjectForm.patchValue({ type: selectedType });
+
+      },
+      err => {
+        this.alert.error(err, 60);
+        this.closeModal();
+      });
+  }
+
+  private validatorProjType(g: FormGroup): ValidationErrors | null {
+    return (g.value !== ProjectType.UNSET) ? null : { validatorProjType: { valid: false } };
+  }
+
+  private validatorProjPath(g: FormGroup): ValidationErrors | null {
+    return (g.disabled || g.value !== '') ? null : { validatorProjPath: { valid: false } };
+  }
+
+}
index da3580a..a83ec0a 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, OnInit, Input } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
 
 import { IProject, ProjectService } from '../../../@core-xds/services/project.service';
 
@@ -7,33 +8,27 @@ import { IProject, ProjectService } from '../../../@core-xds/services/project.se
   template: `
       <div class="form-group">
       <label>Project</label>
-      <select class="form-control">
-        <option *ngFor="let prj of projects" (click)="select(prj)">{{prj.label}}</option>
+      <select class="form-control" [(ngModel)]="curPrj" (click)="select()">
+        <option  *ngFor="let prj of projects$ | async" [ngValue]="prj">{{ prj.label }}</option>
       </select>
     </div>
     `,
 })
 export class ProjectSelectDropdownComponent implements OnInit {
 
-  projects: IProject[];
+  projects$: Observable<IProject[]>;
   curPrj: IProject;
 
-  constructor(private prjSvr: ProjectService) { }
+  constructor(private projectSvr: ProjectService) { }
 
   ngOnInit() {
-    this.curPrj = this.prjSvr.getCurrent();
-    this.prjSvr.Projects$.subscribe((s) => {
-      if (s) {
-        this.projects = s;
-        if (this.curPrj === null || s.indexOf(this.curPrj) === -1) {
-          this.prjSvr.setCurrent(this.curPrj = s.length ? s[0] : null);
-        }
-      }
-    });
+    this.curPrj = this.projectSvr.getCurrent();
+    this.projects$ = this.projectSvr.projects$;
+    this.projectSvr.curProject$.subscribe(p => this.curPrj = p);
   }
 
-  select(s) {
-    this.prjSvr.setCurrent(this.curPrj = s);
+  select() {
+    this.projectSvr.setCurrentById(this.curPrj.id);
   }
 }
 
index 3901331..556316c 100644 (file)
@@ -2,6 +2,8 @@ import { Component } from '@angular/core';
 
 @Component({
     selector: 'xds-dwnl-agent',
+    template: ``,
+    /* FIXME SEB: to be reworked
     template: `
         <template #popTemplate>
             <h3>Install xds-agent:</h3>
@@ -25,7 +27,8 @@ import { Component } from '@angular/core';
         .fa-size-x2 {
             font-size: 20px;
         }
-    `]
+    `],
+    */
 })
 
 export class DwnlAgentComponent {
index 2fdaf94..d11bcf3 100644 (file)
@@ -10,6 +10,6 @@ import { ConfigRoutingModule, routedConfig } from './config-routing.module';
   ],
   declarations: [
     ...routedConfig,
-  ]
+  ],
 })
 export class ConfigModule { }
index e2c6748..9d26c71 100644 (file)
           <input type="text" id="inputLabel" class="form-control" formControlName="label" (keyup)="onKeyLabel($event)">
         </div>
       </div>
-
-      <div class="offset-sm-3 col-sm-9">
-        <button class="btn btn-md btn-secondary" (click)="cancelAction=true; closeModal()"> Cancel </button>
-        <button class="btn btn-md btn-primary" type="submit" [disabled]="!addProjectForm.valid">Add Folder</button>
-      </div>
     </form>
   </div>
 </div>
 <div class="modal-footer form-group">
+  <div class="col-12">
+    <div class="offset-sm-4 col-sm-6">
+      <button class="btn btn-md btn-secondary" (click)="cancelAction=true; closeModal()"> Cancel </button>
+      <button class="btn btn-md btn-primary" (click)="onSubmit()" [disabled]="!addProjectForm.valid">Add Folder</button>
+    </div>
+  </div>
 </div>
index 640ac5c..15b1b24 100644 (file)
@@ -16,7 +16,7 @@ import { XDSConfigService } from '../../../@core-xds/services/xds-config.service
 @Component({
   selector: 'xds-project-add-modal',
   templateUrl: 'project-add-modal.component.html',
-  styleUrls: ['project-add-modal.component.scss']
+  styleUrls: ['project-add-modal.component.scss'],
 })
 export class ProjectAddModalComponent implements OnInit {
   // @Input('server-id') serverID: string;
@@ -24,7 +24,7 @@ export class ProjectAddModalComponent implements OnInit {
 
   cancelAction = false;
   userEditedLabel = false;
-  projectTypes = ProjectTypes;
+  projectTypes = Object.assign([], ProjectTypes);
 
   addProjectForm: FormGroup;
   typeCtrl: FormControl;
@@ -36,7 +36,7 @@ export class ProjectAddModalComponent implements OnInit {
     private projectSvr: ProjectService,
     private XdsConfigSvr: XDSConfigService,
     private fb: FormBuilder,
-    private activeModal: NgbActiveModal
+    private activeModal: NgbActiveModal,
   ) {
     // Define types (first one is special/placeholder)
     this.projectTypes.unshift({ value: ProjectType.UNSET, display: '--Select a type--' });
@@ -108,7 +108,7 @@ export class ProjectAddModalComponent implements OnInit {
     const formVal = this.addProjectForm.value;
 
     const type = formVal['type'].value;
-    this.projectSvr.Add({
+    this.projectSvr.add({
       serverId: this.serverID,
       label: formVal['label'],
       pathClient: formVal['pathCli'],
index a433f58..6ac8d11 100644 (file)
@@ -13,11 +13,6 @@ nb-card-footer {
   text-align: right;
 }
 
-.fa-big {
-  font-size: 20px;
-  font-weight: bold;
-}
-
 .fa-size-x2 {
   font-size: 20px;
 }
@@ -31,14 +26,6 @@ th label {
   margin-bottom: 0;
 }
 
-tr.info>th {
-  vertical-align: middle;
-}
-
-tr.info>td {
-  vertical-align: middle;
-}
-
 .btn-outline-danger.btn-xds {
   color: #ff4c6a;
   &:focus {
index 160c4c8..840d656 100644 (file)
@@ -17,28 +17,28 @@ export class ProjectCardComponent {
 
   constructor(
     private alert: AlertService,
-    private projectSvr: ProjectService
+    private projectSvr: ProjectService,
   ) {
   }
 
   delete(prj: IProject) {
-    this.projectSvr.Delete(prj).subscribe(
+    this.projectSvr.delete(prj).subscribe(
       res => { },
-      err => this.alert.error('ERROR delete: ' + err)
+      err => this.alert.error('ERROR delete: ' + err),
     );
   }
 
   sync(prj: IProject) {
-    this.projectSvr.Sync(prj).subscribe(
+    this.projectSvr.sync(prj).subscribe(
       res => { },
-      err => this.alert.error('ERROR: ' + err)
+      err => this.alert.error('ERROR: ' + err),
     );
   }
 }
 
 // Make Project type human readable
 @Pipe({
-  name: 'readableType'
+  name: 'readableType',
 })
 
 export class ProjectReadableTypePipe implements PipeTransform {
index 662dfcc..ecce824 100644 (file)
@@ -20,7 +20,7 @@
       </div>
     </nb-card-body>
   </div>
-  <div class="col-md-12 col-lg-12 col-xxxl-6" *ngFor="let prj of (projects$ | async)">
+  <div class="col-md-6 col-lg-6" *ngFor="let prj of (projects$ | async)">
     <xds-project-card [project]="prj"></xds-project-card>
   </div>
 </div>
index 3631fbb..92a8807 100644 (file)
@@ -25,7 +25,7 @@
     order: 1;
     flex-direction: row-reverse;
   }
-  nb-actions>nb-action {
+  nb-actions > nb-action {
     padding: 0;
   }
   nb-action {
index 179bbd0..761ff50 100644 (file)
@@ -23,7 +23,7 @@ export class ProjectsComponent implements OnInit {
   }
 
   ngOnInit() {
-    this.projects$ = this.projectSvr.Projects$;
+    this.projects$ = this.projectSvr.projects$;
   }
 
   add() {
index 48f37ce..960a2da 100644 (file)
@@ -17,7 +17,7 @@ import { ProjectAddModalComponent } from './project-add-modal/project-add-modal.
     ProjectReadableTypePipe,
   ],
   entryComponents: [
-    ProjectAddModalComponent
+    ProjectAddModalComponent,
   ],
 })
 export class ProjectsModule { }
index e404610..a83cca4 100644 (file)
@@ -13,11 +13,6 @@ nb-card-footer {
   text-align: right;
 }
 
-.fa-big {
-  font-size: 20px;
-  font-weight: bold;
-}
-
 .fa-size-x2 {
   font-size: 20px;
 }
@@ -31,14 +26,6 @@ th label {
   margin-bottom: 0;
 }
 
-tr.info>th {
-  vertical-align: middle;
-}
-
-tr.info>td {
-  vertical-align: middle;
-}
-
 .btn-outline-danger.btn-xds {
   color: #ff4c6a;
   &:focus {
index 8228570..7135d36 100644 (file)
@@ -17,7 +17,7 @@ export class SdkCardComponent {
 
   constructor(
     private alert: AlertService,
-    private sdkSvr: SdkService
+    private sdkSvr: SdkService,
   ) {
   }
 
@@ -28,7 +28,7 @@ export class SdkCardComponent {
   delete(sdk: ISdk) {
     this.sdkSvr.delete(sdk).subscribe(
       res => { },
-      err => this.alert.error('ERROR delete: ' + err)
+      err => this.alert.error('ERROR delete: ' + err),
     );
   }
 }
index adfd924..94c2501 100644 (file)
@@ -20,7 +20,7 @@
       </div>
     </nb-card-body>
   </div>
-  <div class="col-md-12 col-lg-12 col-xxxl-6" *ngFor="let sdk of (sdks$ | async)">
+  <div class="col-md-6 col-lg-6" *ngFor="let sdk of (sdks$ | async)">
     <xds-sdk-card [sdk]="sdk"></xds-sdk-card>
   </div>
 </div>
index 3631fbb..92a8807 100644 (file)
@@ -25,7 +25,7 @@
     order: 1;
     flex-direction: row-reverse;
   }
-  nb-actions>nb-action {
+  nb-actions > nb-action {
     padding: 0;
   }
   nb-action {
index 3208121..ef933af 100644 (file)
@@ -2,7 +2,7 @@ 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 { SdkAddModalComponent } from './sdk-add-modal/sdk-add-modal.component';
 
 import { SdkService, ISdk } from '../../@core-xds/services/sdk.service';
 
index 48629f6..c811eb4 100644 (file)
@@ -3,7 +3,7 @@ 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 { SdkAddModalComponent } from './sdk-add-modal/sdk-add-modal.component';
 
 
 @NgModule({
@@ -13,10 +13,10 @@ import { SdkCardComponent } from './sdk-card/sdk-card.component';
   declarations: [
     SdksComponent,
     SdkCardComponent,
-    //SdkAddModalComponent,
+    // SdkAddModalComponent,
   ],
   entryComponents: [
-    //SdkAddModalComponent
+    // SdkAddModalComponent,
   ],
 })
 export class SdksModule { }