thingsboard-memoizeit
Changes
application/pom.xml 2(+1 -1)
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java 26(+17 -9)
application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java 2(+1 -1)
application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java 14(+8 -6)
application/src/test/java/org/thingsboard/server/mqtt/rpc/MqttServerSideRpcIntegrationTest.java 108(+108 -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/SessionCloseNotification.java 38(+38 -0)
common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java 20(+19 -1)
common/pom.xml 2(+1 -1)
common/transport/pom.xml 2(+1 -1)
dao/pom.xml 2(+1 -1)
dao/src/main/resources/demo-data.cql 16(+4 -12)
dao/src/main/resources/system-data.cql 16(+13 -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-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceCredentialsUpdateNotificationMsg.java 36(+36 -0)
extensions-core/pom.xml 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/session/CoapSessionCtx.java 5(+4 -1)
transport/http/pom.xml 2(+1 -1)
transport/mqtt/pom.xml 2(+1 -1)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java 3(+1 -2)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java 8(+6 -2)
transport/pom.xml 2(+1 -1)
ui/.eslintrc 3(+3 -0)
ui/package.json 2(+2 -0)
ui/pom.xml 2(+1 -1)
ui/src/app/app.js 2(+2 -0)
ui/src/app/app.run.js 3(+3 -0)
ui/src/app/components/dashboard.directive.js 40(+38 -2)
ui/src/app/components/dashboard.tpl.html 112(+57 -55)
ui/src/app/components/react/json-form-image.jsx 105(+105 -0)
ui/src/app/dashboard/dashboard.controller.js 29(+29 -0)
ui/src/app/dashboard/dashboard.tpl.html 14(+13 -1)
ui/src/app/dashboard/dashboard-settings.scss 91(+91 -0)
ui/src/app/dashboard/dashboard-settings.tpl.html 115(+115 -0)
ui/src/app/dashboard/index.js 2(+2 -0)
ui/src/app/device/device.directive.js 13(+11 -2)
ui/src/locale/en_US.json 23(+22 -1)
Details
application/pom.xml 2(+1 -1)
diff --git a/application/pom.xml b/application/pom.xml
index 0c53121..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-SNAPSHOT</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
diff --git a/application/src/main/conf/logback.xml b/application/src/main/conf/logback.xml
index 4187356..5d53400 100644
--- a/application/src/main/conf/logback.xml
+++ b/application/src/main/conf/logback.xml
@@ -24,7 +24,7 @@
<file>${pkg.logFolder}/${pkg.name}.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
- <fileNamePattern>${pkg.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+ <fileNamePattern>${pkg.logFolder}/${pkg.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
index 8b669e9..bf18df6 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
@@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
import org.thingsboard.server.extensions.api.plugins.msg.*;
@@ -58,6 +59,8 @@ public class DeviceActor extends ContextAwareActor {
processor.processAttributesUpdate(context(), (DeviceAttributesEventNotificationMsg) msg);
} else if (msg instanceof ToDeviceRpcRequestPluginMsg) {
processor.processRpcRequest(context(), (ToDeviceRpcRequestPluginMsg) msg);
+ } else if (msg instanceof DeviceCredentialsUpdateNotificationMsg){
+ processor.processCredentialsUpdate(context(), (DeviceCredentialsUpdateNotificationMsg) msg);
}
} else if (msg instanceof TimeoutMsg) {
processor.processTimeout(context(), (TimeoutMsg) msg);
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
index 3949691..3aef0c8 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
@@ -32,13 +32,7 @@ import org.thingsboard.server.common.data.kv.AttributeKey;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.core.AttributesUpdateNotification;
-import org.thingsboard.server.common.msg.core.BasicCommandAckResponse;
-import org.thingsboard.server.common.msg.core.BasicToDeviceSessionActorMsg;
-import org.thingsboard.server.common.msg.core.SessionCloseMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceRpcRequestMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceRpcResponseMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.core.*;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.common.msg.kv.BasicAttributeKVMsg;
import org.thingsboard.server.common.msg.session.FromDeviceMsg;
@@ -47,6 +41,7 @@ import org.thingsboard.server.common.msg.session.SessionType;
import org.thingsboard.server.common.msg.session.ToDeviceMsg;
import org.thingsboard.server.extensions.api.device.DeviceAttributes;
import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
import org.thingsboard.server.extensions.api.plugins.msg.RpcError;
import org.thingsboard.server.extensions.api.plugins.msg.TimeoutIntMsg;
@@ -74,6 +69,7 @@ import java.util.stream.Collectors;
public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
private final DeviceId deviceId;
+ private final Map<SessionId, SessionInfo> sessions;
private final Map<SessionId, SessionInfo> attributeSubscriptions;
private final Map<SessionId, SessionInfo> rpcSubscriptions;
@@ -85,6 +81,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
public DeviceActorMessageProcessor(ActorSystemContext systemContext, LoggingAdapter logger, DeviceId deviceId) {
super(systemContext, logger);
this.deviceId = deviceId;
+ this.sessions = new HashMap<>();
this.attributeSubscriptions = new HashMap<>();
this.rpcSubscriptions = new HashMap<>();
this.rpcPendingMap = new HashMap<>();
@@ -281,7 +278,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
if (!msg.isAdded()) {
logger.debug("[{}] Clearing attributes/rpc subscription for server [{}]", deviceId, msg.getServerAddress());
Predicate<Map.Entry<SessionId, SessionInfo>> filter = e -> e.getValue().getServer()
- .map(serverAddress -> serverAddress.equals(msg.getServerAddress())).orElse(false);
+ .map(serverAddress -> serverAddress.equals(msg.getServerAddress())).orElse(false);
attributeSubscriptions.entrySet().removeIf(filter);
rpcSubscriptions.entrySet().removeIf(filter);
}
@@ -342,8 +339,12 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
private void processSessionStateMsgs(ToDeviceActorMsg msg) {
SessionId sessionId = msg.getSessionId();
FromDeviceMsg inMsg = msg.getPayload();
- if (inMsg instanceof SessionCloseMsg) {
+ if (inMsg instanceof SessionOpenMsg) {
+ logger.debug("[{}] Processing new session [{}]", deviceId, sessionId);
+ sessions.put(sessionId, new SessionInfo(SessionType.ASYNC, msg.getServerAddress()));
+ } else if (inMsg instanceof SessionCloseMsg) {
logger.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
+ sessions.remove(sessionId);
attributeSubscriptions.remove(sessionId);
rpcSubscriptions.remove(sessionId);
}
@@ -363,4 +364,11 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
return systemContext.getAttributesService().findAll(this.deviceId, attributeType);
}
+ public void processCredentialsUpdate(ActorContext context, DeviceCredentialsUpdateNotificationMsg msg) {
+ sessions.forEach((k, v) -> {
+ sendMsgToSessionActor(new BasicToDeviceSessionActorMsg(new SessionCloseNotification(), k), v.getServer());
+ });
+ attributeSubscriptions.clear();
+ rpcSubscriptions.clear();
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
index 72ae4bb..c1d2678 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
@@ -181,7 +181,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
logger.info("[{}] Plugin requires restart due to clazz change from {} to {}.",
entityId, oldPluginMd.getClazz(), pluginMd.getClazz());
requiresRestart = true;
- } else if (oldPluginMd.getConfiguration().equals(pluginMd.getConfiguration())) {
+ } else if (!oldPluginMd.getConfiguration().equals(pluginMd.getConfiguration())) {
logger.info("[{}] Plugin requires restart due to configuration change from {} to {}.",
entityId, oldPluginMd.getConfiguration(), pluginMd.getConfiguration());
requiresRestart = true;
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
index 82011c0..1f64a92 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
@@ -234,18 +234,18 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
logger.info("[{}] Rule configuration was updated from {} to {}.", entityId, oldRuleMd, ruleMd);
try {
fetchPluginInfo();
- if (!Objects.equals(oldRuleMd.getFilters(), ruleMd.getFilters())) {
+ if (filters == null || !Objects.equals(oldRuleMd.getFilters(), ruleMd.getFilters())) {
logger.info("[{}] Rule filters require restart due to json change from {} to {}.",
entityId, mapper.writeValueAsString(oldRuleMd.getFilters()), mapper.writeValueAsString(ruleMd.getFilters()));
stopFilters();
initFilters();
}
- if (!Objects.equals(oldRuleMd.getProcessor(), ruleMd.getProcessor())) {
+ if (processor == null || !Objects.equals(oldRuleMd.getProcessor(), ruleMd.getProcessor())) {
logger.info("[{}] Rule processor require restart due to configuration change.", entityId);
stopProcessor();
initProcessor();
}
- if (!Objects.equals(oldRuleMd.getAction(), ruleMd.getAction())) {
+ if (action == null || !Objects.equals(oldRuleMd.getAction(), ruleMd.getAction())) {
logger.info("[{}] Rule action require restart due to configuration change.", entityId);
stopAction();
initAction();
@@ -272,13 +272,15 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
if (action != null) {
if (filters != null) {
filters.forEach(f -> f.resume());
+ } else {
+ initFilters();
}
if (processor != null) {
processor.resume();
+ } else {
+ initProcessor();
}
- if (action != null) {
- action.resume();
- }
+ action.resume();
logger.info("[{}] Rule resumed.", entityId);
} else {
start();
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
index 1c64f54..3db7210 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.actors.service;
+import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.PluginId;
import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -28,4 +29,6 @@ public interface ActorService extends SessionMsgProcessor, WebSocketMsgProcessor
void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state);
void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state);
+
+ void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId);
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
index db6526d..bbf1300 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
@@ -32,16 +32,19 @@ import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
import org.thingsboard.server.actors.session.SessionManagerActor;
import org.thingsboard.server.actors.stats.StatsActor;
+import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.PluginId;
import org.thingsboard.server.common.data.id.RuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
@@ -56,6 +59,7 @@ import scala.concurrent.duration.Duration;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
+import java.util.Optional;
@Service
@Slf4j
@@ -221,6 +225,17 @@ public class DefaultActorService implements ActorService {
broadcast(ComponentLifecycleMsg.forRule(tenantId, ruleId, state));
}
+ @Override
+ public void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId) {
+ DeviceCredentialsUpdateNotificationMsg msg = new DeviceCredentialsUpdateNotificationMsg(tenantId, deviceId);
+ Optional<ServerAddress> address = actorContext.getRoutingService().resolve(deviceId);
+ if (address.isPresent()) {
+ rpcService.tell(address.get(), msg);
+ } else {
+ onMsg(msg);
+ }
+ }
+
public void broadcast(ToAllNodesMsg msg) {
rpcService.broadcast(msg);
appActor.tell(msg, ActorRef.noSender());
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
index 90b0cb3..916e678 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
@@ -20,15 +20,14 @@ import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
import org.thingsboard.server.common.data.id.SessionId;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.core.AttributesSubscribeMsg;
-import org.thingsboard.server.common.msg.core.ResponseMsg;
-import org.thingsboard.server.common.msg.core.RpcSubscribeMsg;
+import org.thingsboard.server.common.msg.core.*;
import org.thingsboard.server.common.msg.core.SessionCloseMsg;
import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
import org.thingsboard.server.common.msg.session.*;
import akka.actor.ActorContext;
import akka.event.LoggingAdapter;
+import org.thingsboard.server.common.msg.session.ctrl.*;
import org.thingsboard.server.common.msg.session.ex.SessionException;
import java.util.HashMap;
@@ -37,7 +36,8 @@ import java.util.Optional;
class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
- Map<Integer, ToDeviceActorMsg> pendingMap = new HashMap<>();
+ private boolean firstMsg = true;
+ private Map<Integer, ToDeviceActorMsg> pendingMap = new HashMap<>();
private Optional<ServerAddress> currentTargetServer;
private boolean subscribedToAttributeUpdates;
private boolean subscribedToRpcCommands;
@@ -49,6 +49,10 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
@Override
protected void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg) {
updateSessionCtx(msg, SessionType.ASYNC);
+ if (firstMsg) {
+ toDeviceMsg(new SessionOpenMsg()).ifPresent(m -> forwardToAppActor(ctx, m));
+ firstMsg = false;
+ }
ToDeviceActorMsg pendingMsg = toDeviceMsg(msg);
FromDeviceMsg fromDeviceMsg = pendingMsg.getPayload();
switch (fromDeviceMsg.getMsgType()) {
@@ -80,17 +84,21 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
@Override
public void processToDeviceMsg(ActorContext context, ToDeviceMsg msg) {
try {
- switch (msg.getMsgType()) {
- case STATUS_CODE_RESPONSE:
- case GET_ATTRIBUTES_RESPONSE:
- ResponseMsg responseMsg = (ResponseMsg) msg;
- if (responseMsg.getRequestId() >= 0) {
- logger.debug("[{}] Pending request processed: {}", responseMsg.getRequestId(), responseMsg);
- pendingMap.remove(responseMsg.getRequestId());
- }
- break;
+ if (msg.getMsgType() != MsgType.SESSION_CLOSE) {
+ switch (msg.getMsgType()) {
+ case STATUS_CODE_RESPONSE:
+ case GET_ATTRIBUTES_RESPONSE:
+ ResponseMsg responseMsg = (ResponseMsg) msg;
+ if (responseMsg.getRequestId() >= 0) {
+ logger.debug("[{}] Pending request processed: {}", responseMsg.getRequestId(), responseMsg);
+ pendingMap.remove(responseMsg.getRequestId());
+ }
+ break;
+ }
+ sessionCtx.onMsg(new BasicSessionActorToAdaptorMsg(this.sessionCtx, msg));
+ } else {
+ sessionCtx.onMsg(org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg.onCredentialsRevoked(sessionCtx.getSessionId()));
}
- sessionCtx.onMsg(new BasicSessionActorToAdaptorMsg(this.sessionCtx, msg));
} catch (SessionException e) {
logger.warning("Failed to push session response msg", e);
}
@@ -102,7 +110,7 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
}
protected void cleanupSession(ActorContext ctx) {
- toDeviceMsg(new SessionCloseMsg()).ifPresent(msg -> forwardToAppActor(ctx, msg));
+ toDeviceMsg(new SessionCloseMsg()).ifPresent(m -> forwardToAppActor(ctx, m));
}
@Override
@@ -110,6 +118,7 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
if (pendingMap.size() > 0 || subscribedToAttributeUpdates || subscribedToRpcCommands) {
Optional<ServerAddress> newTargetServer = systemContext.getRoutingService().resolve(getDeviceId());
if (!newTargetServer.equals(currentTargetServer)) {
+ firstMsg = true;
currentTargetServer = newTargetServer;
pendingMap.values().forEach(v -> {
forwardToAppActor(context, v, currentTargetServer);
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
index afb35ac..9fb13d3 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
@@ -52,7 +52,7 @@ class SyncMsgProcessor extends AbstractSessionActorMsgProcessor {
public void processTimeoutMsg(ActorContext context, SessionTimeoutMsg msg) {
if (pendingResponse) {
try {
- sessionCtx.onMsg(new SessionCloseMsg(sessionId, true));
+ sessionCtx.onMsg(SessionCloseMsg.onTimeout(sessionId));
} catch (SessionException e) {
logger.warning("Failed to push session close msg", e);
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index 1c0a7be..43416ce 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
@RestController
@RequestMapping("/api")
@@ -48,7 +49,7 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/device", method = RequestMethod.POST)
- @ResponseBody
+ @ResponseBody
public Device saveDevice(@RequestBody Device device) throws ThingsboardException {
try {
device.setTenantId(getCurrentUser().getTenantId());
@@ -74,7 +75,7 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/{customerId}/device/{deviceId}", method = RequestMethod.POST)
- @ResponseBody
+ @ResponseBody
public Device assignDeviceToCustomer(@PathVariable("customerId") String strCustomerId,
@PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
checkParameter("customerId", strCustomerId);
@@ -85,7 +86,7 @@ public class DeviceController extends BaseController {
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
checkDeviceId(deviceId);
-
+
return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, customerId));
} catch (Exception e) {
throw handleException(e);
@@ -94,7 +95,7 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/device/{deviceId}", method = RequestMethod.DELETE)
- @ResponseBody
+ @ResponseBody
public Device unassignDeviceFromCustomer(@PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
checkParameter("deviceId", strDeviceId);
try {
@@ -125,19 +126,21 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/device/credentials", method = RequestMethod.POST)
- @ResponseBody
+ @ResponseBody
public DeviceCredentials saveDeviceCredentials(@RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException {
checkNotNull(deviceCredentials);
try {
checkDeviceId(deviceCredentials.getDeviceId());
- return checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
+ DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
+ actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId());
+ return result;
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
- @RequestMapping(value = "/tenant/devices", params = { "limit" }, method = RequestMethod.GET)
+ @RequestMapping(value = "/tenant/devices", params = {"limit"}, method = RequestMethod.GET)
@ResponseBody
public TextPageData<Device> getTenantDevices(
@RequestParam int limit,
@@ -154,7 +157,7 @@ public class DeviceController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
- @RequestMapping(value = "/customer/{customerId}/devices", params = { "limit" }, method = RequestMethod.GET)
+ @RequestMapping(value = "/customer/{customerId}/devices", params = {"limit"}, method = RequestMethod.GET)
@ResponseBody
public TextPageData<Device> getCustomerDevices(
@PathVariable("customerId") String strCustomerId,
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index aa7fcc1..2b42bdd 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -138,8 +138,8 @@ cassandra:
default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
# Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
- # Specify max partitions per request
- max_limit_per_request: "${TS_KV_MAX_LIMIT_PER_REQUEST:1000}"
+ # Specify max data points per request
+ max_limit_per_request: "${TS_KV_MAX_LIMIT_PER_REQUEST:86400}"
# Actor system parameters
actors:
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..b1be92a
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/MqttServerSideRpcIntegrationTest.java
@@ -0,0 +1,108 @@
+/**
+ * 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.Test;
+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 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 Device savedDevice;
+
+ private String accessToken;
+ private RestClient restClient;
+
+ @Before
+ public void beforeTest() throws Exception {
+ restClient = new RestClient(BASE_URL);
+ restClient.login(USERNAME, PASSWORD);
+
+ Device device = new Device();
+ device.setName("Test Server-Side RPC Device");
+ savedDevice = restClient.getRestTemplate().postForEntity(BASE_URL + "/api/device", device, Device.class).getBody();
+ DeviceCredentials deviceCredentials =
+ restClient.getRestTemplate().getForEntity(BASE_URL + "/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class).getBody();
+ assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+ accessToken = deviceCredentials.getCredentialsId();
+ assertNotNull(accessToken);
+ }
+
+ @Test
+ public void testServerMqttTwoWayRpc() throws Exception {
+ 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 = restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class).getBody();
+ log.info("Result: " + result);
+ Assert.assertEquals("{\"value1\":\"A\",\"value2\":\"B\"}", result);
+ }
+
+ 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 343ada2..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-SNAPSHOT</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
common/message/pom.xml 2(+1 -1)
diff --git a/common/message/pom.xml b/common/message/pom.xml
index c62732e..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-SNAPSHOT</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/SessionCloseNotification.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java
new file mode 100644
index 0000000..3e96e40
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java
@@ -0,0 +1,38 @@
+/**
+ * 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.common.msg.core;
+
+import lombok.ToString;
+import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+@ToString
+public class SessionCloseNotification implements ToDeviceMsg {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public boolean isSuccess() {
+ return true;
+ }
+
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.SESSION_CLOSE;
+ }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java
new file mode 100644
index 0000000..d18dc9f
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java
@@ -0,0 +1,29 @@
+/**
+ * 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.common.msg.core;
+
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class SessionOpenMsg implements FromDeviceMsg {
+ @Override
+ public MsgType getMsgType() {
+ return MsgType.SESSION_OPEN;
+ }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
index d188527..03b611e 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
@@ -21,11 +21,25 @@ import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
public class SessionCloseMsg implements SessionCtrlMsg {
private final SessionId sessionId;
+ private final boolean revoked;
private final boolean timeout;
- public SessionCloseMsg(SessionId sessionId, boolean timeout) {
+ public static SessionCloseMsg onError(SessionId sessionId) {
+ return new SessionCloseMsg(sessionId, false, false);
+ }
+
+ public static SessionCloseMsg onTimeout(SessionId sessionId) {
+ return new SessionCloseMsg(sessionId, false, true);
+ }
+
+ public static SessionCloseMsg onCredentialsRevoked(SessionId sessionId) {
+ return new SessionCloseMsg(sessionId, true, false);
+ }
+
+ private SessionCloseMsg(SessionId sessionId, boolean unauthorized, boolean timeout) {
super();
this.sessionId = sessionId;
+ this.revoked = unauthorized;
this.timeout = timeout;
}
@@ -34,6 +48,10 @@ public class SessionCloseMsg implements SessionCtrlMsg {
return sessionId;
}
+ public boolean isCredentialsRevoked() {
+ return revoked;
+ }
+
public boolean isTimeout() {
return timeout;
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java
index 1b91425..549a143 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java
@@ -28,7 +28,7 @@ public enum MsgType {
RULE_ENGINE_ERROR,
- SESSION_CLOSE;
+ SESSION_OPEN, SESSION_CLOSE;
private final boolean requiresRulesProcessing;
common/pom.xml 2(+1 -1)
diff --git a/common/pom.xml b/common/pom.xml
index 58ea1cb..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-SNAPSHOT</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 75f8e00..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-SNAPSHOT</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 d1e84df..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-SNAPSHOT</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
dao/src/main/resources/demo-data.cql 16(+4 -12)
diff --git a/dao/src/main/resources/demo-data.cql b/dao/src/main/resources/demo-data.cql
index 84a1dff..cc62b05 100644
--- a/dao/src/main/resources/demo-data.cql
+++ b/dao/src/main/resources/demo-data.cql
@@ -287,10 +287,10 @@ VALUES (
'org.thingsboard.server.extensions.core.plugin.mail.MailPlugin',
true,
'{
- "host": "smtp.gmail.com",
- "port": 587,
- "username": "username@gmail.com",
- "password": "password",
+ "host": "smtp.sendgrid.net",
+ "port": 2525,
+ "username": "apikey",
+ "password": "your_api_key",
"otherProperties": [
{
"key":"mail.smtp.auth",
@@ -303,14 +303,6 @@ VALUES (
{
"key":"mail.smtp.starttls.enable",
"value":"true"
- },
- {
- "key":"mail.smtp.host",
- "value":"smtp.gmail.com"
- },
- {
- "key":"mail.smtp.port",
- "value":"587"
}
]
}'
dao/src/main/resources/system-data.cql 16(+13 -3)
diff --git a/dao/src/main/resources/system-data.cql b/dao/src/main/resources/system-data.cql
index 1358e58..f91243a 100644
--- a/dao/src/main/resources/system-data.cql
+++ b/dao/src/main/resources/system-data.cql
@@ -73,6 +73,11 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'simple_card',
'Simple card' );
INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'label_widget',
+'{"type":"latest","sizeX":4.5,"sizeY":5,"resources":[],"templateHtml":"","templateCss":"#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}","controllerScript":"var bImageHeight;\nvar bImageWidth;\nvar backgroundRect;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\nvar labels;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n\n var container = $(containerElement);\n var imageUrl = settings.backgroundImageUrl ? settings.backgroundImageUrl :\n ''data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg=='';\n\n container.css(''background'', ''url(\"''+imageUrl+''\") no-repeat'');\n container.css(''backgroundSize'', ''contain'');\n container.css(''backgroundPosition'', ''50% 50%'');\n \n function processLabelPattern(pattern, data) {\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 = n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n var configuredLabels = settings.labels;\n if (!configuredLabels) {\n configuredLabels = [];\n }\n \n labels = [];\n\n for (var l in configuredLabels) {\n var labelConfig = configuredLabels[l];\n var localConfig = {};\n localConfig.font = {};\n \n localConfig.pattern = labelConfig.pattern ? labelConfig.pattern : ''${#0}'';\n localConfig.x = labelConfig.x ? labelConfig.x : 0;\n localConfig.y = labelConfig.y ? labelConfig.y : 0;\n localConfig.backgroundColor = labelConfig.backgroundColor ? labelConfig.backgroundColor : ''rgba(0,0,0,0)'';\n \n var settingsFont = labelConfig.font;\n if (!settingsFont) {\n settingsFont = {};\n }\n \n localConfig.font.family = settingsFont.family || ''RobotoDraft'';\n localConfig.font.size = settingsFont.size ? settingsFont.size : 6;\n localConfig.font.style = settingsFont.style ? settingsFont.style : ''normal'';\n localConfig.font.weight = settingsFont.weight ? settingsFont.weight : ''500'';\n localConfig.font.color = settingsFont.color ? settingsFont.color : ''#fff'';\n \n localConfig.replaceInfo = processLabelPattern(localConfig.pattern, data);\n \n var label = {};\n var labelElement = $(''<div/>'');\n labelElement.css(''position'', ''absolute'');\n labelElement.css(''top'', ''0'');\n labelElement.css(''left'', ''0'');\n labelElement.css(''backgroundColor'', localConfig.backgroundColor);\n labelElement.css(''color'', localConfig.font.color);\n labelElement.css(''fontFamily'', localConfig.font.family);\n labelElement.css(''fontStyle'', localConfig.font.style);\n labelElement.css(''fontWeight'', localConfig.font.weight);\n \n labelElement.html(localConfig.pattern);\n container.append(labelElement);\n label.element = labelElement;\n label.config = localConfig;\n labels.push(label);\n }\n\n var bgImg = $(''<img />'');\n bgImg.hide();\n bgImg.bind(''load'', function()\n {\n bImageHeight = $(this).height();\n bImageWidth = $(this).width();\n });\n container.append(bgImg);\n bgImg.attr(''src'', imageUrl);\n \n units = settings.units || \"\";\n valueDec = (typeof settings.valueDec !== ''undefined'' && settings.valueDec !== null)\n ? settings.valueDec : 2;\n \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data, 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 if (bImageHeight && bImageWidth) {\n if (sizeChanged || !backgroundRect) {\n backgroundRect = {};\n var imageRatio = bImageWidth / bImageHeight;\n var componentRatio = width / height;\n if (componentRatio >= imageRatio) {\n backgroundRect.top = 0;\n backgroundRect.bottom = 1.0;\n backgroundRect.xRatio = imageRatio / componentRatio;\n backgroundRect.yRatio = 1;\n var offset = (1 - backgroundRect.xRatio) / 2;\n backgroundRect.left = offset;\n backgroundRect.right = 1 - offset;\n } else {\n backgroundRect.left = 0;\n backgroundRect.right = 1.0;\n backgroundRect.xRatio = 1;\n backgroundRect.yRatio = componentRatio / imageRatio;\n var offset = (1 - backgroundRect.yRatio) / 2;\n backgroundRect.top = offset;\n backgroundRect.bottom = 1 - offset;\n }\n for (var l in labels) {\n var label = labels[l];\n var labelLeft = backgroundRect.left*100 + (label.config.x*backgroundRect.xRatio);\n var labelTop = backgroundRect.top*100 + (label.config.y*backgroundRect.yRatio);\n var fontSize = height * backgroundRect.yRatio * label.config.font.size / 100;\n label.element.css(''top'', labelTop + ''%'');\n label.element.css(''left'', labelLeft + ''%'');\n label.element.css(''fontSize'', fontSize + ''px'');\n }\n \n }\n }\n \n for (var l in labels) {\n var label = labels[l];\n var text = label.config.pattern;\n var replaceInfo = label.config.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 label.element.html(text);\n }\n \n\n};\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"required\": [\"backgroundImageUrl\"],\n \"properties\": {\n \"backgroundImageUrl\": {\n \"title\": \"Background image\",\n \"type\": \"string\",\n \"default\": \"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\"\n },\n \"labels\": {\n \"title\": \"Labels\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Label\",\n \"type\": \"object\",\n \"required\": [\"pattern\"],\n \"properties\": {\n \"pattern\": {\n \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units'' )\",\n \"type\": \"string\",\n \"default\": \"${#0}\"\n },\n \"x\": {\n \"title\": \"X (Percentage relative to background)\",\n \"type\": \"number\",\n \"default\": 50\n },\n \"y\": {\n \"title\": \"Y (Percentage relative to background)\",\n \"type\": \"number\",\n \"default\": 50\n },\n \"backgroundColor\": {\n \"title\": \"Backround color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,0)\"\n },\n \"font\": {\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Relative font size (percents)\",\n \"type\": \"number\",\n \"default\": 6\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\": \"#fff\"\n }\n }\n }\n }\n }\n }\n }\n },\n \"form\": [\n {\n \"key\": \"backgroundImageUrl\",\n \"type\": \"image\"\n },\n {\n \"key\": \"labels\",\n \"items\": [\n \"labels[].pattern\",\n \"labels[].x\",\n \"labels[].y\",\n {\n \"key\": \"labels[].backgroundColor\",\n \"type\": \"color\"\n },\n \"labels[].font.family\",\n \"labels[].font.size\",\n {\n \"key\": \"labels[].font.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 {\n \"key\": \"labels[].font.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\": \"labels[].font.color\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"var\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"backgroundImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"labels\":[{\"pattern\":\"Value: ${#0:2} units.\",\"x\":20,\"y\":47,\"font\":{\"color\":\"#515151\",\"family\":\"RobotoDraft\",\"size\":6,\"style\":\"normal\",\"weight\":\"500\"}}]},\"title\":\"Label widget\"}"}',
+'Label widget' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'speed_gauge_canvas_gauges',
'{"type":"latest","sizeX":7,"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\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 220) {\\n\\tvalue = 220;\\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\":180,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":false,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":80,\"to\":120,\"color\":\"#fdd835\"},{\"color\":\"#e57373\",\"from\":120,\"to\":180}],\"showUnitTitle\":false,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"minValue\":0,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"MPH\",\"majorTicksCount\":9,\"numbersFont\":{\"family\":\"RobotoDraft\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"RobotoDraft\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"size\":32,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\",\"family\":\"Segment7Standard\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Speed gauge - Canvas Gauges\"}"}',
'Speed gauge - Canvas Gauges' );
@@ -123,11 +128,16 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'radial_gauge_canvas_gau
'Radial gauge - Canvas Gauges' );
INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
-VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'google_maps',
-'{"type":"latest","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;\nvar positions;\nvar markers = [];\nvar markersSettings = [];\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar markerCluster;\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 var configuredMarkersSettings = settings.markersSettings;\n if (!configuredMarkersSettings) {\n configuredMarkersSettings = [];\n }\n \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 };\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 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 }\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 createMarker(location, settings) {\n var pinColor = settings.color;\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 return marker; \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 = new google.maps.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.getPosition();\n if (!prevPosition.equals(location)) {\n position.marker.setPosition(location);\n return true;\n }\n }\n }\n }\n return false;\n }\n \n function loadPositions(data) {\n var bounds = new google.maps.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.getPosition());\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n function updatePositions(data) {\n var positionsChanged = false;\n var bounds = new google.maps.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.getPosition());\n }\n }\n if (!dontFitMapBounds && positionsChanged) {\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 (!positions) {\n loadPositions(data);\n } else {\n updatePositions(data);\n }\n }\n if (sizeChanged) {\n google.maps.event.trigger(map, \"resize\");\n var bounds = new google.maps.LatLngBounds();\n for (var m in markers) {\n bounds.extend(markers[m].getPosition());\n }\n fitMapBounds(bounds);\n }\n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Google 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 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 \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n }\n },\n \"required\": [\n \"gmApiKey\"\n ]\n },\n \"form\": [\n \"gmApiKey\",\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"markersSettings\",\n \"items\": [\n \"markersSettings[].latKeyName\",\n \"markersSettings[].lngKeyName\",\n \"markersSettings[].showLabel\",\n \"markersSettings[].label\",\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\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"markersSettings\":[{\"label\":\"First point\",\"color\":\"#1e88e5\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true}],\"fitMapBounds\":true},\"title\":\"Google Maps\"}"}',
+VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'google_maps',
+'{"type":"latest","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;\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 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 \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 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 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 = new google.maps.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.getPosition();\n if (!prevPosition.equals(location)) {\n position.marker.setPosition(location);\n return true;\n }\n }\n }\n }\n return false;\n }\n \n function loadPositions(data) {\n var bounds = new google.maps.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.getPosition());\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n function updatePositions(data) {\n var positionsChanged = false;\n var bounds = new google.maps.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.getPosition());\n }\n }\n if (!dontFitMapBounds && positionsChanged) {\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 (!positions) {\n loadPositions(data);\n } else {\n updatePositions(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 m in markers) {\n bounds.extend(markers[m].getPosition());\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\": \"Google 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 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 \"gmApiKey\"\n ]\n },\n \"form\": [\n \"gmApiKey\",\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\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"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\":\"Google 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\"}"}',
+'Route Map' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'basic_timeseries',
'{"type":"timeseries","sizeX":8,"sizeY":6,"resources":[{"url":"https://rawgithub.com/HumbleSoftware/Flotr2/master/flotr2.min.js"}],"templateHtml":"","templateCss":"","controllerScript":"var graph, options;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n\n var colors = [];\n for (var i in data) {\n data[i].label = data[i].dataKey.label;\n colors.push(data[i].dataKey.color);\n var keySettings = data[i].dataKey.settings;\n\n data[i].lines = {\n fill: keySettings.fillLines || false,\n show: keySettings.showLines || true\n };\n\n data[i].points = {\n show: keySettings.showPoints || false\n };\n }\n options = {\n colors: colors,\n title: null,\n subtitle: null,\n shadowSize: settings.shadowSize || 4,\n fontColor: settings.fontColor || \"#545454\",\n fontSize: settings.fontSize || 7.5,\n xaxis: {\n mode: ''time'',\n timeMode: ''local''\n },\n yaxis: {\n },\n HtmlText: false,\n grid: {\n verticalLines: true,\n horizontalLines: true\n }\n };\n if (settings.grid) {\n options.grid.color = settings.grid.color || \"#545454\";\n options.grid.backgroundColor = settings.grid.backgroundColor || null;\n options.grid.tickColor = settings.grid.tickColor || \"#DDDDDD\";\n options.grid.verticalLines = settings.grid.verticalLines !== false;\n options.grid.horizontalLines = settings.grid.horizontalLines !== false;\n }\n if (settings.xaxis) {\n options.xaxis.showLabels = settings.xaxis.showLabels !== false;\n options.xaxis.color = settings.xaxis.color || null;\n options.xaxis.title = settings.xaxis.title || null;\n options.xaxis.titleAngle = settings.xaxis.titleAngle || 0;\n }\n if (settings.yaxis) {\n options.yaxis.showLabels = settings.yaxis.showLabels !== false;\n options.yaxis.color = settings.yaxis.color || null;\n options.yaxis.title = settings.yaxis.title || null;\n options.yaxis.titleAngle = settings.yaxis.titleAngle || 0;\n }\n}\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n options.xaxis.min = timeWindow.minTime;\n options.xaxis.max = timeWindow.maxTime;\n graph = Flotr.draw(containerElement, data, options);\n};\n\nfns.destroy = function() {\n //console.log(''destroy!'');\n};","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"shadowSize\": {\n \"title\": \"Shadow size\",\n \"type\": \"number\",\n \"default\": 4\n },\n \"fontColor\": {\n \"title\": \"Font color\",\n \"type\": \"string\",\n \"default\": \"#545454\"\n },\n \"fontSize\": {\n \"title\": \"Font size\",\n \"type\": \"number\",\n \"default\": 7.5\n },\n \"grid\": {\n \"title\": \"Grid settings\",\n \"type\": \"object\",\n \"properties\": {\n \"color\": {\n \"title\": \"Primary color\",\n \"type\": \"string\",\n \"default\": \"#545454\"\n },\n \"backgroundColor\": {\n \"title\": \"Background color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"tickColor\": {\n \"title\": \"Ticks color\",\n \"type\": \"string\",\n \"default\": \"#DDDDDD\"\n },\n \"verticalLines\": {\n \"title\": \"Show vertical lines\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"horizontalLines\": {\n \"title\": \"Show horizontal lines\",\n \"type\": \"boolean\",\n \"default\": true\n }\n }\n },\n \"xaxis\": {\n \"title\": \"X axis settings\",\n \"type\": \"object\",\n \"properties\": {\n \"showLabels\": {\n \"title\": \"Show labels\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"title\": {\n \"title\": \"Axis title\",\n \"type\": \"string\",\n \"default\": null\n },\n \"titleAngle\": {\n \"title\": \"Axis title''s angle in degrees\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"color\": {\n \"title\": \"Ticks color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"yaxis\": {\n \"title\": \"Y axis settings\",\n \"type\": \"object\",\n \"properties\": {\n \"showLabels\": {\n \"title\": \"Show labels\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"title\": {\n \"title\": \"Axis title\",\n \"type\": \"string\",\n \"default\": null\n },\n \"titleAngle\": {\n \"title\": \"Axis title''s angle in degrees\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"color\": {\n \"title\": \"Ticks color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n }\n },\n \"required\": []\n },\n \"form\": [\n \"shadowSize\", \n {\n \"key\": \"fontColor\",\n \"type\": \"color\"\n },\n \"fontSize\", \n {\n \"key\": \"grid\",\n \"items\": [\n {\n \"key\": \"grid.color\",\n \"type\": \"color\"\n },\n {\n \"key\": \"grid.backgroundColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"grid.tickColor\",\n \"type\": \"color\"\n },\n \"grid.verticalLines\",\n \"grid.horizontalLines\"\n ]\n },\n {\n \"key\": \"xaxis\",\n \"items\": [\n \"xaxis.showLabels\",\n \"xaxis.title\",\n \"xaxis.titleAngle\",\n {\n \"key\": \"xaxis.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"yaxis\",\n \"items\": [\n \"yaxis.showLabels\",\n \"yaxis.title\",\n \"yaxis.titleAngle\",\n {\n \"key\": \"yaxis.color\",\n \"type\": \"color\"\n }\n ]\n }\n\n ]\n}","dataKeySettingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"showLines\": {\n \"title\": \"Show lines\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"fillLines\": {\n \"title\": \"Fill lines\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showPoints\": {\n \"title\": \"Show points\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": [\"showLines\", \"fillLines\", \"showPoints\"]\n },\n \"form\": [\n \"showLines\",\n \"fillLines\",\n \"showPoints\"\n ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":7.5,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"backgroundColor\":\"#ffffff\"}},\"title\":\"Timeseries - Flotr2\"}"}',
'Timeseries - Flotr2' );
@@ -194,7 +204,7 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', '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 markerCluster;\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 var configuredMarkersSettings = settings.markersSettings;\n if (!configuredMarkersSettings) {\n configuredMarkersSettings = [];\n }\n \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 };\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 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 }\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 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 marker.bindPopup(''<b>'' + settings.label + ''</b>'');\n if (settings.showLabel) {\n marker.bindTooltip(''<b>'' + settings.label + ''</b>'', { className: ''tb-marker-label'', permanent: true, direction: ''top'', offset: [0, -24] });\n }\n return marker;\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\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 \"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 {\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},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true}],\"fitMapBounds\":true},\"title\":\"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" )
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 d4deb77..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-SNAPSHOT</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 926c4e6..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-SNAPSHOT</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 e166abb..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-SNAPSHOT</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 618e58c..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-SNAPSHOT</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 f90a278..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-SNAPSHOT</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceCredentialsUpdateNotificationMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceCredentialsUpdateNotificationMsg.java
new file mode 100644
index 0000000..0104824
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceCredentialsUpdateNotificationMsg.java
@@ -0,0 +1,36 @@
+/**
+ * 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.extensions.api.device;
+
+import lombok.Data;
+import lombok.Getter;
+import lombok.ToString;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.AttributeKey;
+
+import java.util.Set;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class DeviceCredentialsUpdateNotificationMsg implements ToDeviceActorNotificationMsg {
+
+ private final TenantId tenantId;
+ private final DeviceId deviceId;
+
+}
extensions-core/pom.xml 2(+1 -1)
diff --git a/extensions-core/pom.xml b/extensions-core/pom.xml
index bb17d0c..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-SNAPSHOT</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
pom.xml 2(+1 -1)
diff --git a/pom.xml b/pom.xml
index 06e9821..22fb73f 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-SNAPSHOT</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 c1b3cc0..ad5921f 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-SNAPSHOT</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
diff --git a/tools/src/main/resources/test.properties b/tools/src/main/resources/test.properties
new file mode 100644
index 0000000..6e9ed89
--- /dev/null
+++ b/tools/src/main/resources/test.properties
@@ -0,0 +1,5 @@
+restUrl=http://localhost:8080
+mqttUrls=tcp://localhost:1883
+deviceCount=1
+durationMs=60000
+iterationIntervalMs=1000
transport/coap/pom.xml 2(+1 -1)
diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml
index b4bc06b..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-SNAPSHOT</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/session/CoapSessionCtx.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java
index a61f0bf..6f6e35a 100644
--- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java
@@ -36,6 +36,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.atomic.AtomicInteger;
+
@Slf4j
public class CoapSessionCtx extends DeviceAwareSessionContext {
@@ -87,6 +88,8 @@ public class CoapSessionCtx extends DeviceAwareSessionContext {
private void onSessionClose(SessionCloseMsg msg) {
if (msg.isTimeout()) {
exchange.respond(ResponseCode.SERVICE_UNAVAILABLE);
+ } else if (msg.isCredentialsRevoked()) {
+ exchange.respond(ResponseCode.UNAUTHORIZED);
} else {
exchange.respond(ResponseCode.INTERNAL_SERVER_ERROR);
}
@@ -120,7 +123,7 @@ public class CoapSessionCtx extends DeviceAwareSessionContext {
public void close() {
log.info("[{}] Closing processing context. Timeout: {}", sessionId, exchange.advanced().isTimedOut());
- processor.process(new SessionCloseMsg(sessionId, exchange.advanced().isTimedOut()));
+ processor.process(exchange.advanced().isTimedOut() ? SessionCloseMsg.onTimeout(sessionId) : SessionCloseMsg.onError(sessionId));
}
@Override
transport/http/pom.xml 2(+1 -1)
diff --git a/transport/http/pom.xml b/transport/http/pom.xml
index 8909763..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-SNAPSHOT</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>
transport/mqtt/pom.xml 2(+1 -1)
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
index 866ea4f..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-SNAPSHOT</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/MqttTransportHandler.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
index e1bb45c..b4d8108 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -210,7 +210,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
private void processDisconnect(ChannelHandlerContext ctx) {
- processor.process(new SessionCloseMsg(sessionCtx.getSessionId(), false));
ctx.close();
}
@@ -255,6 +254,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
- processor.process(new SessionCloseMsg(sessionCtx.getSessionId(), false));
+ processor.process(SessionCloseMsg.onError(sessionCtx.getSessionId()));
}
}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java
index 5cae9f5..f653682 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java
@@ -16,12 +16,13 @@
package org.thingsboard.server.transport.mqtt.session;
import io.netty.channel.ChannelHandlerContext;
-import io.netty.handler.codec.mqtt.MqttMessage;
+import io.netty.handler.codec.mqtt.*;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.SessionId;
import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
import org.thingsboard.server.common.msg.session.ex.SessionException;
import org.thingsboard.server.common.transport.SessionMsgProcessor;
import org.thingsboard.server.common.transport.adaptor.AdaptorException;
@@ -75,7 +76,10 @@ public class MqttSessionCtx extends DeviceAwareSessionContext {
@Override
public void onMsg(SessionCtrlMsg msg) throws SessionException {
-
+ if (msg instanceof SessionCloseMsg) {
+ pushToNetwork(new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0)));
+ channel.close();
+ }
}
@Override
transport/pom.xml 2(+1 -1)
diff --git a/transport/pom.xml b/transport/pom.xml
index 0e1c261..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-SNAPSHOT</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
ui/.eslintrc 3(+3 -0)
diff --git a/ui/.eslintrc b/ui/.eslintrc
index 5cb89e5..5b8920d 100644
--- a/ui/.eslintrc
+++ b/ui/.eslintrc
@@ -11,5 +11,8 @@
"node_modules",
"\\.tpl\\.html$"
]
+ },
+ "globals": {
+ "FileReader": true
}
}
ui/package.json 2(+2 -0)
diff --git a/ui/package.json b/ui/package.json
index 070c07f..c1101d2 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -14,6 +14,7 @@
"build": "NODE_ENV=production webpack -p"
},
"dependencies": {
+ "@flowjs/ng-flow": "^2.7.1",
"ace-builds": "^1.2.5",
"angular": "1.5.8",
"angular-animate": "1.5.8",
@@ -65,6 +66,7 @@
"react": "^15.4.1",
"react-ace": "^4.1.0",
"react-dom": "^15.4.1",
+ "react-dropzone": "^3.7.3",
"react-schema-form": "^0.3.1",
"react-tap-event-plugin": "^2.0.1",
"reactcss": "^1.0.9",
ui/pom.xml 2(+1 -1)
diff --git a/ui/pom.xml b/ui/pom.xml
index 379511d..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-SNAPSHOT</version>
+ <version>1.1.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
ui/src/app/app.js 2(+2 -0)
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 3d55ac1..3aa6cea 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -42,6 +42,7 @@ import 'react-dom';
import 'material-ui';
import 'react-schema-form';
import react from 'ngreact';
+import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
import thingsboardLogin from './login';
import thingsboardDialogs from './components/datakey-config-dialog.controller';
@@ -88,6 +89,7 @@ angular.module('thingsboard', [
'angular-carousel',
'ngclipboard',
react.name,
+ 'flow',
thingsboardLogin,
thingsboardDialogs,
thingsboardMenu,
ui/src/app/app.run.js 3(+3 -0)
diff --git a/ui/src/app/app.run.js b/ui/src/app/app.run.js
index 934f021..3f2e7d7 100644
--- a/ui/src/app/app.run.js
+++ b/ui/src/app/app.run.js
@@ -14,9 +14,12 @@
* limitations under the License.
*/
+import Flow from '@flowjs/ng-flow/dist/ng-flow-standalone.min';
+
/*@ngInject*/
export default function AppRun($rootScope, $window, $log, $state, $mdDialog, $filter, loginService, userService, $translate) {
+ $window.Flow = Flow;
var frame = $window.frameElement;
var unauthorizedDialog = null;
var forbiddenDialog = null;
ui/src/app/components/dashboard.directive.js 40(+38 -2)
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index ce7668f..f90b055 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -51,6 +51,7 @@ function Dashboard() {
widgets: '=',
deviceAliasList: '=',
columns: '=',
+ margins: '=',
isEdit: '=',
isMobile: '=',
isMobileDisabled: '=?',
@@ -61,7 +62,8 @@ function Dashboard() {
onWidgetClicked: '&?',
loadWidgets: '&?',
onInit: '&?',
- onInitFailed: '&?'
+ onInitFailed: '&?',
+ dashboardStyle: '=?'
},
controller: DashboardController,
controllerAs: 'vm',
@@ -108,7 +110,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
},
isMobile: vm.isMobileDisabled ? false : vm.isMobile,
mobileBreakPoint: vm.isMobileDisabled ? 0 : (vm.isMobile ? 20000 : 960),
- margins: [10, 10],
+ margins: vm.margins ? vm.margins : [10, 10],
saveGridItemCalculatedHeightInMobile: true
};
@@ -159,6 +161,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 () {
@@ -224,6 +240,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()
ui/src/app/components/dashboard.tpl.html 112(+57 -55)
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index 1127cb0..f0cecef 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -20,61 +20,63 @@
<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 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 }">
+ <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>
- </div>
- </li>
- </ul>
+ <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 }">
+ </div>
+ </div>
+ </div>
+ </li>
+ </ul>
+ </div>
</div>
</md-content>
\ No newline at end of file
ui/src/app/components/react/json-form-image.jsx 105(+105 -0)
diff --git a/ui/src/app/components/react/json-form-image.jsx b/ui/src/app/components/react/json-form-image.jsx
new file mode 100644
index 0000000..2da3edc
--- /dev/null
+++ b/ui/src/app/components/react/json-form-image.jsx
@@ -0,0 +1,105 @@
+/*
+ * 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 './json-form-image.scss';
+
+import React from 'react';
+import ThingsboardBaseComponent from './json-form-base-component.jsx';
+import Dropzone from 'react-dropzone';
+import IconButton from 'material-ui/IconButton';
+
+class ThingsboardImage extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.onValueChanged = this.onValueChanged.bind(this);
+ this.onDrop = this.onDrop.bind(this);
+ this.onClear = this.onClear.bind(this);
+ var value = props.value ? props.value + '' : null;
+ this.state = {
+ imageUrl: value
+ };
+ }
+
+ onValueChanged(value) {
+ this.setState({
+ imageUrl: value
+ });
+ this.props.onChangeValidate({
+ target: {
+ value: value
+ }
+ });
+ }
+
+ onDrop(files) {
+ var reader = new FileReader();
+ reader.onload = (function(tImg) {
+ return function(event) {
+ tImg.onValueChanged(event.target.result);
+ };
+ })(this);
+ reader.readAsDataURL(files[0]);
+ }
+
+ onClear(event) {
+ if (event) {
+ event.stopPropagation();
+ }
+ this.onValueChanged(null);
+ }
+
+ render() {
+
+ var labelClass = "tb-label";
+ if (this.props.form.required) {
+ labelClass += " tb-required";
+ }
+ if (this.props.form.readonly) {
+ labelClass += " tb-readonly";
+ }
+ if (this.state.focused) {
+ labelClass += " tb-focused";
+ }
+
+ var previewComponent;
+ if (this.state.imageUrl) {
+ previewComponent = <img className="tb-image-preview" src={this.state.imageUrl} />;
+ } else {
+ previewComponent = <div>No image selected</div>;
+ }
+
+ return (
+ <div className="tb-container">
+ <label className={labelClass}>{this.props.form.title}</label>
+ <div className="tb-image-select-container">
+ <div className="tb-image-preview-container">{previewComponent}</div>
+ <div className="tb-image-clear-container">
+ <IconButton className="tb-image-clear-btn" iconClassName="material-icons" tooltip="Clear" onTouchTap={this.onClear}>clear</IconButton>
+ </div>
+ <Dropzone className="tb-dropzone"
+ onDrop={this.onDrop}
+ multiple={false}
+ accept="image/*">
+ <div>Drop an image or click to select a file to upload.</div>
+ </Dropzone>
+ </div>
+ </div>
+ );
+ }
+}
+
+export default ThingsboardBaseComponent(ThingsboardImage);
\ No newline at end of file
diff --git a/ui/src/app/components/react/json-form-image.scss b/ui/src/app/components/react/json-form-image.scss
new file mode 100644
index 0000000..58b5eaa
--- /dev/null
+++ b/ui/src/app/components/react/json-form-image.scss
@@ -0,0 +1,79 @@
+/**
+ * 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.
+ */
+
+$previewSize: 100px;
+
+.tb-image-select-container {
+ position: relative;
+ height: $previewSize;
+ width: 100%;
+}
+
+.tb-image-preview {
+ max-width: $previewSize;
+ max-height: $previewSize;
+ width: 100%;
+ height: 100%;
+}
+
+.tb-image-preview-container {
+ position: relative;
+ width: $previewSize;
+ height: $previewSize;
+ margin-right: 12px;
+ border: solid 1px;
+ vertical-align: top;
+ float: left;
+ div {
+ width: 100%;
+ font-size: 18px;
+ text-align: center;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%,-50%);
+ }
+}
+
+.tb-dropzone {
+ position: relative;
+ border: dashed 2px;
+ height: $previewSize;
+ vertical-align: top;
+ padding: 0 8px;
+ overflow: hidden;
+ div {
+ width: 100%;
+ font-size: 24px;
+ text-align: center;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%,-50%);
+ }
+}
+
+.tb-image-clear-container {
+ width: 48px;
+ height: $previewSize;
+ position: relative;
+ float: right;
+}
+.tb-image-clear-btn {
+ position: absolute !important;
+ top: 50%;
+ transform: translate(0%,-50%) !important;
+}
diff --git a/ui/src/app/components/react/json-form-schema-form.jsx b/ui/src/app/components/react/json-form-schema-form.jsx
index 3e1c046..c641a0a 100644
--- a/ui/src/app/components/react/json-form-schema-form.jsx
+++ b/ui/src/app/components/react/json-form-schema-form.jsx
@@ -26,6 +26,7 @@ import ThingsboardText from './json-form-text.jsx';
import Select from 'react-schema-form/lib/Select';
import Radios from 'react-schema-form/lib/Radios';
import ThingsboardDate from './json-form-date.jsx';
+import ThingsboardImage from './json-form-image.jsx';
import ThingsboardCheckbox from './json-form-checkbox.jsx';
import Help from 'react-schema-form/lib/Help';
import ThingsboardFieldSet from './json-form-fieldset.jsx';
@@ -45,6 +46,7 @@ class ThingsboardSchemaForm extends React.Component {
'select': Select,
'radios': Radios,
'date': ThingsboardDate,
+ 'image': ThingsboardImage,
'checkbox': ThingsboardCheckbox,
'help': Help,
'array': ThingsboardArray,
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>' +
ui/src/app/dashboard/dashboard.controller.js 29(+29 -0)
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 18c2b5f..68978c3 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -16,6 +16,7 @@
/* eslint-disable import/no-unresolved, import/default */
import deviceAliasesTemplate from './device-aliases.tpl.html';
+import dashboardBackgroundTemplate from './dashboard-settings.tpl.html';
import addWidgetTemplate from './add-widget.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
@@ -55,6 +56,7 @@ export default function DashboardController(types, widgetService, userService,
vm.onAddWidgetClosed = onAddWidgetClosed;
vm.onEditWidgetClosed = onEditWidgetClosed;
vm.openDeviceAliases = openDeviceAliases;
+ vm.openDashboardSettings = openDashboardSettings;
vm.removeWidget = removeWidget;
vm.saveDashboard = saveDashboard;
vm.saveWidget = saveWidget;
@@ -252,6 +254,24 @@ export default function DashboardController(types, widgetService, userService,
});
}
+ function openDashboardSettings($event) {
+ $mdDialog.show({
+ controller: 'DashboardSettingsController',
+ controllerAs: 'vm',
+ templateUrl: dashboardBackgroundTemplate,
+ locals: {
+ gridSettings: angular.copy(vm.dashboard.configuration.gridSettings)
+ },
+ parent: angular.element($document[0].body),
+ skipHide: true,
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function (gridSettings) {
+ vm.dashboard.configuration.gridSettings = gridSettings;
+ }, function () {
+ });
+ }
+
function editWidget($event, widget) {
$event.stopPropagation();
var newEditingIndex = vm.widgets.indexOf(widget);
@@ -368,6 +388,15 @@ export default function DashboardController(types, widgetService, userService,
w.triggerHandler('resize');
}
}).then(function (widget) {
+ var columns = 24;
+ if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) {
+ columns = vm.dashboard.configuration.gridSettings.columns;
+ }
+ if (columns != 24) {
+ var ratio = columns / 24;
+ widget.sizeX *= ratio;
+ widget.sizeY *= ratio;
+ }
vm.widgets.push(widget);
}, function () {
});
ui/src/app/dashboard/dashboard.tpl.html 14(+13 -1)
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index b18c392..0c815a6 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -59,10 +59,22 @@
<md-button class="md-raised" flex="none" ng-show="vm.isEdit" ng-click="vm.openDeviceAliases($event)">
{{ 'device.aliases' | translate }}
</md-button>
+ <md-button class="md-raised" flex="none" ng-show="vm.isEdit" ng-click="vm.openDashboardSettings($event)">
+ {{ 'dashboard.settings' | translate }}
+ </md-button>
</section>
- <div class="tb-absolute-fill" ng-class="{ 'tb-padded' : !vm.widgetEditMode, 'tb-shrinked' : vm.isEditingWidget }">
+ <div class="tb-absolute-fill"
+ ng-class="{ 'tb-padded' : !vm.widgetEditMode, 'tb-shrinked' : vm.isEditingWidget }">
<tb-dashboard
+ dashboard-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,
+ 'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',
+ 'background-repeat': 'no-repeat',
+ 'background-attachment': 'scroll',
+ 'background-size': '100%',
+ 'background-position': '0% 0%'}"
widgets="vm.widgets"
+ columns="vm.dashboard.configuration.gridSettings.columns"
+ margins="vm.dashboard.configuration.gridSettings.margins"
device-alias-list="vm.dashboard.configuration.deviceAliases"
is-edit="vm.isEdit || vm.widgetEditMode"
is-mobile="vm.forceDashboardMobileMode"
diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js
new file mode 100644
index 0000000..d15359e
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-settings.controller.js
@@ -0,0 +1,64 @@
+/*
+ * 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 './dashboard-settings.scss';
+
+/*@ngInject*/
+export default function DashboardSettingsController($scope, $mdDialog, gridSettings) {
+
+ var vm = this;
+
+ vm.cancel = cancel;
+ vm.save = save;
+ vm.imageAdded = imageAdded;
+ vm.clearImage = clearImage;
+
+ vm.gridSettings = gridSettings || {};
+
+ 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];
+ vm.hMargin = vm.gridSettings.margins[0];
+ vm.vMargin = vm.gridSettings.margins[1];
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function imageAdded($file) {
+ var reader = new FileReader();
+ reader.onload = function(event) {
+ $scope.$apply(function() {
+ if (event.target.result && event.target.result.startsWith('data:image/')) {
+ $scope.theForm.$setDirty();
+ vm.gridSettings.backgroundImageUrl = event.target.result;
+ }
+ });
+ };
+ reader.readAsDataURL($file.file);
+ }
+
+ function clearImage() {
+ $scope.theForm.$setDirty();
+ vm.gridSettings.backgroundImageUrl = null;
+ }
+
+ function save() {
+ $scope.theForm.$setPristine();
+ vm.gridSettings.margins = [vm.hMargin, vm.vMargin];
+ $mdDialog.hide(vm.gridSettings);
+ }
+}
ui/src/app/dashboard/dashboard-settings.scss 91(+91 -0)
diff --git a/ui/src/app/dashboard/dashboard-settings.scss b/ui/src/app/dashboard/dashboard-settings.scss
new file mode 100644
index 0000000..2231372
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-settings.scss
@@ -0,0 +1,91 @@
+/**
+ * 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.
+ */
+$previewSize: 100px;
+
+.file-input {
+ display: none;
+}
+
+.tb-container {
+ position: relative;
+ margin-top: 32px;
+ padding: 10px 0;
+}
+
+.tb-image-select-container {
+ position: relative;
+ height: $previewSize;
+ width: 100%;
+}
+
+.tb-image-preview {
+ max-width: $previewSize;
+ max-height: $previewSize;
+ width: auto;
+ height: auto;
+}
+
+.tb-image-preview-container {
+ position: relative;
+ width: $previewSize;
+ height: $previewSize;
+ margin-right: 12px;
+ border: solid 1px;
+ vertical-align: top;
+ float: left;
+ div {
+ width: 100%;
+ font-size: 18px;
+ text-align: center;
+ }
+ div, .tb-image-preview {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%,-50%);
+ }
+}
+
+.tb-flow-drop {
+ position: relative;
+ border: dashed 2px;
+ height: $previewSize;
+ vertical-align: top;
+ padding: 0 8px;
+ overflow: hidden;
+ min-width: 300px;
+ label {
+ width: 100%;
+ font-size: 24px;
+ text-align: center;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%,-50%);
+ }
+}
+
+.tb-image-clear-container {
+ width: 48px;
+ height: $previewSize;
+ position: relative;
+ float: right;
+}
+.tb-image-clear-btn {
+ position: absolute !important;
+ top: 50%;
+ transform: translate(0%,-50%) !important;
+}
ui/src/app/dashboard/dashboard-settings.tpl.html 115(+115 -0)
diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html
new file mode 100644
index 0000000..f69eb02
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-settings.tpl.html
@@ -0,0 +1,115 @@
+<!--
+
+ 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.
+
+-->
+<md-dialog aria-label="{{ 'dashboard.settings' | translate }}">
+ <form name="theForm" ng-submit="vm.save()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>dashboard.settings</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset ng-disabled="loading">
+ <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"
+ max="1000" />
+ <div ng-messages="theForm.columns.$error" multiple md-auto-hide="false">
+ <div ng-message="required" translate>dashboard.columns-count-required</div>
+ <div ng-message="min" translate>dashboard.min-columns-count-message</div>
+ <div ng-message="max">dashboard.max-columns-count-message</div>
+ </div>
+ </md-input-container>
+ <small translate>dashboard.widgets-margins</small>
+ <div flex layout="row">
+ <md-input-container flex class="md-block">
+ <label translate>dashboard.horizontal-margin</label>
+ <input required type="number" step="any" name="hMargin" ng-model="vm.hMargin" min="0"
+ max="50" />
+ <div ng-messages="theForm.hMargin.$error" multiple md-auto-hide="false">
+ <div ng-message="required" translate>dashboard.horizontal-margin-required</div>
+ <div ng-message="min" translate>dashboard.min-horizontal-margin-message</div>
+ <div ng-message="max" translate>dashboard.max-horizontal-margin-message</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex class="md-block">
+ <label translate>dashboard.vertical-margin</label>
+ <input required type="number" step="any" name="vMargin" ng-model="vm.vMargin" min="0"
+ max="50" />
+ <div ng-messages="theForm.vMargin.$error" multiple md-auto-hide="false">
+ <div ng-message="required" translate>dashboard.vertical-margin-required</div>
+ <div ng-message="min" translate>dashboard.min-vertical-margin-message</div>
+ <div ng-message="max" translate>dashboard.max-vertical-margin-message</div>
+ </div>
+ </md-input-container>
+ </div>
+ <div flex
+ ng-required="false"
+ md-color-picker
+ ng-model="vm.gridSettings.backgroundColor"
+ label="{{ 'dashboard.background-color' | translate }}"
+ icon="format_color_fill"
+ default="rgba(0,0,0,0)"
+ md-color-clear-button="false"
+ open-on-input="true"
+ md-color-generic-palette="false"
+ md-color-history="false"
+ ></div>
+ <div class="tb-container">
+ <label class="tb-label" translate>dashboard.background-image</label>
+ <div flow-init="{singleFile:true}"
+ flow-file-added="vm.imageAdded( $file )" class="tb-image-select-container">
+ <div class="tb-image-preview-container">
+ <div ng-show="!vm.gridSettings.backgroundImageUrl" translate>dashboard.no-image</div>
+ <img ng-show="vm.gridSettings.backgroundImageUrl" class="tb-image-preview" src="{{vm.gridSettings.backgroundImageUrl}}" />
+ </div>
+ <div class="tb-image-clear-container">
+ <md-button ng-click="vm.clearImage()"
+ class="tb-image-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+ </div>
+ <div class="alert tb-flow-drop" flow-drop>
+ <label for="select" translate>dashboard.drop-image</label>
+ <input class="file-input" flow-btn flow-attrs="{accept:'image/*'}" id="select">
+ </div>
+ </div>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || !theForm.$dirty || !theForm.$valid" type="submit" class="md-raised md-primary">
+ {{ 'action.save' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
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 d740b10..7400463 100644
--- a/ui/src/app/dashboard/index.js
+++ b/ui/src/app/dashboard/index.js
@@ -34,6 +34,7 @@ import DashboardRoutes from './dashboard.routes';
import DashboardsController from './dashboards.controller';
import DashboardController from './dashboard.controller';
import DeviceAliasesController from './device-aliases.controller';
+import DashboardSettingsController from './dashboard-settings.controller';
import AssignDashboardToCustomerController from './assign-to-customer.controller';
import AddDashboardsToCustomerController from './add-dashboards-to-customer.controller';
import AddWidgetController from './add-widget.controller';
@@ -59,6 +60,7 @@ export default angular.module('thingsboard.dashboard', [
.controller('DashboardsController', DashboardsController)
.controller('DashboardController', DashboardController)
.controller('DeviceAliasesController', DeviceAliasesController)
+ .controller('DashboardSettingsController', DashboardSettingsController)
.controller('AssignDashboardToCustomerController', AssignDashboardToCustomerController)
.controller('AddDashboardsToCustomerController', AddDashboardsToCustomerController)
.controller('AddWidgetController', AddWidgetController)
ui/src/app/device/device.directive.js 13(+11 -2)
diff --git a/ui/src/app/device/device.directive.js b/ui/src/app/device/device.directive.js
index 918040b..7f84016 100644
--- a/ui/src/app/device/device.directive.js
+++ b/ui/src/app/device/device.directive.js
@@ -20,18 +20,23 @@ import deviceFieldsetTemplate from './device-fieldset.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
-export default function DeviceDirective($compile, $templateCache, toast, $translate, types, customerService) {
+export default function DeviceDirective($compile, $templateCache, toast, $translate, types, deviceService, customerService) {
var linker = function (scope, element) {
var template = $templateCache.get(deviceFieldsetTemplate);
element.html(template);
scope.isAssignedToCustomer = false;
-
scope.assignedCustomer = null;
+ scope.deviceCredentials = null;
scope.$watch('device', function(newVal) {
if (newVal) {
+ deviceService.getDeviceCredentials(scope.device.id.id).then(
+ function success(credentials) {
+ scope.deviceCredentials = credentials;
+ }
+ );
if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
scope.isAssignedToCustomer = true;
customerService.getCustomer(scope.device.customerId.id).then(
@@ -50,6 +55,10 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
toast.showSuccess($translate.instant('device.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
};
+ scope.onAccessTokenCopied = function() {
+ toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+ };
+
$compile(element.contents())(scope);
}
return {
diff --git a/ui/src/app/device/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html
index 6d9892e..3bd8f4f 100644
--- a/ui/src/app/device/device-fieldset.tpl.html
+++ b/ui/src/app/device/device-fieldset.tpl.html
@@ -36,6 +36,13 @@
<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
<span translate>device.copyId</span>
</md-button>
+ <md-button ngclipboard data-clipboard-action="copy"
+ ngclipboard-success="onAccessTokenCopied(e)"
+ data-clipboard-text="{{deviceCredentials.credentialsId}}" ng-show="!isEdit"
+ class="md-raised">
+ <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+ <span translate>device.copyAccessToken</span>
+ </md-button>
</div>
<md-content class="md-padding" layout="column">
ui/src/locale/en_US.json 23(+22 -1)
diff --git a/ui/src/locale/en_US.json b/ui/src/locale/en_US.json
index 4a5e48d..9da68d6 100644
--- a/ui/src/locale/en_US.json
+++ b/ui/src/locale/en_US.json
@@ -192,7 +192,26 @@
"select-existing": "Select existing dashboard",
"create-new": "Create new dashboard",
"new-dashboard-title": "New dashboard title",
- "open-dashboard": "Open dashboard"
+ "open-dashboard": "Open dashboard",
+ "set-background": "Set background",
+ "background-color": "Background color",
+ "background-image": "Background image",
+ "no-image": "No image selected",
+ "drop-image": "Drop an image or click to select a file to upload.",
+ "settings": "Settings",
+ "columns-count": "Columns count",
+ "columns-count-required": "Columns count is required.",
+ "min-columns-count-message": "Only 10 minimum column count is allowed.",
+ "max-columns-count-message": "Only 1000 maximum column count is allowed.",
+ "widgets-margins": "Margin between widgets",
+ "horizontal-margin": "Horizontal margin",
+ "horizontal-margin-required": "Horizontal margin value is required.",
+ "min-horizontal-margin-message": "Only 0 is allowed as minimum horizontal margin value.",
+ "max-horizontal-margin-message": "Only 50 is allowed as maximum horizontal margin value.",
+ "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."
},
"datakey": {
"settings": "Settings",
@@ -280,7 +299,9 @@
"events": "Events",
"details": "Details",
"copyId": "Copy device Id",
+ "copyAccessToken": "Copy access token",
"idCopiedMessage": "Device Id has been copied to clipboard",
+ "accessTokenCopiedMessage": "Device access token has been copied to clipboard",
"assignedToCustomer": "Assigned to customer",
"unable-delete-device-alias-title": "Unable to delete device alias",
"unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}"