thingsboard-aplcache

Details

application/pom.xml 120(+116 -4)

diff --git a/application/pom.xml b/application/pom.xml
index d06fbfa..6efb019 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -28,14 +28,17 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard Server Application</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
+    <description>Open-source IoT Platform - Device management, data collection, processing and visualization
+    </description>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <main.dir>${basedir}/..</main.dir>
         <pkg.name>thingsboard</pkg.name>
-        <pkg.logFolder>/var/log/${pkg.name}</pkg.logFolder>
+        <pkg.unixLogFolder>/var/log/${pkg.name}</pkg.unixLogFolder>
         <pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
+        <pkg.win.dist>${project.build.directory}/windows</pkg.win.dist>
     </properties>
 
     <dependencies>
@@ -197,6 +200,13 @@
             <artifactId>springfox-swagger2</artifactId>
         </dependency>
         <dependency>
+            <groupId>com.sun.winsw</groupId>
+            <artifactId>winsw</artifactId>
+            <classifier>bin</classifier>
+            <type>exe</type>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>org.thingsboard</groupId>
             <artifactId>tools</artifactId>
             <scope>test</scope>
@@ -291,6 +301,38 @@
                                     <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>src/main/resources</directory>
+                                    <excludes>
+                                        <exclude>logback.xml</exclude>
+                                    </excludes>
+                                    <filtering>false</filtering>
+                                </resource>
+                                <resource>
+                                    <directory>src/main/conf</directory>
+                                    <excludes>
+                                        <exclude>thingsboard.conf</exclude>
+                                    </excludes>
+                                    <filtering>true</filtering>
+                                </resource>
+                            </resources>
+                            <filters>
+                                <filter>src/main/filters/windows.properties</filter>
+                            </filters>
                         </configuration>
                     </execution>
                     <execution>
@@ -307,6 +349,28 @@
                                     <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>
@@ -361,6 +425,25 @@
                             </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>
@@ -385,7 +468,7 @@
                     <excludeDevtools>true</excludeDevtools>
                     <embeddedLaunchScriptProperties>
                         <confFolder>${pkg.installFolder}/conf</confFolder>
-                        <logFolder>${pkg.logFolder}</logFolder>
+                        <logFolder>${pkg.unixLogFolder}</logFolder>
                         <logFilename>${pkg.name}.out</logFilename>
                     </embeddedLaunchScriptProperties>
                 </configuration>
@@ -412,7 +495,7 @@
                         <arg>-PmainJar=${project.build.directory}/${project.build.finalName}-boot.${project.packaging}</arg>
                         <arg>-PpkgName=${pkg.name}</arg>
                         <arg>-PpkgInstallFolder=${pkg.installFolder}</arg>
-                        <arg>-PpkgLogFolder=${pkg.logFolder}</arg>
+                        <arg>-PpkgLogFolder=${pkg.unixLogFolder}</arg>
                     </args>
                 </configuration>
                 <executions>
@@ -425,6 +508,25 @@
                 </executions>
             </plugin>
             <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <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>org.xolstice.maven.plugins</groupId>
                 <artifactId>protobuf-maven-plugin</artifactId>
             </plugin>
@@ -434,4 +536,14 @@
             </plugin>
         </plugins>
     </build>
