summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--.vscode/launch.json7
-rw-r--r--.vscode/settings.json28
-rw-r--r--Makefile78
-rw-r--r--conf.d/etc/xds-agent/config.json8
-rw-r--r--glide.yaml6
-rw-r--r--lib/agent/agent.go154
-rw-r--r--lib/agent/apiv1-browse.go28
-rw-r--r--lib/agent/apiv1-config.go108
-rw-r--r--lib/agent/apiv1-events.go73
-rw-r--r--lib/agent/apiv1-exec.go131
-rw-r--r--lib/agent/apiv1-projects.go72
-rw-r--r--lib/agent/apiv1-version.go45
-rw-r--r--lib/agent/apiv1.go129
-rw-r--r--lib/agent/events.go132
-rw-r--r--lib/agent/project-interface.go43
-rw-r--r--lib/agent/project-pathmap.go123
-rw-r--r--lib/agent/project-st.go186
-rw-r--r--lib/agent/projects.go241
-rw-r--r--lib/agent/session.go (renamed from lib/session/session.go)31
-rw-r--r--lib/agent/webserver.go244
-rw-r--r--lib/agent/xdsserver.go605
-rw-r--r--lib/apiv1/apiv1.go36
-rw-r--r--lib/apiv1/config.go45
-rw-r--r--lib/apiv1/version.go24
-rw-r--r--lib/syncthing/st.go100
-rw-r--r--lib/syncthing/stEvent.go265
-rw-r--r--lib/syncthing/stfolder.go152
-rw-r--r--lib/webserver/server.go226
-rw-r--r--lib/xdsconfig/config.go88
-rw-r--r--lib/xdsconfig/configfile.go109
-rw-r--r--lib/xdsconfig/fileconfig.go96
-rw-r--r--main.go67
-rwxr-xr-xscripts/install.sh2
-rw-r--r--webapp/README.md45
-rw-r--r--webapp/assets/favicon.icobin0 -> 26463 bytes
-rw-r--r--webapp/assets/images/iot-bzh-logo-small.pngbin0 -> 14449 bytes
-rw-r--r--webapp/assets/images/iot-graphx.jpgbin0 -> 138350 bytes
-rw-r--r--webapp/bs-config.json9
-rw-r--r--webapp/gulp.conf.js34
-rw-r--r--webapp/gulpfile.js123
-rw-r--r--webapp/package.json63
-rw-r--r--webapp/src/app/alert/alert.component.ts30
-rw-r--r--webapp/src/app/app.component.css31
-rw-r--r--webapp/src/app/app.component.html30
-rw-r--r--webapp/src/app/app.component.ts37
-rw-r--r--webapp/src/app/app.module.ts93
-rw-r--r--webapp/src/app/app.routing.ts19
-rw-r--r--webapp/src/app/config/config.component.css35
-rw-r--r--webapp/src/app/config/config.component.html101
-rw-r--r--webapp/src/app/config/config.component.ts107
-rw-r--r--webapp/src/app/config/downloadXdsAgent.component.ts47
-rw-r--r--webapp/src/app/devel/build/build.component.css54
-rw-r--r--webapp/src/app/devel/build/build.component.html115
-rw-r--r--webapp/src/app/devel/build/build.component.ts223
-rw-r--r--webapp/src/app/devel/devel.component.css19
-rw-r--r--webapp/src/app/devel/devel.component.html40
-rw-r--r--webapp/src/app/devel/devel.component.ts35
-rw-r--r--webapp/src/app/home/home.component.ts81
-rw-r--r--webapp/src/app/main.ts6
-rw-r--r--webapp/src/app/projects/projectAddModal.component.css24
-rw-r--r--webapp/src/app/projects/projectAddModal.component.html54
-rw-r--r--webapp/src/app/projects/projectAddModal.component.ts147
-rw-r--r--webapp/src/app/projects/projectCard.component.ts91
-rw-r--r--webapp/src/app/projects/projectsListAccordion.component.ts39
-rw-r--r--webapp/src/app/sdks/sdkAddModal.component.html23
-rw-r--r--webapp/src/app/sdks/sdkAddModal.component.ts24
-rw-r--r--webapp/src/app/sdks/sdkCard.component.ts55
-rw-r--r--webapp/src/app/sdks/sdkSelectDropdown.component.ts48
-rw-r--r--webapp/src/app/sdks/sdksListAccordion.component.ts26
-rw-r--r--webapp/src/app/services/alert.service.ts67
-rw-r--r--webapp/src/app/services/config.service.ts60
-rw-r--r--webapp/src/app/services/project.service.ts199
-rw-r--r--webapp/src/app/services/sdk.service.ts54
-rw-r--r--webapp/src/app/services/utils.service.ts33
-rw-r--r--webapp/src/app/services/xdsagent.service.ts401
-rw-r--r--webapp/src/index.html50
-rw-r--r--webapp/src/systemjs.config.js69
-rw-r--r--webapp/tsconfig.json18
-rw-r--r--webapp/tslint.json55
-rw-r--r--webapp/tslint.prod.json56
-rw-r--r--webapp/typings.json11
82 files changed, 6059 insertions, 610 deletions
diff --git a/.gitignore b/.gitignore
index 6ecdd8f..2d75cb7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,9 +4,11 @@ package
**/glide.lock
**/vendor
*.zip
-
debug
+webapp/dist
+webapp/node_modules
+**/npm*.log
# private (prefixed by 2 underscores) directories or files
__*/
-__* \ No newline at end of file
+__*
diff --git a/.vscode/launch.json b/.vscode/launch.json
index ed892d6..69944df 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -12,11 +12,12 @@
"env": {
"GOPATH": "${workspaceRoot}/../../../..:${env:GOPATH}",
"WORKSPACE_ROOT": "${workspaceRoot}",
- "DEBUG_MODE": "1",
+ "XDS_DEBUG_MODE": "1",
+ "XDS_LOG_SILLY": "0",
"ROOT_DIR": "${workspaceRoot}/../../../.."
},
- "args": ["-log", "debug", "-c", "config.json.in"],
+ "args": ["-log", "debug", "-c", "__agent-config_local_dev.json"],
"showLog": false
}
]
-} \ No newline at end of file
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 1bc5381..eceb734 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -8,8 +8,19 @@
"vendor": true,
"debug": true,
"bin": true,
- "tools": true
+ "tools": true,
+ "webapp/dist": true,
+ "webapp/node_modules": true
},
+ // Specify paths/files to ignore. (Supports Globs)
+ "cSpell.ignorePaths": [
+ "**/node_modules/**",
+ "**/vscode-extension/**",
+ "**/.git/**",
+ "**/vendor/**",
+ ".vscode",
+ "typings"
+ ],
// Words to add to dictionary for a workspace.
"cSpell.words": [
"apiv",
@@ -22,16 +33,27 @@
"socketio",
"ldflags",
"SThg",
+ "stconfig",
"Intf",
"dismissible",
"rpath",
"WSID",
"sess",
"IXDS",
+ "golib",
+ "xdsapi",
"xdsconfig",
"xdsserver",
+ "xdsagent",
+ "nbsp",
"Inot",
"inotify",
- "cmdi"
+ "cmdi",
+ "sdkid",
+ "Flds",
+ "prjs",
+ "iosk",
+ "CIFS",
+ "IPROJECT"
]
-} \ No newline at end of file
+}
diff --git a/Makefile b/Makefile
index 7e8bd6c..db4d874 100644
--- a/Makefile
+++ b/Makefile
@@ -4,9 +4,8 @@
VERSION := 0.2.0
# Syncthing version to install
-SYNCTHING_VERSION = 0.14.28
-SYNCTHING_INOTIFY_VERSION = 0.8.6
-
+SYNCTHING_VERSION = 0.14.38
+SYNCTHING_INOTIFY_VERSION = 0.8.7
# Retrieve git tag/commit to set sub-version string
@@ -24,13 +23,13 @@ ifeq ($(origin SUB_VERSION), undefined)
endif
endif
-# for backward compatibility
-DESTDIR := $(INSTALL_DIR)
-
-# Configurable variables for installation (default /opt/AGL/xds/agent)
+# Configurable variables for installation (default /opt/AGL/...)
ifeq ($(origin DESTDIR), undefined)
DESTDIR := /opt/AGL/xds/agent
endif
+ifeq ($(origin DESTDIR_WWW), undefined)
+ DESTDIR_WWW := $(DESTDIR)/www
+endif
HOST_GOOS=$(shell go env GOOS)
HOST_GOARCH=$(shell go env GOARCH)
@@ -75,28 +74,15 @@ else
endif
-all: tools/syncthing vendor build
+all: tools/syncthing build
-build: tools/syncthing/copytobin
+.PHONY: build
+build: vendor xds webapp
+
+xds: scripts tools/syncthing/copytobin
@echo "### Build XDS agent (version $(VERSION), subversion $(SUB_VERSION)) - $(BUILD_MODE)";
@cd $(ROOT_SRCDIR); $(BUILD_ENV_FLAGS) go build $(VERBOSE_$(V)) -i -o $(LOCAL_BINDIR)/xds-agent$(EXT) -ldflags "$(GO_LDFLAGS) -X main.AppVersion=$(VERSION) -X main.AppSubVersion=$(SUB_VERSION)" -gcflags "$(GO_GCFLAGS)" .
-package: clean tools/syncthing vendor build
- @mkdir -p $(PACKAGE_DIR)/xds-agent $(PACKAGE_DIR)/scripts
- @cp -a $(LOCAL_BINDIR)/* $(PACKAGE_DIR)/xds-agent
- @cp -r $(ROOT_SRCDIR)/conf.d $(ROOT_SRCDIR)/scripts $(PACKAGE_DIR)/xds-agent
- cd $(PACKAGE_DIR) && zip -r $(ROOT_SRCDIR)/$(PACKAGE_ZIPFILE) ./xds-agent
-
-.PHONY: package-all
-package-all:
- @echo "# Build linux amd64..."
- GOOS=linux GOARCH=amd64 RELEASE=1 make -f $(ROOT_SRCDIR)/Makefile package
- @echo "# Build windows amd64..."
- GOOS=windows GOARCH=amd64 RELEASE=1 make -f $(ROOT_SRCDIR)/Makefile package
- @echo "# Build darwin amd64..."
- GOOS=darwin GOARCH=amd64 RELEASE=1 make -f $(ROOT_SRCDIR)/Makefile package
- make -f $(ROOT_SRCDIR)/Makefile clean
-
test: tools/glide
go test --race $(shell $(LOCAL_TOOLSDIR)/glide novendor)
@@ -114,25 +100,55 @@ debug: build/xds tools/syncthing/copytobin
.PHONY: clean
clean:
- rm -rf $(LOCAL_BINDIR)/* debug $(ROOT_GOPRJ)/pkg/*/$(REPOPATH) $(PACKAGE_DIR)
+ rm -rf $(LOCAL_BINDIR)/* $(ROOT_SRCDIR)/debug $(ROOT_GOPRJ)/pkg/*/$(REPOPATH) $(PACKAGE_DIR)
.PHONY: distclean
distclean: clean
- rm -rf $(LOCAL_BINDIR) tools glide.lock vendor $(ROOT_SRCDIR)/*.zip
+ cd $(ROOT_SRCDIR) && rm -rf $(LOCAL_BINDIR) ./tools ./glide.lock ./vendor ./*.zip ./webapp/node_modules ./webapp/dist
+
+webapp: webapp/install
+ (cd webapp && gulp build)
+
+webapp/debug:
+ (cd webapp && gulp watch &)
+
+webapp/install:
+ (cd webapp && npm install)
+ @if [ -d ${DESTDIR}/usr/local/etc ]; then rm -rf ${DESTDIR}/usr; fi
.PHONY: install
install:
@test -e $(LOCAL_BINDIR)/xds-agent$(EXT) || { echo "Please execute first: make all\n"; exit 1; }
- @test -e $(LOCAL_BINDIR)/syncthing$(EXT) -a -e $(LOCAL_BINDIR)/syncthing-inotify$(EXT) || { echo "Please execute first: make all\n"; exit 1; }
- export DESTDIR=$(DESTDIR) && $(ROOT_SRCDIR)/scripts/install.sh
+ @test -e $(LOCAL_BINDIR)/syncthing$(EXT) -a -e $(LOCAL_BINDIR)/syncthing-inotify$(EXT) || { echo "Please execute first: make all\n"; exit 1; }
+ export DESTDIR=$(DESTDIR) && export DESTDIR_WWW=$(DESTDIR_WWW) && $(ROOT_SRCDIR)/scripts/install.sh
.PHONY: uninstall
uninstall:
- export DESTDIR=$(DESTDIR) && $(ROOT_SRCDIR)/scripts/install.sh uninstall
+ export DESTDIR=$(DESTDIR) && export DESTDIR_WWW=$(DESTDIR_WWW) && $(ROOT_SRCDIR)/scripts/install.sh uninstall
+
+package: clean tools/syncthing vendor build
+ @mkdir -p $(PACKAGE_DIR)/xds-agent $(PACKAGE_DIR)/scripts
+ @cp -a $(LOCAL_BINDIR)/* $(PACKAGE_DIR)/xds-agent
+ @cp -r $(ROOT_SRCDIR)/conf.d $(ROOT_SRCDIR)/scripts $(PACKAGE_DIR)/xds-agent
+ cd $(PACKAGE_DIR) && zip -r $(ROOT_SRCDIR)/$(PACKAGE_ZIPFILE) ./xds-agent
+
+.PHONY: package-all
+package-all:
+ @echo "# Build linux amd64..."
+ GOOS=linux GOARCH=amd64 RELEASE=1 make -f $(ROOT_SRCDIR)/Makefile package
+ @echo "# Build windows amd64..."
+ GOOS=windows GOARCH=amd64 RELEASE=1 make -f $(ROOT_SRCDIR)/Makefile package
+ @echo "# Build darwin amd64..."
+ GOOS=darwin GOARCH=amd64 RELEASE=1 make -f $(ROOT_SRCDIR)/Makefile package
+ make -f $(ROOT_SRCDIR)/Makefile clean
vendor: tools/glide glide.yaml
$(LOCAL_TOOLSDIR)/glide install --strip-vendor
+vendor/debug: vendor
+ (cd vendor/github.com/iotbzh && \
+ rm -rf xds-common && ln -s ../../../../xds-common )
+
.PHONY: tools/glide
tools/glide:
@test -f $(LOCAL_TOOLSDIR)/glide || { \
@@ -150,7 +166,7 @@ tools/syncthing:
SYNCTHING_INOTIFY_VERSION=$(SYNCTHING_INOTIFY_VERSION) \
./scripts/get-syncthing.sh; }
-.PHONY:
+.PHONY: tools/syncthing/copytobin
tools/syncthing/copytobin:
@test -e $(LOCAL_TOOLSDIR)/syncthing$(EXT) -a -e $(LOCAL_TOOLSDIR)/syncthing-inotify$(EXT) || { echo "Please execute first: make tools/syncthing\n"; exit 1; }
@mkdir -p $(LOCAL_BINDIR)
diff --git a/conf.d/etc/xds-agent/config.json b/conf.d/etc/xds-agent/config.json
index 35d702d..7c7d7cb 100644
--- a/conf.d/etc/xds-agent/config.json
+++ b/conf.d/etc/xds-agent/config.json
@@ -1,6 +1,12 @@
{
+ "httpPort": "8000",
+ "webAppDir": "./www",
"logsDir": "${HOME}/.xds/agent/logs",
- "xds-apikey": "1234abcezam",
+ "xdsServers": [
+ {
+ "url": "http://localhost:8810"
+ }
+ ],
"syncthing": {
"home": "${HOME}/.xds/agent/syncthing-config",
"gui-address": "http://localhost:8386",
diff --git a/glide.yaml b/glide.yaml
index af9ece2..5e24308 100644
--- a/glide.yaml
+++ b/glide.yaml
@@ -8,7 +8,7 @@ import:
version: ^1.1.4
- package: github.com/gin-contrib/static
- package: github.com/syncthing/syncthing
- version: =0.14.28
+ version: =0.14.38
subpackages:
- lib/sync
- lib/config
@@ -17,8 +17,8 @@ import:
version: ^1.19.1
- package: github.com/Sirupsen/logrus
version: ^0.11.5
-- package: github.com/googollee/go-socket.io
-- package: github.com/zhouhui8915/go-socket.io-client
+- package: github.com/sebd71/go-socket.io-client
+ version: 46defcb47f
- package: github.com/satori/go.uuid
version: ^1.1.0
- package: github.com/iotbzh/xds-common
diff --git a/lib/agent/agent.go b/lib/agent/agent.go
index 74872f7..3bdd89f 100644
--- a/lib/agent/agent.go
+++ b/lib/agent/agent.go
@@ -2,28 +2,39 @@ package agent
import (
"fmt"
+ "log"
"os"
"os/exec"
"os/signal"
+ "path/filepath"
"syscall"
+ "time"
"github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"github.com/iotbzh/xds-agent/lib/syncthing"
"github.com/iotbzh/xds-agent/lib/xdsconfig"
- "github.com/iotbzh/xds-agent/lib/webserver"
)
+const cookieMaxAge = "3600"
+
// Context holds the Agent context structure
type Context struct {
- ProgName string
- Config *xdsconfig.Config
- Log *logrus.Logger
- SThg *st.SyncThing
- SThgCmd *exec.Cmd
- SThgInotCmd *exec.Cmd
- WWWServer *webserver.ServerService
- Exit chan os.Signal
+ ProgName string
+ Config *xdsconfig.Config
+ Log *logrus.Logger
+ LogLevelSilly bool
+ SThg *st.SyncThing
+ SThgCmd *exec.Cmd
+ SThgInotCmd *exec.Cmd
+
+ webServer *WebServer
+ xdsServers map[string]*XdsServer
+ sessions *Sessions
+ events *Events
+ projects *Projects
+
+ Exit chan os.Signal
}
// NewAgent Create a new instance of Agent
@@ -43,11 +54,18 @@ func NewAgent(cliCtx *cli.Context) *Context {
}
log.Formatter = &logrus.TextFormatter{}
+ sillyVal, sillyLog := os.LookupEnv("XDS_LOG_SILLY")
+
// Define default configuration
ctx := Context{
- ProgName: cliCtx.App.Name,
- Log: log,
- Exit: make(chan os.Signal, 1),
+ ProgName: cliCtx.App.Name,
+ Log: log,
+ LogLevelSilly: (sillyLog && sillyVal == "1"),
+ Exit: make(chan os.Signal, 1),
+
+ webServer: nil,
+ xdsServers: make(map[string]*XdsServer),
+ events: nil,
}
// register handler on SIGTERM / exit
@@ -57,6 +75,114 @@ func NewAgent(cliCtx *cli.Context) *Context {
return &ctx
}
+// Run Main function called to run agent
+func (ctx *Context) Run() (int, error) {
+ var err error
+
+ // Logs redirected into a file when logfile option or logsDir config is set
+ ctx.Config.LogVerboseOut = os.Stderr
+ if ctx.Config.FileConf.LogsDir != "" {
+ if ctx.Config.Options.LogFile != "stdout" {
+ logFile := ctx.Config.Options.LogFile
+
+ fdL, err := os.OpenFile(logFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
+ if err != nil {
+ msgErr := fmt.Errorf("Cannot create log file %s", logFile)
+ return int(syscall.EPERM), msgErr
+ }
+ ctx.Log.Out = fdL
+
+ ctx._logPrint("Logging file: %s\n", logFile)
+ }
+
+ logFileHTTPReq := filepath.Join(ctx.Config.FileConf.LogsDir, "xds-agent-verbose.log")
+ fdLH, err := os.OpenFile(logFileHTTPReq, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
+ if err != nil {
+ msgErr := fmt.Errorf("Cannot create log file %s", logFileHTTPReq)
+ return int(syscall.EPERM), msgErr
+ }
+ ctx.Config.LogVerboseOut = fdLH
+
+ ctx._logPrint("Logging file for HTTP requests: %s\n", logFileHTTPReq)
+ }
+
+ // Create syncthing instance when section "syncthing" is present in config.json
+ if ctx.Config.FileConf.SThgConf != nil {
+ ctx.SThg = st.NewSyncThing(ctx.Config, ctx.Log)
+ }
+
+ // Start local instance of Syncthing and Syncthing-notify
+ if ctx.SThg != nil {
+ ctx.Log.Infof("Starting Syncthing...")
+ ctx.SThgCmd, err = ctx.SThg.Start()
+ if err != nil {
+ return 2, err
+ }
+ fmt.Printf("Syncthing started (PID %d)\n", ctx.SThgCmd.Process.Pid)
+
+ ctx.Log.Infof("Starting Syncthing-inotify...")
+ ctx.SThgInotCmd, err = ctx.SThg.StartInotify()
+ if err != nil {
+ return 2, err
+ }
+ fmt.Printf("Syncthing-inotify started (PID %d)\n", ctx.SThgInotCmd.Process.Pid)
+
+ // Establish connection with local Syncthing (retry if connection fail)
+ time.Sleep(3 * time.Second)
+ maxRetry := 30
+ retry := maxRetry
+ for retry > 0 {
+ if err := ctx.SThg.Connect(); err == nil {
+ break
+ }
+ ctx.Log.Infof("Establishing connection to Syncthing (retry %d/%d)", retry, maxRetry)
+ time.Sleep(time.Second)
+ retry--
+ }
+ if err != nil || retry == 0 {
+ return 2, err
+ }
+
+ // Retrieve Syncthing config
+ id, err := ctx.SThg.IDGet()
+ if err != nil {
+ return 2, err
+ }
+ ctx.Log.Infof("Local Syncthing ID: %s", id)
+
+ } else {
+ ctx.Log.Infof("Cloud Sync / Syncthing not supported")
+ }
+
+ // Create Web Server
+ ctx.webServer = NewWebServer(ctx)
+
+ // Sessions manager
+ ctx.sessions = NewClientSessions(ctx, cookieMaxAge)
+
+ // Create events management
+ ctx.events = NewEvents(ctx)
+
+ // Create projects management
+ ctx.projects = NewProjects(ctx, ctx.SThg)
+
+ // Run Web Server until exit requested (blocking call)
+ if err = ctx.webServer.Serve(); err != nil {
+ log.Println(err)
+ return 3, err
+ }
+
+ return 4, fmt.Errorf("Program exited")
+}
+
+// Helper function to log message on both stdout and logger
+func (ctx *Context) _logPrint(format string, args ...interface{}) {
+ fmt.Printf(format, args...)
+ if ctx.Log.Out != os.Stdout {
+ ctx.Log.Infof(format, args...)
+ }
+}
+
// Handle exit and properly stop/close all stuff
func handlerSigTerm(ctx *Context) {
<-ctx.Exit
@@ -68,9 +194,9 @@ func handlerSigTerm(ctx *Context) {
ctx.SThg.Stop()
ctx.SThg.StopInotify()
}
- if ctx.WWWServer != nil {
+ if ctx.webServer != nil {
ctx.Log.Infof("Stoping Web server...")
- ctx.WWWServer.Stop()
+ ctx.webServer.Stop()
}
os.Exit(1)
}
diff --git a/lib/agent/apiv1-browse.go b/lib/agent/apiv1-browse.go
new file mode 100644
index 0000000..1701a2e
--- /dev/null
+++ b/lib/agent/apiv1-browse.go
@@ -0,0 +1,28 @@
+package agent
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+)
+
+type directory struct {
+ Name string `json:"name"`
+ Fullpath string `json:"fullpath"`
+}
+
+type apiDirectory struct {
+ Dir []directory `json:"dir"`
+}
+
+// browseFS used to browse local file system
+func (s *APIService) browseFS(c *gin.Context) {
+
+ response := apiDirectory{
+ Dir: []directory{
+ directory{Name: "TODO SEB"},
+ },
+ }
+
+ c.JSON(http.StatusOK, response)
+}
diff --git a/lib/agent/apiv1-config.go b/lib/agent/apiv1-config.go
new file mode 100644
index 0000000..31d8de6
--- /dev/null
+++ b/lib/agent/apiv1-config.go
@@ -0,0 +1,108 @@
+package agent
+
+import (
+ "net/http"
+ "sync"
+
+ "github.com/gin-gonic/gin"
+ "github.com/iotbzh/xds-agent/lib/xdsconfig"
+ common "github.com/iotbzh/xds-common/golib"
+)
+
+var confMut sync.Mutex
+
+// APIConfig parameters (json format) of /config command
+type APIConfig struct {
+ Servers []ServerCfg `json:"servers"`
+
+ // Not exposed outside in JSON
+ Version string `json:"-"`
+ APIVersion string `json:"-"`
+ VersionGitTag string `json:"-"`
+}
+
+// ServerCfg .
+type ServerCfg struct {
+ ID string `json:"id"`
+ URL string `json:"url"`
+ APIURL string `json:"apiUrl"`
+ PartialURL string `json:"partialUrl"`
+ ConnRetry int `json:"connRetry"`
+ Connected bool `json:"connected"`
+ Disabled bool `json:"disabled"`
+}
+
+// GetConfig returns the configuration
+func (s *APIService) getConfig(c *gin.Context) {
+ confMut.Lock()
+ defer confMut.Unlock()
+
+ cfg := s._getConfig()
+
+ c.JSON(http.StatusOK, cfg)
+}
+
+// SetConfig sets configuration
+func (s *APIService) setConfig(c *gin.Context) {
+ var cfgArg APIConfig
+ if c.BindJSON(&cfgArg) != nil {
+ common.APIError(c, "Invalid arguments")
+ return
+ }
+
+ confMut.Lock()
+ defer confMut.Unlock()
+
+ s.Log.Debugln("SET config: ", cfgArg)
+
+ // First delete/disable XDS Server that are no longer listed
+ for _, svr := range s.xdsServers {
+ found := false
+ for _, svrArg := range cfgArg.Servers {
+ if svr.ID == svrArg.ID {
+ found = true
+ break
+ }
+ }
+ if !found {
+ s.DelXdsServer(svr.ID)
+ }
+ }
+
+ // Add new XDS Server
+ for _, svr := range cfgArg.Servers {
+ cfg := xdsconfig.XDSServerConf{
+ ID: svr.ID,
+ URL: svr.URL,
+ ConnRetry: svr.ConnRetry,
+ }
+ if _, err := s.AddXdsServer(cfg); err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+ }
+
+ c.JSON(http.StatusOK, s._getConfig())
+}
+
+func (s *APIService) _getConfig() APIConfig {
+ cfg := APIConfig{
+ Version: s.Config.Version,
+ APIVersion: s.Config.APIVersion,
+ VersionGitTag: s.Config.VersionGitTag,
+ Servers: []ServerCfg{},
+ }
+
+ for _, svr := range s.xdsServers {
+ cfg.Servers = append(cfg.Servers, ServerCfg{
+ ID: svr.ID,
+ URL: svr.BaseURL,
+ APIURL: svr.APIURL,
+ PartialURL: svr.PartialURL,
+ ConnRetry: svr.ConnRetry,
+ Connected: svr.Connected,
+ Disabled: svr.Disabled,
+ })
+ }
+ return cfg
+}
diff --git a/lib/agent/apiv1-events.go b/lib/agent/apiv1-events.go
new file mode 100644
index 0000000..8aad18a
--- /dev/null
+++ b/lib/agent/apiv1-events.go
@@ -0,0 +1,73 @@
+package agent
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ common "github.com/iotbzh/xds-common/golib"
+)
+
+// EventRegisterArgs is the parameters (json format) of /events/register command
+type EventRegisterArgs struct {
+ Name string `json:"name"`
+ ProjectID string `json:"filterProjectID"`
+}
+
+// EventUnRegisterArgs is the parameters (json format) of /events/unregister command
+type EventUnRegisterArgs struct {
+ Name string `json:"name"`
+ ID int `json:"id"`
+}
+
+// eventsList Registering for events that will be send over a WS
+func (s *APIService) eventsList(c *gin.Context) {
+ c.JSON(http.StatusOK, s.events.GetList())
+}
+
+// eventsRegister Registering for events that will be send over a WS
+func (s *APIService) eventsRegister(c *gin.Context) {
+ var args EventRegisterArgs
+
+ if c.BindJSON(&args) != nil || args.Name == "" {
+ common.APIError(c, "Invalid arguments")
+ return
+ }
+
+ sess := s.webServer.sessions.Get(c)
+ if sess == nil {
+ common.APIError(c, "Unknown sessions")
+ return
+ }
+
+ // Register to all or to a specific events
+ if err := s.events.Register(args.Name, sess.ID); err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"status": "OK"})
+}
+
+// eventsRegister Registering for events that will be send over a WS
+func (s *APIService) eventsUnRegister(c *gin.Context) {
+ var args EventUnRegisterArgs
+
+ if c.BindJSON(&args) != nil || args.Name == "" {
+ common.APIError(c, "Invalid arguments")
+ return
+ }
+
+ sess := s.webServer.sessions.Get(c)
+ if sess == nil {
+ common.APIError(c, "Unknown sessions")
+ return
+ }
+
+ // Register to all or to a specific events
+ if err := s.events.UnRegister(args.Name, sess.ID); err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"status": "OK"})
+}
diff --git a/lib/agent/apiv1-exec.go b/lib/agent/apiv1-exec.go
new file mode 100644
index 0000000..37070f7
--- /dev/null
+++ b/lib/agent/apiv1-exec.go
@@ -0,0 +1,131 @@
+package agent
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ common "github.com/iotbzh/xds-common/golib"
+ uuid "github.com/satori/go.uuid"
+)
+
+// ExecArgs Only define used fields
+type ExecArgs struct {
+ ID string `json:"id" binding:"required"`
+ CmdID string `json:"cmdID"` // command unique ID
+}
+
+var execCmdID = 1
+
+// ExecCmd executes remotely a command
+func (s *APIService) execCmd(c *gin.Context) {
+ s._execRequest("/exec", c)
+}
+
+// execSignalCmd executes remotely a command
+func (s *APIService) execSignalCmd(c *gin.Context) {
+ s._execRequest("/signal", c)
+}
+
+func (s *APIService) _execRequest(cmd string, c *gin.Context) {
+ data, err := c.GetRawData()
+ if err != nil {
+ common.APIError(c, err.Error())
+ }
+
+ args := ExecArgs{}
+ // XXX - we cannot use c.BindJSON, so directly unmarshall it
+ // (see https://github.com/gin-gonic/gin/issues/1078)
+ if err := json.Unmarshal(data, &args); err != nil {
+ common.APIError(c, "Invalid arguments")
+ return
+ }
+
+ // First get Project ID to retrieve Server ID and send command to right server
+ id := c.Param("id")
+ if id == "" {
+ id = args.ID
+ }
+
+ prj := s.projects.Get(id)
+ if prj == nil {
+ common.APIError(c, "Unknown id")
+ return
+ }
+
+ svr := (*prj).GetServer()
+ if svr == nil {
+ common.APIError(c, "Cannot identify XDS Server")
+ return
+ }
+
+ // Retrieve session info
+ sess := s.sessions.Get(c)
+ if sess == nil {
+ common.APIError(c, "Unknown sessions")
+ return
+ }
+ sock := sess.IOSocket
+ if sock == nil {
+ common.APIError(c, "Websocket not established")
+ return
+ }
+
+ // Forward XDSServer WS events to client WS
+ // TODO removed static event name list and get it from XDSServer
+ evtList := []string{
+ "exec:input",
+ "exec:output",
+ "exec:inferior-input",
+ "exec:inferior-output",
+ }
+ so := *sock
+ fwdFuncID := []uuid.UUID{}
+ for _, evName := range evtList {
+ evN := evName
+ fwdFunc := func(evData interface{}) {
+ // Forward event to Client/Dashboard
+ so.Emit(evN, evData)
+ }
+ id, err := svr.EventOn(evN, fwdFunc)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+ fwdFuncID = append(fwdFuncID, id)
+ }
+
+ // Handle Exit event separately to cleanup registered listener
+ var exitFuncID uuid.UUID
+ exitFunc := func(evData interface{}) {
+ so.Emit("exec:exit", evData)
+
+ // cleanup listener
+ for i, evName := range evtList {
+ svr.EventOff(evName, fwdFuncID[i])
+ }
+ svr.EventOff("exec:exit", exitFuncID)
+ }
+ exitFuncID, err = svr.EventOn("exec:exit", exitFunc)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+
+ // Forward back command to right server
+ response, err := svr.SendCommand(cmd, data)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+
+ // Decode response
+ body, err := ioutil.ReadAll(response.Body)
+ if err != nil {
+ common.APIError(c, "Cannot read response body")
+ return
+ }
+ c.JSON(http.StatusOK, string(body))
+
+}
diff --git a/lib/agent/apiv1-projects.go b/lib/agent/apiv1-projects.go
new file mode 100644
index 0000000..d4b5e74
--- /dev/null
+++ b/lib/agent/apiv1-projects.go
@@ -0,0 +1,72 @@
+package agent
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ common "github.com/iotbzh/xds-common/golib"
+)
+
+// getProjects returns all projects configuration
+func (s *APIService) getProjects(c *gin.Context) {
+ c.JSON(http.StatusOK, s.projects.GetProjectArr())
+}
+
+// getProject returns a specific project configuration
+func (s *APIService) getProject(c *gin.Context) {
+ prj := s.projects.Get(c.Param("id"))
+ if prj == nil {
+ common.APIError(c, "Invalid id")
+ return
+ }
+
+ c.JSON(http.StatusOK, (*prj).GetProject())
+}
+
+// addProject adds a new project to server config
+func (s *APIService) addProject(c *gin.Context) {
+ var cfgArg ProjectConfig
+ if c.BindJSON(&cfgArg) != nil {
+ common.APIError(c, "Invalid arguments")
+ return
+ }
+
+ s.Log.Debugln("Add project config: ", cfgArg)
+
+ newFld, err := s.projects.Add(cfgArg)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+
+ c.JSON(http.StatusOK, newFld)
+}
+
+// syncProject force synchronization of project files
+func (s *APIService) syncProject(c *gin.Context) {
+ id := c.Param("id")
+
+ s.Log.Debugln("Sync project id: ", id)
+
+ err := s.projects.ForceSync(id)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+
+ c.JSON(http.StatusOK, "")
+}
+
+// delProject deletes project from server config
+func (s *APIService) delProject(c *gin.Context) {
+ id := c.Param("id")
+
+ s.Log.Debugln("Delete project id ", id)
+
+ delEntry, err := s.projects.Delete(id)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+ c.JSON(http.StatusOK, delEntry)
+}
diff --git a/lib/agent/apiv1-version.go b/lib/agent/apiv1-version.go
new file mode 100644
index 0000000..6b4923f
--- /dev/null
+++ b/lib/agent/apiv1-version.go
@@ -0,0 +1,45 @@
+package agent
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ common "github.com/iotbzh/xds-common/golib"
+)
+
+type version struct {
+ ID string `json:"id"`
+ Version string `json:"version"`
+ APIVersion string `json:"apiVersion"`
+ VersionGitTag string `json:"gitTag"`
+}
+
+type apiVersion struct {
+ Client version `json:"client"`
+ Server []version `json:"servers"`
+}
+
+// getInfo : return various information about server
+func (s *APIService) getVersion(c *gin.Context) {
+ response := apiVersion{
+ Client: version{
+ ID: "",
+ Version: s.Config.Version,
+ APIVersion: s.Config.APIVersion,
+ VersionGitTag: s.Config.VersionGitTag,
+ },
+ }
+
+ svrVer := []version{}
+ for _, svr := range s.xdsServers {
+ res := version{}
+ if err := svr.GetVersion(&res); err != nil {
+ common.APIError(c, "Cannot retrieve version of XDS server ID %s : %v", svr.ID, err.Error())
+ return
+ }
+ svrVer = append(svrVer, res)
+ }
+ response.Server = svrVer
+
+ c.JSON(http.StatusOK, response)
+}
diff --git a/lib/agent/apiv1.go b/lib/agent/apiv1.go
new file mode 100644
index 0000000..77b05ba
--- /dev/null
+++ b/lib/agent/apiv1.go
@@ -0,0 +1,129 @@
+package agent
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+ "github.com/iotbzh/xds-agent/lib/xdsconfig"
+)
+
+const apiBaseUrl = "/api/v1"
+
+// APIService .
+type APIService struct {
+ *Context
+ apiRouter *gin.RouterGroup
+ serverIndex int
+}
+
+// NewAPIV1 creates a new instance of API service
+func NewAPIV1(ctx *Context) *APIService {
+ s := &APIService{
+ Context: ctx,
+ apiRouter: ctx.webServer.router.Group(apiBaseUrl),
+ serverIndex: 0,
+ }
+
+ s.apiRouter.GET("/version", s.getVersion)
+
+ s.apiRouter.GET("/config", s.getConfig)
+ s.apiRouter.POST("/config", s.setConfig)
+
+ s.apiRouter.GET("/browse", s.browseFS)
+
+ s.apiRouter.GET("/projects", s.getProjects)
+ s.apiRouter.GET("/project/:id", s.getProject)
+ s.apiRouter.POST("/project", s.addProject)
+ s.apiRouter.POST("/project/sync/:id", s.syncProject)
+ s.apiRouter.DELETE("/project/:id", s.delProject)
+
+ s.apiRouter.POST("/exec", s.execCmd)
+ s.apiRouter.POST("/exec/:id", s.execCmd)
+ s.apiRouter.POST("/signal", s.execSignalCmd)
+
+ s.apiRouter.GET("/events", s.eventsList)
+ s.apiRouter.POST("/events/register", s.eventsRegister)
+ s.apiRouter.POST("/events/unregister", s.eventsUnRegister)
+
+ return s
+}
+
+// Stop Used to stop/close created services
+func (s *APIService) Stop() {
+ for _, svr := range s.xdsServers {
+ svr.Close()
+ }
+}
+
+// AddXdsServer Add a new XDS Server to the list of a server
+func (s *APIService) AddXdsServer(cfg xdsconfig.XDSServerConf) (*XdsServer, error) {
+ var svr *XdsServer
+ var exist, tempoID bool
+ tempoID = false
+
+ // First check if not already exist and update it
+ if svr, exist = s.xdsServers[cfg.ID]; exist {
+
+ // Update: Found, so just update some settings
+ svr.ConnRetry = cfg.ConnRetry
+
+ tempoID = svr.IsTempoID()
+ if svr.Connected && !svr.Disabled && svr.BaseURL == cfg.URL && tempoID {
+ return svr, nil
+ }
+
+ // URL differ or not connected, so need to reconnect
+ svr.BaseURL = cfg.URL
+
+ } else {
+
+ // Create a new server object
+ if cfg.APIBaseURL == "" {
+ cfg.APIBaseURL = apiBaseUrl
+ }
+ if cfg.APIPartialURL == "" {
+ cfg.APIPartialURL = "/server/" + strconv.Itoa(s.serverIndex)
+ s.serverIndex = s.serverIndex + 1
+ }
+
+ // Create a new XDS Server
+ svr = NewXdsServer(s.Context, cfg)
+
+ svr.SetLoggerOutput(s.Config.LogVerboseOut)
+
+ // Passthrough routes (handle by XDS Server)
+ grp := s.apiRouter.Group(svr.PartialURL)
+ svr.SetAPIRouterGroup(grp)
+ svr.PassthroughGet("/sdks")
+ svr.PassthroughGet("/sdk/:id")
+ }
+
+ // Established connection
+ err := svr.Connect()
+
+ // Delete temporary ID with it has been replaced by right Server ID
+ if tempoID && !svr.IsTempoID() {
+ delete(s.xdsServers, cfg.ID)
+ }
+
+ // Add to map
+ s.xdsServers[svr.ID] = svr
+
+ // Load projects
+ if err == nil && svr.Connected {
+ err = s.projects.Init(svr)
+ }
+
+ return svr, err
+}
+
+// DelXdsServer Delete an XDS Server from the list of a server
+func (s *APIService) DelXdsServer(id string) error {
+ if _, exist := s.xdsServers[id]; !exist {
+ return fmt.Errorf("Unknown Server ID %s", id)
+ }
+ // Don't really delete, just disable it
+ s.xdsServers[id].Close()
+ return nil
+}
diff --git a/lib/agent/events.go b/lib/agent/events.go
new file mode 100644
index 0000000..e66f758
--- /dev/null
+++ b/lib/agent/events.go
@@ -0,0 +1,132 @@
+package agent
+
+import (
+ "fmt"
+ "time"
+)
+
+// Events constants
+const (
+ // EventTypePrefix Used as event prefix
+ EventTypePrefix = "event:" // following by event type
+
+ // Supported Events type
+ EVTAll = "all"
+ EVTServerConfig = "server-config" // data type ServerCfg
+ EVTProjectAdd = "project-add" // data type ProjectConfig
+ EVTProjectDelete = "project-delete" // data type ProjectConfig
+ EVTProjectChange = "project-state-change" // data type ProjectConfig
+)
+
+var _EVTAllList = []string{
+ EVTServerConfig,
+ EVTProjectAdd,
+ EVTProjectDelete,
+ EVTProjectChange,
+}
+
+// EventMsg Message send
+type EventMsg struct {
+ Time string `json:"time"`
+ Type string `json:"type"`
+ Data interface{} `json:"data"`
+}
+
+type EventDef struct {
+ sids map[string]int
+}
+
+type Events struct {
+ *Context
+ eventsMap map[string]*EventDef
+}
+
+// NewEvents creates an instance of Events
+func NewEvents(ctx *Context) *Events {
+ evMap := make(map[string]*EventDef)
+ for _, ev := range _EVTAllList {
+ evMap[ev] = &EventDef{
+ sids: make(map[string]int),
+ }
+ }
+ return &Events{
+ Context: ctx,
+ eventsMap: evMap,
+ }
+}
+
+// GetList returns the list of all supported events
+func (e *Events) GetList() []string {
+ return _EVTAllList
+}
+
+// Register Used by a client/session to register to a specific (or all) event(s)
+func (e *Events) Register(evName, sessionID string) error {
+ evs := _EVTAllList
+ if evName != EVTAll {
+ if _, ok := e.eventsMap[evName]; !ok {
+ return fmt.Errorf("Unsupported event type name")
+ }
+ evs = []string{evName}
+ }
+ for _, ev := range evs {
+ e.eventsMap[ev].sids[sessionID]++
+ }
+ return nil
+}
+
+// UnRegister Used by a client/session to unregister event(s)
+func (e *Events) UnRegister(evName, sessionID string) error {
+ evs := _EVTAllList
+ if evName != EVTAll {
+ if _, ok := e.eventsMap[evName]; !ok {
+ return fmt.Errorf("Unsupported event type name")
+ }
+ evs = []string{evName}
+ }
+ for _, ev := range evs {
+ if _, exist := e.eventsMap[ev].sids[sessionID]; exist {
+ delete(e.eventsMap[ev].sids, sessionID)
+ break
+ }
+ }
+ return nil
+}
+
+// Emit Used to manually emit an event
+func (e *Events) Emit(evName string, data interface{}) error {
+ var firstErr error
+
+ if _, ok := e.eventsMap[evName]; !ok {
+ return fmt.Errorf("Unsupported event type")
+ }
+
+ if e.LogLevelSilly {
+ e.Log.Debugf("Emit Event %s: %v", evName, data)
+ }
+
+ firstErr = nil
+ evm := e.eventsMap[evName]
+ for sid := range evm.sids {
+ so := e.webServer.sessions.IOSocketGet(sid)
+ if so == nil {
+ if firstErr == nil {
+ firstErr = fmt.Errorf("IOSocketGet return nil")
+ }
+ continue
+ }
+ msg := EventMsg{
+ Time: time.Now().String(),
+ Type: evName,
+ Data: data,
+ }
+ if err := (*so).Emit(EventTypePrefix+evName, msg); err != nil {
+ e.Log.Errorf("WS Emit %v error : %v", EventTypePrefix+evName, err)
+ if firstErr == nil {
+ firstErr = err
+ }
+ }
+ }
+
+ return firstErr
+}
diff --git a/lib/agent/project-interface.go b/lib/agent/project-interface.go
new file mode 100644
index 0000000..0a4a17e
--- /dev/null
+++ b/lib/agent/project-interface.go
@@ -0,0 +1,43 @@
+package agent
+
+// ProjectType definition
+type ProjectType string
+
+const (
+ TypePathMap = "PathMap"
+ TypeCloudSync = "CloudSync"
+ TypeCifsSmb = "CIFS"
+)
+
+// Project Status definition
+const (
+ StatusErrorConfig = "ErrorConfig"
+ StatusDisable = "Disable"
+ StatusEnable = "Enable"
+ StatusPause = "Pause"
+ StatusSyncing = "Syncing"
+)
+
+// IPROJECT Project interface
+type IPROJECT interface {
+ Add(cfg ProjectConfig) (*ProjectConfig, error) // Add a new project
+ Delete() error // Delete a project
+ GetProject() *ProjectConfig // Get project public configuration
+ UpdateProject(prj ProjectConfig) (*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
+}
+
+// ProjectConfig is the config for one project
+type ProjectConfig struct {
+ ID string `json:"id"`
+ ServerID string `json:"serverId"`
+ Label string `json:"label"`
+ ClientPath string `json:"clientPath"`
+ ServerPath string `json:"serverPath"`
+ Type ProjectType `json:"type"`
+ Status string `json:"status"`
+ IsInSync bool `json:"isInSync"`
+ DefaultSdk string `json:"defaultSdk"`
+}
diff --git a/lib/agent/project-pathmap.go b/lib/agent/project-pathmap.go
new file mode 100644
index 0000000..aacbd1f
--- /dev/null
+++ b/lib/agent/project-pathmap.go
@@ -0,0 +1,123 @@
+package agent
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "strings"
+
+ common "github.com/iotbzh/xds-common/golib"
+)
+
+// IPROJECT interface implementation for native/path mapping projects
+
+// PathMap .
+type PathMap struct {
+ *Context
+ server *XdsServer
+ folder *XdsFolderConfig
+}
+
+// NewProjectPathMap Create a new instance of PathMap
+func NewProjectPathMap(ctx *Context, svr *XdsServer) *PathMap {
+ p := PathMap{
+ Context: ctx,
+ server: svr,
+ folder: &XdsFolderConfig{},
+ }
+ return &p
+}
+
+// Add a new project
+func (p *PathMap) Add(cfg ProjectConfig) (*ProjectConfig, error) {
+ var err error
+ var file *os.File
+ errMsg := "ClientPath sanity check error (%d): %v"
+
+ // Sanity check to verify that we have RW permission and path-mapping is correct
+ dir := cfg.ClientPath
+ if !common.Exists(dir) {
+ // try to create if not existing
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return nil, fmt.Errorf("Cannot create ClientPath directory: %s", dir)
+ }
+ }
+ if !common.Exists(dir) {
+ return nil, fmt.Errorf("ClientPath directory is not accessible: %s", dir)
+ }
+ if file, err = ioutil.TempFile(dir, ".xds_pathmap_check"); err != nil {
+ return nil, fmt.Errorf(errMsg, 1, err)
+ }
+ // Write a specific message that will be check by server during folder add
+ msg := "Pathmap checked message written by xds-agent ID: " + p.Config.AgentUID + "\n"
+ if n, err := file.WriteString(msg); n != len(msg) || err != nil {
+ return nil, fmt.Errorf(errMsg, 2, err)
+ }
+ defer func() {
+ if file != nil {
+ os.Remove(file.Name())
+ file.Close()
+ }
+ }()
+
+ // Convert to Xds folder
+ fld := p.server.ProjectToFolder(cfg)
+ fld.DataPathMap.CheckFile = file.Name()
+ fld.DataPathMap.CheckContent = msg
+
+ // 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)
+ }
+
+ // 2nd part of sanity checker
+ // check specific message added by XDS Server during folder add processing
+ content, err := ioutil.ReadFile(file.Name())
+ if err != nil {
+ return nil, fmt.Errorf(errMsg, 3, err)
+ }
+ if !strings.Contains(string(content),
+ "Pathmap checked message written by xds-server ID") {
+ return nil, fmt.Errorf(errMsg, 4, "file content differ")
+ }
+
+ return p.GetProject(), nil
+}
+
+// Delete a project
+func (p *PathMap) Delete() error {
+ return p.server.FolderDelete(p.folder.ID)
+}
+
+// GetProject Get public part of project config
+func (p *PathMap) GetProject() *ProjectConfig {
+ prj := p.server.FolderToProject(*p.folder)
+ prj.ServerID = p.server.ID
+ return &prj
+}
+
+// UpdateProject Set project config
+func (p *PathMap) UpdateProject(prj ProjectConfig) (*ProjectConfig, error) {
+ p.folder = p.server.ProjectToFolder(prj)
+ np := p.GetProject()
+ if err := p.events.Emit(EVTProjectChange, np); err != nil {
+ return np, err
+ }
+ return np, nil
+}
+
+// GetServer Get the XdsServer that holds this project
+func (p *PathMap) GetServer() *XdsServer {
+ return p.server
+}
+
+// Sync Force project files synchronization
+func (p *PathMap) Sync() error {
+ return nil
+}
+
+// IsInSync Check if project files are in-sync
+func (p *PathMap) IsInSync() (bool, error) {
+ return true, nil
+}
diff --git a/lib/agent/project-st.go b/lib/agent/project-st.go
new file mode 100644
index 0000000..c0d2550
--- /dev/null
+++ b/lib/agent/project-st.go
@@ -0,0 +1,186 @@
+package agent
+
+import (
+ st "github.com/iotbzh/xds-agent/lib/syncthing"
+)
+
+// IPROJECT interface implementation for syncthing projects
+
+// STProject .
+type STProject struct {
+ *Context
+ server *XdsServer
+ folder *XdsFolderConfig
+ eventIDs []int
+}
+
+// NewProjectST Create a new instance of STProject
+func NewProjectST(ctx *Context, svr *XdsServer) *STProject {
+ p := STProject{
+ Context: ctx,
+ server: svr,
+ folder: &XdsFolderConfig{},
+ }
+ return &p
+}
+
+// Add a new project
+func (p *STProject) Add(cfg ProjectConfig) (*ProjectConfig, error) {
+ var err error
+
+ // Add project/folder into XDS Server
+ err = p.server.FolderAdd(p.server.ProjectToFolder(cfg), p.folder)
+ if err != nil {
+ return nil, err
+ }
+ svrPrj := p.GetProject()
+
+ // Declare project into local Syncthing
+ id, err := p.SThg.FolderChange(st.FolderChangeArg{
+ ID: svrPrj.ID,
+ Label: svrPrj.Label,
+ RelativePath: cfg.ClientPath,
+ SyncThingID: p.server.ServerConfig.Builder.SyncThingID,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ locPrj, err := p.SThg.FolderConfigGet(id)
+ if err != nil {
+ svrPrj.Status = StatusErrorConfig
+ return nil, err
+ }
+ if svrPrj.ID != locPrj.ID {
+ 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)
+}
+
+// Delete a project
+func (p *STProject) Delete() error {
+ errSvr := p.server.FolderDelete(p.folder.ID)
+ errLoc := p.SThg.FolderDelete(p.folder.ID)
+ if errSvr != nil {
+ return errSvr
+ }
+ return errLoc
+}
+
+// GetProject Get public part of project config
+func (p *STProject) GetProject() *ProjectConfig {
+ prj := p.server.FolderToProject(*p.folder)
+ prj.ServerID = p.server.ID
+ return &prj
+}
+
+// UpdateProject Update project config
+func (p *STProject) UpdateProject(prj ProjectConfig) (*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.Log.Warningf("XDS Server EventRegister failed: %v", err)
+ return svrPrj, err
+ }
+
+ // Register to Local Syncthing events
+ for _, evName := range []string{st.EventStateChanged, st.EventFolderPaused} {
+ evID, err := p.SThg.Events.Register(evName, p._cbLocalSTEvents, svrPrj.ID, nil)
+ if err != nil {
+ return nil, err
+ }
+ p.eventIDs = append(p.eventIDs, evID)
+ }
+
+ return svrPrj, nil
+}
+
+// GetServer Get the XdsServer that holds this project
+func (p *STProject) GetServer() *XdsServer {
+ return p.server
+}
+
+// Sync Force project files synchronization
+func (p *STProject) Sync() error {
+ if err := p.server.FolderSync(p.folder.ID); err != nil {
+ return err
+ }
+ return p.SThg.FolderScan(p.folder.ID, "")
+}
+
+// IsInSync Check if project files are in-sync
+func (p *STProject) IsInSync() (bool, error) {
+ // Should be up-to-date by callbacks (see below)
+ return p.folder.IsInSync, nil
+}
+
+/**
+** Private functions
+***/
+
+// callback use to update (XDS Server) folder IsInSync status
+
+func (p *STProject) _cbServerFolderChanged(data interface{}) {
+ evt := data.(XdsEventFolderChange)
+
+ // Only process event that concerns this project/folder ID
+ if p.folder.ID != evt.Folder.ID {
+ return
+ }
+
+ if evt.Folder.IsInSync != p.folder.DataCloudSync.STSvrIsInSync ||
+ evt.Folder.Status != p.folder.DataCloudSync.STSvrStatus {
+
+ p.folder.DataCloudSync.STSvrIsInSync = evt.Folder.IsInSync
+ p.folder.DataCloudSync.STSvrStatus = evt.Folder.Status
+
+ if err := p.events.Emit(EVTProjectChange, p.server.FolderToProject(*p.folder)); err != nil {
+ p.Log.Warningf("Cannot notify project change: %v", err)
+ }
+ }
+}
+
+// callback use to update IsInSync status
+func (p *STProject) _cbLocalSTEvents(ev st.Event, data *st.EventsCBData) {
+
+ inSync := p.folder.DataCloudSync.STLocIsInSync
+ sts := p.folder.DataCloudSync.STLocStatus
+ prevSync := inSync
+ prevStatus := sts
+
+ switch ev.Type {
+
+ case st.EventStateChanged:
+ to := ev.Data["to"]
+ switch to {
+ case "scanning", "syncing":
+ sts = StatusSyncing
+ case "idle":
+ sts = StatusEnable
+ }
+ inSync = (to == "idle")
+
+ case st.EventFolderPaused:
+ if sts == StatusEnable {
+ sts = StatusPause
+ }
+ inSync = false
+ }
+
+ if prevSync != inSync || prevStatus != sts {
+
+ p.folder.DataCloudSync.STLocIsInSync = inSync
+ p.folder.DataCloudSync.STLocStatus = sts
+
+ if err := p.events.Emit(EVTProjectChange, p.server.FolderToProject(*p.folder)); err != nil {
+ p.Log.Warningf("Cannot notify project change: %v", err)
+ }
+ }
+}
diff --git a/lib/agent/projects.go b/lib/agent/projects.go
new file mode 100644
index 0000000..5e20395
--- /dev/null
+++ b/lib/agent/projects.go
@@ -0,0 +1,241 @@
+package agent
+
+import (
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/iotbzh/xds-agent/lib/syncthing"
+ "github.com/syncthing/syncthing/lib/sync"
+)
+
+// Projects Represent a an XDS Projects
+type Projects struct {
+ *Context
+ SThg *st.SyncThing
+ projects map[string]*IPROJECT
+}
+
+// Mutex to make add/delete atomic
+var pjMutex = sync.NewMutex()
+
+// NewProjects Create a new instance of Project Model
+func NewProjects(ctx *Context, st *st.SyncThing) *Projects {
+ return &Projects{
+ Context: ctx,
+ SThg: st,
+ projects: make(map[string]*IPROJECT),
+ }
+}
+
+// Init Load Projects configuration
+func (p *Projects) Init(server *XdsServer) error {
+ svrList := make(map[string]*XdsServer)
+ // If server not set, load for all servers
+ if server == nil {
+ svrList = p.xdsServers
+ } else {
+ svrList[server.ID] = server
+ }
+ errMsg := ""
+ for _, svr := range svrList {
+ if svr.Disabled {
+ continue
+ }
+ xFlds := []XdsFolderConfig{}
+ if err := svr.GetFolders(&xFlds); err != nil {
+ errMsg += fmt.Sprintf("Cannot retrieve folders config of XDS server ID %s : %v \n", svr.ID, err.Error())
+ continue
+ }
+ p.Log.Debugf("Connected to XDS Server %s, %d projects detected", svr.ID, len(xFlds))
+ for _, prj := range xFlds {
+ newP := svr.FolderToProject(prj)
+ if _, err := p.createUpdate(newP, false, true); err != nil {
+ errMsg += "Error while creating project id " + prj.ID + ": " + err.Error() + "\n "
+ continue
+ }
+ }
+ }
+
+ p.Log.Infof("Number of loaded Projects: %d", len(p.projects))
+
+ if errMsg != "" {
+ return fmt.Errorf(errMsg)
+ }
+ return nil
+}
+
+// Get returns the folder config or nil if not existing
+func (p *Projects) Get(id string) *IPROJECT {
+ if id == "" {
+ return nil
+ }
+ fc, exist := p.projects[id]
+ if !exist {
+ return nil
+ }
+ return fc
+}
+
+// GetProjectArr returns the config of all folders as an array
+func (p *Projects) GetProjectArr() []ProjectConfig {
+ pjMutex.Lock()
+ defer pjMutex.Unlock()
+
+ return p.GetProjectArrUnsafe()
+}
+
+// GetProjectArrUnsafe Same as GetProjectArr without mutex protection
+func (p *Projects) GetProjectArrUnsafe() []ProjectConfig {
+ conf := []ProjectConfig{}
+ for _, v := range p.projects {
+ prj := (*v).GetProject()
+ conf = append(conf, *prj)
+ }
+ return conf
+}
+
+// Add adds a new folder
+func (p *Projects) Add(newF ProjectConfig) (*ProjectConfig, error) {
+ prj, err := p.createUpdate(newF, true, false)
+ if err != nil {
+ return prj, err
+ }
+
+ // Notify client with event
+ if err := p.events.Emit(EVTProjectAdd, *prj); err != nil {
+ p.Log.Warningf("Cannot notify project deletion: %v", err)
+ }
+
+ return prj, err
+}
+
+// CreateUpdate creates or update a folder
+func (p *Projects) createUpdate(newF ProjectConfig, create bool, initial bool) (*ProjectConfig, error) {
+ var err error
+
+ pjMutex.Lock()
+ defer pjMutex.Unlock()
+
+ // Sanity check
+ if _, exist := p.projects[newF.ID]; create && exist {
+ return nil, fmt.Errorf("ID already exists")
+ }
+ if newF.ClientPath == "" {
+ return nil, fmt.Errorf("ClientPath must be set")
+ }
+ if newF.ServerID == "" {
+ return nil, fmt.Errorf("Server ID must be set")
+ }
+ var svr *XdsServer
+ var exist bool
+ if svr, exist = p.xdsServers[newF.ServerID]; !exist {
+ return nil, fmt.Errorf("Unknown Server ID %s", newF.ServerID)
+ }
+
+ // Check type supported
+ b, exist := svr.ServerConfig.SupportedSharing[string(newF.Type)]
+ if !exist || !b {
+ return nil, fmt.Errorf("Server doesn't support project type %s", newF.Type)
+ }
+
+ // Create a new folder object
+ var fld IPROJECT
+ switch newF.Type {
+ // SYNCTHING
+ case TypeCloudSync:
+ if p.SThg != nil {
+ fld = NewProjectST(p.Context, svr)
+ } else {
+ return nil, fmt.Errorf("Cloud Sync project not supported")
+ }
+
+ // PATH MAP
+ case TypePathMap:
+ fld = NewProjectPathMap(p.Context, svr)
+ default:
+ return nil, fmt.Errorf("Unsupported folder type")
+ }
+
+ var newPrj *ProjectConfig
+ if create {
+ // Add project on server
+ if newPrj, err = fld.Add(newF); err != nil {
+ newF.Status = StatusErrorConfig
+ log.Printf("ERROR Adding project: %v\n", err)
+ return newPrj, err
+ }
+ } else {
+ // Just update project config
+ if newPrj, err = fld.UpdateProject(newF); err != nil {
+ newF.Status = StatusErrorConfig
+ log.Printf("ERROR Updating project: %v\n", err)
+ return newPrj, err
+ }
+ }
+
+ // Sanity check
+ if newPrj.ID == "" {
+ log.Printf("ERROR project ID empty: %v", newF)
+ return newPrj, fmt.Errorf("Project ID empty")
+ }
+
+ // Add to folders list
+ p.projects[newPrj.ID] = &fld
+
+ // Force sync after creation
+ // (need to defer to be sure that WS events will arrive after HTTP creation reply)
+ go func() {
+ time.Sleep(time.Millisecond * 500)
+ fld.Sync()
+ }()
+
+ return newPrj, nil
+}
+
+// Delete deletes a specific folder
+func (p *Projects) Delete(id string) (ProjectConfig, error) {
+ var err error
+
+ pjMutex.Lock()
+ defer pjMutex.Unlock()
+
+ fld := ProjectConfig{}
+ fc, exist := p.projects[id]
+ if !exist {
+ return fld, fmt.Errorf("unknown id")
+ }
+
+ prj := (*fc).GetProject()
+
+ if err = (*fc).Delete(); err != nil {
+ return *prj, err
+ }
+
+ delete(p.projects, id)
+
+ // Notify client with event
+ if err := p.events.Emit(EVTProjectDelete, *prj); err != nil {
+ p.Log.Warningf("Cannot notify project deletion: %v", err)
+ }
+
+ return *prj, err
+}
+
+// ForceSync Force the synchronization of a folder
+func (p *Projects) ForceSync(id string) error {
+ fc := p.Get(id)
+ if fc == nil {
+ return fmt.Errorf("Unknown id")
+ }
+ return (*fc).Sync()
+}
+
+// IsProjectInSync Returns true when folder is in sync
+func (p *Projects) IsProjectInSync(id string) (bool, error) {
+ fc := p.Get(id)
+ if fc == nil {
+ return false, fmt.Errorf("Unknown id")
+ }
+ return (*fc).IsInSync()
+}
diff --git a/lib/session/session.go b/lib/agent/session.go
index b56f9ff..06789d5 100644
--- a/lib/session/session.go
+++ b/lib/agent/session.go
@@ -1,11 +1,10 @@
-package session
+package agent
import (
"encoding/base64"
"strconv"
"time"
- "github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin"
"github.com/googollee/go-socket.io"
uuid "github.com/satori/go.uuid"
@@ -36,29 +35,27 @@ type ClientSession struct {
// Sessions holds client sessions
type Sessions struct {
- router *gin.Engine
+ *Context
cookieMaxAge int64
sessMap map[string]ClientSession
mutex sync.Mutex
- log *logrus.Logger
stop chan struct{} // signals intentional stop
}
// NewClientSessions .
-func NewClientSessions(router *gin.Engine, log *logrus.Logger, cookieMaxAge string) *Sessions {
+func NewClientSessions(ctx *Context, cookieMaxAge string) *Sessions {
ckMaxAge, err := strconv.ParseInt(cookieMaxAge, 10, 0)
if err != nil {
ckMaxAge = 0
}
s := Sessions{
- router: router,
+ Context: ctx,
cookieMaxAge: ckMaxAge,
sessMap: make(map[string]ClientSession),
mutex: sync.NewMutex(),
- log: log,
stop: make(chan struct{}),
}
- s.router.Use(s.Middleware())
+ s.webServer.router.Use(s.Middleware())
// Start monitoring of sessions Map (use to manage expiration and cleanup)
go s.monitorSessMap()
@@ -174,7 +171,7 @@ func (s *Sessions) newSession(prefix string) *ClientSession {
s.sessMap[se.ID] = se
- s.log.Debugf("NEW session (%d): %s", len(s.sessMap), id)
+ s.Log.Debugf("NEW session (%d): %s", len(s.sessMap), id)
return &se
}
@@ -197,27 +194,27 @@ func (s *Sessions) refresh(sid string) {
}
func (s *Sessions) monitorSessMap() {
- const dbgFullTrace = false // for debugging
-
for {
select {
case <-s.stop:
- s.log.Debugln("Stop monitorSessMap")
+ s.Log.Debugln("Stop monitorSessMap")
return
case <-time.After(sessionMonitorTime * time.Second):
- if dbgFullTrace {
- s.log.Debugf("Sessions Map size: %d", len(s.sessMap))
- s.log.Debugf("Sessions Map : %v", s.sessMap)
+ if s.LogLevelSilly {
+ s.Log.Debugf("Sessions Map size: %d", len(s.sessMap))
+ s.Log.Debugf("Sessions Map : %v", s.sessMap)
}
if len(s.sessMap) > maxSessions {
- s.log.Errorln("TOO MUCH sessions, cleanup old ones !")
+ s.Log.Errorln("TOO MUCH sessions, cleanup old ones !")
}
s.mutex.Lock()
for _, ss := range s.sessMap {
if ss.expireAt.Sub(time.Now()) < 0 {
- s.log.Debugf("Delete expired session id: %s", ss.ID)
+ if s.LogLevelSilly {
+ s.Log.Debugf("Delete expired session id: %s", ss.ID)
+ }
delete(s.sessMap, ss.ID)
}
}
diff --git a/lib/agent/webserver.go b/lib/agent/webserver.go
new file mode 100644
index 0000000..4b2e024
--- /dev/null
+++ b/lib/agent/webserver.go
@@ -0,0 +1,244 @@
+package agent
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "path"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/gin-contrib/static"
+ "github.com/gin-gonic/gin"
+ "github.com/googollee/go-socket.io"
+)
+
+// WebServer .
+type WebServer struct {
+ *Context
+ router *gin.Engine
+ api *APIService
+ sIOServer *socketio.Server
+ webApp *gin.RouterGroup
+ stop chan struct{} // signals intentional stop
+}
+
+const indexFilename = "index.html"
+
+// NewWebServer creates an instance of WebServer
+func NewWebServer(ctx *Context) *WebServer {
+
+ // Setup logging for gin router
+ if ctx.Log.Level == logrus.DebugLevel {
+ gin.SetMode(gin.DebugMode)
+ } else {
+ gin.SetMode(gin.ReleaseMode)
+ }
+
+ // Redirect gin logs into another logger (LogVerboseOut may be stderr or a file)
+ gin.DefaultWriter = ctx.Config.LogVerboseOut
+ gin.DefaultErrorWriter = ctx.Config.LogVerboseOut
+ log.SetOutput(ctx.Config.LogVerboseOut)
+
+ // Creates gin router
+ r := gin.New()
+
+ svr := &WebServer{
+ Context: ctx,
+ router: r,
+ api: nil,
+ sIOServer: nil,
+ webApp: nil,
+ stop: make(chan struct{}),
+ }
+
+ return svr
+}
+
+// Serve starts a new instance of the Web Server
+func (s *WebServer) Serve() error {
+ var err error
+
+ // Setup middlewares
+ s.router.Use(gin.Logger())
+ s.router.Use(gin.Recovery())
+ s.router.Use(s.middlewareCORS())
+ s.router.Use(s.middlewareXDSDetails())
+ s.router.Use(s.middlewareCSRF())
+
+ // Create REST API
+ s.api = NewAPIV1(s.Context)
+
+ // Create connections to XDS Servers
+ // XXX - not sure there is no side effect to do it in background !
+ go func() {
+ for _, svrCfg := range s.Config.FileConf.ServersConf {
+ if svr, err := s.api.AddXdsServer(svrCfg); err != nil {
+ // Just log error, don't consider as critical
+ s.Log.Infof("Cannot connect to XDS Server url=%s: %v", svr.BaseURL, err.Error())
+ }
+ }
+ }()
+
+ // Websocket routes
+ s.sIOServer, err = socketio.NewServer(nil)
+ if err != nil {
+ s.Log.Fatalln(err)
+ }
+
+ s.router.GET("/socket.io/", s.socketHandler)
+ s.router.POST("/socket.io/", s.socketHandler)
+ /* TODO: do we want to support ws://... ?
+ s.router.Handle("WS", "/socket.io/", s.socketHandler)
+ s.router.Handle("WSS", "/socket.io/", s.socketHandler)
+ */
+
+ // Web Application (serve on / )
+ idxFile := path.Join(s.Config.FileConf.WebAppDir, indexFilename)
+ if _, err := os.Stat(idxFile); err != nil {
+ s.Log.Fatalln("Web app directory not found, check/use webAppDir setting in config file: ", idxFile)
+ }
+ s.Log.Infof("Serve WEB app dir: %s", s.Config.FileConf.WebAppDir)
+ s.router.Use(static.Serve("/", static.LocalFile(s.Config.FileConf.WebAppDir, true)))
+ s.webApp = s.router.Group("/", s.serveIndexFile)
+ {
+ s.webApp.GET("/")
+ }
+
+ // Serve in the background
+ serveError := make(chan error, 1)
+ go func() {
+ fmt.Printf("Web Server running on localhost:%s ...\n", s.Config.FileConf.HTTPPort)
+ serveError <- http.ListenAndServe(":"+s.Config.FileConf.HTTPPort, s.router)
+ }()
+
+ fmt.Printf("XDS agent running...\n")
+
+ // Wait for stop, restart or error signals
+ select {
+ case <-s.stop:
+ // Shutting down permanently
+ s.sessions.Stop()
+ s.Log.Infoln("shutting down (stop)")
+ case err = <-serveError:
+ // Error due to listen/serve failure
+ s.Log.Errorln(err)
+ }
+
+ return nil
+}
+
+// Stop web server
+func (s *WebServer) Stop() {
+ s.api.Stop()
+ close(s.stop)
+}
+
+// serveIndexFile provides initial file (eg. index.html) of webapp
+func (s *WebServer) serveIndexFile(c *gin.Context) {
+ c.HTML(200, indexFilename, gin.H{})
+}
+
+// Add details in Header
+func (s *WebServer) middlewareXDSDetails() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Header("XDS-Agent-Version", s.Config.Version)
+ c.Header("XDS-API-Version", s.Config.APIVersion)
+ c.Next()
+ }
+}
+
+func (s *WebServer) isValidAPIKey(key string) bool {
+ return (s.Config.FileConf.XDSAPIKey != "" && key == s.Config.FileConf.XDSAPIKey)
+}
+
+func (s *WebServer) middlewareCSRF() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ // XXX - not used for now
+ c.Next()
+ return
+ /*
+ // Allow requests carrying a valid API key
+ if s.isValidAPIKey(c.Request.Header.Get("X-API-Key")) {
+ // Set the access-control-allow-origin header for CORS requests
+ // since a valid API key has been provided
+ c.Header("Access-Control-Allow-Origin", "*")
+ c.Next()
+ return
+ }
+
+ // Allow io.socket request
+ if strings.HasPrefix(c.Request.URL.Path, "/socket.io") {
+ c.Next()
+ return
+ }
+
+ // FIXME Add really CSRF support
+
+ // Allow requests for anything not under the protected path prefix,
+ // and set a CSRF cookie if there isn't already a valid one.
+ //if !strings.HasPrefix(c.Request.URL.Path, prefix) {
+ // cookie, err := c.Cookie("CSRF-Token-" + unique)
+ // if err != nil || !validCsrfToken(cookie.Value) {
+ // s.Log.Debugln("new CSRF cookie in response to request for", c.Request.URL)
+ // c.SetCookie("CSRF-Token-"+unique, newCsrfToken(), 600, "/", "", false, false)
+ // }
+ // c.Next()
+ // return
+ //}
+
+ // Verify the CSRF token
+ //token := c.Request.Header.Get("X-CSRF-Token-" + unique)
+ //if !validCsrfToken(token) {
+ // c.AbortWithError(403, "CSRF Error")
+ // return
+ //}
+
+ //c.Next()
+
+ c.AbortWithError(403, fmt.Errorf("Not valid API key"))
+ */
+ }
+}
+
+// CORS middleware
+func (s *WebServer) middlewareCORS() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ if c.Request.Method == "OPTIONS" {
+ c.Header("Access-Control-Allow-Origin", "*")
+ c.Header("Access-Control-Allow-Headers", "Content-Type, X-API-Key")
+ c.Header("Access-Control-Allow-Methods", "GET, POST, DELETE")
+ c.Header("Access-Control-Max-Age", cookieMaxAge)
+ c.AbortWithStatus(204)
+ return
+ }
+ c.Next()
+ }
+}
+
+// socketHandler is the handler for the "main" websocket connection
+func (s *WebServer) socketHandler(c *gin.Context) {
+
+ // Retrieve user session
+ sess := s.sessions.Get(c)
+ if sess == nil {
+ c.JSON(500, gin.H{"error": "Cannot retrieve session"})
+ return
+ }
+
+ s.sIOServer.On("connection", func(so socketio.Socket) {
+ s.Log.Debugf("WS Connected (SID=%v)", so.Id())
+ s.sessions.UpdateIOSocket(sess.ID, &so)
+
+ so.On("disconnection", func() {
+ s.Log.Debugf("WS disconnected (SID=%v)", so.Id())
+ s.sessions.UpdateIOSocket(sess.ID, nil)
+ })
+ })
+
+ s.sIOServer.On("error", func(so socketio.Socket, err error) {
+ s.Log.Errorf("WS SID=%v Error : %v", so.Id(), err.Error())
+ })
+
+ s.sIOServer.ServeHTTP(c.Writer, c.Request)
+}
diff --git a/lib/agent/xdsserver.go b/lib/agent/xdsserver.go
new file mode 100644
index 0000000..b76908c
--- /dev/null
+++ b/lib/agent/xdsserver.go
@@ -0,0 +1,605 @@
+package agent
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/iotbzh/xds-agent/lib/xdsconfig"
+ common "github.com/iotbzh/xds-common/golib"
+ uuid "github.com/satori/go.uuid"
+ sio_client "github.com/sebd71/go-socket.io-client"
+)
+
+// XdsServer .
+type XdsServer struct {
+ *Context
+ ID string
+ BaseURL string
+ APIURL string
+ PartialURL string
+ ConnRetry int
+ Connected bool
+ Disabled bool
+ ServerConfig *XdsServerConfig
+
+ // Events management
+ CBOnError func(error)
+ CBOnDisconnect func(error)
+ sockEvents map[string][]*caller
+ sockEventsLock *sync.Mutex
+
+ // Private fields
+ client *common.HTTPClient
+ ioSock *sio_client.Client
+ logOut io.Writer
+ apiRouter *gin.RouterGroup
+}
+
+// XdsServerConfig Data return by GET /config
+type XdsServerConfig struct {
+ ID string `json:"id"`
+ Version string `json:"version"`
+ APIVersion string `json:"apiVersion"`
+ VersionGitTag string `json:"gitTag"`
+ SupportedSharing map[string]bool `json:"supportedSharing"`
+ Builder XdsBuilderConfig `json:"builder"`
+}
+
+// XdsBuilderConfig represents the builder container configuration
+type XdsBuilderConfig struct {
+ IP string `json:"ip"`
+ Port string `json:"port"`
+ SyncThingID string `json:"syncThingID"`
+}
+
+// XdsFolderType XdsServer folder type
+type XdsFolderType string
+
+const (
+ XdsTypePathMap = "PathMap"
+ XdsTypeCloudSync = "CloudSync"
+ XdsTypeCifsSmb = "CIFS"
+)
+
+// XdsFolderConfig XdsServer folder config
+type XdsFolderConfig struct {
+ ID string `json:"id"`
+ Label string `json:"label"`
+ ClientPath string `json:"path"`
+ Type XdsFolderType `json:"type"`
+ Status string `json:"status"`
+ IsInSync bool `json:"isInSync"`
+ DefaultSdk string `json:"defaultSdk"`
+ // Specific data depending on which Type is used
+ DataPathMap XdsPathMapConfig `json:"dataPathMap,omitempty"`
+ DataCloudSync XdsCloudSyncConfig `json:"dataCloudSync,omitempty"`
+}
+
+// XdsPathMapConfig Path mapping specific data
+type XdsPathMapConfig struct {
+ ServerPath string `json:"serverPath"`
+ CheckFile string `json:"checkFile"`
+ CheckContent string `json:"checkContent"`
+}
+
+// XdsCloudSyncConfig CloudSync (AKA Syncthing) specific data
+type XdsCloudSyncConfig struct {
+ SyncThingID string `json:"syncThingID"`
+ STSvrStatus string `json:"-"`
+ STSvrIsInSync bool `json:"-"`
+ STLocStatus string `json:"-"`
+ STLocIsInSync bool `json:"-"`
+}
+
+// XdsEventRegisterArgs arguments used to register to XDS server events
+type XdsEventRegisterArgs struct {
+ Name string `json:"name"`
+ ProjectID string `json:"filterProjectID"`
+}
+
+// XdsEventFolderChange Folder change event structure
+type XdsEventFolderChange struct {
+ Time string `json:"time"`
+ Type string `json:"type"`
+ Folder XdsFolderConfig `json:"folder"`
+}
+
+// caller Used to chain event listeners
+type caller struct {
+ id uuid.UUID
+ EventName string
+ Func func(interface{})
+}
+
+const _IDTempoPrefix = "tempo-"
+
+// NewXdsServer creates an instance of XdsServer
+func NewXdsServer(ctx *Context, conf xdsconfig.XDSServerConf) *XdsServer {
+ return &XdsServer{
+ Context: ctx,
+ ID: _IDTempoPrefix + uuid.NewV1().String(),
+ BaseURL: conf.URL,
+ APIURL: conf.APIBaseURL + conf.APIPartialURL,
+ PartialURL: conf.APIPartialURL,
+ ConnRetry: conf.ConnRetry,
+ Connected: false,
+ Disabled: false,
+
+ sockEvents: make(map[string][]*caller),
+ sockEventsLock: &sync.Mutex{},
+ logOut: ctx.Log.Out,
+ }
+}
+
+// Close Free and close XDS Server connection
+func (xs *XdsServer) Close() error {
+ xs.Connected = false
+ xs.Disabled = true
+ xs.ioSock = nil
+ xs._NotifyState()
+ return nil
+}
+
+// Connect Establish HTTP connection with XDS Server
+func (xs *XdsServer) Connect() error {
+ var err error
+ var retry int
+
+ xs.Disabled = false
+ xs.Connected = false
+
+ err = nil
+ for retry = xs.ConnRetry; retry > 0; retry-- {
+ if err = xs._CreateConnectHTTP(); err == nil {
+ break
+ }
+ if retry == xs.ConnRetry {
+ // Notify only on the first conn error
+ // doing that avoid 2 notifs (conn false; conn true) on startup
+ xs._NotifyState()
+ }
+ xs.Log.Infof("Establishing connection to XDS Server (retry %d/%d)", retry, xs.ConnRetry)
+ time.Sleep(time.Second)
+ }
+ if retry == 0 {
+ // FIXME: re-use _reconnect to wait longer in background
+ return fmt.Errorf("Connection to XDS Server failure")
+ }
+ if err != nil {
+ return err
+ }
+
+ // Check HTTP connection and establish WS connection
+ err = xs._connect(false)
+
+ return err
+}
+
+// IsTempoID returns true when server as a temporary id
+func (xs *XdsServer) IsTempoID() bool {
+ return strings.HasPrefix(xs.ID, _IDTempoPrefix)
+}
+
+// SetLoggerOutput Set logger ou
+func (xs *XdsServer) SetLoggerOutput(out io.Writer) {
+ xs.logOut = out
+}
+
+// SendCommand Send a command to XDS Server
+func (xs *XdsServer) SendCommand(cmd string, body []byte) (*http.Response, error) {
+ url := cmd
+ if !strings.HasPrefix("/", cmd) {
+ url = "/" + cmd
+ }
+ return xs.client.HTTPPostWithRes(url, string(body))
+}
+
+// GetVersion Send Get request to retrieve XDS Server version
+func (xs *XdsServer) GetVersion(res interface{}) error {
+ return xs._HTTPGet("/version", &res)
+}
+
+// GetFolders Send GET request to get current folder configuration
+func (xs *XdsServer) GetFolders(folders *[]XdsFolderConfig) error {
+ return xs._HTTPGet("/folders", folders)
+}
+
+// FolderAdd Send POST request to add a folder
+func (xs *XdsServer) FolderAdd(fld *XdsFolderConfig, res interface{}) error {
+ response, err := xs._HTTPPost("/folder", fld)
+ if err != nil {
+ return err
+ }
+ if response.StatusCode != 200 {
+ return fmt.Errorf("FolderAdd error status=%s", response.Status)
+ }
+ // Result is a XdsFolderConfig that is equivalent to ProjectConfig
+ err = json.Unmarshal(xs.client.ResponseToBArray(response), res)
+
+ return err
+}
+
+// FolderDelete Send DELETE request to delete a folder
+func (xs *XdsServer) FolderDelete(id string) error {
+ return xs.client.HTTPDelete("/folder/" + id)
+}
+
+// FolderSync Send POST request to force synchronization of a folder
+func (xs *XdsServer) FolderSync(id string) error {
+ return xs.client.HTTPPost("/folder/sync/"+id, "")
+}
+
+// SetAPIRouterGroup .
+func (xs *XdsServer) SetAPIRouterGroup(r *gin.RouterGroup) {
+ xs.apiRouter = r
+}
+
+// PassthroughGet Used to declare a route that sends directly a GET request to XDS Server
+func (xs *XdsServer) PassthroughGet(url string) {
+ if xs.apiRouter == nil {
+ xs.Log.Errorf("apiRouter not set !")
+ return
+ }
+
+ xs.apiRouter.GET(url, func(c *gin.Context) {
+ var data interface{}
+ if err := xs._HTTPGet(url, &data); err != nil {
+ if strings.Contains(err.Error(), "connection refused") {
+ xs.Connected = false
+ xs._NotifyState()
+ }
+ common.APIError(c, err.Error())
+ return
+ }
+
+ c.JSON(http.StatusOK, data)
+ })
+}
+
+// PassthroughPost Used to declare a route that sends directly a POST request to XDS Server
+func (xs *XdsServer) PassthroughPost(url string) {
+ if xs.apiRouter == nil {
+ xs.Log.Errorf("apiRouter not set !")
+ return
+ }
+
+ xs.apiRouter.POST(url, func(c *gin.Context) {
+ bodyReq := []byte{}
+ n, err := c.Request.Body.Read(bodyReq)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+
+ response, err := xs._HTTPPost(url, bodyReq[:n])
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+ bodyRes, err := ioutil.ReadAll(response.Body)
+ if err != nil {
+ common.APIError(c, "Cannot read response body")
+ return
+ }
+ c.JSON(http.StatusOK, string(bodyRes))
+ })
+}
+
+// EventRegister Post a request to register to an XdsServer event
+func (xs *XdsServer) EventRegister(evName string, id string) error {
+ var err error
+ _, err = xs._HTTPPost("/events/register", XdsEventRegisterArgs{
+ Name: evName,
+ ProjectID: id,
+ })
+ return err
+}
+
+// EventOn Register a callback on events reception
+func (xs *XdsServer) EventOn(evName string, f func(interface{})) (uuid.UUID, error) {
+ if xs.ioSock == nil {
+ return uuid.Nil, fmt.Errorf("Io.Socket not initialized")
+ }
+
+ xs.sockEventsLock.Lock()
+ defer xs.sockEventsLock.Unlock()
+
+ if _, exist := xs.sockEvents[evName]; !exist {
+ // Register listener only the first time
+ evn := evName
+
+ // FIXME: use generic type: data interface{} instead of data XdsEventFolderChange
+ var err error
+ if evName == "event:FolderStateChanged" {
+ err = xs.ioSock.On(evn, func(data XdsEventFolderChange) error {
+ xs.sockEventsLock.Lock()
+ defer xs.sockEventsLock.Unlock()
+ for _, c := range xs.sockEvents[evn] {
+ c.Func(data)
+ }
+ return nil
+ })
+ } else {
+ err = xs.ioSock.On(evn, f)
+ }
+ if err != nil {
+ return uuid.Nil, err
+ }
+ }
+
+ c := &caller{
+ id: uuid.NewV1(),
+ EventName: evName,
+ Func: f,
+ }
+
+ xs.sockEvents[evName] = append(xs.sockEvents[evName], c)
+ return c.id, nil
+}
+
+// EventOff Un-register a (or all) callbacks associated to an event
+func (xs *XdsServer) EventOff(evName string, id uuid.UUID) error {
+ xs.sockEventsLock.Lock()
+ defer xs.sockEventsLock.Unlock()
+ if _, exist := xs.sockEvents[evName]; exist {
+ if id == uuid.Nil {
+ // Un-register all
+ xs.sockEvents[evName] = []*caller{}
+ } else {
+ // Un-register only the specified callback
+ for i, ff := range xs.sockEvents[evName] {
+ if uuid.Equal(ff.id, id) {
+ xs.sockEvents[evName] = append(xs.sockEvents[evName][:i], xs.sockEvents[evName][i+1:]...)
+ break
+ }
+ }
+ }
+ }
+ return nil
+}
+
+// ProjectToFolder Convert Project structure to Folder structure
+func (xs *XdsServer) ProjectToFolder(pPrj ProjectConfig) *XdsFolderConfig {
+ stID := ""
+ if pPrj.Type == XdsTypeCloudSync {
+ stID, _ = xs.SThg.IDGet()
+ }
+ fPrj := XdsFolderConfig{
+ ID: pPrj.ID,
+ Label: pPrj.Label,
+ ClientPath: pPrj.ClientPath,
+ Type: XdsFolderType(pPrj.Type),
+ Status: pPrj.Status,
+ IsInSync: pPrj.IsInSync,
+ DefaultSdk: pPrj.DefaultSdk,
+ DataPathMap: XdsPathMapConfig{
+ ServerPath: pPrj.ServerPath,
+ },
+ DataCloudSync: XdsCloudSyncConfig{
+ SyncThingID: stID,
+ STLocIsInSync: pPrj.IsInSync,
+ STLocStatus: pPrj.Status,
+ STSvrIsInSync: pPrj.IsInSync,
+ STSvrStatus: pPrj.Status,
+ },
+ }
+
+ return &fPrj
+}
+
+// FolderToProject Convert Folder structure to Project structure
+func (xs *XdsServer) FolderToProject(fPrj XdsFolderConfig) ProjectConfig {
+ inSync := fPrj.IsInSync
+ sts := fPrj.Status
+
+ if fPrj.Type == XdsTypeCloudSync {
+ inSync = fPrj.DataCloudSync.STSvrIsInSync && fPrj.DataCloudSync.STLocIsInSync
+
+ sts = fPrj.DataCloudSync.STSvrStatus
+ switch fPrj.DataCloudSync.STLocStatus {
+ case StatusErrorConfig, StatusDisable, StatusPause:
+ sts = fPrj.DataCloudSync.STLocStatus
+ break
+ case StatusSyncing:
+ if sts != StatusErrorConfig && sts != StatusDisable && sts != StatusPause {
+ sts = StatusSyncing
+ }
+ break
+ case StatusEnable:
+ // keep STSvrStatus
+ break
+ }
+ }
+
+ pPrj := ProjectConfig{
+ ID: fPrj.ID,
+ ServerID: xs.ID,
+ Label: fPrj.Label,
+ ClientPath: fPrj.ClientPath,
+ ServerPath: fPrj.DataPathMap.ServerPath,
+ Type: ProjectType(fPrj.Type),
+ Status: sts,
+ IsInSync: inSync,
+ DefaultSdk: fPrj.DefaultSdk,
+ }
+ return pPrj
+}
+
+/***
+** Private functions
+***/
+
+// Create HTTP client
+func (xs *XdsServer) _CreateConnectHTTP() error {
+ var err error
+ xs.client, err = common.HTTPNewClient(xs.BaseURL,
+ common.HTTPClientConfig{
+ URLPrefix: "/api/v1",
+ HeaderClientKeyName: "Xds-Sid",
+ CsrfDisable: true,
+ LogOut: xs.logOut,
+ LogPrefix: "XDSSERVER: ",
+ LogLevel: common.HTTPLogLevelWarning,
+ })
+
+ xs.client.SetLogLevel(xs.Log.Level.String())
+
+ if err != nil {
+ msg := ": " + err.Error()
+ if strings.Contains(err.Error(), "connection refused") {
+ msg = fmt.Sprintf("(url: %s)", xs.BaseURL)
+ }
+ return fmt.Errorf("ERROR: cannot connect to XDS Server %s", msg)
+ }
+ if xs.client == nil {
+ return fmt.Errorf("ERROR: cannot connect to XDS Server (null client)")
+ }
+
+ return nil
+}
+
+// _HTTPGet .
+func (xs *XdsServer) _HTTPGet(url string, data interface{}) error {
+ var dd []byte
+ if err := xs.client.HTTPGet(url, &dd); err != nil {
+ return err
+ }
+ return json.Unmarshal(dd, &data)
+}
+
+// _HTTPPost .
+func (xs *XdsServer) _HTTPPost(url string, data interface{}) (*http.Response, error) {
+ body, err := json.Marshal(data)
+ if err != nil {
+ return nil, err
+ }
+ return xs.client.HTTPPostWithRes(url, string(body))
+}
+
+// Re-established connection
+func (xs *XdsServer) _reconnect() error {
+ err := xs._connect(true)
+ if err == nil {
+ // Reload projects list for this server
+ err = xs.projects.Init(xs)
+ }
+ return err
+}
+
+// Established HTTP and WS connection and retrieve XDSServer config
+func (xs *XdsServer) _connect(reConn bool) error {
+
+ xdsCfg := XdsServerConfig{}
+ if err := xs._HTTPGet("/config", &xdsCfg); err != nil {
+ xs.Connected = false
+ if !reConn {
+ xs._NotifyState()
+ }
+ return err
+ }
+
+ if reConn && xs.ID != xdsCfg.ID {
+ xs.Log.Warningf("Reconnected to server but ID differs: old=%s, new=%s", xs.ID, xdsCfg.ID)
+ }
+
+ // Update local XDS config
+ xs.ID = xdsCfg.ID
+ xs.ServerConfig = &xdsCfg
+
+ // Establish WS connection and register listen
+ if err := xs._SocketConnect(); err != nil {
+ xs.Connected = false
+ xs._NotifyState()
+ return err
+ }
+
+ xs.Connected = true
+ xs._NotifyState()
+ return nil
+}
+
+// Create WebSocket (io.socket) connection
+func (xs *XdsServer) _SocketConnect() error {
+
+ xs.Log.Infof("Connecting IO.socket for server %s (url %s)", xs.ID, xs.BaseURL)
+
+ opts := &sio_client.Options{
+ Transport: "websocket",
+ Header: make(map[string][]string),
+ }
+ opts.Header["XDS-SID"] = []string{xs.client.GetClientID()}
+
+ iosk, err := sio_client.NewClient(xs.BaseURL, opts)
+ if err != nil {
+ return fmt.Errorf("IO.socket connection error for server %s: %v", xs.ID, err)
+ }
+ xs.ioSock = iosk
+
+ // Register some listeners
+
+ iosk.On("error", func(err error) {
+ xs.Log.Infof("IO.socket Error server %s; err: %v", xs.ID, err)
+ if xs.CBOnError != nil {
+ xs.CBOnError(err)
+ }
+ })
+
+ iosk.On("disconnection", func(err error) {
+ xs.Log.Infof("IO.socket disconnection server %s", xs.ID)
+ if xs.CBOnDisconnect != nil {
+ xs.CBOnDisconnect(err)
+ }
+ xs.Connected = false
+ xs._NotifyState()
+
+ // Try to reconnect during 15min (or at least while not disabled)
+ go func() {
+ count := 0
+ waitTime := 1
+ for !xs.Disabled && !xs.Connected {
+ count++
+ if count%60 == 0 {
+ waitTime *= 5
+ }
+ if waitTime > 15*60 {
+ xs.Log.Infof("Stop reconnection to server url=%s id=%s !", xs.BaseURL, xs.ID)
+ return
+ }
+ time.Sleep(time.Second * time.Duration(waitTime))
+ xs.Log.Infof("Try to reconnect to server %s (%d)", xs.BaseURL, count)
+
+ xs._reconnect()
+ }
+ }()
+ })
+
+ // XXX - There is no connection event generated so, just consider that
+ // we are connected when NewClient return successfully
+ /* iosk.On("connection", func() { ... }) */
+ xs.Log.Infof("IO.socket connected server url=%s id=%s", xs.BaseURL, xs.ID)
+
+ return nil
+}
+
+// Send event to notify changes
+func (xs *XdsServer) _NotifyState() {
+
+ evSts := ServerCfg{
+ ID: xs.ID,
+ URL: xs.BaseURL,
+ APIURL: xs.APIURL,
+ PartialURL: xs.PartialURL,
+ ConnRetry: xs.ConnRetry,
+ Connected: xs.Connected,
+ }
+ if err := xs.events.Emit(EVTServerConfig, evSts); err != nil {
+ xs.Log.Warningf("Cannot notify XdsServer state change: %v", err)
+ }
+}
diff --git a/lib/apiv1/apiv1.go b/lib/apiv1/apiv1.go
deleted file mode 100644
index 734929b..0000000
--- a/lib/apiv1/apiv1.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package apiv1
-
-import (
- "github.com/Sirupsen/logrus"
- "github.com/gin-gonic/gin"
-
- "github.com/iotbzh/xds-agent/lib/session"
- "github.com/iotbzh/xds-agent/lib/xdsconfig"
-)
-
-// APIService .
-type APIService struct {
- router *gin.Engine
- apiRouter *gin.RouterGroup
- sessions *session.Sessions
- cfg *xdsconfig.Config
- log *logrus.Logger
-}
-
-// New creates a new instance of API service
-func New(sess *session.Sessions, conf *xdsconfig.Config, log *logrus.Logger, r *gin.Engine) *APIService {
- s := &APIService{
- router: r,
- sessions: sess,
- apiRouter: r.Group("/api/v1"),
- cfg: conf,
- log: log,
- }
-
- s.apiRouter.GET("/version", s.getVersion)
-
- s.apiRouter.GET("/config", s.getConfig)
- s.apiRouter.POST("/config", s.setConfig)
-
- return s
-}
diff --git a/lib/apiv1/config.go b/lib/apiv1/config.go
deleted file mode 100644
index 47155ed..0000000
--- a/lib/apiv1/config.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package apiv1
-
-import (
- "net/http"
- "sync"
-
- "github.com/gin-gonic/gin"
- "github.com/iotbzh/xds-agent/lib/xdsconfig"
- common "github.com/iotbzh/xds-common/golib"
-)
-
-var confMut sync.Mutex
-
-// GetConfig returns the configuration
-func (s *APIService) getConfig(c *gin.Context) {
- confMut.Lock()
- defer confMut.Unlock()
-
- c.JSON(http.StatusOK, s.cfg)
-}
-
-// SetConfig sets configuration
-func (s *APIService) setConfig(c *gin.Context) {
- // FIXME - must be tested
- c.JSON(http.StatusNotImplemented, "Not implemented")
-
- var cfgArg xdsconfig.Config
-
- if c.BindJSON(&cfgArg) != nil {
- common.APIError(c, "Invalid arguments")
- return
- }
-
- confMut.Lock()
- defer confMut.Unlock()
-
- s.log.Debugln("SET config: ", cfgArg)
-
- if err := s.cfg.UpdateAll(cfgArg); err != nil {
- common.APIError(c, err.Error())
- return
- }
-
- c.JSON(http.StatusOK, s.cfg)
-}
diff --git a/lib/apiv1/version.go b/lib/apiv1/version.go
deleted file mode 100644
index e022441..0000000
--- a/lib/apiv1/version.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package apiv1
-
-import (
- "net/http"
-
- "github.com/gin-gonic/gin"
-)
-
-type version struct {
- Version string `json:"version"`
- APIVersion string `json:"apiVersion"`
- VersionGitTag string `json:"gitTag"`
-}
-
-// getInfo : return various information about server
-func (s *APIService) getVersion(c *gin.Context) {
- response := version{
- Version: s.cfg.Version,
- APIVersion: s.cfg.APIVersion,
- VersionGitTag: s.cfg.VersionGitTag,
- }
-
- c.JSON(http.StatusOK, response)
-}
diff --git a/lib/syncthing/st.go b/lib/syncthing/st.go
index 6199a8f..031a2ac 100644
--- a/lib/syncthing/st.go
+++ b/lib/syncthing/st.go
@@ -24,11 +24,14 @@ import (
// SyncThing .
type SyncThing struct {
- BaseURL string
- APIKey string
- Home string
- STCmd *exec.Cmd
- STICmd *exec.Cmd
+ BaseURL string
+ APIKey string
+ Home string
+ STCmd *exec.Cmd
+ STICmd *exec.Cmd
+ MyID string
+ Connected bool
+ Events *Events
// Private fields
binDir string
@@ -37,6 +40,7 @@ type SyncThing struct {
exitSTIChan chan ExitChan
client *common.HTTPClient
log *logrus.Logger
+ conf *xdsconfig.Config
}
// ExitChan Channel used for process exit
@@ -45,6 +49,42 @@ type ExitChan struct {
err error
}
+// ConfigInSync Check whether if Syncthing configuration is in sync
+type configInSync struct {
+ ConfigInSync bool `json:"configInSync"`
+}
+
+// FolderStatus Information about the current status of a folder.
+type FolderStatus struct {
+ GlobalFiles int `json:"globalFiles"`
+ GlobalDirectories int `json:"globalDirectories"`
+ GlobalSymlinks int `json:"globalSymlinks"`
+ GlobalDeleted int `json:"globalDeleted"`
+ GlobalBytes int64 `json:"globalBytes"`
+
+ LocalFiles int `json:"localFiles"`
+ LocalDirectories int `json:"localDirectories"`
+ LocalSymlinks int `json:"localSymlinks"`
+ LocalDeleted int `json:"localDeleted"`
+ LocalBytes int64 `json:"localBytes"`
+
+ NeedFiles int `json:"needFiles"`
+ NeedDirectories int `json:"needDirectories"`
+ NeedSymlinks int `json:"needSymlinks"`
+ NeedDeletes int `json:"needDeletes"`
+ NeedBytes int64 `json:"needBytes"`
+
+ InSyncFiles int `json:"inSyncFiles"`
+ InSyncBytes int64 `json:"inSyncBytes"`
+
+ State string `json:"state"`
+ StateChanged time.Time `json:"stateChanged"`
+
+ Sequence int64 `json:"sequence"`
+
+ IgnorePatterns bool `json:"ignorePatterns"`
+}
+
// NewSyncThing creates a new instance of Syncthing
func NewSyncThing(conf *xdsconfig.Config, log *logrus.Logger) *SyncThing {
var url, apiKey, home, binDir string
@@ -75,8 +115,12 @@ func NewSyncThing(conf *xdsconfig.Config, log *logrus.Logger) *SyncThing {
binDir: binDir,
logsDir: conf.FileConf.LogsDir,
log: log,
+ conf: conf,
}
+ // Create Events monitoring
+ s.Events = s.NewEventListener()
+
return &s
}
@@ -86,8 +130,9 @@ func (s *SyncThing) startProc(exeName string, args []string, env []string, eChan
var exePath string
// Kill existing process (useful for debug ;-) )
- if os.Getenv("DEBUG_MODE") != "" {
- exec.Command("bash", "-c", "pkill -9 "+exeName).Output()
+ if _, dbg := os.LookupEnv("XDS_DEBUG_MODE"); dbg {
+ fmt.Printf("\n!!! DEBUG_MODE set: KILL existing %s process(es) !!!\n", exeName)
+ exec.Command("bash", "-c", "ps -ax |grep "+exeName+" |grep "+s.BaseURL+" |cut -d' ' -f 1|xargs -I{} kill -9 {}").Output()
}
// When not set (or set to '.') set bin to path of xds-agent executable
@@ -182,6 +227,8 @@ func (s *SyncThing) Start() (*exec.Cmd, error) {
"STNOUPGRADE=1",
}
+ /* FIXME - STILL NEEDED ?, if not SUP code
+
// XXX - temporary hack because -gui-apikey seems to correctly handle by
// syncthing the early first time
stConfigFile := filepath.Join(s.Home, "config.xml")
@@ -211,12 +258,12 @@ func (s *SyncThing) Start() (*exec.Cmd, error) {
return nil, fmt.Errorf("Cannot write Syncthing config file to set apikey")
}
}
-
+ */
s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
// Use autogenerated apikey if not set by config.json
- if s.APIKey == "" {
- if fd, err := os.Open(stConfigFile); err == nil {
+ if err == nil && s.APIKey == "" {
+ if fd, err := os.Open(filepath.Join(s.Home, "config.xml")); err == nil {
defer fd.Close()
if b, err := ioutil.ReadAll(fd); err == nil {
re := regexp.MustCompile("<apikey>(.*)</apikey>")
@@ -294,11 +341,17 @@ func (s *SyncThing) StopInotify() {
// Connect Establish HTTP connection with Syncthing
func (s *SyncThing) Connect() error {
var err error
+ s.Connected = false
s.client, err = common.HTTPNewClient(s.BaseURL,
common.HTTPClientConfig{
URLPrefix: "/rest",
HeaderClientKeyName: "X-Syncthing-ID",
+ LogOut: s.conf.LogVerboseOut,
+ LogPrefix: "SYNCTHING: ",
+ LogLevel: common.HTTPLogLevelWarning,
})
+ s.client.SetLogLevel(s.log.Level.String())
+
if err != nil {
msg := ": " + err.Error()
if strings.Contains(err.Error(), "connection refused") {
@@ -310,11 +363,17 @@ func (s *SyncThing) Connect() error {
return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
}
- s.client.SetLogLevel(s.log.Level.String())
- s.client.LoggerPrefix = "SYNCTHING: "
- s.client.LoggerOut = s.log.Out
+ s.MyID, err = s.IDGet()
+ if err != nil {
+ return fmt.Errorf("ERROR: cannot retrieve ID")
+ }
+
+ s.Connected = true
- return nil
+ // Start events monitoring
+ err = s.Events.Start()
+
+ return err
}
// IDGet returns the Syncthing ID of Syncthing instance running locally
@@ -347,3 +406,16 @@ func (s *SyncThing) ConfigSet(cfg config.Configuration) error {
}
return s.client.HTTPPost("system/config", string(body))
}
+
+// IsConfigInSync Returns true if configuration is in sync
+func (s *SyncThing) IsConfigInSync() (bool, error) {
+ var data []byte
+ var d configInSync
+ if err := s.client.HTTPGet("system/config/insync", &data); err != nil {
+ return false, err
+ }
+ if err := json.Unmarshal(data, &d); err != nil {
+ return false, err
+ }
+ return d.ConfigInSync, nil
+}
diff --git a/lib/syncthing/stEvent.go b/lib/syncthing/stEvent.go
new file mode 100644
index 0000000..0017555
--- /dev/null
+++ b/lib/syncthing/stEvent.go
@@ -0,0 +1,265 @@
+package st
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/Sirupsen/logrus"
+)
+
+// Events .
+type Events struct {
+ MonitorTime time.Duration
+ Debug bool
+
+ stop chan bool
+ st *SyncThing
+ log *logrus.Logger
+ cbArr map[string][]cbMap
+}
+
+type Event struct {
+ Type string `json:"type"`
+ Time time.Time `json:"time"`
+ Data map[string]string `json:"data"`
+}
+
+type EventsCBData map[string]interface{}
+type EventsCB func(ev Event, cbData *EventsCBData)
+
+const (
+ EventFolderCompletion string = "FolderCompletion"
+ EventFolderSummary string = "FolderSummary"
+ EventFolderPaused string = "FolderPaused"
+ EventFolderResumed string = "FolderResumed"
+ EventFolderErrors string = "FolderErrors"
+ EventStateChanged string = "StateChanged"
+)
+
+var EventsAll string = EventFolderCompletion + "|" +
+ EventFolderSummary + "|" +
+ EventFolderPaused + "|" +
+ EventFolderResumed + "|" +
+ EventFolderErrors + "|" +
+ EventStateChanged
+
+type STEvent struct {
+ // Per-subscription sequential event ID. Named "id" for backwards compatibility with the REST API
+ SubscriptionID int `json:"id"`
+ // Global ID of the event across all subscriptions
+ GlobalID int `json:"globalID"`
+ Time time.Time `json:"time"`
+ Type string `json:"type"`
+ Data map[string]interface{} `json:"data"`
+}
+
+type cbMap struct {
+ id int
+ cb EventsCB
+ filterID string
+ data *EventsCBData
+}
+
+// NewEventListener Create a new instance of Event listener
+func (s *SyncThing) NewEventListener() *Events {
+ _, dbg := os.LookupEnv("XDS_LOG_SILLY_STEVENTS") // set to add more debug log
+ return &Events{
+ MonitorTime: 100, // in Milliseconds
+ Debug: dbg,
+ stop: make(chan bool, 1),
+ st: s,
+ log: s.log,
+ cbArr: make(map[string][]cbMap),
+ }
+}
+
+// Start starts event monitoring loop
+func (e *Events) Start() error {
+ go e.monitorLoop()
+ return nil
+}
+
+// Stop stops event monitoring loop
+func (e *Events) Stop() {
+ e.stop <- true
+}
+
+// Register Add a listener on an event
+func (e *Events) Register(evName string, cb EventsCB, filterID string, data *EventsCBData) (int, error) {
+ if evName == "" || !strings.Contains(EventsAll, evName) {
+ return -1, fmt.Errorf("Unknown event name")
+ }
+ if data == nil {
+ data = &EventsCBData{}
+ }
+
+ cbList := []cbMap{}
+ if _, ok := e.cbArr[evName]; ok {
+ cbList = e.cbArr[evName]
+ }
+
+ id := len(cbList)
+ (*data)["id"] = strconv.Itoa(id)
+
+ e.cbArr[evName] = append(cbList, cbMap{id: id, cb: cb, filterID: filterID, data: data})
+
+ return id, nil
+}
+
+// UnRegister Remove a listener event
+func (e *Events) UnRegister(evName string, id int) error {
+ cbKey, ok := e.cbArr[evName]
+ if !ok {
+ return fmt.Errorf("No event registered to such name")
+ }
+
+ // FIXME - NOT TESTED
+ if id >= len(cbKey) {
+ return fmt.Errorf("Invalid id")
+ } else if id == len(cbKey) {
+ e.cbArr[evName] = cbKey[:id-1]
+ } else {
+ e.cbArr[evName] = cbKey[id : id+1]
+ }
+
+ return nil
+}
+
+// GetEvents returns the Syncthing events
+func (e *Events) getEvents(since int) ([]STEvent, error) {
+ var data []byte
+ ev := []STEvent{}
+ url := "events"
+ if since != -1 {
+ url += "?since=" + strconv.Itoa(since)
+ }
+ if err := e.st.client.HTTPGet(url, &data); err != nil {
+ return ev, err
+ }
+ err := json.Unmarshal(data, &ev)
+ return ev, err
+}
+
+// Loop to monitor Syncthing events
+func (e *Events) monitorLoop() {
+ e.log.Infof("Event monitoring running...")
+ since := 0
+ cntErrConn := 0
+ cntErrRetry := 1
+ for {
+ select {
+ case <-e.stop:
+ e.log.Infof("Event monitoring exited")
+ return
+
+ case <-time.After(e.MonitorTime * time.Millisecond):
+
+ if !e.st.Connected {
+ cntErrConn++
+ time.Sleep(time.Second)
+ if cntErrConn > cntErrRetry {
+ e.log.Error("ST Event monitor: ST connection down")
+ cntErrConn = 0
+ cntErrRetry *= 2
+ if _, err := e.getEvents(since); err == nil {
+ e.st.Connected = true
+ cntErrRetry = 1
+ // XXX - should we reset since value ?
+ goto readEvent
+ }
+ }
+ continue
+ }
+
+ readEvent:
+ stEvArr, err := e.getEvents(since)
+ if err != nil {
+ e.log.Errorf("Syncthing Get Events: %v", err)
+ e.st.Connected = false
+ continue
+ }
+
+ // Process events
+ for _, stEv := range stEvArr {
+ since = stEv.SubscriptionID
+ if e.Debug {
+ e.log.Warnf("ST EVENT: %d %s\n %v", stEv.GlobalID, stEv.Type, stEv)
+ }
+
+ cbKey, ok := e.cbArr[stEv.Type]
+ if !ok {
+ continue
+ }
+
+ evData := Event{
+ Type: stEv.Type,
+ Time: stEv.Time,
+ }
+
+ // Decode Events
+ // FIXME: re-define data struct for each events
+ // instead of map of string and use JSON marshing/unmarshing
+ fID := ""
+ evData.Data = make(map[string]string)
+ switch stEv.Type {
+
+ case EventFolderCompletion:
+ fID = convString(stEv.Data["folder"])
+ evData.Data["completion"] = convFloat64(stEv.Data["completion"])
+
+ case EventFolderSummary:
+ fID = convString(stEv.Data["folder"])
+ evData.Data["needBytes"] = convInt64(stEv.Data["needBytes"])
+ evData.Data["state"] = convString(stEv.Data["state"])
+
+ case EventFolderPaused, EventFolderResumed:
+ fID = convString(stEv.Data["id"])
+ evData.Data["label"] = convString(stEv.Data["label"])
+
+ case EventFolderErrors:
+ fID = convString(stEv.Data["folder"])
+ // TODO decode array evData.Data["errors"] = convString(stEv.Data["errors"])
+
+ case EventStateChanged:
+ fID = convString(stEv.Data["folder"])
+ evData.Data["from"] = convString(stEv.Data["from"])
+ evData.Data["to"] = convString(stEv.Data["to"])
+
+ default:
+ e.log.Warnf("Unsupported event type")
+ }
+
+ if fID != "" {
+ evData.Data["id"] = fID
+ }
+
+ // Call all registered callbacks
+ for _, c := range cbKey {
+ if e.Debug {
+ e.log.Warnf("EVENT CB fID=%s, filterID=%s", fID, c.filterID)
+ }
+ // Call when filterID is not set or when it matches
+ if c.filterID == "" || (fID != "" && fID == c.filterID) {
+ c.cb(evData, c.data)
+ }
+ }
+ }
+ }
+ }
+}
+
+func convString(d interface{}) string {
+ return d.(string)
+}
+
+func convFloat64(d interface{}) string {
+ return strconv.FormatFloat(d.(float64), 'f', -1, 64)
+}
+
+func convInt64(d interface{}) string {
+ return strconv.FormatInt(d.(int64), 10)
+}
diff --git a/lib/syncthing/stfolder.go b/lib/syncthing/stfolder.go
index d79e579..196e3c7 100644
--- a/lib/syncthing/stfolder.go
+++ b/lib/syncthing/stfolder.go
@@ -1,42 +1,82 @@
package st
import (
- "path/filepath"
+ "encoding/json"
+ "fmt"
"strings"
- "github.com/syncthing/syncthing/lib/config"
+ common "github.com/iotbzh/xds-common/golib"
+ stconfig "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/protocol"
)
-// FIXME remove and use an interface on xdsconfig.FolderConfig
+// FolderChangeArg argument structure used by FolderChange
type FolderChangeArg struct {
ID string
Label string
RelativePath string
SyncThingID string
- ShareRootDir string
}
+// FolderLoadFromStConfig Load/Retrieve folder config from syncthing database
+/*
+func (s *SyncThing) FolderLoadFromStConfig(f *[]XdsFolderConfig) error {
+
+ defaultSdk := "" // cannot know which was the default sdk
+
+ stCfg, err := s.ConfigGet()
+ if err != nil {
+ return err
+ }
+ if len(stCfg.Devices) < 1 {
+ return fmt.Errorf("Cannot load syncthing config: no device defined")
+ }
+ devID := stCfg.Devices[0].DeviceID.String()
+ if devID == s.MyID {
+ if len(stCfg.Devices) < 2 {
+ return fmt.Errorf("Cannot load syncthing config: no valid device found")
+ }
+ devID = stCfg.Devices[1].DeviceID.String()
+ }
+
+ for _, stFld := range stCfg.Folders {
+ *f = append(*f, XdsFolderConfig{
+ ID: stFld.ID,
+ Label: stFld.Label,
+ ClientPath: strings.TrimRight(stFld.Path, "/"),
+ Type: XdsTypeCloudSync,
+ Status: StatusDisable,
+ DefaultSdk: defaultSdk,
+ RootPath: "",
+ DataCloudSync: XdsCloudSyncConfig{SyncThingID: devID},
+ })
+ }
+
+ return nil
+}
+*/
+
// FolderChange is called when configuration has changed
-func (s *SyncThing) FolderChange(f FolderChangeArg) error {
+func (s *SyncThing) FolderChange(f FolderChangeArg) (string, error) {
// Get current config
stCfg, err := s.ConfigGet()
if err != nil {
s.log.Errorln(err)
- return err
+ return "", err
}
+ stClientID := f.SyncThingID
// Add new Device if needed
var devID protocol.DeviceID
- if err := devID.UnmarshalText([]byte(f.SyncThingID)); err != nil {
- s.log.Errorf("not a valid device id (err %v)\n", err)
- return err
+ if err := devID.UnmarshalText([]byte(stClientID)); err != nil {
+ s.log.Errorf("not a valid device id (err %v)", err)
+ return "", err
}
- newDevice := config.DeviceConfiguration{
+ newDevice := stconfig.DeviceConfiguration{
DeviceID: devID,
- Name: f.SyncThingID,
+ Name: stClientID,
Addresses: []string{"dynamic"},
}
@@ -57,21 +97,34 @@ func (s *SyncThing) FolderChange(f FolderChangeArg) error {
label = strings.Split(id, "/")[0]
}
if id = f.ID; id == "" {
- id = f.SyncThingID[0:15] + "_" + label
+ id = stClientID[0:15] + "_" + label
+ }
+
+ // Resolve local path
+ pathCli, err := common.ResolveEnvVar(f.RelativePath)
+ if err != nil {
+ pathCli = f.RelativePath
+ }
+
+ folder := stconfig.FolderConfiguration{
+ ID: id,
+ Label: label,
+ Path: pathCli,
+ AutoNormalize: true,
}
- folder := config.FolderConfiguration{
- ID: id,
- Label: label,
- RawPath: filepath.Join(f.ShareRootDir, f.RelativePath),
+ /* TODO - add it ?
+ if s.conf.FileConf.SThgConf.RescanIntervalS > 0 {
+ folder.RescanIntervalS = s.conf.FileConf.SThgConf.RescanIntervalS
}
+ */
- folder.Devices = append(folder.Devices, config.FolderDeviceConfiguration{
+ folder.Devices = append(folder.Devices, stconfig.FolderDeviceConfiguration{
DeviceID: newDevice.DeviceID,
})
found = false
- var fld config.FolderConfiguration
+ var fld stconfig.FolderConfiguration
for _, fld = range stCfg.Folders {
if folder.ID == fld.ID {
fld = folder
@@ -85,11 +138,8 @@ func (s *SyncThing) FolderChange(f FolderChangeArg) error {
}
err = s.ConfigSet(stCfg)
- if err != nil {
- s.log.Errorln(err)
- }
- return nil
+ return id, err
}
// FolderDelete is called to delete a folder config
@@ -114,3 +164,61 @@ func (s *SyncThing) FolderDelete(id string) error {
return nil
}
+
+// FolderConfigGet Returns the configuration of a specific folder
+func (s *SyncThing) FolderConfigGet(folderID string) (stconfig.FolderConfiguration, error) {
+ fc := stconfig.FolderConfiguration{}
+ if folderID == "" {
+ return fc, fmt.Errorf("folderID not set")
+ }
+ cfg, err := s.ConfigGet()
+ if err != nil {
+ return fc, err
+ }
+ for _, f := range cfg.Folders {
+ if f.ID == folderID {
+ fc = f
+ return fc, nil
+ }
+ }
+ return fc, fmt.Errorf("id not found")
+}
+
+// FolderStatus Returns all information about the current
+func (s *SyncThing) FolderStatus(folderID string) (*FolderStatus, error) {
+ var data []byte
+ var res FolderStatus
+ if folderID == "" {
+ return nil, fmt.Errorf("folderID not set")
+ }
+ if err := s.client.HTTPGet("db/status?folder="+folderID, &data); err != nil {
+ return nil, err
+ }
+ if err := json.Unmarshal(data, &res); err != nil {
+ return nil, err
+ }
+ return &res, nil
+}
+
+// IsFolderInSync Returns true when folder is in sync
+func (s *SyncThing) IsFolderInSync(folderID string) (bool, error) {
+ sts, err := s.FolderStatus(folderID)
+ if err != nil {
+ return false, err
+ }
+ return sts.NeedBytes == 0 && sts.State == "idle", nil
+}
+
+// FolderScan Request immediate folder scan.
+// Scan all folders if folderID param is empty
+func (s *SyncThing) FolderScan(folderID string, subpath string) error {
+ url := "db/scan"
+ if folderID != "" {
+ url += "?folder=" + folderID
+
+ if subpath != "" {
+ url += "&sub=" + subpath
+ }
+ }
+ return s.client.HTTPPost(url, "")
+}
diff --git a/lib/webserver/server.go b/lib/webserver/server.go
deleted file mode 100644
index b835a65..0000000
--- a/lib/webserver/server.go
+++ /dev/null
@@ -1,226 +0,0 @@
-package webserver
-
-import (
- "fmt"
- "net/http"
- "strings"
-
- "github.com/Sirupsen/logrus"
- "github.com/gin-gonic/gin"
- "github.com/googollee/go-socket.io"
- "github.com/iotbzh/xds-agent/lib/apiv1"
- "github.com/iotbzh/xds-agent/lib/session"
- "github.com/iotbzh/xds-agent/lib/xdsconfig"
-)
-
-// ServerService .
-type ServerService struct {
- router *gin.Engine
- api *apiv1.APIService
- sIOServer *socketio.Server
- webApp *gin.RouterGroup
- cfg *xdsconfig.Config
- sessions *session.Sessions
- log *logrus.Logger
- stop chan struct{} // signals intentional stop
-}
-
-const indexFilename = "index.html"
-const cookieMaxAge = "3600"
-
-// New creates an instance of ServerService
-func New(conf *xdsconfig.Config, log *logrus.Logger) *ServerService {
-
- // Setup logging for gin router
- if log.Level == logrus.DebugLevel {
- gin.SetMode(gin.DebugMode)
- } else {
- gin.SetMode(gin.ReleaseMode)
- }
-
- // TODO
- // - try to bind gin DefaultWriter & DefaultErrorWriter to logrus logger
- // - try to fix pb about isTerminal=false when out is in VSC Debug Console
- //gin.DefaultWriter = ??
- //gin.DefaultErrorWriter = ??
-
- // Creates gin router
- r := gin.New()
-
- svr := &ServerService{
- router: r,
- api: nil,
- sIOServer: nil,
- webApp: nil,
- cfg: conf,
- log: log,
- sessions: nil,
- stop: make(chan struct{}),
- }
-
- return svr
-}
-
-// Serve starts a new instance of the Web Server
-func (s *ServerService) Serve() error {
- var err error
-
- // Setup middlewares
- s.router.Use(gin.Logger())
- s.router.Use(gin.Recovery())
- s.router.Use(s.middlewareCORS())
- s.router.Use(s.middlewareXDSDetails())
- s.router.Use(s.middlewareCSRF())
-
- // Sessions manager
- s.sessions = session.NewClientSessions(s.router, s.log, cookieMaxAge)
-
- s.router.GET("", s.slashHandler)
-
- // Create REST API
- s.api = apiv1.New(s.sessions, s.cfg, s.log, s.router)
-
- // Websocket routes
- s.sIOServer, err = socketio.NewServer(nil)
- if err != nil {
- s.log.Fatalln(err)
- }
-
- s.router.GET("/socket.io/", s.socketHandler)
- s.router.POST("/socket.io/", s.socketHandler)
- /* TODO: do we want to support ws://... ?
- s.router.Handle("WS", "/socket.io/", s.socketHandler)
- s.router.Handle("WSS", "/socket.io/", s.socketHandler)
- */
-
- // Serve in the background
- serveError := make(chan error, 1)
- go func() {
- fmt.Printf("Web Server running on localhost:%s ...\n", s.cfg.HTTPPort)
- serveError <- http.ListenAndServe(":"+s.cfg.HTTPPort, s.router)
- }()
-
- fmt.Printf("XDS agent running...\n")
-
- // Wait for stop, restart or error signals
- select {
- case <-s.stop:
- // Shutting down permanently
- s.sessions.Stop()
- s.log.Infoln("shutting down (stop)")
- case err = <-serveError:
- // Error due to listen/serve failure
- s.log.Errorln(err)
- }
-
- return nil
-}
-
-// Stop web server
-func (s *ServerService) Stop() {
- close(s.stop)
-}
-
-// serveSlash provides response to GET "/"
-func (s *ServerService) slashHandler(c *gin.Context) {
- c.String(200, "Hello from XDS agent!")
-}
-
-// Add details in Header
-func (s *ServerService) middlewareXDSDetails() gin.HandlerFunc {
- return func(c *gin.Context) {
- c.Header("XDS-Agent-Version", s.cfg.Version)
- c.Header("XDS-API-Version", s.cfg.APIVersion)
- c.Next()
- }
-}
-
-func (s *ServerService) isValidAPIKey(key string) bool {
- return (key == s.cfg.FileConf.XDSAPIKey && key != "")
-}
-
-func (s *ServerService) middlewareCSRF() gin.HandlerFunc {
- return func(c *gin.Context) {
- // Allow requests carrying a valid API key
- if s.isValidAPIKey(c.Request.Header.Get("X-API-Key")) {
- // Set the access-control-allow-origin header for CORS requests
- // since a valid API key has been provided
- c.Header("Access-Control-Allow-Origin", "*")
- c.Next()
- return
- }
-
- // Allow io.socket request
- if strings.HasPrefix(c.Request.URL.Path, "/socket.io") {
- c.Next()
- return
- }
-
- /* FIXME Add really CSRF support
-
- // Allow requests for anything not under the protected path prefix,
- // and set a CSRF cookie if there isn't already a valid one.
- if !strings.HasPrefix(c.Request.URL.Path, prefix) {
- cookie, err := c.Cookie("CSRF-Token-" + unique)
- if err != nil || !validCsrfToken(cookie.Value) {
- s.log.Debugln("new CSRF cookie in response to request for", c.Request.URL)
- c.SetCookie("CSRF-Token-"+unique, newCsrfToken(), 600, "/", "", false, false)
- }
- c.Next()
- return
- }
-
- // Verify the CSRF token
- token := c.Request.Header.Get("X-CSRF-Token-" + unique)
- if !validCsrfToken(token) {
- c.AbortWithError(403, "CSRF Error")
- return
- }
-
- c.Next()
- */
- c.AbortWithError(403, fmt.Errorf("Not valid API key"))
- }
-}
-
-// CORS middleware
-func (s *ServerService) middlewareCORS() gin.HandlerFunc {
- return func(c *gin.Context) {
- if c.Request.Method == "OPTIONS" {
- c.Header("Access-Control-Allow-Origin", "*")
- c.Header("Access-Control-Allow-Headers", "Content-Type, X-API-Key")
- c.Header("Access-Control-Allow-Methods", "GET, POST, DELETE")
- c.Header("Access-Control-Max-Age", cookieMaxAge)
- c.AbortWithStatus(204)
- return
- }
- c.Next()
- }
-}
-
-// socketHandler is the handler for the "main" websocket connection
-func (s *ServerService) socketHandler(c *gin.Context) {
-
- // Retrieve user session
- sess := s.sessions.Get(c)
- if sess == nil {
- c.JSON(500, gin.H{"error": "Cannot retrieve session"})
- return
- }
-
- s.sIOServer.On("connection", func(so socketio.Socket) {
- s.log.Debugf("WS Connected (SID=%v)", so.Id())
- s.sessions.UpdateIOSocket(sess.ID, &so)
-
- so.On("disconnection", func() {
- s.log.Debugf("WS disconnected (SID=%v)", so.Id())
- s.sessions.UpdateIOSocket(sess.ID, nil)
- })
- })
-
- s.sIOServer.On("error", func(so socketio.Socket, err error) {
- s.log.Errorf("WS SID=%v Error : %v", so.Id(), err.Error())
- })
-
- s.sIOServer.ServeHTTP(c.Writer, c.Request)
-}
diff --git a/lib/xdsconfig/config.go b/lib/xdsconfig/config.go
index 854d383..9e44070 100644
--- a/lib/xdsconfig/config.go
+++ b/lib/xdsconfig/config.go
@@ -2,24 +2,34 @@ package xdsconfig
import (
"fmt"
+ "io"
+ "path/filepath"
"os"
"github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
common "github.com/iotbzh/xds-common/golib"
+ uuid "github.com/satori/go.uuid"
)
// Config parameters (json format) of /config command
type Config struct {
- Version string `json:"version"`
- APIVersion string `json:"apiVersion"`
- VersionGitTag string `json:"gitTag"`
-
- // Private / un-exported fields
- HTTPPort string `json:"-"`
- FileConf *FileConfig `json:"-"`
- Log *logrus.Logger `json:"-"`
+ AgentUID string
+ Version string
+ APIVersion string
+ VersionGitTag string
+ Options Options
+ FileConf FileConfig
+ Log *logrus.Logger
+ LogVerboseOut io.Writer
+}
+
+// Options set at the command line
+type Options struct {
+ ConfigFile string
+ LogLevel string
+ LogFile string
}
// Config default values
@@ -32,39 +42,81 @@ const (
func Init(ctx *cli.Context, log *logrus.Logger) (*Config, error) {
var err error
+ defaultWebAppDir := "${EXEPATH}/www"
+ defaultSTHomeDir := "${HOME}/.xds/agent/syncthing-config"
+
+ // TODO: allocate uuid only the first time and save+reuse it later
+ uuid := uuid.NewV1().String()
+
// Define default configuration
c := Config{
+ AgentUID: uuid,
Version: ctx.App.Metadata["version"].(string),
APIVersion: DefaultAPIVersion,
VersionGitTag: ctx.App.Metadata["git-tag"].(string),
- HTTPPort: "8010",
- FileConf: &FileConfig{
- LogsDir: "/tmp/logs",
+ Options: Options{
+ ConfigFile: ctx.GlobalString("config"),
+ LogLevel: ctx.GlobalString("log"),
+ LogFile: ctx.GlobalString("logfile"),
+ },
+
+ FileConf: FileConfig{
+ HTTPPort: "8800",
+ WebAppDir: defaultWebAppDir,
+ LogsDir: "/tmp/logs",
+ ServersConf: []XDSServerConf{
+ XDSServerConf{
+ URL: "http://localhost:8000",
+ ConnRetry: 10,
+ },
+ },
SThgConf: &SyncThingConf{
- Home: "${HOME}/.xds/agent/syncthing-config",
+ Home: defaultSTHomeDir,
},
},
Log: log,
}
+ c.Log.Infoln("Agent UUID: ", uuid)
+
// config file settings overwrite default config
- c.FileConf, err = updateConfigFromFile(&c, ctx.GlobalString("config"))
+ err = readGlobalConfig(&c, c.Options.ConfigFile)
if err != nil {
return nil, err
}
+ // Handle where Logs are redirected:
+ // default 'stdout' (logfile option default value)
+ // else use file (or filepath) set by --logfile option
+ // that may be overwritten by LogsDir field of config file
+ logF := c.Options.LogFile
+ logD := c.FileConf.LogsDir
+ if logF != "stdout" {
+ if logD != "" {
+ lf := filepath.Base(logF)
+ if lf == "" || lf == "." {
+ lf = "xds-agent.log"
+ }
+ logF = filepath.Join(logD, lf)
+ } else {
+ logD = filepath.Dir(logF)
+ }
+ }
+ if logD == "" || logD == "." {
+ logD = "/tmp/xds/logs"
+ }
+ c.Options.LogFile = logF
+ c.FileConf.LogsDir = logD
+
if c.FileConf.LogsDir != "" && !common.Exists(c.FileConf.LogsDir) {
if err := os.MkdirAll(c.FileConf.LogsDir, 0770); err != nil {
return nil, fmt.Errorf("Cannot create logs dir: %v", err)
}
}
+
+ c.Log.Infoln("Logs file: ", c.Options.LogFile)
c.Log.Infoln("Logs directory: ", c.FileConf.LogsDir)
return &c, nil
}
-
-// UpdateAll Update the current configuration
-func (c *Config) UpdateAll(newCfg Config) error {
- return fmt.Errorf("Not Supported")
-}
diff --git a/lib/xdsconfig/configfile.go b/lib/xdsconfig/configfile.go
new file mode 100644
index 0000000..e3737f4
--- /dev/null
+++ b/lib/xdsconfig/configfile.go
@@ -0,0 +1,109 @@
+package xdsconfig
+
+import (
+ "encoding/json"
+ "os"
+ "path"
+
+ common "github.com/iotbzh/xds-common/golib"
+)
+
+type SyncThingConf struct {
+ BinDir string `json:"binDir"`
+ Home string `json:"home"`
+ GuiAddress string `json:"gui-address"`
+ GuiAPIKey string `json:"gui-apikey"`
+}
+
+type XDSServerConf struct {
+ URL string `json:"url"`
+ ConnRetry int `json:"connRetry"`
+
+ // private/not exported fields
+ ID string `json:"-"`
+ APIBaseURL string `json:"-"`
+ APIPartialURL string `json:"-"`
+}
+
+type FileConfig struct {
+ HTTPPort string `json:"httpPort"`
+ WebAppDir string `json:"webAppDir"`
+ LogsDir string `json:"logsDir"`
+ XDSAPIKey string `json:"xds-apikey"`
+ ServersConf []XDSServerConf `json:"xdsServers"`
+ SThgConf *SyncThingConf `json:"syncthing"`
+}
+
+// readGlobalConfig reads configuration from a config file.
+// Order to determine which config file is used:
+// 1/ from command line option: "--config myConfig.json"
+// 2/ $HOME/.xds/agent/agent-config.json file
+// 3/ /etc/xds-agent/config.json file
+
+func readGlobalConfig(c *Config, confFile string) error {
+
+ searchIn := make([]string, 0, 3)
+ if confFile != "" {
+ searchIn = append(searchIn, confFile)
+ }
+ if homeDir := common.GetUserHome(); homeDir != "" {
+ searchIn = append(searchIn, path.Join(homeDir, ".xds", "agent", "agent-config.json"))
+ }
+
+ searchIn = append(searchIn, "/etc/xds-agent/agent-config.json")
+
+ var cFile *string
+ for _, p := range searchIn {
+ if _, err := os.Stat(p); err == nil {
+ cFile = &p
+ break
+ }
+ }
+ if cFile == nil {
+ c.Log.Infof("No config file found")
+ return nil
+ }
+
+ c.Log.Infof("Use config file: %s", *cFile)
+
+ // TODO move on viper package to support comments in JSON and also
+ // bind with flags (command line options)
+ // see https://github.com/spf13/viper#working-with-flags
+
+ fd, _ := os.Open(*cFile)
+ defer fd.Close()
+
+ // Decode config file content and save it in a first variable
+ fCfg := FileConfig{}
+ if err := json.NewDecoder(fd).Decode(&fCfg); err != nil {
+ return err
+ }
+
+ // Decode config file content and overwrite default settings
+ fd.Seek(0, 0)
+ json.NewDecoder(fd).Decode(&c.FileConf)
+
+ // Disable Syncthing support when there is no syncthing field in config
+ if fCfg.SThgConf == nil {
+ c.FileConf.SThgConf = nil
+ }
+
+ // Support environment variables (IOW ${MY_ENV_VAR} syntax) in agent-config.json
+ vars := []*string{
+ &c.FileConf.LogsDir,
+ &c.FileConf.WebAppDir,
+ }
+ if c.FileConf.SThgConf != nil {
+ vars = append(vars, &c.FileConf.SThgConf.Home,
+ &c.FileConf.SThgConf.BinDir)
+ }
+ for _, field := range vars {
+ var err error
+ *field, err = common.ResolveEnvVar(*field)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/lib/xdsconfig/fileconfig.go b/lib/xdsconfig/fileconfig.go
deleted file mode 100644
index d936bbe..0000000
--- a/lib/xdsconfig/fileconfig.go
+++ /dev/null
@@ -1,96 +0,0 @@
-package xdsconfig
-
-import (
- "encoding/json"
- "os"
- "os/user"
- "path"
-
- common "github.com/iotbzh/xds-common/golib"
-)
-
-type SyncThingConf struct {
- BinDir string `json:"binDir"`
- Home string `json:"home"`
- GuiAddress string `json:"gui-address"`
- GuiAPIKey string `json:"gui-apikey"`
-}
-
-type FileConfig struct {
- HTTPPort string `json:"httpPort"`
- LogsDir string `json:"logsDir"`
- XDSAPIKey string `json:"xds-apikey"`
- SThgConf *SyncThingConf `json:"syncthing"`
-}
-
-// getConfigFromFile reads configuration from a config file.
-// Order to determine which config file is used:
-// 1/ from command line option: "--config myConfig.json"
-// 2/ $HOME/.xds/agent/agent-config.json file
-// 3/ /etc/xds-agent/config.json file
-
-func updateConfigFromFile(c *Config, confFile string) (*FileConfig, error) {
-
- searchIn := make([]string, 0, 3)
- if confFile != "" {
- searchIn = append(searchIn, confFile)
- }
- if usr, err := user.Current(); err == nil {
- searchIn = append(searchIn, path.Join(usr.HomeDir, ".xds", "agent", "agent-config.json"))
- }
-
- searchIn = append(searchIn, "/etc/xds-agent/config.json")
-
- var cFile *string
- for _, p := range searchIn {
- if _, err := os.Stat(p); err == nil {
- cFile = &p
- break
- }
- }
- // Use default settings
- fCfg := *c.FileConf
-
- // Read config file when existing
- if cFile != nil {
- c.Log.Infof("Use config file: %s", *cFile)
-
- // TODO move on viper package to support comments in JSON and also
- // bind with flags (command line options)
- // see https://github.com/spf13/viper#working-with-flags
-
- fd, _ := os.Open(*cFile)
- defer fd.Close()
- if err := json.NewDecoder(fd).Decode(&fCfg); err != nil {
- return nil, err
- }
- }
-
- // Support environment variables (IOW ${MY_ENV_VAR} syntax) in agent-config.json
- vars := []*string{
- &fCfg.LogsDir,
- }
- if fCfg.SThgConf != nil {
- vars = append(vars, &fCfg.SThgConf.Home, &fCfg.SThgConf.BinDir)
- }
- for _, field := range vars {
- var err error
- *field, err = common.ResolveEnvVar(*field)
- if err != nil {
- return nil, err
- }
- }
-
- // Config file settings overwrite default config
- if fCfg.HTTPPort != "" {
- c.HTTPPort = fCfg.HTTPPort
- }
-
- // Set default apikey
- // FIXME - rework with dynamic key
- if fCfg.XDSAPIKey == "" {
- fCfg.XDSAPIKey = "1234abcezam"
- }
-
- return &fCfg, nil
-}
diff --git a/main.go b/main.go
index 32083bb..b0ec7ca 100644
--- a/main.go
+++ b/main.go
@@ -3,16 +3,11 @@
package main
import (
- "fmt"
- "log"
"os"
- "time"
"github.com/Sirupsen/logrus"
"github.com/codegangsta/cli"
"github.com/iotbzh/xds-agent/lib/agent"
- "github.com/iotbzh/xds-agent/lib/syncthing"
- "github.com/iotbzh/xds-agent/lib/webserver"
"github.com/iotbzh/xds-agent/lib/xdsconfig"
)
@@ -39,62 +34,18 @@ func xdsAgent(cliCtx *cli.Context) error {
var err error
// Create Agent context
- ctx := agent.NewAgent(cliCtx)
+ ctxAgent := agent.NewAgent(cliCtx)
// Load config
- ctx.Config, err = xdsconfig.Init(cliCtx, ctx.Log)
+ ctxAgent.Config, err = xdsconfig.Init(cliCtx, ctxAgent.Log)
if err != nil {
return cli.NewExitError(err, 2)
}
- // Start local instance of Syncthing and Syncthing-notify
- ctx.SThg = st.NewSyncThing(ctx.Config, ctx.Log)
+ // Run Agent (main loop)
+ errCode, err := ctxAgent.Run()
- ctx.Log.Infof("Starting Syncthing...")
- ctx.SThgCmd, err = ctx.SThg.Start()
- if err != nil {
- return cli.NewExitError(err, 2)
- }
- fmt.Printf("Syncthing started (PID %d)\n", ctx.SThgCmd.Process.Pid)
-
- ctx.Log.Infof("Starting Syncthing-inotify...")
- ctx.SThgInotCmd, err = ctx.SThg.StartInotify()
- if err != nil {
- return cli.NewExitError(err, 2)
- }
- fmt.Printf("Syncthing-inotify started (PID %d)\n", ctx.SThgInotCmd.Process.Pid)
-
- // Establish connection with local Syncthing (retry if connection fail)
- time.Sleep(3 * time.Second)
- maxRetry := 30
- retry := maxRetry
- for retry > 0 {
- if err := ctx.SThg.Connect(); err == nil {
- break
- }
- ctx.Log.Infof("Establishing connection to Syncthing (retry %d/%d)", retry, maxRetry)
- time.Sleep(time.Second)
- retry--
- }
- if err != nil || retry == 0 {
- return cli.NewExitError(err, 2)
- }
-
- // Retrieve Syncthing config
- id, err := ctx.SThg.IDGet()
- if err != nil {
- return cli.NewExitError(err, 2)
- }
- ctx.Log.Infof("Local Syncthing ID: %s", id)
-
- // Create and start Web Server
- ctx.WWWServer = webserver.New(ctx.Config, ctx.Log)
- if err = ctx.WWWServer.Serve(); err != nil {
- log.Println(err)
- return cli.NewExitError(err, 3)
- }
-
- return cli.NewExitError("Program exited ", 4)
+ return cli.NewExitError(err, errCode)
}
// main
@@ -126,7 +77,13 @@ func main() {
Name: "log, l",
Value: "error",
Usage: "logging level (supported levels: panic, fatal, error, warn, info, debug)\n\t",
- EnvVar: "LOG_LEVEL",
+ EnvVar: "XDS_LOGLEVEL",
+ },
+ cli.StringFlag{
+ Name: "logfile",
+ Value: "stdout",
+ Usage: "filename where logs will be redirected (default stdout)\n\t",
+ EnvVar: "XDS_LOGFILE",
},
}
diff --git a/scripts/install.sh b/scripts/install.sh
index 6f93f2b..357c5e8 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -3,11 +3,13 @@
# Install XDS agent as a user systemd service
DESTDIR=${DESTDIR:-/opt/AGL/xds/agent}
+DESTDIR_WWW=${DESTDIR_WWW:-${DESTDIR}/www}
ROOT_SRCDIR=$(cd $(dirname "$0")/.. && pwd)
install() {
mkdir -p ${DESTDIR} && cp ${ROOT_SRCDIR}/bin/* ${DESTDIR} || exit 1
+ mkdir -p ${DESTDIR_WWW} && cp -a ${ROOT_SRCDIR}/webapp/dist/* ${DESTDIR_WWW} || exit 1
cp ${ROOT_SRCDIR}/conf.d/etc/xds-agent /etc/ || exit 1
cp ${ROOT_SRCDIR}/conf.d/etc/default/xds-agent /etc/default/ || exit 1
diff --git a/webapp/README.md b/webapp/README.md
new file mode 100644
index 0000000..acee846
--- /dev/null
+++ b/webapp/README.md
@@ -0,0 +1,45 @@
+XDS Dashboard
+=============
+
+This is the web application dashboard for Cross Development System.
+
+## 1. Prerequisites
+
+*nodejs* must be installed on your system and the below global node packages must be installed:
+
+> sudo npm install -g gulp-cli
+
+## 2. Installing dependencies
+
+Install dependencies by running the following command:
+
+> npm install
+
+`node_modules` and `typings` directories will be created during the install.
+
+## 3. Building the project
+
+Build the project by running the following command:
+
+> npm run clean & npm run build
+
+`dist` directory will be created during the build
+
+## 4. Starting the application
+
+Start the application by running the following command:
+
+> npm start
+
+The application will be displayed in the browser.
+
+
+## TODO
+
+- Upgrade to angular 2.4.9 or 2.4.10 AND rxjs 5.2.0
+- Complete README + package.json
+- Add prod mode and use update gulpfile tslint: "./tslint/prod.json"
+- Generate a bundle minified file, using systemjs-builder or find a better way
+ http://stackoverflow.com/questions/35280582/angular2-too-many-file-requests-on-load
+- Add SASS support
+ http://foundation.zurb.com/sites/docs/sass.html \ No newline at end of file
diff --git a/webapp/assets/favicon.ico b/webapp/assets/favicon.ico
new file mode 100644
index 0000000..6bf5138
--- /dev/null
+++ b/webapp/assets/favicon.ico
Binary files differ
diff --git a/webapp/assets/images/iot-bzh-logo-small.png b/webapp/assets/images/iot-bzh-logo-small.png
new file mode 100644
index 0000000..2c3b2ae
--- /dev/null
+++ b/webapp/assets/images/iot-bzh-logo-small.png
Binary files differ
diff --git a/webapp/assets/images/iot-graphx.jpg b/webapp/assets/images/iot-graphx.jpg
new file mode 100644
index 0000000..74c640a
--- /dev/null
+++ b/webapp/assets/images/iot-graphx.jpg
Binary files differ
diff --git a/webapp/bs-config.json b/webapp/bs-config.json
new file mode 100644
index 0000000..0041c6d
--- /dev/null
+++ b/webapp/bs-config.json
@@ -0,0 +1,9 @@
+{
+ "port": 8000,
+ "files": [
+ "dist/**/*.{html,htm,css,js}"
+ ],
+ "server": {
+ "baseDir": "dist"
+ }
+} \ No newline at end of file
diff --git a/webapp/gulp.conf.js b/webapp/gulp.conf.js
new file mode 100644
index 0000000..2e8fa17
--- /dev/null
+++ b/webapp/gulp.conf.js
@@ -0,0 +1,34 @@
+"use strict";
+
+module.exports = {
+ prodMode: process.env.PRODUCTION || false,
+ outDir: "dist",
+ paths: {
+ tsSources: ["src/**/*.ts"],
+ srcDir: "src",
+ assets: ["assets/**"],
+ node_modules_libs: [
+ 'core-js/client/shim.min.js',
+ 'reflect-metadata/Reflect.js',
+ 'rxjs-system-bundle/*.min.js',
+ 'socket.io-client/dist/socket.io*.js',
+ 'systemjs/dist/system-polyfills.js',
+ 'systemjs/dist/system.src.js',
+ 'zone.js/dist/**',
+ '@angular/**/bundles/**',
+ 'ngx-cookie/bundles/**',
+ 'ngx-bootstrap/bundles/**',
+ 'bootstrap/dist/**',
+ 'moment/*.min.js',
+ 'font-awesome-animation/dist/font-awesome-animation.min.css',
+ 'font-awesome/css/font-awesome.min.css',
+ 'font-awesome/fonts/**'
+ ]
+ },
+ deploy: {
+ target_ip: 'ip',
+ username: "user",
+ //port: 6666,
+ dir: '/tmp/xds-agent'
+ }
+}
diff --git a/webapp/gulpfile.js b/webapp/gulpfile.js
new file mode 100644
index 0000000..0226380
--- /dev/null
+++ b/webapp/gulpfile.js
@@ -0,0 +1,123 @@
+"use strict";
+//FIXME in VSC/eslint or add to typings declare function require(v: string): any;
+
+// FIXME: Rework based on
+// https://github.com/iotbzh/app-framework-templates/blob/master/templates/hybrid-html5/gulpfile.js
+// AND
+// https://github.com/antonybudianto/angular-starter
+// and/or
+// https://github.com/smmorneau/tour-of-heroes/blob/master/gulpfile.js
+
+const gulp = require("gulp"),
+ gulpif = require('gulp-if'),
+ del = require("del"),
+ sourcemaps = require('gulp-sourcemaps'),
+ tsc = require("gulp-typescript"),
+ tsProject = tsc.createProject("tsconfig.json"),
+ tslint = require('gulp-tslint'),
+ gulpSequence = require('gulp-sequence'),
+ rsync = require('gulp-rsync'),
+ conf = require('./gulp.conf');
+
+
+var tslintJsonFile = "./tslint.json"
+if (conf.prodMode) {
+ tslintJsonFile = "./tslint.prod.json"
+}
+
+
+/**
+ * Remove output directory.
+ */
+gulp.task('clean', (cb) => {
+ return del([conf.outDir], cb);
+});
+
+/**
+ * Lint all custom TypeScript files.
+ */
+gulp.task('tslint', function() {
+ return gulp.src(conf.paths.tsSources)
+ .pipe(tslint({
+ formatter: 'verbose',
+ configuration: tslintJsonFile
+ }))
+ .pipe(tslint.report());
+});
+
+/**
+ * Compile TypeScript sources and create sourcemaps in build directory.
+ */
+gulp.task("compile", ["tslint"], function() {
+ var tsResult = gulp.src(conf.paths.tsSources)
+ .pipe(sourcemaps.init())
+ .pipe(tsProject());
+ return tsResult.js
+ .pipe(sourcemaps.write(".", { sourceRoot: '/src' }))
+ .pipe(gulp.dest(conf.outDir));
+});
+
+/**
+ * Copy all resources that are not TypeScript files into build directory.
+ */
+gulp.task("resources", function() {
+ return gulp.src(["src/**/*", "!**/*.ts"])
+ .pipe(gulp.dest(conf.outDir));
+});
+
+/**
+ * Copy all assets into build directory.
+ */
+gulp.task("assets", function() {
+ return gulp.src(conf.paths.assets)
+ .pipe(gulp.dest(conf.outDir + "/assets"));
+});
+
+/**
+ * Copy all required libraries into build directory.
+ */
+gulp.task("libs", function() {
+ return gulp.src(conf.paths.node_modules_libs,
+ { cwd: "node_modules/**" }) /* Glob required here. */
+ .pipe(gulp.dest(conf.outDir + "/lib"));
+});
+
+/**
+ * Watch for changes in TypeScript, HTML and CSS files.
+ */
+gulp.task('watch', function () {
+ gulp.watch([conf.paths.tsSources], ['compile']).on('change', function (e) {
+ console.log('TypeScript file ' + e.path + ' has been changed. Compiling.');
+ });
+ gulp.watch(["src/**/*.html", "src/**/*.css"], ['resources']).on('change', function (e) {
+ console.log('Resource file ' + e.path + ' has been changed. Updating.');
+ });
+});
+
+/**
+ * Build the project.
+ */
+gulp.task("build", ['compile', 'resources', 'libs', 'assets'], function() {
+ console.log("Building the project ...");
+});
+
+/**
+ * Deploy the project on another machine/container
+ */
+gulp.task('rsync', function () {
+ return gulp.src(conf.outDir)
+ .pipe(rsync({
+ root: conf.outDir,
+ username: conf.deploy.username,
+ hostname: conf.deploy.target_ip,
+ port: conf.deploy.port || null,
+ archive: true,
+ recursive: true,
+ compress: true,
+ progress: false,
+ incremental: true,
+ destination: conf.deploy.dir
+ }));
+});
+
+gulp.task('deploy', gulpSequence('build', 'rsync')); \ No newline at end of file
diff --git a/webapp/package.json b/webapp/package.json
new file mode 100644
index 0000000..9c22f6b
--- /dev/null
+++ b/webapp/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "xds-dashboard",
+ "version": "1.0.0",
+ "description": "X (cross) Development System dashboard",
+ "scripts": {
+ "clean": "gulp clean",
+ "compile": "gulp compile",
+ "build": "gulp build",
+ "start": "concurrently --kill-others \"gulp watch\" \"lite-server\""
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/iotbzh/xds-agent"
+ },
+ "author": "Sebastien Douheret [IoT.bzh]",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/iotbzh/xds-agent/issues"
+ },
+ "dependencies": {
+ "@angular/common": "2.4.4",
+ "@angular/compiler": "2.4.4",
+ "@angular/core": "2.4.4",
+ "@angular/forms": "2.4.4",
+ "@angular/http": "2.4.4",
+ "@angular/platform-browser": "2.4.4",
+ "@angular/platform-browser-dynamic": "2.4.4",
+ "@angular/router": "3.4.4",
+ "@angular/upgrade": "2.4.4",
+ "@types/core-js": "0.9.35",
+ "@types/node": "7.0.5",
+ "@types/socket.io-client": "^1.4.29",
+ "bootstrap": "^3.3.7",
+ "core-js": "^2.4.1",
+ "font-awesome": "^4.7.0",
+ "font-awesome-animation": "0.0.10",
+ "ngx-bootstrap": "1.6.6",
+ "ngx-cookie": "^1.0.0",
+ "reflect-metadata": "^0.1.8",
+ "rxjs": "5.0.3",
+ "rxjs-system-bundle": "5.0.3",
+ "socket.io-client": "^1.7.3",
+ "socketio": "^1.0.0",
+ "systemjs": "0.20.0",
+ "zone.js": "^0.7.6"
+ },
+ "devDependencies": {
+ "concurrently": "^3.1.0",
+ "del": "^2.2.0",
+ "gulp": "^3.9.1",
+ "gulp-if": "2.0.2",
+ "gulp-rsync": "0.0.7",
+ "gulp-sequence": "^0.4.6",
+ "gulp-sourcemaps": "^1.9.1",
+ "gulp-tslint": "^7.0.1",
+ "gulp-typescript": "^3.1.3",
+ "lite-server": "^2.2.2",
+ "ts-node": "^1.7.2",
+ "tslint": "^4.0.2",
+ "typescript": "^2.2.1",
+ "typings": "^2.0.0"
+ }
+}
diff --git a/webapp/src/app/alert/alert.component.ts b/webapp/src/app/alert/alert.component.ts
new file mode 100644
index 0000000..672d7bf
--- /dev/null
+++ b/webapp/src/app/alert/alert.component.ts
@@ -0,0 +1,30 @@
+import { Component } from '@angular/core';
+import { Observable } from 'rxjs';
+
+import {AlertService, IAlert} from '../services/alert.service';
+
+@Component({
+ selector: 'app-alert',
+ template: `
+ <div style="width:80%; margin-left:auto; margin-right:auto;" *ngFor="let alert of (alerts$ | async)">
+ <alert *ngIf="alert.show" [type]="alert.type" [dismissible]="alert.dismissible" [dismissOnTimeout]="alert.dismissTimeout"
+ (onClose)="onClose(alert)">
+ <div style="text-align:center;" [innerHtml]="alert.msg"></div>
+ </alert>
+ </div>
+ `
+})
+
+export class AlertComponent {
+
+ alerts$: Observable<IAlert[]>;
+
+ constructor(private alertSvr: AlertService) {
+ this.alerts$ = this.alertSvr.alerts;
+ }
+
+ onClose(al) {
+ this.alertSvr.del(al);
+ }
+
+}
diff --git a/webapp/src/app/app.component.css b/webapp/src/app/app.component.css
new file mode 100644
index 0000000..a47ad13
--- /dev/null
+++ b/webapp/src/app/app.component.css
@@ -0,0 +1,31 @@
+.navbar {
+ background-color: whitesmoke;
+}
+
+.navbar-brand {
+ font-size: x-large;
+ font-variant: small-caps;
+ color: #5a28a1;
+}
+
+a.navbar-brand {
+ margin-top: 5px;
+}
+
+
+.navbar-nav ul li a {
+ color: #fff;
+}
+
+.menu-text {
+ color: #fff;
+}
+
+#logo-iot {
+ padding: 0 2px;
+ height: 60px;
+}
+
+li>a {
+ color:#5a28a1;
+}
diff --git a/webapp/src/app/app.component.html b/webapp/src/app/app.component.html
new file mode 100644
index 0000000..a889b12
--- /dev/null
+++ b/webapp/src/app/app.component.html
@@ -0,0 +1,30 @@
+<nav class="navbar navbar-fixed-top">
+ <!-- navbar-inverse"> -->
+ <div class="container-fluid">
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#myNavbar"
+ [attr.aria-expanded]="!isCollapsed" (click)="isCollapsed = !isCollapsed;" [ngClass]="{'collapsed': isCollapsed}">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+
+ <img class="navbar-brand" id="logo-iot" src="assets/images/iot-bzh-logo-small.png">
+ <a class="navbar-brand" href="#">X(cross) Development System Dashboard</a>
+ </div>
+
+ <div class="collapse navbar-collapse" [ngClass]="{'in': !isCollapsed}" id="myNavbar">
+ <ul class="nav navbar-nav navbar-right">
+ <li><a routerLink="/config"><i class="fa fa-2x fa-cog" title="Open configuration page" (click)="isCollapsed=true;"></i></a></li>
+ <li><a routerLink="/devel"><i class="fa fa-2x fa-play-circle" title="Open build page" (click)="isCollapsed=true;"></i></a></li>
+ <li><a routerLink="/home"><i class="fa fa-2x fa-home" title="Back to home page" (click)="isCollapsed=true;"></i></a></li>
+ </ul>
+ </div>
+ </div>
+</nav>
+
+<app-alert id="alert"></app-alert>
+
+<div style="margin:10px;">
+ <router-outlet></router-outlet>
+</div>
diff --git a/webapp/src/app/app.component.ts b/webapp/src/app/app.component.ts
new file mode 100644
index 0000000..40cfb24
--- /dev/null
+++ b/webapp/src/app/app.component.ts
@@ -0,0 +1,37 @@
+import { Component, OnInit, OnDestroy } from "@angular/core";
+import { Router } from '@angular/router';
+//TODO import {TranslateService} from "ng2-translate";
+
+@Component({
+ selector: 'app',
+ templateUrl: './app/app.component.html',
+ styleUrls: ['./app/app.component.css']
+})
+
+export class AppComponent implements OnInit, OnDestroy {
+
+ isCollapsed: boolean = true;
+
+ private defaultLanguage: string = 'en';
+
+ // I initialize the app component.
+ //TODO constructor(private translate: TranslateService) {
+ constructor(public router: Router) {
+ }
+
+ ngOnInit() {
+
+ /* TODO
+ this.translate.addLangs(["en", "fr"]);
+ this.translate.setDefaultLang(this.defaultLanguage);
+
+ let browserLang = this.translate.getBrowserLang();
+ this.translate.use(browserLang.match(/en|fr/) ? browserLang : this.defaultLanguage);
+ */
+ }
+
+ ngOnDestroy(): void {
+ }
+
+
+}
diff --git a/webapp/src/app/app.module.ts b/webapp/src/app/app.module.ts
new file mode 100644
index 0000000..c3fd586
--- /dev/null
+++ b/webapp/src/app/app.module.ts
@@ -0,0 +1,93 @@
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { HttpModule } from "@angular/http";
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { CookieModule } from 'ngx-cookie';
+
+// Import bootstrap
+import { AlertModule } from 'ngx-bootstrap/alert';
+import { ModalModule } from 'ngx-bootstrap/modal';
+import { AccordionModule } from 'ngx-bootstrap/accordion';
+import { CarouselModule } from 'ngx-bootstrap/carousel';
+import { PopoverModule } from 'ngx-bootstrap/popover';
+import { CollapseModule } from 'ngx-bootstrap/collapse';
+import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
+
+// Import the application components and services.
+import { Routing, AppRoutingProviders } from './app.routing';
+import { AppComponent } from "./app.component";
+import { AlertComponent } from './alert/alert.component';
+import { ConfigComponent } from "./config/config.component";
+import { DlXdsAgentComponent, CapitalizePipe } from "./config/downloadXdsAgent.component";
+import { ProjectCardComponent } from "./projects/projectCard.component";
+import { ProjectReadableTypePipe } from "./projects/projectCard.component";
+import { ProjectsListAccordionComponent } from "./projects/projectsListAccordion.component";
+import { ProjectAddModalComponent} from "./projects/projectAddModal.component";
+import { SdkCardComponent } from "./sdks/sdkCard.component";
+import { SdksListAccordionComponent } from "./sdks/sdksListAccordion.component";
+import { SdkSelectDropdownComponent } from "./sdks/sdkSelectDropdown.component";
+import { SdkAddModalComponent} from "./sdks/sdkAddModal.component";
+
+import { HomeComponent } from "./home/home.component";
+import { DevelComponent } from "./devel/devel.component";
+import { BuildComponent } from "./devel/build/build.component";
+import { XDSAgentService } from "./services/xdsagent.service";
+import { ConfigService } from "./services/config.service";
+import { ProjectService } from "./services/project.service";
+import { AlertService } from './services/alert.service';
+import { UtilsService } from './services/utils.service';
+import { SdkService } from "./services/sdk.service";
+
+
+
+@NgModule({
+ imports: [
+ BrowserModule,
+ HttpModule,
+ FormsModule,
+ ReactiveFormsModule,
+ Routing,
+ CookieModule.forRoot(),
+ AlertModule.forRoot(),
+ ModalModule.forRoot(),
+ AccordionModule.forRoot(),
+ CarouselModule.forRoot(),
+ PopoverModule.forRoot(),
+ CollapseModule.forRoot(),
+ BsDropdownModule.forRoot(),
+ ],
+ declarations: [
+ AppComponent,
+ AlertComponent,
+ HomeComponent,
+ BuildComponent,
+ DevelComponent,
+ ConfigComponent,
+ DlXdsAgentComponent,
+ CapitalizePipe,
+ ProjectCardComponent,
+ ProjectReadableTypePipe,
+ ProjectsListAccordionComponent,
+ ProjectAddModalComponent,
+ SdkCardComponent,
+ SdksListAccordionComponent,
+ SdkSelectDropdownComponent,
+ SdkAddModalComponent,
+ ],
+ providers: [
+ AppRoutingProviders,
+ {
+ provide: Window,
+ useValue: window
+ },
+ XDSAgentService,
+ ConfigService,
+ ProjectService,
+ AlertService,
+ UtilsService,
+ SdkService,
+ ],
+ bootstrap: [AppComponent]
+})
+export class AppModule {
+}
diff --git a/webapp/src/app/app.routing.ts b/webapp/src/app/app.routing.ts
new file mode 100644
index 0000000..f0d808f
--- /dev/null
+++ b/webapp/src/app/app.routing.ts
@@ -0,0 +1,19 @@
+import {Routes, RouterModule} from "@angular/router";
+import {ModuleWithProviders} from "@angular/core";
+import {ConfigComponent} from "./config/config.component";
+import {HomeComponent} from "./home/home.component";
+import {DevelComponent} from "./devel/devel.component";
+
+
+const appRoutes: Routes = [
+ {path: '', redirectTo: 'home', pathMatch: 'full'},
+
+ {path: 'config', component: ConfigComponent, data: {title: 'Config'}},
+ {path: 'home', component: HomeComponent, data: {title: 'Home'}},
+ {path: 'devel', component: DevelComponent, data: {title: 'Build & Deploy'}}
+];
+
+export const AppRoutingProviders: any[] = [];
+export const Routing: ModuleWithProviders = RouterModule.forRoot(appRoutes, {
+ useHash: true
+});
diff --git a/webapp/src/app/config/config.component.css b/webapp/src/app/config/config.component.css
new file mode 100644
index 0000000..6412f9a
--- /dev/null
+++ b/webapp/src/app/config/config.component.css
@@ -0,0 +1,35 @@
+.fa-big {
+ font-size: 20px;
+ font-weight: bold;
+}
+
+.fa-size-x2 {
+ font-size: 20px;
+}
+
+h2 {
+ font-family: sans-serif;
+ font-variant: small-caps;
+ font-size: x-large;
+}
+
+th span {
+ font-weight: 100;
+}
+
+th label {
+ font-weight: 100;
+ margin-bottom: 0;
+}
+
+tr.info>th {
+ vertical-align: middle;
+}
+
+tr.info>td {
+ vertical-align: middle;
+}
+
+.panel-heading {
+ background: aliceblue;
+}
diff --git a/webapp/src/app/config/config.component.html b/webapp/src/app/config/config.component.html
new file mode 100644
index 0000000..4dbd238
--- /dev/null
+++ b/webapp/src/app/config/config.component.html
@@ -0,0 +1,101 @@
+<div class="panel panel-default">
+ <div class="panel-heading">
+ <h2 class="panel-title" (click)="gConfigIsCollapsed = !gConfigIsCollapsed">
+ Global Configuration
+ <div class="pull-right">
+ <span class="fa fa-fw fa-exchange fa-size-x2" [style.color]="((agentStatus$ | async)?.WS_connected)?'green':'red'"></span>
+
+ <button class="btn btn-link" (click)="gConfigIsCollapsed = !gConfigIsCollapsed; $event.stopPropagation()">
+ <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': gConfigIsCollapsed, 'fa-angle-double-right': !gConfigIsCollapsed}"></span>
+ </button>
+ </div>
+ </h2>
+ </div>
+ <div class="panel-body" [collapse]="gConfigIsCollapsed && xdsServerConnected">
+ <div class="row">
+ <div class="col-xs-12">
+ <table class="table table-condensed">
+ <tbody>
+ <tr [ngClass]="{'info': xdsServerConnected, 'danger': !xdsServerConnected}">
+ <th><label>XDS Server URL</label></th>
+ <td> <input type="text" [(ngModel)]="xdsServerUrl"></td>
+ <td style="white-space: nowrap">
+ <div class="btn-group">
+ <button class="btn btn-link" (click)="xdsAgentRestartConn()"><span class="fa fa-refresh fa-size-x2"></span></button>
+ <dl-xds-agent class="button"></dl-xds-agent>
+ </div>
+ </td>
+ </tr>
+ <tr class="info">
+ <th><label>XDS Server connection retry</label></th>
+ <td> <input type="text" [(ngModel)]="xdsServerRetry" (ngModelChange)="showApplyBtn['retry'] = true"></td>
+ <td>
+ <button *ngIf="showApplyBtn['retry']" class="btn btn-primary btn-xs" (click)="submitGlobConf('retry')">APPLY</button>
+ </td>
+ </tr>
+ <tr class="info">
+ <th><label>Local Projects root directory</label></th>
+ <td> <input type="text" [(ngModel)]="projectsRootDir" (ngModelChange)="showApplyBtn['rootDir'] = true"></td>
+ <td>
+ <button *ngIf="showApplyBtn['rootDir']" class="btn btn-primary btn-xs" (click)="submitGlobConf('rootDir')">APPLY</button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div class="panel panel-default">
+ <div class="panel-heading">
+ <h2 class="panel-title" (click)="sdksIsCollapsed = !sdksIsCollapsed">
+ Cross SDKs
+ <div class="pull-right">
+ <button class="btn btn-link" (click)="childSdkModal.show(); $event.stopPropagation()"><span class="fa fa-plus fa-size-x2"></span></button>
+
+ <button class="btn btn-link" (click)="sdksIsCollapsed = !sdksIsCollapsed; $event.stopPropagation()">
+ <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': sdksIsCollapsed, 'fa-angle-double-right': !sdksIsCollapsed}"></span>
+ </button>
+ </div>
+ </h2>
+ </div>
+ <div class="panel-body" [collapse]="sdksIsCollapsed">
+ <div class="row col-xs-12">
+ <sdks-list-accordion [sdks]="(sdks$ | async)"></sdks-list-accordion>
+ </div>
+ </div>
+</div>
+
+<div class="panel panel-default">
+ <div class="panel-heading">
+ <h2 class="panel-title" (click)="projectsIsCollapsed = !projectsIsCollapsed; $event.stopPropagation()">
+ Projects
+ <div class="pull-right">
+ <button class="btn btn-link" (click)="childProjectModal.show(); $event.stopPropagation()"><span class="fa fa-plus fa-size-x2"></span></button>
+
+ <button class="btn btn-link" (click)="projectsIsCollapsed = !projectsIsCollapsed; $event.stopPropagation()">
+ <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': projectsIsCollapsed, 'fa-angle-double-right': !projectsIsCollapsed}"></span>
+ </button>
+ </div>
+ </h2>
+ </div>
+ <div class="panel-body" [collapse]="projectsIsCollapsed">
+ <div class="row col-xs-12">
+ <projects-list-accordion [projects]="(projects$ | async)"></projects-list-accordion>
+ </div>
+ </div>
+</div>
+
+<!-- Modals -->
+<project-add-modal #childProjectModal [title]="'Add a new project'" [server-id]=curServerID>
+</project-add-modal>
+<sdk-add-modal #childSdkModal [title]="'Add a new SDK'">
+</sdk-add-modal>
+
+<!-- only for debug -->
+<div *ngIf="false" class="row">
+ <pre>Config: {{config$ | async | json}}</pre>
+ <br>
+ <pre>Projects: {{projects$ | async | json}} </pre>
+</div>
diff --git a/webapp/src/app/config/config.component.ts b/webapp/src/app/config/config.component.ts
new file mode 100644
index 0000000..6377844
--- /dev/null
+++ b/webapp/src/app/config/config.component.ts
@@ -0,0 +1,107 @@
+import { Component, ViewChild, OnInit } from "@angular/core";
+import { Observable } from 'rxjs/Observable';
+import { FormControl, FormGroup, Validators, FormBuilder } from '@angular/forms';
+import { CollapseModule } from 'ngx-bootstrap/collapse';
+
+import { ConfigService, IConfig } from "../services/config.service";
+import { ProjectService, IProject } from "../services/project.service";
+import { XDSAgentService, IAgentStatus, IXDSConfig } from "../services/xdsagent.service";
+import { AlertService } from "../services/alert.service";
+import { ProjectAddModalComponent } from "../projects/projectAddModal.component";
+import { SdkService, ISdk } from "../services/sdk.service";
+import { SdkAddModalComponent } from "../sdks/sdkAddModal.component";
+
+@Component({
+ templateUrl: './app/config/config.component.html',
+ styleUrls: ['./app/config/config.component.css']
+})
+
+// Inspired from https://embed.plnkr.co/jgDTXknPzAaqcg9XA9zq/
+// and from http://plnkr.co/edit/vCdjZM?p=preview
+
+export class ConfigComponent implements OnInit {
+ @ViewChild('childProjectModal') childProjectModal: ProjectAddModalComponent;
+ @ViewChild('childSdkModal') childSdkModal: SdkAddModalComponent;
+
+ config$: Observable<IConfig>;
+ projects$: Observable<IProject[]>;
+ sdks$: Observable<ISdk[]>;
+ agentStatus$: Observable<IAgentStatus>;
+
+ curProj: number;
+ curServer: number;
+ curServerID: string;
+ userEditedLabel: boolean = false;
+
+ gConfigIsCollapsed: boolean = true;
+ sdksIsCollapsed: boolean = true;
+ projectsIsCollapsed: boolean = false;
+
+ // TODO replace by reactive FormControl + add validation
+ xdsServerConnected: boolean = false;
+ xdsServerUrl: string;
+ xdsServerRetry: string;
+ projectsRootDir: string; // FIXME: should be remove when projectAddModal will always return full path
+ showApplyBtn = { // Used to show/hide Apply buttons
+ "retry": false,
+ "rootDir": false,
+ };
+
+ constructor(
+ private configSvr: ConfigService,
+ private projectSvr: ProjectService,
+ private xdsAgentSvr: XDSAgentService,
+ private sdkSvr: SdkService,
+ private alert: AlertService,
+ ) {
+ }
+
+ ngOnInit() {
+ this.config$ = this.configSvr.Conf$;
+ this.projects$ = this.projectSvr.Projects$;
+ this.sdks$ = this.sdkSvr.Sdks$;
+ this.agentStatus$ = this.xdsAgentSvr.Status$;
+
+ // FIXME support multiple servers
+ this.curServer = 0;
+
+ // Bind xdsServerUrl to baseURL
+ this.xdsAgentSvr.XdsConfig$.subscribe(cfg => {
+ if (!cfg || cfg.servers.length < 1) {
+ return;
+ }
+ let svr = cfg.servers[this.curServer];
+ this.curServerID = svr.id;
+ this.xdsServerConnected = svr.connected;
+ this.xdsServerUrl = svr.url;
+ this.xdsServerRetry = String(svr.connRetry);
+ this.projectsRootDir = ''; // SEB FIXME: add in go config? cfg.projectsRootDir;
+ });
+ }
+
+ submitGlobConf(field: string) {
+ switch (field) {
+ case "retry":
+ let re = new RegExp('^[0-9]+$');
+ let rr = parseInt(this.xdsServerRetry, 10);
+ if (re.test(this.xdsServerRetry) && rr >= 0) {
+ this.xdsAgentSvr.setServerRetry(this.curServerID, rr);
+ } else {
+ this.alert.warning("Not a valid number", true);
+ }
+ break;
+ case "rootDir":
+ this.configSvr.projectsRootDir = this.projectsRootDir;
+ break;
+ default:
+ return;
+ }
+ this.showApplyBtn[field] = false;
+ }
+
+ xdsAgentRestartConn() {
+ let url = this.xdsServerUrl;
+ this.xdsAgentSvr.setServerUrl(this.curServerID, url);
+ }
+
+}
diff --git a/webapp/src/app/config/downloadXdsAgent.component.ts b/webapp/src/app/config/downloadXdsAgent.component.ts
new file mode 100644
index 0000000..0b63e50
--- /dev/null
+++ b/webapp/src/app/config/downloadXdsAgent.component.ts
@@ -0,0 +1,47 @@
+import { Component, Input, Pipe, PipeTransform } from '@angular/core';
+
+@Component({
+ selector: 'dl-xds-agent',
+ template: `
+ <template #popTemplate>
+ <h3>Install xds-agent:</h3>
+ <ul>
+ <li>On Linux machine <a href="{{url_OS_Linux}}" target="_blank">
+ <span class="fa fa-external-link"></span></a></li>
+
+ <li>On Windows machine <a href="{{url_OS_Other}}" target="_blank"><span class="fa fa-external-link"></span></a></li>
+
+ <li>On MacOS machine <a href="{{url_OS_Other}}" target="_blank"><span class="fa fa-external-link"></span></a></li>
+ </ul>
+ <button type="button" class="btn btn-sm" (click)="pop.hide()"> Cancel </button>
+ </template>
+ <button type="button" class="btn btn-link fa fa-download fa-size-x2"
+ [popover]="popTemplate"
+ #pop="bs-popover"
+ placement="left">
+ </button>
+ `,
+ styles: [`
+ .fa-size-x2 {
+ font-size: 20px;
+ }
+ `]
+})
+
+export class DlXdsAgentComponent {
+
+ public url_OS_Linux = "https://en.opensuse.org/LinuxAutomotive#Installation_AGL_XDS";
+ public url_OS_Other = "https://github.com/iotbzh/xds-agent#how-to-install-on-other-platform";
+}
+
+@Pipe({
+ name: 'capitalize'
+})
+export class CapitalizePipe implements PipeTransform {
+ transform(value: string): string {
+ if (value) {
+ return value.charAt(0).toUpperCase() + value.slice(1);
+ }
+ return value;
+ }
+}
diff --git a/webapp/src/app/devel/build/build.component.css b/webapp/src/app/devel/build/build.component.css
new file mode 100644
index 0000000..695a89b
--- /dev/null
+++ b/webapp/src/app/devel/build/build.component.css
@@ -0,0 +1,54 @@
+.vcenter {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.blocks .btn-primary {
+ margin-left: 5px;
+ margin-right: 5px;
+ margin-top: 5px;
+ border-radius: 4px !important;
+}
+
+.table-center {
+ width: 80%;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.table-borderless>tbody>tr>td,
+.table-borderless>tbody>tr>th,
+.table-borderless>tfoot>tr>td,
+.table-borderless>tfoot>tr>th,
+.table-borderless>thead>tr>td,
+.table-borderless>thead>tr>th {
+ border: none;
+}
+
+.table-in-accordion>tbody>tr>th {
+ width: 30%
+}
+
+.btn-large {
+ width: 10em;
+}
+
+.fa-big {
+ font-size: 18px;
+ font-weight: bold;
+}
+
+.textarea-scroll {
+ width: 100%;
+ overflow-y: scroll;
+}
+
+h2 {
+ font-family: sans-serif;
+ font-variant: small-caps;
+ font-size: x-large;
+}
+
+.panel-heading {
+ background: aliceblue;
+}
diff --git a/webapp/src/app/devel/build/build.component.html b/webapp/src/app/devel/build/build.component.html
new file mode 100644
index 0000000..2bcd2c7
--- /dev/null
+++ b/webapp/src/app/devel/build/build.component.html
@@ -0,0 +1,115 @@
+<div class="panel panel-default">
+ <div class="panel-heading">
+ <h2 class="panel-title" (click)="buildIsCollapsed = !buildIsCollapsed">
+ Build
+ <div class="pull-right">
+ <button class="btn btn-link" (click)="buildIsCollapsed = !buildIsCollapsed; $event.stopPropagation()">
+ <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': buildIsCollapsed, 'fa-angle-double-right': !buildIsCollapsed}"></span>
+ </button>
+ </div>
+ </h2>
+ </div>
+ <div class="panel-body" [collapse]="buildIsCollapsed">
+ <form [formGroup]="buildForm">
+ <div class="col-xs-12">
+ <table class="table table-borderless table-center">
+ <tbody>
+ <tr>
+ <th>Cross SDK</th>
+ <td>
+ <!-- FIXME why not working ?
+ <sdk-select-dropdown [sdks]="(sdks$ | async)"></sdk-select-dropdown>
+ -->
+ <sdk-select-dropdown></sdk-select-dropdown>
+ </td>
+ </tr>
+ <tr>
+ <th>Project root path</th>
+ <td> <input type="text" disabled style="width:99%;" [value]="curProject && curProject.pathClient"></td>
+ </tr>
+ <tr>
+ <th>Sub-path</th>
+ <td> <input type="text" style="width:99%;" formControlName="subpath"> </td>
+ </tr>
+ <tr>
+ <td colspan="2">
+ <accordion>
+ <accordion-group #group>
+ <div accordion-heading>
+ Advanced Settings
+ <i class="pull-right float-xs-right fa" [ngClass]="{'fa-chevron-down': group.isOpen, 'fa-chevron-right': !group.isOpen}"></i>
+ </div>
+
+ <table class="table table-borderless table-in-accordion">
+ <tbody>
+ <tr>
+ <th>Clean Command</th>
+ <td> <input type="text" style="width:99%;" formControlName="cmdClean"> </td>
+ </tr>
+ <tr>
+ <th>Pre-Build Command</th>
+ <td> <input type="text" style="width:99%;" formControlName="cmdPrebuild"> </td>
+ </tr>
+ <tr>
+ <th>Build Command</th>
+ <td> <input type="text" style="width:99%;" formControlName="cmdBuild"> </td>
+ </tr>
+ <tr>
+ <th>Populate Command</th>
+ <td> <input type="text" style="width:99%;" formControlName="cmdPopulate"> </td>
+ </tr>
+ <tr>
+ <th>Env variables</th>
+ <td> <input type="text" style="width:99%;" formControlName="envVars"> </td>
+ </tr>
+ <tr *ngIf="debugEnable">
+ <th>Args variables</th>
+ <td> <input type="text" style="width:99%;" formControlName="cmdArgs"> </td>
+ </tr>
+ </tbody>
+ </table>
+ </accordion-group>
+ </accordion>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="row">
+ <div class="col-xs-12 text-center">
+ <div class="btn-group blocks">
+ <button class="btn btn-primary btn-large" (click)="clean()" [disabled]="!curProject ">Clean</button>
+ <button class="btn btn-primary btn-large" (click)="preBuild()" [disabled]="!curProject">Pre-Build</button>
+ <button class="btn btn-primary btn-large" (click)="build()" [disabled]="!curProject">Build</button>
+ <button class="btn btn-primary btn-large" (click)="populate()" [disabled]="!curProject ">Populate</button>
+ <button *ngIf="debugEnable" class="btn btn-primary btn-large" (click)="execCmd()" [disabled]="!curProject ">Execute command</button>
+ <button *ngIf="debugEnable" class="btn btn-primary btn-large" (click)="make()" [disabled]="!curProject ">Make</button>
+ </div>
+ </div>
+ </div>
+ </form>
+
+ <div style="margin-left: 2em; margin-right: 2em; ">
+ <div class="row ">
+ <div class="col-xs-10">
+ <div class="row ">
+ <div class="col-xs-4">
+ <label>Command Output</label>
+ </div>
+ <div class="col-xs-8" style="font-size:x-small; margin-top:5px;">
+ {{ cmdInfo }}
+ </div>
+ </div>
+ </div>
+ <div class="col-xs-2">
+ <button class="btn btn-link pull-right " (click)="reset() "><span class="fa fa-eraser fa-size-x2"></span></button>
+ </div>
+ </div>
+ <div class="row ">
+ <div class="col-xs-12 text-center ">
+ <textarea rows="20" class="textarea-scroll" #scrollOutput>{{ cmdOutput }}</textarea>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/webapp/src/app/devel/build/build.component.ts b/webapp/src/app/devel/build/build.component.ts
new file mode 100644
index 0000000..87df4e1
--- /dev/null
+++ b/webapp/src/app/devel/build/build.component.ts
@@ -0,0 +1,223 @@
+import { Component, AfterViewChecked, ElementRef, ViewChild, OnInit, Input } from '@angular/core';
+import { Observable } from 'rxjs';
+import { FormControl, FormGroup, Validators, FormBuilder } from '@angular/forms';
+import { CookieService } from 'ngx-cookie';
+
+import 'rxjs/add/operator/scan';
+import 'rxjs/add/operator/startWith';
+
+import { XDSAgentService, ICmdOutput } from "../../services/xdsagent.service";
+import { ProjectService, IProject } from "../../services/project.service";
+import { AlertService, IAlert } from "../../services/alert.service";
+import { SdkService } from "../../services/sdk.service";
+
+@Component({
+ selector: 'panel-build',
+ moduleId: module.id,
+ templateUrl: './build.component.html',
+ styleUrls: ['./build.component.css']
+})
+
+export class BuildComponent implements OnInit, AfterViewChecked {
+ @ViewChild('scrollOutput') private scrollContainer: ElementRef;
+
+ @Input() curProject: IProject;
+
+ public buildForm: FormGroup;
+ public subpathCtrl = new FormControl("", Validators.required);
+ public debugEnable: boolean = false;
+ public buildIsCollapsed: boolean = false;
+ public cmdOutput: string;
+ public cmdInfo: string;
+
+ private startTime: Map<string, number> = new Map<string, number>();
+
+ constructor(
+ private xdsSvr: XDSAgentService,
+ private fb: FormBuilder,
+ private alertSvr: AlertService,
+ private sdkSvr: SdkService,
+ private cookie: CookieService,
+ ) {
+ 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;
+ this.cmdOutput += data.stderr;
+ });
+
+ // Command exit
+ this.xdsSvr.CmdExit$.subscribe(exit => {
+ if (this.startTime.has(exit.cmdID)) {
+ this.cmdInfo = 'Last command duration: ' + this._computeTime(this.startTime.get(exit.cmdID));
+ this.startTime.delete(exit.cmdID);
+ }
+
+ if (exit && exit.code !== 0) {
+ this.cmdOutput += "--- Command exited with code " + exit.code + " ---\n\n";
+ }
+ });
+
+ this._scrollToBottom();
+
+ // only use for debug
+ this.debugEnable = (this.cookie.get("debug_build") === "1");
+ }
+
+ ngAfterViewChecked() {
+ this._scrollToBottom();
+ }
+
+ reset() {
+ this.cmdOutput = '';
+ }
+
+ clean() {
+ this._exec(
+ this.buildForm.value.cmdClean,
+ this.buildForm.value.subpath,
+ [],
+ this.buildForm.value.envVars);
+ }
+
+ preBuild() {
+ this._exec(
+ this.buildForm.value.cmdPrebuild,
+ this.buildForm.value.subpath,
+ [],
+ this.buildForm.value.envVars);
+ }
+
+ build() {
+ this._exec(
+ this.buildForm.value.cmdBuild,
+ this.buildForm.value.subpath,
+ [],
+ this.buildForm.value.envVars
+ );
+ }
+
+ populate() {
+ this._exec(
+ this.buildForm.value.cmdPopulate,
+ this.buildForm.value.subpath,
+ [], // args
+ this.buildForm.value.envVars
+ );
+ }
+
+ execCmd() {
+ this._exec(
+ this.buildForm.value.cmdArgs,
+ this.buildForm.value.subpath,
+ [],
+ this.buildForm.value.envVars
+ );
+ }
+
+ private _exec(cmd: string, dir: string, args: string[], env: string) {
+ if (!this.curProject) {
+ this.alertSvr.warning('No active project', true);
+ }
+
+ let prjID = this.curProject.id;
+
+ this.cmdOutput += this._outputHeader();
+
+ let sdkid = this.sdkSvr.getCurrentId();
+
+ // Detect key=value in env string to build array of string
+ let envArr = [];
+ env.split(';').forEach(v => envArr.push(v.trim()));
+
+ let t0 = performance.now();
+ this.cmdInfo = 'Start build of ' + prjID + ' at ' + t0;
+
+ this.xdsSvr.exec(prjID, dir, cmd, sdkid, args, envArr)
+ .subscribe(res => {
+ this.startTime.set(String(res.cmdID), t0);
+ },
+ err => {
+ this.cmdInfo = 'Last command duration: ' + this._computeTime(t0);
+ this.alertSvr.error('ERROR: ' + err);
+ });
+ }
+
+ make(args: string) {
+ if (!this.curProject) {
+ this.alertSvr.warning('No active project', true);
+ }
+
+ let prjID = this.curProject.id;
+
+ this.cmdOutput += this._outputHeader();
+
+ let sdkid = this.sdkSvr.getCurrentId();
+
+ let argsArr = args ? args.split(' ') : this.buildForm.value.cmdArgs.split(' ');
+
+ // Detect key=value in env string to build array of string
+ let envArr = [];
+ this.buildForm.value.envVars.split(';').forEach(v => envArr.push(v.trim()));
+
+ let t0 = performance.now();
+ this.cmdInfo = 'Start build of ' + prjID + ' at ' + t0;
+
+ this.xdsSvr.make(prjID, this.buildForm.value.subpath, sdkid, argsArr, envArr)
+ .subscribe(res => {
+ this.startTime.set(String(res.cmdID), t0);
+ },
+ err => {
+ this.cmdInfo = 'Last command duration: ' + this._computeTime(t0);
+ this.alertSvr.error('ERROR: ' + err);
+ });
+ }
+
+ private _scrollToBottom(): void {
+ try {
+ this.scrollContainer.nativeElement.scrollTop = this.scrollContainer.nativeElement.scrollHeight;
+ } catch (err) { }
+ }
+
+ private _computeTime(t0: number, t1?: number): string {
+ let enlap = Math.round((t1 || performance.now()) - t0);
+ if (enlap < 1000.0) {
+ return enlap.toFixed(2) + ' ms';
+ } else {
+ return (enlap / 1000.0).toFixed(3) + ' seconds';
+ }
+ }
+
+ private _outputHeader(): string {
+ return "--- " + new Date().toString() + " ---\n";
+ }
+
+ private _outputFooter(): string {
+ return "\n";
+ }
+}
diff --git a/webapp/src/app/devel/devel.component.css b/webapp/src/app/devel/devel.component.css
new file mode 100644
index 0000000..4b03dcb
--- /dev/null
+++ b/webapp/src/app/devel/devel.component.css
@@ -0,0 +1,19 @@
+.table-center {
+ width: 60%;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.table-borderless>tbody>tr>td,
+.table-borderless>tbody>tr>th,
+.table-borderless>tfoot>tr>td,
+.table-borderless>tfoot>tr>th,
+.table-borderless>thead>tr>td,
+.table-borderless>thead>tr>th {
+ border: none;
+}
+
+a.dropdown-item.disabled {
+ pointer-events:none;
+ opacity:0.4;
+}
diff --git a/webapp/src/app/devel/devel.component.html b/webapp/src/app/devel/devel.component.html
new file mode 100644
index 0000000..cc62889
--- /dev/null
+++ b/webapp/src/app/devel/devel.component.html
@@ -0,0 +1,40 @@
+<div class="row">
+ <div class="col-md-8">
+ <table class="table table-borderless table-center">
+ <tbody>
+ <tr>
+ <th style="border: none;">Project</th>
+ <td>
+ <div class="btn-group" dropdown *ngIf="curPrj">
+ <button dropdownToggle type="button" class="btn btn-primary dropdown-toggle" style="width: 20em;">
+ {{curPrj.label}}
+ <span class="caret" style="float: right; margin-top: 8px;"></span>
+ </button>
+ <ul *dropdownMenu class="dropdown-menu" role="menu">
+ <li role="menuitem"><a class="dropdown-item" *ngFor="let prj of (Prjs$ | async)" [class.disabled]="!prj.isUsable"
+ (click)="curPrj=prj">{{prj.label}}</a>
+ </li>
+
+ </ul>
+ </div>
+ <span *ngIf="!curPrj" style="color:red; font-style: italic;">
+ No project detected, please create first a project using the configuration page.
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</div>
+
+<div class="row">
+ <!--<div class="col-md-8">-->
+ <div class="col-md-12">
+ <panel-build [curProject]=curPrj></panel-build>
+ </div>
+ <!-- TODO: disable for now
+ <div class="col-md-4">
+ <panel-deploy [curProject]=curPrj></panel-deploy>
+ </div>
+ -->
+</div>
diff --git a/webapp/src/app/devel/devel.component.ts b/webapp/src/app/devel/devel.component.ts
new file mode 100644
index 0000000..5c8b9f2
--- /dev/null
+++ b/webapp/src/app/devel/devel.component.ts
@@ -0,0 +1,35 @@
+import { Component } from '@angular/core';
+
+import { Observable } from 'rxjs';
+
+import { ProjectService, IProject } from "../services/project.service";
+
+@Component({
+ selector: 'devel',
+ moduleId: module.id,
+ templateUrl: './devel.component.html',
+ styleUrls: ['./devel.component.css'],
+})
+
+export class DevelComponent {
+
+ curPrj: IProject;
+ Prjs$: Observable<IProject[]>;
+
+ constructor(private projectSvr: ProjectService) {
+ }
+
+ ngOnInit() {
+ this.Prjs$ = this.projectSvr.Projects$;
+ this.Prjs$.subscribe((prjs) => {
+ // Select project if no one is selected or no project exists
+ if (this.curPrj && "id" in this.curPrj) {
+ this.curPrj = prjs.find(p => p.id === this.curPrj.id) || prjs[0];
+ } else if (this.curPrj == null) {
+ this.curPrj = prjs[0];
+ } else {
+ this.curPrj = null;
+ }
+ });
+ }
+}
diff --git a/webapp/src/app/home/home.component.ts b/webapp/src/app/home/home.component.ts
new file mode 100644
index 0000000..0e3c995
--- /dev/null
+++ b/webapp/src/app/home/home.component.ts
@@ -0,0 +1,81 @@
+import { Component, OnInit } from '@angular/core';
+
+export interface ISlide {
+ img?: string;
+ imgAlt?: string;
+ hText?: string;
+ hHtml?: string;
+ text?: string;
+ html?: string;
+ btn?: string;
+ btnHref?: string;
+}
+
+@Component({
+ selector: 'home',
+ moduleId: module.id,
+ template: `
+ <style>
+ .wide img {
+ width: 98%;
+ }
+ .carousel-item {
+ max-height: 90%;
+ }
+ h1, h2, h3, h4, p {
+ color: #330066;
+ }
+ .html-inner {
+ color: #330066;
+ }
+ h1 {
+ font-size: 4em;
+ }
+ p {
+ font-size: 2.5em;
+ }
+
+ </style>
+
+ <div class="wide">
+ <carousel [interval]="carInterval" [(activeSlide)]="activeSlideIndex">
+ <slide *ngFor="let sl of slides; let index=index">
+ <img [src]="sl.img" [alt]="sl.imgAlt">
+ <div class="carousel-caption">
+ <h1 *ngIf="sl.hText">{{ sl.hText }}</h1>
+ <h1 *ngIf="sl.hHtml" class="html-inner" [innerHtml]="sl.hHtml"></h1>
+ <p *ngIf="sl.text">{{ sl.text }}</p>
+ <div *ngIf="sl.html" class="html-inner" [innerHtml]="sl.html"></div>
+ </div>
+ </slide>
+ </carousel>
+ </div>
+ `
+})
+
+export class HomeComponent {
+
+ public carInterval: number = 4000;
+
+ // FIXME SEB - Add more slides and info
+ public slides: ISlide[] = [
+ {
+ img: 'assets/images/iot-graphx.jpg',
+ imgAlt: "iot graphx image",
+ hText: "Welcome to XDS Dashboard !",
+ text: "X(cross) Development System allows developers to easily cross-compile applications.",
+ },
+ {
+ img: 'assets/images/iot-graphx.jpg',
+ imgAlt: "iot graphx image",
+ hText: "Create, Build, Deploy, Enjoy !",
+ },
+ {
+ img: 'assets/images/iot-graphx.jpg',
+ imgAlt: "iot graphx image",
+ hHtml: '<p>To Start: click on <i class="fa fa-cog" style="color:#9d9d9d;"></i> icon and add new folder</p>',
+ }
+ ];
+
+ constructor() { }
+} \ No newline at end of file
diff --git a/webapp/src/app/main.ts b/webapp/src/app/main.ts
new file mode 100644
index 0000000..1f68ccc
--- /dev/null
+++ b/webapp/src/app/main.ts
@@ -0,0 +1,6 @@
+import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
+import {AppModule} from './app.module';
+
+const platform = platformBrowserDynamic();
+
+platform.bootstrapModule(AppModule); \ No newline at end of file
diff --git a/webapp/src/app/projects/projectAddModal.component.css b/webapp/src/app/projects/projectAddModal.component.css
new file mode 100644
index 0000000..77f73a5
--- /dev/null
+++ b/webapp/src/app/projects/projectAddModal.component.css
@@ -0,0 +1,24 @@
+.table-borderless>tbody>tr>td,
+.table-borderless>tbody>tr>th,
+.table-borderless>tfoot>tr>td,
+.table-borderless>tfoot>tr>th,
+.table-borderless>thead>tr>td,
+.table-borderless>thead>tr>th {
+ border: none;
+}
+
+tr>th {
+ vertical-align: middle;
+}
+
+tr>td {
+ vertical-align: middle;
+}
+
+th label {
+ margin-bottom: 0;
+}
+
+td input {
+ width: 100%;
+}
diff --git a/webapp/src/app/projects/projectAddModal.component.html b/webapp/src/app/projects/projectAddModal.component.html
new file mode 100644
index 0000000..dc84985
--- /dev/null
+++ b/webapp/src/app/projects/projectAddModal.component.html
@@ -0,0 +1,54 @@
+<div bsModal #childProjectModal="bs-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel"
+ [config]="{backdrop: 'static'}" aria-hidden="true">
+ <div class="modal-dialog modal-lg">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title pull-left">{{title}}</h4>
+ <button type="button" class="close pull-right" aria-label="Close" (click)="hide()">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+
+ <form [formGroup]="addProjectForm" (ngSubmit)="onSubmit()">
+ <div class="modal-body">
+ <div class="row ">
+ <div class="col-xs-12">
+ <table class="table table-borderless">
+ <tbody>
+ <tr>
+ <th><label>Sharing Type </label></th>
+ <td><select class="form-control" formControlName="type">
+ <option *ngFor="let t of projectTypes" [value]="t.value">{{t.display}}
+ </option>
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <th><label for="select-local-path">Local Path </label></th>
+ <td><input type="text" id="select-local-path" formControlName="pathCli" placeholder="/tmp/myProject" (change)="onChangeLocalProject($event)"></td>
+ </tr>
+ <tr>
+ <th><label for="select-server-path">Server Path </label></th>
+ <td><input type="text" id="select-server-path" formControlName="pathSvr"></td>
+ </tr>
+ <tr>
+ <th><label for="select-label">Label </label></th>
+ <td><input type="text" formControlName="label" id="select-label" (keyup)="onKeyLabel($event)"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <div class="pull-left">
+ <button class="btn btn-default" (click)="cancelAction=true; hide()"> Cancel </button>
+ </div>
+ <div class="">
+ <button class="btn btn-primary" type="submit" [disabled]="!addProjectForm.valid">Add Folder</button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+</div>
diff --git a/webapp/src/app/projects/projectAddModal.component.ts b/webapp/src/app/projects/projectAddModal.component.ts
new file mode 100644
index 0000000..1352060
--- /dev/null
+++ b/webapp/src/app/projects/projectAddModal.component.ts
@@ -0,0 +1,147 @@
+import { Component, Input, ViewChild, OnInit } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { ModalDirective } from 'ngx-bootstrap/modal';
+import { FormControl, FormGroup, Validators, 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 "../services/alert.service";
+import { ProjectService, IProject, ProjectType, ProjectTypes } from "../services/project.service";
+
+
+@Component({
+ selector: 'project-add-modal',
+ templateUrl: './app/projects/projectAddModal.component.html',
+ styleUrls: ['./app/projects/projectAddModal.component.css']
+})
+export class ProjectAddModalComponent {
+ @ViewChild('childProjectModal') public childProjectModal: ModalDirective;
+ @Input() title?: string;
+ @Input('server-id') serverID: string;
+
+ cancelAction: boolean = false;
+ userEditedLabel: boolean = false;
+ projectTypes = ProjectTypes;
+
+ addProjectForm: FormGroup;
+ typeCtrl: FormControl;
+ pathCliCtrl: FormControl;
+ pathSvrCtrl: FormControl;
+
+ constructor(
+ private alert: AlertService,
+ private projectSvr: ProjectService,
+ private fb: FormBuilder
+ ) {
+ // 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, Validators.pattern("[A-Za-z]+"));
+ this.pathCliCtrl = new FormControl("", Validators.required);
+ this.pathSvrCtrl = new FormControl({ value: "", disabled: true }, [Validators.required, Validators.minLength(1)]);
+
+ this.addProjectForm = fb.group({
+ type: this.typeCtrl,
+ pathCli: this.pathCliCtrl,
+ pathSvr: this.pathSvrCtrl,
+ label: ["", Validators.nullValidator],
+ });
+ }
+
+ ngOnInit() {
+ // Auto create label name
+ this.pathCliCtrl.valueChanges
+ .debounceTime(100)
+ .filter(n => n)
+ .map(n => {
+ let 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 => {
+ let dis = (valType === String(ProjectType.SYNCTHING));
+ this.pathSvrCtrl.reset({ value: "", disabled: dis });
+ });
+ }
+
+ show() {
+ this.cancelAction = false;
+ this.userEditedLabel = false;
+ this.childProjectModal.show();
+ }
+
+ hide() {
+ this.childProjectModal.hide();
+ }
+
+ onKeyLabel(event: any) {
+ this.userEditedLabel = (this.addProjectForm.value.label !== "");
+ }
+
+ /* FIXME: change input to file type
+ <td><input type="file" id="select-local-path" webkitdirectory
+ formControlName="pathCli" placeholder="myProject" (change)="onChangeLocalProject($event)"></td>
+
+ onChangeLocalProject(e) {
+ if e.target.files.length < 1 {
+ console.log('NO files');
+ }
+ let dir = e.target.files[0].webkitRelativePath;
+ console.log("files: " + dir);
+ let u = URL.createObjectURL(e.target.files[0]);
+ }
+ */
+ onChangeLocalProject(e) {
+ }
+
+ onSubmit() {
+ if (this.cancelAction) {
+ return;
+ }
+
+ let formVal = this.addProjectForm.value;
+
+ let 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.hide();
+
+ // Reset Value for the next creation
+ this.addProjectForm.reset();
+ let selectedType = this.projectTypes[0].value;
+ this.addProjectForm.patchValue({ type: selectedType });
+
+ },
+ err => {
+ this.alert.error(err, 60);
+ this.hide();
+ });
+ }
+
+}
diff --git a/webapp/src/app/projects/projectCard.component.ts b/webapp/src/app/projects/projectCard.component.ts
new file mode 100644
index 0000000..fdacba4
--- /dev/null
+++ b/webapp/src/app/projects/projectCard.component.ts
@@ -0,0 +1,91 @@
+import { Component, Input, Pipe, PipeTransform } from '@angular/core';
+import { ProjectService, IProject, ProjectType } from "../services/project.service";
+import { AlertService } from "../services/alert.service";
+
+@Component({
+ selector: 'project-card',
+ template: `
+ <div class="row">
+ <div class="col-xs-12">
+ <div class="text-right" role="group">
+ <button class="btn btn-link" (click)="delete(project)">
+ <span class="fa fa-trash fa-size-x2"></span>
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <table class="table table-striped">
+ <tbody>
+ <tr>
+ <th><span class="fa fa-fw fa-id-badge"></span>&nbsp;<span>Project ID</span></th>
+ <td>{{ project.id }}</td>
+ </tr>
+ <tr>
+ <th><span class="fa fa-fw fa-exchange"></span>&nbsp;<span>Sharing type</span></th>
+ <td>{{ project.type | readableType }}</td>
+ </tr>
+ <tr>
+ <th><span class="fa fa-fw fa-folder-open-o"></span>&nbsp;<span>Local path</span></th>
+ <td>{{ project.pathClient }}</td>
+ </tr>
+ <tr *ngIf="project.pathServer && project.pathServer != ''">
+ <th><span class="fa fa-fw fa-folder-open-o"></span>&nbsp;<span>Server path</span></th>
+ <td>{{ project.pathServer }}</td>
+ </tr>
+ <tr>
+ <th><span class="fa fa-fw fa-flag"></span>&nbsp;<span>Status</span></th>
+ <td>{{ project.status }} - {{ project.isInSync ? "Up to Date" : "Out of Sync"}}
+ <button *ngIf="!project.isInSync" class="btn btn-link" (click)="sync(project)">
+ <span class="fa fa-refresh fa-size-x2"></span>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table >
+ `,
+ styleUrls: ['./app/config/config.component.css']
+})
+
+export class ProjectCardComponent {
+
+ @Input() project: IProject;
+
+ constructor(
+ private alert: AlertService,
+ private projectSvr: ProjectService
+ ) {
+ }
+
+ delete(prj: IProject) {
+ this.projectSvr.Delete(prj)
+ .subscribe(res => {
+ }, err => {
+ this.alert.error("Delete ERROR: " + err);
+ });
+ }
+
+ sync(prj: IProject) {
+ this.projectSvr.Sync(prj)
+ .subscribe(res => {
+ }, err => {
+ this.alert.error("ERROR: " + err);
+ });
+ }
+
+}
+
+// Remove APPS. prefix if translate has failed
+@Pipe({
+ name: 'readableType'
+})
+
+export class ProjectReadableTypePipe implements PipeTransform {
+ transform(type: ProjectType): string {
+ switch (type) {
+ case ProjectType.NATIVE_PATHMAP: return "Native (path mapping)";
+ case ProjectType.SYNCTHING: return "Cloud (Syncthing)";
+ default: return String(type);
+ }
+ }
+}
diff --git a/webapp/src/app/projects/projectsListAccordion.component.ts b/webapp/src/app/projects/projectsListAccordion.component.ts
new file mode 100644
index 0000000..210be5c
--- /dev/null
+++ b/webapp/src/app/projects/projectsListAccordion.component.ts
@@ -0,0 +1,39 @@
+import { Component, Input } from "@angular/core";
+
+import { IProject } from "../services/project.service";
+
+@Component({
+ selector: 'projects-list-accordion',
+ template: `
+ <style>
+ .fa.fa-exclamation-triangle {
+ margin-right: 2em;
+ color: red;
+ }
+ .fa.fa-refresh {
+ margin-right: 10px;
+ color: darkviolet;
+ }
+ </style>
+ <accordion>
+ <accordion-group #group *ngFor="let prj of projects">
+ <div accordion-heading>
+ {{ prj.label }}
+ <div class="pull-right">
+ <i *ngIf="prj.status == 'Syncing'" class="fa fa-refresh faa-spin animated"></i>
+ <i *ngIf="!prj.isInSync && prj.status != 'Syncing'" class="fa fa-exclamation-triangle"></i>
+ <i class="fa" [ngClass]="{'fa-chevron-down': group.isOpen, 'fa-chevron-right': !group.isOpen}"></i>
+ </div>
+ </div>
+ <project-card [project]="prj"></project-card>
+ </accordion-group>
+ </accordion>
+ `
+})
+export class ProjectsListAccordionComponent {
+
+ @Input() projects: IProject[];
+
+}
+
+
diff --git a/webapp/src/app/sdks/sdkAddModal.component.html b/webapp/src/app/sdks/sdkAddModal.component.html
new file mode 100644
index 0000000..2c07fca
--- /dev/null
+++ b/webapp/src/app/sdks/sdkAddModal.component.html
@@ -0,0 +1,23 @@
+<div bsModal #sdkChildModal="bs-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel"
+ aria-hidden="true">
+ <div class="modal-dialog modal-lg">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title pull-left">{{title}}</h4>
+ <button type="button" class="close pull-right" aria-label="Close" (click)="hideChildModal()">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <ng-content select=".modal-body"> </ng-content>
+ <i>Not available for now.</i>
+ </div>
+
+ <div class="modal-footer">
+ <div class="pull-left">
+ <button class="btn btn-default" (click)="hide()"> Cancel </button>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/webapp/src/app/sdks/sdkAddModal.component.ts b/webapp/src/app/sdks/sdkAddModal.component.ts
new file mode 100644
index 0000000..b6c8eb2
--- /dev/null
+++ b/webapp/src/app/sdks/sdkAddModal.component.ts
@@ -0,0 +1,24 @@
+import { Component, Input, ViewChild } from '@angular/core';
+import { ModalDirective } from 'ngx-bootstrap/modal';
+
+@Component({
+ selector: 'sdk-add-modal',
+ templateUrl: './app/sdks/sdkAddModal.component.html',
+})
+export class SdkAddModalComponent {
+ @ViewChild('sdkChildModal') public sdkChildModal: ModalDirective;
+
+ @Input() title?: string;
+
+ // TODO
+ constructor() {
+ }
+
+ show() {
+ this.sdkChildModal.show();
+ }
+
+ hide() {
+ this.sdkChildModal.hide();
+ }
+}
diff --git a/webapp/src/app/sdks/sdkCard.component.ts b/webapp/src/app/sdks/sdkCard.component.ts
new file mode 100644
index 0000000..3256a0b
--- /dev/null
+++ b/webapp/src/app/sdks/sdkCard.component.ts
@@ -0,0 +1,55 @@
+import { Component, Input } from '@angular/core';
+import { ISdk } from "../services/sdk.service";
+
+@Component({
+ selector: 'sdk-card',
+ template: `
+ <div class="row">
+ <div class="col-xs-12">
+ <div class="text-right" role="group">
+ <button disabled class="btn btn-link" (click)="delete(sdk)"><span class="fa fa-trash fa-size-x2"></span></button>
+ </div>
+ </div>
+ </div>
+
+ <table class="table table-striped">
+ <tbody>
+ <tr>
+ <th><span class="fa fa-fw fa-id-badge"></span>&nbsp;<span>SDK ID</span></th>
+ <td>{{ sdk.id }}</td>
+ </tr>
+ <tr>
+ <th><span class="fa fa-fw fa-user"></span>&nbsp;<span>Profile</span></th>
+ <td>{{ sdk.profile }}</td>
+ </tr>
+ <tr>
+ <th><span class="fa fa-fw fa-tasks"></span>&nbsp;<span>Architecture</span></th>
+ <td>{{ sdk.arch }}</td>
+ </tr>
+ <tr>
+ <th><span class="fa fa-fw fa-code-fork"></span>&nbsp;<span>Version</span></th>
+ <td>{{ sdk.version }}</td>
+ </tr>
+ <tr>
+ <th><span class="fa fa-fw fa-folder-open-o"></span>&nbsp;<span>Sdk path</span></th>
+ <td>{{ sdk.path}}</td>
+ </tr>
+
+ </tbody>
+ </table >
+ `,
+ styleUrls: ['./app/config/config.component.css']
+})
+
+export class SdkCardComponent {
+
+ @Input() sdk: ISdk;
+
+ constructor() { }
+
+
+ delete(sdk: ISdk) {
+ // Not supported for now
+ }
+
+}
diff --git a/webapp/src/app/sdks/sdkSelectDropdown.component.ts b/webapp/src/app/sdks/sdkSelectDropdown.component.ts
new file mode 100644
index 0000000..a2fe37a
--- /dev/null
+++ b/webapp/src/app/sdks/sdkSelectDropdown.component.ts
@@ -0,0 +1,48 @@
+import { Component, Input } from "@angular/core";
+
+import { ISdk, SdkService } from "../services/sdk.service";
+
+@Component({
+ selector: 'sdk-select-dropdown',
+ template: `
+ <div class="btn-group" dropdown *ngIf="curSdk" >
+ <button dropdownToggle type="button" class="btn btn-primary dropdown-toggle" style="width: 20em;">
+ {{curSdk.name}} <span class="caret" style="float: right; margin-top: 8px;"></span>
+ </button>
+ <ul *dropdownMenu class="dropdown-menu" role="menu">
+ <li role="menuitem"><a class="dropdown-item" *ngFor="let sdk of sdks" (click)="select(sdk)">
+ {{sdk.name}}</a>
+ </li>
+ </ul>
+ </div>
+ `
+})
+export class SdkSelectDropdownComponent {
+
+ // FIXME investigate to understand why not working with sdks as input
+ // <sdk-select-dropdown [sdks]="(sdks$ | async)"></sdk-select-dropdown>
+ //@Input() sdks: ISdk[];
+ sdks: ISdk[];
+
+ curSdk: ISdk;
+
+ constructor(private sdkSvr: SdkService) { }
+
+ ngOnInit() {
+ this.curSdk = this.sdkSvr.getCurrent();
+ this.sdkSvr.Sdks$.subscribe((s) => {
+ if (s) {
+ this.sdks = s;
+ if (this.curSdk === null || s.indexOf(this.curSdk) === -1) {
+ this.sdkSvr.setCurrent(this.curSdk = s.length ? s[0] : null);
+ }
+ }
+ });
+ }
+
+ select(s) {
+ this.sdkSvr.setCurrent(this.curSdk = s);
+ }
+}
+
+
diff --git a/webapp/src/app/sdks/sdksListAccordion.component.ts b/webapp/src/app/sdks/sdksListAccordion.component.ts
new file mode 100644
index 0000000..9d5f7e9
--- /dev/null
+++ b/webapp/src/app/sdks/sdksListAccordion.component.ts
@@ -0,0 +1,26 @@
+import { Component, Input } from "@angular/core";
+
+import { ISdk } from "../services/sdk.service";
+
+@Component({
+ selector: 'sdks-list-accordion',
+ template: `
+ <accordion>
+ <accordion-group #group *ngFor="let sdk of sdks">
+ <div accordion-heading>
+ {{ sdk.name }}
+ <i class="pull-right float-xs-right fa"
+ [ngClass]="{'fa-chevron-down': group.isOpen, 'fa-chevron-right': !group.isOpen}"></i>
+ </div>
+ <sdk-card [sdk]="sdk"></sdk-card>
+ </accordion-group>
+ </accordion>
+ `
+})
+export class SdksListAccordionComponent {
+
+ @Input() sdks: ISdk[];
+
+}
+
+
diff --git a/webapp/src/app/services/alert.service.ts b/webapp/src/app/services/alert.service.ts
new file mode 100644
index 0000000..2978e84
--- /dev/null
+++ b/webapp/src/app/services/alert.service.ts
@@ -0,0 +1,67 @@
+import { Injectable, SecurityContext } from '@angular/core';
+import { DomSanitizer } from '@angular/platform-browser';
+import { Observable } from 'rxjs/Observable';
+import { Subject } from 'rxjs/Subject';
+
+
+export type AlertType = "danger" | "warning" | "info" | "success";
+
+export interface IAlert {
+ type: AlertType;
+ msg: string;
+ show?: boolean;
+ dismissible?: boolean;
+ dismissTimeout?: number; // close alert after this time (in seconds)
+ id?: number;
+}
+
+@Injectable()
+export class AlertService {
+ public alerts: Observable<IAlert[]>;
+
+ private _alerts: IAlert[];
+ private alertsSubject = <Subject<IAlert[]>>new Subject();
+ private uid = 0;
+ private defaultDissmissTmo = 5; // in seconds
+
+ constructor(private sanitizer: DomSanitizer) {
+ this.alerts = this.alertsSubject.asObservable();
+ this._alerts = [];
+ this.uid = 0;
+ }
+
+ public error(msg: string, dismissTime?: number) {
+ this.add({
+ type: "danger", msg: msg, dismissible: true, dismissTimeout: dismissTime
+ });
+ }
+
+ public warning(msg: string, dismissible?: boolean) {
+ this.add({ type: "warning", msg: msg, dismissible: true, dismissTimeout: (dismissible ? this.defaultDissmissTmo : 0) });
+ }
+
+ public info(msg: string) {
+ this.add({ type: "info", msg: msg, dismissible: true, dismissTimeout: this.defaultDissmissTmo });
+ }
+
+ public add(al: IAlert) {
+ let msg = String(al.msg).replace("\n", "<br>");
+ this._alerts.push({
+ show: true,
+ type: al.type,
+ msg: this.sanitizer.sanitize(SecurityContext.HTML, msg),
+ dismissible: al.dismissible || true,
+ dismissTimeout: (al.dismissTimeout * 1000) || 0,
+ id: this.uid,
+ });
+ this.uid += 1;
+ this.alertsSubject.next(this._alerts);
+ }
+
+ public del(al: IAlert) {
+ let idx = this._alerts.findIndex((a) => a.id === al.id);
+ if (idx > -1) {
+ this._alerts.splice(idx, 1);
+ }
+ }
+}
diff --git a/webapp/src/app/services/config.service.ts b/webapp/src/app/services/config.service.ts
new file mode 100644
index 0000000..bbe2fb8
--- /dev/null
+++ b/webapp/src/app/services/config.service.ts
@@ -0,0 +1,60 @@
+import { Injectable } from '@angular/core';
+import { CookieService } from 'ngx-cookie';
+import { Observable } from 'rxjs/Observable';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+
+import { AlertService, IAlert } from "../services/alert.service";
+import { UtilsService } from "../services/utils.service";
+
+export interface IConfig {
+ projectsRootDir: string;
+}
+
+@Injectable()
+export class ConfigService {
+
+ public Conf$: Observable<IConfig>;
+
+ private confSubject: BehaviorSubject<IConfig>;
+ private confStore: IConfig;
+
+ constructor(private _window: Window,
+ private cookie: CookieService,
+ private alert: AlertService,
+ private utils: UtilsService,
+ ) {
+ this.load();
+ this.confSubject = <BehaviorSubject<IConfig>>new BehaviorSubject(this.confStore);
+ this.Conf$ = this.confSubject.asObservable();
+ }
+
+ // Load config
+ load() {
+ // Try to retrieve previous config from cookie
+ let cookConf = this.cookie.getObject("xds-config");
+ if (cookConf != null) {
+ this.confStore = <IConfig>cookConf;
+ } else {
+ // Set default config
+ this.confStore = {
+ projectsRootDir: "",
+ //projects: []
+ };
+ }
+ }
+
+ // Save config into cookie
+ save() {
+ // Notify subscribers
+ this.confSubject.next(Object.assign({}, this.confStore));
+
+ // Don't save projects in cookies (too big!)
+ let cfg = Object.assign({}, this.confStore);
+ this.cookie.putObject("xds-config", cfg);
+ }
+
+ set projectsRootDir(p: string) {
+ this.confStore.projectsRootDir = p;
+ this.save();
+ }
+}
diff --git a/webapp/src/app/services/project.service.ts b/webapp/src/app/services/project.service.ts
new file mode 100644
index 0000000..53adc80
--- /dev/null
+++ b/webapp/src/app/services/project.service.ts
@@ -0,0 +1,199 @@
+import { Injectable, SecurityContext } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+
+import { XDSAgentService, IXDSProjectConfig } from "../services/xdsagent.service";
+
+export enum ProjectType {
+ UNSET = "",
+ NATIVE_PATHMAP = "PathMap",
+ SYNCTHING = "CloudSync"
+}
+
+export var ProjectTypes = [
+ { value: ProjectType.NATIVE_PATHMAP, display: "Path mapping" },
+ { value: ProjectType.SYNCTHING, display: "Cloud Sync" }
+];
+
+export var ProjectStatus = {
+ ErrorConfig: "ErrorConfig",
+ Disable: "Disable",
+ Enable: "Enable",
+ Pause: "Pause",
+ Syncing: "Syncing"
+};
+
+export interface IProject {
+ id?: string;
+ serverId: string;
+ label: string;
+ pathClient: string;
+ pathServer?: string;
+ type: ProjectType;
+ status?: string;
+ isInSync?: boolean;
+ isUsable?: boolean;
+ serverPrjDef?: IXDSProjectConfig;
+ isExpanded?: boolean;
+ visible?: boolean;
+ defaultSdkID?: string;
+}
+
+@Injectable()
+export class ProjectService {
+ public Projects$: Observable<IProject[]>;
+
+ private _prjsList: IProject[] = [];
+ private current: IProject;
+ private prjsSubject = <BehaviorSubject<IProject[]>>new BehaviorSubject(this._prjsList);
+
+ constructor(private xdsSvr: XDSAgentService) {
+ this.current = null;
+ this.Projects$ = this.prjsSubject.asObservable();
+
+ 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 => {
+ let 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));
+ }
+ });
+
+ // 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) {
+ let 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);
+ }
+ });
+
+ }
+
+ public setCurrent(s: IProject) {
+ this.current = s;
+ }
+
+ public getCurrent(): IProject {
+ return this.current;
+ }
+
+ public getCurrentId(): string {
+ if (this.current && this.current.id) {
+ return this.current.id;
+ }
+ return "";
+ }
+
+ Add(prj: IProject): Observable<IProject> {
+ let xdsPrj: IXDSProjectConfig = {
+ id: "",
+ serverId: prj.serverId,
+ label: prj.label || "",
+ clientPath: prj.pathClient.trim(),
+ serverPath: prj.pathServer,
+ type: prj.type,
+ defaultSdkID: prj.defaultSdkID,
+ };
+ // Send config to XDS server
+ return this.xdsSvr.addProject(xdsPrj)
+ .map(xdsPrj => this._convToIProject(xdsPrj));
+ }
+
+ Delete(prj: IProject): Observable<IProject> {
+ let idx = this._getProjectIdx(prj.id);
+ let delPrj = prj;
+ if (idx === -1) {
+ throw new Error("Invalid project id (id=" + prj.id + ")");
+ }
+ return this.xdsSvr.deleteProject(prj.id)
+ .map(res => { return delPrj; });
+ }
+
+ Sync(prj: IProject): Observable<string> {
+ let idx = this._getProjectIdx(prj.id);
+ if (idx === -1) {
+ throw new Error("Invalid project id (id=" + prj.id + ")");
+ }
+ return this.xdsSvr.syncProject(prj.id);
+ }
+
+ private _isUsableProject(p) {
+ return p && p.isInSync &&
+ (p.status === ProjectStatus.Enable) &&
+ (p.status !== ProjectStatus.Syncing);
+ }
+
+ private _getProjectIdx(id: string): number {
+ return this._prjsList.findIndex((item) => item.id === id);
+ }
+
+ private _convToIProject(rPrj: IXDSProjectConfig): IProject {
+ // Convert XDSFolderConfig to IProject
+ let pp: IProject = {
+ id: rPrj.id,
+ serverId: rPrj.serverId,
+ label: rPrj.label,
+ pathClient: rPrj.clientPath,
+ pathServer: rPrj.serverPath,
+ type: rPrj.type,
+ status: rPrj.status,
+ isInSync: rPrj.isInSync,
+ isUsable: this._isUsableProject(rPrj),
+ defaultSdkID: rPrj.defaultSdkID,
+ serverPrjDef: Object.assign({}, rPrj), // do a copy
+ };
+ return pp;
+ }
+
+ private _addProject(rPrj: IXDSProjectConfig, noNext?: boolean): IProject {
+
+ // Convert XDSFolderConfig to IProject
+ let pp = this._convToIProject(rPrj);
+
+ // add new project
+ this._prjsList.push(pp);
+
+ // sort project array
+ this._prjsList.sort((a, b) => {
+ if (a.label < b.label) {
+ return -1;
+ }
+ if (a.label > b.label) {
+ return 1;
+ }
+ return 0;
+ });
+
+ if (!noNext) {
+ this.prjsSubject.next(Object.assign([], this._prjsList));
+ }
+
+ return pp;
+ }
+}
diff --git a/webapp/src/app/services/sdk.service.ts b/webapp/src/app/services/sdk.service.ts
new file mode 100644
index 0000000..6d8a5f6
--- /dev/null
+++ b/webapp/src/app/services/sdk.service.ts
@@ -0,0 +1,54 @@
+import { Injectable, SecurityContext } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+
+import { XDSAgentService } from "../services/xdsagent.service";
+
+export interface ISdk {
+ id: string;
+ profile: string;
+ version: string;
+ arch: number;
+ path: 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 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 "";
+ }
+}
diff --git a/webapp/src/app/services/utils.service.ts b/webapp/src/app/services/utils.service.ts
new file mode 100644
index 0000000..84b9ab6
--- /dev/null
+++ b/webapp/src/app/services/utils.service.ts
@@ -0,0 +1,33 @@
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class UtilsService {
+ constructor() { }
+
+ getOSName(lowerCase?: boolean): string {
+ var checkField = function (ff) {
+ if (ff.indexOf("Linux") !== -1) {
+ return "Linux";
+ } else if (ff.indexOf("Win") !== -1) {
+ return "Windows";
+ } else if (ff.indexOf("Mac") !== -1) {
+ return "MacOS";
+ } else if (ff.indexOf("X11") !== -1) {
+ return "UNIX";
+ }
+ return "";
+ };
+
+ let OSName = checkField(navigator.platform);
+ if (OSName === "") {
+ OSName = checkField(navigator.appVersion);
+ }
+ if (OSName === "") {
+ OSName = "Unknown OS";
+ }
+ if (lowerCase) {
+ return OSName.toLowerCase();
+ }
+ return OSName;
+ }
+} \ No newline at end of file
diff --git a/webapp/src/app/services/xdsagent.service.ts b/webapp/src/app/services/xdsagent.service.ts
new file mode 100644
index 0000000..fd84ccb
--- /dev/null
+++ b/webapp/src/app/services/xdsagent.service.ts
@@ -0,0 +1,401 @@
+import { Injectable } from '@angular/core';
+import { Http, Headers, RequestOptionsArgs, Response } from '@angular/http';
+import { Location } from '@angular/common';
+import { Observable } from 'rxjs/Observable';
+import { Subject } from 'rxjs/Subject';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+import * as io from 'socket.io-client';
+
+import { AlertService } from './alert.service';
+import { ISdk } from './sdk.service';
+import { ProjectType} from "./project.service";
+
+// Import RxJs required methods
+import 'rxjs/add/operator/map';
+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';
+
+
+export interface IXDSConfigProject {
+ id: string;
+ path: string;
+ clientSyncThingID: string;
+ type: string;
+ label?: string;
+ defaultSdkID?: string;
+}
+
+interface IXDSBuilderConfig {
+ ip: string;
+ port: string;
+ syncThingID: string;
+}
+
+export interface IXDSProjectConfig {
+ id: string;
+ serverId: string;
+ label: string;
+ clientPath: string;
+ serverPath?: string;
+ type: ProjectType;
+ status?: string;
+ isInSync?: boolean;
+ defaultSdkID: string;
+}
+
+export interface IXDSVer {
+ id: string;
+ version: string;
+ apiVersion: string;
+ gitTag: string;
+}
+
+export interface IXDSVersions {
+ client: IXDSVer;
+ servers: IXDSVer[];
+}
+
+export interface IXDServerCfg {
+ id: string;
+ url: string;
+ apiUrl: string;
+ partialUrl: string;
+ connRetry: number;
+ connected: boolean;
+}
+
+export interface IXDSConfig {
+ servers: IXDServerCfg[];
+}
+
+export interface ISdkMessage {
+ wsID: string;
+ msgType: string;
+ data: any;
+}
+
+export interface ICmdOutput {
+ cmdID: string;
+ timestamp: string;
+ stdout: string;
+ stderr: string;
+}
+
+export interface ICmdExit {
+ cmdID: string;
+ timestamp: string;
+ code: number;
+ error: string;
+}
+
+export interface IAgentStatus {
+ WS_connected: boolean;
+}
+
+
+@Injectable()
+export class XDSAgentService {
+
+ public XdsConfig$: Observable<IXDSConfig>;
+ public Status$: Observable<IAgentStatus>;
+ public ProjectState$ = <Subject<IXDSProjectConfig>>new Subject();
+ public CmdOutput$ = <Subject<ICmdOutput>>new Subject();
+ public CmdExit$ = <Subject<ICmdExit>>new Subject();
+
+ private baseUrl: string;
+ private wsUrl: string;
+ private _config = <IXDSConfig>{ servers: [] };
+ private _status = { WS_connected: false };
+
+ private configSubject = <BehaviorSubject<IXDSConfig>>new BehaviorSubject(this._config);
+ private statusSubject = <BehaviorSubject<IAgentStatus>>new BehaviorSubject(this._status);
+
+ private socket: SocketIOClient.Socket;
+
+ constructor(private http: Http, private _window: Window, private alert: AlertService) {
+
+ this.XdsConfig$ = this.configSubject.asObservable();
+ this.Status$ = this.statusSubject.asObservable();
+
+ this.baseUrl = this._window.location.origin + '/api/v1';
+
+ let re = this._window.location.origin.match(/http[s]?:\/\/([^\/]*)[\/]?/);
+ if (re === null || re.length < 2) {
+ console.error('ERROR: cannot determine Websocket url');
+ } else {
+ this.wsUrl = 'ws://' + re[1];
+ this._handleIoSocket();
+ this._RegisterEvents();
+ }
+ }
+
+ private _WSState(sts: boolean) {
+ this._status.WS_connected = sts;
+ this.statusSubject.next(Object.assign({}, this._status));
+
+ // Update XDS config including XDS Server list when connected
+ if (sts) {
+ this.getConfig().subscribe(c => {
+ this._config = c;
+ this.configSubject.next(
+ Object.assign({ servers: [] }, this._config)
+ );
+ });
+ }
+ }
+
+ private _handleIoSocket() {
+ this.socket = io(this.wsUrl, { transports: ['websocket'] });
+
+ this.socket.on('connect_error', (res) => {
+ this._WSState(false);
+ console.error('XDS Agent WebSocket Connection error !');
+ });
+
+ this.socket.on('connect', (res) => {
+ this._WSState(true);
+ });
+
+ this.socket.on('disconnection', (res) => {
+ this._WSState(false);
+ this.alert.error('WS disconnection: ' + res);
+ });
+
+ this.socket.on('error', (err) => {
+ console.error('WS error:', err);
+ });
+
+ this.socket.on('make:output', data => {
+ this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data));
+ });
+
+ this.socket.on('make:exit', data => {
+ this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
+ });
+
+ this.socket.on('exec:output', data => {
+ this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data));
+ });
+
+ this.socket.on('exec:exit', data => {
+ this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
+ });
+
+ // Events
+ // (project-add and project-delete events are managed by project.service)
+ this.socket.on('event:server-config', ev => {
+ if (ev && ev.data) {
+ let cfg: IXDServerCfg = ev.data;
+ let idx = this._config.servers.findIndex(el => el.id === cfg.id);
+ if (idx >= 0) {
+ this._config.servers[idx] = Object.assign({}, cfg);
+ }
+ this.configSubject.next(Object.assign({}, this._config));
+ }
+ });
+
+ this.socket.on('event:project-state-change', ev => {
+ if (ev && ev.data) {
+ this.ProjectState$.next(Object.assign({}, ev.data));
+ }
+ });
+
+ }
+
+ /**
+ ** Events
+ ***/
+ addEventListener(ev: string, fn: Function): SocketIOClient.Emitter {
+ return this.socket.addEventListener(ev, fn);
+ }
+
+ /**
+ ** Misc / Version
+ ***/
+ getVersion(): Observable<IXDSVersions> {
+ return this._get('/version');
+ }
+
+ /***
+ ** Config
+ ***/
+ getConfig(): Observable<IXDSConfig> {
+ return this._get('/config');
+ }
+
+ setConfig(cfg: IXDSConfig): Observable<IXDSConfig> {
+ return this._post('/config', cfg);
+ }
+
+ setServerRetry(serverID: string, r: number) {
+ let svr = this._getServer(serverID);
+ if (!svr) {
+ return Observable.of([]);
+ }
+
+ svr.connRetry = r;
+ this.setConfig(this._config).subscribe(
+ newCfg => {
+ this._config = newCfg;
+ this.configSubject.next(Object.assign({}, this._config));
+ },
+ err => {
+ this.alert.error(err);
+ }
+ );
+ }
+
+ setServerUrl(serverID: string, url: string) {
+ let svr = this._getServer(serverID);
+ if (!svr) {
+ return Observable.of([]);
+ }
+ svr.url = url;
+ this.setConfig(this._config).subscribe(
+ newCfg => {
+ this._config = newCfg;
+ this.configSubject.next(Object.assign({}, this._config));
+ },
+ err => {
+ this.alert.error(err);
+ }
+ );
+ }
+
+ /***
+ ** SDKs
+ ***/
+ getSdks(serverID: string): Observable<ISdk[]> {
+ let svr = this._getServer(serverID);
+ if (!svr || !svr.connected) {
+ return Observable.of([]);
+ }
+
+ return this._get(svr.partialUrl + '/sdks');
+ }
+
+ /***
+ ** Projects
+ ***/
+ getProjects(): Observable<IXDSProjectConfig[]> {
+ return this._get('/projects');
+ }
+
+ addProject(cfg: IXDSProjectConfig): Observable<IXDSProjectConfig> {
+ return this._post('/project', cfg);
+ }
+
+ deleteProject(id: string): Observable<IXDSProjectConfig> {
+ return this._delete('/project/' + id);
+ }
+
+ syncProject(id: string): Observable<string> {
+ return this._post('/project/sync/' + id, {});
+ }
+
+ /***
+ ** Exec
+ ***/
+ exec(prjID: string, dir: string, cmd: string, sdkid?: string, args?: string[], env?: string[]): Observable<any> {
+ return this._post('/exec',
+ {
+ id: prjID,
+ rpath: dir,
+ cmd: cmd,
+ sdkID: sdkid || "",
+ args: args || [],
+ env: env || [],
+ });
+ }
+
+ make(prjID: string, dir: string, sdkid?: string, args?: string[], env?: string[]): Observable<any> {
+ // SEB TODO add serverID
+ return this._post('/make',
+ {
+ id: prjID,
+ rpath: dir,
+ sdkID: sdkid,
+ args: args || [],
+ env: env || [],
+ });
+ }
+
+
+ /**
+ ** Private functions
+ ***/
+
+ private _RegisterEvents() {
+ // Register to all existing events
+ this._post('/events/register', { "name": "all" })
+ .subscribe(
+ res => { },
+ error => {
+ this.alert.error("ERROR while registering to all events: ", error);
+ }
+ );
+ }
+
+ private _getServer(ID: string): IXDServerCfg {
+ let svr = this._config.servers.filter(item => item.id === ID);
+ if (svr.length < 1) {
+ return null;
+ }
+ return svr[0];
+ }
+
+ private _attachAuthHeaders(options?: any) {
+ options = options || {};
+ let headers = options.headers || new Headers();
+ // headers.append('Authorization', 'Basic ' + btoa('username:password'));
+ headers.append('Accept', 'application/json');
+ headers.append('Content-Type', 'application/json');
+ // headers.append('Access-Control-Allow-Origin', '*');
+
+ options.headers = headers;
+ return options;
+ }
+
+ private _get(url: string): Observable<any> {
+ return this.http.get(this.baseUrl + url, this._attachAuthHeaders())
+ .map((res: Response) => res.json())
+ .catch(this._decodeError);
+ }
+ private _post(url: string, body: any): Observable<any> {
+ return this.http.post(this.baseUrl + url, JSON.stringify(body), this._attachAuthHeaders())
+ .map((res: Response) => res.json())
+ .catch((error) => {
+ return this._decodeError(error);
+ });
+ }
+ private _delete(url: string): Observable<any> {
+ return this.http.delete(this.baseUrl + url, this._attachAuthHeaders())
+ .map((res: Response) => res.json())
+ .catch(this._decodeError);
+ }
+
+ private _decodeError(err: any) {
+ let e: string;
+ if (err instanceof Response) {
+ const body = err.json() || 'Agent error';
+ e = body.error || JSON.stringify(body);
+ if (!e || e === "" || e === '{"isTrusted":true}') {
+ e = `${err.status} - ${err.statusText || 'Unknown error'}`;
+ }
+ } else if (typeof err === "object") {
+ if (err.statusText) {
+ e = err.statusText;
+ } else if (err.error) {
+ e = String(err.error);
+ } else {
+ e = JSON.stringify(err);
+ }
+ } else {
+ e = err.message ? err.message : err.toString();
+ }
+ return Observable.throw(e);
+ }
+}
diff --git a/webapp/src/index.html b/webapp/src/index.html
new file mode 100644
index 0000000..290b4be
--- /dev/null
+++ b/webapp/src/index.html
@@ -0,0 +1,50 @@
+<html>
+
+<head>
+ <title>
+ XDS Dashboard
+ </title>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+
+ <link rel="icon" type="image/x-icon" href="assets/favicon.ico">
+
+ <!-- TODO cleanup
+ <link rel="stylesheet" href="lib/foundation-sites/dist/css/foundation.min.css">
+ -->
+ <link <link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
+
+ <link rel="stylesheet" href="lib/font-awesome/css/font-awesome.min.css">
+ <link rel="stylesheet" href="lib/font-awesome-animation/dist/font-awesome-animation.min.css">
+
+ <!-- 1. Load libraries -->
+ <!-- Polyfill(s) for older browsers -->
+ <script src="lib/core-js/client/shim.min.js"></script>
+
+ <script src="lib/zone.js/dist/zone.js"></script>
+ <script src="lib/reflect-metadata/Reflect.js"></script>
+ <script src="lib/systemjs/dist/system.src.js"></script>
+
+ <!-- 2. Configure SystemJS -->
+ <script src="systemjs.config.js"></script>
+ <script>
+ System.import('app')
+ .then(null, console.error.bind(console));
+ </script>
+
+ <script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
+
+</head>
+
+<!-- 3. Display the application -->
+
+<body style="padding-top: 70px;"> <!-- padding needed due to fixed navbar -->
+ <app>
+ <div style="text-align:center; position:absolute; top:50%; width:100%; transform:translate(0,-50%);">
+ <img id="logo-iot" src="assets/images/iot-bzh-logo-small.png">
+ <br> Loading...
+ <i class="fa fa-spinner fa-spin fa-fw"></i>
+ </div>
+ </app>
+</body>
+
+</html>
diff --git a/webapp/src/systemjs.config.js b/webapp/src/systemjs.config.js
new file mode 100644
index 0000000..15c52ba
--- /dev/null
+++ b/webapp/src/systemjs.config.js
@@ -0,0 +1,69 @@
+(function (global) {
+ System.config({
+ paths: {
+ // paths serve as alias
+ 'npm:': 'lib/'
+ },
+ bundles: {
+ "npm:rxjs-system-bundle/Rx.system.min.js": [
+ "rxjs",
+ "rxjs/*",
+ "rxjs/operator/*",
+ "rxjs/observable/*",
+ "rxjs/scheduler/*",
+ "rxjs/symbol/*",
+ "rxjs/add/operator/*",
+ "rxjs/add/observable/*",
+ "rxjs/util/*"
+ ]
+ },
+ // map tells the System loader where to look for things
+ map: {
+ // our app is within the app folder
+ app: 'app',
+ // angular bundles
+ '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
+ '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
+ '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
+ '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
+ '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
+ '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
+ '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
+ '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
+ 'ngx-cookie': 'npm:ngx-cookie/bundles/ngx-cookie.umd.js',
+ // ng2-bootstrap
+ 'moment': 'npm:moment',
+ 'ngx-bootstrap/alert': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ 'ngx-bootstrap/modal': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ 'ngx-bootstrap/accordion': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ 'ngx-bootstrap/carousel': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ 'ngx-bootstrap/popover': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ 'ngx-bootstrap/dropdown': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ 'ngx-bootstrap/collapse': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ // other libraries
+ 'socket.io-client': 'npm:socket.io-client/dist/socket.io.min.js'
+ },
+ // packages tells the System loader how to load when no filename and/or no extension
+ packages: {
+ 'app': {
+ main: './main.js',
+ defaultExtension: 'js'
+ },
+ 'rxjs': {
+ defaultExtension: false
+ },
+ 'socket.io-client': {
+ defaultExtension: 'js'
+ },
+ 'ngx-bootstrap': {
+ format: 'cjs',
+ main: 'bundles/ng2-bootstrap.umd.js',
+ defaultExtension: 'js'
+ },
+ 'moment': {
+ main: 'moment.js',
+ defaultExtension: 'js'
+ }
+ }
+ });
+})(this);
diff --git a/webapp/tsconfig.json b/webapp/tsconfig.json
new file mode 100644
index 0000000..9bad681
--- /dev/null
+++ b/webapp/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "outDir": "dist/app",
+ "target": "es5",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "removeComments": false,
+ "noImplicitAny": false,
+ "noStrictGenericChecks": true // better to switch to RxJS 5.4.2 ; workaround https://stackoverflow.com/questions/44810195/how-do-i-get-around-this-subject-incorrectly-extends-observable-error-in-types
+ },
+ "exclude": [
+ "gulpfile.ts",
+ "node_modules"
+ ]
+}
diff --git a/webapp/tslint.json b/webapp/tslint.json
new file mode 100644
index 0000000..15969a4
--- /dev/null
+++ b/webapp/tslint.json
@@ -0,0 +1,55 @@
+{
+ "rules": {
+ "class-name": true,
+ "curly": true,
+ "eofline": false,
+ "forin": true,
+ "indent": [
+ true,
+ 4
+ ],
+ "label-position": true,
+ "max-line-length": [
+ true,
+ 140
+ ],
+ "no-arg": true,
+ "no-bitwise": true,
+ "no-console": [
+ true,
+ "info",
+ "time",
+ "timeEnd",
+ "trace"
+ ],
+ "no-construct": true,
+ "no-debugger": true,
+ "no-duplicate-variable": true,
+ "no-empty": false,
+ "no-eval": true,
+ "no-string-literal": false,
+ "no-trailing-whitespace": true,
+ "no-use-before-declare": true,
+ "one-line": [
+ true,
+ "check-open-brace",
+ "check-catch",
+ "check-else",
+ "check-whitespace"
+ ],
+ "radix": true,
+ "semicolon": true,
+ "triple-equals": [
+ true,
+ "allow-null-check"
+ ],
+ "variable-name": false,
+ "whitespace": [
+ true,
+ "check-branch",
+ "check-decl",
+ "check-operator",
+ "check-separator"
+ ]
+ }
+}
diff --git a/webapp/tslint.prod.json b/webapp/tslint.prod.json
new file mode 100644
index 0000000..aa64c7f
--- /dev/null
+++ b/webapp/tslint.prod.json
@@ -0,0 +1,56 @@
+{
+ "rules": {
+ "class-name": true,
+ "curly": true,
+ "eofline": false,
+ "forin": true,
+ "indent": [
+ true,
+ 4
+ ],
+ "label-position": true,
+ "max-line-length": [
+ true,
+ 140
+ ],
+ "no-arg": true,
+ "no-bitwise": true,
+ "no-console": [
+ true,
+ "debug",
+ "info",
+ "time",
+ "timeEnd",
+ "trace"
+ ],
+ "no-construct": true,
+ "no-debugger": true,
+ "no-duplicate-variable": true,
+ "no-empty": false,
+ "no-eval": true,
+ "no-string-literal": false,
+ "no-trailing-whitespace": true,
+ "no-use-before-declare": true,
+ "one-line": [
+ true,
+ "check-open-brace",
+ "check-catch",
+ "check-else",
+ "check-whitespace"
+ ],
+ "radix": true,
+ "semicolon": true,
+ "triple-equals": [
+ true,
+ "allow-null-check"
+ ],
+ "variable-name": false,
+ "whitespace": [
+ true,
+ "check-branch",
+ "check-decl",
+ "check-operator",
+ "check-separator"
+ ]
+ }
+}
diff --git a/webapp/typings.json b/webapp/typings.json
new file mode 100644
index 0000000..23c6a41
--- /dev/null
+++ b/webapp/typings.json
@@ -0,0 +1,11 @@
+{
+ "dependencies": {},
+ "devDependencies": {},
+ "globalDependencies": {
+ "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654",
+ "socket.io-client": "registry:dt/socket.io-client#1.4.4+20160317120654"
+ },
+ "globalDevDependencies": {
+ "jasmine": "registry:dt/jasmine#2.2.0+20160505161446"
+ }
+}