thingsboard-developers

ThingsBoard Web UI Microservice.

10/2/2018 11:42:09 AM

Changes

msa/pom.xml 1(+1 -0)

msa/web-ui/pom.xml 372(+372 -0)

msa/web-ui/server.js 129(+129 -0)

ui/package.json 2(+1 -1)

Details

diff --git a/msa/docker/docker-compose.yml b/msa/docker/docker-compose.yml
index a077c0e..727936c 100644
--- a/msa/docker/docker-compose.yml
+++ b/msa/docker/docker-compose.yml
@@ -29,7 +29,7 @@ services:
     environment:
       KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
       KAFKA_LISTENERS: INSIDE://:9093,OUTSIDE://:9092
-      KAFKA_ADVERTISED_LISTENERS: INSIDE://:9093,OUTSIDE://${KAFKA_HOSTNAME}:9092
+      KAFKA_ADVERTISED_LISTENERS: INSIDE://:9093,OUTSIDE://${EXTERNAL_HOSTNAME}:9092
       KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT
       KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE
       KAFKA_CREATE_TOPICS: "${KAFKA_TOPICS}"
@@ -44,3 +44,14 @@ services:
       - tb-js-executor.env
     depends_on:
       - kafka
+  tb-web-ui:
+    image: "local-maven-build/tb-web-ui:latest"
+    ports:
+      - "8090:8090"
+    environment:
+      HTTP_BIND_ADDRESS: 0.0.0.0
+      HTTP_BIND_PORT: 8090
+      TB_HOST: ${EXTERNAL_HOSTNAME}
+      TB_PORT: 8080
+    env_file:
+      - tb-web-ui.env
diff --git a/msa/docker/tb-web-ui.env b/msa/docker/tb-web-ui.env
new file mode 100644
index 0000000..8d6157b
--- /dev/null
+++ b/msa/docker/tb-web-ui.env
@@ -0,0 +1,9 @@
+
+HTTP_BIND_ADDRESS=0.0.0.0
+HTTP_BIND_PORT=8090
+TB_HOST=localhost
+TB_PORT=8080
+LOGGER_LEVEL=debug
+LOG_FOLDER=logs
+LOGGER_FILENAME=tb-web-ui-%DATE%.log
+DOCKER_MODE=true
\ No newline at end of file

msa/pom.xml 1(+1 -0)

diff --git a/msa/pom.xml b/msa/pom.xml
index 85d45a1..3241d0c 100644
--- a/msa/pom.xml
+++ b/msa/pom.xml
@@ -36,6 +36,7 @@
 
     <modules>
         <module>js-executor</module>
+        <module>web-ui</module>
     </modules>
 
 </project>
