From 4d843d2bde236ec23810d0904dfb8aebbc53a37b Mon Sep 17 00:00:00 2001 From: Sebastien Douheret Date: Fri, 24 Nov 2017 01:14:30 +0100 Subject: New dashboard improvements. - add build buttons - add build settings support and backup into project clientData - improved async alert - fixed project dropdown Signed-off-by: Sebastien Douheret --- .vscode/settings.json | 52 +----- glide.yaml | 2 + lib/agent/apiv1-exec.go | 18 +- lib/agent/apiv1-projects.go | 28 ++- lib/agent/apiv1.go | 7 +- lib/agent/events.go | 9 +- lib/agent/project-interface.go | 15 +- lib/agent/project-pathmap.go | 22 ++- lib/agent/project-st.go | 33 +++- lib/agent/projects.go | 58 ++++++- lib/agent/sessions.go | 8 + lib/agent/xdsserver.go | 23 ++- lib/apiv1/events.go | 7 +- lib/apiv1/projects.go | 6 + webapp/.stylelintrc.json | 2 +- .../app/@core-xds/services/alert.service.spec.ts | 14 +- webapp/src/app/@core-xds/services/alert.service.ts | 2 +- .../@core-xds/services/build-settings.service.ts | 78 +++++++++ .../app/@core-xds/services/config.service.spec.ts | 2 +- .../app/@core-xds/services/project.service.spec.ts | 2 +- .../src/app/@core-xds/services/project.service.ts | 189 ++++++++++++++------- .../app/@core-xds/services/xds-config.service.ts | 8 +- .../src/app/@core-xds/services/xdsagent.service.ts | 100 ++++++++--- .../@theme/components/header/header.component.html | 35 ++-- .../build-settings-modal.component.html | 60 +++++++ .../build-settings-modal.component.ts | 77 +++++++++ webapp/src/app/pages/build/build.component.html | 65 +++---- webapp/src/app/pages/build/build.component.scss | 6 + webapp/src/app/pages/build/build.component.spec.ts | 2 +- webapp/src/app/pages/build/build.component.ts | 85 ++++----- webapp/src/app/pages/build/build.module.ts | 3 + .../build-settings-modal.component.ts | 143 ++++++++++++++++ .../settings/project-select-dropdown.component.ts | 25 ++- .../config-xds/downloadXdsAgent.component.ts | 5 +- webapp/src/app/pages/config/config.module.ts | 2 +- .../project-add-modal.component.html | 11 +- .../project-add-modal.component.ts | 8 +- .../project-card/project-card.component.scss | 13 -- .../project-card/project-card.component.ts | 12 +- .../src/app/pages/projects/projects.component.html | 2 +- .../src/app/pages/projects/projects.component.scss | 2 +- .../src/app/pages/projects/projects.component.ts | 2 +- webapp/src/app/pages/projects/projects.module.ts | 2 +- .../pages/sdks/sdk-card/sdk-card.component.scss | 13 -- .../app/pages/sdks/sdk-card/sdk-card.component.ts | 4 +- webapp/src/app/pages/sdks/sdks.component.html | 2 +- webapp/src/app/pages/sdks/sdks.component.scss | 2 +- webapp/src/app/pages/sdks/sdks.component.ts | 2 +- webapp/src/app/pages/sdks/sdks.module.ts | 6 +- 49 files changed, 918 insertions(+), 356 deletions(-) create mode 100644 webapp/src/app/@core-xds/services/build-settings.service.ts create mode 100644 webapp/src/app/pages/build/build-settings-modal/build-settings-modal.component.html create mode 100644 webapp/src/app/pages/build/build-settings-modal/build-settings-modal.component.ts create mode 100644 webapp/src/app/pages/build/settings-modal/build-settings-modal.component.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 4c9ba28..5d43dd0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,51 +29,17 @@ ], // 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 +} diff --git a/glide.yaml b/glide.yaml index 3370e2d..1630276 100644 --- a/glide.yaml +++ b/glide.yaml @@ -25,3 +25,5 @@ import: version: ^0.1.0 subpackages: - golib/common +- package: github.com/franciscocpg/reflectme + version: ^0.1.9 diff --git a/lib/agent/apiv1-exec.go b/lib/agent/apiv1-exec.go index c199267..3cb4d23 100644 --- a/lib/agent/apiv1-exec.go +++ b/lib/agent/apiv1-exec.go @@ -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]) diff --git a/lib/agent/apiv1-projects.go b/lib/agent/apiv1-projects.go index c835967..5784896 100644 --- a/lib/agent/apiv1-projects.go +++ b/lib/agent/apiv1-projects.go @@ -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) +} diff --git a/lib/agent/apiv1.go b/lib/agent/apiv1.go index 3e742f5..36e5a54 100644 --- a/lib/agent/apiv1.go +++ b/lib/agent/apiv1.go @@ -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) diff --git a/lib/agent/events.go b/lib/agent/events.go index 9ff72ac..ccf8ddc 100644 --- a/lib/agent/events.go +++ b/lib/agent/events.go @@ -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 { diff --git a/lib/agent/project-interface.go b/lib/agent/project-interface.go index c9e9ec5..0d6bb1a 100644 --- a/lib/agent/project-interface.go +++ b/lib/agent/project-interface.go @@ -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 } diff --git a/lib/agent/project-pathmap.go b/lib/agent/project-pathmap.go index 7a96e6e..3c87770 100644 --- a/lib/agent/project-pathmap.go +++ b/lib/agent/project-pathmap.go @@ -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 diff --git a/lib/agent/project-st.go b/lib/agent/project-st.go index e2cd3cb..c4e8fce 100644 --- a/lib/agent/project-st.go +++ b/lib/agent/project-st.go @@ -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) } } diff --git a/lib/agent/projects.go b/lib/agent/projects.go index f089882..966c231 100644 --- a/lib/agent/projects.go +++ b/lib/agent/projects.go @@ -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 +} diff --git a/lib/agent/sessions.go b/lib/agent/sessions.go index 7347480..3d8b0f4 100644 --- a/lib/agent/sessions.go +++ b/lib/agent/sessions.go @@ -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() diff --git a/lib/agent/xdsserver.go b/lib/agent/xdsserver.go index 73a5bd9..7b03579 100644 --- a/lib/agent/xdsserver.go +++ b/lib/agent/xdsserver.go @@ -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) } } diff --git a/lib/apiv1/events.go b/lib/apiv1/events.go index cdd0889..b2fda62 100644 --- a/lib/apiv1/events.go +++ b/lib/apiv1/events.go @@ -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 diff --git a/lib/apiv1/projects.go b/lib/apiv1/projects.go index d76fa09..b1e64c8 100644 --- a/lib/apiv1/projects.go +++ b/lib/apiv1/projects.go @@ -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", } diff --git a/webapp/.stylelintrc.json b/webapp/.stylelintrc.json index 9732970..8132883 100644 --- a/webapp/.stylelintrc.json +++ b/webapp/.stylelintrc.json @@ -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, diff --git a/webapp/src/app/@core-xds/services/alert.service.spec.ts b/webapp/src/app/@core-xds/services/alert.service.spec.ts index b3d364c..2de2ac3 100644 --- a/webapp/src/app/@core-xds/services/alert.service.spec.ts +++ b/webapp/src/app/@core-xds/services/alert.service.spec.ts @@ -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(); + })); }); diff --git a/webapp/src/app/@core-xds/services/alert.service.ts b/webapp/src/app/@core-xds/services/alert.service.ts index c15e176..23a5e5d 100644 --- a/webapp/src/app/@core-xds/services/alert.service.ts +++ b/webapp/src/app/@core-xds/services/alert.service.ts @@ -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 index 0000000..cb52ce3 --- /dev/null +++ b/webapp/src/app/@core-xds/services/build-settings.service.ts @@ -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; + + private settingsSubject: BehaviorSubject; + 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 = 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(); + } + +} diff --git a/webapp/src/app/@core-xds/services/config.service.spec.ts b/webapp/src/app/@core-xds/services/config.service.spec.ts index a20d4ba..f39b9d9 100644 --- a/webapp/src/app/@core-xds/services/config.service.spec.ts +++ b/webapp/src/app/@core-xds/services/config.service.spec.ts @@ -5,7 +5,7 @@ import { ConfigService } from './config.service'; describe('ConfigService', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [ConfigService] + providers: [ConfigService], }); }); diff --git a/webapp/src/app/@core-xds/services/project.service.spec.ts b/webapp/src/app/@core-xds/services/project.service.spec.ts index b8edfc7..0924a73 100644 --- a/webapp/src/app/@core-xds/services/project.service.spec.ts +++ b/webapp/src/app/@core-xds/services/project.service.spec.ts @@ -5,7 +5,7 @@ import { ProjectService } from './project.service'; describe('ProjectService', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [ProjectService] + providers: [ProjectService], }); }); diff --git a/webapp/src/app/@core-xds/services/project.service.ts b/webapp/src/app/@core-xds/services/project.service.ts index 8aeed80..94469fe 100644 --- a/webapp/src/app/@core-xds/services/project.service.ts +++ b/webapp/src/app/@core-xds/services/project.service.ts @@ -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; + projects$: Observable; + curProject$: Observable; private _prjsList: IProject[] = []; - private current: IProject; private prjsSubject = >new BehaviorSubject(this._prjsList); + private _current: IProject; + private curPrjSubject = >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 { - 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 { // 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 { + delete(prj: IProject): Observable { 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 { + sync(prj: IProject): Observable { 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 { + 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); + } + } + } diff --git a/webapp/src/app/@core-xds/services/xds-config.service.ts b/webapp/src/app/@core-xds/services/xds-config.service.ts index 7559673..2f751a7 100644 --- a/webapp/src/app/@core-xds/services/xds-config.service.ts +++ b/webapp/src/app/@core-xds/services/xds-config.service.ts @@ -47,7 +47,7 @@ export class XDSConfigService { } getCurServer(): IXDServerCfg { - return Object.assign({}, this._curServer); + return this._curServer; } setCurServer(svr: IXDServerCfg): Observable { @@ -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 { diff --git a/webapp/src/app/@core-xds/services/xdsagent.service.ts b/webapp/src/app/@core-xds/services/xdsagent.service.ts index 56e493f..06ca557 100644 --- a/webapp/src/app/@core-xds/services/xdsagent.service.ts +++ b/webapp/src/app/@core-xds/services/xdsagent.service.ts @@ -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; public Status$: Observable; - public ProjectState$ = >new Subject(); public CmdOutput$ = >new Subject(); public CmdExit$ = >new Subject(); + protected projectAdd$ = new Subject(); + protected projectDel$ = new Subject(); + protected projectChange$ = new Subject(); + private baseUrl: string; private wsUrl: string; + private httpSessionID: string; private _config = { 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({}, data)); }); @@ -198,8 +216,6 @@ export class XDSAgentService { this.CmdExit$.next(Object.assign({}, 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 { + return this.projectAdd$.asObservable(); + } + + onProjectDelete(): Observable { + return this.projectDel$.asObservable(); + } + + onProjectChange(): Observable { + return this.projectChange$.asObservable(); } /** @@ -307,6 +356,10 @@ export class XDSAgentService { return this._delete('/projects/' + id); } + updateProject(cfg: IXDSProjectConfig): Observable { + return this._put('/projects/' + cfg.id, cfg); + } + syncProject(id: string): Observable { 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 { + return this.http.put(this.baseUrl + url, JSON.stringify(body), this._attachAuthHeaders()) + .catch((error) => { + return this._decodeError(error); + }); + } private _delete(url: string): Observable { 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); } } diff --git a/webapp/src/app/@theme/components/header/header.component.html b/webapp/src/app/@theme/components/header/header.component.html index 5d5eff6..4fa66b8 100644 --- a/webapp/src/app/@theme/components/header/header.component.html +++ b/webapp/src/app/@theme/components/header/header.component.html @@ -1,32 +1,37 @@ -
+
- - - +
-
- + + - - +--> + 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 index 0000000..7dd2ec7 --- /dev/null +++ b/webapp/src/app/pages/build/build-settings-modal/build-settings-modal.component.html @@ -0,0 +1,60 @@ + + + + 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 index 0000000..01c6d1e --- /dev/null +++ b/webapp/src/app/pages/build/build-settings-modal/build-settings-modal.component.ts @@ -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(); + }); + } + +} diff --git a/webapp/src/app/pages/build/build.component.html b/webapp/src/app/pages/build/build.component.html index a1ef62d..1ce9484 100644 --- a/webapp/src/app/pages/build/build.component.html +++ b/webapp/src/app/pages/build/build.component.html @@ -11,43 +11,48 @@
-
+
- + -
- -
- -
+
+ + +
+
+
- - - - Clean - - - - Pre-Build - - - - Build - - - - Populate - - - + + + + Settings + + + + + + + + Clean + + + + Pre-Build + + + + Build + + + + Populate + + + diff --git a/webapp/src/app/pages/build/build.component.scss b/webapp/src/app/pages/build/build.component.scss index b256f66..5308f3f 100644 --- a/webapp/src/app/pages/build/build.component.scss +++ b/webapp/src/app/pages/build/build.component.scss @@ -23,6 +23,12 @@ } } +.right { + display: flex; + flex-direction: row-reverse; + padding-right: 30px; +} + nb-action { i { font-size: 2rem; diff --git a/webapp/src/app/pages/build/build.component.spec.ts b/webapp/src/app/pages/build/build.component.spec.ts index 016192c..a676a66 100644 --- a/webapp/src/app/pages/build/build.component.spec.ts +++ b/webapp/src/app/pages/build/build.component.spec.ts @@ -8,7 +8,7 @@ describe('BuildComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ BuildComponent ] + declarations: [ BuildComponent ], }) .compileComponents(); })); diff --git a/webapp/src/app/pages/build/build.component.ts b/webapp/src/app/pages/build/build.component.ts index 5adb9bc..99b7e54 100644 --- a/webapp/src/app/pages/build/build.component.ts +++ b/webapp/src/app/pages/build/build.component.ts @@ -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 = 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(); diff --git a/webapp/src/app/pages/build/build.module.ts b/webapp/src/app/pages/build/build.module.ts index ac1dfab..34f05f2 100644 --- a/webapp/src/app/pages/build/build.module.ts +++ b/webapp/src/app/pages/build/build.module.ts @@ -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 index 0000000..fd1b904 --- /dev/null +++ b/webapp/src/app/pages/build/settings-modal/build-settings-modal.component.ts @@ -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 } }; + } + +} diff --git a/webapp/src/app/pages/build/settings/project-select-dropdown.component.ts b/webapp/src/app/pages/build/settings/project-select-dropdown.component.ts index da3580a..a83ec0a 100644 --- a/webapp/src/app/pages/build/settings/project-select-dropdown.component.ts +++ b/webapp/src/app/pages/build/settings/project-select-dropdown.component.ts @@ -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: `
- +
`, }) export class ProjectSelectDropdownComponent implements OnInit { - projects: IProject[]; + projects$: Observable; 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); } } diff --git a/webapp/src/app/pages/config/config-xds/downloadXdsAgent.component.ts b/webapp/src/app/pages/config/config-xds/downloadXdsAgent.component.ts index 3901331..556316c 100644 --- a/webapp/src/app/pages/config/config-xds/downloadXdsAgent.component.ts +++ b/webapp/src/app/pages/config/config-xds/downloadXdsAgent.component.ts @@ -2,6 +2,8 @@ import { Component } from '@angular/core'; @Component({ selector: 'xds-dwnl-agent', + template: ``, + /* FIXME SEB: to be reworked template: `