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