+    <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>
diff --git a/application/src/main/assembly/windows.xml b/application/src/main/assembly/windows.xml
new file mode 100644
index 0000000..023c435
--- /dev/null
+++ b/application/src/main/assembly/windows.xml
@@ -0,0 +1,79 @@
+<!--
+
+    Copyright © 2016-2017 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}/extensions</directory>
+            <outputDirectory>extensions</outputDirectory>
+        </fileSet>
+        <fileSet>
+            <directory>${project.build.directory}/data</directory>
+            <outputDirectory>data</outputDirectory>
+        </fileSet>
+    </fileSets>
+
+    <files>
+        <file>
+            <source>${project.build.directory}/${project.build.finalName}-boot.${project.packaging}</source>
+            <outputDirectory>lib</outputDirectory>
+            <destName>${pkg.name}.jar</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/application/src/main/filters/unix.properties b/application/src/main/filters/unix.properties
new file mode 100644
index 0000000..8967278
--- /dev/null
+++ b/application/src/main/filters/unix.properties
@@ -0,0 +1 @@
+pkg.logFolder=${pkg.unixLogFolder}
\ No newline at end of file
diff --git a/application/src/main/filters/windows.properties b/application/src/main/filters/windows.properties
new file mode 100644
index 0000000..a6e48d9
--- /dev/null
+++ b/application/src/main/filters/windows.properties
@@ -0,0 +1,2 @@
+pkg.logFolder=${BASE}\\logs
+pkg.winWrapperLogFolder=%BASE%\\logs
diff --git a/application/src/main/scripts/windows/install.bat b/application/src/main/scripts/windows/install.bat
new file mode 100644
index 0000000..97c4feb
--- /dev/null
+++ b/application/src/main/scripts/windows/install.bat
@@ -0,0 +1,90 @@
+@ECHO OFF
+
+setlocal ENABLEEXTENSIONS
+
+IF %PROCESSOR_ARCHITECTURE%==AMD64 GOTO CHECK_JAVA_64
+IF %PROCESSOR_ARCHITECTURE%==x86 GOTO CHECK_JAVA_32
+
+@ECHO Detecting Java version installed.
+:CHECK_JAVA_64
+@ECHO Detecting if it is 64 bit machine
+set KEY_NAME="HKEY_LOCAL_MACHINE\Software\Wow6432Node\JavaSoft\Java Runtime Environment"
+set VALUE_NAME=CurrentVersion
+
+FOR /F "usebackq skip=2 tokens=1-3" %%A IN (`REG QUERY %KEY_NAME% /v %VALUE_NAME% 2^>nul`) DO (
+    set ValueName=%%A
+    set ValueType=%%B
+    set ValueValue=%%C
+)
+@ECHO CurrentVersion %ValueValue%
+
+SET KEY_NAME="%KEY_NAME:~1,-1%\%ValueValue%"
+SET VALUE_NAME=JavaHome
+
+if defined ValueName (
+    FOR /F "usebackq skip=2 tokens=1,2*" %%A IN (`REG QUERY %KEY_NAME% /v %VALUE_NAME% 2^>nul`) DO (
+        set ValueName2=%%A
+        set ValueType2=%%B
+        set JRE_PATH2=%%C
+
+        if defined ValueName2 (
+            set ValueName = %ValueName2%
+            set ValueType = %ValueType2%
+            set ValueValue =  %JRE_PATH2%
+        )
+    )
+)
+
+IF NOT "%JRE_PATH2%" == "" GOTO JAVA_INSTALLED
+IF "%JRE_PATH2%" == "" GOTO JAVA_NOT_INSTALLED
+
+:CHECK_JAVA_32
+@ECHO Detecting if it is 32 bit machine
+set KEY_NAME="HKEY_LOCAL_MACHINE\Software\JavaSoft\Java Runtime Environment"
+set VALUE_NAME=CurrentVersion
+
+FOR /F "usebackq skip=2 tokens=1-3" %%A IN (`REG QUERY %KEY_NAME% /v %VALUE_NAME% 2^>nul`) DO (
+    set ValueName=%%A
+    set ValueType=%%B
+    set ValueValue=%%C
+)
+@ECHO CurrentVersion %ValueValue%
+
+SET KEY_NAME="%KEY_NAME:~1,-1%\%ValueValue%"
+SET VALUE_NAME=JavaHome
+
+if defined ValueName (
+    FOR /F "usebackq skip=2 tokens=1,2*" %%A IN (`REG QUERY %KEY_NAME% /v %VALUE_NAME% 2^>nul`) DO (
+        set ValueName2=%%A
+        set ValueType2=%%B
+        set JRE_PATH2=%%C
+
+        if defined ValueName2 (
+            set ValueName = %ValueName2%
+            set ValueType = %ValueType2%
+            set ValueValue =  %JRE_PATH2%
+        )
+    )
+)
+
+IF "%JRE_PATH2%" == ""  GOTO JAVA_NOT_INSTALLED
+
+:JAVA_INSTALLED
+
+@ECHO Java 1.8 found!
+@ECHO Installing ${pkg.name} ...
+${pkg.name}.exe install
+
+@ECHO DONE.
+
+GOTO END
+
+:JAVA_NOT_INSTALLED
+@ECHO Java 1.8 or above is not installed
+@ECHO Please go to https://java.com/ and install Java. Then retry installation.
+PAUSE
+GOTO END
+
+:END
+
+
diff --git a/application/src/main/scripts/windows/service.xml b/application/src/main/scripts/windows/service.xml
new file mode 100644
index 0000000..b2acc45
--- /dev/null
+++ b/application/src/main/scripts/windows/service.xml
@@ -0,0 +1,12 @@
+<service>
+    <id>${pkg.name}</id>
+    <name>${project.name}</name>
+    <description>${project.description}</description>
+    <workingdirectory>%BASE%\conf</workingdirectory>
+    <logpath>${pkg.winWrapperLogFolder}</logpath>
+    <logmode>rotate</logmode>
+    <env name="LOADER_PATH" value="%BASE%\conf,%BASE%\extensions" />
+    <executable>java</executable>
+    <startargument>-jar</startargument>
+    <startargument>%BASE%\lib\${pkg.name}.jar</startargument>
+</service>
diff --git a/application/src/main/scripts/windows/uninstall.bat b/application/src/main/scripts/windows/uninstall.bat
new file mode 100644
index 0000000..d07cf87
--- /dev/null
+++ b/application/src/main/scripts/windows/uninstall.bat
@@ -0,0 +1,9 @@
+@ECHO OFF
+
+@ECHO Stopping ${pkg.name} ...
+net stop ${pkg.name}
+
+@ECHO Uninstalling ${pkg.name} ...
+${pkg.name}.exe uninstall
+
+@ECHO DONE.
\ No newline at end of file
diff --git a/dao/src/main/resources/system-data.cql b/dao/src/main/resources/system-data.cql
index 5694926..a687a4e 100644
--- a/dao/src/main/resources/system-data.cql
+++ b/dao/src/main/resources/system-data.cql
@@ -79,7 +79,7 @@ VALUES (  now ( ), minTimeuuid ( 0 ), 'cards', 'label_widget',
 
 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
 VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'timeseries_table',
-'{"type":"timeseries","sizeX":8,"sizeY":6.5,"resources":[],"templateHtml":"<md-tabs md-selected=\"sourceIndex\" ng-class=\"{''tb-headless'': sources.length === 1}\"\n    id=\"tabs\" md-border-bottom flex class=\"tb-absolute-fill\">\n    <md-tab ng-repeat=\"source in sources\" label=\"{{ source.label }}\">\n        <md-table-container>\n            <table md-table>\n                <thead md-head md-order=\"source.query.order\" md-on-reorder=\"onReorder(source)\">\n                    <tr md-row>\n                        <th md-column md-order-by=\"0\"><span>Timestamp</span></th>\n                        <th md-column md-order-by=\"{{ h.index }}\" ng-repeat=\"h in source.ts.header\"><span>{{ h.label }}</span></th>\n                    </tr>\n                </thead>\n                <tbody md-body>\n                    <tr md-row ng-repeat=\"row in source.ts.data\">\n                        <td md-cell ng-repeat=\"d in row track by $index\" ng-style=\"cellStyle(source, $index, d)\">\n                            {{ $index === 0 ? (d | date : ''yyyy-MM-dd HH:mm:ss'') : d }}\n                        </td>\n                    </tr>    \n                </tbody>    \n            </table>\n        </md-table-container>\n        <md-table-pagination md-limit=\"source.query.limit\" md-limit-options=\"[5, 10, 15]\"\n                             md-page=\"source.query.page\" md-total=\"{{source.ts.count}}\"\n                             md-on-paginate=\"onPaginate(source)\" md-page-select>\n        </md-table-pagination>\n    </md-tab>\n</md-tabs>","templateCss":"table.md-table thead.md-head>tr.md-row {\n    height: 40px;\n}\n\ntable.md-table tbody.md-body>tr.md-row, table.md-table tfoot.md-foot>tr.md-row {\n    height: 38px;\n}\n\n.md-table-pagination>* {\n    height: 46px;\n}\n","controllerScript":"var filter;\n\nfns.init = function(containerElement, settings, datasources,\n    data, scope) {\n    \n    filter = scope.$injector.get(\"$filter\");\n    \n    scope.sources = [];\n    scope.sourceIndex = 0;\n    \n    var keyOffset = 0;\n    for (var ds in datasources) {\n        var source = {};\n        var datasource = datasources[ds];\n        source.keyStartIndex = keyOffset;\n        keyOffset += datasource.dataKeys.length;\n        source.keyEndIndex = keyOffset;\n        source.label = datasource.name;\n        source.data = [];\n        source.rawData = [];\n        source.query = {\n            limit: 5,\n            page: 1,\n            order: ''-0''\n        }\n        source.ts = {\n            header: [],\n            count: 0,\n            data: [],\n            stylesInfo: []\n        }\n        for (var a = 0; a < datasource.dataKeys.length; a++ ) {\n            var dataKey = datasource.dataKeys[a];\n            var keySettings = dataKey.settings;\n            source.ts.header.push({\n                index: a+1,\n                label: dataKey.label\n            });\n\n            var cellStyleFunction = null;\n            var useCellStyleFunction = false;\n            \n            if (keySettings.useCellStyleFunction === true) {\n                if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {\n                    try {\n                       cellStyleFunction = new Function(''value'', keySettings.cellStyleFunction);\n                       useCellStyleFunction = true;\n                    } catch (e) {\n                       cellStyleFunction = null;\n                       useCellStyleFunction = false;\n                    }\n                }\n            }\n\n            source.ts.stylesInfo.push({\n                useCellStyleFunction: useCellStyleFunction,\n                cellStyleFunction: cellStyleFunction\n            });\n        }\n        scope.sources.push(source);\n    }\n\n    scope.onPaginate = function(source) {\n        updatePage(source);\n    }\n    \n    scope.onReorder = function(source) {\n        reorder(source);\n        updatePage(source);\n    }\n    \n    scope.cellStyle = function(source, index, value) {\n        var style = {};\n        if (index > 0) {\n            var styleInfo = source.ts.stylesInfo[index-1];\n            if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {\n                try {\n                    style = styleInfo.cellStyleFunction(value);\n                } catch (e) {\n                    style = {};\n                }\n            }\n        }\n        return style;\n    }\n    \n    scope.$watch(''sourceIndex'', function(newIndex, oldIndex) {\n       if (newIndex != oldIndex) {\n           updateSourceData(scope.sources[scope.sourceIndex]);\n       } \n    });\n    \n    scope.$apply();\n}\n\nfunction updatePage(source) {\n    var startIndex = source.query.limit * (source.query.page - 1);\n    source.ts.data = source.data.slice(startIndex, startIndex + source.query.limit);\n}\n\nfunction reorder(source) {\n    source.data = filter(''orderBy'')(source.data, source.query.order);\n}\n\nfunction convertData(data) {\n    var rows = [];\n    var count = data[0].data.length;\n    for (var i = 0; i < count; i++) {\n        var row = [];\n        for (var d = 0; d < data.length; d++) {\n            var columnData = data[d].data;\n            var cellData = columnData[i];\n            if (d === 0) {\n                row.push(cellData[0]);\n            }\n            row.push(cellData[1]);\n        }\n        rows.push(row);\n    }\n    return rows;\n}\n\nfunction updateSourceData(source) {\n    source.data = convertData(source.rawData);\n    source.ts.count = source.data.length;\n    reorder(source);\n    updatePage(source);\n}\n\nfns.redraw = function(containerElement, width, height, data,\n    timeWindow, sizeChanged, scope) {\n    for (var s in scope.sources) {\n        var source = scope.sources[s];\n        source.rawData = data.slice(source.keyStartIndex, source.keyEndIndex);\n    }\n    updateSourceData(scope.sources[scope.sourceIndex]);\n    scope.$apply();\n};\n\nfns.destroy = function() {\n};","settingsSchema":"{}","dataKeySettingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"DataKeySettings\",\n        \"properties\": {\n            \"useCellStyleFunction\": {\n                \"title\": \"Use cell style function\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"cellStyleFunction\": {\n                \"title\": \"Cell style function: f(value)\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"useCellStyleFunction\",\n        {\n            \"key\": \"cellStyleFunction\",\n            \"type\": \"javascript\"\n        }\n    ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature  °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"var percent = (value + 60)/120 * 100;\\nvar color = tinycolor.mix(''blue'', ''red'', amount = percent);\\ncolor.setAlpha(.5);\\nreturn {\\n  paddingLeft: ''20px'',\\n  color: ''#ffffff'',\\n  background: color.toRgbString(),\\n  fontSize: ''18px''\\n};\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"var percent = value;\\nvar backgroundColor = tinycolor(''blue'');\\nbackgroundColor.setAlpha(value/100);\\nvar color = ''blue'';\\nif (value > 50) {\\n    color = ''white'';\\n}\\n\\nreturn {\\n  paddingLeft: ''20px'',\\n  color: color,\\n  background: backgroundColor.toRgbString(),\\n  fontSize: ''18px''\\n};\"},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Timeseries table\"}"}',
+'{"type":"timeseries","sizeX":8,"sizeY":6.5,"resources":[],"templateHtml":"<md-tabs md-selected=\"sourceIndex\" ng-class=\"{''tb-headless'': sources.length === 1}\"\n    id=\"tabs\" md-border-bottom flex class=\"tb-absolute-fill\">\n    <md-tab ng-repeat=\"source in sources\" label=\"{{ source.label }}\">\n        <md-table-container>\n            <table md-table>\n                <thead md-head md-order=\"source.query.order\" md-on-reorder=\"onReorder(source)\">\n                    <tr md-row>\n                        <th ng-show=\"showTimestamp\" md-column md-order-by=\"0\"><span>Timestamp</span></th>\n                        <th md-column md-order-by=\"{{ h.index }}\" ng-repeat=\"h in source.ts.header\"><span>{{ h.label }}</span></th>\n                    </tr>\n                </thead>\n                <tbody md-body>\n                    <tr md-row ng-repeat=\"row in source.ts.data\">\n                        <td ng-show=\"$index > 0 || ($index === 0 && showTimestamp)\" md-cell ng-repeat=\"d in row track by $index\" ng-style=\"cellStyle(source, $index, d)\" ng-bind-html=\"cellContent(source, $index, row, d)\">\n                        </td>\n                    </tr>    \n                </tbody>    \n            </table>\n        </md-table-container>\n        <md-table-pagination md-limit=\"source.query.limit\" md-limit-options=\"[5, 10, 15]\"\n                             md-page=\"source.query.page\" md-total=\"{{source.ts.count}}\"\n                             md-on-paginate=\"onPaginate(source)\" md-page-select>\n        </md-table-pagination>\n    </md-tab>\n</md-tabs>","templateCss":"table.md-table thead.md-head>tr.md-row {\n    height: 40px;\n}\n\ntable.md-table tbody.md-body>tr.md-row, table.md-table tfoot.md-foot>tr.md-row {\n    height: 38px;\n}\n\n.md-table-pagination>* {\n    height: 46px;\n}\n","controllerScript":"var filter;\n\nfns.init = function(containerElement, settings, datasources,\n    data, scope) {\n    \n    filter = scope.$injector.get(\"$filter\");\n    \n    scope.sources = [];\n    scope.sourceIndex = 0;\n    scope.showTimestamp = settings.showTimestamp !== false;\n    \n    var keyOffset = 0;\n    for (var ds in datasources) {\n        var source = {};\n        var datasource = datasources[ds];\n        source.keyStartIndex = keyOffset;\n        keyOffset += datasource.dataKeys.length;\n        source.keyEndIndex = keyOffset;\n        source.label = datasource.name;\n        source.data = [];\n        source.rawData = [];\n        source.query = {\n            limit: 5,\n            page: 1,\n            order: ''-0''\n        }\n        source.ts = {\n            header: [],\n            count: 0,\n            data: [],\n            stylesInfo: [],\n            contentsInfo: [],\n            rowDataTemplate: {}\n        }\n        source.ts.rowDataTemplate[''Timestamp''] = null;\n        for (var a = 0; a < datasource.dataKeys.length; a++ ) {\n            var dataKey = datasource.dataKeys[a];\n            var keySettings = dataKey.settings;\n            source.ts.header.push({\n                index: a+1,\n                label: dataKey.label\n            });\n            source.ts.rowDataTemplate[dataKey.label] = null;\n\n            var cellStyleFunction = null;\n            var useCellStyleFunction = false;\n            \n            if (keySettings.useCellStyleFunction === true) {\n                if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {\n                    try {\n                       cellStyleFunction = new Function(''value'', keySettings.cellStyleFunction);\n                       useCellStyleFunction = true;\n                    } catch (e) {\n                       cellStyleFunction = null;\n                       useCellStyleFunction = false;\n                    }\n                }\n            }\n\n            source.ts.stylesInfo.push({\n                useCellStyleFunction: useCellStyleFunction,\n                cellStyleFunction: cellStyleFunction\n            });\n            \n            var cellContentFunction = null;\n            var useCellContentFunction = false;\n            \n            if (keySettings.useCellContentFunction === true) {\n                if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {\n                    try {\n                       cellContentFunction = new Function(''value, rowData, filter'', keySettings.cellContentFunction);\n                       useCellContentFunction = true;\n                    } catch (e) {\n                       cellContentFunction = null;\n                       useCellContentFunction = false;\n                    }\n                }\n            }\n            \n            source.ts.contentsInfo.push({\n                useCellContentFunction: useCellContentFunction,\n                cellContentFunction: cellContentFunction\n            });\n            \n        }\n        scope.sources.push(source);\n    }\n\n    scope.onPaginate = function(source) {\n        updatePage(source);\n    }\n    \n    scope.onReorder = function(source) {\n        reorder(source);\n        updatePage(source);\n    }\n    \n    scope.cellStyle = function(source, index, value) {\n        var style = {};\n        if (index > 0) {\n            var styleInfo = source.ts.stylesInfo[index-1];\n            if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {\n                try {\n                    style = styleInfo.cellStyleFunction(value);\n                } catch (e) {\n                    style = {};\n                }\n            }\n        }\n        return style;\n    }\n\n    scope.cellContent = function(source, index, row, value) {\n        if (index === 0) {\n            return filter(''date'')(value, ''yyyy-MM-dd HH:mm:ss'');\n        } else {\n            var strContent = '''';\n            if (value) {\n                strContent = ''''+value;\n            }\n            var content = strContent;\n            var contentInfo = source.ts.contentsInfo[index-1];\n            if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {\n                try {\n                    var rowData = source.ts.rowDataTemplate;\n                    rowData[''Timestamp''] = row[0];\n                    for (var h in source.ts.header) {\n                        var headerInfo = source.ts.header[h];\n                        rowData[headerInfo.label] = row[headerInfo.index];\n                    }\n                    content = contentInfo.cellContentFunction(value, rowData, filter);\n                } catch (e) {\n                    content = strContent;\n                }\n            }            \n            return content;\n        }\n    }\n    \n    scope.$watch(''sourceIndex'', function(newIndex, oldIndex) {\n       if (newIndex != oldIndex) {\n           updateSourceData(scope.sources[scope.sourceIndex]);\n       } \n    });\n    \n    scope.$apply();\n}\n\nfunction updatePage(source) {\n    var startIndex = source.query.limit * (source.query.page - 1);\n    source.ts.data = source.data.slice(startIndex, startIndex + source.query.limit);\n}\n\nfunction reorder(source) {\n    source.data = filter(''orderBy'')(source.data, source.query.order);\n}\n\nfunction convertData(data) {\n    var rowsMap = [];\n    for (var d = 0; d < data.length; d++) {\n        var columnData = data[d].data;\n        for (var i = 0; i < columnData.length; i++) {\n            var cellData = columnData[i];\n            var timestamp = cellData[0];\n            var row = rowsMap[timestamp];\n            if (!row) {\n                row = [];\n                row[0] = timestamp;\n                for (var c = 0; c < data.length; c++) {\n                    row[c+1] = null;\n                }\n                rowsMap[timestamp] = row;\n            }\n            row[d+1] = cellData[1];\n        }\n    }\n    var rows = [];\n    for (var t in rowsMap) {\n        rows.push(rowsMap[t]);\n    }\n    return rows;\n}\n\nfunction updateSourceData(source) {\n    source.data = convertData(source.rawData);\n    source.ts.count = source.data.length;\n    reorder(source);\n    updatePage(source);\n}\n\nfns.redraw = function(containerElement, width, height, data,\n    timeWindow, sizeChanged, scope) {\n    for (var s in scope.sources) {\n        var source = scope.sources[s];\n        source.rawData = data.slice(source.keyStartIndex, source.keyEndIndex);\n    }\n    updateSourceData(scope.sources[scope.sourceIndex]);\n    scope.$apply();\n};\n\nfns.destroy = function() {\n};","settingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"TimeseriesTableSettings\",\n        \"properties\": {\n            \"showTimestamp\": {\n                \"title\": \"Display timestamp column\",\n                \"type\": \"boolean\",\n                \"default\": true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"showTimestamp\"\n    ]\n}","dataKeySettingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"DataKeySettings\",\n        \"properties\": {\n            \"useCellStyleFunction\": {\n                \"title\": \"Use cell style function\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"cellStyleFunction\": {\n                \"title\": \"Cell style function: f(value)\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"useCellContentFunction\": {\n                \"title\": \"Use cell content function\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"cellContentFunction\": {\n                \"title\": \"Cell content function: f(value, rowData, filter)\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"useCellStyleFunction\",\n        {\n            \"key\": \"cellStyleFunction\",\n            \"type\": \"javascript\"\n        },\n        \"useCellContentFunction\",\n        {\n            \"key\": \"cellContentFunction\",\n            \"type\": \"javascript\"\n        }\n    ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature  °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n    var percent = (value + 60)/120 * 100;\\n    var color = tinycolor.mix(''blue'', ''red'', amount = percent);\\n    color.setAlpha(.5);\\n    return {\\n      paddingLeft: ''20px'',\\n      color: ''#ffffff'',\\n      background: color.toRgbString(),\\n      fontSize: ''18px''\\n    };\\n} else {\\n    return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n    var percent = value;\\n    var backgroundColor = tinycolor(''blue'');\\n    backgroundColor.setAlpha(value/100);\\n    var color = ''blue'';\\n    if (value > 50) {\\n        color = ''white'';\\n    }\\n    \\n    return {\\n      paddingLeft: ''20px'',\\n      color: color,\\n      background: backgroundColor.toRgbString(),\\n      fontSize: ''18px''\\n    };\\n} else {\\n    return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":70000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true},\"title\":\"Timeseries table\"}"}',
 'Timeseries table' );
 
 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )

