Commit e1a2df9b478b73ddc6020402f08800b7bb1da4d8

Authored by volodymyr-babak
2 parents 4e5bb26c 7a8cc68f

Merge branch 'master' into gatling-mqtt

Showing 65 changed files with 1037 additions and 159 deletions
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>thingsboard</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard</groupId>
... ...
... ... @@ -24,7 +24,7 @@
24 24 <file>${pkg.logFolder}/${pkg.name}.log</file>
25 25 <rollingPolicy
26 26 class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
27   - <fileNamePattern>${pkg.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
  27 + <fileNamePattern>${pkg.logFolder}/${pkg.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
28 28 <maxFileSize>100MB</maxFileSize>
29 29 <maxHistory>30</maxHistory>
30 30 <totalSizeCap>3GB</totalSizeCap>
... ...
... ... @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.TenantId;
27 27 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
28 28 import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
29 29 import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
  30 +import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
30 31 import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
31 32 import org.thingsboard.server.extensions.api.plugins.msg.*;
32 33
... ... @@ -58,6 +59,8 @@ public class DeviceActor extends ContextAwareActor {
58 59 processor.processAttributesUpdate(context(), (DeviceAttributesEventNotificationMsg) msg);
59 60 } else if (msg instanceof ToDeviceRpcRequestPluginMsg) {
60 61 processor.processRpcRequest(context(), (ToDeviceRpcRequestPluginMsg) msg);
  62 + } else if (msg instanceof DeviceCredentialsUpdateNotificationMsg){
  63 + processor.processCredentialsUpdate(context(), (DeviceCredentialsUpdateNotificationMsg) msg);
61 64 }
62 65 } else if (msg instanceof TimeoutMsg) {
63 66 processor.processTimeout(context(), (TimeoutMsg) msg);
... ...
... ... @@ -32,13 +32,7 @@ import org.thingsboard.server.common.data.kv.AttributeKey;
32 32 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
33 33 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
34 34 import org.thingsboard.server.common.msg.cluster.ServerAddress;
35   -import org.thingsboard.server.common.msg.core.AttributesUpdateNotification;
36   -import org.thingsboard.server.common.msg.core.BasicCommandAckResponse;
37   -import org.thingsboard.server.common.msg.core.BasicToDeviceSessionActorMsg;
38   -import org.thingsboard.server.common.msg.core.SessionCloseMsg;
39   -import org.thingsboard.server.common.msg.core.ToDeviceRpcRequestMsg;
40   -import org.thingsboard.server.common.msg.core.ToDeviceRpcResponseMsg;
41   -import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
  35 +import org.thingsboard.server.common.msg.core.*;
42 36 import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
43 37 import org.thingsboard.server.common.msg.kv.BasicAttributeKVMsg;
44 38 import org.thingsboard.server.common.msg.session.FromDeviceMsg;
... ... @@ -47,6 +41,7 @@ import org.thingsboard.server.common.msg.session.SessionType;
47 41 import org.thingsboard.server.common.msg.session.ToDeviceMsg;
48 42 import org.thingsboard.server.extensions.api.device.DeviceAttributes;
49 43 import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
  44 +import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
50 45 import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
51 46 import org.thingsboard.server.extensions.api.plugins.msg.RpcError;
52 47 import org.thingsboard.server.extensions.api.plugins.msg.TimeoutIntMsg;
... ... @@ -74,6 +69,7 @@ import java.util.stream.Collectors;
74 69 public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
75 70
76 71 private final DeviceId deviceId;
  72 + private final Map<SessionId, SessionInfo> sessions;
77 73 private final Map<SessionId, SessionInfo> attributeSubscriptions;
78 74 private final Map<SessionId, SessionInfo> rpcSubscriptions;
79 75
... ... @@ -85,6 +81,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
85 81 public DeviceActorMessageProcessor(ActorSystemContext systemContext, LoggingAdapter logger, DeviceId deviceId) {
86 82 super(systemContext, logger);
87 83 this.deviceId = deviceId;
  84 + this.sessions = new HashMap<>();
88 85 this.attributeSubscriptions = new HashMap<>();
89 86 this.rpcSubscriptions = new HashMap<>();
90 87 this.rpcPendingMap = new HashMap<>();
... ... @@ -281,7 +278,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
281 278 if (!msg.isAdded()) {
282 279 logger.debug("[{}] Clearing attributes/rpc subscription for server [{}]", deviceId, msg.getServerAddress());
283 280 Predicate<Map.Entry<SessionId, SessionInfo>> filter = e -> e.getValue().getServer()
284   - .map(serverAddress -> serverAddress.equals(msg.getServerAddress())).orElse(false);
  281 + .map(serverAddress -> serverAddress.equals(msg.getServerAddress())).orElse(false);
285 282 attributeSubscriptions.entrySet().removeIf(filter);
286 283 rpcSubscriptions.entrySet().removeIf(filter);
287 284 }
... ... @@ -342,8 +339,12 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
342 339 private void processSessionStateMsgs(ToDeviceActorMsg msg) {
343 340 SessionId sessionId = msg.getSessionId();
344 341 FromDeviceMsg inMsg = msg.getPayload();
345   - if (inMsg instanceof SessionCloseMsg) {
  342 + if (inMsg instanceof SessionOpenMsg) {
  343 + logger.debug("[{}] Processing new session [{}]", deviceId, sessionId);
  344 + sessions.put(sessionId, new SessionInfo(SessionType.ASYNC, msg.getServerAddress()));
  345 + } else if (inMsg instanceof SessionCloseMsg) {
346 346 logger.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
  347 + sessions.remove(sessionId);
347 348 attributeSubscriptions.remove(sessionId);
348 349 rpcSubscriptions.remove(sessionId);
349 350 }
... ... @@ -363,4 +364,11 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
363 364 return systemContext.getAttributesService().findAll(this.deviceId, attributeType);
364 365 }
365 366
  367 + public void processCredentialsUpdate(ActorContext context, DeviceCredentialsUpdateNotificationMsg msg) {
  368 + sessions.forEach((k, v) -> {
  369 + sendMsgToSessionActor(new BasicToDeviceSessionActorMsg(new SessionCloseNotification(), k), v.getServer());
  370 + });
  371 + attributeSubscriptions.clear();
  372 + rpcSubscriptions.clear();
  373 + }
366 374 }
... ...
... ... @@ -181,7 +181,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
181 181 logger.info("[{}] Plugin requires restart due to clazz change from {} to {}.",
182 182 entityId, oldPluginMd.getClazz(), pluginMd.getClazz());
183 183 requiresRestart = true;
184   - } else if (oldPluginMd.getConfiguration().equals(pluginMd.getConfiguration())) {
  184 + } else if (!oldPluginMd.getConfiguration().equals(pluginMd.getConfiguration())) {
185 185 logger.info("[{}] Plugin requires restart due to configuration change from {} to {}.",
186 186 entityId, oldPluginMd.getConfiguration(), pluginMd.getConfiguration());
187 187 requiresRestart = true;
... ...
... ... @@ -234,18 +234,18 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
234 234 logger.info("[{}] Rule configuration was updated from {} to {}.", entityId, oldRuleMd, ruleMd);
235 235 try {
236 236 fetchPluginInfo();
237   - if (!Objects.equals(oldRuleMd.getFilters(), ruleMd.getFilters())) {
  237 + if (filters == null || !Objects.equals(oldRuleMd.getFilters(), ruleMd.getFilters())) {
238 238 logger.info("[{}] Rule filters require restart due to json change from {} to {}.",
239 239 entityId, mapper.writeValueAsString(oldRuleMd.getFilters()), mapper.writeValueAsString(ruleMd.getFilters()));
240 240 stopFilters();
241 241 initFilters();
242 242 }
243   - if (!Objects.equals(oldRuleMd.getProcessor(), ruleMd.getProcessor())) {
  243 + if (processor == null || !Objects.equals(oldRuleMd.getProcessor(), ruleMd.getProcessor())) {
244 244 logger.info("[{}] Rule processor require restart due to configuration change.", entityId);
245 245 stopProcessor();
246 246 initProcessor();
247 247 }
248   - if (!Objects.equals(oldRuleMd.getAction(), ruleMd.getAction())) {
  248 + if (action == null || !Objects.equals(oldRuleMd.getAction(), ruleMd.getAction())) {
249 249 logger.info("[{}] Rule action require restart due to configuration change.", entityId);
250 250 stopAction();
251 251 initAction();
... ... @@ -272,13 +272,15 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
272 272 if (action != null) {
273 273 if (filters != null) {
274 274 filters.forEach(f -> f.resume());
  275 + } else {
  276 + initFilters();
275 277 }
276 278 if (processor != null) {
277 279 processor.resume();
  280 + } else {
  281 + initProcessor();
278 282 }
279   - if (action != null) {
280   - action.resume();
281   - }
  283 + action.resume();
282 284 logger.info("[{}] Rule resumed.", entityId);
283 285 } else {
284 286 start();
... ...
... ... @@ -15,6 +15,7 @@
15 15 */
16 16 package org.thingsboard.server.actors.service;
17 17
  18 +import org.thingsboard.server.common.data.id.DeviceId;
18 19 import org.thingsboard.server.common.data.id.PluginId;
19 20 import org.thingsboard.server.common.data.id.RuleId;
20 21 import org.thingsboard.server.common.data.id.TenantId;
... ... @@ -28,4 +29,6 @@ public interface ActorService extends SessionMsgProcessor, WebSocketMsgProcessor
28 29 void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state);
29 30
30 31 void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state);
  32 +
  33 + void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId);
31 34 }
... ...
... ... @@ -32,16 +32,19 @@ import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
32 32 import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
33 33 import org.thingsboard.server.actors.session.SessionManagerActor;
34 34 import org.thingsboard.server.actors.stats.StatsActor;
  35 +import org.thingsboard.server.common.data.id.DeviceId;
35 36 import org.thingsboard.server.common.data.id.PluginId;
36 37 import org.thingsboard.server.common.data.id.RuleId;
37 38 import org.thingsboard.server.common.data.id.TenantId;
38 39 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
39 40 import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
40 41 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
  42 +import org.thingsboard.server.common.msg.cluster.ServerAddress;
41 43 import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
42 44 import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
43 45 import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
44 46 import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
  47 +import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
45 48 import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
46 49 import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
47 50 import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
... ... @@ -56,6 +59,7 @@ import scala.concurrent.duration.Duration;
56 59
57 60 import javax.annotation.PostConstruct;
58 61 import javax.annotation.PreDestroy;
  62 +import java.util.Optional;
59 63
60 64 @Service
61 65 @Slf4j
... ... @@ -221,6 +225,17 @@ public class DefaultActorService implements ActorService {
221 225 broadcast(ComponentLifecycleMsg.forRule(tenantId, ruleId, state));
222 226 }
223 227
  228 + @Override
  229 + public void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId) {
  230 + DeviceCredentialsUpdateNotificationMsg msg = new DeviceCredentialsUpdateNotificationMsg(tenantId, deviceId);
  231 + Optional<ServerAddress> address = actorContext.getRoutingService().resolve(deviceId);
  232 + if (address.isPresent()) {
  233 + rpcService.tell(address.get(), msg);
  234 + } else {
  235 + onMsg(msg);
  236 + }
  237 + }
  238 +
224 239 public void broadcast(ToAllNodesMsg msg) {
225 240 rpcService.broadcast(msg);
226 241 appActor.tell(msg, ActorRef.noSender());
... ...
... ... @@ -20,15 +20,14 @@ import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
20 20 import org.thingsboard.server.common.data.id.SessionId;
21 21 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
22 22 import org.thingsboard.server.common.msg.cluster.ServerAddress;
23   -import org.thingsboard.server.common.msg.core.AttributesSubscribeMsg;
24   -import org.thingsboard.server.common.msg.core.ResponseMsg;
25   -import org.thingsboard.server.common.msg.core.RpcSubscribeMsg;
  23 +import org.thingsboard.server.common.msg.core.*;
26 24 import org.thingsboard.server.common.msg.core.SessionCloseMsg;
27 25 import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
28 26 import org.thingsboard.server.common.msg.session.*;
29 27
30 28 import akka.actor.ActorContext;
31 29 import akka.event.LoggingAdapter;
  30 +import org.thingsboard.server.common.msg.session.ctrl.*;
32 31 import org.thingsboard.server.common.msg.session.ex.SessionException;
33 32
34 33 import java.util.HashMap;
... ... @@ -37,7 +36,8 @@ import java.util.Optional;
37 36
38 37 class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
39 38
40   - Map<Integer, ToDeviceActorMsg> pendingMap = new HashMap<>();
  39 + private boolean firstMsg = true;
  40 + private Map<Integer, ToDeviceActorMsg> pendingMap = new HashMap<>();
41 41 private Optional<ServerAddress> currentTargetServer;
42 42 private boolean subscribedToAttributeUpdates;
43 43 private boolean subscribedToRpcCommands;
... ... @@ -49,6 +49,10 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
49 49 @Override
50 50 protected void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg) {
51 51 updateSessionCtx(msg, SessionType.ASYNC);
  52 + if (firstMsg) {
  53 + toDeviceMsg(new SessionOpenMsg()).ifPresent(m -> forwardToAppActor(ctx, m));
  54 + firstMsg = false;
  55 + }
52 56 ToDeviceActorMsg pendingMsg = toDeviceMsg(msg);
53 57 FromDeviceMsg fromDeviceMsg = pendingMsg.getPayload();
54 58 switch (fromDeviceMsg.getMsgType()) {
... ... @@ -80,17 +84,21 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
80 84 @Override
81 85 public void processToDeviceMsg(ActorContext context, ToDeviceMsg msg) {
82 86 try {
83   - switch (msg.getMsgType()) {
84   - case STATUS_CODE_RESPONSE:
85   - case GET_ATTRIBUTES_RESPONSE:
86   - ResponseMsg responseMsg = (ResponseMsg) msg;
87   - if (responseMsg.getRequestId() >= 0) {
88   - logger.debug("[{}] Pending request processed: {}", responseMsg.getRequestId(), responseMsg);
89   - pendingMap.remove(responseMsg.getRequestId());
90   - }
91   - break;
  87 + if (msg.getMsgType() != MsgType.SESSION_CLOSE) {
  88 + switch (msg.getMsgType()) {
  89 + case STATUS_CODE_RESPONSE:
  90 + case GET_ATTRIBUTES_RESPONSE:
  91 + ResponseMsg responseMsg = (ResponseMsg) msg;
  92 + if (responseMsg.getRequestId() >= 0) {
  93 + logger.debug("[{}] Pending request processed: {}", responseMsg.getRequestId(), responseMsg);
  94 + pendingMap.remove(responseMsg.getRequestId());
  95 + }
  96 + break;
  97 + }
  98 + sessionCtx.onMsg(new BasicSessionActorToAdaptorMsg(this.sessionCtx, msg));
  99 + } else {
  100 + sessionCtx.onMsg(org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg.onCredentialsRevoked(sessionCtx.getSessionId()));
92 101 }
93   - sessionCtx.onMsg(new BasicSessionActorToAdaptorMsg(this.sessionCtx, msg));
94 102 } catch (SessionException e) {
95 103 logger.warning("Failed to push session response msg", e);
96 104 }
... ... @@ -102,7 +110,7 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
102 110 }
103 111
104 112 protected void cleanupSession(ActorContext ctx) {
105   - toDeviceMsg(new SessionCloseMsg()).ifPresent(msg -> forwardToAppActor(ctx, msg));
  113 + toDeviceMsg(new SessionCloseMsg()).ifPresent(m -> forwardToAppActor(ctx, m));
106 114 }
107 115
108 116 @Override
... ... @@ -110,6 +118,7 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
110 118 if (pendingMap.size() > 0 || subscribedToAttributeUpdates || subscribedToRpcCommands) {
111 119 Optional<ServerAddress> newTargetServer = systemContext.getRoutingService().resolve(getDeviceId());
112 120 if (!newTargetServer.equals(currentTargetServer)) {
  121 + firstMsg = true;
113 122 currentTargetServer = newTargetServer;
114 123 pendingMap.values().forEach(v -> {
115 124 forwardToAppActor(context, v, currentTargetServer);
... ...
... ... @@ -52,7 +52,7 @@ class SyncMsgProcessor extends AbstractSessionActorMsgProcessor {
52 52 public void processTimeoutMsg(ActorContext context, SessionTimeoutMsg msg) {
53 53 if (pendingResponse) {
54 54 try {
55   - sessionCtx.onMsg(new SessionCloseMsg(sessionId, true));
  55 + sessionCtx.onMsg(SessionCloseMsg.onTimeout(sessionId));
56 56 } catch (SessionException e) {
57 57 logger.warning("Failed to push session close msg", e);
58 58 }
... ...
... ... @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
28 28 import org.thingsboard.server.dao.exception.IncorrectParameterException;
29 29 import org.thingsboard.server.dao.model.ModelConstants;
30 30 import org.thingsboard.server.exception.ThingsboardException;
  31 +import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
31 32
32 33 @RestController
33 34 @RequestMapping("/api")
... ... @@ -48,7 +49,7 @@ public class DeviceController extends BaseController {
48 49
49 50 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
50 51 @RequestMapping(value = "/device", method = RequestMethod.POST)
51   - @ResponseBody
  52 + @ResponseBody
52 53 public Device saveDevice(@RequestBody Device device) throws ThingsboardException {
53 54 try {
54 55 device.setTenantId(getCurrentUser().getTenantId());
... ... @@ -74,7 +75,7 @@ public class DeviceController extends BaseController {
74 75
75 76 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
76 77 @RequestMapping(value = "/customer/{customerId}/device/{deviceId}", method = RequestMethod.POST)
77   - @ResponseBody
  78 + @ResponseBody
78 79 public Device assignDeviceToCustomer(@PathVariable("customerId") String strCustomerId,
79 80 @PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
80 81 checkParameter("customerId", strCustomerId);
... ... @@ -85,7 +86,7 @@ public class DeviceController extends BaseController {
85 86
86 87 DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
87 88 checkDeviceId(deviceId);
88   -
  89 +
89 90 return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, customerId));
90 91 } catch (Exception e) {
91 92 throw handleException(e);
... ... @@ -94,7 +95,7 @@ public class DeviceController extends BaseController {
94 95
95 96 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
96 97 @RequestMapping(value = "/customer/device/{deviceId}", method = RequestMethod.DELETE)
97   - @ResponseBody
  98 + @ResponseBody
98 99 public Device unassignDeviceFromCustomer(@PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
99 100 checkParameter("deviceId", strDeviceId);
100 101 try {
... ... @@ -125,19 +126,21 @@ public class DeviceController extends BaseController {
125 126
126 127 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
127 128 @RequestMapping(value = "/device/credentials", method = RequestMethod.POST)
128   - @ResponseBody
  129 + @ResponseBody
129 130 public DeviceCredentials saveDeviceCredentials(@RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException {
130 131 checkNotNull(deviceCredentials);
131 132 try {
132 133 checkDeviceId(deviceCredentials.getDeviceId());
133   - return checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
  134 + DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
  135 + actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId());
  136 + return result;
134 137 } catch (Exception e) {
135 138 throw handleException(e);
136 139 }
137 140 }
138 141
139 142 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
140   - @RequestMapping(value = "/tenant/devices", params = { "limit" }, method = RequestMethod.GET)
  143 + @RequestMapping(value = "/tenant/devices", params = {"limit"}, method = RequestMethod.GET)
141 144 @ResponseBody
142 145 public TextPageData<Device> getTenantDevices(
143 146 @RequestParam int limit,
... ... @@ -154,7 +157,7 @@ public class DeviceController extends BaseController {
154 157 }
155 158
156 159 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
157   - @RequestMapping(value = "/customer/{customerId}/devices", params = { "limit" }, method = RequestMethod.GET)
  160 + @RequestMapping(value = "/customer/{customerId}/devices", params = {"limit"}, method = RequestMethod.GET)
158 161 @ResponseBody
159 162 public TextPageData<Device> getCustomerDevices(
160 163 @PathVariable("customerId") String strCustomerId,
... ...
... ... @@ -138,8 +138,8 @@ cassandra:
138 138 default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
139 139 # Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS
140 140 ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
141   - # Specify max partitions per request
142   - max_limit_per_request: "${TS_KV_MAX_LIMIT_PER_REQUEST:1000}"
  141 + # Specify max data points per request
  142 + max_limit_per_request: "${TS_KV_MAX_LIMIT_PER_REQUEST:86400}"
143 143
144 144 # Actor system parameters
145 145 actors:
... ...
  1 +/**
  2 + * Copyright © 2016 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.mqtt.rpc;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.eclipse.paho.client.mqttv3.*;
  20 +import org.junit.Assert;
  21 +import org.junit.Before;
  22 +import org.junit.Test;
  23 +import org.thingsboard.client.tools.RestClient;
  24 +import org.thingsboard.server.common.data.Device;
  25 +import org.thingsboard.server.common.data.security.DeviceCredentials;
  26 +import org.thingsboard.server.mqtt.AbstractFeatureIntegrationTest;
  27 +
  28 +import static org.junit.Assert.assertEquals;
  29 +import static org.junit.Assert.assertNotNull;
  30 +
  31 +/**
  32 + * @author Valerii Sosliuk
  33 + */
  34 +@Slf4j
  35 +public class MqttServerSideRpcIntegrationTest extends AbstractFeatureIntegrationTest {
  36 +
  37 + private static final String MQTT_URL = "tcp://localhost:1883";
  38 + private static final String BASE_URL = "http://localhost:8080";
  39 +
  40 + private static final String USERNAME = "tenant@thingsboard.org";
  41 + private static final String PASSWORD = "tenant";
  42 +
  43 + private Device savedDevice;
  44 +
  45 + private String accessToken;
  46 + private RestClient restClient;
  47 +
  48 + @Before
  49 + public void beforeTest() throws Exception {
  50 + restClient = new RestClient(BASE_URL);
  51 + restClient.login(USERNAME, PASSWORD);
  52 +
  53 + Device device = new Device();
  54 + device.setName("Test Server-Side RPC Device");
  55 + savedDevice = restClient.getRestTemplate().postForEntity(BASE_URL + "/api/device", device, Device.class).getBody();
  56 + DeviceCredentials deviceCredentials =
  57 + restClient.getRestTemplate().getForEntity(BASE_URL + "/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class).getBody();
  58 + assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
  59 + accessToken = deviceCredentials.getCredentialsId();
  60 + assertNotNull(accessToken);
  61 + }
  62 +
  63 + @Test
  64 + public void testServerMqttTwoWayRpc() throws Exception {
  65 + String clientId = MqttAsyncClient.generateClientId();
  66 + MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId);
  67 +
  68 + MqttConnectOptions options = new MqttConnectOptions();
  69 + options.setUserName(accessToken);
  70 + client.connect(options);
  71 + Thread.sleep(3000);
  72 + client.subscribe("v1/devices/me/rpc/request/+",1);
  73 + client.setCallback(new TestMqttCallback(client));
  74 +
  75 + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
  76 + String deviceId = savedDevice.getId().getId().toString();
  77 + String result = restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class).getBody();
  78 + log.info("Result: " + result);
  79 + Assert.assertEquals("{\"value1\":\"A\",\"value2\":\"B\"}", result);
  80 + }
  81 +
  82 + private static class TestMqttCallback implements MqttCallback {
  83 +
  84 + private final MqttAsyncClient client;
  85 +
  86 + TestMqttCallback(MqttAsyncClient client) {
  87 + this.client = client;
  88 + }
  89 +
  90 + @Override
  91 + public void connectionLost(Throwable throwable) {
  92 + }
  93 +
  94 + @Override
  95 + public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception {
  96 + log.info("Message Arrived: " + mqttMessage.getPayload().toString());
  97 + MqttMessage message = new MqttMessage();
  98 + String responseTopic = requestTopic.replace("request", "response");
  99 + message.setPayload("{\"value1\":\"A\", \"value2\":\"B\"}".getBytes());
  100 + client.publish(responseTopic, message);
  101 + }
  102 +
  103 + @Override
  104 + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
  105 +
  106 + }
  107 + }
  108 +}
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>common</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard.common</groupId>
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>common</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard.common</groupId>
... ...
  1 +/**
  2 + * Copyright © 2016 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.msg.core;
  17 +
  18 +import lombok.ToString;
  19 +import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
  20 +import org.thingsboard.server.common.msg.session.MsgType;
  21 +import org.thingsboard.server.common.msg.session.ToDeviceMsg;
  22 +
  23 +@ToString
  24 +public class SessionCloseNotification implements ToDeviceMsg {
  25 +
  26 + private static final long serialVersionUID = 1L;
  27 +
  28 + @Override
  29 + public boolean isSuccess() {
  30 + return true;
  31 + }
  32 +
  33 + @Override
  34 + public MsgType getMsgType() {
  35 + return MsgType.SESSION_CLOSE;
  36 + }
  37 +
  38 +}
... ...
  1 +/**
  2 + * Copyright © 2016 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.msg.core;
  17 +
  18 +import org.thingsboard.server.common.msg.session.FromDeviceMsg;
  19 +import org.thingsboard.server.common.msg.session.MsgType;
  20 +
  21 +/**
  22 + * @author Andrew Shvayka
  23 + */
  24 +public class SessionOpenMsg implements FromDeviceMsg {
  25 + @Override
  26 + public MsgType getMsgType() {
  27 + return MsgType.SESSION_OPEN;
  28 + }
  29 +}
... ...
... ... @@ -28,7 +28,7 @@ public enum MsgType {
28 28
29 29 RULE_ENGINE_ERROR,
30 30
31   - SESSION_CLOSE;
  31 + SESSION_OPEN, SESSION_CLOSE;
32 32
33 33 private final boolean requiresRulesProcessing;
34 34
... ...
... ... @@ -21,11 +21,25 @@ import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
21 21 public class SessionCloseMsg implements SessionCtrlMsg {
22 22
23 23 private final SessionId sessionId;
  24 + private final boolean revoked;
24 25 private final boolean timeout;
25 26
26   - public SessionCloseMsg(SessionId sessionId, boolean timeout) {
  27 + public static SessionCloseMsg onError(SessionId sessionId) {
  28 + return new SessionCloseMsg(sessionId, false, false);
  29 + }
  30 +
  31 + public static SessionCloseMsg onTimeout(SessionId sessionId) {
  32 + return new SessionCloseMsg(sessionId, false, true);
  33 + }
  34 +
  35 + public static SessionCloseMsg onCredentialsRevoked(SessionId sessionId) {
  36 + return new SessionCloseMsg(sessionId, true, false);
  37 + }
  38 +
  39 + private SessionCloseMsg(SessionId sessionId, boolean unauthorized, boolean timeout) {
27 40 super();
28 41 this.sessionId = sessionId;
  42 + this.revoked = unauthorized;
29 43 this.timeout = timeout;
30 44 }
31 45
... ... @@ -34,6 +48,10 @@ public class SessionCloseMsg implements SessionCtrlMsg {
34 48 return sessionId;
35 49 }
36 50
  51 + public boolean isCredentialsRevoked() {
  52 + return revoked;
  53 + }
  54 +
37 55 public boolean isTimeout() {
38 56 return timeout;
39 57 }
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>thingsboard</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard</groupId>
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>common</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard.common</groupId>
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>thingsboard</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard</groupId>
... ...
... ... @@ -287,10 +287,10 @@ VALUES (
287 287 'org.thingsboard.server.extensions.core.plugin.mail.MailPlugin',
288 288 true,
289 289 '{
290   - "host": "smtp.gmail.com",
291   - "port": 587,
292   - "username": "username@gmail.com",
293   - "password": "password",
  290 + "host": "smtp.sendgrid.net",
  291 + "port": 2525,
  292 + "username": "apikey",
  293 + "password": "your_api_key",
294 294 "otherProperties": [
295 295 {
296 296 "key":"mail.smtp.auth",
... ... @@ -303,14 +303,6 @@ VALUES (
303 303 {
304 304 "key":"mail.smtp.starttls.enable",
305 305 "value":"true"
306   - },
307   - {
308   - "key":"mail.smtp.host",
309   - "value":"smtp.gmail.com"
310   - },
311   - {
312   - "key":"mail.smtp.port",
313   - "value":"587"
314 306 }
315 307 ]
316 308 }'
... ...
... ... @@ -73,6 +73,11 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'simple_card',
73 73 'Simple card' );
74 74
75 75 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
  76 +VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'label_widget',
  77 +'{"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\"}"}',
  78 +'Label widget' );
  79 +
  80 +INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
76 81 VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'speed_gauge_canvas_gauges',
77 82 '{"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\"}"}',
78 83 'Speed gauge - Canvas Gauges' );
... ... @@ -123,11 +128,16 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'radial_gauge_canvas_gau
123 128 'Radial gauge - Canvas Gauges' );
124 129
125 130 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
126   -VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'google_maps',
127   -'{"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\"}"}',
  131 +VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'google_maps',
  132 +'{"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\"}"}',
128 133 'Google Maps' );
129 134
130 135 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
  136 +VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'route_map',
  137 +'{"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\"}"}',
  138 +'Route Map' );
  139 +
  140 +INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
131 141 VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'basic_timeseries',
132 142 '{"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\"}"}',
133 143 'Timeseries - Flotr2' );
... ... @@ -194,7 +204,7 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'digital_vertical_bar',
194 204
195 205 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
196 206 VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'openstreetmap',
197   -'{"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: ''&copy; <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\"}"}',
  207 +'{"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: ''&copy; <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\"}"}',
198 208 'OpenStreetMap' );
199 209
200 210 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
... ...
... ... @@ -18,7 +18,7 @@ version: '2'
18 18
19 19 services:
20 20 thingsboard:
21   - image: "thingsboard/application:1.0"
  21 + image: "thingsboard/application:1.0.1"
22 22 ports:
23 23 - "8080:8080"
24 24 - "1883:1883"
... ... @@ -27,7 +27,7 @@ services:
27 27 - thingsboard.env
28 28 entrypoint: ./run_thingsboard.sh
29 29 thingsboard-db-schema:
30   - image: "thingsboard/thingsboard-db-schema:1.0"
  30 + image: "thingsboard/thingsboard-db-schema:1.0.1"
31 31 env_file:
32 32 - thingsboard-db-schema.env
33 33 entrypoint: ./install_schema.sh
... ...
... ... @@ -20,8 +20,8 @@ cp ../../dao/src/main/resources/schema.cql schema.cql
20 20 cp ../../dao/src/main/resources/demo-data.cql demo-data.cql
21 21 cp ../../dao/src/main/resources/system-data.cql system-data.cql
22 22
23   -docker build -t thingsboard/thingsboard-db-schema:1.0 .
  23 +docker build -t thingsboard/thingsboard-db-schema:1.0.1 .
24 24
25 25 docker login
26 26
27   -docker push thingsboard/thingsboard-db-schema:1.0
\ No newline at end of file
  27 +docker push thingsboard/thingsboard-db-schema:1.0.1
\ No newline at end of file
... ...
... ... @@ -18,8 +18,8 @@
18 18
19 19 cp ../../application/target/thingsboard.deb thingsboard.deb
20 20
21   -docker build -t thingsboard/application:1.0 .
  21 +docker build -t thingsboard/application:1.0.1 .
22 22
23 23 docker login
24 24
25   -docker push thingsboard/application:1.0
\ No newline at end of file
  25 +docker push thingsboard/application:1.0.1
\ No newline at end of file
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>thingsboard</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard</groupId>
... ...
  1 +/**
  2 + * Copyright © 2016 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.extensions.api.device;
  17 +
  18 +import lombok.Data;
  19 +import lombok.Getter;
  20 +import lombok.ToString;
  21 +import org.thingsboard.server.common.data.id.DeviceId;
  22 +import org.thingsboard.server.common.data.id.TenantId;
  23 +import org.thingsboard.server.common.data.kv.AttributeKey;
  24 +
  25 +import java.util.Set;
  26 +
  27 +/**
  28 + * @author Andrew Shvayka
  29 + */
  30 +@Data
  31 +public class DeviceCredentialsUpdateNotificationMsg implements ToDeviceActorNotificationMsg {
  32 +
  33 + private final TenantId tenantId;
  34 + private final DeviceId deviceId;
  35 +
  36 +}
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>thingsboard</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard</groupId>
... ...
... ... @@ -22,7 +22,7 @@
22 22 <modelVersion>4.0.0</modelVersion>
23 23 <parent>
24 24 <groupId>org.thingsboard</groupId>
25   - <version>1.0.1-SNAPSHOT</version>
  25 + <version>1.1.0-SNAPSHOT</version>
26 26 <artifactId>extensions</artifactId>
27 27 </parent>
28 28 <groupId>org.thingsboard.extensions</groupId>
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>extensions</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard.extensions</groupId>
... ...
... ... @@ -22,7 +22,7 @@
22 22 <modelVersion>4.0.0</modelVersion>
23 23 <parent>
24 24 <groupId>org.thingsboard</groupId>
25   - <version>1.0.1-SNAPSHOT</version>
  25 + <version>1.1.0-SNAPSHOT</version>
26 26 <artifactId>extensions</artifactId>
27 27 </parent>
28 28 <groupId>org.thingsboard.extensions</groupId>
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>thingsboard</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard</groupId>
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <groupId>org.thingsboard</groupId>
22 22 <artifactId>thingsboard</artifactId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <packaging>pom</packaging>
25 25
26 26 <name>Thingsboard</name>
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>thingsboard</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard</groupId>
... ...
  1 +restUrl=http://localhost:8080
  2 +mqttUrls=tcp://localhost:1883
  3 +deviceCount=1
  4 +durationMs=60000
  5 +iterationIntervalMs=1000
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>transport</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard.transport</groupId>
... ...
... ... @@ -36,6 +36,7 @@ import org.slf4j.Logger;
36 36 import org.slf4j.LoggerFactory;
37 37
38 38 import java.util.concurrent.atomic.AtomicInteger;
  39 +
39 40 @Slf4j
40 41 public class CoapSessionCtx extends DeviceAwareSessionContext {
41 42
... ... @@ -87,6 +88,8 @@ public class CoapSessionCtx extends DeviceAwareSessionContext {
87 88 private void onSessionClose(SessionCloseMsg msg) {
88 89 if (msg.isTimeout()) {
89 90 exchange.respond(ResponseCode.SERVICE_UNAVAILABLE);
  91 + } else if (msg.isCredentialsRevoked()) {
  92 + exchange.respond(ResponseCode.UNAUTHORIZED);
90 93 } else {
91 94 exchange.respond(ResponseCode.INTERNAL_SERVER_ERROR);
92 95 }
... ... @@ -120,7 +123,7 @@ public class CoapSessionCtx extends DeviceAwareSessionContext {
120 123
121 124 public void close() {
122 125 log.info("[{}] Closing processing context. Timeout: {}", sessionId, exchange.advanced().isTimedOut());
123   - processor.process(new SessionCloseMsg(sessionId, exchange.advanced().isTimedOut()));
  126 + processor.process(exchange.advanced().isTimedOut() ? SessionCloseMsg.onTimeout(sessionId) : SessionCloseMsg.onError(sessionId));
124 127 }
125 128
126 129 @Override
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>transport</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard.transport</groupId>
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>transport</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard.transport</groupId>
... ...
... ... @@ -210,7 +210,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
210 210 }
211 211
212 212 private void processDisconnect(ChannelHandlerContext ctx) {
213   - processor.process(new SessionCloseMsg(sessionCtx.getSessionId(), false));
214 213 ctx.close();
215 214 }
216 215
... ... @@ -255,6 +254,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
255 254
256 255 @Override
257 256 public void operationComplete(Future<? super Void> future) throws Exception {
258   - processor.process(new SessionCloseMsg(sessionCtx.getSessionId(), false));
  257 + processor.process(SessionCloseMsg.onError(sessionCtx.getSessionId()));
259 258 }
260 259 }
... ...
... ... @@ -16,12 +16,13 @@
16 16 package org.thingsboard.server.transport.mqtt.session;
17 17
18 18 import io.netty.channel.ChannelHandlerContext;
19   -import io.netty.handler.codec.mqtt.MqttMessage;
  19 +import io.netty.handler.codec.mqtt.*;
20 20 import lombok.extern.slf4j.Slf4j;
21 21 import org.thingsboard.server.common.data.id.SessionId;
22 22 import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
23 23 import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
24 24 import org.thingsboard.server.common.msg.session.SessionType;
  25 +import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
25 26 import org.thingsboard.server.common.msg.session.ex.SessionException;
26 27 import org.thingsboard.server.common.transport.SessionMsgProcessor;
27 28 import org.thingsboard.server.common.transport.adaptor.AdaptorException;
... ... @@ -75,7 +76,10 @@ public class MqttSessionCtx extends DeviceAwareSessionContext {
75 76
76 77 @Override
77 78 public void onMsg(SessionCtrlMsg msg) throws SessionException {
78   -
  79 + if (msg instanceof SessionCloseMsg) {
  80 + pushToNetwork(new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0)));
  81 + channel.close();
  82 + }
79 83 }
80 84
81 85 @Override
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>thingsboard</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard</groupId>
... ...
... ... @@ -11,5 +11,8 @@
11 11 "node_modules",
12 12 "\\.tpl\\.html$"
13 13 ]
  14 + },
  15 + "globals": {
  16 + "FileReader": true
14 17 }
15 18 }
... ...
... ... @@ -14,6 +14,7 @@
14 14 "build": "NODE_ENV=production webpack -p"
15 15 },
16 16 "dependencies": {
  17 + "@flowjs/ng-flow": "^2.7.1",
17 18 "ace-builds": "^1.2.5",
18 19 "angular": "1.5.8",
19 20 "angular-animate": "1.5.8",
... ... @@ -65,6 +66,7 @@
65 66 "react": "^15.4.1",
66 67 "react-ace": "^4.1.0",
67 68 "react-dom": "^15.4.1",
  69 + "react-dropzone": "^3.7.3",
68 70 "react-schema-form": "^0.3.1",
69 71 "react-tap-event-plugin": "^2.0.1",
70 72 "reactcss": "^1.0.9",
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>1.0.1-SNAPSHOT</version>
  23 + <version>1.1.0-SNAPSHOT</version>
24 24 <artifactId>thingsboard</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard</groupId>
... ...
... ... @@ -42,6 +42,7 @@ import 'react-dom';
42 42 import 'material-ui';
43 43 import 'react-schema-form';
44 44 import react from 'ngreact';
  45 +import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
45 46
46 47 import thingsboardLogin from './login';
47 48 import thingsboardDialogs from './components/datakey-config-dialog.controller';
... ... @@ -88,6 +89,7 @@ angular.module('thingsboard', [
88 89 'angular-carousel',
89 90 'ngclipboard',
90 91 react.name,
  92 + 'flow',
91 93 thingsboardLogin,
92 94 thingsboardDialogs,
93 95 thingsboardMenu,
... ...
... ... @@ -14,9 +14,12 @@
14 14 * limitations under the License.
15 15 */
16 16
  17 +import Flow from '@flowjs/ng-flow/dist/ng-flow-standalone.min';
  18 +
17 19 /*@ngInject*/
18 20 export default function AppRun($rootScope, $window, $log, $state, $mdDialog, $filter, loginService, userService, $translate) {
19 21
  22 + $window.Flow = Flow;
20 23 var frame = $window.frameElement;
21 24 var unauthorizedDialog = null;
22 25 var forbiddenDialog = null;
... ...
... ... @@ -51,6 +51,7 @@ function Dashboard() {
51 51 widgets: '=',
52 52 deviceAliasList: '=',
53 53 columns: '=',
  54 + margins: '=',
54 55 isEdit: '=',
55 56 isMobile: '=',
56 57 isMobileDisabled: '=?',
... ... @@ -61,7 +62,8 @@ function Dashboard() {
61 62 onWidgetClicked: '&?',
62 63 loadWidgets: '&?',
63 64 onInit: '&?',
64   - onInitFailed: '&?'
  65 + onInitFailed: '&?',
  66 + dashboardStyle: '=?'
65 67 },
66 68 controller: DashboardController,
67 69 controllerAs: 'vm',
... ... @@ -108,7 +110,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
108 110 },
109 111 isMobile: vm.isMobileDisabled ? false : vm.isMobile,
110 112 mobileBreakPoint: vm.isMobileDisabled ? 0 : (vm.isMobile ? 20000 : 960),
111   - margins: [10, 10],
  113 + margins: vm.margins ? vm.margins : [10, 10],
112 114 saveGridItemCalculatedHeightInMobile: true
113 115 };
114 116
... ... @@ -159,6 +161,20 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
159 161
160 162 $scope.$watch('vm.columns', function () {
161 163 vm.gridsterOpts.columns = vm.columns ? vm.columns : 24;
  164 + if (gridster) {
  165 + gridster.columns = vm.columns;
  166 + updateGridsterParams();
  167 + }
  168 + updateVisibleRect();
  169 + });
  170 +
  171 + $scope.$watch('vm.margins', function () {
  172 + vm.gridsterOpts.margins = vm.margins ? vm.margins : [10, 10];
  173 + if (gridster) {
  174 + gridster.margins = vm.margins;
  175 + updateGridsterParams();
  176 + }
  177 + updateVisibleRect();
162 178 });
163 179
164 180 $scope.$watch('vm.isEdit', function () {
... ... @@ -224,6 +240,26 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
224 240 }, 0, false);
225 241 }
226 242
  243 + function updateGridsterParams() {
  244 + if (gridster) {
  245 + if (gridster.colWidth === 'auto') {
  246 + gridster.curColWidth = (gridster.curWidth + (gridster.outerMargin ? -gridster.margins[1] : gridster.margins[1])) / gridster.columns;
  247 + } else {
  248 + gridster.curColWidth = gridster.colWidth;
  249 + }
  250 + gridster.curRowHeight = gridster.rowHeight;
  251 + if (angular.isString(gridster.rowHeight)) {
  252 + if (gridster.rowHeight === 'match') {
  253 + gridster.curRowHeight = Math.round(gridster.curColWidth);
  254 + } else if (gridster.rowHeight.indexOf('*') !== -1) {
  255 + gridster.curRowHeight = Math.round(gridster.curColWidth * gridster.rowHeight.replace('*', '').replace(' ', ''));
  256 + } else if (gridster.rowHeight.indexOf('/') !== -1) {
  257 + gridster.curRowHeight = Math.round(gridster.curColWidth / gridster.rowHeight.replace('/', '').replace(' ', ''));
  258 + }
  259 + }
  260 + }
  261 + }
  262 +
227 263 function updateVisibleRect (force, containerResized) {
228 264 if (gridster) {
229 265 var position = $(gridster.$element).position()
... ...
... ... @@ -20,61 +20,63 @@
20 20 <md-progress-circular md-mode="indeterminate" class="md-warn" md-diameter="100"></md-progress-circular>
21 21 </md-content>
22 22 <md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap>
23   - <div id="gridster-child" gridster="vm.gridsterOpts">
24   - <ul>
25   -<!-- ng-click="widgetClicked($event, widget)" -->
26   - <li gridster-item="widget" ng-repeat="widget in vm.widgets">
27   - <div tb-expand-fullscreen expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp"
28   - ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}"
29   - tb-mousedown="vm.widgetMouseDown($event, widget)"
30   - tb-mousemove="vm.widgetMouseMove($event, widget)"
31   - tb-mouseup="vm.widgetMouseUp($event, widget)"
32   - style="
33   - cursor: pointer;
34   - color: {{vm.widgetColor(widget)}};
35   - background-color: {{vm.widgetBackgroundColor(widget)}};
36   - padding: {{vm.widgetPadding(widget)}}
37   - ">
38   - <div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
39   - <span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span>
40   - <tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
41   - </div>
42   - <div class="tb-widget-actions" layout="row" layout-align="start center">
43   - <md-button id="expand-button"
44   - aria-label="{{ 'fullscreen.fullscreen' | translate }}"
45   - class="md-icon-button md-primary"></md-button>
46   - <md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
47   - ng-disabled="vm.loading()"
48   - class="md-icon-button md-primary"
49   - ng-click="vm.editWidget($event, widget)"
50   - aria-label="{{ 'widget.edit' | translate }}">
51   - <md-tooltip md-direction="top">
52   - {{ 'widget.edit' | translate }}
53   - </md-tooltip>
54   - <md-icon class="material-icons">
55   - edit
56   - </md-icon>
57   - </md-button>
58   - <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
59   - ng-disabled="vm.loading()"
60   - class="md-icon-button md-primary"
61   - ng-click="vm.removeWidget($event, widget)"
62   - aria-label="{{ 'widget.remove' | translate }}">
63   - <md-tooltip md-direction="top">
64   - {{ 'widget.remove' | translate }}
65   - </md-tooltip>
66   - <md-icon class="material-icons">
67   - close
68   - </md-icon>
69   - </md-button>
70   - </div>
71   - <div flex layout="column" class="tb-widget-content">
72   - <div flex tb-widget
73   - locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
  23 + <div ng-style="vm.dashboardStyle" id="gridster-background" style="height: auto; min-height: 100%;">
  24 + <div id="gridster-child" gridster="vm.gridsterOpts">
  25 + <ul>
  26 + <!-- ng-click="widgetClicked($event, widget)" -->
  27 + <li gridster-item="widget" ng-repeat="widget in vm.widgets">
  28 + <div tb-expand-fullscreen expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp"
  29 + ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}"
  30 + tb-mousedown="vm.widgetMouseDown($event, widget)"
  31 + tb-mousemove="vm.widgetMouseMove($event, widget)"
  32 + tb-mouseup="vm.widgetMouseUp($event, widget)"
  33 + style="
  34 + cursor: pointer;
  35 + color: {{vm.widgetColor(widget)}};
  36 + background-color: {{vm.widgetBackgroundColor(widget)}};
  37 + padding: {{vm.widgetPadding(widget)}}
  38 + ">
  39 + <div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
  40 + <span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span>
  41 + <tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
74 42 </div>
75   - </div>
76   - </div>
77   - </li>
78   - </ul>
  43 + <div class="tb-widget-actions" layout="row" layout-align="start center">
  44 + <md-button id="expand-button"
  45 + aria-label="{{ 'fullscreen.fullscreen' | translate }}"
  46 + class="md-icon-button md-primary"></md-button>
  47 + <md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
  48 + ng-disabled="vm.loading()"
  49 + class="md-icon-button md-primary"
  50 + ng-click="vm.editWidget($event, widget)"
  51 + aria-label="{{ 'widget.edit' | translate }}">
  52 + <md-tooltip md-direction="top">
  53 + {{ 'widget.edit' | translate }}
  54 + </md-tooltip>
  55 + <md-icon class="material-icons">
  56 + edit
  57 + </md-icon>
  58 + </md-button>
  59 + <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
  60 + ng-disabled="vm.loading()"
  61 + class="md-icon-button md-primary"
  62 + ng-click="vm.removeWidget($event, widget)"
  63 + aria-label="{{ 'widget.remove' | translate }}">
  64 + <md-tooltip md-direction="top">
  65 + {{ 'widget.remove' | translate }}
  66 + </md-tooltip>
  67 + <md-icon class="material-icons">
  68 + close
  69 + </md-icon>
  70 + </md-button>
  71 + </div>
  72 + <div flex layout="column" class="tb-widget-content">
  73 + <div flex tb-widget
  74 + locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
  75 + </div>
  76 + </div>
  77 + </div>
  78 + </li>
  79 + </ul>
  80 + </div>
79 81 </div>
80 82 </md-content>
\ No newline at end of file
... ...
  1 +/*
  2 + * Copyright © 2016 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import './json-form-image.scss';
  18 +
  19 +import React from 'react';
  20 +import ThingsboardBaseComponent from './json-form-base-component.jsx';
  21 +import Dropzone from 'react-dropzone';
  22 +import IconButton from 'material-ui/IconButton';
  23 +
  24 +class ThingsboardImage extends React.Component {
  25 +
  26 + constructor(props) {
  27 + super(props);
  28 + this.onValueChanged = this.onValueChanged.bind(this);
  29 + this.onDrop = this.onDrop.bind(this);
  30 + this.onClear = this.onClear.bind(this);
  31 + var value = props.value ? props.value + '' : null;
  32 + this.state = {
  33 + imageUrl: value
  34 + };
  35 + }
  36 +
  37 + onValueChanged(value) {
  38 + this.setState({
  39 + imageUrl: value
  40 + });
  41 + this.props.onChangeValidate({
  42 + target: {
  43 + value: value
  44 + }
  45 + });
  46 + }
  47 +
  48 + onDrop(files) {
  49 + var reader = new FileReader();
  50 + reader.onload = (function(tImg) {
  51 + return function(event) {
  52 + tImg.onValueChanged(event.target.result);
  53 + };
  54 + })(this);
  55 + reader.readAsDataURL(files[0]);
  56 + }
  57 +
  58 + onClear(event) {
  59 + if (event) {
  60 + event.stopPropagation();
  61 + }
  62 + this.onValueChanged(null);
  63 + }
  64 +
  65 + render() {
  66 +
  67 + var labelClass = "tb-label";
  68 + if (this.props.form.required) {
  69 + labelClass += " tb-required";
  70 + }
  71 + if (this.props.form.readonly) {
  72 + labelClass += " tb-readonly";
  73 + }
  74 + if (this.state.focused) {
  75 + labelClass += " tb-focused";
  76 + }
  77 +
  78 + var previewComponent;
  79 + if (this.state.imageUrl) {
  80 + previewComponent = <img className="tb-image-preview" src={this.state.imageUrl} />;
  81 + } else {
  82 + previewComponent = <div>No image selected</div>;
  83 + }
  84 +
  85 + return (
  86 + <div className="tb-container">
  87 + <label className={labelClass}>{this.props.form.title}</label>
  88 + <div className="tb-image-select-container">
  89 + <div className="tb-image-preview-container">{previewComponent}</div>
  90 + <div className="tb-image-clear-container">
  91 + <IconButton className="tb-image-clear-btn" iconClassName="material-icons" tooltip="Clear" onTouchTap={this.onClear}>clear</IconButton>
  92 + </div>
  93 + <Dropzone className="tb-dropzone"
  94 + onDrop={this.onDrop}
  95 + multiple={false}
  96 + accept="image/*">
  97 + <div>Drop an image or click to select a file to upload.</div>
  98 + </Dropzone>
  99 + </div>
  100 + </div>
  101 + );
  102 + }
  103 +}
  104 +
  105 +export default ThingsboardBaseComponent(ThingsboardImage);
\ No newline at end of file
... ...
  1 +/**
  2 + * Copyright © 2016 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +$previewSize: 100px;
  18 +
  19 +.tb-image-select-container {
  20 + position: relative;
  21 + height: $previewSize;
  22 + width: 100%;
  23 +}
  24 +
  25 +.tb-image-preview {
  26 + max-width: $previewSize;
  27 + max-height: $previewSize;
  28 + width: 100%;
  29 + height: 100%;
  30 +}
  31 +
  32 +.tb-image-preview-container {
  33 + position: relative;
  34 + width: $previewSize;
  35 + height: $previewSize;
  36 + margin-right: 12px;
  37 + border: solid 1px;
  38 + vertical-align: top;
  39 + float: left;
  40 + div {
  41 + width: 100%;
  42 + font-size: 18px;
  43 + text-align: center;
  44 + position: absolute;
  45 + top: 50%;
  46 + left: 50%;
  47 + transform: translate(-50%,-50%);
  48 + }
  49 +}
  50 +
  51 +.tb-dropzone {
  52 + position: relative;
  53 + border: dashed 2px;
  54 + height: $previewSize;
  55 + vertical-align: top;
  56 + padding: 0 8px;
  57 + overflow: hidden;
  58 + div {
  59 + width: 100%;
  60 + font-size: 24px;
  61 + text-align: center;
  62 + position: absolute;
  63 + top: 50%;
  64 + left: 50%;
  65 + transform: translate(-50%,-50%);
  66 + }
  67 +}
  68 +
  69 +.tb-image-clear-container {
  70 + width: 48px;
  71 + height: $previewSize;
  72 + position: relative;
  73 + float: right;
  74 +}
  75 +.tb-image-clear-btn {
  76 + position: absolute !important;
  77 + top: 50%;
  78 + transform: translate(0%,-50%) !important;
  79 +}
... ...
... ... @@ -26,6 +26,7 @@ import ThingsboardText from './json-form-text.jsx';
26 26 import Select from 'react-schema-form/lib/Select';
27 27 import Radios from 'react-schema-form/lib/Radios';
28 28 import ThingsboardDate from './json-form-date.jsx';
  29 +import ThingsboardImage from './json-form-image.jsx';
29 30 import ThingsboardCheckbox from './json-form-checkbox.jsx';
30 31 import Help from 'react-schema-form/lib/Help';
31 32 import ThingsboardFieldSet from './json-form-fieldset.jsx';
... ... @@ -45,6 +46,7 @@ class ThingsboardSchemaForm extends React.Component {
45 46 'select': Select,
46 47 'radios': Radios,
47 48 'date': ThingsboardDate,
  49 + 'image': ThingsboardImage,
48 50 'checkbox': ThingsboardCheckbox,
49 51 'help': Help,
50 52 'array': ThingsboardArray,
... ...
... ... @@ -159,6 +159,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
159 159 };
160 160
161 161 vm.gridsterItemInitialized = gridsterItemInitialized;
  162 + vm.visibleRectChanged = visibleRectChanged;
162 163
163 164 function gridsterItemInitialized(item) {
164 165 if (item) {
... ... @@ -167,6 +168,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
167 168 }
168 169 }
169 170
  171 + function visibleRectChanged(newVisibleRect) {
  172 + visibleRect = newVisibleRect;
  173 + updateVisibility();
  174 + }
  175 +
170 176 initWidget();
171 177
172 178 function initWidget() {
... ... @@ -221,11 +227,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
221 227 $scope.$emit("widgetPositionChanged", widget);
222 228 });
223 229
224   - $scope.$on('visibleRectChanged', function (event, newVisibleRect) {
225   - visibleRect = newVisibleRect;
226   - updateVisibility();
227   - });
228   -
229 230 $scope.$on('onWidgetFullscreenChanged', function(event, isWidgetExpanded, fullscreenWidget) {
230 231 if (widget === fullscreenWidget) {
231 232 onRedraw(0);
... ... @@ -318,9 +319,10 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
318 319
319 320
320 321 function onRedraw(delay, dataUpdate) {
321   - if (!visible) {
  322 + //TODO:
  323 + /*if (!visible) {
322 324 return;
323   - }
  325 + }*/
324 326 if (angular.isUndefined(delay)) {
325 327 delay = 0;
326 328 }
... ...
... ... @@ -34,12 +34,19 @@ function Widget($controller, $compile, widgetService) {
34 34 var widget = locals.widget;
35 35 var gridsterItem;
36 36
  37 + scope.$on('visibleRectChanged', function (event, newVisibleRect) {
  38 + locals.visibleRect = newVisibleRect;
  39 + if (widgetController) {
  40 + widgetController.visibleRectChanged(newVisibleRect);
  41 + }
  42 + });
  43 +
37 44 scope.$on('gridster-item-initialized', function (event, item) {
38 45 gridsterItem = item;
39 46 if (widgetController) {
40 47 widgetController.gridsterItemInitialized(gridsterItem);
41 48 }
42   - })
  49 + });
43 50
44 51 elem.html('<div flex layout="column" layout-align="center center" style="height: 100%;">' +
45 52 ' <md-progress-circular md-mode="indeterminate" class="md-accent md-hue-2" md-diameter="120"></md-progress-circular>' +
... ...
  1 +/*
  2 + * Copyright © 2016 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import './dashboard-settings.scss';
  18 +
  19 +/*@ngInject*/
  20 +export default function DashboardSettingsController($scope, $mdDialog, gridSettings) {
  21 +
  22 + var vm = this;
  23 +
  24 + vm.cancel = cancel;
  25 + vm.save = save;
  26 + vm.imageAdded = imageAdded;
  27 + vm.clearImage = clearImage;
  28 +
  29 + vm.gridSettings = gridSettings || {};
  30 +
  31 + vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)';
  32 + vm.gridSettings.columns = vm.gridSettings.columns || 24;
  33 + vm.gridSettings.margins = vm.gridSettings.margins || [10, 10];
  34 + vm.hMargin = vm.gridSettings.margins[0];
  35 + vm.vMargin = vm.gridSettings.margins[1];
  36 +
  37 + function cancel() {
  38 + $mdDialog.cancel();
  39 + }
  40 +
  41 + function imageAdded($file) {
  42 + var reader = new FileReader();
  43 + reader.onload = function(event) {
  44 + $scope.$apply(function() {
  45 + if (event.target.result && event.target.result.startsWith('data:image/')) {
  46 + $scope.theForm.$setDirty();
  47 + vm.gridSettings.backgroundImageUrl = event.target.result;
  48 + }
  49 + });
  50 + };
  51 + reader.readAsDataURL($file.file);
  52 + }
  53 +
  54 + function clearImage() {
  55 + $scope.theForm.$setDirty();
  56 + vm.gridSettings.backgroundImageUrl = null;
  57 + }
  58 +
  59 + function save() {
  60 + $scope.theForm.$setPristine();
  61 + vm.gridSettings.margins = [vm.hMargin, vm.vMargin];
  62 + $mdDialog.hide(vm.gridSettings);
  63 + }
  64 +}
... ...
  1 +/**
  2 + * Copyright © 2016 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +$previewSize: 100px;
  17 +
  18 +.file-input {
  19 + display: none;
  20 +}
  21 +
  22 +.tb-container {
  23 + position: relative;
  24 + margin-top: 32px;
  25 + padding: 10px 0;
  26 +}
  27 +
  28 +.tb-image-select-container {
  29 + position: relative;
  30 + height: $previewSize;
  31 + width: 100%;
  32 +}
  33 +
  34 +.tb-image-preview {
  35 + max-width: $previewSize;
  36 + max-height: $previewSize;
  37 + width: auto;
  38 + height: auto;
  39 +}
  40 +
  41 +.tb-image-preview-container {
  42 + position: relative;
  43 + width: $previewSize;
  44 + height: $previewSize;
  45 + margin-right: 12px;
  46 + border: solid 1px;
  47 + vertical-align: top;
  48 + float: left;
  49 + div {
  50 + width: 100%;
  51 + font-size: 18px;
  52 + text-align: center;
  53 + }
  54 + div, .tb-image-preview {
  55 + position: absolute;
  56 + top: 50%;
  57 + left: 50%;
  58 + transform: translate(-50%,-50%);
  59 + }
  60 +}
  61 +
  62 +.tb-flow-drop {
  63 + position: relative;
  64 + border: dashed 2px;
  65 + height: $previewSize;
  66 + vertical-align: top;
  67 + padding: 0 8px;
  68 + overflow: hidden;
  69 + min-width: 300px;
  70 + label {
  71 + width: 100%;
  72 + font-size: 24px;
  73 + text-align: center;
  74 + position: absolute;
  75 + top: 50%;
  76 + left: 50%;
  77 + transform: translate(-50%,-50%);
  78 + }
  79 +}
  80 +
  81 +.tb-image-clear-container {
  82 + width: 48px;
  83 + height: $previewSize;
  84 + position: relative;
  85 + float: right;
  86 +}
  87 +.tb-image-clear-btn {
  88 + position: absolute !important;
  89 + top: 50%;
  90 + transform: translate(0%,-50%) !important;
  91 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-dialog aria-label="{{ 'dashboard.settings' | translate }}">
  19 + <form name="theForm" ng-submit="vm.save()">
  20 + <md-toolbar>
  21 + <div class="md-toolbar-tools">
  22 + <h2 translate>dashboard.settings</h2>
  23 + <span flex></span>
  24 + <md-button class="md-icon-button" ng-click="vm.cancel()">
  25 + <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
  26 + </md-button>
  27 + </div>
  28 + </md-toolbar>
  29 + <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
  30 + <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
  31 + <md-dialog-content>
  32 + <div class="md-dialog-content">
  33 + <fieldset ng-disabled="loading">
  34 + <md-input-container class="md-block">
  35 + <label translate>dashboard.columns-count</label>
  36 + <input required type="number" step="any" name="columns" ng-model="vm.gridSettings.columns" min="10"
  37 + max="1000" />
  38 + <div ng-messages="theForm.columns.$error" multiple md-auto-hide="false">
  39 + <div ng-message="required" translate>dashboard.columns-count-required</div>
  40 + <div ng-message="min" translate>dashboard.min-columns-count-message</div>
  41 + <div ng-message="max">dashboard.max-columns-count-message</div>
  42 + </div>
  43 + </md-input-container>
  44 + <small translate>dashboard.widgets-margins</small>
  45 + <div flex layout="row">
  46 + <md-input-container flex class="md-block">
  47 + <label translate>dashboard.horizontal-margin</label>
  48 + <input required type="number" step="any" name="hMargin" ng-model="vm.hMargin" min="0"
  49 + max="50" />
  50 + <div ng-messages="theForm.hMargin.$error" multiple md-auto-hide="false">
  51 + <div ng-message="required" translate>dashboard.horizontal-margin-required</div>
  52 + <div ng-message="min" translate>dashboard.min-horizontal-margin-message</div>
  53 + <div ng-message="max" translate>dashboard.max-horizontal-margin-message</div>
  54 + </div>
  55 + </md-input-container>
  56 + <md-input-container flex class="md-block">
  57 + <label translate>dashboard.vertical-margin</label>
  58 + <input required type="number" step="any" name="vMargin" ng-model="vm.vMargin" min="0"
  59 + max="50" />
  60 + <div ng-messages="theForm.vMargin.$error" multiple md-auto-hide="false">
  61 + <div ng-message="required" translate>dashboard.vertical-margin-required</div>
  62 + <div ng-message="min" translate>dashboard.min-vertical-margin-message</div>
  63 + <div ng-message="max" translate>dashboard.max-vertical-margin-message</div>
  64 + </div>
  65 + </md-input-container>
  66 + </div>
  67 + <div flex
  68 + ng-required="false"
  69 + md-color-picker
  70 + ng-model="vm.gridSettings.backgroundColor"
  71 + label="{{ 'dashboard.background-color' | translate }}"
  72 + icon="format_color_fill"
  73 + default="rgba(0,0,0,0)"
  74 + md-color-clear-button="false"
  75 + open-on-input="true"
  76 + md-color-generic-palette="false"
  77 + md-color-history="false"
  78 + ></div>
  79 + <div class="tb-container">
  80 + <label class="tb-label" translate>dashboard.background-image</label>
  81 + <div flow-init="{singleFile:true}"
  82 + flow-file-added="vm.imageAdded( $file )" class="tb-image-select-container">
  83 + <div class="tb-image-preview-container">
  84 + <div ng-show="!vm.gridSettings.backgroundImageUrl" translate>dashboard.no-image</div>
  85 + <img ng-show="vm.gridSettings.backgroundImageUrl" class="tb-image-preview" src="{{vm.gridSettings.backgroundImageUrl}}" />
  86 + </div>
  87 + <div class="tb-image-clear-container">
  88 + <md-button ng-click="vm.clearImage()"
  89 + class="tb-image-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
  90 + <md-tooltip md-direction="top">
  91 + {{ 'action.remove' | translate }}
  92 + </md-tooltip>
  93 + <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">
  94 + close
  95 + </md-icon>
  96 + </md-button>
  97 + </div>
  98 + <div class="alert tb-flow-drop" flow-drop>
  99 + <label for="select" translate>dashboard.drop-image</label>
  100 + <input class="file-input" flow-btn flow-attrs="{accept:'image/*'}" id="select">
  101 + </div>
  102 + </div>
  103 + </div>
  104 + </fieldset>
  105 + </div>
  106 + </md-dialog-content>
  107 + <md-dialog-actions layout="row">
  108 + <span flex></span>
  109 + <md-button ng-disabled="loading || !theForm.$dirty || !theForm.$valid" type="submit" class="md-raised md-primary">
  110 + {{ 'action.save' | translate }}
  111 + </md-button>
  112 + <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
  113 + </md-dialog-actions>
  114 + </form>
  115 +</md-dialog>
... ...
... ... @@ -16,6 +16,7 @@
16 16 /* eslint-disable import/no-unresolved, import/default */
17 17
18 18 import deviceAliasesTemplate from './device-aliases.tpl.html';
  19 +import dashboardBackgroundTemplate from './dashboard-settings.tpl.html';
19 20 import addWidgetTemplate from './add-widget.tpl.html';
20 21
21 22 /* eslint-enable import/no-unresolved, import/default */
... ... @@ -55,6 +56,7 @@ export default function DashboardController(types, widgetService, userService,
55 56 vm.onAddWidgetClosed = onAddWidgetClosed;
56 57 vm.onEditWidgetClosed = onEditWidgetClosed;
57 58 vm.openDeviceAliases = openDeviceAliases;
  59 + vm.openDashboardSettings = openDashboardSettings;
58 60 vm.removeWidget = removeWidget;
59 61 vm.saveDashboard = saveDashboard;
60 62 vm.saveWidget = saveWidget;
... ... @@ -252,6 +254,24 @@ export default function DashboardController(types, widgetService, userService,
252 254 });
253 255 }
254 256
  257 + function openDashboardSettings($event) {
  258 + $mdDialog.show({
  259 + controller: 'DashboardSettingsController',
  260 + controllerAs: 'vm',
  261 + templateUrl: dashboardBackgroundTemplate,
  262 + locals: {
  263 + gridSettings: angular.copy(vm.dashboard.configuration.gridSettings)
  264 + },
  265 + parent: angular.element($document[0].body),
  266 + skipHide: true,
  267 + fullscreen: true,
  268 + targetEvent: $event
  269 + }).then(function (gridSettings) {
  270 + vm.dashboard.configuration.gridSettings = gridSettings;
  271 + }, function () {
  272 + });
  273 + }
  274 +
255 275 function editWidget($event, widget) {
256 276 $event.stopPropagation();
257 277 var newEditingIndex = vm.widgets.indexOf(widget);
... ... @@ -368,6 +388,15 @@ export default function DashboardController(types, widgetService, userService,
368 388 w.triggerHandler('resize');
369 389 }
370 390 }).then(function (widget) {
  391 + var columns = 24;
  392 + if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) {
  393 + columns = vm.dashboard.configuration.gridSettings.columns;
  394 + }
  395 + if (columns != 24) {
  396 + var ratio = columns / 24;
  397 + widget.sizeX *= ratio;
  398 + widget.sizeY *= ratio;
  399 + }
371 400 vm.widgets.push(widget);
372 401 }, function () {
373 402 });
... ...
... ... @@ -59,10 +59,22 @@
59 59 <md-button class="md-raised" flex="none" ng-show="vm.isEdit" ng-click="vm.openDeviceAliases($event)">
60 60 {{ 'device.aliases' | translate }}
61 61 </md-button>
  62 + <md-button class="md-raised" flex="none" ng-show="vm.isEdit" ng-click="vm.openDashboardSettings($event)">
  63 + {{ 'dashboard.settings' | translate }}
  64 + </md-button>
62 65 </section>
63   - <div class="tb-absolute-fill" ng-class="{ 'tb-padded' : !vm.widgetEditMode, 'tb-shrinked' : vm.isEditingWidget }">
  66 + <div class="tb-absolute-fill"
  67 + ng-class="{ 'tb-padded' : !vm.widgetEditMode, 'tb-shrinked' : vm.isEditingWidget }">
64 68 <tb-dashboard
  69 + dashboard-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,
  70 + 'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',
  71 + 'background-repeat': 'no-repeat',
  72 + 'background-attachment': 'scroll',
  73 + 'background-size': '100%',
  74 + 'background-position': '0% 0%'}"
65 75 widgets="vm.widgets"
  76 + columns="vm.dashboard.configuration.gridSettings.columns"
  77 + margins="vm.dashboard.configuration.gridSettings.margins"
66 78 device-alias-list="vm.dashboard.configuration.deviceAliases"
67 79 is-edit="vm.isEdit || vm.widgetEditMode"
68 80 is-mobile="vm.forceDashboardMobileMode"
... ...
... ... @@ -34,6 +34,7 @@ import DashboardRoutes from './dashboard.routes';
34 34 import DashboardsController from './dashboards.controller';
35 35 import DashboardController from './dashboard.controller';
36 36 import DeviceAliasesController from './device-aliases.controller';
  37 +import DashboardSettingsController from './dashboard-settings.controller';
37 38 import AssignDashboardToCustomerController from './assign-to-customer.controller';
38 39 import AddDashboardsToCustomerController from './add-dashboards-to-customer.controller';
39 40 import AddWidgetController from './add-widget.controller';
... ... @@ -59,6 +60,7 @@ export default angular.module('thingsboard.dashboard', [
59 60 .controller('DashboardsController', DashboardsController)
60 61 .controller('DashboardController', DashboardController)
61 62 .controller('DeviceAliasesController', DeviceAliasesController)
  63 + .controller('DashboardSettingsController', DashboardSettingsController)
62 64 .controller('AssignDashboardToCustomerController', AssignDashboardToCustomerController)
63 65 .controller('AddDashboardsToCustomerController', AddDashboardsToCustomerController)
64 66 .controller('AddWidgetController', AddWidgetController)
... ...
... ... @@ -36,6 +36,13 @@
36 36 <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
37 37 <span translate>device.copyId</span>
38 38 </md-button>
  39 + <md-button ngclipboard data-clipboard-action="copy"
  40 + ngclipboard-success="onAccessTokenCopied(e)"
  41 + data-clipboard-text="{{deviceCredentials.credentialsId}}" ng-show="!isEdit"
  42 + class="md-raised">
  43 + <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
  44 + <span translate>device.copyAccessToken</span>
  45 + </md-button>
39 46 </div>
40 47
41 48 <md-content class="md-padding" layout="column">
... ...
... ... @@ -20,18 +20,23 @@ import deviceFieldsetTemplate from './device-fieldset.tpl.html';
20 20 /* eslint-enable import/no-unresolved, import/default */
21 21
22 22 /*@ngInject*/
23   -export default function DeviceDirective($compile, $templateCache, toast, $translate, types, customerService) {
  23 +export default function DeviceDirective($compile, $templateCache, toast, $translate, types, deviceService, customerService) {
24 24 var linker = function (scope, element) {
25 25 var template = $templateCache.get(deviceFieldsetTemplate);
26 26 element.html(template);
27 27
28 28 scope.isAssignedToCustomer = false;
29   -
30 29 scope.assignedCustomer = null;
31 30
  31 + scope.deviceCredentials = null;
32 32
33 33 scope.$watch('device', function(newVal) {
34 34 if (newVal) {
  35 + deviceService.getDeviceCredentials(scope.device.id.id).then(
  36 + function success(credentials) {
  37 + scope.deviceCredentials = credentials;
  38 + }
  39 + );
35 40 if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
36 41 scope.isAssignedToCustomer = true;
37 42 customerService.getCustomer(scope.device.customerId.id).then(
... ... @@ -50,6 +55,10 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
50 55 toast.showSuccess($translate.instant('device.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
51 56 };
52 57
  58 + scope.onAccessTokenCopied = function() {
  59 + toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
  60 + };
  61 +
53 62 $compile(element.contents())(scope);
54 63 }
55 64 return {
... ...
... ... @@ -192,7 +192,26 @@
192 192 "select-existing": "Select existing dashboard",
193 193 "create-new": "Create new dashboard",
194 194 "new-dashboard-title": "New dashboard title",
195   - "open-dashboard": "Open dashboard"
  195 + "open-dashboard": "Open dashboard",
  196 + "set-background": "Set background",
  197 + "background-color": "Background color",
  198 + "background-image": "Background image",
  199 + "no-image": "No image selected",
  200 + "drop-image": "Drop an image or click to select a file to upload.",
  201 + "settings": "Settings",
  202 + "columns-count": "Columns count",
  203 + "columns-count-required": "Columns count is required.",
  204 + "min-columns-count-message": "Only 10 minimum column count is allowed.",
  205 + "max-columns-count-message": "Only 1000 maximum column count is allowed.",
  206 + "widgets-margins": "Margin between widgets",
  207 + "horizontal-margin": "Horizontal margin",
  208 + "horizontal-margin-required": "Horizontal margin value is required.",
  209 + "min-horizontal-margin-message": "Only 0 is allowed as minimum horizontal margin value.",
  210 + "max-horizontal-margin-message": "Only 50 is allowed as maximum horizontal margin value.",
  211 + "vertical-margin": "Vertical margin",
  212 + "vertical-margin-required": "Vertical margin value is required.",
  213 + "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.",
  214 + "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value."
196 215 },
197 216 "datakey": {
198 217 "settings": "Settings",
... ... @@ -280,7 +299,9 @@
280 299 "events": "Events",
281 300 "details": "Details",
282 301 "copyId": "Copy device Id",
  302 + "copyAccessToken": "Copy access token",
283 303 "idCopiedMessage": "Device Id has been copied to clipboard",
  304 + "accessTokenCopiedMessage": "Device access token has been copied to clipboard",
284 305 "assignedToCustomer": "Assigned to customer",
285 306 "unable-delete-device-alias-title": "Unable to delete device alias",
286 307 "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}"
... ...