diff --git a/msa/web-ui/.gitignore b/msa/web-ui/.gitignore
new file mode 100644
index 0000000..c5cfd3d
--- /dev/null
+++ b/msa/web-ui/.gitignore
@@ -0,0 +1,31 @@
+*.toDelete
+output/**
+*.class
+*~
+*.iml
+*/.idea/**
+.idea/**
+.idea
+*.log
+*.log.[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]
+*/.classpath
+.classpath
+*/.project
+.project
+.cache/**
+target/
+logs/
+build/
+.settings/
+/bin
+bin/
+**/dependency-reduced-pom.xml
+pom.xml.versionsBackup
+.DS_Store
+**/.gradle
+**/local.properties
+**/build
+**/target
+**/.env
+node_modules
+package-lock.json
diff --git a/msa/web-ui/build.gradle b/msa/web-ui/build.gradle
new file mode 100644
index 0000000..7372c0a
--- /dev/null
+++ b/msa/web-ui/build.gradle
@@ -0,0 +1,125 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import org.apache.tools.ant.filters.ReplaceTokens
+
+buildscript {
+    ext {
+        osPackageVersion = "3.8.0"
+    }
+    repositories {
+        jcenter()
+    }
+    dependencies {
+        classpath("com.netflix.nebula:gradle-ospackage-plugin:${osPackageVersion}")
+    }
+}
+
+apply plugin: "nebula.ospackage"
+
+buildDir = projectBuildDir
+version = projectVersion
+distsDirName = "./"
+
+// OS Package plugin configuration
+ospackage {
+    packageName = pkgName
+    version = "${project.version}"
+    release = 1
+    os = LINUX
+    type = BINARY
+
+    into pkgInstallFolder
+
+    user pkgUser
+    permissionGroup pkgUser
+
+    // Copy the executable file
+    from("target/package/linux/bin/${pkgName}") {
+        fileMode 0500
+        into "bin"
+    }
+
+    // Copy the init file
+    from("target/package/linux/init/${pkgName}") {
+        fileMode 0500
+        into "init"
+    }
+
+    // Copy the config files
+    from("target/package/linux/conf") {
+        fileType CONFIG | NOREPLACE
+        fileMode 0754
+        into "conf"
+    }
+
+    // Copy web files
+    from("target/package/linux/web") {
+        into "web"
+    }
+
+}
+
+// Configure our RPM build task
+buildRpm {
+
+    arch = X86_64
+
+    version = projectVersion.replace('-', '')
+    archiveName = "${pkgName}.rpm"
+
+    preInstall file("${buildDir}/control/rpm/preinst")
+    postInstall file("${buildDir}/control/rpm/postinst")
+    preUninstall file("${buildDir}/control/rpm/prerm")
+    postUninstall file("${buildDir}/control/rpm/postrm")
+
+    user pkgUser
+    permissionGroup pkgUser
+
+    // Copy the system unit files
+    from("${buildDir}/control/${pkgName}.service") {
+        addParentDirs = false
+        fileMode 0644
+        into "/usr/lib/systemd/system"
+    }
+
+    directory(pkgLogFolder, 0755)
+    link("/etc/${pkgName}/conf", "${pkgInstallFolder}/conf")
+}
+
+// Same as the buildRpm task
+buildDeb {
+
+    arch = "amd64"
+
+    archiveName = "${pkgName}.deb"
+
+    configurationFile("${pkgInstallFolder}/conf/${pkgName}.conf")
+    configurationFile("${pkgInstallFolder}/conf/custom-environment-variables.yml")
+    configurationFile("${pkgInstallFolder}/conf/default.yml")
+    configurationFile("${pkgInstallFolder}/conf/logger.js")
+
+    preInstall file("${buildDir}/control/deb/preinst")
+    postInstall file("${buildDir}/control/deb/postinst")
+    preUninstall file("${buildDir}/control/deb/prerm")
+    postUninstall file("${buildDir}/control/deb/postrm")
+
+    user pkgUser
+    permissionGroup pkgUser
+
+    directory(pkgLogFolder, 0755)
+    link("/etc/init.d/${pkgName}", "${pkgInstallFolder}/init/${pkgName}")
+    link("/etc/${pkgName}/conf", "${pkgInstallFolder}/conf")
+}
diff --git a/msa/web-ui/config/custom-environment-variables.yml b/msa/web-ui/config/custom-environment-variables.yml
new file mode 100644
index 0000000..9472a50
--- /dev/null
+++ b/msa/web-ui/config/custom-environment-variables.yml
@@ -0,0 +1,30 @@
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+server:
+  # Server bind address
+  address: "HTTP_BIND_ADDRESS"
+  # Server bind port
+  port: "HTTP_BIND_PORT"
+thingsboard:
+  # ThingsBoard node host
+  host: "TB_HOST"
+  # ThingsBoard node port
+  port: "TB_PORT"
+logger:
+  level: "LOGGER_LEVEL"
+  path: "LOG_FOLDER"
+  filename: "LOGGER_FILENAME"
diff --git a/msa/web-ui/config/default.yml b/msa/web-ui/config/default.yml
new file mode 100644
index 0000000..cf27a14
--- /dev/null
+++ b/msa/web-ui/config/default.yml
@@ -0,0 +1,30 @@
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+server:
+  # Server bind address
+  address: "0.0.0.0"
+  # Server bind port
+  port: "8090"
+thingsboard:
+  # ThingsBoard node host
+  host: "localhost"
+  # ThingsBoard node port
+  port: "8080"
+logger:
+  level: "info"
+  path: "logs"
+  filename: "tb-web-ui-%DATE%.log"
diff --git a/msa/web-ui/config/logger.js b/msa/web-ui/config/logger.js
new file mode 100644
index 0000000..695b453
--- /dev/null
+++ b/msa/web-ui/config/logger.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+var config = require('config'),
+    path = require('path'),
+    DailyRotateFile = require('winston-daily-rotate-file');
+
+const { createLogger, format, transports } = require('winston');
+const { combine, timestamp, label, printf, splat } = format;
+
+var loggerTransports = [];
+
+if (process.env.NODE_ENV !== 'production' || process.env.DOCKER_MODE === 'true') {
+    loggerTransports.push(new transports.Console({
+        handleExceptions: true
+    }));
+} else {
+    var filename = path.join(config.get('logger.path'), config.get('logger.filename'));
+    var transport = new (DailyRotateFile)({
+        filename: filename,
+        datePattern: 'YYYY-MM-DD-HH',
+        zippedArchive: true,
+        maxSize: '20m',
+        maxFiles: '14d',
+        handleExceptions: true
+    });
+    loggerTransports.push(transport);
+}
+
+const tbFormat = printf(info => {
+    return `${info.timestamp} [${info.label}] ${info.level.toUpperCase()}: ${info.message}`;
+});
+
+function _logger(moduleLabel) {
+    return createLogger({
+        level: config.get('logger.level'),
+        format:combine(
+            splat(),
+            label({ label: moduleLabel }),
+            timestamp({format: 'YYYY-MM-DD HH:mm:ss,SSS'}),
+            tbFormat
+        ),
+        transports: loggerTransports
+    });
+}
+
+module.exports = _logger;
\ No newline at end of file
diff --git a/msa/web-ui/config/tb-web-ui.conf b/msa/web-ui/config/tb-web-ui.conf
new file mode 100644
index 0000000..d002038
--- /dev/null
+++ b/msa/web-ui/config/tb-web-ui.conf
@@ -0,0 +1,20 @@
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+export NODE_CONFIG_DIR=${pkg.installFolder}/conf
+export LOG_FOLDER=${pkg.logFolder}
+export NODE_ENV=production
+export WEB_FOLDER=${pkg.installFolder}/web
\ No newline at end of file
diff --git a/msa/web-ui/docker/Dockerfile b/msa/web-ui/docker/Dockerfile
new file mode 100644
index 0000000..7a75cc3
--- /dev/null
+++ b/msa/web-ui/docker/Dockerfile
@@ -0,0 +1,26 @@
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+FROM debian:stretch
+
+COPY start-web-ui.sh ${pkg.name}.deb /tmp/
+
+RUN chmod a+x /tmp/*.sh \
+    && mv /tmp/start-web-ui.sh /usr/bin
+
+RUN dpkg -i /tmp/${pkg.name}.deb
+
+CMD ["start-web-ui.sh"]
diff --git a/msa/web-ui/docker/start-web-ui.sh b/msa/web-ui/docker/start-web-ui.sh
new file mode 100755
index 0000000..af7c686
--- /dev/null
+++ b/msa/web-ui/docker/start-web-ui.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+#
+# Copyright © 2016-2018 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+echo "Starting '${project.name}' ..."
+
+CONF_FOLDER="${pkg.installFolder}/conf"
+
+mainfile=${pkg.installFolder}/bin/${pkg.name}
+configfile=${pkg.name}.conf
+identity=${pkg.name}
+
+source "${CONF_FOLDER}/${configfile}"
+
+su -s /bin/sh -c "$mainfile"
diff --git a/msa/web-ui/install.js b/msa/web-ui/install.js
new file mode 100644
index 0000000..63e73db
--- /dev/null
+++ b/msa/web-ui/install.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const fs = require('fs');
+const fse = require('fs-extra');
+const path = require('path');
+
+let _projectRoot = null;
+
+
+(async() => {
+    await fse.move(path.join(projectRoot(), 'target', 'thingsboard-web-ui-linux'),
+                   path.join(targetPackageDir('linux'), 'bin', 'tb-web-ui'),
+                   {overwrite: true});
+    await fse.move(path.join(projectRoot(), 'target', 'thingsboard-web-ui-win.exe'),
+                   path.join(targetPackageDir('windows'), 'bin', 'tb-web-ui.exe'),
+                   {overwrite: true});
+})();
+
+
+function projectRoot() {
+    if (!_projectRoot) {
+        _projectRoot = __dirname;
+    }
+    return _projectRoot;
+}
+
+function targetPackageDir(platform) {
+    return path.join(projectRoot(), 'target', 'package', platform);
+}
diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json
new file mode 100644
index 0000000..f30f2fc
--- /dev/null
+++ b/msa/web-ui/package.json
@@ -0,0 +1,38 @@
+{
+  "name": "thingsboard-web-ui",
+  "private": true,
+  "version": "2.2.0",
+  "description": "ThingsBoard Web UI Microservice",
+  "main": "server.js",
+  "bin": "server.js",
+  "scripts": {
+    "install": "pkg -t node8-linux-x64,node8-win-x64 --out-path ./target . && node install.js",
+    "test": "echo \"Error: no test specified\" && exit 1",
+    "start": "WEB_FOLDER=./target/web nodemon server.js",
+    "start-prod": "NODE_ENV=production nodemon server.js"
+  },
+  "dependencies": {
+    "config": "^1.30.0",
+    "connect-history-api-fallback": "^1.5.0",
+    "express": "^4.16.3",
+    "http": "0.0.0",
+    "http-proxy": "^1.17.0",
+    "js-yaml": "^3.12.0",
+    "winston": "^3.0.0",
+    "winston-daily-rotate-file": "^3.2.1"
+  },
+  "engine": "node >= 5.9.0",
+  "nyc": {
+    "exclude": [
+      "test",
+      "__tests__",
+      "node_modules",
+      "target"
+    ]
+  },
+  "devDependencies": {
+    "fs-extra": "^6.0.1",
+    "nodemon": "^1.17.5",
+    "pkg": "^4.3.3"
+  }
+}

msa/web-ui/pom.xml 372(+372 -0)

diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml
new file mode 100644
index 0000000..54c3bb4
--- /dev/null
+++ b/msa/web-ui/pom.xml
@@ -0,0 +1,372 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.thingsboard</groupId>
+        <version>2.2.0-SNAPSHOT</version>
+        <artifactId>msa</artifactId>
+    </parent>
+    <groupId>org.thingsboard.msa</groupId>
+    <artifactId>web-ui</artifactId>
+    <packaging>pom</packaging>
+
+    <name>ThingsBoard Web UI Microservice</name>
+    <url>https://thingsboard.io</url>
+    <description>Service for hosting ThingsBoard Web UI</description>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <main.dir>${basedir}/../..</main.dir>
+        <pkg.name>tb-web-ui</pkg.name>
+        <pkg.user>thingsboard</pkg.user>
+        <pkg.unixLogFolder>/var/log/${pkg.name}</pkg.unixLogFolder>
+        <pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
+        <pkg.linux.dist>${project.build.directory}/package/linux</pkg.linux.dist>
+        <pkg.win.dist>${project.build.directory}/package/windows</pkg.win.dist>
+        <dockerfile.skip>true</dockerfile.skip>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.thingsboard</groupId>
+            <artifactId>ui</artifactId>
+            <version>${project.version}</version>
+            <type>jar</type>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.winsw</groupId>
+            <artifactId>winsw</artifactId>
+            <classifier>bin</classifier>
+            <type>exe</type>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>com.github.eirslett</groupId>
+                <artifactId>frontend-maven-plugin</artifactId>
+                <version>1.0</version>
+                <configuration>
+                    <installDirectory>target</installDirectory>
+                    <workingDirectory>${basedir}</workingDirectory>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>install node and npm</id>
+                        <goals>
+                            <goal>install-node-and-npm</goal>
+                        </goals>
+                        <configuration>
+                            <nodeVersion>v8.11.3</nodeVersion>
+                            <npmVersion>5.6.0</npmVersion>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>npm install</id>
+                        <goals>
+                            <goal>npm</goal>
+                        </goals>
+                        <configuration>
+                            <arguments>install</arguments>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>extract-web-ui</id>
+                        <goals>
+                            <goal>unpack</goal>
+                        </goals>
+                        <configuration>
+                            <artifactItems>
+                                <artifactItem>
+                                    <groupId>org.thingsboard</groupId>
+                                    <artifactId>ui</artifactId>
+                                    <type>jar</type>
+                                    <overWrite>false</overWrite>
+                                    <outputDirectory>${project.build.directory}/web</outputDirectory>
+                                </artifactItem>
+                            </artifactItems>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>copy-winsw-service</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>copy</goal>
+                        </goals>
+                        <configuration>
+                            <artifactItems>
+                                <artifactItem>
+                                    <groupId>com.sun.winsw</groupId>
+                                    <artifactId>winsw</artifactId>
+                                    <classifier>bin</classifier>
+                                    <type>exe</type>
+                                    <destFileName>service.exe</destFileName>
+                                </artifactItem>
+                            </artifactItems>
+                            <outputDirectory>${pkg.win.dist}</outputDirectory>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-resources-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-linux-conf</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${pkg.linux.dist}/conf</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>config</directory>
+                                    <filtering>true</filtering>
+                                </resource>
+                            </resources>
+                            <filters>
+                                <filter>src/main/filters/unix.properties</filter>
+                            </filters>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>copy-linux-init</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${pkg.linux.dist}/init</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>src/main/scripts/init</directory>
+                                    <filtering>true</filtering>
+                                </resource>
+                            </resources>
+                            <filters>
+                                <filter>src/main/filters/unix.properties</filter>
+                            </filters>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>copy-win-conf</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${pkg.win.dist}/conf</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>config</directory>
+                                    <excludes>
+                                        <exclude>tb-web-ui.conf</exclude>
+                                    </excludes>
+                                    <filtering>true</filtering>
+                                </resource>
+                            </resources>
+                            <filters>
+                                <filter>src/main/filters/windows.properties</filter>
+                            </filters>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>copy-control</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/control</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>src/main/scripts/control</directory>
+                                    <filtering>true</filtering>
+                                </resource>
+                            </resources>
+                            <filters>
+                                <filter>src/main/filters/unix.properties</filter>
+                            </filters>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>copy-windows-control</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${pkg.win.dist}</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>src/main/scripts/windows</directory>
+                                    <filtering>true</filtering>
+                                </resource>
+                            </resources>
+                            <filters>
+                                <filter>src/main/filters/windows.properties</filter>
+                            </filters>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>copy-docker-config</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>docker</directory>
+                                    <filtering>true</filtering>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.fortasoft</groupId>
+                <artifactId>gradle-maven-plugin</artifactId>
+                <configuration>
+                    <tasks>
+                        <task>build</task>
+                        <task>buildDeb</task>
+                        <task>buildRpm</task>
+                    </tasks>
+                    <args>
+                        <arg>-PprojectBuildDir=${project.build.directory}</arg>
+                        <arg>-PprojectVersion=${project.version}</arg>
+                        <arg>-PpkgName=${pkg.name}</arg>
+                        <arg>-PpkgUser=${pkg.user}</arg>
+                        <arg>-PpkgInstallFolder=${pkg.installFolder}</arg>
+                        <arg>-PpkgLogFolder=${pkg.unixLogFolder}</arg>
+                    </args>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>invoke</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <version>3.0.0</version>
+                <configuration>
+                    <finalName>${pkg.name}</finalName>
+                    <descriptors>
+                        <descriptor>src/main/assembly/windows.xml</descriptor>
+                    </descriptors>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>assembly</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>com.spotify</groupId>
+                <artifactId>dockerfile-maven-plugin</artifactId>
+                <version>1.4.4</version>
+                <executions>
+                    <execution>
+                        <id>build-docker-image</id>
+                        <phase>pre-integration-test</phase>
+                        <goals>
+                            <goal>build</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <skip>${dockerfile.skip}</skip>
+                    <repository>local-maven-build/${pkg.name}</repository>
+                    <verbose>true</verbose>
+                    <googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
+                    <contextDirectory>${project.build.directory}</contextDirectory>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    <profiles>
+        <profile>
+            <id>npm-start</id>
+            <activation>
+                <property>
+                    <name>npm-start</name>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>com.github.eirslett</groupId>
+                        <artifactId>frontend-maven-plugin</artifactId>
+                        <version>1.0</version>
+                        <configuration>
+                            <installDirectory>target</installDirectory>
+                            <workingDirectory>${basedir}</workingDirectory>
+                        </configuration>
+                        <executions>
+                            <execution>
+                                <id>npm start</id>
+                                <goals>
+                                    <goal>npm</goal>
+                                </goals>
+
+                                <configuration>
+                                    <arguments>start</arguments>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+    <repositories>
+        <repository>
+            <id>jenkins</id>
+            <name>Jenkins Repository</name>
+            <url>http://repo.jenkins-ci.org/releases</url>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+        </repository>
+    </repositories>
+</project>

msa/web-ui/server.js 129(+129 -0)

diff --git a/msa/web-ui/server.js b/msa/web-ui/server.js
new file mode 100644
index 0000000..46d9bfd
--- /dev/null
+++ b/msa/web-ui/server.js
@@ -0,0 +1,129 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const config = require('config'),
+      logger = require('./config/logger')('main'),
+      express = require('express'),
+      http = require('http'),
+      httpProxy = require('http-proxy'),
+      path = require('path'),
+      historyApiFallback = require("connect-history-api-fallback");
+
+var server;
+
+(async() => {
+    try {
+        logger.info('Starting ThingsBoard Web UI Microservice...');
+
+        const bindAddress = config.get('server.address');
+        const bindPort = config.get('server.port');
+
+        const thingsboardHost = config.get('thingsboard.host');
+        const thingsboardPort = config.get('thingsboard.port');
+
+        logger.info('Bind address: %s', bindAddress);
+        logger.info('Bind port: %s', bindPort);
+        logger.info('ThingsBoard host: %s', thingsboardHost);
+        logger.info('ThingsBoard port: %s', thingsboardPort);
+
+        var webDir = path.join(__dirname, 'web');
+
+        if (typeof process.env.WEB_FOLDER === 'string') {
+            webDir = path.resolve(process.env.WEB_FOLDER);
+        }
+        logger.info('Web folder: %s', webDir);
+
+        const app = express();
+        server = http.createServer(app);
+
+        const apiProxy = httpProxy.createProxyServer({
+            target: {
+                host: thingsboardHost,
+                port: thingsboardPort
+            }
+        });
+
+        apiProxy.on('error', function (err, req, res) {
+            logger.warn('API proxy error: %s', err.message);
+            res.writeHead(500);
+            if (err.code && err.code === 'ECONNREFUSED') {
+                res.end('Unable to connect to ThingsBoard server.');
+            } else {
+                res.end('Thingsboard server connection error: ' + err.code ? err.code : '');
+            }
+        });
+
+        const root = path.join(webDir, 'public');
+
+        const staticDir = path.join(root, 'static');
+
+        app.all('/api/*', (req, res) => {
+            logger.info(req.method + ' ' + req.originalUrl);
+            apiProxy.web(req, res);
+        });
+
+        app.all('/static/rulenode/*', (req, res) => {
+            apiProxy.web(req, res);
+        });
+
+        app.use(historyApiFallback());
+
+        app.use('/static', express.static(staticDir));
+
+        app.get('*', (req, res) => {
+            apiProxy.web(req, res);
+        });
+
+        server.on('upgrade', (req, socket, head) => {
+            apiProxy.ws(req, socket, head);
+        });
+
+        server.listen(bindPort, bindAddress, (error) => {
+            if (error) {
+                logger.error('Failed to start ThingsBoard Web UI Microservice: %s', e.message);
+                logger.error(error.stack);
+                exit(-1);
+            } else {
+                logger.info('==> 🌎  Listening on port %s.', bindPort);
+                logger.info('Started ThingsBoard Web UI Microservice.');
+            }
+        });
+
+    } catch (e) {
+        logger.error('Failed to start ThingsBoard Web UI Microservice: %s', e.message);
+        logger.error(e.stack);
+        exit(-1);
+    }
+})();
+
+process.on('exit', function () {
+    exit(0);
+});
+
+function exit(status) {
+    logger.info('Exiting with status: %d ...', status);
+    if (server) {
+        logger.info('Stopping HTTP Server...');
+        var _server = server;
+        server = null;
+        _server.close(() => {
+            logger.info('HTTP Server stopped.');
+            process.exit(status);
+        });
+    } else {
+        process.exit(status);
+    }
+}
diff --git a/msa/web-ui/src/main/assembly/windows.xml b/msa/web-ui/src/main/assembly/windows.xml
new file mode 100644
index 0000000..1211990
--- /dev/null
+++ b/msa/web-ui/src/main/assembly/windows.xml
@@ -0,0 +1,75 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
+    <id>windows</id>
+
+    <formats>
+        <format>zip</format>
+    </formats>
+
+    <!-- Workaround to create logs directory -->
+    <fileSets>
+        <fileSet>
+            <directory>${pkg.win.dist}</directory>
+            <outputDirectory>logs</outputDirectory>
+            <excludes>
+                <exclude>*/**</exclude>
+            </excludes>
+        </fileSet>
+        <fileSet>
+            <directory>${pkg.win.dist}/conf</directory>
+            <outputDirectory>conf</outputDirectory>
+            <lineEnding>windows</lineEnding>
+        </fileSet>
+        <fileSet>
+            <directory>${project.build.directory}/web</directory>
+            <outputDirectory>web</outputDirectory>
+        </fileSet>
+    </fileSets>
+
+    <files>
+        <file>
+            <source>${pkg.win.dist}/bin/${pkg.name}.exe</source>
+            <outputDirectory>bin</outputDirectory>
+            <destName>${pkg.name}.exe</destName>
+        </file>
+        <file>
+            <source>${pkg.win.dist}/service.exe</source>
+            <outputDirectory/>
+            <destName>${pkg.name}.exe</destName>
+        </file>
+        <file>
+            <source>${pkg.win.dist}/service.xml</source>
+            <outputDirectory/>
+            <destName>${pkg.name}.xml</destName>
+            <lineEnding>windows</lineEnding>
+        </file>
+        <file>
+            <source>${pkg.win.dist}/install.bat</source>
+            <outputDirectory/>
+            <lineEnding>windows</lineEnding>
+        </file>
+        <file>
+            <source>${pkg.win.dist}/uninstall.bat</source>
+            <outputDirectory/>
+            <lineEnding>windows</lineEnding>
+        </file>
+    </files>
+</assembly>
diff --git a/msa/web-ui/src/main/filters/unix.properties b/msa/web-ui/src/main/filters/unix.properties
new file mode 100644
index 0000000..8967278
--- /dev/null
+++ b/msa/web-ui/src/main/filters/unix.properties
@@ -0,0 +1 @@
+pkg.logFolder=${pkg.unixLogFolder}
\ No newline at end of file
diff --git a/msa/web-ui/src/main/filters/windows.properties b/msa/web-ui/src/main/filters/windows.properties
new file mode 100644
index 0000000..a6e48d9
--- /dev/null
+++ b/msa/web-ui/src/main/filters/windows.properties
@@ -0,0 +1,2 @@
+pkg.logFolder=${BASE}\\logs
+pkg.winWrapperLogFolder=%BASE%\\logs
diff --git a/msa/web-ui/src/main/scripts/control/deb/postinst b/msa/web-ui/src/main/scripts/control/deb/postinst
new file mode 100644
index 0000000..4c48c55
--- /dev/null
+++ b/msa/web-ui/src/main/scripts/control/deb/postinst
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+chown -R ${pkg.user}: ${pkg.logFolder}
+chown -R ${pkg.user}: ${pkg.installFolder}
+# update-rc.d ${pkg.name} defaults
+
diff --git a/msa/web-ui/src/main/scripts/control/deb/postrm b/msa/web-ui/src/main/scripts/control/deb/postrm
new file mode 100644
index 0000000..6186580
--- /dev/null
+++ b/msa/web-ui/src/main/scripts/control/deb/postrm
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+update-rc.d -f ${pkg.name} remove
diff --git a/msa/web-ui/src/main/scripts/control/deb/preinst b/msa/web-ui/src/main/scripts/control/deb/preinst
new file mode 100644
index 0000000..d2ebea4
--- /dev/null
+++ b/msa/web-ui/src/main/scripts/control/deb/preinst
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+if ! getent group ${pkg.user} >/dev/null; then
+    addgroup --system ${pkg.user}
+fi
+
+if ! getent passwd ${pkg.user} >/dev/null; then
+    adduser --quiet \
+            --system \
+            --ingroup ${pkg.user} \
+            --quiet \
+            --disabled-login \
+            --disabled-password \
+            --home ${pkg.installFolder} \
+            --no-create-home \
+            -gecos "Thingsboard application" \
+            ${pkg.user}
+fi
diff --git a/msa/web-ui/src/main/scripts/control/deb/prerm b/msa/web-ui/src/main/scripts/control/deb/prerm
new file mode 100644
index 0000000..898d3ef
--- /dev/null
+++ b/msa/web-ui/src/main/scripts/control/deb/prerm
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+if [ -e /var/run/${pkg.name}/${pkg.name}.pid ]; then
+    service ${pkg.name} stop
+fi
diff --git a/msa/web-ui/src/main/scripts/control/rpm/postinst b/msa/web-ui/src/main/scripts/control/rpm/postinst
new file mode 100644
index 0000000..d8021e2
--- /dev/null
+++ b/msa/web-ui/src/main/scripts/control/rpm/postinst
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+chown -R ${pkg.user}: ${pkg.logFolder}
+chown -R ${pkg.user}: ${pkg.installFolder}
+
+if [ $1 -eq 1 ] ; then
+        # Initial installation
+        systemctl --no-reload enable ${pkg.name}.service >/dev/null 2>&1 || :
+fi
diff --git a/msa/web-ui/src/main/scripts/control/rpm/postrm b/msa/web-ui/src/main/scripts/control/rpm/postrm
new file mode 100644
index 0000000..8e1f8a2
--- /dev/null
+++ b/msa/web-ui/src/main/scripts/control/rpm/postrm
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+if [ $1 -ge 1 ] ; then
+        # Package upgrade, not uninstall
+        systemctl try-restart ${pkg.name}.service >/dev/null 2>&1 || :
+fi
diff --git a/msa/web-ui/src/main/scripts/control/rpm/preinst b/msa/web-ui/src/main/scripts/control/rpm/preinst
new file mode 100644
index 0000000..db6306e
--- /dev/null
+++ b/msa/web-ui/src/main/scripts/control/rpm/preinst
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+getent group ${pkg.user} >/dev/null || groupadd -r ${pkg.user}
+getent passwd ${pkg.user} >/dev/null || \
+useradd -d ${pkg.installFolder} -g ${pkg.user} -M -r ${pkg.user} -s /sbin/nologin \
+-c "Thingsboard application"
diff --git a/msa/web-ui/src/main/scripts/control/rpm/prerm b/msa/web-ui/src/main/scripts/control/rpm/prerm
new file mode 100644
index 0000000..accb487
--- /dev/null
+++ b/msa/web-ui/src/main/scripts/control/rpm/prerm
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+if [ $1 -eq 0 ] ; then
+        # Package removal, not upgrade
+        systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || :
+fi
diff --git a/msa/web-ui/src/main/scripts/control/tb-web-ui.service b/msa/web-ui/src/main/scripts/control/tb-web-ui.service
new file mode 100644
index 0000000..f542dd0
--- /dev/null
+++ b/msa/web-ui/src/main/scripts/control/tb-web-ui.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=${pkg.name}
+After=syslog.target
+
+[Service]
+User=${pkg.user}
+ExecStart=${pkg.installFolder}/init/${pkg.name}
+SuccessExitStatus=143
+
+[Install]
+WantedBy=multi-user.target
diff --git a/msa/web-ui/src/main/scripts/init/tb-web-ui b/msa/web-ui/src/main/scripts/init/tb-web-ui
new file mode 100644
index 0000000..d0d3f95
--- /dev/null
+++ b/msa/web-ui/src/main/scripts/init/tb-web-ui
@@ -0,0 +1,233 @@
+#!/bin/bash
+#
+
+
+### BEGIN INIT INFO
+# Provides:          tb-web-ui
+# Required-Start:    $remote_fs $syslog $network
+# Required-Stop:     $remote_fs $syslog $network
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: ${project.name}
+# Description:       ${project.description}
+# chkconfig:         2345 99 01
+### END INIT INFO
+
+[[ -n "$DEBUG" ]] && set -x
+
+# Initialize variables that cannot be provided by a .conf file
+WORKING_DIR="$(pwd)"
+# shellcheck disable=SC2153
+
+mainfile=${pkg.installFolder}/bin/${pkg.name}
+configfile=${pkg.name}.conf
+
+# Follow symlinks to find the real script and detect init.d script
+cd "$(dirname "$0")" || exit 1
+[[ -z "$initfile" ]] && initfile=$(pwd)/$(basename "$0")
+while [[ -L "$initfile" ]]; do
+  [[ "$initfile" =~ init\.d ]] && init_script=$(basename "$initfile")
+  initfile=$(readlink "$initfile")
+  cd "$(dirname "$initfile")" || exit 1
+  initfile=$(pwd)/$(basename "$initfile")
+done
+initfolder="$( (cd "$(dirname "initfile")" && pwd -P) )"
+cd "$WORKING_DIR" || exit 1
+
+# Initialize CONF_FOLDER location
+[[ -z "$CONF_FOLDER" ]] && CONF_FOLDER="${pkg.installFolder}/conf"
+
+# shellcheck source=/dev/null
+[[ -r "${CONF_FOLDER}/${configfile}" ]] && source "${CONF_FOLDER}/${configfile}"
+
+# Initialize PID/LOG locations if they weren't provided by the config file
+[[ -z "$PID_FOLDER" ]] && PID_FOLDER="/var/run"
+[[ -z "$LOG_FOLDER" ]] && LOG_FOLDER="${pkg.unixLogFolder}"
+! [[ "$PID_FOLDER" == /* ]] && PID_FOLDER="$(dirname "$mainfile")"/"$PID_FOLDER"
+! [[ "$LOG_FOLDER" == /* ]] && LOG_FOLDER="$(dirname "$mainfile")"/"$LOG_FOLDER"
+! [[ -x "$PID_FOLDER" ]] && PID_FOLDER="/tmp"
+! [[ -x "$LOG_FOLDER" ]] && LOG_FOLDER="/tmp"
+
+# Set up defaults
+[[ -z "$MODE" ]] && MODE="auto" # modes are "auto", "service" or "run"
+[[ -z "$USE_START_STOP_DAEMON" ]] && USE_START_STOP_DAEMON="true"
+
+# Create an identity for log/pid files
+if [[ -z "$identity" ]]; then
+  if [[ -n "$init_script" ]]; then
+    identity="${init_script}"
+  else
+    identity=$(basename "${initfile%.*}")_${initfolder//\//}
+  fi
+fi
+
+# Initialize log file name if not provided by the config file
+[[ -z "$LOG_FILENAME" ]] && LOG_FILENAME="${identity}.log"
+
+# ANSI Colors
+echoRed() { echo $'\e[0;31m'"$1"$'\e[0m'; }
+echoGreen() { echo $'\e[0;32m'"$1"$'\e[0m'; }
+echoYellow() { echo $'\e[0;33m'"$1"$'\e[0m'; }
+
+# Utility functions
+checkPermissions() {
+  touch "$pid_file" &> /dev/null || { echoRed "Operation not permitted (cannot access pid file)"; return 4; }
+  touch "$log_file" &> /dev/null || { echoRed "Operation not permitted (cannot access log file)"; return 4; }
+}
+
+isRunning() {
+  ps -p "$1" &> /dev/null
+}
+
+await_file() {
+  end=$(date +%s)
+  let "end+=10"
+  while [[ ! -s "$1" ]]
+  do
+    now=$(date +%s)
+    if [[ $now -ge $end ]]; then
+      break
+    fi
+    sleep 1
+  done
+}
+
+# Determine the script mode
+action="run"
+if [[ "$MODE" == "auto" && -n "$init_script" ]] || [[ "$MODE" == "service" ]]; then
+  action="$1"
+  shift
+fi
+
+# Build the pid and log filenames
+if [[ "$identity" == "$init_script" ]] || [[ "$identity" == "$APP_NAME" ]]; then
+  PID_FOLDER="$PID_FOLDER/${identity}"
+  pid_subfolder=$PID_FOLDER
+fi
+pid_file="$PID_FOLDER/${identity}.pid"
+log_file="$LOG_FOLDER/$LOG_FILENAME"
+
+# Determine the user to run as if we are root
+# shellcheck disable=SC2012
+[[ $(id -u) == "0" ]] && run_user=$(ls -ld "$mainfile" | awk '{print $3}')
+
+arguments=($RUN_ARGS "$@")
+
+# Action functions
+start() {
+  if [[ -f "$pid_file" ]]; then
+    pid=$(cat "$pid_file")
+    isRunning "$pid" && { echoYellow "Already running [$pid]"; return 0; }
+  fi
+  do_start "$@"
+}
+
+do_start() {
+  working_dir=$(dirname "$mainfile")
+  pushd "$working_dir" > /dev/null
+  mkdir -p "$PID_FOLDER" &> /dev/null
+  if [[ -n "$run_user" ]]; then
+    checkPermissions || return $?
+    if [[ -z "$pid_subfolder" ]]; then
+      chown "$run_user" "$pid_subfolder"
+    fi
+    chown "$run_user" "$pid_file"
+    chown "$run_user" "$log_file"
+    if [ $USE_START_STOP_DAEMON = true ] && type start-stop-daemon > /dev/null 2>&1; then
+      start-stop-daemon --start --quiet \
+        --chuid "$run_user" \
+        --name "$identity" \
+        --make-pidfile --pidfile "$pid_file" \
+        --background --no-close \
+        --startas "$mainfile" \
+        --chdir "$working_dir" \
+        -- "${arguments[@]}" \
+        >> "$log_file" 2>&1
+      await_file "$pid_file"
+    else
+      su -s /bin/sh -c "$mainfile $(printf "\"%s\" " "${arguments[@]}") >> \"$log_file\" 2>&1 & echo \$!" "$run_user" > "$pid_file"
+    fi
+    pid=$(cat "$pid_file")
+  else
+    checkPermissions || return $?
+    "$mainfile" "${arguments[@]}" >> "$log_file" 2>&1 &
+    pid=$!
+    disown $pid
+    echo "$pid" > "$pid_file"
+  fi
+  [[ -z $pid ]] && { echoRed "Failed to start"; return 1; }
+  echoGreen "Started [$pid]"
+}
+
+stop() {
+  working_dir=$(dirname "$mainfile")
+  pushd "$working_dir" > /dev/null
+  [[ -f $pid_file ]] || { echoYellow "Not running (pidfile not found)"; return 0; }
+  pid=$(cat "$pid_file")
+  isRunning "$pid" || { echoYellow "Not running (process ${pid}). Removing stale pid file."; rm -f "$pid_file"; return 0; }
+  do_stop "$pid" "$pid_file"
+}
+
+do_stop() {
+  kill -2 "$1" &> /dev/null || { echoRed "Unable to kill process $1"; return 1; }
+  for i in $(seq 1 60); do
+    isRunning "$1" || { echoGreen "Stopped [$1]"; rm -f "$2"; return 0; }
+    [[ $i -eq 30 ]] && kill -9 "$1" &> /dev/null
+    sleep 1
+  done
+  echoRed "Unable to kill process $1";
+  return 1;
+}
+
+restart() {
+  stop && start
+}
+
+orce_reload() {
+  working_dir=$(dirname "$mainfile")
+  pushd "$working_dir" > /dev/null
+  [[ -f $pid_file ]] || { echoRed "Not running (pidfile not found)"; return 7; }
+  pid=$(cat "$pid_file")
+  rm -f "$pid_file"
+  isRunning "$pid" || { echoRed "Not running (process ${pid} not found)"; return 7; }
+  do_stop "$pid" "$pid_file"
+  do_start
+}
+
+status() {
+  working_dir=$(dirname "$mainfile")
+  pushd "$working_dir" > /dev/null
+  [[ -f "$pid_file" ]] || { echoRed "Not running"; return 3; }
+  pid=$(cat "$pid_file")
+  isRunning "$pid" || { echoRed "Not running (process ${pid} not found)"; return 1; }
+  echoGreen "Running [$pid]"
+  return 0
+}
+
+run() {
+  pushd "$(dirname "$mainfile")" > /dev/null
+  "$mainfile" "${arguments[@]}"
+  result=$?
+  popd > /dev/null
+  return "$result"
+}
+
+# Call the appropriate action function
+case "$action" in
+start)
+  start "$@"; exit $?;;
+stop)
+  stop "$@"; exit $?;;
+restart)
+  restart "$@"; exit $?;;
+force-reload)
+  force_reload "$@"; exit $?;;
+status)
+  status "$@"; exit $?;;
+run)
+  run "$@"; exit $?;;
+*)
+  echo "Usage: $0 {start|stop|restart|force-reload|status|run}"; exit 1;
+esac
+
+exit 0
diff --git a/msa/web-ui/src/main/scripts/windows/install.bat b/msa/web-ui/src/main/scripts/windows/install.bat
new file mode 100644
index 0000000..4da5542
--- /dev/null
+++ b/msa/web-ui/src/main/scripts/windows/install.bat
@@ -0,0 +1,31 @@
+@REM
+@REM Copyright © 2016-2018 The Thingsboard Authors
+@REM
+@REM Licensed under the Apache License, Version 2.0 (the "License");
+@REM you may not use this file except in compliance with the License.
+@REM You may obtain a copy of the License at
+@REM
+@REM     http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing, software
+@REM distributed under the License is distributed on an "AS IS" BASIS,
+@REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@REM See the License for the specific language governing permissions and
+@REM limitations under the License.
+@REM
+
+@ECHO OFF
+
+setlocal ENABLEEXTENSIONS
+
+@ECHO Installing ${pkg.name} ...
+
+SET BASE=%~dp0
+
+%BASE%${pkg.name}.exe install
+
+@ECHO ${pkg.name} installed successfully!
+
+GOTO END
+
+:END
diff --git a/msa/web-ui/src/main/scripts/windows/service.xml b/msa/web-ui/src/main/scripts/windows/service.xml
new file mode 100644
index 0000000..e512aad
--- /dev/null
+++ b/msa/web-ui/src/main/scripts/windows/service.xml
@@ -0,0 +1,30 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<service>
+    <id>${pkg.name}</id>
+    <name>${project.name}</name>
+    <description>${project.description}</description>
+    <workingdirectory>%BASE%\bin</workingdirectory>
+    <logpath>${pkg.winWrapperLogFolder}</logpath>
+    <logmode>rotate</logmode>
+    <env name="NODE_CONFIG_DIR" value="%BASE%\conf" />
+    <env name="LOG_FOLDER" value="${pkg.winWrapperLogFolder}" />
+    <env name="NODE_ENV" value="production" />
+    <env name="WEB_FOLDER" value="%BASE%\web" />
+    <executable>%BASE%\bin\${pkg.name}.exe</executable>
+</service>
diff --git a/msa/web-ui/src/main/scripts/windows/uninstall.bat b/msa/web-ui/src/main/scripts/windows/uninstall.bat
new file mode 100644
index 0000000..7061d2a
--- /dev/null
+++ b/msa/web-ui/src/main/scripts/windows/uninstall.bat
@@ -0,0 +1,25 @@
+@REM
+@REM Copyright © 2016-2018 The Thingsboard Authors
+@REM
+@REM Licensed under the Apache License, Version 2.0 (the "License");
+@REM you may not use this file except in compliance with the License.
+@REM You may obtain a copy of the License at
+@REM
+@REM     http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing, software
+@REM distributed under the License is distributed on an "AS IS" BASIS,
+@REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@REM See the License for the specific language governing permissions and
+@REM limitations under the License.
+@REM
+
+@ECHO OFF
+
+@ECHO Stopping ${pkg.name} ...
+net stop ${pkg.name}
+
+@ECHO Uninstalling ${pkg.name} ...
+%~dp0${pkg.name}.exe uninstall
+
+@ECHO DONE.
\ No newline at end of file

ui/package.json 2(+1 -1)

diff --git a/ui/package.json b/ui/package.json
index e730c39..417948e 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -2,7 +2,7 @@
   "name": "thingsboard",
   "private": true,
   "version": "2.2.0",
-  "description": "Thingsboard UI",
+  "description": "ThingsBoard UI",
   "licenses": [
     {
       "type": "Apache-2.0",