pom.xml 19(+17 -2)

diff --git a/pom.xml b/pom.xml
index 909e255..069c00c 100755
--- a/pom.xml
+++ b/pom.xml
@@ -24,12 +24,12 @@
     <packaging>pom</packaging>
 
     <name>Thingsboard</name>
-    <url>http://thingsboard.io</url>
+    <url>https://thingsboard.io</url>
     <inceptionYear>2016</inceptionYear>
 
     <properties>
         <main.dir>${basedir}</main.dir>
-        <spring-boot.version>1.4.2.RELEASE</spring-boot.version>
+        <spring-boot.version>1.4.3.RELEASE</spring-boot.version>
         <spring.version>4.3.4.RELEASE</spring.version>
         <spring-security.version>4.2.0.RELEASE</spring-security.version>
         <jjwt.version>0.7.0</jjwt.version>
@@ -70,6 +70,7 @@
         <jar-plugin.version>3.0.2</jar-plugin.version>
         <springfox-swagger.version>2.6.1</springfox-swagger.version>
         <bouncycastle.version>1.56</bouncycastle.version>
+        <winsw.version>2.0.1</winsw.version>
     </properties>
 
     <modules>
@@ -128,6 +129,11 @@
                     <version>3.0.2</version>
                 </plugin>
                 <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-assembly-plugin</artifactId>
