thingsboard-aplcache
Changes
application/pom.xml 2(+1 -1)
application/src/test/java/org/thingsboard/server/mqtt/rpc/MqttServerSideRpcIntegrationTest.java 221(+221 -0)
common/data/pom.xml 2(+1 -1)
common/message/pom.xml 2(+1 -1)
common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java 14(+10 -4)
common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesRequest.java 5(+3 -2)
common/pom.xml 2(+1 -1)
common/transport/pom.xml 2(+1 -1)
dao/pom.xml 2(+1 -1)
dao/src/main/resources/system-data.cql 11(+8 -3)
docker/docker-compose.yml 4(+2 -2)
extensions/extension-kafka/pom.xml 2(+1 -1)
extensions/pom.xml 2(+1 -1)
extensions-api/pom.xml 2(+1 -1)
extensions-core/pom.xml 2(+1 -1)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java 4(+0 -4)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java 9(+9 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java 5(+0 -5)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java 21(+14 -7)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java 83(+75 -8)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java 10(+7 -3)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java 15(+13 -2)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java 17(+17 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java 2(+1 -1)
pom.xml 2(+1 -1)
tools/pom.xml 2(+1 -1)
transport/coap/pom.xml 2(+1 -1)
transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java 18(+7 -11)
transport/http/pom.xml 2(+1 -1)
transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java 25(+14 -11)
transport/mqtt/pom.xml 2(+1 -1)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java 11(+8 -3)
transport/pom.xml 2(+1 -1)
ui/pom.xml 2(+1 -1)
ui/src/app/api/device.service.js 3(+2 -1)
ui/src/app/app.js 2(+2 -0)
ui/src/app/components/dashboard.directive.js 138(+123 -15)
ui/src/app/components/dashboard.scss 2(+2 -0)
ui/src/app/components/dashboard.tpl.html 144(+85 -59)
ui/src/app/components/datasource.scss 12(+12 -0)
ui/src/app/components/datasource-device.scss 16(+16 -0)
ui/src/app/components/datasource-device.tpl.html 58(+34 -24)
ui/src/app/components/datasource-func.tpl.html 33(+19 -14)
ui/src/app/components/details-sidenav.scss 13(+13 -0)
ui/src/app/dashboard/dashboard.controller.js 217(+209 -8)
ui/src/app/dashboard/dashboard.tpl.html 36(+30 -6)
ui/src/app/dashboard/index.js 2(+2 -0)
ui/src/app/layout/home.scss 5(+5 -0)
ui/src/app/layout/home.tpl.html 4(+2 -2)
ui/src/app/services/item-buffer.service.js 191(+191 -0)
ui/src/locale/en_US.json 7(+5 -2)
ui/src/scss/main.scss 3(+3 -0)
Details
application/pom.xml 2(+1 -1)
diff --git a/application/pom.xml b/application/pom.xml
index d0c7035..a5559ee 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/MqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/MqttServerSideRpcIntegrationTest.java
new file mode 100644
index 0000000..55e705f
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/MqttServerSideRpcIntegrationTest.java
@@ -0,0 +1,221 @@
+/**
+ * Copyright © 2016 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.
+ */
+package org.thingsboard.server.mqtt.rpc;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.*;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.HttpClientErrorException;
+import org.thingsboard.client.tools.RestClient;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.mqtt.AbstractFeatureIntegrationTest;
+
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author Valerii Sosliuk
+ */
+@Slf4j
+public class MqttServerSideRpcIntegrationTest extends AbstractFeatureIntegrationTest {
+
+ private static final String MQTT_URL = "tcp://localhost:1883";
+ private static final String BASE_URL = "http://localhost:8080";
+
+ private static final String USERNAME = "tenant@thingsboard.org";
+ private static final String PASSWORD = "tenant";
+
+ private RestClient restClient;
+
+ @Before
+ public void beforeTest() throws Exception {
+ restClient = new RestClient(BASE_URL);
+ restClient.login(USERNAME, PASSWORD);
+ }
+
+ @Test
+ public void testServerMqttOneWayRpc() throws Exception {
+ Device device = new Device();
+ device.setName("Test One-Way Server-Side RPC");
+ Device savedDevice = getSavedDevice(device);
+ DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice);
+ assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+ String accessToken = deviceCredentials.getCredentialsId();
+ assertNotNull(accessToken);
+
+ String clientId = MqttAsyncClient.generateClientId();
+ MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId);
+
+ MqttConnectOptions options = new MqttConnectOptions();
+ options.setUserName(accessToken);
+ client.connect(options);
+ Thread.sleep(3000);
+ client.subscribe("v1/devices/me/rpc/request/+", 1);
+ client.setCallback(new TestMqttCallback(client));
+
+ String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
+ String deviceId = savedDevice.getId().getId().toString();
+ ResponseEntity result = restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class);
+ Assert.assertEquals(HttpStatus.OK, result.getStatusCode());
+ Assert.assertNull(result.getBody());
+ }
+
+ @Test
+ public void testServerMqttOneWayRpcDeviceOffline() throws Exception {
+ Device device = new Device();
+ device.setName("Test One-Way Server-Side RPC Device Offline");
+ Device savedDevice = getSavedDevice(device);
+ DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice);
+ assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+ String accessToken = deviceCredentials.getCredentialsId();
+ assertNotNull(accessToken);
+
+ String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
+ String deviceId = savedDevice.getId().getId().toString();
+ try {
+ restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class);
+ Assert.fail("HttpClientErrorException expected, but not encountered");
+ } catch (HttpClientErrorException e) {
+ log.error(e.getMessage(), e);
+ Assert.assertEquals(HttpStatus.REQUEST_TIMEOUT, e.getStatusCode());
+ Assert.assertEquals("408 null", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testServerMqttOneWayRpcDeviceDoesNotExist() throws Exception {
+ String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
+ String nonExistentDeviceId = UUID.randomUUID().toString();
+ try {
+ restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class);
+ Assert.fail("HttpClientErrorException expected, but not encountered");
+ } catch (HttpClientErrorException e) {
+ log.error(e.getMessage(), e);
+ Assert.assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode());
+ Assert.assertEquals("400 null", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testServerMqttTwoWayRpc() throws Exception {
+
+ Device device = new Device();
+ device.setName("Test Two-Way Server-Side RPC");
+ Device savedDevice = getSavedDevice(device);
+ DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice);
+ assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+ String accessToken = deviceCredentials.getCredentialsId();
+ assertNotNull(accessToken);
+
+ String clientId = MqttAsyncClient.generateClientId();
+ MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId);
+
+ MqttConnectOptions options = new MqttConnectOptions();
+ options.setUserName(accessToken);
+ client.connect(options);
+ Thread.sleep(3000);
+ client.subscribe("v1/devices/me/rpc/request/+", 1);
+ client.setCallback(new TestMqttCallback(client));
+
+ String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
+ String deviceId = savedDevice.getId().getId().toString();
+ String result = getStringResult(setGpioRequest, "twoway", deviceId);
+ Assert.assertEquals("{\"value1\":\"A\",\"value2\":\"B\"}", result);
+ }
+
+ @Test
+ public void testServerMqttTwoWayRpcDeviceOffline() throws Exception {
+ Device device = new Device();
+ device.setName("Test Two-Way Server-Side RPC Device Offline");
+ Device savedDevice = getSavedDevice(device);
+ DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice);
+ assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+ String accessToken = deviceCredentials.getCredentialsId();
+ assertNotNull(accessToken);
+
+ String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
+ String deviceId = savedDevice.getId().getId().toString();
+ try {
+ restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class);
+ Assert.fail("HttpClientErrorException expected, but not encountered");
+ } catch (HttpClientErrorException e) {
+ log.error(e.getMessage(), e);
+ Assert.assertEquals(HttpStatus.REQUEST_TIMEOUT, e.getStatusCode());
+ Assert.assertEquals("408 null", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testServerMqttTwoWayRpcDeviceDoesNotExist() throws Exception {
+ String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
+ String nonExistentDeviceId = UUID.randomUUID().toString();
+ try {
+ restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class);
+ Assert.fail("HttpClientErrorException expected, but not encountered");
+ } catch (HttpClientErrorException e) {
+ log.error(e.getMessage(), e);
+ Assert.assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode());
+ Assert.assertEquals("400 null", e.getMessage());
+ }
+ }
+
+ private Device getSavedDevice(Device device) {
+ return restClient.getRestTemplate().postForEntity(BASE_URL + "/api/device", device, Device.class).getBody();
+ }
+
+ private DeviceCredentials getDeviceCredentials(Device savedDevice) {
+ return restClient.getRestTemplate().getForEntity(BASE_URL + "/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class).getBody();
+ }
+
+ private String getStringResult(String requestData, String callType, String deviceId) {
+ return restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/" + callType + "/" + deviceId, requestData, String.class).getBody();
+ }
+
+ private static class TestMqttCallback implements MqttCallback {
+
+ private final MqttAsyncClient client;
+
+ TestMqttCallback(MqttAsyncClient client) {
+ this.client = client;
+ }
+
+ @Override
+ public void connectionLost(Throwable throwable) {
+ }
+
+ @Override
+ public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception {
+ log.info("Message Arrived: " + mqttMessage.getPayload().toString());
+ MqttMessage message = new MqttMessage();
+ String responseTopic = requestTopic.replace("request", "response");
+ message.setPayload("{\"value1\":\"A\", \"value2\":\"B\"}".getBytes());
+ client.publish(responseTopic, message);
+ }
+
+ @Override
+ public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
+
+ }
+ }
+}
common/data/pom.xml 2(+1 -1)
diff --git a/common/data/pom.xml b/common/data/pom.xml
index 6ba5e01..971f570 100644
--- a/common/data/pom.xml
+++ b/common/data/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
index 34ae9b2..a446680 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
@@ -29,6 +29,8 @@ public class DataConstants {
public static final String SERVER_SCOPE = "SERVER_SCOPE";
public static final String SHARED_SCOPE = "SHARED_SCOPE";
+ public static final String[] ALL_SCOPES = {CLIENT_SCOPE, SHARED_SCOPE, SERVER_SCOPE};
+
public static final String ALARM = "ALARM";
public static final String ERROR = "ERROR";
public static final String LC_EVENT = "LC_EVENT";
common/message/pom.xml 2(+1 -1)
diff --git a/common/message/pom.xml b/common/message/pom.xml
index 28144f0..13ac70f 100644
--- a/common/message/pom.xml
+++ b/common/message/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java
index a8fdc86..676cfb7 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java
@@ -18,6 +18,8 @@ package org.thingsboard.server.common.msg.core;
import lombok.ToString;
import org.thingsboard.server.common.msg.session.MsgType;
+import java.util.Collections;
+import java.util.Optional;
import java.util.Set;
@ToString
@@ -28,6 +30,10 @@ public class BasicGetAttributesRequest extends BasicRequest implements GetAttrib
private final Set<String> clientKeys;
private final Set<String> sharedKeys;
+ public BasicGetAttributesRequest(Integer requestId) {
+ this(requestId, Collections.emptySet(), Collections.emptySet());
+ }
+
public BasicGetAttributesRequest(Integer requestId, Set<String> clientKeys, Set<String> sharedKeys) {
super(requestId);
this.clientKeys = clientKeys;
@@ -40,13 +46,13 @@ public class BasicGetAttributesRequest extends BasicRequest implements GetAttrib
}
@Override
- public Set<String> getClientAttributeNames() {
- return clientKeys;
+ public Optional<Set<String>> getClientAttributeNames() {
+ return Optional.of(clientKeys);
}
@Override
- public Set<String> getSharedAttributeNames() {
- return sharedKeys;
+ public Optional<Set<String>> getSharedAttributeNames() {
+ return Optional.ofNullable(sharedKeys);
}
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesRequest.java
index 49bca53..0a9e1c2 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesRequest.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesRequest.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.msg.core;
+import java.util.Optional;
import java.util.Set;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
@@ -22,7 +23,7 @@ import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
public interface GetAttributesRequest extends FromDeviceRequestMsg {
- Set<String> getClientAttributeNames();
- Set<String> getSharedAttributeNames();
+ Optional<Set<String>> getClientAttributeNames();
+ Optional<Set<String>> getSharedAttributeNames();
}
common/pom.xml 2(+1 -1)
diff --git a/common/pom.xml b/common/pom.xml
index 77a44ac..4494af7 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
common/transport/pom.xml 2(+1 -1)
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
index 8bcd650..c49c6eb 100644
--- a/common/transport/pom.xml
+++ b/common/transport/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
dao/pom.xml 2(+1 -1)
diff --git a/dao/pom.xml b/dao/pom.xml
index de56032..a2edc7a 100644
--- a/dao/pom.xml
+++ b/dao/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
dao/src/main/resources/system-data.cql 11(+8 -3)
diff --git a/dao/src/main/resources/system-data.cql b/dao/src/main/resources/system-data.cql
index 06b9351..c538a1c 100644
--- a/dao/src/main/resources/system-data.cql
+++ b/dao/src/main/resources/system-data.cql
@@ -134,7 +134,7 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'google_maps',
INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'route_map',
-'{"type":"timeseries","sizeX":8.5,"sizeY":6,"resources":[],"templateHtml":"","templateCss":".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 100px;\n white-space: nowrap;\n}","controllerScript":"var map;\n\nvar routesSettings = [];\nvar routes;\nvar polylines = [];\n\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n if (settings.defaultZoomLevel) {\n if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n }\n }\n \n dontFitMapBounds = settings.fitMapBounds === false;\n \n function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n var match = varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split('':'');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith(''#'')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = datasourceOffset + n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < datasource.dataKeys.length; i++) {\n var dataKey = datasource.dataKeys[i];\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = datasourceOffset + i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n \n var configuredRoutesSettings = settings.routesSettings;\n if (!configuredRoutesSettings) {\n configuredRoutesSettings = [];\n }\n \n var datasourceOffset = 0;\n for (var i=0;i<datasources.length;i++) {\n routesSettings[i] = {\n latKeyName: \"lat\",\n lngKeyName: \"lng\",\n showLabel: true,\n label: datasources[i].name, \n color: \"#FE7569\",\n strokeWeight: 2,\n strokeOpacity: 1.0,\n tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n };\n if (configuredRoutesSettings[i]) {\n routesSettings[i].latKeyName = configuredRoutesSettings[i].latKeyName || routesSettings[i].latKeyName;\n routesSettings[i].lngKeyName = configuredRoutesSettings[i].lngKeyName || routesSettings[i].lngKeyName;\n routesSettings[i].tooltipPattern = configuredRoutesSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+routesSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+routesSettings[i].lngKeyName+\":7}\";\n \n routesSettings[i].tooltipReplaceInfo = procesTooltipPattern(routesSettings[i].tooltipPattern, datasources[i], datasourceOffset);\n \n routesSettings[i].showLabel = configuredRoutesSettings[i].showLabel !== false;\n routesSettings[i].label = configuredRoutesSettings[i].label || routesSettings[i].label;\n routesSettings[i].color = configuredRoutesSettings[i].color ? tinycolor(configuredRoutesSettings[i].color).toHexString() : routesSettings[i].color;\n routesSettings[i].strokeWeight = configuredRoutesSettings[i].strokeWeight || routesSettings[i].strokeWeight;\n routesSettings[i].strokeOpacity = typeof configuredRoutesSettings[i].strokeOpacity !== \"undefined\" ? configuredRoutesSettings[i].strokeOpacity : routesSettings[i].strokeOpacity; \n }\n datasourceOffset += datasources[i].dataKeys.length;\n }\n\n var mapId = '''' + Math.random().toString(36).substr(2, 9);\n \n function clearGlobalId() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n }\n }\n \n $window.gm_authFailure = function() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n $window.gmApiKeys[apiKey].error = ''Unable to authentificate for Google Map API.</br>Please check your API key.'';\n displayError($window.gmApiKeys[apiKey].error);\n }\n };\n \n function displayError(message) {\n $(containerElement).html(\n \"<div class=''error''>\"+ message + \"</div>\"\n );\n }\n\n var initMapFunctionName = ''initGoogleMap_'' + mapId;\n $window[initMapFunctionName] = function() {\n lazyLoad.load({ type: ''js'', path: ''https://cdn.rawgit.com/googlemaps/v3-utility-library/master/markerwithlabel/src/markerwithlabel.js'' }).then(\n function success() {\n initMap();\n },\n function fail() {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n \n }; \n \n var apiKey = settings.gmApiKey || '''';\n\n if (apiKey && apiKey.length > 0) {\n if (!$window.gmApiKeys) {\n $window.gmApiKeys = {};\n }\n if ($window.gmApiKeys[apiKey]) {\n if ($window.gmApiKeys[apiKey].error) {\n displayError($window.gmApiKeys[apiKey].error);\n } else {\n initMap();\n }\n } else {\n $window.gmApiKeys[apiKey] = {};\n var googleMapScriptRes = ''https://maps.googleapis.com/maps/api/js?key=''+apiKey+''&callback=''+initMapFunctionName;\n \n $window.loadingGmId = mapId;\n lazyLoad.load({ type: ''js'', path: googleMapScriptRes }).then(\n function success() {\n setTimeout(clearGlobalId, 2000);\n },\n function fail(e) {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n }\n } else {\n displayError(''No Google Map Api Key provided!'');\n }\n\n function initMap() {\n \n map = new google.maps.Map(containerElement, {\n scrollwheel: false,\n zoom: defaultZoomLevel || 8\n });\n\n }\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n \n function padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n \n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n \n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split(''.'');\n s = int - strVal[0].length;\n \n for (; i < s; ++i) {\n strVal[0] = ''0'' + strVal[0];\n }\n \n strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n }\n \n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n \n for (; i < s; ++i) {\n strVal = ''0'' + strVal;\n }\n \n strVal = (n ? ''-'' : '''') + strVal;\n }\n \n return strVal;\n } \n \n function createMarker(location, settings) {\n var pinColor = settings.color.substr(1);\n var pinImage = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|\" + pinColor,\n new google.maps.Size(21, 34),\n new google.maps.Point(0,0),\n new google.maps.Point(10, 34));\n var pinShadow = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_shadow\",\n new google.maps.Size(40, 37),\n new google.maps.Point(0, 0),\n new google.maps.Point(12, 35)); \n var marker;\n if (settings.showLabel) { \n marker = new MarkerWithLabel({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow,\n labelContent: ''<b>''+settings.label+''</b>'',\n labelClass: \"tb-labels\",\n labelAnchor: new google.maps.Point(50, 55)\n }); \n } else {\n marker = new google.maps.Marker({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow\n }); \n }\n \n createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n \n return marker; \n }\n \n function createTooltip(marker, pattern, replaceInfo) {\n var infowindow = new google.maps.InfoWindow({\n content: ''''\n });\n marker.addListener(''click'', function() {\n infowindow.open(map, marker);\n });\n tooltips.push( {\n infowindow: infowindow,\n pattern: pattern,\n replaceInfo: replaceInfo\n });\n }\n\n function createPolyline(locations, settings) {\n var polyline = new google.maps.Polyline({\n path: locations,\n strokeColor: settings.color,\n strokeOpacity: settings.strokeOpacity,\n strokeWeight: settings.strokeWeight,\n map: map\n });\n \n return polyline; \n } \n \n function arraysEqual(a, b) {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (a.length != b.length) return false;\n\n for (var i = 0; i < a.length; ++i) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n }\n \n \n function updateRoute(route, data) {\n if (route.latIndex > -1 && route.lngIndex > -1) {\n var latData = data[route.latIndex].data;\n var lngData = data[route.lngIndex].data;\n if (latData.length > 0 && lngData.length > 0) {\n var locations = [];\n for (var i = 0; i < latData.length; i++) {\n var lat = latData[i][1];\n var lng = lngData[i][1];\n var location = new google.maps.LatLng(lat, lng);\n locations.push(location);\n }\n var markerLocation;\n if (locations.length > 0) {\n markerLocation = locations[locations.length-1];\n }\n if (!route.polyline) {\n route.polyline = createPolyline(locations, route.settings);\n if (markerLocation) {\n route.marker = createMarker(markerLocation, route.settings);\n }\n polylines.push(route.polyline);\n return true;\n } else {\n var prevPath = route.polyline.getPath();\n if (!prevPath || !arraysEqual(prevPath.getArray(), locations)) {\n route.polyline.setPath(locations);\n if (markerLocation) {\n if (!route.marker) {\n route.marker = createMarker(markerLocation, route.settings);\n } else {\n route.marker.setPosition(markerLocation);\n }\n }\n return true;\n }\n }\n }\n }\n return false;\n }\n \n function extendBounds(bounds, polyline) {\n if (polyline && polyline.getPath()) {\n var locations = polyline.getPath();\n for (var i = 0; i < locations.getLength(); i++) {\n bounds.extend(locations.getAt(i));\n }\n }\n }\n \n function loadRoutes(data) {\n var bounds = new google.maps.LatLngBounds();\n routes = [];\n var datasourceIndex = -1;\n var routeSettings;\n var datasource;\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n if (!datasource || datasource != datasourceData.datasource) {\n datasourceIndex++;\n datasource = datasourceData.datasource;\n routeSettings = routesSettings[datasourceIndex];\n }\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === routeSettings.latKeyName ||\n dataKey.label === routeSettings.lngKeyName) {\n var route = routes[datasourceIndex];\n if (!route) {\n route = {\n latIndex: -1,\n lngIndex: -1,\n settings: routeSettings\n };\n routes[datasourceIndex] = route;\n } else if (route.polyline) {\n continue;\n }\n if (dataKey.label === routeSettings.latKeyName) {\n route.latIndex = i;\n } else {\n route.lngIndex = i;\n }\n if (route.latIndex > -1 && route.lngIndex > -1) {\n updateRoute(route, data);\n if (route.polyline) {\n extendBounds(bounds, route.polyline);\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n \n function updateRoutes(data) {\n var routesChanged = false;\n var bounds = new google.maps.LatLngBounds();\n for (var r in routes) {\n var route = routes[r];\n routesChanged |= updateRoute(route, data);\n if (route.polyline) {\n extendBounds(bounds, route.polyline);\n }\n }\n if (!dontFitMapBounds && routesChanged) {\n fitMapBounds(bounds);\n }\n }\n \n function fitMapBounds(bounds) {\n google.maps.event.addListenerOnce(map, ''bounds_changed'', function(event) {\n var zoomLevel = defaultZoomLevel || map.getZoom();\n this.setZoom(zoomLevel);\n if (!defaultZoomLevel && this.getZoom() > 15) {\n this.setZoom(15);\n }\n });\n map.fitBounds(bounds);\n }\n\n if (map) {\n if (data) {\n if (!routes) {\n loadRoutes(data);\n } else {\n updateRoutes(data);\n }\n }\n if (sizeChanged) {\n google.maps.event.trigger(map, \"resize\");\n if (!dontFitMapBounds) {\n var bounds = new google.maps.LatLngBounds();\n for (var p in polylines) {\n extendBounds(bounds, polylines[p]);\n }\n fitMapBounds(bounds);\n }\n }\n \n for (var t in tooltips) {\n var tooltip = tooltips[t];\n var text = tooltip.pattern;\n var replaceInfo = tooltip.replaceInfo;\n for (var v in replaceInfo.variables) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '''';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n tooltip.infowindow.setContent(text);\n }\n \n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Route Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"gmApiKey\": {\n \"title\": \"Google Maps API Key\",\n \"type\": \"string\"\n },\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all routes\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"routesSettings\": {\n \"title\": \"Routes settings, same order as datasources\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Route settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"tooltipPattern\": {\n \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units'' )\",\n \"type\": \"string\",\n \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n },\n \"strokeWeight\": {\n \"title\": \"Stroke weight\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"strokeOpacity\": {\n \"title\": \"Stroke opacity\",\n \"type\": \"number\",\n \"default\": 1.0\n }\n }\n }\n }\n },\n \"required\": [\n \"gmApiKey\"\n ]\n },\n \"form\": [\n \"gmApiKey\",\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"routesSettings\",\n \"items\": [\n \"routesSettings[].latKeyName\",\n \"routesSettings[].lngKeyName\",\n \"routesSettings[].showLabel\",\n \"routesSettings[].label\",\n \"routesSettings[].tooltipPattern\",\n {\n \"key\": \"routesSettings[].color\",\n \"type\": \"color\"\n },\n \"routesSettings[].strokeWeight\",\n \"routesSettings[].strokeOpacity\"\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.3467277073670627,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.058309787276281666,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"}],\"intervalSec\":60}],\"timewindow\":{\"realtime\":{\"timewindowMs\":30000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"fitMapBounds\":false,\"routesSettings\":[{\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"color\":\"#1976d2\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"label\":\"First route\",\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\"}],\"defaultZoomLevel\":16},\"title\":\"Route Map\"}"}',
+'{"type":"timeseries","sizeX":8.5,"sizeY":6,"resources":[],"templateHtml":"","templateCss":".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 100px;\n white-space: nowrap;\n}","controllerScript":"var map;\n\nvar routesSettings = [];\nvar routes;\nvar polylines = [];\n\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n if (settings.defaultZoomLevel) {\n if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n }\n }\n \n dontFitMapBounds = settings.fitMapBounds === false;\n \n function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n var match = varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split('':'');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith(''#'')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = datasourceOffset + n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < datasource.dataKeys.length; i++) {\n var dataKey = datasource.dataKeys[i];\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = datasourceOffset + i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n \n var configuredRoutesSettings = settings.routesSettings;\n if (!configuredRoutesSettings) {\n configuredRoutesSettings = [];\n }\n \n var datasourceOffset = 0;\n for (var i=0;i<datasources.length;i++) {\n routesSettings[i] = {\n latKeyName: \"lat\",\n lngKeyName: \"lng\",\n showLabel: true,\n label: datasources[i].name, \n color: \"#FE7569\",\n strokeWeight: 2,\n strokeOpacity: 1.0,\n tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n };\n if (configuredRoutesSettings[i]) {\n routesSettings[i].latKeyName = configuredRoutesSettings[i].latKeyName || routesSettings[i].latKeyName;\n routesSettings[i].lngKeyName = configuredRoutesSettings[i].lngKeyName || routesSettings[i].lngKeyName;\n routesSettings[i].tooltipPattern = configuredRoutesSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+routesSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+routesSettings[i].lngKeyName+\":7}\";\n \n routesSettings[i].tooltipReplaceInfo = procesTooltipPattern(routesSettings[i].tooltipPattern, datasources[i], datasourceOffset);\n \n routesSettings[i].showLabel = configuredRoutesSettings[i].showLabel !== false;\n routesSettings[i].label = configuredRoutesSettings[i].label || routesSettings[i].label;\n routesSettings[i].color = configuredRoutesSettings[i].color ? tinycolor(configuredRoutesSettings[i].color).toHexString() : routesSettings[i].color;\n routesSettings[i].strokeWeight = configuredRoutesSettings[i].strokeWeight || routesSettings[i].strokeWeight;\n routesSettings[i].strokeOpacity = typeof configuredRoutesSettings[i].strokeOpacity !== \"undefined\" ? configuredRoutesSettings[i].strokeOpacity : routesSettings[i].strokeOpacity; \n }\n datasourceOffset += datasources[i].dataKeys.length;\n }\n\n var mapId = '''' + Math.random().toString(36).substr(2, 9);\n \n function clearGlobalId() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n }\n }\n \n $window.gm_authFailure = function() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n $window.gmApiKeys[apiKey].error = ''Unable to authentificate for Google Map API.</br>Please check your API key.'';\n displayError($window.gmApiKeys[apiKey].error);\n }\n };\n \n function displayError(message) {\n $(containerElement).html(\n \"<div class=''error''>\"+ message + \"</div>\"\n );\n }\n\n var initMapFunctionName = ''initGoogleMap_'' + mapId;\n $window[initMapFunctionName] = function() {\n lazyLoad.load({ type: ''js'', path: ''https://cdn.rawgit.com/googlemaps/v3-utility-library/master/markerwithlabel/src/markerwithlabel.js'' }).then(\n function success() {\n initMap();\n },\n function fail() {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n \n }; \n \n var apiKey = settings.gmApiKey || '''';\n\n if (apiKey && apiKey.length > 0) {\n if (!$window.gmApiKeys) {\n $window.gmApiKeys = {};\n }\n if ($window.gmApiKeys[apiKey]) {\n if ($window.gmApiKeys[apiKey].error) {\n displayError($window.gmApiKeys[apiKey].error);\n } else {\n initMap();\n }\n } else {\n $window.gmApiKeys[apiKey] = {};\n var googleMapScriptRes = ''https://maps.googleapis.com/maps/api/js?key=''+apiKey+''&callback=''+initMapFunctionName;\n \n $window.loadingGmId = mapId;\n lazyLoad.load({ type: ''js'', path: googleMapScriptRes }).then(\n function success() {\n setTimeout(clearGlobalId, 2000);\n },\n function fail(e) {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n }\n } else {\n displayError(''No Google Map Api Key provided!'');\n }\n\n function initMap() {\n \n map = new google.maps.Map(containerElement, {\n scrollwheel: false,\n zoom: defaultZoomLevel || 8\n });\n\n }\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n \n function padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n \n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n \n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split(''.'');\n s = int - strVal[0].length;\n \n for (; i < s; ++i) {\n strVal[0] = ''0'' + strVal[0];\n }\n \n strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n }\n \n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n \n for (; i < s; ++i) {\n strVal = ''0'' + strVal;\n }\n \n strVal = (n ? ''-'' : '''') + strVal;\n }\n \n return strVal;\n } \n \n function createMarker(location, settings) {\n var pinColor = settings.color.substr(1);\n var pinImage = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|\" + pinColor,\n new google.maps.Size(21, 34),\n new google.maps.Point(0,0),\n new google.maps.Point(10, 34));\n var pinShadow = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_shadow\",\n new google.maps.Size(40, 37),\n new google.maps.Point(0, 0),\n new google.maps.Point(12, 35)); \n var marker;\n if (settings.showLabel) { \n marker = new MarkerWithLabel({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow,\n labelContent: ''<b>''+settings.label+''</b>'',\n labelClass: \"tb-labels\",\n labelAnchor: new google.maps.Point(50, 55)\n }); \n } else {\n marker = new google.maps.Marker({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow\n }); \n }\n \n createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n \n return marker; \n }\n \n function createTooltip(marker, pattern, replaceInfo) {\n var infowindow = new google.maps.InfoWindow({\n content: ''''\n });\n marker.addListener(''click'', function() {\n infowindow.open(map, marker);\n });\n tooltips.push( {\n infowindow: infowindow,\n pattern: pattern,\n replaceInfo: replaceInfo\n });\n }\n\n function createPolyline(locations, settings) {\n var polyline = new google.maps.Polyline({\n path: locations,\n strokeColor: settings.color,\n strokeOpacity: settings.strokeOpacity,\n strokeWeight: settings.strokeWeight,\n map: map\n });\n \n return polyline; \n } \n \n function arraysEqual(a, b) {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (a.length != b.length) return false;\n\n for (var i = 0; i < a.length; ++i) {\n if (!a[i].equals(b[i])) return false;\n }\n return true;\n }\n \n \n function updateRoute(route, data) {\n if (route.latIndex > -1 && route.lngIndex > -1) {\n var latData = data[route.latIndex].data;\n var lngData = data[route.lngIndex].data;\n if (latData.length > 0 && lngData.length > 0) {\n var locations = [];\n for (var i = 0; i < latData.length; i++) {\n var lat = latData[i][1];\n var lng = lngData[i][1];\n var location = new google.maps.LatLng(lat, lng);\n if (i == 0 || !locations[locations.length-1].equals(location)) {\n locations.push(location);\n }\n }\n var markerLocation;\n if (locations.length > 0) {\n markerLocation = locations[locations.length-1];\n }\n if (!route.polyline) {\n route.polyline = createPolyline(locations, route.settings);\n if (markerLocation) {\n route.marker = createMarker(markerLocation, route.settings);\n }\n polylines.push(route.polyline);\n return true;\n } else {\n var prevPath = route.polyline.getPath();\n if (!prevPath || !arraysEqual(prevPath.getArray(), locations)) {\n route.polyline.setPath(locations);\n if (markerLocation) {\n if (!route.marker) {\n route.marker = createMarker(markerLocation, route.settings);\n } else {\n route.marker.setPosition(markerLocation);\n }\n }\n return true;\n }\n }\n }\n }\n return false;\n }\n \n function extendBounds(bounds, polyline) {\n if (polyline && polyline.getPath()) {\n var locations = polyline.getPath();\n for (var i = 0; i < locations.getLength(); i++) {\n bounds.extend(locations.getAt(i));\n }\n }\n }\n \n function loadRoutes(data) {\n var bounds = new google.maps.LatLngBounds();\n routes = [];\n var datasourceIndex = -1;\n var routeSettings;\n var datasource;\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n if (!datasource || datasource != datasourceData.datasource) {\n datasourceIndex++;\n datasource = datasourceData.datasource;\n routeSettings = routesSettings[datasourceIndex];\n }\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === routeSettings.latKeyName ||\n dataKey.label === routeSettings.lngKeyName) {\n var route = routes[datasourceIndex];\n if (!route) {\n route = {\n latIndex: -1,\n lngIndex: -1,\n settings: routeSettings\n };\n routes[datasourceIndex] = route;\n } else if (route.polyline) {\n continue;\n }\n if (dataKey.label === routeSettings.latKeyName) {\n route.latIndex = i;\n } else {\n route.lngIndex = i;\n }\n if (route.latIndex > -1 && route.lngIndex > -1) {\n updateRoute(route, data);\n if (route.polyline) {\n extendBounds(bounds, route.polyline);\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n \n function updateRoutes(data) {\n var routesChanged = false;\n var bounds = new google.maps.LatLngBounds();\n for (var r in routes) {\n var route = routes[r];\n routesChanged |= updateRoute(route, data);\n if (route.polyline) {\n extendBounds(bounds, route.polyline);\n }\n }\n if (!dontFitMapBounds && routesChanged) {\n fitMapBounds(bounds);\n }\n }\n \n function fitMapBounds(bounds) {\n google.maps.event.addListenerOnce(map, ''bounds_changed'', function(event) {\n var newZoomLevel = map.getZoom();\n if (dontFitMapBounds && defaultZoomLevel) {\n newZoomLevel = defaultZoomLevel;\n }\n map.setZoom(newZoomLevel);\n if (!defaultZoomLevel && map.getZoom() > 18) {\n map.setZoom(18);\n }\n });\n map.fitBounds(bounds);\n }\n\n if (map) {\n if (data) {\n if (!routes) {\n loadRoutes(data);\n } else {\n updateRoutes(data);\n }\n }\n if (sizeChanged) {\n google.maps.event.trigger(map, \"resize\");\n if (!dontFitMapBounds) {\n var bounds = new google.maps.LatLngBounds();\n for (var p in polylines) {\n extendBounds(bounds, polylines[p]);\n }\n fitMapBounds(bounds);\n }\n }\n \n for (var t in tooltips) {\n var tooltip = tooltips[t];\n var text = tooltip.pattern;\n var replaceInfo = tooltip.replaceInfo;\n for (var v in replaceInfo.variables) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '''';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n tooltip.infowindow.setContent(text);\n }\n \n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Route Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"gmApiKey\": {\n \"title\": \"Google Maps API Key\",\n \"type\": \"string\"\n },\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all routes\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"routesSettings\": {\n \"title\": \"Routes settings, same order as datasources\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Route settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"tooltipPattern\": {\n \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units'' )\",\n \"type\": \"string\",\n \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n },\n \"strokeWeight\": {\n \"title\": \"Stroke weight\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"strokeOpacity\": {\n \"title\": \"Stroke opacity\",\n \"type\": \"number\",\n \"default\": 1.0\n }\n }\n }\n }\n },\n \"required\": [\n \"gmApiKey\"\n ]\n },\n \"form\": [\n \"gmApiKey\",\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"routesSettings\",\n \"items\": [\n \"routesSettings[].latKeyName\",\n \"routesSettings[].lngKeyName\",\n \"routesSettings[].showLabel\",\n \"routesSettings[].label\",\n \"routesSettings[].tooltipPattern\",\n {\n \"key\": \"routesSettings[].color\",\n \"type\": \"color\"\n },\n \"routesSettings[].strokeWeight\",\n \"routesSettings[].strokeOpacity\"\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.3467277073670627,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.058309787276281666,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"}],\"intervalSec\":60}],\"timewindow\":{\"realtime\":{\"timewindowMs\":30000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"fitMapBounds\":true,\"routesSettings\":[{\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"color\":\"#1976d2\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"label\":\"First route\",\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\"}]},\"title\":\"Route Map\"}"}',
'Route Map' );
INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
@@ -203,11 +203,16 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'digital_vertical_bar',
'Digital vertical bar' );
INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
-VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'openstreetmap',
-'{"type":"latest","sizeX":8.5,"sizeY":6,"resources":[{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.css"},{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.js"}],"templateHtml":"","templateCss":".tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n","controllerScript":"var map;\nvar positions;\nvar markers = [];\nvar markersSettings = [];\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n if (settings.defaultZoomLevel) {\n if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n }\n }\n \n dontFitMapBounds = settings.fitMapBounds === false;\n \n function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n var match = varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split('':'');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith(''#'')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = datasourceOffset + n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < datasource.dataKeys.length; i++) {\n var dataKey = datasource.dataKeys[i];\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = datasourceOffset + i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = varsRegex.exec(pattern);\n }\n return replaceInfo;\n } \n \n var configuredMarkersSettings = settings.markersSettings;\n if (!configuredMarkersSettings) {\n configuredMarkersSettings = [];\n }\n \n var datasourceOffset = 0;\n for (var i=0;i<datasources.length;i++) {\n markersSettings[i] = {\n latKeyName: \"lat\",\n lngKeyName: \"lng\",\n showLabel: true,\n label: datasources[i].name,\n color: \"FE7569\",\n tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n };\n if (configuredMarkersSettings[i]) {\n markersSettings[i].latKeyName = configuredMarkersSettings[i].latKeyName || markersSettings[i].latKeyName;\n markersSettings[i].lngKeyName = configuredMarkersSettings[i].lngKeyName || markersSettings[i].lngKeyName;\n \n markersSettings[i].tooltipPattern = configuredMarkersSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+markersSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+markersSettings[i].lngKeyName+\":7}\";\n \n markersSettings[i].tooltipReplaceInfo = procesTooltipPattern(markersSettings[i].tooltipPattern, datasources[i], datasourceOffset); \n \n markersSettings[i].showLabel = configuredMarkersSettings[i].showLabel !== false;\n markersSettings[i].label = configuredMarkersSettings[i].label || markersSettings[i].label;\n markersSettings[i].color = configuredMarkersSettings[i].color ? tinycolor(configuredMarkersSettings[i].color).toHex() : markersSettings[i].color;\n }\n datasourceOffset += datasources[i].dataKeys.length;\n }\n \n map = L.map(containerElement).setView([0, 0], defaultZoomLevel || 8);\n\n L.tileLayer(''http://{s}.tile.osm.org/{z}/{x}/{y}.png'', {\n attribution: ''© <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors''\n }).addTo(map);\n\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n \n function padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n \n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n \n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split(''.'');\n s = int - strVal[0].length;\n \n for (; i < s; ++i) {\n strVal[0] = ''0'' + strVal[0];\n }\n \n strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n }\n \n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n \n for (; i < s; ++i) {\n strVal = ''0'' + strVal;\n }\n \n strVal = (n ? ''-'' : '''') + strVal;\n }\n \n return strVal;\n } \n \n function createMarker(location, settings) {\n var pinColor = settings.color;\n\n var icon = L.icon({\n iconUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|'' + pinColor,\n iconSize: [21, 34],\n iconAnchor: [10, 34],\n popupAnchor: [0, -34],\n shadowUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_shadow'',\n shadowSize: [40, 37],\n shadowAnchor: [12, 35]\n });\n \n var marker = L.marker(location, {icon: icon}).addTo(map);\n if (settings.showLabel) {\n marker.bindTooltip(''<b>'' + settings.label + ''</b>'', { className: ''tb-marker-label'', permanent: true, direction: ''top'', offset: [0, -24] });\n }\n \n createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n \n return marker;\n }\n \n \n function createTooltip(marker, pattern, replaceInfo) {\n var popup = L.popup();\n popup.setContent('''');\n marker.bindPopup(popup);\n tooltips.push( {\n popup: popup,\n pattern: pattern,\n replaceInfo: replaceInfo\n });\n }\n \n function updatePosition(position, data) {\n if (position.latIndex > -1 && position.lngIndex > -1) {\n var latData = data[position.latIndex].data;\n var lngData = data[position.lngIndex].data;\n if (latData.length > 0 && lngData.length > 0) {\n var lat = latData[latData.length-1][1];\n var lng = lngData[lngData.length-1][1];\n var location = L.latLng(lat, lng);\n if (!position.marker) {\n position.marker = createMarker(location, position.settings);\n markers.push(position.marker);\n return true;\n } else {\n var prevPosition = position.marker.getLatLng();\n if (!prevPosition.equals(location)) {\n position.marker.setLatLng(location);\n return true;\n }\n }\n }\n }\n return false;\n }\n \n function loadPositions(data) {\n var bounds = L.latLngBounds();\n positions = [];\n var datasourceIndex = -1;\n var markerSettings;\n var datasource;\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n if (!datasource || datasource != datasourceData.datasource) {\n datasourceIndex++;\n datasource = datasourceData.datasource;\n markerSettings = markersSettings[datasourceIndex];\n }\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === markerSettings.latKeyName ||\n dataKey.label === markerSettings.lngKeyName) {\n var position = positions[datasourceIndex];\n if (!position) {\n position = {\n latIndex: -1,\n lngIndex: -1,\n settings: markerSettings\n };\n positions[datasourceIndex] = position;\n } else if (position.marker) {\n continue;\n }\n if (dataKey.label === markerSettings.latKeyName) {\n position.latIndex = i;\n } else {\n position.lngIndex = i;\n }\n if (position.latIndex > -1 && position.lngIndex > -1) {\n updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getLatLng());\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n function updatePositions(data) {\n var positionsChanged = false;\n var bounds = L.latLngBounds();\n for (var p in positions) {\n var position = positions[p];\n positionsChanged |= updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getLatLng());\n }\n }\n if (!dontFitMapBounds && positionsChanged) {\n fitMapBounds(bounds);\n }\n }\n \n function fitMapBounds(bounds) {\n map.once(''zoomend'', function(event) {\n var zoomLevel = defaultZoomLevel || map.getZoom();\n map.setZoom(zoomLevel, {animate: false});\n if (!defaultZoomLevel && this.getZoom() > 15) {\n map.setZoom(15, {animate: false});\n }\n });\n map.fitBounds(bounds, {padding: [50, 50], animate: false});\n }\n \n if (map) {\n if (data) {\n if (!positions) {\n loadPositions(data);\n } else {\n updatePositions(data);\n }\n }\n if (sizeChanged) {\n map.invalidateSize(true);\n var bounds = L.latLngBounds();\n for (var m in markers) {\n bounds.extend(markers[m].getLatLng());\n }\n fitMapBounds(bounds);\n }\n \n for (var t in tooltips) {\n var tooltip = tooltips[t];\n var text = tooltip.pattern;\n var replaceInfo = tooltip.replaceInfo;\n for (var v in replaceInfo.variables) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '''';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n tooltip.popup.setContent(text);\n } \n \n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Google Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all markers\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"markersSettings\": {\n \"title\": \"Markers settings, same order as datasources\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Marker settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n }, \n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"tooltipPattern\": {\n \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units'' )\",\n \"type\": \"string\",\n \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n }\n },\n \"required\": [\n ]\n },\n \"form\": [\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"markersSettings\",\n \"items\": [\n \"markersSettings[].latKeyName\",\n \"markersSettings[].lngKeyName\",\n \"markersSettings[].showLabel\",\n \"markersSettings[].label\",\n \"markersSettings[].tooltipPattern\",\n {\n \"key\": \"markersSettings[].color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lat\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lng\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"markersSettings\":[{\"label\":\"First point\",\"color\":\"#1e88e5\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\"},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true,\"tooltipPattern\":\"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"}],\"fitMapBounds\":true},\"title\":\"OpenStreetMap\"}"}',
+VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'openstreetmap',
+'{"type":"latest","sizeX":8.5,"sizeY":6,"resources":[{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.css"},{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.js"}],"templateHtml":"","templateCss":".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n","controllerScript":"var map;\nvar positions;\nvar markers = [];\nvar markersSettings = [];\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n if (settings.defaultZoomLevel) {\n if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n }\n }\n \n dontFitMapBounds = settings.fitMapBounds === false;\n \n function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n var match = varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split('':'');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith(''#'')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = datasourceOffset + n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < datasource.dataKeys.length; i++) {\n var dataKey = datasource.dataKeys[i];\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = datasourceOffset + i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = varsRegex.exec(pattern);\n }\n return replaceInfo;\n } \n \n var configuredMarkersSettings = settings.markersSettings;\n if (!configuredMarkersSettings) {\n configuredMarkersSettings = [];\n }\n \n var datasourceOffset = 0;\n for (var i=0;i<datasources.length;i++) {\n markersSettings[i] = {\n latKeyName: \"lat\",\n lngKeyName: \"lng\",\n showLabel: true,\n label: datasources[i].name,\n color: \"FE7569\",\n tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n };\n if (configuredMarkersSettings[i]) {\n markersSettings[i].latKeyName = configuredMarkersSettings[i].latKeyName || markersSettings[i].latKeyName;\n markersSettings[i].lngKeyName = configuredMarkersSettings[i].lngKeyName || markersSettings[i].lngKeyName;\n \n markersSettings[i].tooltipPattern = configuredMarkersSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+markersSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+markersSettings[i].lngKeyName+\":7}\";\n \n markersSettings[i].tooltipReplaceInfo = procesTooltipPattern(markersSettings[i].tooltipPattern, datasources[i], datasourceOffset); \n \n markersSettings[i].showLabel = configuredMarkersSettings[i].showLabel !== false;\n markersSettings[i].label = configuredMarkersSettings[i].label || markersSettings[i].label;\n markersSettings[i].color = configuredMarkersSettings[i].color ? tinycolor(configuredMarkersSettings[i].color).toHex() : markersSettings[i].color;\n }\n datasourceOffset += datasources[i].dataKeys.length;\n }\n \n map = L.map(containerElement).setView([0, 0], defaultZoomLevel || 8);\n\n L.tileLayer(''http://{s}.tile.osm.org/{z}/{x}/{y}.png'', {\n attribution: ''© <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors''\n }).addTo(map);\n\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n \n function padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n \n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n \n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split(''.'');\n s = int - strVal[0].length;\n \n for (; i < s; ++i) {\n strVal[0] = ''0'' + strVal[0];\n }\n \n strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n }\n \n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n \n for (; i < s; ++i) {\n strVal = ''0'' + strVal;\n }\n \n strVal = (n ? ''-'' : '''') + strVal;\n }\n \n return strVal;\n } \n \n function createMarker(location, settings) {\n var pinColor = settings.color;\n\n var icon = L.icon({\n iconUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|'' + pinColor,\n iconSize: [21, 34],\n iconAnchor: [10, 34],\n popupAnchor: [0, -34],\n shadowUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_shadow'',\n shadowSize: [40, 37],\n shadowAnchor: [12, 35]\n });\n \n var marker = L.marker(location, {icon: icon}).addTo(map);\n if (settings.showLabel) {\n marker.bindTooltip(''<b>'' + settings.label + ''</b>'', { className: ''tb-marker-label'', permanent: true, direction: ''top'', offset: [0, -24] });\n }\n \n createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n \n return marker;\n }\n \n \n function createTooltip(marker, pattern, replaceInfo) {\n var popup = L.popup();\n popup.setContent('''');\n marker.bindPopup(popup, {autoClose: false, closeOnClick: false});\n tooltips.push( {\n popup: popup,\n pattern: pattern,\n replaceInfo: replaceInfo\n });\n }\n \n function updatePosition(position, data) {\n if (position.latIndex > -1 && position.lngIndex > -1) {\n var latData = data[position.latIndex].data;\n var lngData = data[position.lngIndex].data;\n if (latData.length > 0 && lngData.length > 0) {\n var lat = latData[latData.length-1][1];\n var lng = lngData[lngData.length-1][1];\n var location = L.latLng(lat, lng);\n if (!position.marker) {\n position.marker = createMarker(location, position.settings);\n markers.push(position.marker);\n return true;\n } else {\n var prevPosition = position.marker.getLatLng();\n if (!prevPosition.equals(location)) {\n position.marker.setLatLng(location);\n return true;\n }\n }\n }\n }\n return false;\n }\n \n function loadPositions(data) {\n var bounds = L.latLngBounds();\n positions = [];\n var datasourceIndex = -1;\n var markerSettings;\n var datasource;\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n if (!datasource || datasource != datasourceData.datasource) {\n datasourceIndex++;\n datasource = datasourceData.datasource;\n markerSettings = markersSettings[datasourceIndex];\n }\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === markerSettings.latKeyName ||\n dataKey.label === markerSettings.lngKeyName) {\n var position = positions[datasourceIndex];\n if (!position) {\n position = {\n latIndex: -1,\n lngIndex: -1,\n settings: markerSettings\n };\n positions[datasourceIndex] = position;\n } else if (position.marker) {\n continue;\n }\n if (dataKey.label === markerSettings.latKeyName) {\n position.latIndex = i;\n } else {\n position.lngIndex = i;\n }\n if (position.latIndex > -1 && position.lngIndex > -1) {\n updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getLatLng());\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n function updatePositions(data) {\n var positionsChanged = false;\n var bounds = L.latLngBounds();\n for (var p in positions) {\n var position = positions[p];\n positionsChanged |= updatePosition(position, data);\n if (position.marker) {\n bounds.extend(position.marker.getLatLng());\n }\n }\n if (!dontFitMapBounds && positionsChanged) {\n fitMapBounds(bounds);\n }\n }\n \n function fitMapBounds(bounds) {\n map.once(''zoomend'', function(event) {\n var zoomLevel = defaultZoomLevel || map.getZoom();\n map.setZoom(zoomLevel, {animate: false});\n if (!defaultZoomLevel && this.getZoom() > 15) {\n map.setZoom(15, {animate: false});\n }\n });\n map.fitBounds(bounds, {padding: [50, 50], animate: false});\n }\n \n if (map) {\n if (data) {\n if (!positions) {\n loadPositions(data);\n } else {\n updatePositions(data);\n }\n }\n if (sizeChanged) {\n map.invalidateSize(true);\n var bounds = L.latLngBounds();\n for (var m in markers) {\n bounds.extend(markers[m].getLatLng());\n }\n fitMapBounds(bounds);\n }\n \n for (var t in tooltips) {\n var tooltip = tooltips[t];\n var text = tooltip.pattern;\n var replaceInfo = tooltip.replaceInfo;\n for (var v in replaceInfo.variables) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '''';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n tooltip.popup.setContent(text);\n } \n \n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Google Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all markers\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"markersSettings\": {\n \"title\": \"Markers settings, same order as datasources\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Marker settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n }, \n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"tooltipPattern\": {\n \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units'' )\",\n \"type\": \"string\",\n \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n }\n },\n \"required\": [\n ]\n },\n \"form\": [\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"markersSettings\",\n \"items\": [\n \"markersSettings[].latKeyName\",\n \"markersSettings[].lngKeyName\",\n \"markersSettings[].showLabel\",\n \"markersSettings[].label\",\n \"markersSettings[].tooltipPattern\",\n {\n \"key\": \"markersSettings[].color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lat\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lng\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"markersSettings\":[{\"label\":\"First point\",\"color\":\"#1e88e5\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\"},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true,\"tooltipPattern\":\"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"}],\"fitMapBounds\":true},\"title\":\"OpenStreetMap\"}"}',
'OpenStreetMap' );
INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'route_map_openstreetmap',
+'{"type":"timeseries","sizeX":8.5,"sizeY":6,"resources":[{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.css"},{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.js"}],"templateHtml":"","templateCss":".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n","controllerScript":"var map;\n\nvar routesSettings = [];\nvar routes;\nvar polylines = [];\n\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n if (settings.defaultZoomLevel) {\n if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n }\n }\n \n dontFitMapBounds = settings.fitMapBounds === false;\n \n function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n var match = varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split('':'');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith(''#'')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = datasourceOffset + n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < datasource.dataKeys.length; i++) {\n var dataKey = datasource.dataKeys[i];\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = datasourceOffset + i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = varsRegex.exec(pattern);\n }\n return replaceInfo;\n } \n \n var configuredRoutesSettings = settings.routesSettings;\n if (!configuredRoutesSettings) {\n configuredRoutesSettings = [];\n }\n \n var datasourceOffset = 0;\n for (var i=0;i<datasources.length;i++) {\n routesSettings[i] = {\n latKeyName: \"lat\",\n lngKeyName: \"lng\",\n showLabel: true,\n label: datasources[i].name, \n color: \"#FE7569\",\n strokeWeight: 2,\n strokeOpacity: 1.0,\n tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n };\n if (configuredRoutesSettings[i]) {\n \n routesSettings[i].latKeyName = configuredRoutesSettings[i].latKeyName || routesSettings[i].latKeyName;\n routesSettings[i].lngKeyName = configuredRoutesSettings[i].lngKeyName || routesSettings[i].lngKeyName;\n routesSettings[i].tooltipPattern = configuredRoutesSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+routesSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+routesSettings[i].lngKeyName+\":7}\";\n \n routesSettings[i].tooltipReplaceInfo = procesTooltipPattern(routesSettings[i].tooltipPattern, datasources[i], datasourceOffset);\n \n routesSettings[i].showLabel = configuredRoutesSettings[i].showLabel !== false;\n routesSettings[i].label = configuredRoutesSettings[i].label || routesSettings[i].label;\n routesSettings[i].color = configuredRoutesSettings[i].color ? tinycolor(configuredRoutesSettings[i].color).toHex() : routesSettings[i].color;\n routesSettings[i].strokeWeight = configuredRoutesSettings[i].strokeWeight || routesSettings[i].strokeWeight;\n routesSettings[i].strokeOpacity = typeof configuredRoutesSettings[i].strokeOpacity !== \"undefined\" ? configuredRoutesSettings[i].strokeOpacity : routesSettings[i].strokeOpacity; \n }\n datasourceOffset += datasources[i].dataKeys.length;\n }\n \n map = L.map(containerElement).setView([0, 0], defaultZoomLevel || 8);\n\n L.tileLayer(''http://{s}.tile.osm.org/{z}/{x}/{y}.png'', {\n attribution: ''© <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors''\n }).addTo(map);\n\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n \n function padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n \n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n \n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split(''.'');\n s = int - strVal[0].length;\n \n for (; i < s; ++i) {\n strVal[0] = ''0'' + strVal[0];\n }\n \n strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n }\n \n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n \n for (; i < s; ++i) {\n strVal = ''0'' + strVal;\n }\n \n strVal = (n ? ''-'' : '''') + strVal;\n }\n \n return strVal;\n } \n \n function createMarker(location, settings) {\n var pinColor = settings.color;\n\n var icon = L.icon({\n iconUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|'' + pinColor,\n iconSize: [21, 34],\n iconAnchor: [10, 34],\n popupAnchor: [0, -34],\n shadowUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_shadow'',\n shadowSize: [40, 37],\n shadowAnchor: [12, 35]\n });\n \n var marker = L.marker(location, {icon: icon}).addTo(map);\n if (settings.showLabel) {\n marker.bindTooltip(''<b>'' + settings.label + ''</b>'', { className: ''tb-marker-label'', permanent: true, direction: ''top'', offset: [0, -24] });\n }\n \n createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n \n return marker;\n }\n \n \n function createTooltip(marker, pattern, replaceInfo) {\n var popup = L.popup();\n popup.setContent('''');\n marker.bindPopup(popup, {autoClose: false, closeOnClick: false});\n tooltips.push( {\n popup: popup,\n pattern: pattern,\n replaceInfo: replaceInfo\n });\n }\n \n function createPolyline(locations, settings) {\n var polyline = L.polyline(locations, \n {\n color: \"#\" + settings.color,\n opacity: settings.strokeOpacity,\n weight: settings.strokeWeight\n }\n ).addTo(map);\n return polyline; \n } \n \n function arraysEqual(a, b) {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (a.length != b.length) return false;\n\n for (var i = 0; i < a.length; ++i) {\n if (!a[i].equals(b[i])) return false;\n }\n return true;\n }\n \n function updateRoute(route, data) {\n if (route.latIndex > -1 && route.lngIndex > -1) {\n var latData = data[route.latIndex].data;\n var lngData = data[route.lngIndex].data;\n if (latData.length > 0 && lngData.length > 0) {\n var locations = [];\n for (var i = 0; i < latData.length; i++) {\n var lat = latData[i][1];\n var lng = lngData[i][1];\n var location = L.latLng(lat, lng);\n if (i == 0 || !locations[locations.length-1].equals(location)) {\n locations.push(location);\n }\n }\n var markerLocation;\n if (locations.length > 0) {\n markerLocation = locations[locations.length-1];\n }\n if (!route.polyline) {\n route.polyline = createPolyline(locations, route.settings);\n if (markerLocation) {\n route.marker = createMarker(markerLocation, route.settings);\n }\n polylines.push(route.polyline);\n return true;\n } else {\n var prevPath = route.polyline.getLatLngs();\n if (!prevPath || !arraysEqual(prevPath, locations)) {\n route.polyline.setLatLngs(locations);\n if (markerLocation) {\n if (!route.marker) {\n route.marker = createMarker(markerLocation, route.settings);\n } else {\n route.marker.setLatLng(markerLocation);\n }\n }\n return true;\n }\n }\n }\n }\n return false;\n } \n \n function extendBounds(bounds, polyline) {\n if (polyline && polyline.getLatLngs()) {\n bounds.extend(polyline.getBounds());\n }\n }\n \n function loadRoutes(data) {\n var bounds = L.latLngBounds();\n routes = [];\n var datasourceIndex = -1;\n var routeSettings;\n var datasource;\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n if (!datasource || datasource != datasourceData.datasource) {\n datasourceIndex++;\n datasource = datasourceData.datasource;\n routeSettings = routesSettings[datasourceIndex];\n }\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === routeSettings.latKeyName ||\n dataKey.label === routeSettings.lngKeyName) {\n var route = routes[datasourceIndex];\n if (!route) {\n route = {\n latIndex: -1,\n lngIndex: -1,\n settings: routeSettings\n };\n routes[datasourceIndex] = route;\n } else if (route.polyline) {\n continue;\n }\n if (dataKey.label === routeSettings.latKeyName) {\n route.latIndex = i;\n } else {\n route.lngIndex = i;\n }\n if (route.latIndex > -1 && route.lngIndex > -1) {\n updateRoute(route, data);\n if (route.polyline) {\n extendBounds(bounds, route.polyline);\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n function updateRoutes(data) {\n var routesChanged = false;\n var bounds = L.latLngBounds();\n for (var r in routes) {\n var route = routes[r];\n routesChanged |= updateRoute(route, data);\n if (route.polyline) {\n extendBounds(bounds, route.polyline);\n }\n }\n if (!dontFitMapBounds && routesChanged) {\n fitMapBounds(bounds);\n }\n }\n \n function fitMapBounds(bounds) {\n map.once(''zoomend'', function(event) {\n var newZoomLevel = map.getZoom();\n if (dontFitMapBounds && defaultZoomLevel) {\n newZoomLevel = defaultZoomLevel;\n }\n map.setZoom(newZoomLevel, {animate: false});\n if (!defaultZoomLevel && this.getZoom() > 18) {\n map.setZoom(18, {animate: false});\n }\n });\n map.fitBounds(bounds, {padding: [50, 50], animate: false});\n }\n\n if (map) {\n if (data) {\n if (!routes) {\n loadRoutes(data);\n } else {\n updateRoutes(data);\n }\n }\n if (sizeChanged) {\n map.invalidateSize(true);\n if (!dontFitMapBounds) {\n var bounds = L.latLngBounds();\n for (var p in polylines) {\n extendBounds(bounds, polylines[p]);\n }\n fitMapBounds(bounds);\n } \n }\n \n for (var t in tooltips) {\n var tooltip = tooltips[t];\n var text = tooltip.pattern;\n var replaceInfo = tooltip.replaceInfo;\n if (replaceInfo && replaceInfo.variables) {\n for (var v in replaceInfo.variables) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '''';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n }\n tooltip.popup.setContent(text);\n } \n \n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Route Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all markers\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"routesSettings\": {\n \"title\": \"Routes settings, same order as datasources\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Route settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"tooltipPattern\": {\n \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units'' )\",\n \"type\": \"string\",\n \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n },\n \"strokeWeight\": {\n \"title\": \"Stroke weight\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"strokeOpacity\": {\n \"title\": \"Stroke opacity\",\n \"type\": \"number\",\n \"default\": 1.0\n }\n }\n }\n }\n },\n \"required\": [\n ]\n },\n \"form\": [\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"routesSettings\",\n \"items\": [\n \"routesSettings[].latKeyName\",\n \"routesSettings[].lngKeyName\",\n \"routesSettings[].showLabel\",\n \"routesSettings[].label\",\n \"routesSettings[].tooltipPattern\",\n {\n \"key\": \"routesSettings[].color\",\n \"type\": \"color\"\n },\n \"routesSettings[].strokeWeight\",\n \"routesSettings[].strokeOpacity\"\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8950926999078694,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.2757675428823283,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"}],\"intervalSec\":60}],\"timewindow\":{\"realtime\":{\"timewindowMs\":30000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"routesSettings\":[{\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\",\"strokeWeight\":4,\"label\":\"First route\",\"color\":\"#3d5afe\",\"strokeOpacity\":1}]},\"title\":\"Route Map - OpenStreetMap\"}"}',
+'Route Map - OpenStreetMap' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'temperature_radial_gauge_canvas_gauges',
'{"type":"latest","sizeX":6,"sizeY":5,"resources":[],"templateHtml":"<canvas id=\"radialGauge\"></canvas>\n","templateCss":"","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbAnalogueRadialGauge(containerElement, settings, data, ''radialGauge''); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged) {\n gauge.redraw(width, height, data, sizeChanged);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n },\n \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n },\n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"units\": {\n \"title\": \"Units\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"majorTicksCount\": {\n \"title\": \"Major ticks count\",\n \"type\": \"number\",\n \"default\": null\n },\n \"minorTicks\": {\n \"title\": \"Minor ticks count\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"valueBox\": {\n \"title\": \"Show value box\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"valueInt\": {\n \"title\": \"Digits count for integer part of value\",\n \"type\": \"number\",\n \"default\": 3\n },\n \"valueDec\": {\n \"title\": \"Digits count for decimal part of value\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorPlate\": {\n \"title\": \"Plate color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"colorMajorTicks\": {\n \"title\": \"Major ticks color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"colorMinorTicks\": {\n \"title\": \"Minor ticks color\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorNeedle\": {\n \"title\": \"Needle color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleEnd\": {\n \"title\": \"Needle color - end gradient\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleShadowUp\": {\n \"title\": \"Upper half of the needle shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(2,255,255,0.2)\"\n },\n \"colorNeedleShadowDown\": {\n \"title\": \"Drop shadow needle color.\",\n \"type\": \"string\",\n \"default\": \"rgba(188,143,143,0.45)\"\n },\n \"colorValueBoxRect\": {\n \"title\": \"Value box rectangle stroke color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n },\n \"colorValueBoxRectEnd\": {\n \"title\": \"Value box rectangle stroke color - end gradient\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorValueBoxBackground\": {\n \"title\": \"Value box background color\",\n \"type\": \"string\",\n \"default\": \"#babab2\"\n },\n \"colorValueBoxShadow\": {\n \"title\": \"Value box shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,1)\"\n },\n \"highlights\": {\n \"title\": \"Highlights\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Highlight\",\n \"type\": \"object\",\n \"properties\": {\n \"from\": {\n \"title\": \"From\",\n \"type\": \"number\"\n },\n \"to\": {\n \"title\": \"To\",\n \"type\": \"number\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n },\n \"highlightsWidth\": {\n \"title\": \"Highlights width\",\n \"type\": \"number\",\n \"default\": 15\n },\n \"showBorder\": {\n \"title\": \"Show border\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"numbersFont\": {\n \"title\": \"Tick numbers font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"titleFont\": {\n \"title\": \"Title text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 24\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"unitsFont\": {\n \"title\": \"Units text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 22\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Value text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 40\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"shadowColor\": {\n \"title\": \"Shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,0.3)\"\n }\n }\n },\n \"animation\": {\n \"title\": \"Enable animation\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"animationDuration\": {\n \"title\": \"Animation duration\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"animationRule\": {\n \"title\": \"Animation rule\",\n \"type\": \"string\",\n \"default\": \"cycle\"\n },\n \"startAngle\": {\n \"title\": \"Start ticks angle\",\n \"type\": \"number\",\n \"default\": 45\n },\n \"ticksAngle\": {\n \"title\": \"Ticks angle\",\n \"type\": \"number\",\n \"default\": 270\n },\n \"needleCircleSize\": {\n \"title\": \"Needle circle size\",\n \"type\": \"number\",\n \"default\": 10\n }\n },\n \"required\": []\n },\n \"form\": [\n \"startAngle\",\n \"ticksAngle\",\n \"needleCircleSize\",\n \"minValue\",\n \"maxValue\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"units\",\n \"majorTicksCount\",\n \"minorTicks\",\n \"valueBox\",\n \"valueInt\",\n \"valueDec\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorPlate\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMajorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMinorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedle\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowUp\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowDown\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRect\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRectEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxBackground\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxShadow\",\n \"type\": \"color\"\n },\n {\n \"key\": \"highlights\",\n \"items\": [\n \"highlights[].from\",\n \"highlights[].to\",\n {\n \"key\": \"highlights[].color\",\n \"type\": \"color\"\n }\n ]\n },\n \"highlightsWidth\",\n \"showBorder\",\n {\n \"key\": \"numbersFont\",\n \"items\": [\n \"numbersFont.family\",\n \"numbersFont.size\",\n {\n \"key\": \"numbersFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"unitsFont\",\n \"items\": [\n \"unitsFont.family\",\n \"unitsFont.size\",\n {\n \"key\": \"unitsFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n },\n {\n \"key\": \"valueFont.shadowColor\",\n \"type\": \"color\"\n }\n ]\n }, \n \"animation\",\n \"animationDuration\",\n {\n \"key\": \"animationRule\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \"quad\",\n \"label\": \"Quad\"\n },\n {\n \"value\": \"quint\",\n \"label\": \"Quint\"\n },\n {\n \"value\": \"cycle\",\n \"label\": \"Cycle\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n },\n {\n \"value\": \"elastic\",\n \"label\": \"Elastic\"\n },\n {\n \"value\": \"dequad\",\n \"label\": \"Dequad\"\n },\n {\n \"value\": \"dequint\",\n \"label\": \"Dequint\"\n },\n {\n \"value\": \"decycle\",\n \"label\": \"Decycle\"\n },\n {\n \"value\": \"debounce\",\n \"label\": \"Debounce\"\n },\n {\n \"value\": \"delastic\",\n \"label\": \"Delastic\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":60,\"startAngle\":67.5,\"ticksAngle\":225,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":-60,\"to\":-50,\"color\":\"#42a5f5\"},{\"from\":-50,\"to\":-40,\"color\":\"rgba(66, 165, 245, 0.83)\"},{\"from\":-40,\"to\":-30,\"color\":\"rgba(66, 165, 245, 0.66)\"},{\"from\":-30,\"to\":-20,\"color\":\"rgba(66, 165, 245, 0.5)\"},{\"from\":-20,\"to\":-10,\"color\":\"rgba(66, 165, 245, 0.33)\"},{\"from\":-10,\"to\":0,\"color\":\"rgba(66, 165, 245, 0.16)\"},{\"from\":0,\"to\":10,\"color\":\"rgba(229, 115, 115, 0.16)\"},{\"from\":10,\"to\":20,\"color\":\"rgba(229, 115, 115, 0.33)\"},{\"from\":20,\"to\":30,\"color\":\"rgba(229, 115, 115, 0.5)\"},{\"from\":30,\"to\":40,\"color\":\"rgba(229, 115, 115, 0.66)\"},{\"from\":40,\"to\":50,\"color\":\"rgba(229, 115, 115, 0.83)\"},{\"from\":50,\"to\":60,\"color\":\"#e57373\"}],\"showUnitTitle\":true,\"colorPlate\":\"#cfd8dc\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"valueDec\":1,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1000,\"animationRule\":\"bounce\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"°C\",\"majorTicksCount\":12,\"numbersFont\":{\"family\":\"RobotoDraft\",\"size\":20,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"unitsFont\":{\"family\":\"RobotoDraft\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":30,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"unitTitle\":\"Temperature\",\"minValue\":-60},\"title\":\"Temperature radial gauge - Canvas Gauges\"}"}',
'Temperature radial gauge - Canvas Gauges' );
docker/docker-compose.yml 4(+2 -2)
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 185d54f..a6aee60 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -18,7 +18,7 @@ version: '2'
services:
thingsboard:
- image: "thingsboard/application:1.0"
+ image: "thingsboard/application:1.0.1"
ports:
- "8080:8080"
- "1883:1883"
@@ -27,7 +27,7 @@ services:
- thingsboard.env
entrypoint: ./run_thingsboard.sh
thingsboard-db-schema:
- image: "thingsboard/thingsboard-db-schema:1.0"
+ image: "thingsboard/thingsboard-db-schema:1.0.1"
env_file:
- thingsboard-db-schema.env
entrypoint: ./install_schema.sh
diff --git a/docker/thingsboard/build_and_deploy.sh b/docker/thingsboard/build_and_deploy.sh
index acbb420..47cf1e8 100755
--- a/docker/thingsboard/build_and_deploy.sh
+++ b/docker/thingsboard/build_and_deploy.sh
@@ -18,8 +18,8 @@
cp ../../application/target/thingsboard.deb thingsboard.deb
-docker build -t thingsboard/application:1.0 .
+docker build -t thingsboard/application:1.0.1 .
docker login
-docker push thingsboard/application:1.0
\ No newline at end of file
+docker push thingsboard/application:1.0.1
\ No newline at end of file
diff --git a/docker/thingsboard-db-schema/build_and_deploy.sh b/docker/thingsboard-db-schema/build_and_deploy.sh
index 76d8743..a050d83 100755
--- a/docker/thingsboard-db-schema/build_and_deploy.sh
+++ b/docker/thingsboard-db-schema/build_and_deploy.sh
@@ -20,8 +20,8 @@ cp ../../dao/src/main/resources/schema.cql schema.cql
cp ../../dao/src/main/resources/demo-data.cql demo-data.cql
cp ../../dao/src/main/resources/system-data.cql system-data.cql
-docker build -t thingsboard/thingsboard-db-schema:1.0 .
+docker build -t thingsboard/thingsboard-db-schema:1.0.1 .
docker login
-docker push thingsboard/thingsboard-db-schema:1.0
\ No newline at end of file
+docker push thingsboard/thingsboard-db-schema:1.0.1
\ No newline at end of file
extensions/extension-kafka/pom.xml 2(+1 -1)
diff --git a/extensions/extension-kafka/pom.xml b/extensions/extension-kafka/pom.xml
index 431482b..0cbf7f2 100644
--- a/extensions/extension-kafka/pom.xml
+++ b/extensions/extension-kafka/pom.xml
@@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>extensions</artifactId>
</parent>
<groupId>org.thingsboard.extensions</groupId>
diff --git a/extensions/extension-rabbitmq/pom.xml b/extensions/extension-rabbitmq/pom.xml
index 99167bd..841a3ae 100644
--- a/extensions/extension-rabbitmq/pom.xml
+++ b/extensions/extension-rabbitmq/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>extensions</artifactId>
</parent>
<groupId>org.thingsboard.extensions</groupId>
diff --git a/extensions/extension-rest-api-call/pom.xml b/extensions/extension-rest-api-call/pom.xml
index 39072ee..5a5f464 100644
--- a/extensions/extension-rest-api-call/pom.xml
+++ b/extensions/extension-rest-api-call/pom.xml
@@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>extensions</artifactId>
</parent>
<groupId>org.thingsboard.extensions</groupId>
extensions/pom.xml 2(+1 -1)
diff --git a/extensions/pom.xml b/extensions/pom.xml
index fc48d41..4ae86ee 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
extensions-api/pom.xml 2(+1 -1)
diff --git a/extensions-api/pom.xml b/extensions-api/pom.xml
index aad2469..951abd7 100644
--- a/extensions-api/pom.xml
+++ b/extensions-api/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
extensions-core/pom.xml 2(+1 -1)
diff --git a/extensions-core/pom.xml b/extensions-core/pom.xml
index f4e7697..fb9e534 100644
--- a/extensions-core/pom.xml
+++ b/extensions-core/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java
index e7bc414..3190bbf 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java
@@ -24,10 +24,6 @@ import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionT
@NoArgsConstructor
public class AttributesSubscriptionCmd extends SubscriptionCmd {
- public AttributesSubscriptionCmd(int cmdId, String deviceId, String keys, boolean unsubscribe) {
- super(cmdId, deviceId, keys, unsubscribe);
- }
-
@Override
public SubscriptionType getType() {
return SubscriptionType.ATTRIBUTES;
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java
index 249dfa9..0d5fa48 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java
@@ -26,6 +26,7 @@ public abstract class SubscriptionCmd implements TelemetryPluginCmd {
private int cmdId;
private String deviceId;
private String keys;
+ private String scope;
private boolean unsubscribe;
public abstract SubscriptionType getType();
@@ -62,6 +63,14 @@ public abstract class SubscriptionCmd implements TelemetryPluginCmd {
this.unsubscribe = unsubscribe;
}
+ public String getScope() {
+ return scope;
+ }
+
+ public void setKeys(String keys) {
+ this.keys = keys;
+ }
+
@Override
public String toString() {
return "SubscriptionCmd [deviceId=" + deviceId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]";
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
index 0b0ff91..92d7259 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
@@ -26,11 +26,6 @@ public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
private long timeWindow;
- public TimeseriesSubscriptionCmd(int cmdId, String deviceId, String keys, boolean unsubscribe, long timeWindow) {
- super(cmdId, deviceId, keys, unsubscribe);
- this.timeWindow = timeWindow;
- }
-
public long getTimeWindow() {
return timeWindow;
}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
index dee981a..1103635 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
@@ -29,6 +29,7 @@ import org.thingsboard.server.extensions.api.plugins.handlers.DefaultRestMsgHand
import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
import org.thingsboard.server.extensions.api.plugins.rest.RestRequest;
import org.thingsboard.server.extensions.core.plugin.telemetry.AttributeData;
+import org.thingsboard.server.extensions.core.plugin.telemetry.SubscriptionManager;
import org.thingsboard.server.extensions.core.plugin.telemetry.TsData;
import javax.servlet.ServletException;
@@ -39,6 +40,12 @@ import java.util.stream.Collectors;
@Slf4j
public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
+ private final SubscriptionManager subscriptionManager;
+
+ public TelemetryRestMsgHandler(SubscriptionManager subscriptionManager) {
+ this.subscriptionManager = subscriptionManager;
+ }
+
@Override
public void handleHttpGetRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
RestRequest request = msg.getRequest();
@@ -74,9 +81,8 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
if (!StringUtils.isEmpty(scope)) {
attributes = ctx.loadAttributes(deviceId, scope);
} else {
- attributes = ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE);
- attributes.addAll(ctx.loadAttributes(deviceId, DataConstants.SERVER_SCOPE));
- attributes.addAll(ctx.loadAttributes(deviceId, DataConstants.SHARED_SCOPE));
+ attributes = new ArrayList<>();
+ Arrays.stream(DataConstants.ALL_SCOPES).forEach(s -> attributes.addAll(ctx.loadAttributes(deviceId, s)));
}
List<String> keys = attributes.stream().map(attrKv -> attrKv.getKey()).collect(Collectors.toList());
msg.getResponseHolder().setResult(new ResponseEntity<>(keys, HttpStatus.OK));
@@ -99,9 +105,8 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
if (!StringUtils.isEmpty(scope)) {
attributes = getAttributeKvEntries(ctx, scope, deviceId, keys);
} else {
- attributes = getAttributeKvEntries(ctx, DataConstants.CLIENT_SCOPE, deviceId, keys);
- attributes.addAll(getAttributeKvEntries(ctx, DataConstants.SHARED_SCOPE, deviceId, keys));
- attributes.addAll(getAttributeKvEntries(ctx, DataConstants.SERVER_SCOPE, deviceId, keys));
+ attributes = new ArrayList<>();
+ Arrays.stream(DataConstants.ALL_SCOPES).forEach(s -> attributes.addAll(getAttributeKvEntries(ctx, s, deviceId, keys)));
}
List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(),
attribute.getKey(), attribute.getValue())).collect(Collectors.toList());
@@ -145,6 +150,7 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
@Override
public void onSuccess(PluginContext ctx, Void value) {
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
+ subscriptionManager.onAttributesUpdateFromServer(ctx, deviceId, scope, attributes);
}
@Override
@@ -172,7 +178,8 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
DeviceId deviceId = DeviceId.fromString(pathParams[0]);
String scope = pathParams[1];
if (DataConstants.SERVER_SCOPE.equals(scope) ||
- DataConstants.SHARED_SCOPE.equals(scope)) {
+ DataConstants.SHARED_SCOPE.equals(scope) ||
+ DataConstants.CLIENT_SCOPE.equals(scope)) {
String keysParam = request.getParameter("keys");
if (!StringUtils.isEmpty(keysParam)) {
String[] keys = keysParam.split(",");
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
index 06467fe..e59fa64 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
@@ -19,6 +19,7 @@ import com.google.protobuf.InvalidProtocolBufferException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.kv.*;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
import org.thingsboard.server.extensions.api.plugins.handlers.RpcMsgHandler;
@@ -42,9 +43,10 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
private final SubscriptionManager subscriptionManager;
private static final int SUBSCRIPTION_CLAZZ = 1;
- private static final int SUBSCRIPTION_UPDATE_CLAZZ = 2;
- private static final int SESSION_CLOSE_CLAZZ = 3;
- private static final int SUBSCRIPTION_CLOSE_CLAZZ = 4;
+ private static final int ATTRIBUTES_UPDATE_CLAZZ = 2;
+ private static final int SUBSCRIPTION_UPDATE_CLAZZ = 3;
+ private static final int SESSION_CLOSE_CLAZZ = 4;
+ private static final int SUBSCRIPTION_CLOSE_CLAZZ = 5;
@Override
public void process(PluginContext ctx, RpcMsg msg) {
@@ -55,6 +57,9 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
case SUBSCRIPTION_UPDATE_CLAZZ:
processRemoteSubscriptionUpdate(ctx, msg);
break;
+ case ATTRIBUTES_UPDATE_CLAZZ:
+ processAttributeUpdate(ctx, msg);
+ break;
case SESSION_CLOSE_CLAZZ:
processSessionClose(ctx, msg);
break;
@@ -76,6 +81,17 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
subscriptionManager.onRemoteSubscriptionUpdate(ctx, proto.getSessionId(), convert(proto));
}
+ private void processAttributeUpdate(PluginContext ctx, RpcMsg msg) {
+ AttributeUpdateProto proto;
+ try {
+ proto = AttributeUpdateProto.parseFrom(msg.getMsgData());
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ subscriptionManager.onAttributesUpdateFromServer(ctx, DeviceId.fromString(proto.getDeviceId()), proto.getScope(),
+ proto.getDataList().stream().map(this::toAttribute).collect(Collectors.toList()));
+ }
+
private void processSubscriptionCmd(PluginContext ctx, RpcMsg msg) {
SubscriptionProto proto;
try {
@@ -167,11 +183,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
} else {
Map<String, List<Object>> data = new TreeMap<>();
proto.getDataList().forEach(v -> {
- List<Object> values = data.get(v.getKey());
- if (values == null) {
- values = new ArrayList<>();
- data.put(v.getKey(), values);
- }
+ List<Object> values = data.computeIfAbsent(v.getKey(), k -> new ArrayList<>());
for (int i = 0; i < v.getTsCount(); i++) {
Object[] value = new Object[2];
value[0] = v.getTs(i);
@@ -182,4 +194,59 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
return new SubscriptionUpdate(proto.getSubscriptionId(), data);
}
}
+
+ public void onAttributesUpdate(PluginContext ctx, ServerAddress address, DeviceId deviceId, String scope, List<AttributeKvEntry> attributes) {
+ ctx.sendPluginRpcMsg(new RpcMsg(address, ATTRIBUTES_UPDATE_CLAZZ, getAttributesUpdateProto(deviceId, scope, attributes).toByteArray()));
+ }
+
+ private AttributeUpdateProto getAttributesUpdateProto(DeviceId deviceId, String scope, List<AttributeKvEntry> attributes) {
+ AttributeUpdateProto.Builder builder = AttributeUpdateProto.newBuilder();
+ builder.setDeviceId(deviceId.toString());
+ builder.setScope(scope);
+ attributes.forEach(
+ attr -> {
+ AttributeUpdateValueListProto.Builder dataBuilder = AttributeUpdateValueListProto.newBuilder();
+ dataBuilder.setKey(attr.getKey());
+ dataBuilder.setTs(attr.getLastUpdateTs());
+ dataBuilder.setValueType(attr.getDataType().ordinal());
+ switch (attr.getDataType()) {
+ case BOOLEAN:
+ dataBuilder.setBoolValue(attr.getBooleanValue().get());
+ break;
+ case LONG:
+ dataBuilder.setLongValue(attr.getLongValue().get());
+ break;
+ case DOUBLE:
+ dataBuilder.setDoubleValue(attr.getDoubleValue().get());
+ break;
+ case STRING:
+ dataBuilder.setStrValue(attr.getStrValue().get());
+ break;
+ }
+ builder.addData(dataBuilder.build());
+ }
+ );
+ return builder.build();
+ }
+
+ private AttributeKvEntry toAttribute(AttributeUpdateValueListProto proto) {
+ KvEntry entry = null;
+ DataType type = DataType.values()[proto.getValueType()];
+ switch (type) {
+ case BOOLEAN:
+ entry = new BooleanDataEntry(proto.getKey(), proto.getBoolValue());
+ break;
+ case LONG:
+ entry = new LongDataEntry(proto.getKey(), proto.getLongValue());
+ break;
+ case DOUBLE:
+ entry = new DoubleDataEntry(proto.getKey(), proto.getDoubleValue());
+ break;
+ case STRING:
+ entry = new StringDataEntry(proto.getKey(), proto.getStrValue());
+ break;
+ }
+ return new BaseAttributeKvEntry(entry, proto.getTs());
+ }
+
}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java
index f69d17b..f14d25d 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java
@@ -58,10 +58,14 @@ public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, response));
}
- private List<AttributeKvEntry> getAttributeKvEntries(PluginContext ctx, DeviceId deviceId, String scope, Set<String> names) {
+ private List<AttributeKvEntry> getAttributeKvEntries(PluginContext ctx, DeviceId deviceId, String scope, Optional<Set<String>> names) {
List<AttributeKvEntry> attributes;
- if (!names.isEmpty()) {
- attributes = ctx.loadAttributes(deviceId, scope, new ArrayList<>(names));
+ if (names.isPresent()) {
+ if (!names.get().isEmpty()) {
+ attributes = ctx.loadAttributes(deviceId, scope, new ArrayList<>(names.get()));
+ } else {
+ attributes = ctx.loadAttributes(deviceId, scope);
+ }
} else {
attributes = Collections.emptyList();
}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
index 8e2d62a..f268dd8 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
@@ -104,7 +104,13 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
SubscriptionState sub;
if (keysOptional.isPresent()) {
List<String> keys = new ArrayList<>(keysOptional.get());
- List<AttributeKvEntry> data = ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE, keys);
+ List<AttributeKvEntry> data = new ArrayList<>();
+ if (StringUtils.isEmpty(cmd.getScope())) {
+ Arrays.stream(DataConstants.ALL_SCOPES).forEach(s -> data.addAll(ctx.loadAttributes(deviceId, s, keys)));
+ } else {
+ data.addAll(ctx.loadAttributes(deviceId, cmd.getScope(), keys));
+ }
+
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
@@ -114,7 +120,12 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, false, subState);
} else {
- List<AttributeKvEntry> data = ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE);
+ List<AttributeKvEntry> data = new ArrayList<>();
+ if (StringUtils.isEmpty(cmd.getScope())) {
+ Arrays.stream(DataConstants.ALL_SCOPES).forEach(s -> data.addAll(ctx.loadAttributes(deviceId, s)));
+ } else {
+ data.addAll(ctx.loadAttributes(deviceId, cmd.getScope()));
+ }
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
index 190d9ff..07c9629 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
@@ -175,6 +175,23 @@ public class SubscriptionManager {
}
}
+ public void onAttributesUpdateFromServer(PluginContext ctx, DeviceId deviceId, String scope, List<AttributeKvEntry> attributes) {
+ Optional<ServerAddress> serverAddress = ctx.resolve(deviceId);
+ if (!serverAddress.isPresent()) {
+ onLocalSubscriptionUpdate(ctx, deviceId, SubscriptionType.ATTRIBUTES, s -> {
+ List<TsKvEntry> subscriptionUpdate = new ArrayList<TsKvEntry>();
+ for (AttributeKvEntry kv : attributes) {
+ if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
+ subscriptionUpdate.add(new BasicTsKvEntry(kv.getLastUpdateTs(), kv));
+ }
+ }
+ return subscriptionUpdate;
+ });
+ } else {
+ rpcHandler.onAttributesUpdate(ctx, serverAddress.get(), deviceId, scope, attributes);
+ }
+ }
+
private void updateSubscriptionState(String sessionId, Subscription subState, SubscriptionUpdate update) {
log.trace("[{}] updating subscription state {} using onUpdate {}", sessionId, subState, update);
update.getLatestValues().entrySet().forEach(e -> subState.setKeyState(e.getKey(), e.getValue()));
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java
index 63d145d..8668639 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java
@@ -43,7 +43,7 @@ public class TelemetryStoragePlugin extends AbstractPlugin<EmptyComponentConfigu
public TelemetryStoragePlugin() {
this.subscriptionManager = new SubscriptionManager();
- this.restMsgHandler = new TelemetryRestMsgHandler();
+ this.restMsgHandler = new TelemetryRestMsgHandler(subscriptionManager);
this.ruleMsgHandler = new TelemetryRuleMsgHandler(subscriptionManager);
this.websocketMsgHandler = new TelemetryWebsocketMsgHandler(subscriptionManager);
this.rpcMsgHandler = new TelemetryRpcMsgHandler(subscriptionManager);
diff --git a/extensions-core/src/main/proto/telemetry.proto b/extensions-core/src/main/proto/telemetry.proto
index 5c7d7a4..60e40f7 100644
--- a/extensions-core/src/main/proto/telemetry.proto
+++ b/extensions-core/src/main/proto/telemetry.proto
@@ -36,6 +36,12 @@ message SubscriptionUpdateProto {
repeated SubscriptionUpdateValueListProto data = 5;
}
+message AttributeUpdateProto {
+ string deviceId = 1;
+ string scope = 2;
+ repeated AttributeUpdateValueListProto data = 3;
+}
+
message SessionCloseProto {
string sessionId = 1;
}
@@ -54,4 +60,14 @@ message SubscriptionUpdateValueListProto {
string key = 1;
repeated int64 ts = 2;
repeated string value = 3;
+}
+
+message AttributeUpdateValueListProto {
+ string key = 1;
+ int64 ts = 2;
+ int32 valueType = 3;
+ string strValue = 4;
+ int64 longValue = 5;
+ double doubleValue = 6;
+ bool boolValue = 7;
}
\ No newline at end of file
pom.xml 2(+1 -1)
diff --git a/pom.xml b/pom.xml
index 8e9ac9f..b68bee7 100755
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.thingsboard</groupId>
<artifactId>thingsboard</artifactId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Thingsboard</name>
tools/pom.xml 2(+1 -1)
diff --git a/tools/pom.xml b/tools/pom.xml
index 4ef32d4..a3cfe6a 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
transport/coap/pom.xml 2(+1 -1)
diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml
index 09fb573..f845b7a 100644
--- a/transport/coap/pom.xml
+++ b/transport/coap/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
index 26a9056..a9c6086 100644
--- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
@@ -167,17 +167,13 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
private FromDeviceMsg convertToGetAttributesRequest(SessionContext ctx, Request inbound) throws AdaptorException {
List<String> queryElements = inbound.getOptions().getUriQuery();
- if (queryElements == null || queryElements.size() == 0) {
- log.warn("[{}] Query is empty!", ctx.getSessionId());
- throw new AdaptorException(new IllegalArgumentException("Query is empty!"));
- }
-
- Set<String> clientKeys = toKeys(ctx, queryElements, "clientKeys");
- Set<String> sharedKeys = toKeys(ctx, queryElements, "sharedKeys");
- if (clientKeys.isEmpty() && sharedKeys.isEmpty()) {
- throw new AdaptorException("No clientKeys and serverKeys parameters!");
+ if (queryElements != null || queryElements.size() > 0) {
+ Set<String> clientKeys = toKeys(ctx, queryElements, "clientKeys");
+ Set<String> sharedKeys = toKeys(ctx, queryElements, "sharedKeys");
+ return new BasicGetAttributesRequest(0, clientKeys, sharedKeys);
+ } else {
+ return new BasicGetAttributesRequest(0);
}
- return new BasicGetAttributesRequest(0, clientKeys, sharedKeys);
}
private Set<String> toKeys(SessionContext ctx, List<String> queryElements, String attributeName) throws AdaptorException {
@@ -191,7 +187,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
if (!StringUtils.isEmpty(keys)) {
return new HashSet<>(Arrays.asList(keys.split(",")));
} else {
- return Collections.emptySet();
+ return null;
}
}
diff --git a/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
index a2d6c25..ceb6813 100644
--- a/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
+++ b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
@@ -182,7 +182,7 @@ public class CoapServerTest {
public void testNoKeysAttributesGetRequest() {
CoapClient client = new CoapClient(getBaseTestUrl() + DEVICE1_TOKEN + "/" + FeatureType.ATTRIBUTES.name().toLowerCase() + "?data=key1,key2");
CoapResponse response = client.setTimeout(6000).get();
- Assert.assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
+ Assert.assertEquals(ResponseCode.CONTENT, response.getCode());
}
@Test
transport/http/pom.xml 2(+1 -1)
diff --git a/transport/http/pom.xml b/transport/http/pom.xml
index d1b68a5..1d93af8 100644
--- a/transport/http/pom.xml
+++ b/transport/http/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>
diff --git a/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
index e3e0666..e815bc5 100644
--- a/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
+++ b/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
@@ -38,6 +38,7 @@ import org.thingsboard.server.common.transport.auth.DeviceAuthService;
import org.thingsboard.server.transport.http.session.HttpSessionCtx;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@@ -60,20 +61,22 @@ public class DeviceApiController {
@RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.GET, produces = "application/json")
public DeferredResult<ResponseEntity> getDeviceAttributes(@PathVariable("deviceToken") String deviceToken,
- @RequestParam(value = "clientKeys", required = false) String clientKeys,
- @RequestParam(value = "sharedKeys", required = false) String sharedKeys) {
+ @RequestParam(value = "clientKeys", required = false, defaultValue = "") String clientKeys,
+ @RequestParam(value = "sharedKeys", required = false, defaultValue = "") String sharedKeys) {
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
- if (StringUtils.isEmpty(clientKeys) && StringUtils.isEmpty(sharedKeys)) {
- responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
- } else {
- HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
- if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
- Set<String> clientKeySet = new HashSet<>(Arrays.asList(clientKeys.split(",")));
- Set<String> sharedKeySet = new HashSet<>(Arrays.asList(clientKeys.split(",")));
- process(ctx, new BasicGetAttributesRequest(0, clientKeySet, sharedKeySet));
+ HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
+ if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
+ GetAttributesRequest request;
+ if (StringUtils.isEmpty(clientKeys) && StringUtils.isEmpty(sharedKeys)) {
+ request = new BasicGetAttributesRequest(0);
} else {
- responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
+ Set<String> clientKeySet = !StringUtils.isEmpty(clientKeys) ? new HashSet<>(Arrays.asList(clientKeys.split(","))) : null;
+ Set<String> sharedKeySet = !StringUtils.isEmpty(sharedKeys) ? new HashSet<>(Arrays.asList(sharedKeys.split(","))) : null;
+ request = new BasicGetAttributesRequest(0, clientKeySet, sharedKeySet);
}
+ process(ctx, request);
+ } else {
+ responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
}
return responseWriter;
transport/mqtt/pom.xml 2(+1 -1)
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
index d5826b0..ba7128d 100644
--- a/transport/mqtt/pom.xml
+++ b/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
index 5af244a..e84e848 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
@@ -162,8 +162,13 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
Integer requestId = Integer.valueOf(topicName.substring(MqttTransportHandler.ATTRIBUTES_REQUEST_TOPIC_PREFIX.length()));
String payload = inbound.payload().toString(UTF8);
JsonElement requestBody = new JsonParser().parse(payload);
- return new BasicGetAttributesRequest(requestId,
- toStringSet(requestBody, "clientKeys"), toStringSet(requestBody, "sharedKeys"));
+ Set<String> clientKeys = toStringSet(requestBody, "clientKeys");
+ Set<String> sharedKeys = toStringSet(requestBody, "sharedKeys");
+ if (clientKeys == null && sharedKeys == null) {
+ return new BasicGetAttributesRequest(requestId);
+ } else {
+ return new BasicGetAttributesRequest(requestId, clientKeys, sharedKeys);
+ }
} catch (RuntimeException e) {
log.warn("Failed to decode get attributes request", e);
throw new AdaptorException(e);
@@ -189,7 +194,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
if (element != null) {
return new HashSet<>(Arrays.asList(element.getAsString().split(",")));
} else {
- return Collections.emptySet();
+ return null;
}
}
transport/pom.xml 2(+1 -1)
diff --git a/transport/pom.xml b/transport/pom.xml
index ac12461..4b7c8ab 100644
--- a/transport/pom.xml
+++ b/transport/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
ui/pom.xml 2(+1 -1)
diff --git a/ui/pom.xml b/ui/pom.xml
index 1559ecb..d72f7ff 100644
--- a/ui/pom.xml
+++ b/ui/pom.xml
@@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
- <version>1.0.1</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
ui/src/app/api/device.service.js 3(+2 -1)
diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js
index cf38d12..27cf605 100644
--- a/ui/src/app/api/device.service.js
+++ b/ui/src/app/api/device.service.js
@@ -293,7 +293,8 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
var deviceAttributesSubscription = deviceAttributesSubscriptionMap[subscriptionId];
if (!deviceAttributesSubscription) {
var subscriptionCommand = {
- deviceId: deviceId
+ deviceId: deviceId,
+ scope: attributeScope
};
var type = attributeScope === types.latestTelemetry.value ?
ui/src/app/app.js 2(+2 -0)
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 3aa6cea..982f15e 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -49,6 +49,7 @@ import thingsboardDialogs from './components/datakey-config-dialog.controller';
import thingsboardMenu from './services/menu.service';
import thingsboardUtils from './common/utils.service';
import thingsboardTypes from './common/types.constant';
+import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter';
import thingsboardHelp from './help/help.directive';
import thingsboardToast from './services/toast';
import thingsboardHome from './layout';
@@ -95,6 +96,7 @@ angular.module('thingsboard', [
thingsboardMenu,
thingsboardUtils,
thingsboardTypes,
+ thingsboardKeyboardShortcut,
thingsboardHelp,
thingsboardToast,
thingsboardHome,
ui/src/app/components/dashboard.directive.js 138(+123 -15)
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index e5a1b02..7c5094a 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -23,6 +23,7 @@ import thingsboardWidget from './widget.directive';
import thingsboardToast from '../services/toast';
import thingsboardTimewindow from './timewindow.directive';
import thingsboardEvents from './tb-event-directives';
+import thingsboardMousepointMenu from './mousepoint-menu.directive';
/* eslint-disable import/no-unresolved, import/default */
@@ -38,6 +39,7 @@ export default angular.module('thingsboard.directives.dashboard', [thingsboardTy
thingsboardWidget,
thingsboardTimewindow,
thingsboardEvents,
+ thingsboardMousepointMenu,
gridster.name])
.directive('tbDashboard', Dashboard)
.name;
@@ -59,7 +61,10 @@ function Dashboard() {
isRemoveActionEnabled: '=',
onEditWidget: '&?',
onRemoveWidget: '&?',
+ onWidgetMouseDown: '&?',
onWidgetClicked: '&?',
+ prepareDashboardContextMenu: '&?',
+ prepareWidgetContextMenu: '&?',
loadWidgets: '&?',
onInit: '&?',
onInitFailed: '&?',
@@ -75,8 +80,9 @@ function Dashboard() {
function DashboardController($scope, $rootScope, $element, $timeout, $log, toast, types) {
var highlightedMode = false;
- var highlightedIndex = -1;
- var mouseDownIndex = -1;
+ var highlightedWidget = null;
+ var selectedWidget = null;
+ var mouseDownWidget = -1;
var widgetMouseMoved = false;
var gridsterParent = null;
@@ -117,6 +123,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
vm.isWidgetExpanded = false;
vm.isHighlighted = isHighlighted;
vm.isNotHighlighted = isNotHighlighted;
+ vm.selectWidget = selectWidget;
+ vm.getSelectedWidget = getSelectedWidget;
vm.highlightWidget = highlightWidget;
vm.resetHighlight = resetHighlight;
@@ -134,6 +142,17 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
vm.removeWidget = removeWidget;
vm.loading = loading;
+ vm.openDashboardContextMenu = openDashboardContextMenu;
+ vm.openWidgetContextMenu = openWidgetContextMenu;
+
+ vm.getEventGridPosition = getEventGridPosition;
+
+ vm.contextMenuItems = [];
+ vm.contextMenuEvent = null;
+
+ vm.widgetContextMenuItems = [];
+ vm.widgetContextMenuEvent = null;
+
//$element[0].onmousemove=function(){
// widgetMouseMove();
// }
@@ -161,10 +180,20 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
$scope.$watch('vm.columns', function () {
vm.gridsterOpts.columns = vm.columns ? vm.columns : 24;
+ if (gridster) {
+ gridster.columns = vm.columns;
+ updateGridsterParams();
+ }
+ updateVisibleRect();
});
$scope.$watch('vm.margins', function () {
vm.gridsterOpts.margins = vm.margins ? vm.margins : [10, 10];
+ if (gridster) {
+ gridster.margins = vm.margins;
+ updateGridsterParams();
+ }
+ updateVisibleRect();
});
$scope.$watch('vm.isEdit', function () {
@@ -230,6 +259,26 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
}, 0, false);
}
+ function updateGridsterParams() {
+ if (gridster) {
+ if (gridster.colWidth === 'auto') {
+ gridster.curColWidth = (gridster.curWidth + (gridster.outerMargin ? -gridster.margins[1] : gridster.margins[1])) / gridster.columns;
+ } else {
+ gridster.curColWidth = gridster.colWidth;
+ }
+ gridster.curRowHeight = gridster.rowHeight;
+ if (angular.isString(gridster.rowHeight)) {
+ if (gridster.rowHeight === 'match') {
+ gridster.curRowHeight = Math.round(gridster.curColWidth);
+ } else if (gridster.rowHeight.indexOf('*') !== -1) {
+ gridster.curRowHeight = Math.round(gridster.curColWidth * gridster.rowHeight.replace('*', '').replace(' ', ''));
+ } else if (gridster.rowHeight.indexOf('/') !== -1) {
+ gridster.curRowHeight = Math.round(gridster.curColWidth / gridster.rowHeight.replace('/', '').replace(' ', ''));
+ }
+ }
+ }
+ }
+
function updateVisibleRect (force, containerResized) {
if (gridster) {
var position = $(gridster.$element).position()
@@ -275,7 +324,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
}
function resetWidgetClick () {
- mouseDownIndex = -1;
+ mouseDownWidget = -1;
widgetMouseMoved = false;
}
@@ -285,25 +334,27 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
}
function widgetMouseDown ($event, widget) {
- mouseDownIndex = vm.widgets.indexOf(widget);
+ mouseDownWidget = widget;
widgetMouseMoved = false;
+ if (vm.onWidgetMouseDown) {
+ vm.onWidgetMouseDown({event: $event, widget: widget});
+ }
}
function widgetMouseMove () {
- if (mouseDownIndex > -1) {
+ if (mouseDownWidget) {
widgetMouseMoved = true;
}
}
function widgetMouseUp ($event, widget) {
$timeout(function () {
- if (!widgetMouseMoved && mouseDownIndex > -1) {
- var index = vm.widgets.indexOf(widget);
- if (index === mouseDownIndex) {
+ if (!widgetMouseMoved && mouseDownWidget) {
+ if (widget === mouseDownWidget) {
widgetClicked($event, widget);
}
}
- mouseDownIndex = -1;
+ mouseDownWidget = null;
widgetMouseMoved = false;
}, 0);
}
@@ -317,6 +368,41 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
}
}
+ function openDashboardContextMenu($event, $mdOpenMousepointMenu) {
+ if (vm.prepareDashboardContextMenu) {
+ vm.contextMenuItems = vm.prepareDashboardContextMenu();
+ if (vm.contextMenuItems && vm.contextMenuItems.length > 0) {
+ vm.contextMenuEvent = $event;
+ $mdOpenMousepointMenu($event);
+ }
+ }
+ }
+
+ function openWidgetContextMenu($event, widget, $mdOpenMousepointMenu) {
+ if (vm.prepareWidgetContextMenu) {
+ vm.widgetContextMenuItems = vm.prepareWidgetContextMenu({widget: widget});
+ if (vm.widgetContextMenuItems && vm.widgetContextMenuItems.length > 0) {
+ vm.widgetContextMenuEvent = $event;
+ $mdOpenMousepointMenu($event);
+ }
+ }
+ }
+
+ function getEventGridPosition(event) {
+ var pos = {
+ row: 0,
+ column: 0
+ }
+ var offset = gridsterParent.offset();
+ var x = event.pageX - offset.left + gridsterParent.scrollLeft();
+ var y = event.pageY - offset.top + gridsterParent.scrollTop();
+ if (gridster) {
+ pos.row = gridster.pixelsToRows(y);
+ pos.column = gridster.pixelsToColumns(x);
+ }
+ return pos;
+ }
+
function editWidget ($event, widget) {
resetWidgetClick();
if ($event) {
@@ -337,10 +423,27 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
}
}
- function highlightWidget(widgetIndex, delay) {
+ function highlightWidget(widget, delay) {
highlightedMode = true;
- highlightedIndex = widgetIndex;
- var item = $('.gridster-item', gridster.$element)[widgetIndex];
+ highlightedWidget = widget;
+ var item = $('.gridster-item', gridster.$element)[vm.widgets.indexOf(widget)];
+ if (item) {
+ var height = $(item).outerHeight(true);
+ var rectHeight = gridsterParent.height();
+ var offset = (rectHeight - height) / 2;
+ var scrollTop = item.offsetTop;
+ if (offset > 0) {
+ scrollTop -= offset;
+ }
+ gridsterParent.animate({
+ scrollTop: scrollTop
+ }, delay);
+ }
+ }
+
+ function selectWidget(widget, delay) {
+ selectedWidget = widget;
+ var item = $('.gridster-item', gridster.$element)[vm.widgets.indexOf(widget)];
if (item) {
var height = $(item).outerHeight(true);
var rectHeight = gridsterParent.height();
@@ -355,17 +458,22 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
}
}
+ function getSelectedWidget() {
+ return selectedWidget;
+ }
+
function resetHighlight() {
highlightedMode = false;
- highlightedIndex = -1;
+ highlightedWidget = null;
+ selectedWidget = null;
}
function isHighlighted(widget) {
- return highlightedMode && vm.widgets.indexOf(widget) === highlightedIndex;
+ return (highlightedMode && highlightedWidget === widget) || (selectedWidget === widget);
}
function isNotHighlighted(widget) {
- return highlightedMode && vm.widgets.indexOf(widget) != highlightedIndex;
+ return highlightedMode && highlightedWidget != widget;
}
function widgetColor(widget) {
ui/src/app/components/dashboard.scss 2(+2 -0)
diff --git a/ui/src/app/components/dashboard.scss b/ui/src/app/components/dashboard.scss
index 1b08cdd..d5e4aee 100644
--- a/ui/src/app/components/dashboard.scss
+++ b/ui/src/app/components/dashboard.scss
@@ -20,6 +20,7 @@ div.tb-widget {
height: 100%;
margin: 0;
overflow: hidden;
+ outline: none;
@include transition(all .2s ease-in-out);
.tb-widget-title {
@@ -91,6 +92,7 @@ md-content.tb-dashboard-content {
left: 0;
right: 0;
bottom: 0;
+ outline: none;
}
.tb-widget-error-container {
ui/src/app/components/dashboard.tpl.html 144(+85 -59)
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index f0cecef..c43caac 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -19,64 +19,90 @@
ng-show="(vm.loading() || vm.dashboardLoading) && !vm.isEdit">
<md-progress-circular md-mode="indeterminate" class="md-warn" md-diameter="100"></md-progress-circular>
</md-content>
-<md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap>
- <div ng-style="vm.dashboardStyle" id="gridster-background" style="height: auto; min-height: 100%;">
- <div id="gridster-child" gridster="vm.gridsterOpts">
- <ul>
- <!-- ng-click="widgetClicked($event, widget)" -->
- <li gridster-item="widget" ng-repeat="widget in vm.widgets">
- <div tb-expand-fullscreen expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp"
- ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}"
- tb-mousedown="vm.widgetMouseDown($event, widget)"
- tb-mousemove="vm.widgetMouseMove($event, widget)"
- tb-mouseup="vm.widgetMouseUp($event, widget)"
- style="
- cursor: pointer;
- color: {{vm.widgetColor(widget)}};
- background-color: {{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>
- </div>
- <div class="tb-widget-actions" layout="row" layout-align="start center">
- <md-button id="expand-button"
- aria-label="{{ 'fullscreen.fullscreen' | translate }}"
- class="md-icon-button md-primary"></md-button>
- <md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
- ng-disabled="vm.loading()"
- class="md-icon-button md-primary"
- ng-click="vm.editWidget($event, widget)"
- aria-label="{{ 'widget.edit' | translate }}">
- <md-tooltip md-direction="top">
- {{ 'widget.edit' | translate }}
- </md-tooltip>
- <md-icon class="material-icons">
- edit
- </md-icon>
- </md-button>
- <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
- ng-disabled="vm.loading()"
- class="md-icon-button md-primary"
- ng-click="vm.removeWidget($event, widget)"
- aria-label="{{ 'widget.remove' | translate }}">
- <md-tooltip md-direction="top">
- {{ 'widget.remove' | translate }}
- </md-tooltip>
- <md-icon class="material-icons">
- close
- </md-icon>
- </md-button>
- </div>
- <div flex layout="column" class="tb-widget-content">
- <div flex tb-widget
- locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
+<md-menu md-position-mode="target target" tb-mousepoint-menu>
+ <md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap ng-click="" tb-contextmenu="vm.openDashboardContextMenu($event, $mdOpenMousepointMenu)">
+ <div ng-style="vm.dashboardStyle" id="gridster-background" style="height: auto; min-height: 100%;">
+ <div id="gridster-child" gridster="vm.gridsterOpts">
+ <ul>
+ <!-- ng-click="widgetClicked($event, widget)" -->
+ <li gridster-item="widget" ng-repeat="widget in vm.widgets">
+ <md-menu md-position-mode="target target" tb-mousepoint-menu>
+ <div tb-expand-fullscreen
+ expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp"
+ ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}"
+ tb-mousedown="vm.widgetMouseDown($event, widget)"
+ tb-mousemove="vm.widgetMouseMove($event, widget)"
+ 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)}}
+ ">
+ <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>
+ </div>
+ <div class="tb-widget-actions" layout="row" layout-align="start center">
+ <md-button id="expand-button"
+ ng-show="!vm.isEdit"
+ aria-label="{{ 'fullscreen.fullscreen' | translate }}"
+ class="md-icon-button md-primary"></md-button>
+ <md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
+ ng-disabled="vm.loading()"
+ class="md-icon-button md-primary"
+ ng-click="vm.editWidget($event, widget)"
+ aria-label="{{ 'widget.edit' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'widget.edit' | translate }}
+ </md-tooltip>
+ <md-icon class="material-icons">
+ edit
+ </md-icon>
+ </md-button>
+ <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
+ ng-disabled="vm.loading()"
+ class="md-icon-button md-primary"
+ ng-click="vm.removeWidget($event, widget)"
+ aria-label="{{ 'widget.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'widget.remove' | translate }}
+ </md-tooltip>
+ <md-icon class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+ </div>
+ <div flex layout="column" class="tb-widget-content">
+ <div flex tb-widget
+ locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
+ </div>
+ </div>
</div>
- </div>
- </div>
- </li>
- </ul>
+ <md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
+ <md-menu-item ng-repeat ="item in vm.widgetContextMenuItems">
+ <md-button ng-disabled="!item.enabled" ng-click="item.action(vm.widgetContextMenuEvent, widget)">
+ <md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
+ <span translate>{{item.value}}</span>
+ <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
+ </md-button>
+ </md-menu-item>
+ </md-menu-content>
+ </md-menu>
+ </li>
+ </ul>
+ </div>
</div>
- </div>
-</md-content>
\ No newline at end of file
+ </md-content>
+ <md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
+ <md-menu-item ng-repeat ="item in vm.contextMenuItems">
+ <md-button ng-disabled="!item.enabled" ng-click="item.action(vm.contextMenuEvent)">
+ <md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
+ <span translate>{{item.value}}</span>
+ <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
+ </md-button>
+ </md-menu-item>
+ </md-menu-content>
+</md-menu>
\ No newline at end of file
ui/src/app/components/datasource.scss 12(+12 -0)
diff --git a/ui/src/app/components/datasource.scss b/ui/src/app/components/datasource.scss
index 0b22134..b6196ac 100644
--- a/ui/src/app/components/datasource.scss
+++ b/ui/src/app/components/datasource.scss
@@ -38,6 +38,7 @@
.tb-color-preview {
content: '';
+ min-width: 24px;
width: 24px;
height: 24px;
border: 2px solid #fff;
@@ -52,3 +53,14 @@
height: 100%;
}
}
+
+.tb-attribute-chip {
+ .tb-chip-label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .tb-chip-separator {
+ white-space: pre;
+ }
+}
diff --git a/ui/src/app/components/datasource.tpl.html b/ui/src/app/components/datasource.tpl.html
index e698cc3..7ffd19f 100644
--- a/ui/src/app/components/datasource.tpl.html
+++ b/ui/src/app/components/datasource.tpl.html
@@ -15,7 +15,7 @@
limitations under the License.
-->
-<section flex layout='row' layout-align="start center" class="tb-datasource">
+<section flex layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center" class="tb-datasource">
<md-input-container style="min-width: 110px;">
<md-select placeholder="{{ 'datasource.type' | translate }}" required id="datasourceType" ng-model="model.type">
<md-option ng-repeat="datasourceType in datasourceTypes" value="{{datasourceType}}">
@@ -23,15 +23,15 @@
</md-option>
</md-select>
</md-input-container>
- <section flex layout='row' layout-align="start center" class="datasource" ng-switch on="model.type">
- <tb-datasource-func flex style="padding-left: 8px;"
+ <section flex class="datasource" ng-switch on="model.type">
+ <tb-datasource-func flex
ng-switch-default
ng-model="model"
datakey-settings-schema="datakeySettingsSchema"
ng-required="model.type === types.datasourceType.function"
generate-data-key="generateDataKey({chip: chip, type: type})">
</tb-datasource-func>
- <tb-datasource-device flex style="padding-left: 4px; padding-right: 4px;"
+ <tb-datasource-device flex
ng-model="model"
datakey-settings-schema="datakeySettingsSchema"
ng-switch-when="device"
ui/src/app/components/datasource-device.scss 16(+16 -0)
diff --git a/ui/src/app/components/datasource-device.scss b/ui/src/app/components/datasource-device.scss
index 5e01d1f..a584b13 100644
--- a/ui/src/app/components/datasource-device.scss
+++ b/ui/src/app/components/datasource-device.scss
@@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+@import '../../scss/constants';
+
.tb-device-alias-autocomplete, .tb-timeseries-datakey-autocomplete, .tb-attribute-datakey-autocomplete {
.tb-not-found {
display: block;
@@ -27,3 +30,16 @@
white-space: normal !important;
}
}
+
+tb-datasource-device {
+ @media (min-width: $layout-breakpoint-gt-sm) {
+ padding-left: 4px;
+ padding-right: 4px;
+ }
+ tb-device-alias-select {
+ @media (min-width: $layout-breakpoint-gt-sm) {
+ width: 200px;
+ max-width: 200px;
+ }
+ }
+}
\ No newline at end of file
ui/src/app/components/datasource-device.tpl.html 58(+34 -24)
diff --git a/ui/src/app/components/datasource-device.tpl.html b/ui/src/app/components/datasource-device.tpl.html
index c9f4b10..1368cd1 100644
--- a/ui/src/app/components/datasource-device.tpl.html
+++ b/ui/src/app/components/datasource-device.tpl.html
@@ -15,16 +15,16 @@
limitations under the License.
-->
-<section flex layout='row' layout-align="start center">
- <tb-device-alias-select flex="40"
+<section flex layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
+ <tb-device-alias-select
tb-required="true"
device-aliases="deviceAliases"
ng-model="deviceAlias"
on-create-device-alias="onCreateDeviceAlias({event: event, alias: alias})">
</tb-device-alias-select>
- <section flex="120" layout='column'>
- <section flex layout='row' layout-align="start center">
- <md-chips flex style="padding-left: 4px;"
+ <section flex layout='column'>
+ <section flex layout='column' layout-align="center" style="padding-left: 4px;">
+ <md-chips flex
id="timeseries_datakey_chips"
ng-required="true"
ng-model="timeseriesDataKeys" md-autocomplete-snap
@@ -56,14 +56,19 @@
</md-not-found>
</md-autocomplete>
<md-chip-template>
- <div layout="row" layout-align="start center">
+ <div layout="row" layout-align="start center" class="tb-attribute-chip">
<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
</div>
- <div>
- {{$chip.label}}:
- <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
- <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
+ <div layout="row" flex>
+ <div class="tb-chip-label">
+ {{$chip.label}}
+ </div>
+ <div class="tb-chip-separator">: </div>
+ <div class="tb-chip-label">
+ <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
+ <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
+ </div>
</div>
<md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
<md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
@@ -71,7 +76,7 @@
</div>
</md-chip-template>
</md-chips>
- <md-chips flex ng-if="widgetType === types.widgetType.latest.value" style="padding-left: 4px;"
+ <md-chips flex ng-if="widgetType === types.widgetType.latest.value"
id="attribute_datakey_chips"
ng-required="true"
ng-model="attributeDataKeys" md-autocomplete-snap
@@ -103,19 +108,24 @@
</md-not-found>
</md-autocomplete>
<md-chip-template>
- <div layout="row" layout-align="start center">
- <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
- <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
- </div>
- <div>
- {{$chip.label}}:
- <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
- <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
- </div>
- <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
- <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
- </md-button>
- </div>
+ <div layout="row" layout-align="start center" class="tb-attribute-chip">
+ <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
+ <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
+ </div>
+ <div layout="row" flex>
+ <div class="tb-chip-label">
+ {{$chip.label}}
+ </div>
+ <div class="tb-chip-separator">: </div>
+ <div class="tb-chip-label">
+ <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
+ <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
+ </div>
+ </div>
+ <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
+ <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
+ </md-button>
+ </div>
</md-chip-template>
</md-chips>
</section>
diff --git a/ui/src/app/components/datasource-func.scss b/ui/src/app/components/datasource-func.scss
index 08dbd4e..84e4150 100644
--- a/ui/src/app/components/datasource-func.scss
+++ b/ui/src/app/components/datasource-func.scss
@@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+@import '../../scss/constants';
+
.tb-func-datakey-autocomplete {
.tb-not-found {
display: block;
@@ -27,3 +30,9 @@
white-space: normal !important;
}
}
+
+tb-datasource-func {
+ @media (min-width: $layout-breakpoint-gt-sm) {
+ padding-left: 8px;
+ }
+}
\ No newline at end of file
ui/src/app/components/datasource-func.tpl.html 33(+19 -14)
diff --git a/ui/src/app/components/datasource-func.tpl.html b/ui/src/app/components/datasource-func.tpl.html
index 2b509f5..9dce208 100644
--- a/ui/src/app/components/datasource-func.tpl.html
+++ b/ui/src/app/components/datasource-func.tpl.html
@@ -15,8 +15,8 @@
limitations under the License.
-->
-<section flex layout='column'>
- <md-chips flex style="padding-left: 4px;"
+<section flex layout='column' style="padding-left: 4px;">
+ <md-chips flex
id="function_datakey_chips"
ng-required="true"
ng-model="funcDataKeys" md-autocomplete-snap
@@ -48,18 +48,23 @@
</md-not-found>
</md-autocomplete>
<md-chip-template>
- <div layout="row" layout-align="start center">
- <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
- <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
- </div>
- <div>
- {{$chip.label}}:
- <strong>{{$chip.name}}</strong>
- </div>
- <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
- <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
- </md-button>
- </div>
+ <div layout="row" layout-align="start center" class="tb-attribute-chip">
+ <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
+ <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
+ </div>
+ <div layout="row" flex>
+ <div class="tb-chip-label">
+ {{$chip.label}}
+ </div>
+ <div class="tb-chip-separator">: </div>
+ <div class="tb-chip-label">
+ <strong>{{$chip.name}}</strong>
+ </div>
+ </div>
+ <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
+ <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
+ </md-button>
+ </div>
</md-chip-template>
</md-chips>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
ui/src/app/components/details-sidenav.scss 13(+13 -0)
diff --git a/ui/src/app/components/details-sidenav.scss b/ui/src/app/components/details-sidenav.scss
index 2dc2b51..4ad2988 100644
--- a/ui/src/app/components/details-sidenav.scss
+++ b/ui/src/app/components/details-sidenav.scss
@@ -20,15 +20,28 @@
font-weight: 400;
text-transform: uppercase;
margin: 20px 8px 0 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: inherit;
}
.tb-details-subtitle {
font-size: 1.000rem;
margin: 10px 0;
opacity: 0.8;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: inherit;
}
md-sidenav.tb-sidenav-details {
+ .md-toolbar-tools {
+ min-height: 100px;
+ max-height: 120px;
+ height: 100%;
+ }
width: 100% !important;
max-width: 100% !important;
z-index: 59 !important;
diff --git a/ui/src/app/components/details-sidenav.tpl.html b/ui/src/app/components/details-sidenav.tpl.html
index a5e84e0..eddcc42 100644
--- a/ui/src/app/components/details-sidenav.tpl.html
+++ b/ui/src/app/components/details-sidenav.tpl.html
@@ -22,13 +22,12 @@
layout="column">
<header>
<md-toolbar class="md-theme-light" ng-style="{'height':headerHeightPx+'px'}">
- <div class="md-toolbar-tools">
- <div class="md-toolbar-tools" layout="column" layout-align="start start">
+ <div class="md-toolbar-tools" layout="row">
+ <div flex class="md-toolbar-tools" layout="column" layout-align="start start">
<span class="tb-details-title">{{headerTitle}}</span>
<span class="tb-details-subtitle">{{headerSubtitle}}</span>
<span style="width: 100%;" ng-transclude="headerPane"></span>
</div>
- <span flex></span>
<div ng-transclude="detailsButtons"></div>
<md-button class="md-icon-button" ng-click="closeDetails()">
<md-icon aria-label="close" class="material-icons">close</md-icon>
diff --git a/ui/src/app/components/keyboard-shortcut.filter.js b/ui/src/app/components/keyboard-shortcut.filter.js
new file mode 100644
index 0000000..289afe2
--- /dev/null
+++ b/ui/src/app/components/keyboard-shortcut.filter.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2016 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 default angular.module('thingsboard.filters.keyboardShortcut', [])
+ .filter('keyboardShortcut', KeyboardShortcut)
+ .name;
+
+/*@ngInject*/
+function KeyboardShortcut($window) {
+ return function(str) {
+ if (!str) return;
+ var keys = str.split('-');
+ var isOSX = /Mac OS X/.test($window.navigator.userAgent);
+
+ var seperator = (!isOSX || keys.length > 2) ? '+' : '';
+
+ var abbreviations = {
+ M: isOSX ? '⌘' : 'Ctrl',
+ A: isOSX ? 'Option' : 'Alt',
+ S: 'Shift'
+ };
+
+ return keys.map(function(key, index) {
+ var last = index == keys.length - 1;
+ return last ? key : abbreviations[key];
+ }).join(seperator);
+ };
+}
diff --git a/ui/src/app/components/mousepoint-menu.directive.js b/ui/src/app/components/mousepoint-menu.directive.js
new file mode 100644
index 0000000..b3f24c7
--- /dev/null
+++ b/ui/src/app/components/mousepoint-menu.directive.js
@@ -0,0 +1,51 @@
+/*
+ * Copyright © 2016 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 default angular.module('thingsboard.directives.mousepointMenu', [])
+ .directive('tbMousepointMenu', MousepointMenu)
+ .name;
+
+/*@ngInject*/
+function MousepointMenu() {
+
+ var linker = function ($scope, $element, $attrs, RightClickContextMenu) {
+
+ $scope.$mdOpenMousepointMenu = function($event){
+ RightClickContextMenu.offsets = function(){
+ var offset = $element.offset();
+ var x = $event.pageX - offset.left;
+ var y = $event.pageY - offset.top;
+
+ var offsets = {
+ left: x,
+ top: y
+ }
+ return offsets;
+ }
+ RightClickContextMenu.open($event);
+ };
+
+ $scope.$mdCloseMousepointMenu = function() {
+ RightClickContextMenu.close();
+ }
+ }
+
+ return {
+ restrict: "A",
+ link: linker,
+ require: 'mdMenu'
+ };
+}
diff --git a/ui/src/app/components/tb-event-directives.js b/ui/src/app/components/tb-event-directives.js
index 1fefd51..f7e7fe9 100644
--- a/ui/src/app/components/tb-event-directives.js
+++ b/ui/src/app/components/tb-event-directives.js
@@ -20,7 +20,7 @@ const PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
var tbEventDirectives = {};
angular.forEach(
- 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
+ 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave contextmenu keydown keyup keypress submit focus blur copy cut paste'.split(' '),
function(eventName) {
var directiveName = directiveNormalize('tb-' + eventName);
tbEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse) {
diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js
index 565c8a1..fbc559b 100644
--- a/ui/src/app/components/widget.controller.js
+++ b/ui/src/app/components/widget.controller.js
@@ -159,6 +159,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
};
vm.gridsterItemInitialized = gridsterItemInitialized;
+ vm.visibleRectChanged = visibleRectChanged;
function gridsterItemInitialized(item) {
if (item) {
@@ -167,6 +168,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
}
}
+ function visibleRectChanged(newVisibleRect) {
+ visibleRect = newVisibleRect;
+ updateVisibility();
+ }
+
initWidget();
function initWidget() {
@@ -221,11 +227,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
$scope.$emit("widgetPositionChanged", widget);
});
- $scope.$on('visibleRectChanged', function (event, newVisibleRect) {
- visibleRect = newVisibleRect;
- updateVisibility();
- });
-
$scope.$on('onWidgetFullscreenChanged', function(event, isWidgetExpanded, fullscreenWidget) {
if (widget === fullscreenWidget) {
onRedraw(0);
@@ -318,9 +319,10 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
function onRedraw(delay, dataUpdate) {
- if (!visible) {
+ //TODO:
+ /*if (!visible) {
return;
- }
+ }*/
if (angular.isUndefined(delay)) {
delay = 0;
}
diff --git a/ui/src/app/components/widget.directive.js b/ui/src/app/components/widget.directive.js
index 6ba25c3..b6000cb 100644
--- a/ui/src/app/components/widget.directive.js
+++ b/ui/src/app/components/widget.directive.js
@@ -34,12 +34,19 @@ function Widget($controller, $compile, widgetService) {
var widget = locals.widget;
var gridsterItem;
+ scope.$on('visibleRectChanged', function (event, newVisibleRect) {
+ locals.visibleRect = newVisibleRect;
+ if (widgetController) {
+ widgetController.visibleRectChanged(newVisibleRect);
+ }
+ });
+
scope.$on('gridster-item-initialized', function (event, item) {
gridsterItem = item;
if (widgetController) {
widgetController.gridsterItemInitialized(gridsterItem);
}
- })
+ });
elem.html('<div flex layout="column" layout-align="center center" style="height: 100%;">' +
' <md-progress-circular md-mode="indeterminate" class="md-accent md-hue-2" md-diameter="120"></md-progress-circular>' +
diff --git a/ui/src/app/components/widget-config.tpl.html b/ui/src/app/components/widget-config.tpl.html
index 896e1e8..b76bad6 100644
--- a/ui/src/app/components/widget-config.tpl.html
+++ b/ui/src/app/components/widget-config.tpl.html
@@ -25,7 +25,7 @@
<input name="title" ng-model="title">
</md-input-container>
<span translate>widget-config.general-settings</span>
- <div layout="row" layout-align="start center">
+ <div layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
<div layout="row" layout-padding>
<md-checkbox flex aria-label="{{ 'widget-config.display-title' | translate }}"
ng-model="showTitle">{{ 'widget-config.display-title' | translate }}
@@ -80,7 +80,7 @@
<div flex layout="row" layout-align="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<span translate style="min-width: 110px;">widget-config.datasource-type</span>
- <span translate flex
+ <span hide show-gt-sm translate flex
style="padding-left: 10px;">widget-config.datasource-parameters</span>
<span style="min-width: 40px;"></span>
</div>
diff --git a/ui/src/app/components/widgets-bundle-select.directive.js b/ui/src/app/components/widgets-bundle-select.directive.js
index fcf5def..1525523 100644
--- a/ui/src/app/components/widgets-bundle-select.directive.js
+++ b/ui/src/app/components/widgets-bundle-select.directive.js
@@ -55,12 +55,26 @@ function WidgetsBundleSelect($compile, $templateCache, widgetService, types) {
if (widgetsBundles.length > 0) {
scope.widgetsBundle = widgetsBundles[0];
}
+ } else if (angular.isDefined(scope.selectBundleAlias)) {
+ selectWidgetsBundleByAlias(scope.selectBundleAlias);
}
},
function fail() {
}
);
+ function selectWidgetsBundleByAlias(alias) {
+ if (scope.widgetsBundles && alias) {
+ for (var w in scope.widgetsBundles) {
+ var widgetsBundle = scope.widgetsBundles[w];
+ if (widgetsBundle.alias === alias) {
+ scope.widgetsBundle = widgetsBundle;
+ break;
+ }
+ }
+ }
+ }
+
scope.isSystem = function(item) {
return item && item.tenantId.id === types.id.nullUid;
}
@@ -79,6 +93,12 @@ function WidgetsBundleSelect($compile, $templateCache, widgetService, types) {
scope.updateView();
});
+ scope.$watch('selectBundleAlias', function (newVal, prevVal) {
+ if (newVal !== prevVal) {
+ selectWidgetsBundleByAlias(scope.selectBundleAlias);
+ }
+ });
+
$compile(element.contents())(scope);
}
@@ -90,7 +110,8 @@ function WidgetsBundleSelect($compile, $templateCache, widgetService, types) {
bundlesScope: '@',
theForm: '=?',
tbRequired: '=?',
- selectFirstBundle: '='
+ selectFirstBundle: '=',
+ selectBundleAlias: '=?'
}
};
}
\ No newline at end of file
diff --git a/ui/src/app/components/widgets-bundle-select.scss b/ui/src/app/components/widgets-bundle-select.scss
index 7b573f2..e5492c6 100644
--- a/ui/src/app/components/widgets-bundle-select.scss
+++ b/ui/src/app/components/widgets-bundle-select.scss
@@ -35,10 +35,12 @@ tb-widgets-bundle-select {
tb-widgets-bundle-select, .tb-widgets-bundle-select {
.md-text {
+ display: block;
width: 100%;
}
.tb-bundle-item {
- display: block;
+ display: inline-block;
+ width: 100%;
span {
display: inline-block;
vertical-align: middle;
ui/src/app/dashboard/dashboard.controller.js 217(+209 -8)
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 68978c3..43225f5 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -23,7 +23,7 @@ import addWidgetTemplate from './add-widget.tpl.html';
/*@ngInject*/
export default function DashboardController(types, widgetService, userService,
- dashboardService, $window, $rootScope,
+ dashboardService, itembuffer, hotkeys, $window, $rootScope,
$scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
var user = userService.getCurrentUser();
@@ -48,7 +48,10 @@ export default function DashboardController(types, widgetService, userService,
vm.addWidgetFromType = addWidgetFromType;
vm.dashboardInited = dashboardInited;
vm.dashboardInitFailed = dashboardInitFailed;
+ vm.widgetMouseDown = widgetMouseDown;
vm.widgetClicked = widgetClicked;
+ vm.prepareDashboardContextMenu = prepareDashboardContextMenu;
+ vm.prepareWidgetContextMenu = prepareWidgetContextMenu;
vm.editWidget = editWidget;
vm.isTenantAdmin = isTenantAdmin;
vm.loadDashboard = loadDashboard;
@@ -63,6 +66,7 @@ export default function DashboardController(types, widgetService, userService,
vm.toggleDashboardEditMode = toggleDashboardEditMode;
vm.onRevertWidgetEdit = onRevertWidgetEdit;
vm.helpLinkIdForWidgetType = helpLinkIdForWidgetType;
+ vm.displayTitle = displayTitle;
vm.widgetsBundle;
@@ -194,6 +198,7 @@ export default function DashboardController(types, widgetService, userService,
function dashboardInited(dashboard) {
vm.dashboardContainer = dashboard;
+ initHotKeys();
}
function isTenantAdmin() {
@@ -289,18 +294,194 @@ export default function DashboardController(types, widgetService, userService,
var delayOffset = transition ? 350 : 0;
var delay = transition ? 400 : 300;
$timeout(function () {
- vm.dashboardContainer.highlightWidget(vm.editingWidgetIndex, delay);
+ vm.dashboardContainer.highlightWidget(widget, delay);
}, delayOffset, false);
}
}
}
+ function widgetMouseDown($event, widget) {
+ if (vm.isEdit && !vm.isEditingWidget) {
+ vm.dashboardContainer.selectWidget(widget, 0);
+ }
+ }
+
function widgetClicked($event, widget) {
if (vm.isEditingWidget) {
editWidget($event, widget);
}
}
+ function isHotKeyAllowed(event) {
+ var target = event.target || event.srcElement;
+ var scope = angular.element(target).scope();
+ return scope && scope.$parent !== $rootScope;
+ }
+
+ function initHotKeys() {
+ $translate(['action.copy', 'action.paste', 'action.delete']).then(function (translations) {
+ hotkeys.bindTo($scope)
+ .add({
+ combo: 'ctrl+c',
+ description: translations['action.copy'],
+ callback: function (event) {
+ if (isHotKeyAllowed(event) &&
+ vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+ var widget = vm.dashboardContainer.getSelectedWidget();
+ if (widget) {
+ event.preventDefault();
+ copyWidget(event, widget);
+ }
+ }
+ }
+ })
+ .add({
+ combo: 'ctrl+v',
+ description: translations['action.paste'],
+ callback: function (event) {
+ if (isHotKeyAllowed(event) &&
+ vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+ if (itembuffer.hasWidget()) {
+ event.preventDefault();
+ pasteWidget(event);
+ }
+ }
+ }
+ })
+ .add({
+ combo: 'ctrl+x',
+ description: translations['action.delete'],
+ callback: function (event) {
+ if (isHotKeyAllowed(event) &&
+ vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+ var widget = vm.dashboardContainer.getSelectedWidget();
+ if (widget) {
+ event.preventDefault();
+ removeWidget(event, widget);
+ }
+ }
+ }
+ });
+ });
+ }
+
+ function prepareDashboardContextMenu() {
+ var dashboardContextActions = [];
+ if (vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) {
+ dashboardContextActions.push(
+ {
+ action: openDashboardSettings,
+ enabled: true,
+ value: "dashboard.settings",
+ icon: "settings"
+ }
+ );
+ dashboardContextActions.push(
+ {
+ action: openDeviceAliases,
+ enabled: true,
+ value: "device.aliases",
+ icon: "devices_other"
+ }
+ );
+ dashboardContextActions.push(
+ {
+ action: pasteWidget,
+ enabled: itembuffer.hasWidget(),
+ value: "action.paste",
+ icon: "content_paste",
+ shortcut: "M-V"
+ }
+ );
+ }
+ return dashboardContextActions;
+ }
+
+ function pasteWidget($event) {
+ var pos = vm.dashboardContainer.getEventGridPosition($event);
+ itembuffer.pasteWidget(vm.dashboard, pos);
+ }
+
+ function prepareWidgetContextMenu() {
+ var widgetContextActions = [];
+ if (vm.isEdit && !vm.isEditingWidget) {
+ widgetContextActions.push(
+ {
+ action: editWidget,
+ enabled: true,
+ value: "action.edit",
+ icon: "edit"
+ }
+ );
+ if (!vm.widgetEditMode) {
+ widgetContextActions.push(
+ {
+ action: copyWidget,
+ enabled: true,
+ value: "action.copy",
+ icon: "content_copy",
+ shortcut: "M-C"
+ }
+ );
+ widgetContextActions.push(
+ {
+ action: removeWidget,
+ enabled: true,
+ value: "action.delete",
+ icon: "clear",
+ shortcut: "M-X"
+ }
+ );
+ }
+ }
+ return widgetContextActions;
+ }
+
+ function copyWidget($event, widget) {
+ var aliasesInfo = {
+ datasourceAliases: {},
+ targetDeviceAliases: {}
+ };
+ var originalColumns = 24;
+ if (vm.dashboard.configuration.gridSettings &&
+ vm.dashboard.configuration.gridSettings.columns) {
+ originalColumns = vm.dashboard.configuration.gridSettings.columns;
+ }
+ if (widget.config && vm.dashboard.configuration
+ && vm.dashboard.configuration.deviceAliases) {
+ var deviceAlias;
+ if (widget.config.datasources) {
+ for (var i=0;i<widget.config.datasources.length;i++) {
+ var datasource = widget.config.datasources[i];
+ if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
+ deviceAlias = vm.dashboard.configuration.deviceAliases[datasource.deviceAliasId];
+ if (deviceAlias) {
+ aliasesInfo.datasourceAliases[i] = {
+ aliasName: deviceAlias.alias,
+ deviceId: deviceAlias.deviceId
+ }
+ }
+ }
+ }
+ }
+ if (widget.config.targetDeviceAliasIds) {
+ for (i=0;i<widget.config.targetDeviceAliasIds.length;i++) {
+ var targetDeviceAliasId = widget.config.targetDeviceAliasIds[i];
+ if (targetDeviceAliasId) {
+ deviceAlias = vm.dashboard.configuration.deviceAliases[targetDeviceAliasId];
+ if (deviceAlias) {
+ aliasesInfo.targetDeviceAliases[i] = {
+ aliasName: deviceAlias.alias,
+ deviceId: deviceAlias.deviceId
+ }
+ }
+ }
+ }
+ }
+ }
+ itembuffer.copyWidget(widget, aliasesInfo, originalColumns);
+ }
+
function helpLinkIdForWidgetType() {
var link = 'widgetsConfig';
if (vm.editingWidget && vm.editingWidget.type) {
@@ -322,6 +503,15 @@ export default function DashboardController(types, widgetService, userService,
return link;
}
+ function displayTitle() {
+ if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
+ angular.isDefined(vm.dashboard.configuration.gridSettings.showTitle)) {
+ return vm.dashboard.configuration.gridSettings.showTitle;
+ } else {
+ return true;
+ }
+ }
+
function onRevertWidgetEdit(widgetForm) {
if (widgetForm.$dirty) {
widgetForm.$setPristine();
@@ -331,7 +521,9 @@ export default function DashboardController(types, widgetService, userService,
function saveWidget(widgetForm) {
widgetForm.$setPristine();
- vm.widgets[vm.editingWidgetIndex] = angular.copy(vm.editingWidget);
+ var widget = angular.copy(vm.editingWidget);
+ vm.widgets[vm.editingWidgetIndex] = widget;
+ vm.dashboardContainer.highlightWidget(widget, 0);
}
function onEditWidgetClosed() {
@@ -421,8 +613,8 @@ export default function DashboardController(types, widgetService, userService,
});
}
- function toggleDashboardEditMode() {
- vm.isEdit = !vm.isEdit;
+ function setEditMode(isEdit, revert) {
+ vm.isEdit = isEdit;
if (vm.isEdit) {
if (vm.widgetEditMode) {
vm.prevWidgets = angular.copy(vm.widgets);
@@ -433,14 +625,23 @@ export default function DashboardController(types, widgetService, userService,
if (vm.widgetEditMode) {
vm.widgets = vm.prevWidgets;
} else {
- vm.dashboard = vm.prevDashboard;
- vm.widgets = vm.dashboard.configuration.widgets;
+ if (vm.dashboardContainer) {
+ vm.dashboardContainer.resetHighlight();
+ }
+ if (revert) {
+ vm.dashboard = vm.prevDashboard;
+ vm.widgets = vm.dashboard.configuration.widgets;
+ }
}
}
}
+ function toggleDashboardEditMode() {
+ setEditMode(!vm.isEdit, true);
+ }
+
function saveDashboard() {
- vm.isEdit = false;
+ setEditMode(false, false);
notifyDashboardUpdated();
}
ui/src/app/dashboard/dashboard.tpl.html 36(+30 -6)
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index 0c815a6..4d9c21e 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -16,7 +16,7 @@
-->
<md-content flex tb-expand-fullscreen="vm.widgetEditMode" hide-expand-button="vm.widgetEditMode">
- <section ng-show="!vm.isAddingWidget && !loading && !vm.widgetEditMode" layout="row" layout-wrap
+ <!--section ng-show="!vm.isAddingWidget && !loading && !vm.widgetEditMode" layout="row" layout-wrap
class="tb-header-buttons tb-top-header-buttons md-fab" ng-style="{'right': '50px'}">
<md-button ng-if="vm.isTenantAdmin()" ng-show="vm.isEdit" ng-disabled="loading"
class="tb-btn-header md-accent md-hue-2 md-fab md-fab-bottom-right"
@@ -37,7 +37,7 @@
<ng-md-icon icon="{{vm.isEdit ? 'close' : 'edit'}}"
options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>
</md-button>
- </section>
+ </section-->
<section ng-show="!loading && vm.noData()" layout-align="center center"
ng-class="{'tb-padded' : !vm.widgetEditMode}"
style="text-transform: uppercase; display: flex; z-index: 1;"
@@ -51,7 +51,7 @@
</md-button>
</section>
<section ng-if="!vm.widgetEditMode" class="tb-dashboard-title" layout="row" layout-align="center center">
- <h3 ng-show="!vm.isEdit">{{ vm.dashboard.title }}</h3>
+ <h3 ng-show="!vm.isEdit && vm.displayTitle()">{{ vm.dashboard.title }}</h3>
<md-input-container ng-show="vm.isEdit" class="md-block" style="height: 30px;">
<label translate>dashboard.title</label>
<input class="tb-dashboard-title" required name="title" ng-model="vm.dashboard.title">
@@ -64,7 +64,7 @@
</md-button>
</section>
<div class="tb-absolute-fill"
- ng-class="{ 'tb-padded' : !vm.widgetEditMode, 'tb-shrinked' : vm.isEditingWidget }">
+ ng-class="{ 'tb-padded' : !vm.widgetEditMode && (vm.isEdit || vm.displayTitle()), 'tb-shrinked' : vm.isEditingWidget }">
<tb-dashboard
dashboard-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,
'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',
@@ -82,7 +82,11 @@
is-edit-action-enabled="vm.isEdit || vm.widgetEditMode"
is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
on-edit-widget="vm.editWidget(event, widget)"
+ on-widget-mouse-down="vm.widgetMouseDown(event, widget)"
on-widget-clicked="vm.widgetClicked(event, widget)"
+ on-widget-context-menu="vm.widgetContextMenu(event, widget)"
+ prepare-dashboard-context-menu="vm.prepareDashboardContextMenu()"
+ prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)"
on-remove-widget="vm.removeWidget(event, widget)"
load-widgets="vm.loadDashboard()"
on-init="vm.dashboardInited(dashboard)"
@@ -176,8 +180,8 @@
</div>
</tb-details-sidenav>
<!-- </section> -->
- <section layout="row" layout-wrap class="tb-footer-buttons md-fab ">
- <md-button ng-disabled="loading" ng-if="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode"
+ <section layout="row" layout-wrap class="tb-footer-buttons md-fab">
+ <md-button ng-disabled="loading" ng-show="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode"
class="tb-btn-footer md-accent md-hue-2 md-fab" ng-click="vm.addWidget($event)"
aria-label="{{ 'dashboard.add-widget' | translate }}">
<md-tooltip md-direction="top">
@@ -185,5 +189,25 @@
</md-tooltip>
<ng-md-icon icon="add"></ng-md-icon>
</md-button>
+ <md-button ng-if="vm.isTenantAdmin()" ng-show="vm.isEdit && !vm.isAddingWidget && !loading && !vm.widgetEditMode" ng-disabled="loading"
+ class="tb-btn-footer md-accent md-hue-2 md-fab"
+ aria-label="{{ 'action.apply' | translate }}"
+ ng-click="vm.saveDashboard()">
+ <md-tooltip md-direction="top">
+ {{ 'action.apply-changes' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="done"></ng-md-icon>
+ </md-button>
+ <md-button ng-show="!vm.isAddingWidget && !loading && !vm.widgetEditMode"
+ ng-if="vm.isTenantAdmin()" ng-disabled="loading"
+ class="tb-btn-footer md-accent md-hue-2 md-fab"
+ aria-label="{{ 'action.edit-mode' | translate }}"
+ ng-click="vm.toggleDashboardEditMode()">
+ <md-tooltip md-direction="top">
+ {{ (vm.isEdit ? 'action.decline-changes' : 'action.enter-edit-mode') | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="{{vm.isEdit ? 'close' : 'edit'}}"
+ options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>
+ </md-button>
</section>
</md-content>
diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js
index d15359e..3a46566 100644
--- a/ui/src/app/dashboard/dashboard-settings.controller.js
+++ b/ui/src/app/dashboard/dashboard-settings.controller.js
@@ -28,6 +28,10 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti
vm.gridSettings = gridSettings || {};
+ if (angular.isUndefined(vm.gridSettings.showTitle)) {
+ vm.gridSettings.showTitle = true;
+ }
+
vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)';
vm.gridSettings.columns = vm.gridSettings.columns || 24;
vm.gridSettings.margins = vm.gridSettings.margins || [10, 10];
diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html
index f69eb02..e28798d 100644
--- a/ui/src/app/dashboard/dashboard-settings.tpl.html
+++ b/ui/src/app/dashboard/dashboard-settings.tpl.html
@@ -31,6 +31,11 @@
<md-dialog-content>
<div class="md-dialog-content">
<fieldset ng-disabled="loading">
+ <div layout="row" layout-padding>
+ <md-checkbox flex aria-label="{{ 'dashboard.display-title' | translate }}"
+ ng-model="vm.gridSettings.showTitle">{{ 'dashboard.display-title' | translate }}
+ </md-checkbox>
+ </div>
<md-input-container class="md-block">
<label translate>dashboard.columns-count</label>
<input required type="number" step="any" name="columns" ng-model="vm.gridSettings.columns" min="10"
ui/src/app/dashboard/index.js 2(+2 -0)
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js
index 7400463..461f56e 100644
--- a/ui/src/app/dashboard/index.js
+++ b/ui/src/app/dashboard/index.js
@@ -29,6 +29,7 @@ import thingsboardDashboard from '../components/dashboard.directive';
import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive';
import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive';
import thingsboardTypes from '../common/types.constant';
+import thingsboardItemBuffer from '../services/item-buffer.service';
import DashboardRoutes from './dashboard.routes';
import DashboardsController from './dashboards.controller';
@@ -45,6 +46,7 @@ export default angular.module('thingsboard.dashboard', [
uiRouter,
gridster.name,
thingsboardTypes,
+ thingsboardItemBuffer,
thingsboardGrid,
thingsboardApiWidget,
thingsboardApiUser,
diff --git a/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js
index 9149cf1..3f6d769 100644
--- a/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js
+++ b/ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js
@@ -14,7 +14,7 @@
* limitations under the License.
*/
/*@ngInject*/
-export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, dashboardService, deviceId, deviceName, widget) {
+export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, itembuffer, dashboardService, deviceId, deviceName, widget) {
var vm = this;
@@ -34,62 +34,20 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog,
function add() {
$scope.theForm.$setPristine();
var theDashboard;
- var deviceAliases;
- widget.col = 0;
- widget.sizeX /= 2;
- widget.sizeY /= 2;
if (vm.addToDashboardType === 0) {
theDashboard = vm.dashboard;
- if (!theDashboard.configuration) {
- theDashboard.configuration = {};
- }
- deviceAliases = theDashboard.configuration.deviceAliases;
- if (!deviceAliases) {
- deviceAliases = {};
- theDashboard.configuration.deviceAliases = deviceAliases;
- }
- var newAliasId;
- for (var aliasId in deviceAliases) {
- if (deviceAliases[aliasId].deviceId === deviceId) {
- newAliasId = aliasId;
- break;
- }
- }
- if (!newAliasId) {
- var newAliasName = createDeviceAliasName(deviceAliases, deviceName);
- newAliasId = 0;
- for (aliasId in deviceAliases) {
- newAliasId = Math.max(newAliasId, aliasId);
- }
- newAliasId++;
- deviceAliases[newAliasId] = {alias: newAliasName, deviceId: deviceId};
- }
- widget.config.datasources[0].deviceAliasId = newAliasId;
-
- if (!theDashboard.configuration.widgets) {
- theDashboard.configuration.widgets = [];
- }
-
- var row = 0;
- for (var w in theDashboard.configuration.widgets) {
- var existingWidget = theDashboard.configuration.widgets[w];
- var wRow = existingWidget.row ? existingWidget.row : 0;
- var wSizeY = existingWidget.sizeY ? existingWidget.sizeY : 1;
- var bottom = wRow + wSizeY;
- row = Math.max(row, bottom);
- }
- widget.row = row;
- theDashboard.configuration.widgets.push(widget);
} else {
theDashboard = vm.newDashboard;
- deviceAliases = {};
- deviceAliases['1'] = {alias: deviceName, deviceId: deviceId};
- theDashboard.configuration = {};
- theDashboard.configuration.widgets = [];
- widget.row = 0;
- theDashboard.configuration.widgets.push(widget);
- theDashboard.configuration.deviceAliases = deviceAliases;
}
+ var aliasesInfo = {
+ datasourceAliases: {},
+ targetDeviceAliases: {}
+ };
+ aliasesInfo.datasourceAliases[0] = {
+ aliasName: deviceName,
+ deviceId: deviceId
+ };
+ theDashboard = itembuffer.addWidgetToDashboard(theDashboard, widget, aliasesInfo, 48, -1, -1);
dashboardService.saveDashboard(theDashboard).then(
function success(dashboard) {
$mdDialog.hide();
@@ -98,25 +56,6 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog,
}
}
);
-
- }
-
- function createDeviceAliasName(deviceAliases, alias) {
- var c = 0;
- var newAlias = angular.copy(alias);
- var unique = false;
- while (!unique) {
- unique = true;
- for (var devAliasId in deviceAliases) {
- var devAlias = deviceAliases[devAliasId];
- if (newAlias === devAlias.alias) {
- c++;
- newAlias = alias + c;
- unique = false;
- }
- }
- }
- return newAlias;
}
}
diff --git a/ui/src/app/device/attribute/attribute-table.directive.js b/ui/src/app/device/attribute/attribute-table.directive.js
index 0a5e0cd..24e9022 100644
--- a/ui/src/app/device/attribute/attribute-table.directive.js
+++ b/ui/src/app/device/attribute/attribute-table.directive.js
@@ -239,6 +239,8 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
index: 0
}
scope.widgetsBundle = null;
+ scope.firstBundle = true;
+ scope.selectedWidgetsBundleAlias = types.systemBundleAlias.cards;
scope.deviceAliases = {};
scope.deviceAliases['1'] = {alias: scope.deviceName, deviceId: scope.deviceId};
@@ -326,13 +328,6 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
}
}
});
-
- widgetService.getWidgetsBundleByAlias(types.systemBundleAlias.cards).then(
- function success(widgetsBundle) {
- scope.firstBundle = true;
- scope.widgetsBundle = widgetsBundle;
- }
- );
}
scope.exitWidgetMode = function() {
@@ -344,6 +339,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
scope.widgetsIndexWatch();
scope.widgetsIndexWatch = null;
}
+ scope.selectedWidgetsBundleAlias = null;
scope.mode = 'default';
scope.getDeviceAttributes(true);
}
diff --git a/ui/src/app/device/attribute/attribute-table.tpl.html b/ui/src/app/device/attribute/attribute-table.tpl.html
index 880d880..a88fe02 100644
--- a/ui/src/app/device/attribute/attribute-table.tpl.html
+++ b/ui/src/app/device/attribute/attribute-table.tpl.html
@@ -105,7 +105,8 @@
<tb-widgets-bundle-select flex-offset="5"
flex
ng-model="widgetsBundle"
- select-first-bundle="false">
+ select-first-bundle="false"
+ select-bundle-alias="selectedWidgetsBundleAlias">
</tb-widgets-bundle-select>
</div>
<md-button ng-show="widgetsList.length > 0" class="md-accent md-hue-2 md-raised" ng-click="addWidgetToDashboard($event)">
diff --git a/ui/src/app/layout/breadcrumb.tpl.html b/ui/src/app/layout/breadcrumb.tpl.html
index b5ae863..3253974 100644
--- a/ui/src/app/layout/breadcrumb.tpl.html
+++ b/ui/src/app/layout/breadcrumb.tpl.html
@@ -15,7 +15,7 @@
limitations under the License.
-->
-<div class="tb-breadcrumb">
+<div flex class="tb-breadcrumb" layout="row">
<h1 flex hide-gt-sm>{{ steps[steps.length-1].ncyBreadcrumbLabel | breadcrumbLabel }}</h1>
<span hide-xs hide-sm ng-repeat="step in steps" ng-switch="$last || !!step.abstract">
<a ng-switch-when="false" href="{{step.ncyBreadcrumbLink}}">
ui/src/app/layout/home.scss 5(+5 -0)
diff --git a/ui/src/app/layout/home.scss b/ui/src/app/layout/home.scss
index f2e4100..f3f9d25 100644
--- a/ui/src/app/layout/home.scss
+++ b/ui/src/app/layout/home.scss
@@ -29,6 +29,11 @@
.tb-breadcrumb {
font-size: 18px !important;
font-weight: 400 !important;
+ h1, a, span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
a {
border: none;
opacity: 0.75;
ui/src/app/layout/home.tpl.html 4(+2 -2)
diff --git a/ui/src/app/layout/home.tpl.html b/ui/src/app/layout/home.tpl.html
index e5a4c15..f5e1571 100644
--- a/ui/src/app/layout/home.tpl.html
+++ b/ui/src/app/layout/home.tpl.html
@@ -39,7 +39,7 @@
<div flex layout="column" tabIndex="-1" role="main">
<md-toolbar class="md-whiteframe-z1 tb-primary-toolbar" ng-class="{'md-hue-1': vm.displaySearchMode()}">
- <div flex class="md-toolbar-tools">
+ <div layout="row" flex class="md-toolbar-tools">
<md-button id="main" hide-gt-sm
class="md-icon-button" ng-click="vm.openSidenav()" aria-label="{{ 'home.menu' | translate }}" ng-class="{'tb-invisible': vm.displaySearchMode()}">
<md-icon aria-label="{{ 'home.menu' | translate }}" class="material-icons">menu</md-icon>
@@ -47,7 +47,7 @@
<md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="searchConfig.showSearch = !searchConfig.showSearch" ng-class="{'tb-invisible': !vm.displaySearchMode()}" >
<md-icon aria-label="{{ 'action.back' | translate }}" class="material-icons">arrow_back</md-icon>
</md-button>
- <div flex ng-show="!vm.displaySearchMode()" tb-no-animate flex class="md-toolbar-tools">
+ <div flex layout="row" ng-show="!vm.displaySearchMode()" tb-no-animate class="md-toolbar-tools">
<span ng-cloak ncy-breadcrumb></span>
</div>
<md-input-container ng-show="vm.displaySearchMode()" md-theme="tb-search-input" flex>
ui/src/app/services/item-buffer.service.js 191(+191 -0)
diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js
new file mode 100644
index 0000000..56d8d6f
--- /dev/null
+++ b/ui/src/app/services/item-buffer.service.js
@@ -0,0 +1,191 @@
+/*
+ * Copyright © 2016 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 angularStorage from 'angular-storage';
+
+export default angular.module('thingsboard.itembuffer', [angularStorage])
+ .factory('itembuffer', ItemBuffer)
+ .factory('bufferStore', function(store) {
+ var newStore = store.getNamespacedStore('tbBufferStore', null, null, false);
+ return newStore;
+ })
+ .name;
+
+/*@ngInject*/
+function ItemBuffer(bufferStore) {
+
+ const WIDGET_ITEM = "widget_item";
+
+ var service = {
+ copyWidget: copyWidget,
+ hasWidget: hasWidget,
+ pasteWidget: pasteWidget,
+ addWidgetToDashboard: addWidgetToDashboard
+ }
+
+ return service;
+
+ /**
+ aliasesInfo {
+ datasourceAliases: {
+ datasourceIndex: {
+ aliasName: "...",
+ deviceId: "..."
+ }
+ }
+ targetDeviceAliases: {
+ targetDeviceAliasIndex: {
+ aliasName: "...",
+ deviceId: "..."
+ }
+ }
+ ....
+ }
+ **/
+
+ function copyWidget(widget, aliasesInfo, originalColumns) {
+ var widgetItem = {
+ widget: widget,
+ aliasesInfo: aliasesInfo,
+ originalColumns: originalColumns
+ }
+ bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
+ }
+
+ function hasWidget() {
+ return bufferStore.get(WIDGET_ITEM);
+ }
+
+ function pasteWidget(targetDasgboard, position) {
+ var widgetItemJson = bufferStore.get(WIDGET_ITEM);
+ if (widgetItemJson) {
+ var widgetItem = angular.fromJson(widgetItemJson);
+ var widget = widgetItem.widget;
+ var aliasesInfo = widgetItem.aliasesInfo;
+ var originalColumns = widgetItem.originalColumns;
+ var targetRow = -1;
+ var targetColumn = -1;
+ if (position) {
+ targetRow = position.row;
+ targetColumn = position.column;
+ }
+ addWidgetToDashboard(targetDasgboard, widget, aliasesInfo, originalColumns, targetRow, targetColumn);
+ }
+ }
+
+ function addWidgetToDashboard(dashboard, widget, aliasesInfo, originalColumns, row, column) {
+ var theDashboard;
+ if (dashboard) {
+ theDashboard = dashboard;
+ } else {
+ theDashboard = {};
+ }
+ if (!theDashboard.configuration) {
+ theDashboard.configuration = {};
+ }
+ if (!theDashboard.configuration.deviceAliases) {
+ theDashboard.configuration.deviceAliases = {};
+ }
+ updateAliases(theDashboard, widget, aliasesInfo);
+
+ if (!theDashboard.configuration.widgets) {
+ theDashboard.configuration.widgets = [];
+ }
+ var targetColumns = 24;
+ if (theDashboard.configuration.gridSettings &&
+ theDashboard.configuration.gridSettings.columns) {
+ targetColumns = theDashboard.configuration.gridSettings.columns;
+ }
+ if (targetColumns != originalColumns) {
+ var ratio = targetColumns / originalColumns;
+ widget.sizeX *= ratio;
+ widget.sizeY *= ratio;
+ }
+ if (row > -1 && column > - 1) {
+ widget.row = row;
+ widget.col = column;
+ } else {
+ row = 0;
+ for (var w in theDashboard.configuration.widgets) {
+ var existingWidget = theDashboard.configuration.widgets[w];
+ var wRow = existingWidget.row ? existingWidget.row : 0;
+ var wSizeY = existingWidget.sizeY ? existingWidget.sizeY : 1;
+ var bottom = wRow + wSizeY;
+ row = Math.max(row, bottom);
+ }
+ widget.row = row;
+ widget.col = 0;
+ }
+ theDashboard.configuration.widgets.push(widget);
+ return theDashboard;
+ }
+
+ function updateAliases(dashboard, widget, aliasesInfo) {
+ var deviceAliases = dashboard.configuration.deviceAliases;
+ var aliasInfo;
+ var newAliasId;
+ for (var datasourceIndex in aliasesInfo.datasourceAliases) {
+ aliasInfo = aliasesInfo.datasourceAliases[datasourceIndex];
+ newAliasId = getDeviceAliasId(deviceAliases, aliasInfo);
+ widget.config.datasources[datasourceIndex].deviceAliasId = newAliasId;
+ }
+ for (var targetDeviceAliasIndex in aliasesInfo.targetDeviceAliases) {
+ aliasInfo = aliasesInfo.targetDeviceAliases[targetDeviceAliasIndex];
+ newAliasId = getDeviceAliasId(deviceAliases, aliasInfo);
+ widget.config.targetDeviceAliasIds[targetDeviceAliasIndex] = newAliasId;
+ }
+ }
+
+ function getDeviceAliasId(deviceAliases, aliasInfo) {
+ var newAliasId;
+ for (var aliasId in deviceAliases) {
+ if (deviceAliases[aliasId].deviceId === aliasInfo.deviceId) {
+ newAliasId = aliasId;
+ break;
+ }
+ }
+ if (!newAliasId) {
+ var newAliasName = createDeviceAliasName(deviceAliases, aliasInfo.aliasName);
+ newAliasId = 0;
+ for (aliasId in deviceAliases) {
+ newAliasId = Math.max(newAliasId, aliasId);
+ }
+ newAliasId++;
+ deviceAliases[newAliasId] = {alias: newAliasName, deviceId: aliasInfo.deviceId};
+ }
+ return newAliasId;
+ }
+
+ function createDeviceAliasName(deviceAliases, alias) {
+ var c = 0;
+ var newAlias = angular.copy(alias);
+ var unique = false;
+ while (!unique) {
+ unique = true;
+ for (var devAliasId in deviceAliases) {
+ var devAlias = deviceAliases[devAliasId];
+ if (newAlias === devAlias.alias) {
+ c++;
+ newAlias = alias + c;
+ unique = false;
+ }
+ }
+ }
+ return newAlias;
+ }
+
+
+}
\ No newline at end of file
ui/src/locale/en_US.json 7(+5 -2)
diff --git a/ui/src/locale/en_US.json b/ui/src/locale/en_US.json
index 9da68d6..5de8938 100644
--- a/ui/src/locale/en_US.json
+++ b/ui/src/locale/en_US.json
@@ -38,7 +38,9 @@
"create": "Create",
"drag": "Drag",
"refresh": "Refresh",
- "undo": "Undo"
+ "undo": "Undo",
+ "copy": "Copy",
+ "paste": "Paste"
},
"admin": {
"general": "General",
@@ -211,7 +213,8 @@
"vertical-margin": "Vertical margin",
"vertical-margin-required": "Vertical margin value is required.",
"min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.",
- "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value."
+ "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.",
+ "display-title": "Display dashboard title"
},
"datakey": {
"settings": "Settings",
ui/src/scss/main.scss 3(+3 -0)
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index e8f8283..b9b869e 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -169,6 +169,9 @@ md-menu-item {
md-menu-item {
.md-button {
display: block;
+ .tb-alt-text {
+ float: right;
+ }
}
}