+                    <version>3.0.0</version>
+                </plugin>
+                <plugin>
                     <groupId>org.springframework.boot</groupId>
                     <artifactId>spring-boot-maven-plugin</artifactId>
                     <version>${spring-boot.version}</version>
@@ -264,6 +270,7 @@
                             <exclude>src/font/**</exclude>
                             <exclude>src/sh/**</exclude>
                             <exclude>src/main/scripts/control/**</exclude>
+                            <exclude>src/main/scripts/windows/**</exclude>
                         </excludes>
                         <mapping>
                             <proto>JAVADOC_STYLE</proto>
@@ -700,6 +707,14 @@
                 <artifactId>bcpkix-jdk15on</artifactId>
                 <version>${bouncycastle.version}</version>
             </dependency>
+            <dependency>
+                <groupId>com.sun.winsw</groupId>
+                <artifactId>winsw</artifactId>
+                <version>${winsw.version}</version>
+                <classifier>bin</classifier>
+                <type>exe</type>
+                <scope>provided</scope>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 

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

diff --git a/ui/package.json b/ui/package.json
index fd69493..365dd0c 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -27,7 +27,7 @@
     "angular-gridster": "^0.13.14",
     "angular-hotkeys": "^1.7.0",
     "angular-jwt": "^0.1.6",
-    "angular-material": "^1.1.1",
+    "angular-material": "1.1.1",
     "angular-material-data-table": "^0.10.9",
     "angular-material-icons": "^0.7.1",
     "angular-messages": "1.5.8",
diff --git a/ui/src/app/api/datasource.service.js b/ui/src/app/api/datasource.service.js
index a519d8b..2da5224 100644
--- a/ui/src/app/api/datasource.service.js
+++ b/ui/src/app/api/datasource.service.js
@@ -357,23 +357,8 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
         return data;
     }
 
-    function generateSeries(dataKey) {
-
+    function generateSeries(dataKey, startTime, endTime) {
         var data = [];
-        var startTime;
-        var endTime;
-
-        if (realtime) {
-            endTime = (new Date).getTime();
-            if (dataKey.lastUpdateTime) {
-                startTime = dataKey.lastUpdateTime + frequency;
-            } else {
-                startTime = endTime - datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
-            }
-        } else {
-            startTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs;
-            endTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs;
-        }
         var prevSeries;
         var datasourceKeyData = datasourceData[dataKey.key];
         if (datasourceKeyData.length > 0) {
@@ -429,9 +414,33 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
     }
 
     function onTick() {
-        for (var key in dataKeys) {
-            dataGenFunction(dataKeys[key]);
+        var key;
+        if (datasourceSubscription.type === types.widgetType.timeseries.value) {
+            var startTime;
+            var endTime;
+            for (key in dataKeys) {
+                var dataKey = dataKeys[key];
+                if (!startTime) {
+                    if (realtime) {
+                        endTime = (new Date).getTime();
+                        if (dataKey.lastUpdateTime) {
+                            startTime = dataKey.lastUpdateTime + frequency;
+                        } else {
+                            startTime = endTime - datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
+                        }
+                    } else {
+                        startTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs;
+                        endTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs;
+                    }
+                }
+                generateSeries(dataKey, startTime, endTime);
+            }
+        } else if (datasourceSubscription.type === types.widgetType.latest.value) {
+            for (key in dataKeys) {
+                generateLatest(dataKeys[key]);
+            }
         }
+
         if (!history) {
             timer = $timeout(onTick, frequency / 2, false);
         }
diff --git a/ui/src/app/api/telemetry-websocket.service.js b/ui/src/app/api/telemetry-websocket.service.js
index 99a7c80..0469364 100644
--- a/ui/src/app/api/telemetry-websocket.service.js
+++ b/ui/src/app/api/telemetry-websocket.service.js
@@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.telemetryWebsocket', [thingsboard
     .factory('telemetryWebsocketService', TelemetryWebsocketService)
     .name;
 
-const RECONNECT_INTERVAL = 5000;
+const RECONNECT_INTERVAL = 2000;
 const WS_IDLE_TIMEOUT = 90000;
 
 /*@ngInject*/
@@ -145,6 +145,7 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
     }
 
     function subscribe (subscriber) {
+        isActive = true;
         var cmdId = nextCmdId();
         subscribers[cmdId] = subscriber;
         subscribersCount++;
@@ -163,19 +164,25 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
     }
 
     function unsubscribe (subscriber) {
-        if (subscriber.subscriptionCommand) {
-            subscriber.subscriptionCommand.unsubscribe = true;
-            if (subscriber.type === types.dataKeyType.timeseries) {
-                cmdsWrapper.tsSubCmds.push(subscriber.subscriptionCommand);
-            } else if (subscriber.type === types.dataKeyType.attribute) {
-                cmdsWrapper.attrSubCmds.push(subscriber.subscriptionCommand);
+        if (isActive) {
+            var cmdId = null;
+            if (subscriber.subscriptionCommand) {
+                subscriber.subscriptionCommand.unsubscribe = true;
+                if (subscriber.type === types.dataKeyType.timeseries) {
+                    cmdsWrapper.tsSubCmds.push(subscriber.subscriptionCommand);
+                } else if (subscriber.type === types.dataKeyType.attribute) {
+                    cmdsWrapper.attrSubCmds.push(subscriber.subscriptionCommand);
+                }
+                cmdId = subscriber.subscriptionCommand.cmdId;
+            } else if (subscriber.historyCommand) {
+                cmdId = subscriber.historyCommand.cmdId;
             }
-            delete subscribers[subscriber.subscriptionCommand.cmdId];
-        } else if (subscriber.historyCommand) {
-            delete subscribers[subscriber.historyCommand.cmdId];
+            if (cmdId && subscribers[cmdId]) {
+                delete subscribers[cmdId];
+                subscribersCount--;
+            }
+            publishCommands();
         }
-        subscribersCount--;
-        publishCommands();
     }
 
     function checkToClose () {
@@ -187,23 +194,24 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
     }
 
     function tryOpenSocket () {
-        isActive = true;
-        if (!isOpened && !isOpening) {
-            isOpening = true;
-            if (userService.isJwtTokenValid()) {
-                openSocket(userService.getJwtToken());
-            } else {
-                userService.refreshJwtToken().then(function success() {
+        if (isActive) {
+            if (!isOpened && !isOpening) {
+                isOpening = true;
+                if (userService.isJwtTokenValid()) {
                     openSocket(userService.getJwtToken());
-                }, function fail() {
-                    isOpening = false;
-                    $rootScope.$broadcast('unauthenticated');
-                });
+                } else {
+                    userService.refreshJwtToken().then(function success() {
+                        openSocket(userService.getJwtToken());
+                    }, function fail() {
+                        isOpening = false;
+                        $rootScope.$broadcast('unauthenticated');
+                    });
+                }
+            }
+            if (socketCloseTimer) {
+                $timeout.cancel(socketCloseTimer);
+                socketCloseTimer = null;
             }
-        }
-        if (socketCloseTimer) {
-            $timeout.cancel(socketCloseTimer);
-            socketCloseTimer = null;
         }
     }
 
@@ -222,7 +230,7 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
         }
     }
 
-    function reset(closeSocket) {
+    function reset(close) {
         if (socketCloseTimer) {
             $timeout.cancel(socketCloseTimer);
             socketCloseTimer = null;
@@ -233,7 +241,7 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
         cmdsWrapper.tsSubCmds = [];
         cmdsWrapper.historyCmds = [];
         cmdsWrapper.attrSubCmds = [];
-        if (closeSocket) {
+        if (close) {
             closeSocket();
         }
     }
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index 2f39429..111f0d3 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -35,12 +35,10 @@
 										tb-mouseup="vm.widgetMouseUp($event, widget)"
 										ng-click=""
 										tb-contextmenu="vm.openWidgetContextMenu($event, widget, $mdOpenMousepointMenu)"
-								style="
-								cursor: pointer;
-								color: {{vm.widgetColor(widget)}};
-								background-color: {{vm.widgetBackgroundColor(widget)}};
-								padding: {{vm.widgetPadding(widget)}}
-								">
+									    ng-style="{cursor: 'pointer',
+            									   color: vm.widgetColor(widget),
+            									   backgroundColor: vm.widgetBackgroundColor(widget),
+            									   padding: vm.widgetPadding(widget)}">
 								<div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
 									<span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span>
 									<tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
diff --git a/ui/src/app/components/menu-toggle.tpl.html b/ui/src/app/components/menu-toggle.tpl.html
index fb7fdf2..454b462 100644
--- a/ui/src/app/components/menu-toggle.tpl.html
+++ b/ui/src/app/components/menu-toggle.tpl.html
@@ -26,7 +26,7 @@
 	class=" pull-right fa fa-chevron-down md-toggle-icon"
 	ng-class="{'tb-toggled' : sectionActive()}"></span>
 </md-button>
-<ul id="docs-menu-{{section.name | nospace}}" class="tb-menu-toggle-list" style="height: {{sectionHeight()}};">
+<ul id="docs-menu-{{section.name | nospace}}" class="tb-menu-toggle-list" ng-style="{height: sectionHeight()}">
 	<li ng-repeat="page in section.pages">
      	<tb-menu-link section="page"></tb-menu-link>
 	</li>
diff --git a/ui/src/app/device/attribute/attribute-table.directive.js b/ui/src/app/device/attribute/attribute-table.directive.js
index a3a4390..63794dc 100644
--- a/ui/src/app/device/attribute/attribute-table.directive.js
+++ b/ui/src/app/device/attribute/attribute-table.directive.js
@@ -119,6 +119,10 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
                 scope.attributesDeferred.resolve();
             }
             if (scope.deviceId && scope.attributeScope) {
+                scope.attributes = {
+                    count: 0,
+                    data: []
+                };
                 scope.checkSubscription();
                 scope.attributesDeferred = deviceService.getDeviceAttributes(scope.deviceId, scope.attributeScope.value,
                     scope.query, function(attributes, update) {