Commit 37407c4214bdc0462288976cf07e1623a1e2cda6

Authored by ShvaykaD
2 parents d3e16ad6 1f9b4c09

Merge branch 'master' of github.com:thingsboard/thingsboard into fix/coap-transport/obesrve-sessions

Showing 73 changed files with 2550 additions and 488 deletions
@@ -782,15 +782,17 @@ public class DeviceController extends BaseController { @@ -782,15 +782,17 @@ public class DeviceController extends BaseController {
782 } 782 }
783 783
784 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 784 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
785 - @RequestMapping(value = "/devices/count/{otaPackageType}", method = RequestMethod.GET) 785 + @RequestMapping(value = "/devices/count/{otaPackageType}/{deviceProfileId}", method = RequestMethod.GET)
786 @ResponseBody 786 @ResponseBody
787 - public Long countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(@PathVariable("otaPackageType") String otaPackageType,  
788 - @RequestParam String deviceProfileId) throws ThingsboardException { 787 + public Long countByDeviceProfileAndEmptyOtaPackage(@PathVariable("otaPackageType") String otaPackageType,
  788 + @PathVariable("deviceProfileId") String deviceProfileId) throws ThingsboardException {
789 checkParameter("OtaPackageType", otaPackageType); 789 checkParameter("OtaPackageType", otaPackageType);
790 checkParameter("DeviceProfileId", deviceProfileId); 790 checkParameter("DeviceProfileId", deviceProfileId);
791 try { 791 try {
792 return deviceService.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage( 792 return deviceService.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(
793 - getCurrentUser().getTenantId(), new DeviceProfileId(UUID.fromString(deviceProfileId)), OtaPackageType.valueOf(otaPackageType)); 793 + getTenantId(),
  794 + new DeviceProfileId(UUID.fromString(deviceProfileId)),
  795 + OtaPackageType.valueOf(otaPackageType));
794 } catch (Exception e) { 796 } catch (Exception e) {
795 throw handleException(e); 797 throw handleException(e);
796 } 798 }
@@ -74,6 +74,7 @@ import java.util.List; @@ -74,6 +74,7 @@ import java.util.List;
74 import java.util.Map; 74 import java.util.Map;
75 import java.util.Set; 75 import java.util.Set;
76 import java.util.concurrent.ConcurrentMap; 76 import java.util.concurrent.ConcurrentMap;
  77 +import java.util.concurrent.TimeUnit;
77 import java.util.stream.Collectors; 78 import java.util.stream.Collectors;
78 79
79 @Slf4j 80 @Slf4j
@@ -86,6 +87,7 @@ public class RuleChainController extends BaseController { @@ -86,6 +87,7 @@ public class RuleChainController extends BaseController {
86 public static final String RULE_NODE_ID = "ruleNodeId"; 87 public static final String RULE_NODE_ID = "ruleNodeId";
87 88
88 private static final ObjectMapper objectMapper = new ObjectMapper(); 89 private static final ObjectMapper objectMapper = new ObjectMapper();
  90 + public static final int TIMEOUT = 20;
89 91
90 @Autowired 92 @Autowired
91 private InstallScripts installScripts; 93 private InstallScripts installScripts;
@@ -388,25 +390,25 @@ public class RuleChainController extends BaseController { @@ -388,25 +390,25 @@ public class RuleChainController extends BaseController {
388 TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data); 390 TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data);
389 switch (scriptType) { 391 switch (scriptType) {
390 case "update": 392 case "update":
391 - output = msgToOutput(engine.executeUpdate(inMsg)); 393 + output = msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
392 break; 394 break;
393 case "generate": 395 case "generate":
394 - output = msgToOutput(engine.executeGenerate(inMsg)); 396 + output = msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
395 break; 397 break;
396 case "filter": 398 case "filter":
397 - boolean result = engine.executeFilter(inMsg); 399 + boolean result = engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
398 output = Boolean.toString(result); 400 output = Boolean.toString(result);
399 break; 401 break;
400 case "switch": 402 case "switch":
401 - Set<String> states = engine.executeSwitch(inMsg); 403 + Set<String> states = engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
402 output = objectMapper.writeValueAsString(states); 404 output = objectMapper.writeValueAsString(states);
403 break; 405 break;
404 case "json": 406 case "json":
405 - JsonNode json = engine.executeJson(inMsg); 407 + JsonNode json = engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
406 output = objectMapper.writeValueAsString(json); 408 output = objectMapper.writeValueAsString(json);
407 break; 409 break;
408 case "string": 410 case "string":
409 - output = engine.executeToString(inMsg); 411 + output = engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
410 break; 412 break;
411 default: 413 default:
412 throw new IllegalArgumentException("Unsupported script type: " + scriptType); 414 throw new IllegalArgumentException("Unsupported script type: " + scriptType);
@@ -30,6 +30,7 @@ import java.util.UUID; @@ -30,6 +30,7 @@ import java.util.UUID;
30 import java.util.concurrent.ConcurrentHashMap; 30 import java.util.concurrent.ConcurrentHashMap;
31 import java.util.concurrent.Executors; 31 import java.util.concurrent.Executors;
32 import java.util.concurrent.ScheduledExecutorService; 32 import java.util.concurrent.ScheduledExecutorService;
  33 +import java.util.concurrent.TimeoutException;
33 import java.util.concurrent.atomic.AtomicInteger; 34 import java.util.concurrent.atomic.AtomicInteger;
34 35
35 /** 36 /**
@@ -84,8 +85,10 @@ public abstract class AbstractJsInvokeService implements JsInvokeService { @@ -84,8 +85,10 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
84 apiUsageClient.report(tenantId, customerId, ApiUsageRecordKey.JS_EXEC_COUNT, 1); 85 apiUsageClient.report(tenantId, customerId, ApiUsageRecordKey.JS_EXEC_COUNT, 1);
85 return doInvokeFunction(scriptId, functionName, args); 86 return doInvokeFunction(scriptId, functionName, args);
86 } else { 87 } else {
87 - return Futures.immediateFailedFuture(  
88 - new RuntimeException("Script invocation is blocked due to maximum error count " + getMaxErrors() + "!")); 88 + String message = "Script invocation is blocked due to maximum error count "
  89 + + getMaxErrors() + ", scriptId " + scriptId + "!";
  90 + log.warn(message);
  91 + return Futures.immediateFailedFuture(new RuntimeException(message));
89 } 92 }
90 } else { 93 } else {
91 return Futures.immediateFailedFuture(new RuntimeException("JS Execution is disabled due to API limits!")); 94 return Futures.immediateFailedFuture(new RuntimeException("JS Execution is disabled due to API limits!"));
@@ -117,8 +120,11 @@ public abstract class AbstractJsInvokeService implements JsInvokeService { @@ -117,8 +120,11 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
117 120
118 protected abstract long getMaxBlacklistDuration(); 121 protected abstract long getMaxBlacklistDuration();
119 122
120 - protected void onScriptExecutionError(UUID scriptId) {  
121 - disabledFunctions.computeIfAbsent(scriptId, key -> new DisableListInfo()).incrementAndGet(); 123 + protected void onScriptExecutionError(UUID scriptId, Throwable t, String scriptBody) {
  124 + DisableListInfo disableListInfo = disabledFunctions.computeIfAbsent(scriptId, key -> new DisableListInfo());
  125 + log.warn("Script has exception and will increment counter {} on disabledFunctions for id {}, exception {}, cause {}, scriptBody {}",
  126 + disableListInfo.get(), scriptId, t, t.getCause(), scriptBody);
  127 + disableListInfo.incrementAndGet();
122 } 128 }
123 129
124 private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) { 130 private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) {
@@ -160,7 +160,7 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer @@ -160,7 +160,7 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer
160 return ((Invocable) engine).invokeFunction(functionName, args); 160 return ((Invocable) engine).invokeFunction(functionName, args);
161 } 161 }
162 } catch (Exception e) { 162 } catch (Exception e) {
163 - onScriptExecutionError(scriptId); 163 + onScriptExecutionError(scriptId, e, functionName);
164 throw new ExecutionException(e); 164 throw new ExecutionException(e);
165 } 165 }
166 }); 166 });
@@ -18,7 +18,6 @@ package org.thingsboard.server.service.script; @@ -18,7 +18,6 @@ package org.thingsboard.server.service.script;
18 import com.google.common.util.concurrent.FutureCallback; 18 import com.google.common.util.concurrent.FutureCallback;
19 import com.google.common.util.concurrent.Futures; 19 import com.google.common.util.concurrent.Futures;
20 import com.google.common.util.concurrent.ListenableFuture; 20 import com.google.common.util.concurrent.ListenableFuture;
21 -import com.google.common.util.concurrent.MoreExecutors;  
22 import lombok.Getter; 21 import lombok.Getter;
23 import lombok.extern.slf4j.Slf4j; 22 import lombok.extern.slf4j.Slf4j;
24 import org.springframework.beans.factory.annotation.Autowired; 23 import org.springframework.beans.factory.annotation.Autowired;
@@ -26,6 +25,7 @@ import org.springframework.beans.factory.annotation.Value; @@ -26,6 +25,7 @@ import org.springframework.beans.factory.annotation.Value;
26 import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; 25 import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
27 import org.springframework.scheduling.annotation.Scheduled; 26 import org.springframework.scheduling.annotation.Scheduled;
28 import org.springframework.stereotype.Service; 27 import org.springframework.stereotype.Service;
  28 +import org.springframework.util.StopWatch;
29 import org.thingsboard.common.util.ThingsBoardThreadFactory; 29 import org.thingsboard.common.util.ThingsBoardThreadFactory;
30 import org.thingsboard.server.gen.js.JsInvokeProtos; 30 import org.thingsboard.server.gen.js.JsInvokeProtos;
31 import org.thingsboard.server.queue.TbQueueRequestTemplate; 31 import org.thingsboard.server.queue.TbQueueRequestTemplate;
@@ -161,7 +161,8 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { @@ -161,7 +161,8 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
161 161
162 @Override 162 @Override
163 protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, String functionName, Object[] args) { 163 protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, String functionName, Object[] args) {
164 - String scriptBody = scriptIdToBodysMap.get(scriptId); 164 + log.trace("doInvokeFunction js-request for uuid {} with timeout {}ms", scriptId, maxRequestsTimeout);
  165 + final String scriptBody = scriptIdToBodysMap.get(scriptId);
165 if (scriptBody == null) { 166 if (scriptBody == null) {
166 return Futures.immediateFailedFuture(new RuntimeException("No script body found for scriptId: [" + scriptId + "]!")); 167 return Futures.immediateFailedFuture(new RuntimeException("No script body found for scriptId: [" + scriptId + "]!"));
167 } 168 }
@@ -170,7 +171,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { @@ -170,7 +171,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
170 .setScriptIdLSB(scriptId.getLeastSignificantBits()) 171 .setScriptIdLSB(scriptId.getLeastSignificantBits())
171 .setFunctionName(functionName) 172 .setFunctionName(functionName)
172 .setTimeout((int) maxRequestsTimeout) 173 .setTimeout((int) maxRequestsTimeout)
173 - .setScriptBody(scriptIdToBodysMap.get(scriptId)); 174 + .setScriptBody(scriptBody);
174 175
175 for (Object arg : args) { 176 for (Object arg : args) {
176 jsRequestBuilder.addArgs(arg.toString()); 177 jsRequestBuilder.addArgs(arg.toString());
@@ -180,6 +181,9 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { @@ -180,6 +181,9 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
180 .setInvokeRequest(jsRequestBuilder.build()) 181 .setInvokeRequest(jsRequestBuilder.build())
181 .build(); 182 .build();
182 183
  184 + StopWatch stopWatch = new StopWatch();
  185 + stopWatch.start();
  186 +
183 ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(UUID.randomUUID(), jsRequestWrapper)); 187 ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(UUID.randomUUID(), jsRequestWrapper));
184 if (maxRequestsTimeout > 0) { 188 if (maxRequestsTimeout > 0) {
185 future = Futures.withTimeout(future, maxRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService); 189 future = Futures.withTimeout(future, maxRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService);
@@ -193,7 +197,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { @@ -193,7 +197,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
193 197
194 @Override 198 @Override
195 public void onFailure(Throwable t) { 199 public void onFailure(Throwable t) {
196 - onScriptExecutionError(scriptId); 200 + onScriptExecutionError(scriptId, t, scriptBody);
197 if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) { 201 if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) {
198 queueTimeoutMsgs.incrementAndGet(); 202 queueTimeoutMsgs.incrementAndGet();
199 } 203 }
@@ -201,13 +205,16 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { @@ -201,13 +205,16 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
201 } 205 }
202 }, callbackExecutor); 206 }, callbackExecutor);
203 return Futures.transform(future, response -> { 207 return Futures.transform(future, response -> {
  208 + stopWatch.stop();
  209 + log.trace("doInvokeFunction js-response took {}ms for uuid {}", stopWatch.getTotalTimeMillis(), response.getKey());
204 JsInvokeProtos.JsInvokeResponse invokeResult = response.getValue().getInvokeResponse(); 210 JsInvokeProtos.JsInvokeResponse invokeResult = response.getValue().getInvokeResponse();
205 if (invokeResult.getSuccess()) { 211 if (invokeResult.getSuccess()) {
206 return invokeResult.getResult(); 212 return invokeResult.getResult();
207 } else { 213 } else {
208 - onScriptExecutionError(scriptId); 214 + final RuntimeException e = new RuntimeException(invokeResult.getErrorDetails());
  215 + onScriptExecutionError(scriptId, e, scriptBody);
209 log.debug("[{}] Failed to compile script due to [{}]: {}", scriptId, invokeResult.getErrorCode().name(), invokeResult.getErrorDetails()); 216 log.debug("[{}] Failed to compile script due to [{}]: {}", scriptId, invokeResult.getErrorCode().name(), invokeResult.getErrorDetails());
210 - throw new RuntimeException(invokeResult.getErrorDetails()); 217 + throw e;
211 } 218 }
212 }, callbackExecutor); 219 }, callbackExecutor);
213 } 220 }
@@ -18,12 +18,12 @@ package org.thingsboard.server.service.script; @@ -18,12 +18,12 @@ package org.thingsboard.server.service.script;
18 import com.fasterxml.jackson.core.type.TypeReference; 18 import com.fasterxml.jackson.core.type.TypeReference;
19 import com.fasterxml.jackson.databind.JsonNode; 19 import com.fasterxml.jackson.databind.JsonNode;
20 import com.fasterxml.jackson.databind.ObjectMapper; 20 import com.fasterxml.jackson.databind.ObjectMapper;
21 -import com.google.common.collect.Sets;  
22 import com.google.common.util.concurrent.Futures; 21 import com.google.common.util.concurrent.Futures;
23 import com.google.common.util.concurrent.ListenableFuture; 22 import com.google.common.util.concurrent.ListenableFuture;
24 import com.google.common.util.concurrent.MoreExecutors; 23 import com.google.common.util.concurrent.MoreExecutors;
25 import lombok.extern.slf4j.Slf4j; 24 import lombok.extern.slf4j.Slf4j;
26 import org.apache.commons.lang3.StringUtils; 25 import org.apache.commons.lang3.StringUtils;
  26 +import org.thingsboard.server.common.data.id.CustomerId;
27 import org.thingsboard.server.common.data.id.EntityId; 27 import org.thingsboard.server.common.data.id.EntityId;
28 import org.thingsboard.server.common.data.id.TenantId; 28 import org.thingsboard.server.common.data.id.TenantId;
29 import org.thingsboard.server.common.msg.TbMsg; 29 import org.thingsboard.server.common.msg.TbMsg;
@@ -32,6 +32,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -32,6 +32,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
32 import javax.script.ScriptException; 32 import javax.script.ScriptException;
33 import java.util.ArrayList; 33 import java.util.ArrayList;
34 import java.util.Collections; 34 import java.util.Collections;
  35 +import java.util.HashSet;
35 import java.util.List; 36 import java.util.List;
36 import java.util.Map; 37 import java.util.Map;
37 import java.util.Set; 38 import java.util.Set;
@@ -102,140 +103,115 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S @@ -102,140 +103,115 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
102 String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType(); 103 String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType();
103 return TbMsg.transformMsg(msg, newMessageType, msg.getOriginator(), newMetadata, newData); 104 return TbMsg.transformMsg(msg, newMessageType, msg.getOriginator(), newMetadata, newData);
104 } catch (Throwable th) { 105 } catch (Throwable th) {
105 - th.printStackTrace();  
106 throw new RuntimeException("Failed to unbind message data from javascript result", th); 106 throw new RuntimeException("Failed to unbind message data from javascript result", th);
107 } 107 }
108 } 108 }
109 109
110 @Override 110 @Override
111 - public List<TbMsg> executeUpdate(TbMsg msg) throws ScriptException {  
112 - JsonNode result = executeScript(msg);  
113 - if (result.isObject()) {  
114 - return Collections.singletonList(unbindMsg(result, msg));  
115 - } else if (result.isArray()){  
116 - List<TbMsg> res = new ArrayList<>(result.size());  
117 - result.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg)));  
118 - return res;  
119 - } else {  
120 - log.warn("Wrong result type: {}", result.getNodeType());  
121 - throw new ScriptException("Wrong result type: " + result.getNodeType()); 111 + public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) {
  112 + ListenableFuture<JsonNode> result = executeScriptAsync(msg);
  113 + return Futures.transformAsync(result,
  114 + json -> executeUpdateTransform(msg, json),
  115 + MoreExecutors.directExecutor());
  116 + }
  117 +
  118 + ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, JsonNode json) {
  119 + if (json.isObject()) {
  120 + return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg)));
  121 + } else if (json.isArray()) {
  122 + List<TbMsg> res = new ArrayList<>(json.size());
  123 + json.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg)));
  124 + return Futures.immediateFuture(res);
122 } 125 }
  126 + log.warn("Wrong result type: {}", json.getNodeType());
  127 + return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
123 } 128 }
124 129
125 @Override 130 @Override
126 - public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) {  
127 - ListenableFuture<JsonNode> result = executeScriptAsync(msg);  
128 - return Futures.transformAsync(result, json -> {  
129 - if (json.isObject()) {  
130 - return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg)));  
131 - } else if (json.isArray()){  
132 - List<TbMsg> res = new ArrayList<>(json.size());  
133 - json.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg)));  
134 - return Futures.immediateFuture(res);  
135 - }  
136 - else{  
137 - log.warn("Wrong result type: {}", json.getNodeType());  
138 - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));  
139 - }  
140 - }, MoreExecutors.directExecutor()); 131 + public ListenableFuture<TbMsg> executeGenerateAsync(TbMsg prevMsg) {
  132 + return Futures.transformAsync(executeScriptAsync(prevMsg),
  133 + result -> executeGenerateTransform(prevMsg, result),
  134 + MoreExecutors.directExecutor());
141 } 135 }
142 136
143 - @Override  
144 - public TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException {  
145 - JsonNode result = executeScript(prevMsg); 137 + ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, JsonNode result) {
146 if (!result.isObject()) { 138 if (!result.isObject()) {
147 log.warn("Wrong result type: {}", result.getNodeType()); 139 log.warn("Wrong result type: {}", result.getNodeType());
148 - throw new ScriptException("Wrong result type: " + result.getNodeType()); 140 + Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
149 } 141 }
150 - return unbindMsg(result, prevMsg); 142 + return Futures.immediateFuture(unbindMsg(result, prevMsg));
151 } 143 }
152 144
153 @Override 145 @Override
154 - public JsonNode executeJson(TbMsg msg) throws ScriptException {  
155 - return executeScript(msg);  
156 - }  
157 -  
158 - @Override  
159 - public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) throws ScriptException { 146 + public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) {
160 return executeScriptAsync(msg); 147 return executeScriptAsync(msg);
161 } 148 }
162 149
163 @Override 150 @Override
164 - public String executeToString(TbMsg msg) throws ScriptException {  
165 - JsonNode result = executeScript(msg);  
166 - if (!result.isTextual()) {  
167 - log.warn("Wrong result type: {}", result.getNodeType());  
168 - throw new ScriptException("Wrong result type: " + result.getNodeType());  
169 - }  
170 - return result.asText(); 151 + public ListenableFuture<String> executeToStringAsync(TbMsg msg) {
  152 + return Futures.transformAsync(executeScriptAsync(msg),
  153 + this::executeToStringTransform,
  154 + MoreExecutors.directExecutor());
171 } 155 }
172 156
173 - @Override  
174 - public boolean executeFilter(TbMsg msg) throws ScriptException {  
175 - JsonNode result = executeScript(msg);  
176 - if (!result.isBoolean()) {  
177 - log.warn("Wrong result type: {}", result.getNodeType());  
178 - throw new ScriptException("Wrong result type: " + result.getNodeType()); 157 + ListenableFuture<String> executeToStringTransform(JsonNode result) {
  158 + if (result.isTextual()) {
  159 + return Futures.immediateFuture(result.asText());
179 } 160 }
180 - return result.asBoolean(); 161 + log.warn("Wrong result type: {}", result.getNodeType());
  162 + return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
181 } 163 }
182 164
183 @Override 165 @Override
184 public ListenableFuture<Boolean> executeFilterAsync(TbMsg msg) { 166 public ListenableFuture<Boolean> executeFilterAsync(TbMsg msg) {
185 - ListenableFuture<JsonNode> result = executeScriptAsync(msg);  
186 - return Futures.transformAsync(result, json -> {  
187 - if (!json.isBoolean()) {  
188 - log.warn("Wrong result type: {}", json.getNodeType());  
189 - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));  
190 - } else {  
191 - return Futures.immediateFuture(json.asBoolean());  
192 - }  
193 - }, MoreExecutors.directExecutor()); 167 + return Futures.transformAsync(executeScriptAsync(msg),
  168 + this::executeFilterTransform,
  169 + MoreExecutors.directExecutor());
194 } 170 }
195 171
196 - @Override  
197 - public Set<String> executeSwitch(TbMsg msg) throws ScriptException {  
198 - JsonNode result = executeScript(msg); 172 + ListenableFuture<Boolean> executeFilterTransform(JsonNode json) {
  173 + if (json.isBoolean()) {
  174 + return Futures.immediateFuture(json.asBoolean());
  175 + }
  176 + log.warn("Wrong result type: {}", json.getNodeType());
  177 + return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
  178 + }
  179 +
  180 + ListenableFuture<Set<String>> executeSwitchTransform(JsonNode result) {
199 if (result.isTextual()) { 181 if (result.isTextual()) {
200 - return Collections.singleton(result.asText());  
201 - } else if (result.isArray()) {  
202 - Set<String> nextStates = Sets.newHashSet(); 182 + return Futures.immediateFuture(Collections.singleton(result.asText()));
  183 + }
  184 + if (result.isArray()) {
  185 + Set<String> nextStates = new HashSet<>();
203 for (JsonNode val : result) { 186 for (JsonNode val : result) {
204 if (!val.isTextual()) { 187 if (!val.isTextual()) {
205 log.warn("Wrong result type: {}", val.getNodeType()); 188 log.warn("Wrong result type: {}", val.getNodeType());
206 - throw new ScriptException("Wrong result type: " + val.getNodeType()); 189 + return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + val.getNodeType()));
207 } else { 190 } else {
208 nextStates.add(val.asText()); 191 nextStates.add(val.asText());
209 } 192 }
210 } 193 }
211 - return nextStates;  
212 - } else {  
213 - log.warn("Wrong result type: {}", result.getNodeType());  
214 - throw new ScriptException("Wrong result type: " + result.getNodeType()); 194 + return Futures.immediateFuture(nextStates);
215 } 195 }
  196 + log.warn("Wrong result type: {}", result.getNodeType());
  197 + return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
216 } 198 }
217 199
218 - private JsonNode executeScript(TbMsg msg) throws ScriptException {  
219 - try {  
220 - String[] inArgs = prepareArgs(msg);  
221 - String eval = sandboxService.invokeFunction(tenantId, msg.getCustomerId(), this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString();  
222 - return mapper.readTree(eval);  
223 - } catch (ExecutionException e) {  
224 - if (e.getCause() instanceof ScriptException) {  
225 - throw (ScriptException) e.getCause();  
226 - } else if (e.getCause() instanceof RuntimeException) {  
227 - throw new ScriptException(e.getCause().getMessage());  
228 - } else {  
229 - throw new ScriptException(e);  
230 - }  
231 - } catch (Exception e) {  
232 - throw new ScriptException(e);  
233 - } 200 + @Override
  201 + public ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg) {
  202 + return Futures.transformAsync(executeScriptAsync(msg),
  203 + this::executeSwitchTransform,
  204 + MoreExecutors.directExecutor()); //usually runs in a callbackExecutor
234 } 205 }
235 206
236 - private ListenableFuture<JsonNode> executeScriptAsync(TbMsg msg) { 207 + ListenableFuture<JsonNode> executeScriptAsync(TbMsg msg) {
  208 + log.trace("execute script async, msg {}", msg);
237 String[] inArgs = prepareArgs(msg); 209 String[] inArgs = prepareArgs(msg);
238 - return Futures.transformAsync(sandboxService.invokeFunction(tenantId, msg.getCustomerId(), this.scriptId, inArgs[0], inArgs[1], inArgs[2]), 210 + return executeScriptAsync(msg.getCustomerId(), inArgs[0], inArgs[1], inArgs[2]);
  211 + }
  212 +
  213 + ListenableFuture<JsonNode> executeScriptAsync(CustomerId customerId, Object... args) {
  214 + return Futures.transformAsync(sandboxService.invokeFunction(tenantId, customerId, this.scriptId, args),
239 o -> { 215 o -> {
240 try { 216 try {
241 return Futures.immediateFuture(mapper.readTree(o.toString())); 217 return Futures.immediateFuture(mapper.readTree(o.toString()));
@@ -332,6 +332,7 @@ actors: @@ -332,6 +332,7 @@ actors:
332 cache: 332 cache:
333 # caffeine or redis 333 # caffeine or redis
334 type: "${CACHE_TYPE:caffeine}" 334 type: "${CACHE_TYPE:caffeine}"
  335 + maximumPoolSize: "${CACHE_MAXIMUM_POOL_SIZE:16}" # max pool size to process futures that calls the external cache
335 attributes: 336 attributes:
336 # make sure that if cache.type is 'redis' and cache.attributes.enabled is 'true' that you change 'maxmemory-policy' Redis config property to 'allkeys-lru', 'allkeys-lfu' or 'allkeys-random' 337 # make sure that if cache.type is 'redis' and cache.attributes.enabled is 'true' that you change 'maxmemory-policy' Redis config property to 'allkeys-lru', 'allkeys-lfu' or 'allkeys-random'
337 enabled: "${CACHE_ATTRIBUTES_ENABLED:true}" 338 enabled: "${CACHE_ATTRIBUTES_ENABLED:true}"
@@ -51,6 +51,13 @@ public class SnmpDeviceTransportConfiguration implements DeviceTransportConfigur @@ -51,6 +51,13 @@ public class SnmpDeviceTransportConfiguration implements DeviceTransportConfigur
51 private String privacyPassphrase; 51 private String privacyPassphrase;
52 private String engineId; 52 private String engineId;
53 53
  54 + public SnmpDeviceTransportConfiguration() {
  55 + this.host = "localhost";
  56 + this.port = 161;
  57 + this.protocolVersion = SnmpProtocolVersion.V2C;
  58 + this.community = "public";
  59 + }
  60 +
54 @Override 61 @Override
55 public DeviceTransportType getType() { 62 public DeviceTransportType getType() {
56 return DeviceTransportType.SNMP; 63 return DeviceTransportType.SNMP;
@@ -76,7 +83,7 @@ public class SnmpDeviceTransportConfiguration implements DeviceTransportConfigur @@ -76,7 +83,7 @@ public class SnmpDeviceTransportConfiguration implements DeviceTransportConfigur
76 isValid = StringUtils.isNotBlank(username) && StringUtils.isNotBlank(securityName) 83 isValid = StringUtils.isNotBlank(username) && StringUtils.isNotBlank(securityName)
77 && contextName != null && authenticationProtocol != null 84 && contextName != null && authenticationProtocol != null
78 && StringUtils.isNotBlank(authenticationPassphrase) 85 && StringUtils.isNotBlank(authenticationPassphrase)
79 - && privacyProtocol != null && privacyPassphrase != null && engineId != null; 86 + && privacyProtocol != null && StringUtils.isNotBlank(privacyPassphrase) && engineId != null;
80 break; 87 break;
81 } 88 }
82 } 89 }
@@ -24,6 +24,8 @@ public interface TbQueueRequestTemplate<Request extends TbQueueMsg, Response ext @@ -24,6 +24,8 @@ public interface TbQueueRequestTemplate<Request extends TbQueueMsg, Response ext
24 24
25 ListenableFuture<Response> send(Request request); 25 ListenableFuture<Response> send(Request request);
26 26
  27 + ListenableFuture<Response> send(Request request, long timeoutNs);
  28 +
27 void stop(); 29 void stop();
28 30
29 void setMessagesStats(MessagesStats messagesStats); 31 void setMessagesStats(MessagesStats messagesStats);
@@ -19,7 +19,9 @@ import com.google.common.util.concurrent.Futures; @@ -19,7 +19,9 @@ import com.google.common.util.concurrent.Futures;
19 import com.google.common.util.concurrent.ListenableFuture; 19 import com.google.common.util.concurrent.ListenableFuture;
20 import com.google.common.util.concurrent.SettableFuture; 20 import com.google.common.util.concurrent.SettableFuture;
21 import lombok.Builder; 21 import lombok.Builder;
  22 +import lombok.Getter;
22 import lombok.extern.slf4j.Slf4j; 23 import lombok.extern.slf4j.Slf4j;
  24 +import org.thingsboard.common.util.TbStopWatch;
23 import org.thingsboard.common.util.ThingsBoardThreadFactory; 25 import org.thingsboard.common.util.ThingsBoardThreadFactory;
24 import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; 26 import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
25 import org.thingsboard.server.queue.TbQueueAdmin; 27 import org.thingsboard.server.queue.TbQueueAdmin;
@@ -31,13 +33,17 @@ import org.thingsboard.server.queue.TbQueueProducer; @@ -31,13 +33,17 @@ import org.thingsboard.server.queue.TbQueueProducer;
31 import org.thingsboard.server.queue.TbQueueRequestTemplate; 33 import org.thingsboard.server.queue.TbQueueRequestTemplate;
32 import org.thingsboard.server.common.stats.MessagesStats; 34 import org.thingsboard.server.common.stats.MessagesStats;
33 35
  36 +import javax.annotation.Nullable;
34 import java.util.List; 37 import java.util.List;
35 import java.util.UUID; 38 import java.util.UUID;
36 import java.util.concurrent.ConcurrentHashMap; 39 import java.util.concurrent.ConcurrentHashMap;
37 -import java.util.concurrent.ConcurrentMap;  
38 import java.util.concurrent.ExecutorService; 40 import java.util.concurrent.ExecutorService;
39 import java.util.concurrent.Executors; 41 import java.util.concurrent.Executors;
  42 +import java.util.concurrent.TimeUnit;
40 import java.util.concurrent.TimeoutException; 43 import java.util.concurrent.TimeoutException;
  44 +import java.util.concurrent.locks.Lock;
  45 +import java.util.concurrent.locks.LockSupport;
  46 +import java.util.concurrent.locks.ReentrantLock;
41 47
42 @Slf4j 48 @Slf4j
43 public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response extends TbQueueMsg> extends AbstractTbQueueTemplate 49 public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response extends TbQueueMsg> extends AbstractTbQueueTemplate
@@ -46,15 +52,15 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response @@ -46,15 +52,15 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
46 private final TbQueueAdmin queueAdmin; 52 private final TbQueueAdmin queueAdmin;
47 private final TbQueueProducer<Request> requestTemplate; 53 private final TbQueueProducer<Request> requestTemplate;
48 private final TbQueueConsumer<Response> responseTemplate; 54 private final TbQueueConsumer<Response> responseTemplate;
49 - private final ConcurrentMap<UUID, DefaultTbQueueRequestTemplate.ResponseMetaData<Response>> pendingRequests;  
50 - private final boolean internalExecutor;  
51 - private final ExecutorService executor;  
52 - private final long maxRequestTimeout;  
53 - private final long maxPendingRequests;  
54 - private final long pollInterval;  
55 - private volatile long tickTs = 0L;  
56 - private volatile long tickSize = 0L;  
57 - private volatile boolean stopped = false; 55 + final ConcurrentHashMap<UUID, DefaultTbQueueRequestTemplate.ResponseMetaData<Response>> pendingRequests = new ConcurrentHashMap<>();
  56 + final boolean internalExecutor;
  57 + final ExecutorService executor;
  58 + final long maxRequestTimeoutNs;
  59 + final long maxPendingRequests;
  60 + final long pollInterval;
  61 + volatile boolean stopped = false;
  62 + long nextCleanupNs = 0L;
  63 + private final Lock cleanerLock = new ReentrantLock();
58 64
59 private MessagesStats messagesStats; 65 private MessagesStats messagesStats;
60 66
@@ -65,79 +71,113 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response @@ -65,79 +71,113 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
65 long maxRequestTimeout, 71 long maxRequestTimeout,
66 long maxPendingRequests, 72 long maxPendingRequests,
67 long pollInterval, 73 long pollInterval,
68 - ExecutorService executor) { 74 + @Nullable ExecutorService executor) {
69 this.queueAdmin = queueAdmin; 75 this.queueAdmin = queueAdmin;
70 this.requestTemplate = requestTemplate; 76 this.requestTemplate = requestTemplate;
71 this.responseTemplate = responseTemplate; 77 this.responseTemplate = responseTemplate;
72 - this.pendingRequests = new ConcurrentHashMap<>();  
73 - this.maxRequestTimeout = maxRequestTimeout; 78 + this.maxRequestTimeoutNs = TimeUnit.MILLISECONDS.toNanos(maxRequestTimeout);
74 this.maxPendingRequests = maxPendingRequests; 79 this.maxPendingRequests = maxPendingRequests;
75 this.pollInterval = pollInterval; 80 this.pollInterval = pollInterval;
76 - if (executor != null) {  
77 - internalExecutor = false;  
78 - this.executor = executor;  
79 - } else {  
80 - internalExecutor = true;  
81 - this.executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-queue-request-template-" + responseTemplate.getTopic()));  
82 - } 81 + this.internalExecutor = (executor == null);
  82 + this.executor = internalExecutor ? createExecutor() : executor;
  83 + }
  84 +
  85 + ExecutorService createExecutor() {
  86 + return Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-queue-request-template-" + responseTemplate.getTopic()));
83 } 87 }
84 88
85 @Override 89 @Override
86 public void init() { 90 public void init() {
87 queueAdmin.createTopicIfNotExists(responseTemplate.getTopic()); 91 queueAdmin.createTopicIfNotExists(responseTemplate.getTopic());
88 - this.requestTemplate.init();  
89 - tickTs = System.currentTimeMillis(); 92 + requestTemplate.init();
90 responseTemplate.subscribe(); 93 responseTemplate.subscribe();
91 - executor.submit(() -> {  
92 - long nextCleanupMs = 0L;  
93 - while (!stopped) {  
94 - try {  
95 - List<Response> responses = responseTemplate.poll(pollInterval);  
96 - if (responses.size() > 0) {  
97 - log.trace("Polling responses completed, consumer records count [{}]", responses.size());  
98 - }  
99 - responses.forEach(response -> {  
100 - byte[] requestIdHeader = response.getHeaders().get(REQUEST_ID_HEADER);  
101 - UUID requestId;  
102 - if (requestIdHeader == null) {  
103 - log.error("[{}] Missing requestId in header and body", response);  
104 - } else {  
105 - requestId = bytesToUuid(requestIdHeader);  
106 - log.trace("[{}] Response received: {}", requestId, response);  
107 - ResponseMetaData<Response> expectedResponse = pendingRequests.remove(requestId);  
108 - if (expectedResponse == null) {  
109 - log.trace("[{}] Invalid or stale request", requestId);  
110 - } else {  
111 - expectedResponse.future.set(response);  
112 - } 94 + executor.submit(this::mainLoop);
  95 + }
  96 +
  97 + void mainLoop() {
  98 + while (!stopped) {
  99 + TbStopWatch sw = TbStopWatch.startNew();
  100 + try {
  101 + fetchAndProcessResponses();
  102 + } catch (Throwable e) {
  103 + long sleepNanos = TimeUnit.MILLISECONDS.toNanos(this.pollInterval) - sw.stopAndGetTotalTimeNanos();
  104 + log.warn("Failed to obtain and process responses from queue. Going to sleep " + sleepNanos + "ns", e);
  105 + sleep(sleepNanos);
  106 + }
  107 + }
  108 + }
  109 +
  110 + void fetchAndProcessResponses() {
  111 + final long pendingRequestsCount = pendingRequests.mappingCount();
  112 + log.trace("Starting template pool topic {}, for pendingRequests {}", responseTemplate.getTopic(), pendingRequestsCount);
  113 + List<Response> responses = doPoll(); //poll js responses
  114 + log.trace("Completed template poll topic {}, for pendingRequests [{}], received [{}] responses", responseTemplate.getTopic(), pendingRequestsCount, responses.size());
  115 + responses.forEach(this::processResponse); //this can take a long time
  116 + responseTemplate.commit();
  117 + tryCleanStaleRequests();
  118 + }
  119 +
  120 + private boolean tryCleanStaleRequests() {
  121 + if (!cleanerLock.tryLock()) {
  122 + return false;
  123 + }
  124 + try {
  125 + log.trace("tryCleanStaleRequest...");
  126 + final long currentNs = getCurrentClockNs();
  127 + if (nextCleanupNs < currentNs) {
  128 + pendingRequests.forEach((key, value) -> {
  129 + if (value.expTime < currentNs) {
  130 + ResponseMetaData<Response> staleRequest = pendingRequests.remove(key);
  131 + if (staleRequest != null) {
  132 + setTimeoutException(key, staleRequest, currentNs);
113 } 133 }
114 - });  
115 - responseTemplate.commit();  
116 - tickTs = System.currentTimeMillis();  
117 - tickSize = pendingRequests.size();  
118 - if (nextCleanupMs < tickTs) {  
119 - //cleanup;  
120 - pendingRequests.forEach((key, value) -> {  
121 - if (value.expTime < tickTs) {  
122 - ResponseMetaData<Response> staleRequest = pendingRequests.remove(key);  
123 - if (staleRequest != null) {  
124 - log.trace("[{}] Request timeout detected, expTime [{}], tickTs [{}]", key, staleRequest.expTime, tickTs);  
125 - staleRequest.future.setException(new TimeoutException());  
126 - }  
127 - }  
128 - });  
129 - nextCleanupMs = tickTs + maxRequestTimeout;  
130 - }  
131 - } catch (Throwable e) {  
132 - log.warn("Failed to obtain responses from queue.", e);  
133 - try {  
134 - Thread.sleep(pollInterval);  
135 - } catch (InterruptedException e2) {  
136 - log.trace("Failed to wait until the server has capacity to handle new responses", e2);  
137 } 134 }
138 - } 135 + });
  136 + setupNextCleanup();
139 } 137 }
140 - }); 138 + } finally {
  139 + cleanerLock.unlock();
  140 + }
  141 + return true;
  142 + }
  143 +
  144 + void setupNextCleanup() {
  145 + nextCleanupNs = getCurrentClockNs() + maxRequestTimeoutNs;
  146 + log.trace("setupNextCleanup {}", nextCleanupNs);
  147 + }
  148 +
  149 + List<Response> doPoll() {
  150 + return responseTemplate.poll(pollInterval);
  151 + }
  152 +
  153 + void sleep(long nanos) {
  154 + LockSupport.parkNanos(nanos);
  155 + }
  156 +
  157 + void setTimeoutException(UUID key, ResponseMetaData<Response> staleRequest, long currentNs) {
  158 + if (currentNs >= staleRequest.getSubmitTime() + staleRequest.getTimeout()) {
  159 + log.warn("Request timeout detected, currentNs [{}], {}, key [{}]", currentNs, staleRequest, key);
  160 + } else {
  161 + log.error("Request timeout detected, currentNs [{}], {}, key [{}]", currentNs, staleRequest, key);
  162 + }
  163 + staleRequest.future.setException(new TimeoutException());
  164 + }
  165 +
  166 + void processResponse(Response response) {
  167 + byte[] requestIdHeader = response.getHeaders().get(REQUEST_ID_HEADER);
  168 + UUID requestId;
  169 + if (requestIdHeader == null) {
  170 + log.error("[{}] Missing requestId in header and body", response);
  171 + } else {
  172 + requestId = bytesToUuid(requestIdHeader);
  173 + log.trace("[{}] Response received: {}", requestId, String.valueOf(response).replace("\n", " ")); //TODO remove overhead
  174 + ResponseMetaData<Response> expectedResponse = pendingRequests.remove(requestId);
  175 + if (expectedResponse == null) {
  176 + log.warn("[{}] Invalid or stale request, response: {}", requestId, String.valueOf(response).replace("\n", " "));
  177 + } else {
  178 + expectedResponse.future.set(response);
  179 + }
  180 + }
141 } 181 }
142 182
143 @Override 183 @Override
@@ -164,17 +204,48 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response @@ -164,17 +204,48 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
164 204
165 @Override 205 @Override
166 public ListenableFuture<Response> send(Request request) { 206 public ListenableFuture<Response> send(Request request) {
167 - if (tickSize > maxPendingRequests) { 207 + return send(request, this.maxRequestTimeoutNs);
  208 + }
  209 +
  210 + @Override
  211 + public ListenableFuture<Response> send(Request request, long requestTimeoutNs) {
  212 + if (pendingRequests.mappingCount() >= maxPendingRequests) {
  213 + log.warn("Pending request map is full [{}]! Consider to increase maxPendingRequests or increase processing performance", maxPendingRequests);
168 return Futures.immediateFailedFuture(new RuntimeException("Pending request map is full!")); 214 return Futures.immediateFailedFuture(new RuntimeException("Pending request map is full!"));
169 } 215 }
170 UUID requestId = UUID.randomUUID(); 216 UUID requestId = UUID.randomUUID();
171 request.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId)); 217 request.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId));
172 request.getHeaders().put(RESPONSE_TOPIC_HEADER, stringToBytes(responseTemplate.getTopic())); 218 request.getHeaders().put(RESPONSE_TOPIC_HEADER, stringToBytes(responseTemplate.getTopic()));
173 - request.getHeaders().put(REQUEST_TIME, longToBytes(System.currentTimeMillis())); 219 + request.getHeaders().put(REQUEST_TIME, longToBytes(getCurrentTimeMs()));
  220 + long currentClockNs = getCurrentClockNs();
174 SettableFuture<Response> future = SettableFuture.create(); 221 SettableFuture<Response> future = SettableFuture.create();
175 - ResponseMetaData<Response> responseMetaData = new ResponseMetaData<>(tickTs + maxRequestTimeout, future);  
176 - pendingRequests.putIfAbsent(requestId, responseMetaData);  
177 - log.trace("[{}] Sending request, key [{}], expTime [{}]", requestId, request.getKey(), responseMetaData.expTime); 222 + ResponseMetaData<Response> responseMetaData = new ResponseMetaData<>(currentClockNs + requestTimeoutNs, future, currentClockNs, requestTimeoutNs);
  223 + log.trace("pending {}", responseMetaData);
  224 + if (pendingRequests.putIfAbsent(requestId, responseMetaData) != null) {
  225 + log.warn("Pending request already exists [{}]!", maxPendingRequests);
  226 + return Futures.immediateFailedFuture(new RuntimeException("Pending request already exists !" + requestId));
  227 + }
  228 + sendToRequestTemplate(request, requestId, future, responseMetaData);
  229 + return future;
  230 + }
  231 +
  232 + /**
  233 + * MONOTONIC clock instead jumping wall clock.
  234 + * Wrapped into the method for the test purposes to travel through the time
  235 + * */
  236 + long getCurrentClockNs() {
  237 + return System.nanoTime();
  238 + }
  239 +
  240 + /**
  241 + * Wall clock to send timestamp to an external service
  242 + * */
  243 + long getCurrentTimeMs() {
  244 + return System.currentTimeMillis();
  245 + }
  246 +
  247 + void sendToRequestTemplate(Request request, UUID requestId, SettableFuture<Response> future, ResponseMetaData<Response> responseMetaData) {
  248 + log.trace("[{}] Sending request, key [{}], expTime [{}], request {}", requestId, request.getKey(), responseMetaData.expTime, request);
178 if (messagesStats != null) { 249 if (messagesStats != null) {
179 messagesStats.incrementTotal(); 250 messagesStats.incrementTotal();
180 } 251 }
@@ -184,7 +255,7 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response @@ -184,7 +255,7 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
184 if (messagesStats != null) { 255 if (messagesStats != null) {
185 messagesStats.incrementSuccessful(); 256 messagesStats.incrementSuccessful();
186 } 257 }
187 - log.trace("[{}] Request sent: {}", requestId, metadata); 258 + log.trace("[{}] Request sent: {}, request {}", requestId, metadata, request);
188 } 259 }
189 260
190 @Override 261 @Override
@@ -196,17 +267,32 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response @@ -196,17 +267,32 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
196 future.setException(t); 267 future.setException(t);
197 } 268 }
198 }); 269 });
199 - return future;  
200 } 270 }
201 271
202 - private static class ResponseMetaData<T> { 272 + @Getter
  273 + static class ResponseMetaData<T> {
  274 + private final long submitTime;
  275 + private final long timeout;
203 private final long expTime; 276 private final long expTime;
204 private final SettableFuture<T> future; 277 private final SettableFuture<T> future;
205 278
206 - ResponseMetaData(long ts, SettableFuture<T> future) { 279 + ResponseMetaData(long ts, SettableFuture<T> future, long submitTime, long timeout) {
  280 + this.submitTime = submitTime;
  281 + this.timeout = timeout;
207 this.expTime = ts; 282 this.expTime = ts;
208 this.future = future; 283 this.future = future;
209 } 284 }
  285 +
  286 + @Override
  287 + public String toString() {
  288 + return "ResponseMetaData{" +
  289 + "submitTime=" + submitTime +
  290 + ", calculatedExpTime=" + (submitTime + timeout) +
  291 + ", deltaMs=" + (expTime - submitTime) +
  292 + ", expTime=" + expTime +
  293 + ", future=" + future +
  294 + '}';
  295 + }
210 } 296 }
211 297
212 } 298 }
@@ -21,6 +21,7 @@ import org.apache.kafka.clients.consumer.ConsumerConfig; @@ -21,6 +21,7 @@ import org.apache.kafka.clients.consumer.ConsumerConfig;
21 import org.apache.kafka.clients.consumer.ConsumerRecord; 21 import org.apache.kafka.clients.consumer.ConsumerRecord;
22 import org.apache.kafka.clients.consumer.ConsumerRecords; 22 import org.apache.kafka.clients.consumer.ConsumerRecords;
23 import org.apache.kafka.clients.consumer.KafkaConsumer; 23 import org.apache.kafka.clients.consumer.KafkaConsumer;
  24 +import org.springframework.util.StopWatch;
24 import org.thingsboard.server.queue.TbQueueAdmin; 25 import org.thingsboard.server.queue.TbQueueAdmin;
25 import org.thingsboard.server.queue.TbQueueMsg; 26 import org.thingsboard.server.queue.TbQueueMsg;
26 import org.thingsboard.server.queue.common.AbstractTbQueueConsumerTemplate; 27 import org.thingsboard.server.queue.common.AbstractTbQueueConsumerTemplate;
@@ -82,7 +83,16 @@ public class TbKafkaConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQue @@ -82,7 +83,16 @@ public class TbKafkaConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQue
82 83
83 @Override 84 @Override
84 protected List<ConsumerRecord<String, byte[]>> doPoll(long durationInMillis) { 85 protected List<ConsumerRecord<String, byte[]>> doPoll(long durationInMillis) {
  86 + StopWatch stopWatch = new StopWatch();
  87 + stopWatch.start();
  88 +
  89 + log.trace("poll topic {} maxDuration {}", getTopic(), durationInMillis);
  90 +
85 ConsumerRecords<String, byte[]> records = consumer.poll(Duration.ofMillis(durationInMillis)); 91 ConsumerRecords<String, byte[]> records = consumer.poll(Duration.ofMillis(durationInMillis));
  92 +
  93 + stopWatch.stop();
  94 + log.trace("poll topic {} took {}ms", getTopic(), stopWatch.getTotalTimeMillis());
  95 +
86 if (records.isEmpty()) { 96 if (records.isEmpty()) {
87 return Collections.emptyList(); 97 return Collections.emptyList();
88 } else { 98 } else {
  1 +/**
  2 + * Copyright © 2016-2021 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.queue.common;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.junit.After;
  20 +import org.junit.Before;
  21 +import org.junit.Test;
  22 +import org.junit.runner.RunWith;
  23 +import org.mockito.ArgumentCaptor;
  24 +import org.mockito.Mock;
  25 +import org.mockito.junit.MockitoJUnitRunner;
  26 +import org.thingsboard.server.queue.TbQueueAdmin;
  27 +import org.thingsboard.server.queue.TbQueueConsumer;
  28 +import org.thingsboard.server.queue.TbQueueMsg;
  29 +import org.thingsboard.server.queue.TbQueueProducer;
  30 +
  31 +import java.util.Collections;
  32 +import java.util.List;
  33 +import java.util.UUID;
  34 +import java.util.concurrent.CountDownLatch;
  35 +import java.util.concurrent.ExecutorService;
  36 +import java.util.concurrent.TimeUnit;
  37 +import java.util.concurrent.atomic.AtomicLong;
  38 +
  39 +import static org.hamcrest.Matchers.equalTo;
  40 +import static org.hamcrest.Matchers.greaterThanOrEqualTo;
  41 +import static org.hamcrest.Matchers.is;
  42 +import static org.hamcrest.Matchers.lessThan;
  43 +import static org.mockito.ArgumentMatchers.any;
  44 +import static org.mockito.ArgumentMatchers.anyLong;
  45 +import static org.mockito.BDDMockito.willAnswer;
  46 +import static org.mockito.BDDMockito.willDoNothing;
  47 +import static org.mockito.BDDMockito.willReturn;
  48 +import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
  49 +import static org.mockito.Mockito.atLeastOnce;
  50 +import static org.mockito.Mockito.mock;
  51 +import static org.mockito.Mockito.never;
  52 +import static org.mockito.Mockito.spy;
  53 +import static org.mockito.Mockito.times;
  54 +import static org.mockito.Mockito.verify;
  55 +
  56 +import static org.hamcrest.MatcherAssert.assertThat;
  57 +import static org.mockito.hamcrest.MockitoHamcrest.longThat;
  58 +
  59 +@Slf4j
  60 +@RunWith(MockitoJUnitRunner.class)
  61 +public class DefaultTbQueueRequestTemplateTest {
  62 +
  63 + @Mock
  64 + TbQueueAdmin queueAdmin;
  65 + @Mock
  66 + TbQueueProducer<TbQueueMsg> requestTemplate;
  67 + @Mock
  68 + TbQueueConsumer<TbQueueMsg> responseTemplate;
  69 + @Mock
  70 + ExecutorService executorMock;
  71 +
  72 + ExecutorService executor;
  73 + String topic = "js-responses-tb-node-0";
  74 + long maxRequestTimeout = 10;
  75 + long maxPendingRequests = 32;
  76 + long pollInterval = 5;
  77 +
  78 + DefaultTbQueueRequestTemplate inst;
  79 +
  80 + @Before
  81 + public void setUp() throws Exception {
  82 + willReturn(topic).given(responseTemplate).getTopic();
  83 + inst = spy(new DefaultTbQueueRequestTemplate(
  84 + queueAdmin, requestTemplate, responseTemplate,
  85 + maxRequestTimeout, maxPendingRequests, pollInterval, executorMock));
  86 +
  87 + }
  88 +
  89 + @After
  90 + public void tearDown() throws Exception {
  91 + if (executor != null) {
  92 + executor.shutdownNow();
  93 + }
  94 + }
  95 +
  96 + @Test
  97 + public void givenInstance_whenVerifyInitialParameters_thenOK() {
  98 + assertThat(inst.maxPendingRequests, equalTo(maxPendingRequests));
  99 + assertThat(inst.maxRequestTimeoutNs, equalTo(TimeUnit.MILLISECONDS.toNanos(maxRequestTimeout)));
  100 + assertThat(inst.pollInterval, equalTo(pollInterval));
  101 + assertThat(inst.executor, is(executorMock));
  102 + assertThat(inst.stopped, is(false));
  103 + assertThat(inst.internalExecutor, is(false));
  104 + }
  105 +
  106 + @Test
  107 + public void givenExternalExecutor_whenInitStop_thenOK() {
  108 + inst.init();
  109 + assertThat(inst.nextCleanupNs, equalTo(0L));
  110 + verify(queueAdmin, times(1)).createTopicIfNotExists(topic);
  111 + verify(requestTemplate, times(1)).init();
  112 + verify(responseTemplate, times(1)).subscribe();
  113 + verify(executorMock, times(1)).submit(any(Runnable.class));
  114 +
  115 + inst.stop();
  116 + assertThat(inst.stopped, is(true));
  117 + verify(responseTemplate, times(1)).unsubscribe();
  118 + verify(requestTemplate, times(1)).stop();
  119 + verify(executorMock, never()).shutdownNow();
  120 + }
  121 +
  122 + @Test
  123 + public void givenMainLoop_whenLoopFewTimes_thenVerifyInvocationCount() throws InterruptedException {
  124 + executor = inst.createExecutor();
  125 + CountDownLatch latch = new CountDownLatch(5);
  126 + willDoNothing().given(inst).sleep(anyLong());
  127 + willAnswer(invocation -> {
  128 + if (latch.getCount() == 1) {
  129 + inst.stop(); //stop the loop in natural way
  130 + }
  131 + if (latch.getCount() == 3 || latch.getCount() == 4) {
  132 + latch.countDown();
  133 + throw new RuntimeException("test catch block");
  134 + }
  135 + latch.countDown();
  136 + return null;
  137 + }).given(inst).fetchAndProcessResponses();
  138 +
  139 + executor.submit(inst::mainLoop);
  140 + latch.await(10, TimeUnit.SECONDS);
  141 +
  142 + verify(inst, times(5)).fetchAndProcessResponses();
  143 + verify(inst, times(2)).sleep(longThat(lessThan(TimeUnit.MILLISECONDS.toNanos(inst.pollInterval))));
  144 + }
  145 +
  146 + @Test
  147 + public void givenMessages_whenSend_thenOK() {
  148 + willDoNothing().given(inst).sendToRequestTemplate(any(), any(), any(), any());
  149 + inst.init();
  150 + final int msgCount = 10;
  151 + for (int i = 0; i < msgCount; i++) {
  152 + inst.send(getRequestMsgMock());
  153 + }
  154 + assertThat(inst.pendingRequests.mappingCount(), equalTo((long) msgCount));
  155 + verify(inst, times(msgCount)).sendToRequestTemplate(any(), any(), any(), any());
  156 + }
  157 +
  158 + @Test
  159 + public void givenMessagesOverMaxPendingRequests_whenSend_thenImmediateFailedFutureForTheOfRequests() {
  160 + willDoNothing().given(inst).sendToRequestTemplate(any(), any(), any(), any());
  161 + inst.init();
  162 + int msgOverflowCount = 10;
  163 + for (int i = 0; i < inst.maxPendingRequests; i++) {
  164 + assertThat(inst.send(getRequestMsgMock()).isDone(), is(false)); //SettableFuture future - pending only
  165 + }
  166 + for (int i = 0; i < msgOverflowCount; i++) {
  167 + assertThat("max pending requests overflow", inst.send(getRequestMsgMock()).isDone(), is(true)); //overflow, immediate failed future
  168 + }
  169 + assertThat(inst.pendingRequests.mappingCount(), equalTo(inst.maxPendingRequests));
  170 + verify(inst, times((int) inst.maxPendingRequests)).sendToRequestTemplate(any(), any(), any(), any());
  171 + }
  172 +
  173 + @Test
  174 + public void givenNothing_whenSendAndFetchAndProcessResponsesWithTimeout_thenFail() {
  175 + //given
  176 + AtomicLong currentTime = new AtomicLong();
  177 + willAnswer(x -> {
  178 + log.info("currentTime={}", currentTime.get());
  179 + return currentTime.get();
  180 + }).given(inst).getCurrentClockNs();
  181 + inst.init();
  182 + inst.setupNextCleanup();
  183 + willReturn(Collections.emptyList()).given(inst).doPoll();
  184 +
  185 + //when
  186 + long stepNs = TimeUnit.MILLISECONDS.toNanos(1);
  187 + for (long i = 0; i <= inst.maxRequestTimeoutNs * 2; i = i + stepNs) {
  188 + currentTime.addAndGet(stepNs);
  189 + assertThat(inst.send(getRequestMsgMock()).isDone(), is(false)); //SettableFuture future - pending only
  190 + if (i % (inst.maxRequestTimeoutNs * 3 / 2) == 0) {
  191 + inst.fetchAndProcessResponses();
  192 + }
  193 + }
  194 +
  195 + //then
  196 + ArgumentCaptor<DefaultTbQueueRequestTemplate.ResponseMetaData> argumentCaptorResp = ArgumentCaptor.forClass(DefaultTbQueueRequestTemplate.ResponseMetaData.class);
  197 + ArgumentCaptor<UUID> argumentCaptorUUID = ArgumentCaptor.forClass(UUID.class);
  198 + ArgumentCaptor<Long> argumentCaptorLong = ArgumentCaptor.forClass(Long.class);
  199 + verify(inst, atLeastOnce()).setTimeoutException(argumentCaptorUUID.capture(), argumentCaptorResp.capture(), argumentCaptorLong.capture());
  200 +
  201 + List<DefaultTbQueueRequestTemplate.ResponseMetaData> responseMetaDataList = argumentCaptorResp.getAllValues();
  202 + List<Long> tickTsList = argumentCaptorLong.getAllValues();
  203 + for (int i = 0; i < responseMetaDataList.size(); i++) {
  204 + assertThat("tickTs >= calculatedExpTime", tickTsList.get(i), greaterThanOrEqualTo(responseMetaDataList.get(i).getSubmitTime() + responseMetaDataList.get(i).getTimeout()));
  205 + }
  206 + }
  207 +
  208 + TbQueueMsg getRequestMsgMock() {
  209 + return mock(TbQueueMsg.class, RETURNS_DEEP_STUBS);
  210 + }
  211 +}
@@ -37,6 +37,10 @@ @@ -37,6 +37,10 @@
37 37
38 <dependencies> 38 <dependencies>
39 <dependency> 39 <dependency>
  40 + <groupId>org.springframework</groupId>
  41 + <artifactId>spring-core</artifactId>
  42 + </dependency>
  43 + <dependency>
40 <groupId>com.google.guava</groupId> 44 <groupId>com.google.guava</groupId>
41 <artifactId>guava</artifactId> 45 <artifactId>guava</artifactId>
42 <scope>provided</scope> 46 <scope>provided</scope>
  1 +/**
  2 + * Copyright © 2016-2021 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.common.util;
  17 +
  18 +import org.springframework.util.StopWatch;
  19 +
  20 +/**
  21 + * Utility method that extends Spring Framework StopWatch
  22 + * It is a MONOTONIC time stopwatch.
  23 + * It is a replacement for any measurements with a wall-clock like System.currentTimeMillis()
  24 + * It is not affected by leap second, day-light saving and wall-clock adjustments by manual or network time synchronization
  25 + * The main features is a single call for common use cases:
  26 + * - create and start: TbStopWatch sw = TbStopWatch.startNew()
  27 + * - stop and get: sw.stopAndGetTotalTimeMillis() or sw.stopAndGetLastTaskTimeMillis()
  28 + * */
  29 +public class TbStopWatch extends StopWatch {
  30 +
  31 + public static TbStopWatch startNew(){
  32 + TbStopWatch stopWatch = new TbStopWatch();
  33 + stopWatch.start();
  34 + return stopWatch;
  35 + }
  36 +
  37 + public long stopAndGetTotalTimeMillis(){
  38 + stop();
  39 + return getTotalTimeMillis();
  40 + }
  41 +
  42 + public long stopAndGetTotalTimeNanos(){
  43 + stop();
  44 + return getLastTaskTimeNanos();
  45 + }
  46 +
  47 + public long stopAndGetLastTaskTimeMillis(){
  48 + stop();
  49 + return getLastTaskTimeMillis();
  50 + }
  51 +
  52 + public long stopAndGetLastTaskTimeNanos(){
  53 + stop();
  54 + return getLastTaskTimeNanos();
  55 + }
  56 +
  57 +}
@@ -21,7 +21,6 @@ import com.google.common.util.concurrent.MoreExecutors; @@ -21,7 +21,6 @@ import com.google.common.util.concurrent.MoreExecutors;
21 import lombok.extern.slf4j.Slf4j; 21 import lombok.extern.slf4j.Slf4j;
22 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 22 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
23 import org.springframework.cache.Cache; 23 import org.springframework.cache.Cache;
24 -import org.springframework.cache.CacheManager;  
25 import org.springframework.context.annotation.Primary; 24 import org.springframework.context.annotation.Primary;
26 import org.springframework.stereotype.Service; 25 import org.springframework.stereotype.Service;
27 import org.thingsboard.server.common.data.EntityType; 26 import org.thingsboard.server.common.data.EntityType;
@@ -32,6 +31,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -32,6 +31,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
32 import org.thingsboard.server.common.data.kv.KvEntry; 31 import org.thingsboard.server.common.data.kv.KvEntry;
33 import org.thingsboard.server.common.stats.DefaultCounter; 32 import org.thingsboard.server.common.stats.DefaultCounter;
34 import org.thingsboard.server.common.stats.StatsFactory; 33 import org.thingsboard.server.common.stats.StatsFactory;
  34 +import org.thingsboard.server.dao.cache.CacheExecutorService;
35 import org.thingsboard.server.dao.service.Validator; 35 import org.thingsboard.server.dao.service.Validator;
36 36
37 import java.util.ArrayList; 37 import java.util.ArrayList;
@@ -45,7 +45,6 @@ import java.util.Optional; @@ -45,7 +45,6 @@ import java.util.Optional;
45 import java.util.Set; 45 import java.util.Set;
46 import java.util.stream.Collectors; 46 import java.util.stream.Collectors;
47 47
48 -import static org.thingsboard.server.common.data.CacheConstants.ATTRIBUTES_CACHE;  
49 import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; 48 import static org.thingsboard.server.dao.attributes.AttributeUtils.validate;
50 49
51 @Service 50 @Service
@@ -59,12 +58,15 @@ public class CachedAttributesService implements AttributesService { @@ -59,12 +58,15 @@ public class CachedAttributesService implements AttributesService {
59 private final AttributesCacheWrapper cacheWrapper; 58 private final AttributesCacheWrapper cacheWrapper;
60 private final DefaultCounter hitCounter; 59 private final DefaultCounter hitCounter;
61 private final DefaultCounter missCounter; 60 private final DefaultCounter missCounter;
  61 + private final CacheExecutorService cacheExecutorService;
62 62
63 public CachedAttributesService(AttributesDao attributesDao, 63 public CachedAttributesService(AttributesDao attributesDao,
64 AttributesCacheWrapper cacheWrapper, 64 AttributesCacheWrapper cacheWrapper,
65 - StatsFactory statsFactory) { 65 + StatsFactory statsFactory,
  66 + CacheExecutorService cacheExecutorService) {
66 this.attributesDao = attributesDao; 67 this.attributesDao = attributesDao;
67 this.cacheWrapper = cacheWrapper; 68 this.cacheWrapper = cacheWrapper;
  69 + this.cacheExecutorService = cacheExecutorService;
68 70
69 this.hitCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "hit"); 71 this.hitCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "hit");
70 this.missCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "miss"); 72 this.missCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "miss");
@@ -88,7 +90,7 @@ public class CachedAttributesService implements AttributesService { @@ -88,7 +90,7 @@ public class CachedAttributesService implements AttributesService {
88 // TODO: think if it's a good idea to store 'empty' attributes 90 // TODO: think if it's a good idea to store 'empty' attributes
89 cacheWrapper.put(attributeCacheKey, foundAttrKvEntry.orElse(null)); 91 cacheWrapper.put(attributeCacheKey, foundAttrKvEntry.orElse(null));
90 return foundAttrKvEntry; 92 return foundAttrKvEntry;
91 - }, MoreExecutors.directExecutor()); 93 + }, cacheExecutorService);
92 } 94 }
93 } 95 }
94 96
@@ -111,7 +113,7 @@ public class CachedAttributesService implements AttributesService { @@ -111,7 +113,7 @@ public class CachedAttributesService implements AttributesService {
111 notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet()); 113 notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet());
112 114
113 ListenableFuture<List<AttributeKvEntry>> result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); 115 ListenableFuture<List<AttributeKvEntry>> result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys);
114 - return Futures.transform(result, foundInDbAttributes -> mergeDbAndCacheAttributes(entityId, scope, cachedAttributes, notFoundAttributeKeys, foundInDbAttributes), MoreExecutors.directExecutor()); 116 + return Futures.transform(result, foundInDbAttributes -> mergeDbAndCacheAttributes(entityId, scope, cachedAttributes, notFoundAttributeKeys, foundInDbAttributes), cacheExecutorService);
115 117
116 } 118 }
117 119
@@ -169,7 +171,7 @@ public class CachedAttributesService implements AttributesService { @@ -169,7 +171,7 @@ public class CachedAttributesService implements AttributesService {
169 171
170 // TODO: can do if (attributesCache.get() != null) attributesCache.put() instead, but will be more twice more requests to cache 172 // TODO: can do if (attributesCache.get() != null) attributesCache.put() instead, but will be more twice more requests to cache
171 List<String> attributeKeys = attributes.stream().map(KvEntry::getKey).collect(Collectors.toList()); 173 List<String> attributeKeys = attributes.stream().map(KvEntry::getKey).collect(Collectors.toList());
172 - future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), MoreExecutors.directExecutor()); 174 + future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), cacheExecutorService);
173 return future; 175 return future;
174 } 176 }
175 177
@@ -177,7 +179,7 @@ public class CachedAttributesService implements AttributesService { @@ -177,7 +179,7 @@ public class CachedAttributesService implements AttributesService {
177 public ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys) { 179 public ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys) {
178 validate(entityId, scope); 180 validate(entityId, scope);
179 ListenableFuture<List<Void>> future = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys); 181 ListenableFuture<List<Void>> future = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys);
180 - future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), MoreExecutors.directExecutor()); 182 + future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), cacheExecutorService);
181 return future; 183 return future;
182 } 184 }
183 185
  1 +/**
  2 + * Copyright © 2016-2021 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.dao.cache;
  17 +
  18 +import org.springframework.beans.factory.annotation.Value;
  19 +import org.springframework.stereotype.Component;
  20 +import org.thingsboard.common.util.AbstractListeningExecutor;
  21 +
  22 +@Component
  23 +public class CacheExecutorService extends AbstractListeningExecutor {
  24 +
  25 + @Value("${cache.maximumPoolSize}")
  26 + private int poolSize;
  27 +
  28 + @Override
  29 + protected int getThreadPollSize() {
  30 + return poolSize;
  31 + }
  32 +
  33 +}
@@ -10,6 +10,7 @@ audit-log.default_query_period=30 @@ -10,6 +10,7 @@ audit-log.default_query_period=30
10 audit-log.sink.type=none 10 audit-log.sink.type=none
11 11
12 cache.type=caffeine 12 cache.type=caffeine
  13 +cache.maximumPoolSize=16
13 #cache.type=redis 14 #cache.type=redis
14 15
15 caffeine.specs.relations.timeToLiveInMinutes=1440 16 caffeine.specs.relations.timeToLiveInMinutes=1440
  1 +/*
  2 + * Copyright © 2016-2021 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 +const config = require('config'),
  17 + logger = require('../config/logger')._logger('httpServer'),
  18 + express = require('express');
  19 +
  20 +const httpPort = Number(config.get('http_port'));
  21 +
  22 +const app = express();
  23 +
  24 +app.get('/livenessProbe', async (req, res) => {
  25 + const date = new Date();
  26 + const message = { now: date.toISOString() };
  27 + res.send(message);
  28 +})
  29 +
  30 +app.listen(httpPort, () => logger.info(`Started http endpoint on port ${httpPort}. Please, use /livenessProbe !`))
@@ -25,6 +25,7 @@ const config = require('config'), @@ -25,6 +25,7 @@ const config = require('config'),
25 Utils = require('./utils'), 25 Utils = require('./utils'),
26 JsExecutor = require('./jsExecutor'); 26 JsExecutor = require('./jsExecutor');
27 27
  28 +const statFrequency = Number(config.get('script.stat_print_frequency'));
28 const scriptBodyTraceFrequency = Number(config.get('script.script_body_trace_frequency')); 29 const scriptBodyTraceFrequency = Number(config.get('script.script_body_trace_frequency'));
29 const useSandbox = config.get('script.use_sandbox') === 'true'; 30 const useSandbox = config.get('script.use_sandbox') === 'true';
30 const maxActiveScripts = Number(config.get('script.max_active_scripts')); 31 const maxActiveScripts = Number(config.get('script.max_active_scripts'));
@@ -34,15 +35,15 @@ const slowQueryLogBody = config.get('script.slow_query_log_body') === 'true'; @@ -34,15 +35,15 @@ const slowQueryLogBody = config.get('script.slow_query_log_body') === 'true';
34 const {performance} = require('perf_hooks'); 35 const {performance} = require('perf_hooks');
35 36
36 function JsInvokeMessageProcessor(producer) { 37 function JsInvokeMessageProcessor(producer) {
37 - console.log("Producer:", producer);  
38 this.producer = producer; 38 this.producer = producer;
39 this.executor = new JsExecutor(useSandbox); 39 this.executor = new JsExecutor(useSandbox);
40 - this.scriptMap = {}; 40 + this.scriptMap = new Map();
41 this.scriptIds = []; 41 this.scriptIds = [];
42 this.executedScriptsCounter = 0; 42 this.executedScriptsCounter = 0;
  43 + this.lastStatTime = performance.now();
43 } 44 }
44 45
45 -JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) { 46 +JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function (message) {
46 var tStart = performance.now(); 47 var tStart = performance.now();
47 let requestId; 48 let requestId;
48 let responseTopic; 49 let responseTopic;
@@ -77,13 +78,13 @@ JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) { @@ -77,13 +78,13 @@ JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) {
77 var tFinish = performance.now(); 78 var tFinish = performance.now();
78 var tTook = tFinish - tStart; 79 var tTook = tFinish - tStart;
79 80
80 - if ( tTook > slowQueryLogMs ) { 81 + if (tTook > slowQueryLogMs) {
81 let functionName; 82 let functionName;
82 if (request.invokeRequest) { 83 if (request.invokeRequest) {
83 try { 84 try {
84 buf = Buffer.from(request.invokeRequest['functionName']); 85 buf = Buffer.from(request.invokeRequest['functionName']);
85 functionName = buf.toString('utf8'); 86 functionName = buf.toString('utf8');
86 - } catch (err){ 87 + } catch (err) {
87 logger.error('[%s] Failed to read functionName from message header: %s', requestId, err.message); 88 logger.error('[%s] Failed to read functionName from message header: %s', requestId, err.message);
88 logger.error(err.stack); 89 logger.error(err.stack);
89 } 90 }
@@ -96,7 +97,7 @@ JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) { @@ -96,7 +97,7 @@ JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) {
96 97
97 } 98 }
98 99
99 -JsInvokeMessageProcessor.prototype.processCompileRequest = function(requestId, responseTopic, headers, compileRequest) { 100 +JsInvokeMessageProcessor.prototype.processCompileRequest = function (requestId, responseTopic, headers, compileRequest) {
100 var scriptId = getScriptId(compileRequest); 101 var scriptId = getScriptId(compileRequest);
101 logger.debug('[%s] Processing compile request, scriptId: [%s]', requestId, scriptId); 102 logger.debug('[%s] Processing compile request, scriptId: [%s]', requestId, scriptId);
102 103
@@ -115,15 +116,20 @@ JsInvokeMessageProcessor.prototype.processCompileRequest = function(requestId, r @@ -115,15 +116,20 @@ JsInvokeMessageProcessor.prototype.processCompileRequest = function(requestId, r
115 ); 116 );
116 } 117 }
117 118
118 -JsInvokeMessageProcessor.prototype.processInvokeRequest = function(requestId, responseTopic, headers, invokeRequest) { 119 +JsInvokeMessageProcessor.prototype.processInvokeRequest = function (requestId, responseTopic, headers, invokeRequest) {
119 var scriptId = getScriptId(invokeRequest); 120 var scriptId = getScriptId(invokeRequest);
120 logger.debug('[%s] Processing invoke request, scriptId: [%s]', requestId, scriptId); 121 logger.debug('[%s] Processing invoke request, scriptId: [%s]', requestId, scriptId);
121 this.executedScriptsCounter++; 122 this.executedScriptsCounter++;
122 - if ( this.executedScriptsCounter >= scriptBodyTraceFrequency ) {  
123 - this.executedScriptsCounter = 0;  
124 - if (logger.levels[logger.level] >= logger.levels['debug']) {  
125 - logger.debug('[%s] Executing script body: [%s]', scriptId, invokeRequest.scriptBody);  
126 - } 123 + if (this.executedScriptsCounter % statFrequency == 0) {
  124 + const nowMs = performance.now();
  125 + const msSinceLastStat = nowMs - this.lastStatTime;
  126 + const requestsPerSec = msSinceLastStat == 0 ? statFrequency : statFrequency / msSinceLastStat * 1000;
  127 + this.lastStatTime = nowMs;
  128 + logger.info('STAT[%s]: requests [%s], took [%s]ms, request/s [%s]', this.executedScriptsCounter, statFrequency, msSinceLastStat, requestsPerSec);
  129 + }
  130 +
  131 + if (this.executedScriptsCounter % scriptBodyTraceFrequency == 0) {
  132 + logger.info('[%s] Executing script body: [%s]', scriptId, invokeRequest.scriptBody);
127 } 133 }
128 this.getOrCompileScript(scriptId, invokeRequest.scriptBody).then( 134 this.getOrCompileScript(scriptId, invokeRequest.scriptBody).then(
129 (script) => { 135 (script) => {
@@ -154,15 +160,15 @@ JsInvokeMessageProcessor.prototype.processInvokeRequest = function(requestId, re @@ -154,15 +160,15 @@ JsInvokeMessageProcessor.prototype.processInvokeRequest = function(requestId, re
154 ); 160 );
155 } 161 }
156 162
157 -JsInvokeMessageProcessor.prototype.processReleaseRequest = function(requestId, responseTopic, headers, releaseRequest) { 163 +JsInvokeMessageProcessor.prototype.processReleaseRequest = function (requestId, responseTopic, headers, releaseRequest) {
158 var scriptId = getScriptId(releaseRequest); 164 var scriptId = getScriptId(releaseRequest);
159 logger.debug('[%s] Processing release request, scriptId: [%s]', requestId, scriptId); 165 logger.debug('[%s] Processing release request, scriptId: [%s]', requestId, scriptId);
160 - if (this.scriptMap[scriptId]) { 166 + if (this.scriptMap.has(scriptId)) {
161 var index = this.scriptIds.indexOf(scriptId); 167 var index = this.scriptIds.indexOf(scriptId);
162 if (index > -1) { 168 if (index > -1) {
163 this.scriptIds.splice(index, 1); 169 this.scriptIds.splice(index, 1);
164 } 170 }
165 - delete this.scriptMap[scriptId]; 171 + this.scriptMap.delete(scriptId);
166 } 172 }
167 var releaseResponse = createReleaseResponse(scriptId, true); 173 var releaseResponse = createReleaseResponse(scriptId, true);
168 logger.debug('[%s] Sending success release response, scriptId: [%s]', requestId, scriptId); 174 logger.debug('[%s] Sending success release response, scriptId: [%s]', requestId, scriptId);
@@ -173,6 +179,7 @@ JsInvokeMessageProcessor.prototype.sendResponse = function (requestId, responseT @@ -173,6 +179,7 @@ JsInvokeMessageProcessor.prototype.sendResponse = function (requestId, responseT
173 var tStartSending = performance.now(); 179 var tStartSending = performance.now();
174 var remoteResponse = createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse); 180 var remoteResponse = createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse);
175 var rawResponse = Buffer.from(JSON.stringify(remoteResponse), 'utf8'); 181 var rawResponse = Buffer.from(JSON.stringify(remoteResponse), 'utf8');
  182 + logger.debug('[%s] Sending response to queue, scriptId: [%s]', requestId, scriptId);
176 this.producer.send(responseTopic, scriptId, rawResponse, headers).then( 183 this.producer.send(responseTopic, scriptId, rawResponse, headers).then(
177 () => { 184 () => {
178 logger.debug('[%s] Response sent to queue, took [%s]ms, scriptId: [%s]', requestId, (performance.now() - tStartSending), scriptId); 185 logger.debug('[%s] Response sent to queue, took [%s]ms, scriptId: [%s]', requestId, (performance.now() - tStartSending), scriptId);
@@ -186,16 +193,17 @@ JsInvokeMessageProcessor.prototype.sendResponse = function (requestId, responseT @@ -186,16 +193,17 @@ JsInvokeMessageProcessor.prototype.sendResponse = function (requestId, responseT
186 ); 193 );
187 } 194 }
188 195
189 -JsInvokeMessageProcessor.prototype.getOrCompileScript = function(scriptId, scriptBody) { 196 +JsInvokeMessageProcessor.prototype.getOrCompileScript = function (scriptId, scriptBody) {
190 var self = this; 197 var self = this;
191 - return new Promise(function(resolve, reject) {  
192 - if (self.scriptMap[scriptId]) {  
193 - resolve(self.scriptMap[scriptId]); 198 + return new Promise(function (resolve, reject) {
  199 + const script = self.scriptMap.get(scriptId);
  200 + if (script) {
  201 + resolve(script);
194 } else { 202 } else {
195 self.executor.compileScript(scriptBody).then( 203 self.executor.compileScript(scriptBody).then(
196 - (script) => {  
197 - self.cacheScript(scriptId, script);  
198 - resolve(script); 204 + (compiledScript) => {
  205 + self.cacheScript(scriptId, compiledScript);
  206 + resolve(compiledScript);
199 }, 207 },
200 (err) => { 208 (err) => {
201 reject(err); 209 reject(err);
@@ -205,56 +213,57 @@ JsInvokeMessageProcessor.prototype.getOrCompileScript = function(scriptId, scrip @@ -205,56 +213,57 @@ JsInvokeMessageProcessor.prototype.getOrCompileScript = function(scriptId, scrip
205 }); 213 });
206 } 214 }
207 215
208 -JsInvokeMessageProcessor.prototype.cacheScript = function(scriptId, script) {  
209 - if (!this.scriptMap[scriptId]) { 216 +JsInvokeMessageProcessor.prototype.cacheScript = function (scriptId, script) {
  217 + if (!this.scriptMap.has(scriptId)) {
210 this.scriptIds.push(scriptId); 218 this.scriptIds.push(scriptId);
211 while (this.scriptIds.length > maxActiveScripts) { 219 while (this.scriptIds.length > maxActiveScripts) {
212 logger.info('Active scripts count [%s] exceeds maximum limit [%s]', this.scriptIds.length, maxActiveScripts); 220 logger.info('Active scripts count [%s] exceeds maximum limit [%s]', this.scriptIds.length, maxActiveScripts);
213 const prevScriptId = this.scriptIds.shift(); 221 const prevScriptId = this.scriptIds.shift();
214 logger.info('Removing active script with id [%s]', prevScriptId); 222 logger.info('Removing active script with id [%s]', prevScriptId);
215 - delete this.scriptMap[prevScriptId]; 223 + this.scriptMap.delete(prevScriptId);
216 } 224 }
217 } 225 }
218 - this.scriptMap[scriptId] = script; 226 + this.scriptMap.set(scriptId, script);
  227 + logger.info("scriptMap size is [%s]", this.scriptMap.size);
219 } 228 }
220 229
221 function createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse) { 230 function createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse) {
222 const requestIdBits = Utils.UUIDToBits(requestId); 231 const requestIdBits = Utils.UUIDToBits(requestId);
223 return { 232 return {
224 - requestIdMSB: requestIdBits[0],  
225 - requestIdLSB: requestIdBits[1],  
226 - compileResponse: compileResponse,  
227 - invokeResponse: invokeResponse,  
228 - releaseResponse: releaseResponse 233 + requestIdMSB: requestIdBits[0],
  234 + requestIdLSB: requestIdBits[1],
  235 + compileResponse: compileResponse,
  236 + invokeResponse: invokeResponse,
  237 + releaseResponse: releaseResponse
229 }; 238 };
230 } 239 }
231 240
232 function createCompileResponse(scriptId, success, errorCode, err) { 241 function createCompileResponse(scriptId, success, errorCode, err) {
233 const scriptIdBits = Utils.UUIDToBits(scriptId); 242 const scriptIdBits = Utils.UUIDToBits(scriptId);
234 - return {  
235 - errorCode: errorCode,  
236 - success: success,  
237 - errorDetails: parseJsErrorDetails(err),  
238 - scriptIdMSB: scriptIdBits[0],  
239 - scriptIdLSB: scriptIdBits[1] 243 + return {
  244 + errorCode: errorCode,
  245 + success: success,
  246 + errorDetails: parseJsErrorDetails(err),
  247 + scriptIdMSB: scriptIdBits[0],
  248 + scriptIdLSB: scriptIdBits[1]
240 }; 249 };
241 } 250 }
242 251
243 function createInvokeResponse(result, success, errorCode, err) { 252 function createInvokeResponse(result, success, errorCode, err) {
244 - return {  
245 - errorCode: errorCode,  
246 - success: success,  
247 - errorDetails: parseJsErrorDetails(err),  
248 - result: result 253 + return {
  254 + errorCode: errorCode,
  255 + success: success,
  256 + errorDetails: parseJsErrorDetails(err),
  257 + result: result
249 }; 258 };
250 } 259 }
251 260
252 function createReleaseResponse(scriptId, success) { 261 function createReleaseResponse(scriptId, success) {
253 const scriptIdBits = Utils.UUIDToBits(scriptId); 262 const scriptIdBits = Utils.UUIDToBits(scriptId);
254 return { 263 return {
255 - success: success,  
256 - scriptIdMSB: scriptIdBits[0],  
257 - scriptIdLSB: scriptIdBits[1] 264 + success: success,
  265 + scriptIdMSB: scriptIdBits[0],
  266 + scriptIdLSB: scriptIdBits[1]
258 }; 267 };
259 } 268 }
260 269
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 16
17 queue_type: "TB_QUEUE_TYPE" #kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) 17 queue_type: "TB_QUEUE_TYPE" #kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ)
18 request_topic: "REMOTE_JS_EVAL_REQUEST_TOPIC" 18 request_topic: "REMOTE_JS_EVAL_REQUEST_TOPIC"
  19 +http_port: "HTTP_PORT" # /livenessProbe
19 20
20 js: 21 js:
21 response_poll_interval: "REMOTE_JS_RESPONSE_POLL_INTERVAL_MS" 22 response_poll_interval: "REMOTE_JS_RESPONSE_POLL_INTERVAL_MS"
@@ -26,6 +27,9 @@ kafka: @@ -26,6 +27,9 @@ kafka:
26 servers: "TB_KAFKA_SERVERS" 27 servers: "TB_KAFKA_SERVERS"
27 replication_factor: "TB_QUEUE_KAFKA_REPLICATION_FACTOR" 28 replication_factor: "TB_QUEUE_KAFKA_REPLICATION_FACTOR"
28 acks: "TB_KAFKA_ACKS" # -1 = all; 0 = no acknowledgments; 1 = only waits for the leader to acknowledge 29 acks: "TB_KAFKA_ACKS" # -1 = all; 0 = no acknowledgments; 1 = only waits for the leader to acknowledge
  30 + batch_size: "TB_KAFKA_BATCH_SIZE" # for producer
  31 + linger_ms: "TB_KAFKA_LINGER_MS" # for producer
  32 + partitions_consumed_concurrently: "TB_KAFKA_PARTITIONS_CONSUMED_CONCURRENTLY" # (EXPERIMENTAL) increase this value if you are planning to handle more than one partition (scale up, scale down) - this will decrease the latency
29 requestTimeout: "TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS" 33 requestTimeout: "TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS"
30 compression: "TB_QUEUE_KAFKA_COMPRESSION" # gzip or uncompressed 34 compression: "TB_QUEUE_KAFKA_COMPRESSION" # gzip or uncompressed
31 topic_properties: "TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES" 35 topic_properties: "TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES"
@@ -70,6 +74,7 @@ logger: @@ -70,6 +74,7 @@ logger:
70 74
71 script: 75 script:
72 use_sandbox: "SCRIPT_USE_SANDBOX" 76 use_sandbox: "SCRIPT_USE_SANDBOX"
  77 + stat_print_frequency: "SCRIPT_STAT_PRINT_FREQUENCY"
73 script_body_trace_frequency: "SCRIPT_BODY_TRACE_FREQUENCY" 78 script_body_trace_frequency: "SCRIPT_BODY_TRACE_FREQUENCY"
74 max_active_scripts: "MAX_ACTIVE_SCRIPTS" 79 max_active_scripts: "MAX_ACTIVE_SCRIPTS"
75 slow_query_log_ms: "SLOW_QUERY_LOG_MS" #1.123456 80 slow_query_log_ms: "SLOW_QUERY_LOG_MS" #1.123456
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 16
17 queue_type: "kafka" 17 queue_type: "kafka"
18 request_topic: "js_eval.requests" 18 request_topic: "js_eval.requests"
  19 +http_port: "8888" # /livenessProbe
19 20
20 js: 21 js:
21 response_poll_interval: "25" 22 response_poll_interval: "25"
@@ -26,6 +27,9 @@ kafka: @@ -26,6 +27,9 @@ kafka:
26 servers: "localhost:9092" 27 servers: "localhost:9092"
27 replication_factor: "1" 28 replication_factor: "1"
28 acks: "1" # -1 = all; 0 = no acknowledgments; 1 = only waits for the leader to acknowledge 29 acks: "1" # -1 = all; 0 = no acknowledgments; 1 = only waits for the leader to acknowledge
  30 + batch_size: "128" # for producer
  31 + linger_ms: "1" # for producer
  32 + partitions_consumed_concurrently: "1" # (EXPERIMENTAL) increase this value if you are planning to handle more than one partition (scale up, scale down) - this will decrease the latency
29 requestTimeout: "30000" # The default value in kafkajs is: 30000 33 requestTimeout: "30000" # The default value in kafkajs is: 30000
30 compression: "gzip" # gzip or uncompressed 34 compression: "gzip" # gzip or uncompressed
31 topic_properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100;min.insync.replicas:1" 35 topic_properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100;min.insync.replicas:1"
@@ -59,7 +63,8 @@ logger: @@ -59,7 +63,8 @@ logger:
59 63
60 script: 64 script:
61 use_sandbox: "true" 65 use_sandbox: "true"
62 - script_body_trace_frequency: "1000" 66 + script_body_trace_frequency: "10000"
  67 + stat_print_frequency: "10000"
63 max_active_scripts: "1000" 68 max_active_scripts: "1000"
64 - slow_query_log_ms: "1.000000" #millis 69 + slow_query_log_ms: "5.000000" #millis
65 slow_query_log_body: "false" 70 slow_query_log_body: "false"
@@ -18,6 +18,7 @@ @@ -18,6 +18,7 @@
18 "aws-sdk": "^2.741.0", 18 "aws-sdk": "^2.741.0",
19 "azure-sb": "^0.11.1", 19 "azure-sb": "^0.11.1",
20 "config": "^3.3.1", 20 "config": "^3.3.1",
  21 + "express": "^4.17.1",
21 "js-yaml": "^3.14.0", 22 "js-yaml": "^3.14.0",
22 "kafkajs": "^1.15.0", 23 "kafkajs": "^1.15.0",
23 "long": "^4.0.0", 24 "long": "^4.0.0",
@@ -23,8 +23,11 @@ const replicationFactor = Number(config.get('kafka.replication_factor')); @@ -23,8 +23,11 @@ const replicationFactor = Number(config.get('kafka.replication_factor'));
23 const topicProperties = config.get('kafka.topic_properties'); 23 const topicProperties = config.get('kafka.topic_properties');
24 const kafkaClientId = config.get('kafka.client_id'); 24 const kafkaClientId = config.get('kafka.client_id');
25 const acks = Number(config.get('kafka.acks')); 25 const acks = Number(config.get('kafka.acks'));
  26 +const maxBatchSize = Number(config.get('kafka.batch_size'));
  27 +const linger = Number(config.get('kafka.linger_ms'));
26 const requestTimeout = Number(config.get('kafka.requestTimeout')); 28 const requestTimeout = Number(config.get('kafka.requestTimeout'));
27 const compressionType = (config.get('kafka.compression') === "gzip") ? CompressionTypes.GZIP : CompressionTypes.None; 29 const compressionType = (config.get('kafka.compression') === "gzip") ? CompressionTypes.GZIP : CompressionTypes.None;
  30 +const partitionsConsumedConcurrently = Number(config.get('kafka.partitions_consumed_concurrently'));
28 31
29 let kafkaClient; 32 let kafkaClient;
30 let kafkaAdmin; 33 let kafkaAdmin;
@@ -33,22 +36,65 @@ let producer; @@ -33,22 +36,65 @@ let producer;
33 36
34 const configEntries = []; 37 const configEntries = [];
35 38
  39 +let batchMessages = [];
  40 +let sendLoopInstance;
  41 +
36 function KafkaProducer() { 42 function KafkaProducer() {
37 this.send = async (responseTopic, scriptId, rawResponse, headers) => { 43 this.send = async (responseTopic, scriptId, rawResponse, headers) => {
38 - return producer.send(  
39 - {  
40 - topic: responseTopic, 44 + logger.debug('Pending queue response, scriptId: [%s]', scriptId);
  45 + const message = {
  46 + topic: responseTopic,
  47 + messages: [{
  48 + key: scriptId,
  49 + value: rawResponse,
  50 + headers: headers.data
  51 + }]
  52 + };
  53 +
  54 + await pushMessageToSendLater(message);
  55 + }
  56 +}
  57 +
  58 +async function pushMessageToSendLater(message) {
  59 + batchMessages.push(message);
  60 + if (batchMessages.length >= maxBatchSize) {
  61 + await sendMessagesAsBatch(true);
  62 + }
  63 +}
  64 +
  65 +function sendLoopWithLinger() {
  66 + if (sendLoopInstance) {
  67 + clearTimeout(sendLoopInstance);
  68 + } else {
  69 + logger.debug("Starting new send loop with linger [%s]", linger)
  70 + }
  71 + sendLoopInstance = setTimeout(sendMessagesAsBatch, linger);
  72 +}
  73 +
  74 +async function sendMessagesAsBatch(isImmediately) {
  75 + if (sendLoopInstance) {
  76 + logger.debug("sendMessagesAsBatch: Clear sendLoop scheduler. Starting new send loop with linger [%s]", linger);
  77 + clearTimeout(sendLoopInstance);
  78 + }
  79 + sendLoopInstance = null;
  80 + if (batchMessages.length > 0) {
  81 + logger.debug('sendMessagesAsBatch, length: [%s], %s', batchMessages.length, isImmediately ? 'immediately' : '');
  82 + const messagesToSend = batchMessages;
  83 + batchMessages = [];
  84 + try {
  85 + await producer.sendBatch({
  86 + topicMessages: messagesToSend,
41 acks: acks, 87 acks: acks,
42 - compression: compressionType,  
43 - messages: [  
44 - {  
45 - key: scriptId,  
46 - value: rawResponse,  
47 - headers: headers.data  
48 - }  
49 - ]  
50 - }); 88 + compression: compressionType
  89 + })
  90 + logger.debug('Response batch sent to kafka, length: [%s]', messagesToSend.length);
  91 + } catch(err) {
  92 + logger.error('Failed batch send to kafka, length: [%s], pending to reprocess msgs', messagesToSend.length);
  93 + logger.error(err.stack);
  94 + batchMessages = messagesToSend.concat(batchMessages);
  95 + }
51 } 96 }
  97 + sendLoopWithLinger();
52 } 98 }
53 99
54 (async () => { 100 (async () => {
@@ -64,8 +110,8 @@ function KafkaProducer() { @@ -64,8 +110,8 @@ function KafkaProducer() {
64 110
65 let kafkaConfig = { 111 let kafkaConfig = {
66 brokers: kafkaBootstrapServers.split(','), 112 brokers: kafkaBootstrapServers.split(','),
67 - logLevel: logLevel.INFO,  
68 - logCreator: KafkaJsWinstonLogCreator 113 + logLevel: logLevel.INFO,
  114 + logCreator: KafkaJsWinstonLogCreator
69 }; 115 };
70 116
71 if (kafkaClientId) { 117 if (kafkaClientId) {
@@ -114,14 +160,45 @@ function KafkaProducer() { @@ -114,14 +160,45 @@ function KafkaProducer() {
114 160
115 consumer = kafkaClient.consumer({groupId: 'js-executor-group'}); 161 consumer = kafkaClient.consumer({groupId: 'js-executor-group'});
116 producer = kafkaClient.producer(); 162 producer = kafkaClient.producer();
  163 +
  164 +/*
  165 + //producer event instrumentation to debug
  166 + const { CONNECT } = producer.events;
  167 + const removeListenerC = producer.on(CONNECT, e => logger.info(`producer CONNECT`));
  168 + const { DISCONNECT } = producer.events;
  169 + const removeListenerD = producer.on(DISCONNECT, e => logger.info(`producer DISCONNECT`));
  170 + const { REQUEST } = producer.events;
  171 + const removeListenerR = producer.on(REQUEST, e => logger.info(`producer REQUEST ${e.payload.broker}`));
  172 + const { REQUEST_TIMEOUT } = producer.events;
  173 + const removeListenerRT = producer.on(REQUEST_TIMEOUT, e => logger.info(`producer REQUEST_TIMEOUT ${e.payload.broker}`));
  174 + const { REQUEST_QUEUE_SIZE } = producer.events;
  175 + const removeListenerRQS = producer.on(REQUEST_QUEUE_SIZE, e => logger.info(`producer REQUEST_QUEUE_SIZE ${e.payload.broker} size ${e.queueSize}`));
  176 +*/
  177 +
  178 +/*
  179 + //consumer event instrumentation to debug
  180 + const removeListeners = {}
  181 + const { FETCH_START } = consumer.events;
  182 + removeListeners[FETCH_START] = consumer.on(FETCH_START, e => logger.info(`consumer FETCH_START`));
  183 + const { FETCH } = consumer.events;
  184 + removeListeners[FETCH] = consumer.on(FETCH, e => logger.info(`consumer FETCH numberOfBatches ${e.payload.numberOfBatches} duration ${e.payload.duration}`));
  185 + const { START_BATCH_PROCESS } = consumer.events;
  186 + removeListeners[START_BATCH_PROCESS] = consumer.on(START_BATCH_PROCESS, e => logger.info(`consumer START_BATCH_PROCESS topic ${e.payload.topic} batchSize ${e.payload.batchSize}`));
  187 + const { END_BATCH_PROCESS } = consumer.events;
  188 + removeListeners[END_BATCH_PROCESS] = consumer.on(END_BATCH_PROCESS, e => logger.info(`consumer END_BATCH_PROCESS topic ${e.payload.topic} batchSize ${e.payload.batchSize}`));
  189 + const { COMMIT_OFFSETS } = consumer.events;
  190 + removeListeners[COMMIT_OFFSETS] = consumer.on(COMMIT_OFFSETS, e => logger.info(`consumer COMMIT_OFFSETS topics ${e.payload.topics}`));
  191 +*/
  192 +
117 const messageProcessor = new JsInvokeMessageProcessor(new KafkaProducer()); 193 const messageProcessor = new JsInvokeMessageProcessor(new KafkaProducer());
118 await consumer.connect(); 194 await consumer.connect();
119 await producer.connect(); 195 await producer.connect();
  196 + sendLoopWithLinger();
120 await consumer.subscribe({topic: requestTopic}); 197 await consumer.subscribe({topic: requestTopic});
121 198
122 logger.info('Started ThingsBoard JavaScript Executor Microservice.'); 199 logger.info('Started ThingsBoard JavaScript Executor Microservice.');
123 await consumer.run({ 200 await consumer.run({
124 - //partitionsConsumedConcurrently: 1, // Default: 1 201 + partitionsConsumedConcurrently: partitionsConsumedConcurrently,
125 eachMessage: async ({topic, partition, message}) => { 202 eachMessage: async ({topic, partition, message}) => {
126 let headers = message.headers; 203 let headers = message.headers;
127 let key = message.key; 204 let key = message.key;
@@ -197,6 +274,9 @@ async function disconnectProducer() { @@ -197,6 +274,9 @@ async function disconnectProducer() {
197 var _producer = producer; 274 var _producer = producer;
198 producer = null; 275 producer = null;
199 try { 276 try {
  277 + logger.info('Stopping loop...');
  278 + clearTimeout(sendLoopInstance);
  279 + await sendMessagesAsBatch();
200 await _producer.disconnect(); 280 await _producer.disconnect();
201 logger.info('Kafka Producer stopped.'); 281 logger.info('Kafka Producer stopped.');
202 } catch (e) { 282 } catch (e) {
@@ -51,3 +51,5 @@ switch (serviceType) { @@ -51,3 +51,5 @@ switch (serviceType) {
51 process.exit(-1); 51 process.exit(-1);
52 } 52 }
53 53
  54 +require('./api/httpServer');
  55 +
@@ -418,6 +418,14 @@ abort-controller@^3.0.0: @@ -418,6 +418,14 @@ abort-controller@^3.0.0:
418 dependencies: 418 dependencies:
419 event-target-shim "^5.0.0" 419 event-target-shim "^5.0.0"
420 420
  421 +accepts@~1.3.7:
  422 + version "1.3.7"
  423 + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
  424 + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
  425 + dependencies:
  426 + mime-types "~2.1.24"
  427 + negotiator "0.6.2"
  428 +
421 agent-base@6: 429 agent-base@6:
422 version "6.0.1" 430 version "6.0.1"
423 resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" 431 resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4"
@@ -487,6 +495,11 @@ argparse@^1.0.7: @@ -487,6 +495,11 @@ argparse@^1.0.7:
487 dependencies: 495 dependencies:
488 sprintf-js "~1.0.2" 496 sprintf-js "~1.0.2"
489 497
  498 +array-flatten@1.1.1:
  499 + version "1.1.1"
  500 + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
  501 + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
  502 +
490 array-union@^2.1.0: 503 array-union@^2.1.0:
491 version "2.1.0" 504 version "2.1.0"
492 resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" 505 resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
@@ -621,6 +634,22 @@ bluebird@^3.5.2: @@ -621,6 +634,22 @@ bluebird@^3.5.2:
621 resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" 634 resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
622 integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== 635 integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
623 636
  637 +body-parser@1.19.0:
  638 + version "1.19.0"
  639 + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
  640 + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
  641 + dependencies:
  642 + bytes "3.1.0"
  643 + content-type "~1.0.4"
  644 + debug "2.6.9"
  645 + depd "~1.1.2"
  646 + http-errors "1.7.2"
  647 + iconv-lite "0.4.24"
  648 + on-finished "~2.3.0"
  649 + qs "6.7.0"
  650 + raw-body "2.4.0"
  651 + type-is "~1.6.17"
  652 +
624 boxen@^4.2.0: 653 boxen@^4.2.0:
625 version "4.2.0" 654 version "4.2.0"
626 resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" 655 resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64"
@@ -682,6 +711,11 @@ byline@^5.0.0: @@ -682,6 +711,11 @@ byline@^5.0.0:
682 resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" 711 resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1"
683 integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= 712 integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=
684 713
  714 +bytes@3.1.0:
  715 + version "3.1.0"
  716 + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
  717 + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
  718 +
685 cacheable-request@^6.0.0: 719 cacheable-request@^6.0.0:
686 version "6.1.0" 720 version "6.1.0"
687 resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" 721 resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912"
@@ -838,6 +872,28 @@ configstore@^5.0.1: @@ -838,6 +872,28 @@ configstore@^5.0.1:
838 write-file-atomic "^3.0.0" 872 write-file-atomic "^3.0.0"
839 xdg-basedir "^4.0.0" 873 xdg-basedir "^4.0.0"
840 874
  875 +content-disposition@0.5.3:
  876 + version "0.5.3"
  877 + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
  878 + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
  879 + dependencies:
  880 + safe-buffer "5.1.2"
  881 +
  882 +content-type@~1.0.4:
  883 + version "1.0.4"
  884 + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
  885 + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
  886 +
  887 +cookie-signature@1.0.6:
  888 + version "1.0.6"
  889 + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
  890 + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
  891 +
  892 +cookie@0.4.0:
  893 + version "0.4.0"
  894 + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
  895 + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
  896 +
841 core-util-is@1.0.2, core-util-is@~1.0.0: 897 core-util-is@1.0.2, core-util-is@~1.0.0:
842 version "1.0.2" 898 version "1.0.2"
843 resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 899 resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -867,6 +923,13 @@ dateformat@1.0.2-1.2.3: @@ -867,6 +923,13 @@ dateformat@1.0.2-1.2.3:
867 dependencies: 923 dependencies:
868 ms "^2.1.1" 924 ms "^2.1.1"
869 925
  926 +debug@2.6.9, debug@^2.2.0, debug@~2.6.9:
  927 + version "2.6.9"
  928 + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
  929 + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
  930 + dependencies:
  931 + ms "2.0.0"
  932 +
870 debug@4, debug@^4.1.1: 933 debug@4, debug@^4.1.1:
871 version "4.1.1" 934 version "4.1.1"
872 resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" 935 resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
@@ -874,13 +937,6 @@ debug@4, debug@^4.1.1: @@ -874,13 +937,6 @@ debug@4, debug@^4.1.1:
874 dependencies: 937 dependencies:
875 ms "^2.1.1" 938 ms "^2.1.1"
876 939
877 -debug@^2.2.0, debug@~2.6.9:  
878 - version "2.6.9"  
879 - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"  
880 - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==  
881 - dependencies:  
882 - ms "2.0.0"  
883 -  
884 decamelize@^1.2.0: 940 decamelize@^1.2.0:
885 version "1.2.0" 941 version "1.2.0"
886 resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 942 resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -913,6 +969,16 @@ delayed-stream@~1.0.0: @@ -913,6 +969,16 @@ delayed-stream@~1.0.0:
913 resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 969 resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
914 integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= 970 integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
915 971
  972 +depd@~1.1.2:
  973 + version "1.1.2"
  974 + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
  975 + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
  976 +
  977 +destroy@~1.0.4:
  978 + version "1.0.4"
  979 + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
  980 + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
  981 +
916 dir-glob@^3.0.1: 982 dir-glob@^3.0.1:
917 version "3.0.1" 983 version "3.0.1"
918 resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" 984 resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -962,6 +1028,11 @@ ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: @@ -962,6 +1028,11 @@ ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11:
962 dependencies: 1028 dependencies:
963 safe-buffer "^5.0.1" 1029 safe-buffer "^5.0.1"
964 1030
  1031 +ee-first@1.1.1:
  1032 + version "1.1.1"
  1033 + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
  1034 + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
  1035 +
965 emoji-regex@^7.0.1: 1036 emoji-regex@^7.0.1:
966 version "7.0.3" 1037 version "7.0.3"
967 resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" 1038 resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
@@ -977,6 +1048,11 @@ enabled@2.0.x: @@ -977,6 +1048,11 @@ enabled@2.0.x:
977 resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" 1048 resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
978 integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== 1049 integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==
979 1050
  1051 +encodeurl@~1.0.2:
  1052 + version "1.0.2"
  1053 + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
  1054 + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
  1055 +
980 end-of-stream@^1.0.0, end-of-stream@^1.1.0: 1056 end-of-stream@^1.0.0, end-of-stream@^1.1.0:
981 version "1.4.4" 1057 version "1.4.4"
982 resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" 1058 resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
@@ -994,6 +1070,11 @@ escape-goat@^2.0.0: @@ -994,6 +1070,11 @@ escape-goat@^2.0.0:
994 resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" 1070 resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
995 integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== 1071 integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==
996 1072
  1073 +escape-html@~1.0.3:
  1074 + version "1.0.3"
  1075 + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
  1076 + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
  1077 +
997 escodegen@^1.14.1: 1078 escodegen@^1.14.1:
998 version "1.14.3" 1079 version "1.14.3"
999 resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" 1080 resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
@@ -1021,6 +1102,11 @@ esutils@^2.0.2: @@ -1021,6 +1102,11 @@ esutils@^2.0.2:
1021 resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" 1102 resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
1022 integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== 1103 integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
1023 1104
  1105 +etag@~1.8.1:
  1106 + version "1.8.1"
  1107 + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
  1108 + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
  1109 +
1024 event-target-shim@^5.0.0: 1110 event-target-shim@^5.0.0:
1025 version "5.0.1" 1111 version "5.0.1"
1026 resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" 1112 resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
@@ -1041,6 +1127,42 @@ expand-template@^2.0.3: @@ -1041,6 +1127,42 @@ expand-template@^2.0.3:
1041 resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" 1127 resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
1042 integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== 1128 integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
1043 1129
  1130 +express@^4.17.1:
  1131 + version "4.17.1"
  1132 + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
  1133 + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
  1134 + dependencies:
  1135 + accepts "~1.3.7"
  1136 + array-flatten "1.1.1"
  1137 + body-parser "1.19.0"
  1138 + content-disposition "0.5.3"
  1139 + content-type "~1.0.4"
  1140 + cookie "0.4.0"
  1141 + cookie-signature "1.0.6"
  1142 + debug "2.6.9"
  1143 + depd "~1.1.2"
  1144 + encodeurl "~1.0.2"
  1145 + escape-html "~1.0.3"
  1146 + etag "~1.8.1"
  1147 + finalhandler "~1.1.2"
  1148 + fresh "0.5.2"
  1149 + merge-descriptors "1.0.1"
  1150 + methods "~1.1.2"
  1151 + on-finished "~2.3.0"
  1152 + parseurl "~1.3.3"
  1153 + path-to-regexp "0.1.7"
  1154 + proxy-addr "~2.0.5"
  1155 + qs "6.7.0"
  1156 + range-parser "~1.2.1"
  1157 + safe-buffer "5.1.2"
  1158 + send "0.17.1"
  1159 + serve-static "1.14.1"
  1160 + setprototypeof "1.1.1"
  1161 + statuses "~1.5.0"
  1162 + type-is "~1.6.18"
  1163 + utils-merge "1.0.1"
  1164 + vary "~1.1.2"
  1165 +
1044 extend@^3.0.2, extend@~3.0.2: 1166 extend@^3.0.2, extend@~3.0.2:
1045 version "3.0.2" 1167 version "3.0.2"
1046 resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 1168 resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
@@ -1119,6 +1241,19 @@ fill-range@^7.0.1: @@ -1119,6 +1241,19 @@ fill-range@^7.0.1:
1119 dependencies: 1241 dependencies:
1120 to-regex-range "^5.0.1" 1242 to-regex-range "^5.0.1"
1121 1243
  1244 +finalhandler@~1.1.2:
  1245 + version "1.1.2"
  1246 + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
  1247 + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
  1248 + dependencies:
  1249 + debug "2.6.9"
  1250 + encodeurl "~1.0.2"
  1251 + escape-html "~1.0.3"
  1252 + on-finished "~2.3.0"
  1253 + parseurl "~1.3.3"
  1254 + statuses "~1.5.0"
  1255 + unpipe "~1.0.0"
  1256 +
1122 find-up@^4.1.0: 1257 find-up@^4.1.0:
1123 version "4.1.0" 1258 version "4.1.0"
1124 resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" 1259 resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
@@ -1155,6 +1290,16 @@ form-data@~2.3.2: @@ -1155,6 +1290,16 @@ form-data@~2.3.2:
1155 combined-stream "^1.0.6" 1290 combined-stream "^1.0.6"
1156 mime-types "^2.1.12" 1291 mime-types "^2.1.12"
1157 1292
  1293 +forwarded@0.2.0:
  1294 + version "0.2.0"
  1295 + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
  1296 + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
  1297 +
  1298 +fresh@0.5.2:
  1299 + version "0.5.2"
  1300 + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
  1301 + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
  1302 +
1158 from2@^2.3.0: 1303 from2@^2.3.0:
1159 version "2.3.0" 1304 version "2.3.0"
1160 resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" 1305 resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
@@ -1384,6 +1529,28 @@ http-cache-semantics@^4.0.0: @@ -1384,6 +1529,28 @@ http-cache-semantics@^4.0.0:
1384 resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" 1529 resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
1385 integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== 1530 integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
1386 1531
  1532 +http-errors@1.7.2:
  1533 + version "1.7.2"
  1534 + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
  1535 + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
  1536 + dependencies:
  1537 + depd "~1.1.2"
  1538 + inherits "2.0.3"
  1539 + setprototypeof "1.1.1"
  1540 + statuses ">= 1.5.0 < 2"
  1541 + toidentifier "1.0.0"
  1542 +
  1543 +http-errors@~1.7.2:
  1544 + version "1.7.3"
  1545 + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
  1546 + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
  1547 + dependencies:
  1548 + depd "~1.1.2"
  1549 + inherits "2.0.4"
  1550 + setprototypeof "1.1.1"
  1551 + statuses ">= 1.5.0 < 2"
  1552 + toidentifier "1.0.0"
  1553 +
1387 http-signature@~1.2.0: 1554 http-signature@~1.2.0:
1388 version "1.2.0" 1555 version "1.2.0"
1389 resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" 1556 resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
@@ -1401,6 +1568,13 @@ https-proxy-agent@^5.0.0: @@ -1401,6 +1568,13 @@ https-proxy-agent@^5.0.0:
1401 agent-base "6" 1568 agent-base "6"
1402 debug "4" 1569 debug "4"
1403 1570
  1571 +iconv-lite@0.4.24:
  1572 + version "0.4.24"
  1573 + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
  1574 + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
  1575 + dependencies:
  1576 + safer-buffer ">= 2.1.2 < 3"
  1577 +
1404 ieee754@1.1.13, ieee754@^1.1.4: 1578 ieee754@1.1.13, ieee754@^1.1.4:
1405 version "1.1.13" 1579 version "1.1.13"
1406 resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" 1580 resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
@@ -1431,7 +1605,7 @@ inherits@2.0.3: @@ -1431,7 +1605,7 @@ inherits@2.0.3:
1431 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 1605 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
1432 integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= 1606 integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
1433 1607
1434 -inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: 1608 +inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
1435 version "2.0.4" 1609 version "2.0.4"
1436 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 1610 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
1437 integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 1611 integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -1449,6 +1623,11 @@ into-stream@^5.1.1: @@ -1449,6 +1623,11 @@ into-stream@^5.1.1:
1449 from2 "^2.3.0" 1623 from2 "^2.3.0"
1450 p-is-promise "^3.0.0" 1624 p-is-promise "^3.0.0"
1451 1625
  1626 +ipaddr.js@1.9.1:
  1627 + version "1.9.1"
  1628 + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
  1629 + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
  1630 +
1452 is-arrayish@^0.3.1: 1631 is-arrayish@^0.3.1:
1453 version "0.3.2" 1632 version "0.3.2"
1454 resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" 1633 resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
@@ -1764,11 +1943,26 @@ make-dir@^3.0.0: @@ -1764,11 +1943,26 @@ make-dir@^3.0.0:
1764 dependencies: 1943 dependencies:
1765 semver "^6.0.0" 1944 semver "^6.0.0"
1766 1945
  1946 +media-typer@0.3.0:
  1947 + version "0.3.0"
  1948 + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
  1949 + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
  1950 +
  1951 +merge-descriptors@1.0.1:
  1952 + version "1.0.1"
  1953 + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
  1954 + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
  1955 +
1767 merge2@^1.3.0: 1956 merge2@^1.3.0:
1768 version "1.4.1" 1957 version "1.4.1"
1769 resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" 1958 resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
1770 integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== 1959 integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
1771 1960
  1961 +methods@~1.1.2:
  1962 + version "1.1.2"
  1963 + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
  1964 + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
  1965 +
1772 micromatch@^4.0.2: 1966 micromatch@^4.0.2:
1773 version "4.0.2" 1967 version "4.0.2"
1774 resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" 1968 resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
@@ -1782,6 +1976,11 @@ mime-db@1.44.0: @@ -1782,6 +1976,11 @@ mime-db@1.44.0:
1782 resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" 1976 resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
1783 integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== 1977 integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
1784 1978
  1979 +mime-db@1.48.0:
  1980 + version "1.48.0"
  1981 + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d"
  1982 + integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==
  1983 +
1785 mime-types@^2.1.12, mime-types@~2.1.19: 1984 mime-types@^2.1.12, mime-types@~2.1.19:
1786 version "2.1.27" 1985 version "2.1.27"
1787 resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" 1986 resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
@@ -1789,6 +1988,18 @@ mime-types@^2.1.12, mime-types@~2.1.19: @@ -1789,6 +1988,18 @@ mime-types@^2.1.12, mime-types@~2.1.19:
1789 dependencies: 1988 dependencies:
1790 mime-db "1.44.0" 1989 mime-db "1.44.0"
1791 1990
  1991 +mime-types@~2.1.24:
  1992 + version "2.1.31"
  1993 + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b"
  1994 + integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==
  1995 + dependencies:
  1996 + mime-db "1.48.0"
  1997 +
  1998 +mime@1.6.0:
  1999 + version "1.6.0"
  2000 + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
  2001 + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
  2002 +
1792 mime@^2.2.0: 2003 mime@^2.2.0:
1793 version "2.4.6" 2004 version "2.4.6"
1794 resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" 2005 resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
@@ -1833,6 +2044,11 @@ ms@2.0.0: @@ -1833,6 +2044,11 @@ ms@2.0.0:
1833 resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 2044 resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
1834 integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 2045 integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
1835 2046
  2047 +ms@2.1.1:
  2048 + version "2.1.1"
  2049 + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
  2050 + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
  2051 +
1836 ms@^2.1.1: 2052 ms@^2.1.1:
1837 version "2.1.2" 2053 version "2.1.2"
1838 resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 2054 resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@@ -1846,6 +2062,11 @@ multistream@^2.1.1: @@ -1846,6 +2062,11 @@ multistream@^2.1.1:
1846 inherits "^2.0.1" 2062 inherits "^2.0.1"
1847 readable-stream "^2.0.5" 2063 readable-stream "^2.0.5"
1848 2064
  2065 +negotiator@0.6.2:
  2066 + version "0.6.2"
  2067 + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
  2068 + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
  2069 +
1849 node-fetch@^2.3.0, node-fetch@^2.6.0: 2070 node-fetch@^2.3.0, node-fetch@^2.6.0:
1850 version "2.6.0" 2071 version "2.6.0"
1851 resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" 2072 resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
@@ -1899,6 +2120,13 @@ object-hash@^2.0.1: @@ -1899,6 +2120,13 @@ object-hash@^2.0.1:
1899 resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea" 2120 resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea"
1900 integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg== 2121 integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==
1901 2122
  2123 +on-finished@~2.3.0:
  2124 + version "2.3.0"
  2125 + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
  2126 + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
  2127 + dependencies:
  2128 + ee-first "1.1.1"
  2129 +
1902 once@^1.3.1, once@^1.4.0: 2130 once@^1.3.1, once@^1.4.0:
1903 version "1.4.0" 2131 version "1.4.0"
1904 resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 2132 resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -1974,6 +2202,11 @@ package-json@^6.3.0: @@ -1974,6 +2202,11 @@ package-json@^6.3.0:
1974 registry-url "^5.0.0" 2202 registry-url "^5.0.0"
1975 semver "^6.2.0" 2203 semver "^6.2.0"
1976 2204
  2205 +parseurl@~1.3.3:
  2206 + version "1.3.3"
  2207 + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
  2208 + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
  2209 +
1977 path-exists@^4.0.0: 2210 path-exists@^4.0.0:
1978 version "4.0.0" 2211 version "4.0.0"
1979 resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 2212 resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -1984,6 +2217,11 @@ path-parse@^1.0.6: @@ -1984,6 +2217,11 @@ path-parse@^1.0.6:
1984 resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" 2217 resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
1985 integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== 2218 integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
1986 2219
  2220 +path-to-regexp@0.1.7:
  2221 + version "0.1.7"
  2222 + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
  2223 + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
  2224 +
1987 path-type@^4.0.0: 2225 path-type@^4.0.0:
1988 version "4.0.0" 2226 version "4.0.0"
1989 resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" 2227 resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -2079,6 +2317,14 @@ protobufjs@^6.8.6, protobufjs@^6.9.0: @@ -2079,6 +2317,14 @@ protobufjs@^6.8.6, protobufjs@^6.9.0:
2079 "@types/node" "^13.7.0" 2317 "@types/node" "^13.7.0"
2080 long "^4.0.0" 2318 long "^4.0.0"
2081 2319
  2320 +proxy-addr@~2.0.5:
  2321 + version "2.0.7"
  2322 + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
  2323 + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
  2324 + dependencies:
  2325 + forwarded "0.2.0"
  2326 + ipaddr.js "1.9.1"
  2327 +
2082 psl@^1.1.28, psl@^1.1.33: 2328 psl@^1.1.28, psl@^1.1.33:
2083 version "1.8.0" 2329 version "1.8.0"
2084 resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" 2330 resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
@@ -2114,6 +2360,11 @@ pupa@^2.0.1: @@ -2114,6 +2360,11 @@ pupa@^2.0.1:
2114 dependencies: 2360 dependencies:
2115 escape-goat "^2.0.0" 2361 escape-goat "^2.0.0"
2116 2362
  2363 +qs@6.7.0:
  2364 + version "6.7.0"
  2365 + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
  2366 + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
  2367 +
2117 qs@~6.5.2: 2368 qs@~6.5.2:
2118 version "6.5.2" 2369 version "6.5.2"
2119 resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" 2370 resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@@ -2129,6 +2380,21 @@ querystringify@^2.1.1: @@ -2129,6 +2380,21 @@ querystringify@^2.1.1:
2129 resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" 2380 resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
2130 integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== 2381 integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
2131 2382
  2383 +range-parser@~1.2.1:
  2384 + version "1.2.1"
  2385 + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
  2386 + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
  2387 +
  2388 +raw-body@2.4.0:
  2389 + version "2.4.0"
  2390 + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
  2391 + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
  2392 + dependencies:
  2393 + bytes "3.1.0"
  2394 + http-errors "1.7.2"
  2395 + iconv-lite "0.4.24"
  2396 + unpipe "1.0.0"
  2397 +
2132 rc@^1.2.8: 2398 rc@^1.2.8:
2133 version "1.2.8" 2399 version "1.2.8"
2134 resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" 2400 resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
@@ -2292,17 +2558,17 @@ run-parallel@^1.1.9: @@ -2292,17 +2558,17 @@ run-parallel@^1.1.9:
2292 resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" 2558 resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
2293 integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== 2559 integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==
2294 2560
  2561 +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@~5.1.2:
  2562 + version "5.1.2"
  2563 + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
  2564 + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
  2565 +
2295 safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: 2566 safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
2296 version "5.2.1" 2567 version "5.2.1"
2297 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 2568 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
2298 integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 2569 integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
2299 2570
2300 -safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@~5.1.2:  
2301 - version "5.1.2"  
2302 - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"  
2303 - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==  
2304 -  
2305 -safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: 2571 +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
2306 version "2.1.2" 2572 version "2.1.2"
2307 resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 2573 resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
2308 integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 2574 integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@@ -2339,11 +2605,45 @@ semver@^7.1.3: @@ -2339,11 +2605,45 @@ semver@^7.1.3:
2339 resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" 2605 resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
2340 integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== 2606 integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
2341 2607
  2608 +send@0.17.1:
  2609 + version "0.17.1"
  2610 + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
  2611 + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
  2612 + dependencies:
  2613 + debug "2.6.9"
  2614 + depd "~1.1.2"
  2615 + destroy "~1.0.4"
  2616 + encodeurl "~1.0.2"
  2617 + escape-html "~1.0.3"
  2618 + etag "~1.8.1"
  2619 + fresh "0.5.2"
  2620 + http-errors "~1.7.2"
  2621 + mime "1.6.0"
  2622 + ms "2.1.1"
  2623 + on-finished "~2.3.0"
  2624 + range-parser "~1.2.1"
  2625 + statuses "~1.5.0"
  2626 +
  2627 +serve-static@1.14.1:
  2628 + version "1.14.1"
  2629 + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
  2630 + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
  2631 + dependencies:
  2632 + encodeurl "~1.0.2"
  2633 + escape-html "~1.0.3"
  2634 + parseurl "~1.3.3"
  2635 + send "0.17.1"
  2636 +
2342 set-blocking@^2.0.0: 2637 set-blocking@^2.0.0:
2343 version "2.0.0" 2638 version "2.0.0"
2344 resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 2639 resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
2345 integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= 2640 integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
2346 2641
  2642 +setprototypeof@1.1.1:
  2643 + version "1.1.1"
  2644 + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
  2645 + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
  2646 +
2347 signal-exit@^3.0.2: 2647 signal-exit@^3.0.2:
2348 version "3.0.3" 2648 version "3.0.3"
2349 resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" 2649 resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
@@ -2391,6 +2691,11 @@ stack-trace@0.0.x: @@ -2391,6 +2691,11 @@ stack-trace@0.0.x:
2391 resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" 2691 resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
2392 integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= 2692 integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
2393 2693
  2694 +"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
  2695 + version "1.5.0"
  2696 + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
  2697 + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
  2698 +
2394 stream-browserify@^2.0.2: 2699 stream-browserify@^2.0.2:
2395 version "2.0.2" 2700 version "2.0.2"
2396 resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" 2701 resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"
@@ -2513,6 +2818,11 @@ to-regex-range@^5.0.1: @@ -2513,6 +2818,11 @@ to-regex-range@^5.0.1:
2513 dependencies: 2818 dependencies:
2514 is-number "^7.0.0" 2819 is-number "^7.0.0"
2515 2820
  2821 +toidentifier@1.0.0:
  2822 + version "1.0.0"
  2823 + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
  2824 + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
  2825 +
2516 touch@^3.1.0: 2826 touch@^3.1.0:
2517 version "3.1.0" 2827 version "3.1.0"
2518 resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" 2828 resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
@@ -2581,6 +2891,14 @@ type-fest@^0.8.1: @@ -2581,6 +2891,14 @@ type-fest@^0.8.1:
2581 resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" 2891 resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
2582 integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== 2892 integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
2583 2893
  2894 +type-is@~1.6.17, type-is@~1.6.18:
  2895 + version "1.6.18"
  2896 + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
  2897 + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
  2898 + dependencies:
  2899 + media-typer "0.3.0"
  2900 + mime-types "~2.1.24"
  2901 +
2584 typedarray-to-buffer@^3.1.5: 2902 typedarray-to-buffer@^3.1.5:
2585 version "3.1.5" 2903 version "3.1.5"
2586 resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" 2904 resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
@@ -2636,6 +2954,11 @@ universalify@^1.0.0: @@ -2636,6 +2954,11 @@ universalify@^1.0.0:
2636 resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" 2954 resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d"
2637 integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== 2955 integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==
2638 2956
  2957 +unpipe@1.0.0, unpipe@~1.0.0:
  2958 + version "1.0.0"
  2959 + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
  2960 + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
  2961 +
2639 update-notifier@^4.0.0: 2962 update-notifier@^4.0.0:
2640 version "4.1.1" 2963 version "4.1.1"
2641 resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.1.tgz#895fc8562bbe666179500f9f2cebac4f26323746" 2964 resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.1.tgz#895fc8562bbe666179500f9f2cebac4f26323746"
@@ -2705,6 +3028,11 @@ util@^0.11.1: @@ -2705,6 +3028,11 @@ util@^0.11.1:
2705 dependencies: 3028 dependencies:
2706 inherits "2.0.3" 3029 inherits "2.0.3"
2707 3030
  3031 +utils-merge@1.0.1:
  3032 + version "1.0.1"
  3033 + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
  3034 + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
  3035 +
2708 uuid-parse@^1.1.0: 3036 uuid-parse@^1.1.0:
2709 version "1.1.0" 3037 version "1.1.0"
2710 resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b" 3038 resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b"
@@ -2735,6 +3063,11 @@ validator@^9.4.1: @@ -2735,6 +3063,11 @@ validator@^9.4.1:
2735 resolved "https://registry.yarnpkg.com/validator/-/validator-9.4.1.tgz#abf466d398b561cd243050112c6ff1de6cc12663" 3063 resolved "https://registry.yarnpkg.com/validator/-/validator-9.4.1.tgz#abf466d398b561cd243050112c6ff1de6cc12663"
2736 integrity sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA== 3064 integrity sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==
2737 3065
  3066 +vary@~1.1.2:
  3067 + version "1.1.2"
  3068 + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
  3069 + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
  3070 +
2738 verror@1.10.0: 3071 verror@1.10.0:
2739 version "1.10.0" 3072 version "1.10.0"
2740 resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" 3073 resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
@@ -1010,6 +1010,11 @@ @@ -1010,6 +1010,11 @@
1010 </exclusions> 1010 </exclusions>
1011 </dependency> 1011 </dependency>
1012 <dependency> 1012 <dependency>
  1013 + <groupId>org.springframework</groupId>
  1014 + <artifactId>spring-core</artifactId>
  1015 + <version>${spring.version}</version>
  1016 + </dependency>
  1017 + <dependency>
1013 <groupId>org.springframework.boot</groupId> 1018 <groupId>org.springframework.boot</groupId>
1014 <artifactId>spring-boot-starter-web</artifactId> 1019 <artifactId>spring-boot-starter-web</artifactId>
1015 <version>${spring-boot.version}</version> 1020 <version>${spring-boot.version}</version>
@@ -104,6 +104,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -104,6 +104,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
104 import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; 104 import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
105 import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; 105 import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
106 import org.thingsboard.server.common.data.oauth2.OAuth2Info; 106 import org.thingsboard.server.common.data.oauth2.OAuth2Info;
  107 +import org.thingsboard.server.common.data.oauth2.PlatformType;
107 import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; 108 import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
108 import org.thingsboard.server.common.data.ota.OtaPackageType; 109 import org.thingsboard.server.common.data.ota.OtaPackageType;
109 import org.thingsboard.server.common.data.page.PageData; 110 import org.thingsboard.server.common.data.page.PageData;
@@ -1257,7 +1258,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { @@ -1257,7 +1258,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
1257 params.put("deviceProfileId", deviceProfileId.getId().toString()); 1258 params.put("deviceProfileId", deviceProfileId.getId().toString());
1258 1259
1259 return restTemplate.exchange( 1260 return restTemplate.exchange(
1260 - baseURL + "/api/devices/count/{otaPackageType}?deviceProfileId={deviceProfileId}", 1261 + baseURL + "/api/devices/count/{otaPackageType}/{deviceProfileId}",
1261 HttpMethod.GET, 1262 HttpMethod.GET,
1262 HttpEntity.EMPTY, 1263 HttpEntity.EMPTY,
1263 new ParameterizedTypeReference<Long>() { 1264 new ParameterizedTypeReference<Long>() {
@@ -1772,7 +1773,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { @@ -1772,7 +1773,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
1772 }).getBody(); 1773 }).getBody();
1773 } 1774 }
1774 1775
1775 - public List<OAuth2ClientInfo> getOAuth2Clients(String pkgName) { 1776 + public List<OAuth2ClientInfo> getOAuth2Clients(String pkgName, PlatformType platformType) {
1776 Map<String, String> params = new HashMap<>(); 1777 Map<String, String> params = new HashMap<>();
1777 StringBuilder urlBuilder = new StringBuilder(baseURL); 1778 StringBuilder urlBuilder = new StringBuilder(baseURL);
1778 urlBuilder.append("/api/noauth/oauth2Clients"); 1779 urlBuilder.append("/api/noauth/oauth2Clients");
@@ -1780,6 +1781,15 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { @@ -1780,6 +1781,15 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
1780 urlBuilder.append("?pkgName={pkgName}"); 1781 urlBuilder.append("?pkgName={pkgName}");
1781 params.put("pkgName", pkgName); 1782 params.put("pkgName", pkgName);
1782 } 1783 }
  1784 + if (platformType != null) {
  1785 + if (pkgName != null) {
  1786 + urlBuilder.append("&");
  1787 + } else {
  1788 + urlBuilder.append("?");
  1789 + }
  1790 + urlBuilder.append("platform={platform}");
  1791 + params.put("platform", platformType.name());
  1792 + }
1783 return restTemplate.exchange( 1793 return restTemplate.exchange(
1784 urlBuilder.toString(), 1794 urlBuilder.toString(),
1785 HttpMethod.POST, 1795 HttpMethod.POST,
@@ -19,29 +19,22 @@ import com.fasterxml.jackson.databind.JsonNode; @@ -19,29 +19,22 @@ import com.fasterxml.jackson.databind.JsonNode;
19 import com.google.common.util.concurrent.ListenableFuture; 19 import com.google.common.util.concurrent.ListenableFuture;
20 import org.thingsboard.server.common.msg.TbMsg; 20 import org.thingsboard.server.common.msg.TbMsg;
21 21
22 -import javax.script.ScriptException;  
23 import java.util.List; 22 import java.util.List;
24 import java.util.Set; 23 import java.util.Set;
25 24
26 public interface ScriptEngine { 25 public interface ScriptEngine {
27 26
28 - List<TbMsg> executeUpdate(TbMsg msg) throws ScriptException;  
29 -  
30 ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg); 27 ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg);
31 28
32 - TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException;  
33 -  
34 - boolean executeFilter(TbMsg msg) throws ScriptException; 29 + ListenableFuture<TbMsg> executeGenerateAsync(TbMsg prevMsg);
35 30
36 ListenableFuture<Boolean> executeFilterAsync(TbMsg msg); 31 ListenableFuture<Boolean> executeFilterAsync(TbMsg msg);
37 32
38 - Set<String> executeSwitch(TbMsg msg) throws ScriptException;  
39 -  
40 - JsonNode executeJson(TbMsg msg) throws ScriptException; 33 + ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg);
41 34
42 - ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) throws ScriptException; 35 + ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg);
43 36
44 - String executeToString(TbMsg msg) throws ScriptException; 37 + ListenableFuture<String> executeToStringAsync(TbMsg msg);
45 38
46 void destroy(); 39 void destroy();
47 40
@@ -214,6 +214,10 @@ public interface TbContext { @@ -214,6 +214,10 @@ public interface TbContext {
214 214
215 EdgeEventService getEdgeEventService(); 215 EdgeEventService getEdgeEventService();
216 216
  217 + /**
  218 + * Js script executors call are completely asynchronous
  219 + * */
  220 + @Deprecated
217 ListeningExecutor getJsExecutor(); 221 ListeningExecutor getJsExecutor();
218 222
219 ListeningExecutor getMailExecutor(); 223 ListeningExecutor getMailExecutor();
@@ -15,8 +15,11 @@ @@ -15,8 +15,11 @@
15 */ 15 */
16 package org.thingsboard.rule.engine.action; 16 package org.thingsboard.rule.engine.action;
17 17
  18 +import com.google.common.util.concurrent.FutureCallback;
  19 +import com.google.common.util.concurrent.Futures;
  20 +import com.google.common.util.concurrent.MoreExecutors;
18 import lombok.extern.slf4j.Slf4j; 21 import lombok.extern.slf4j.Slf4j;
19 -import org.thingsboard.common.util.ListeningExecutor; 22 +import org.checkerframework.checker.nullness.qual.Nullable;
20 import org.thingsboard.rule.engine.api.RuleNode; 23 import org.thingsboard.rule.engine.api.RuleNode;
21 import org.thingsboard.rule.engine.api.ScriptEngine; 24 import org.thingsboard.rule.engine.api.ScriptEngine;
22 import org.thingsboard.rule.engine.api.TbContext; 25 import org.thingsboard.rule.engine.api.TbContext;
@@ -55,18 +58,21 @@ public class TbLogNode implements TbNode { @@ -55,18 +58,21 @@ public class TbLogNode implements TbNode {
55 58
56 @Override 59 @Override
57 public void onMsg(TbContext ctx, TbMsg msg) { 60 public void onMsg(TbContext ctx, TbMsg msg) {
58 - ListeningExecutor jsExecutor = ctx.getJsExecutor();  
59 ctx.logJsEvalRequest(); 61 ctx.logJsEvalRequest();
60 - withCallback(jsExecutor.executeAsync(() -> jsEngine.executeToString(msg)),  
61 - toString -> {  
62 - ctx.logJsEvalResponse();  
63 - log.info(toString);  
64 - ctx.tellSuccess(msg);  
65 - },  
66 - t -> {  
67 - ctx.logJsEvalResponse();  
68 - ctx.tellFailure(msg, t);  
69 - }); 62 + Futures.addCallback(jsEngine.executeToStringAsync(msg), new FutureCallback<String>() {
  63 + @Override
  64 + public void onSuccess(@Nullable String result) {
  65 + ctx.logJsEvalResponse();
  66 + log.info(result);
  67 + ctx.tellSuccess(msg);
  68 + }
  69 +
  70 + @Override
  71 + public void onFailure(Throwable t) {
  72 + ctx.logJsEvalResponse();
  73 + ctx.tellFailure(msg, t);
  74 + }
  75 + }, MoreExecutors.directExecutor()); //usually js responses runs on js callback executor
70 } 76 }
71 77
72 @Override 78 @Override
@@ -15,9 +15,12 @@ @@ -15,9 +15,12 @@
15 */ 15 */
16 package org.thingsboard.rule.engine.debug; 16 package org.thingsboard.rule.engine.debug;
17 17
  18 +import com.google.common.util.concurrent.Futures;
18 import com.google.common.util.concurrent.ListenableFuture; 19 import com.google.common.util.concurrent.ListenableFuture;
  20 +import com.google.common.util.concurrent.MoreExecutors;
19 import lombok.extern.slf4j.Slf4j; 21 import lombok.extern.slf4j.Slf4j;
20 import org.springframework.util.StringUtils; 22 import org.springframework.util.StringUtils;
  23 +import org.thingsboard.common.util.TbStopWatch;
21 import org.thingsboard.rule.engine.api.RuleNode; 24 import org.thingsboard.rule.engine.api.RuleNode;
22 import org.thingsboard.rule.engine.api.ScriptEngine; 25 import org.thingsboard.rule.engine.api.ScriptEngine;
23 import org.thingsboard.rule.engine.api.TbContext; 26 import org.thingsboard.rule.engine.api.TbContext;
@@ -35,6 +38,7 @@ import org.thingsboard.server.common.msg.queue.ServiceQueue; @@ -35,6 +38,7 @@ import org.thingsboard.server.common.msg.queue.ServiceQueue;
35 38
36 import java.util.UUID; 39 import java.util.UUID;
37 import java.util.concurrent.TimeUnit; 40 import java.util.concurrent.TimeUnit;
  41 +import java.util.concurrent.atomic.AtomicBoolean;
38 42
39 import static org.thingsboard.common.util.DonAsynchron.withCallback; 43 import static org.thingsboard.common.util.DonAsynchron.withCallback;
40 import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; 44 import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
@@ -64,10 +68,11 @@ public class TbMsgGeneratorNode implements TbNode { @@ -64,10 +68,11 @@ public class TbMsgGeneratorNode implements TbNode {
64 private EntityId originatorId; 68 private EntityId originatorId;
65 private UUID nextTickId; 69 private UUID nextTickId;
66 private TbMsg prevMsg; 70 private TbMsg prevMsg;
67 - private volatile boolean initialized; 71 + private final AtomicBoolean initialized = new AtomicBoolean(false);
68 72
69 @Override 73 @Override
70 public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { 74 public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
  75 + log.trace("init generator with config {}", configuration);
71 this.config = TbNodeUtils.convert(configuration, TbMsgGeneratorNodeConfiguration.class); 76 this.config = TbNodeUtils.convert(configuration, TbMsgGeneratorNodeConfiguration.class);
72 this.delay = TimeUnit.SECONDS.toMillis(config.getPeriodInSeconds()); 77 this.delay = TimeUnit.SECONDS.toMillis(config.getPeriodInSeconds());
73 this.currentMsgCount = 0; 78 this.currentMsgCount = 0;
@@ -81,35 +86,39 @@ public class TbMsgGeneratorNode implements TbNode { @@ -81,35 +86,39 @@ public class TbMsgGeneratorNode implements TbNode {
81 86
82 @Override 87 @Override
83 public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) { 88 public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) {
  89 + log.trace("onPartitionChangeMsg, PartitionChangeMsg {}, config {}", msg, config);
84 updateGeneratorState(ctx); 90 updateGeneratorState(ctx);
85 } 91 }
86 92
87 private void updateGeneratorState(TbContext ctx) { 93 private void updateGeneratorState(TbContext ctx) {
  94 + log.trace("updateGeneratorState, config {}", config);
88 if (ctx.isLocalEntity(originatorId)) { 95 if (ctx.isLocalEntity(originatorId)) {
89 - if (!initialized) {  
90 - initialized = true; 96 + if (initialized.compareAndSet(false, true)) {
91 this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType"); 97 this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType");
92 scheduleTickMsg(ctx); 98 scheduleTickMsg(ctx);
93 } 99 }
94 - } else if (initialized) {  
95 - initialized = false; 100 + } else if (initialized.compareAndSet(true, false)) {
96 destroy(); 101 destroy();
97 } 102 }
98 } 103 }
99 104
100 @Override 105 @Override
101 public void onMsg(TbContext ctx, TbMsg msg) { 106 public void onMsg(TbContext ctx, TbMsg msg) {
102 - if (initialized && msg.getType().equals(TB_MSG_GENERATOR_NODE_MSG) && msg.getId().equals(nextTickId)) { 107 + log.trace("onMsg, config {}, msg {}", config, msg);
  108 + if (initialized.get() && msg.getType().equals(TB_MSG_GENERATOR_NODE_MSG) && msg.getId().equals(nextTickId)) {
  109 + TbStopWatch sw = TbStopWatch.startNew();
103 withCallback(generate(ctx, msg), 110 withCallback(generate(ctx, msg),
104 m -> { 111 m -> {
105 - if (initialized && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) { 112 + log.trace("onMsg onSuccess callback, took {}ms, config {}, msg {}", sw.stopAndGetTotalTimeMillis(), config, msg);
  113 + if (initialized.get() && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) {
106 ctx.enqueueForTellNext(m, SUCCESS); 114 ctx.enqueueForTellNext(m, SUCCESS);
107 scheduleTickMsg(ctx); 115 scheduleTickMsg(ctx);
108 currentMsgCount++; 116 currentMsgCount++;
109 } 117 }
110 }, 118 },
111 t -> { 119 t -> {
112 - if (initialized && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) { 120 + log.warn("onMsg onFailure callback, took {}ms, config {}, msg {}, exception {}", sw.stopAndGetTotalTimeMillis(), config, msg, t);
  121 + if (initialized.get() && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) {
113 ctx.tellFailure(msg, t); 122 ctx.tellFailure(msg, t);
114 scheduleTickMsg(ctx); 123 scheduleTickMsg(ctx);
115 currentMsgCount++; 124 currentMsgCount++;
@@ -119,6 +128,7 @@ public class TbMsgGeneratorNode implements TbNode { @@ -119,6 +128,7 @@ public class TbMsgGeneratorNode implements TbNode {
119 } 128 }
120 129
121 private void scheduleTickMsg(TbContext ctx) { 130 private void scheduleTickMsg(TbContext ctx) {
  131 + log.trace("scheduleTickMsg, config {}", config);
122 long curTs = System.currentTimeMillis(); 132 long curTs = System.currentTimeMillis();
123 if (lastScheduledTs == 0L) { 133 if (lastScheduledTs == 0L) {
124 lastScheduledTs = curTs; 134 lastScheduledTs = curTs;
@@ -131,22 +141,26 @@ public class TbMsgGeneratorNode implements TbNode { @@ -131,22 +141,26 @@ public class TbMsgGeneratorNode implements TbNode {
131 } 141 }
132 142
133 private ListenableFuture<TbMsg> generate(TbContext ctx, TbMsg msg) { 143 private ListenableFuture<TbMsg> generate(TbContext ctx, TbMsg msg) {
134 - return ctx.getJsExecutor().executeAsync(() -> {  
135 - if (prevMsg == null) {  
136 - prevMsg = ctx.newMsg(ServiceQueue.MAIN, "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}");  
137 - }  
138 - if (initialized) {  
139 - ctx.logJsEvalRequest();  
140 - TbMsg generated = jsEngine.executeGenerate(prevMsg); 144 + log.trace("generate, config {}", config);
  145 + if (prevMsg == null) {
  146 + prevMsg = ctx.newMsg(ServiceQueue.MAIN, "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}");
  147 + }
  148 + if (initialized.get()) {
  149 + ctx.logJsEvalRequest();
  150 + return Futures.transformAsync(jsEngine.executeGenerateAsync(prevMsg), generated -> {
  151 + log.trace("generate process response, generated {}, config {}", generated, config);
141 ctx.logJsEvalResponse(); 152 ctx.logJsEvalResponse();
142 prevMsg = ctx.newMsg(ServiceQueue.MAIN, generated.getType(), originatorId, msg.getCustomerId(), generated.getMetaData(), generated.getData()); 153 prevMsg = ctx.newMsg(ServiceQueue.MAIN, generated.getType(), originatorId, msg.getCustomerId(), generated.getMetaData(), generated.getData());
143 - }  
144 - return prevMsg;  
145 - }); 154 + return Futures.immediateFuture(prevMsg);
  155 + }, MoreExecutors.directExecutor()); //usually it runs on js-executor-remote-callback thread pool
  156 + }
  157 + return Futures.immediateFuture(prevMsg);
  158 +
146 } 159 }
147 160
148 @Override 161 @Override
149 public void destroy() { 162 public void destroy() {
  163 + log.trace("destroy, config {}", config);
150 prevMsg = null; 164 prevMsg = null;
151 if (jsEngine != null) { 165 if (jsEngine != null) {
152 jsEngine.destroy(); 166 jsEngine.destroy();
@@ -15,7 +15,11 @@ @@ -15,7 +15,11 @@
15 */ 15 */
16 package org.thingsboard.rule.engine.filter; 16 package org.thingsboard.rule.engine.filter;
17 17
  18 +import com.google.common.util.concurrent.FutureCallback;
  19 +import com.google.common.util.concurrent.Futures;
  20 +import com.google.common.util.concurrent.MoreExecutors;
18 import lombok.extern.slf4j.Slf4j; 21 import lombok.extern.slf4j.Slf4j;
  22 +import org.checkerframework.checker.nullness.qual.Nullable;
19 import org.thingsboard.common.util.ListeningExecutor; 23 import org.thingsboard.common.util.ListeningExecutor;
20 import org.thingsboard.rule.engine.api.RuleNode; 24 import org.thingsboard.rule.engine.api.RuleNode;
21 import org.thingsboard.rule.engine.api.ScriptEngine; 25 import org.thingsboard.rule.engine.api.ScriptEngine;
@@ -29,8 +33,6 @@ import org.thingsboard.server.common.msg.TbMsg; @@ -29,8 +33,6 @@ import org.thingsboard.server.common.msg.TbMsg;
29 33
30 import java.util.Set; 34 import java.util.Set;
31 35
32 -import static org.thingsboard.common.util.DonAsynchron.withCallback;  
33 -  
34 @Slf4j 36 @Slf4j
35 @RuleNode( 37 @RuleNode(
36 type = ComponentType.FILTER, 38 type = ComponentType.FILTER,
@@ -58,17 +60,20 @@ public class TbJsSwitchNode implements TbNode { @@ -58,17 +60,20 @@ public class TbJsSwitchNode implements TbNode {
58 60
59 @Override 61 @Override
60 public void onMsg(TbContext ctx, TbMsg msg) { 62 public void onMsg(TbContext ctx, TbMsg msg) {
61 - ListeningExecutor jsExecutor = ctx.getJsExecutor();  
62 ctx.logJsEvalRequest(); 63 ctx.logJsEvalRequest();
63 - withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(msg)),  
64 - result -> {  
65 - ctx.logJsEvalResponse();  
66 - processSwitch(ctx, msg, result);  
67 - },  
68 - t -> {  
69 - ctx.logJsEvalFailure();  
70 - ctx.tellFailure(msg, t);  
71 - }, ctx.getDbCallbackExecutor()); 64 + Futures.addCallback(jsEngine.executeSwitchAsync(msg), new FutureCallback<Set<String>>() {
  65 + @Override
  66 + public void onSuccess(@Nullable Set<String> result) {
  67 + ctx.logJsEvalResponse();
  68 + processSwitch(ctx, msg, result);
  69 + }
  70 +
  71 + @Override
  72 + public void onFailure(Throwable t) {
  73 + ctx.logJsEvalFailure();
  74 + ctx.tellFailure(msg, t);
  75 + }
  76 + }, MoreExecutors.directExecutor()); //usually runs in a callbackExecutor
72 } 77 }
73 78
74 private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) { 79 private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) {
@@ -64,7 +64,7 @@ public class TbJsSwitchNodeTest { @@ -64,7 +64,7 @@ public class TbJsSwitchNodeTest {
64 private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); 64 private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased());
65 65
66 @Test 66 @Test
67 - public void multipleRoutesAreAllowed() throws TbNodeException, ScriptException { 67 + public void multipleRoutesAreAllowed() throws TbNodeException {
68 initWithScript(); 68 initWithScript();
69 TbMsgMetaData metaData = new TbMsgMetaData(); 69 TbMsgMetaData metaData = new TbMsgMetaData();
70 metaData.putValue("temp", "10"); 70 metaData.putValue("temp", "10");
@@ -72,11 +72,9 @@ public class TbJsSwitchNodeTest { @@ -72,11 +72,9 @@ public class TbJsSwitchNodeTest {
72 String rawJson = "{\"name\": \"Vit\", \"passed\": 5}"; 72 String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
73 73
74 TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); 74 TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId);
75 - mockJsExecutor();  
76 - when(scriptEngine.executeSwitch(msg)).thenReturn(Sets.newHashSet("one", "three")); 75 + when(scriptEngine.executeSwitchAsync(msg)).thenReturn(Futures.immediateFuture(Sets.newHashSet("one", "three")));
77 76
78 node.onMsg(ctx, msg); 77 node.onMsg(ctx, msg);
79 - verify(ctx).getJsExecutor();  
80 verify(ctx).tellNext(msg, Sets.newHashSet("one", "three")); 78 verify(ctx).tellNext(msg, Sets.newHashSet("one", "three"));
81 } 79 }
82 80
@@ -92,19 +90,6 @@ public class TbJsSwitchNodeTest { @@ -92,19 +90,6 @@ public class TbJsSwitchNodeTest {
92 node.init(ctx, nodeConfiguration); 90 node.init(ctx, nodeConfiguration);
93 } 91 }
94 92
95 - @SuppressWarnings("unchecked")  
96 - private void mockJsExecutor() {  
97 - when(ctx.getJsExecutor()).thenReturn(executor);  
98 - doAnswer((Answer<ListenableFuture<Set<String>>>) invocationOnMock -> {  
99 - try {  
100 - Callable task = (Callable) (invocationOnMock.getArguments())[0];  
101 - return Futures.immediateFuture((Set<String>) task.call());  
102 - } catch (Throwable th) {  
103 - return Futures.immediateFailedFuture(th);  
104 - }  
105 - }).when(executor).executeAsync(ArgumentMatchers.any(Callable.class));  
106 - }  
107 -  
108 private void verifyError(TbMsg msg, String message, Class expectedClass) { 93 private void verifyError(TbMsg msg, String message, Class expectedClass) {
109 ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class); 94 ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
110 verify(ctx).tellFailure(same(msg), captor.capture()); 95 verify(ctx).tellFailure(same(msg), captor.capture());
@@ -133,7 +133,7 @@ export class OtaPackageService { @@ -133,7 +133,7 @@ export class OtaPackageService {
133 } 133 }
134 134
135 public countUpdateDeviceAfterChangePackage(type: OtaUpdateType, entityId: EntityId, config?: RequestConfig): Observable<number> { 135 public countUpdateDeviceAfterChangePackage(type: OtaUpdateType, entityId: EntityId, config?: RequestConfig): Observable<number> {
136 - return this.http.get<number>(`/api/devices/count/${type}?deviceProfileId=${entityId.id}`, defaultHttpOptionsFromConfig(config)); 136 + return this.http.get<number>(`/api/devices/count/${type}/${entityId.id}`, defaultHttpOptionsFromConfig(config));
137 } 137 }
138 138
139 public confirmDialogUpdatePackage(entity: BaseData<EntityId>&OtaPagesIds, 139 public confirmDialogUpdatePackage(entity: BaseData<EntityId>&OtaPagesIds,
@@ -293,24 +293,24 @@ export class MenuService { @@ -293,24 +293,24 @@ export class MenuService {
293 sections.push( 293 sections.push(
294 { 294 {
295 id: guid(), 295 id: guid(),
  296 + name: 'edge.edge-instances',
  297 + type: 'link',
  298 + path: '/edgeInstances',
  299 + icon: 'router'
  300 + },
  301 + {
  302 + id: guid(),
296 name: 'edge.management', 303 name: 'edge.management',
297 type: 'toggle', 304 type: 'toggle',
298 - path: '/edges',  
299 - height: '80px',  
300 - icon: 'router', 305 + path: '/edgeManagement',
  306 + height: '40px',
  307 + icon: 'settings_input_antenna',
301 pages: [ 308 pages: [
302 { 309 {
303 id: guid(), 310 id: guid(),
304 - name: 'edge.edge-instances',  
305 - type: 'link',  
306 - path: '/edges',  
307 - icon: 'router'  
308 - },  
309 - {  
310 - id: guid(),  
311 name: 'edge.rulechain-templates', 311 name: 'edge.rulechain-templates',
312 type: 'link', 312 type: 'link',
313 - path: '/edges/ruleChains', 313 + path: '/edgeManagement/ruleChains',
314 icon: 'settings_ethernet' 314 icon: 'settings_ethernet'
315 } 315 }
316 ] 316 ]
@@ -448,12 +448,12 @@ export class MenuService { @@ -448,12 +448,12 @@ export class MenuService {
448 { 448 {
449 name: 'edge.edge-instances', 449 name: 'edge.edge-instances',
450 icon: 'router', 450 icon: 'router',
451 - path: '/edges' 451 + path: '/edgeInstances'
452 }, 452 },
453 { 453 {
454 name: 'edge.rulechain-templates', 454 name: 'edge.rulechain-templates',
455 icon: 'settings_ethernet', 455 icon: 'settings_ethernet',
456 - path: '/edges/ruleChains' 456 + path: '/edgeManagement/ruleChains'
457 } 457 }
458 ] 458 ]
459 } 459 }
@@ -548,7 +548,7 @@ export class MenuService { @@ -548,7 +548,7 @@ export class MenuService {
548 id: guid(), 548 id: guid(),
549 name: 'edge.edge-instances', 549 name: 'edge.edge-instances',
550 type: 'link', 550 type: 'link',
551 - path: '/edges', 551 + path: '/edgeInstances',
552 icon: 'router' 552 icon: 'router'
553 } 553 }
554 ); 554 );
@@ -606,8 +606,8 @@ export class MenuService { @@ -606,8 +606,8 @@ export class MenuService {
606 places: [ 606 places: [
607 { 607 {
608 name: 'edge.edge-instances', 608 name: 'edge.edge-instances',
609 - icon: 'router',  
610 - path: '/edges' 609 + icon: 'settings_input_antenna',
  610 + path: '/edgeInstances'
611 } 611 }
612 ] 612 ]
613 } 613 }
@@ -128,6 +128,7 @@ import { @@ -128,6 +128,7 @@ import {
128 DashboardImageDialogComponent, 128 DashboardImageDialogComponent,
129 DashboardImageDialogData, DashboardImageDialogResult 129 DashboardImageDialogData, DashboardImageDialogResult
130 } from '@home/components/dashboard-page/dashboard-image-dialog.component'; 130 } from '@home/components/dashboard-page/dashboard-image-dialog.component';
  131 +import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
131 132
132 // @dynamic 133 // @dynamic
133 @Component({ 134 @Component({
@@ -211,7 +212,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -211,7 +212,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
211 212
212 addingLayoutCtx: DashboardPageLayoutContext; 213 addingLayoutCtx: DashboardPageLayoutContext;
213 214
214 - logo = 'assets/logo_title_white.svg'; 215 + private dashboardLogoCache: SafeUrl;
  216 + private defaultDashboardLogo = 'assets/logo_title_white.svg';
215 217
216 dashboardCtx: DashboardContext = { 218 dashboardCtx: DashboardContext = {
217 instanceId: this.utils.guid(), 219 instanceId: this.utils.guid(),
@@ -312,7 +314,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -312,7 +314,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
312 private ngZone: NgZone, 314 private ngZone: NgZone,
313 private overlay: Overlay, 315 private overlay: Overlay,
314 private viewContainerRef: ViewContainerRef, 316 private viewContainerRef: ViewContainerRef,
315 - private cd: ChangeDetectorRef) { 317 + private cd: ChangeDetectorRef,
  318 + private sanitizer: DomSanitizer) {
316 super(store); 319 super(store);
317 320
318 } 321 }
@@ -413,6 +416,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -413,6 +416,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
413 private reset() { 416 private reset() {
414 this.dashboard = null; 417 this.dashboard = null;
415 this.dashboardConfiguration = null; 418 this.dashboardConfiguration = null;
  419 + this.dashboardLogoCache = undefined;
416 this.prevDashboard = null; 420 this.prevDashboard = null;
417 421
418 this.widgetEditMode = false; 422 this.widgetEditMode = false;
@@ -570,8 +574,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -570,8 +574,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
570 } 574 }
571 } 575 }
572 576
573 - public get dashboardLogo(): string {  
574 - return this.dashboard.configuration.settings.dashboardLogoUrl || this.logo; 577 + public get dashboardLogo(): SafeUrl {
  578 + if (!this.dashboardLogoCache) {
  579 + const logo = this.dashboard.configuration.settings.dashboardLogoUrl || this.defaultDashboardLogo;
  580 + this.dashboardLogoCache = this.sanitizer.bypassSecurityTrustUrl(logo);
  581 + }
  582 + return this.dashboardLogoCache;
575 } 583 }
576 584
577 public showRightLayoutSwitch(): boolean { 585 public showRightLayoutSwitch(): boolean {
@@ -702,6 +710,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -702,6 +710,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
702 }).afterClosed().subscribe((data) => { 710 }).afterClosed().subscribe((data) => {
703 if (data) { 711 if (data) {
704 this.dashboard.configuration.settings = data.settings; 712 this.dashboard.configuration.settings = data.settings;
  713 + this.dashboardLogoCache = undefined;
705 const newGridSettings = data.gridSettings; 714 const newGridSettings = data.gridSettings;
706 if (newGridSettings) { 715 if (newGridSettings) {
707 const layout = this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout]; 716 const layout = this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout];
@@ -855,11 +864,13 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -855,11 +864,13 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
855 if (this.widgetEditMode) { 864 if (this.widgetEditMode) {
856 if (revert) { 865 if (revert) {
857 this.dashboard = this.prevDashboard; 866 this.dashboard = this.prevDashboard;
  867 + this.dashboardLogoCache = undefined;
858 } 868 }
859 } else { 869 } else {
860 this.resetHighlight(); 870 this.resetHighlight();
861 if (revert) { 871 if (revert) {
862 this.dashboard = this.prevDashboard; 872 this.dashboard = this.prevDashboard;
  873 + this.dashboardLogoCache = undefined;
863 this.dashboardConfiguration = this.dashboard.configuration; 874 this.dashboardConfiguration = this.dashboard.configuration;
864 this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; 875 this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
865 this.entityAliasesUpdated(); 876 this.entityAliasesUpdated();
@@ -99,7 +99,6 @@ import { DeviceProfileDialogComponent } from '@home/components/profile/device-pr @@ -99,7 +99,6 @@ import { DeviceProfileDialogComponent } from '@home/components/profile/device-pr
99 import { DeviceProfileAutocompleteComponent } from '@home/components/profile/device-profile-autocomplete.component'; 99 import { DeviceProfileAutocompleteComponent } from '@home/components/profile/device-profile-autocomplete.component';
100 import { MqttDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/mqtt-device-profile-transport-configuration.component'; 100 import { MqttDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/mqtt-device-profile-transport-configuration.component';
101 import { CoapDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/coap-device-profile-transport-configuration.component'; 101 import { CoapDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/coap-device-profile-transport-configuration.component';
102 -import { SnmpDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/snmp-device-profile-transport-configuration.component';  
103 import { DeviceProfileAlarmsComponent } from '@home/components/profile/alarm/device-profile-alarms.component'; 102 import { DeviceProfileAlarmsComponent } from '@home/components/profile/alarm/device-profile-alarms.component';
104 import { DeviceProfileAlarmComponent } from '@home/components/profile/alarm/device-profile-alarm.component'; 103 import { DeviceProfileAlarmComponent } from '@home/components/profile/alarm/device-profile-alarm.component';
105 import { CreateAlarmRulesComponent } from '@home/components/profile/alarm/create-alarm-rules.component'; 104 import { CreateAlarmRulesComponent } from '@home/components/profile/alarm/create-alarm-rules.component';
@@ -143,6 +142,7 @@ import { SecurityConfigLwm2mComponent } from '@home/components/device/security-c @@ -143,6 +142,7 @@ import { SecurityConfigLwm2mComponent } from '@home/components/device/security-c
143 import { SecurityConfigLwm2mServerComponent } from '@home/components/device/security-config-lwm2m-server.component'; 142 import { SecurityConfigLwm2mServerComponent } from '@home/components/device/security-config-lwm2m-server.component';
144 import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component'; 143 import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component';
145 import { WidgetContainerComponent } from '@home/components/widget/widget-container.component'; 144 import { WidgetContainerComponent } from '@home/components/widget/widget-container.component';
  145 +import { SnmpDeviceProfileTransportModule } from '@home/components/profile/device/snpm/snmp-device-profile-transport.module';
146 146
147 @NgModule({ 147 @NgModule({
148 declarations: 148 declarations:
@@ -228,7 +228,6 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain @@ -228,7 +228,6 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain
228 DefaultDeviceProfileTransportConfigurationComponent, 228 DefaultDeviceProfileTransportConfigurationComponent,
229 MqttDeviceProfileTransportConfigurationComponent, 229 MqttDeviceProfileTransportConfigurationComponent,
230 CoapDeviceProfileTransportConfigurationComponent, 230 CoapDeviceProfileTransportConfigurationComponent,
231 - SnmpDeviceProfileTransportConfigurationComponent,  
232 DeviceProfileTransportConfigurationComponent, 231 DeviceProfileTransportConfigurationComponent,
233 CreateAlarmRulesComponent, 232 CreateAlarmRulesComponent,
234 AlarmRuleComponent, 233 AlarmRuleComponent,
@@ -272,6 +271,7 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain @@ -272,6 +271,7 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain
272 SharedModule, 271 SharedModule,
273 SharedHomeComponentsModule, 272 SharedHomeComponentsModule,
274 Lwm2mProfileComponentsModule, 273 Lwm2mProfileComponentsModule,
  274 + SnmpDeviceProfileTransportModule,
275 StatesControllerModule 275 StatesControllerModule
276 ], 276 ],
277 exports: [ 277 exports: [
@@ -339,7 +339,6 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain @@ -339,7 +339,6 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain
339 DefaultDeviceProfileTransportConfigurationComponent, 339 DefaultDeviceProfileTransportConfigurationComponent,
340 MqttDeviceProfileTransportConfigurationComponent, 340 MqttDeviceProfileTransportConfigurationComponent,
341 CoapDeviceProfileTransportConfigurationComponent, 341 CoapDeviceProfileTransportConfigurationComponent,
342 - SnmpDeviceProfileTransportConfigurationComponent,  
343 DeviceProfileTransportConfigurationComponent, 342 DeviceProfileTransportConfigurationComponent,
344 CreateAlarmRulesComponent, 343 CreateAlarmRulesComponent,
345 AlarmRuleComponent, 344 AlarmRuleComponent,
@@ -89,7 +89,9 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu @@ -89,7 +89,9 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu
89 if (configuration) { 89 if (configuration) {
90 delete configuration.type; 90 delete configuration.type;
91 } 91 }
92 - this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); 92 + setTimeout(() => {
  93 + this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false});
  94 + });
93 } 95 }
94 96
95 private updateModel() { 97 private updateModel() {
1 -<!--  
2 -  
3 - Copyright © 2016-2021 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 -<form [formGroup]="snmpDeviceProfileTransportConfigurationFormGroup" style="padding-bottom: 16px;">  
19 - <tb-json-object-edit  
20 - required  
21 - formControlName="configuration">  
22 - </tb-json-object-edit>  
23 -</form>  
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 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 +<div fxLayout="column">
  19 + <div *ngFor="let deviceProfileCommunication of communicationConfigFormArray().controls; let $index = index;
  20 + last as isLast;" fxLayout="row" fxLayoutAlign="start center"
  21 + fxLayoutGap="8px" class="scope-row" [formGroup]="deviceProfileCommunication">
  22 + <div class="communication-config" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
  23 + <mat-form-field class="spec mat-block" floatLabel="always" hideRequiredMarker>
  24 + <mat-label translate>device-profile.snmp.scope</mat-label>
  25 + <mat-select formControlName="spec" required>
  26 + <mat-option *ngFor="let snmpSpecType of snmpSpecTypes" [value]="snmpSpecType"
  27 + [disabled]="isDisabledSeverity(snmpSpecType, $index)">
  28 + {{ snmpSpecTypeTranslationMap.get(snmpSpecType) }}
  29 + </mat-option>
  30 + </mat-select>
  31 + <mat-error *ngIf="deviceProfileCommunication.get('spec').hasError('required')">
  32 + {{ 'device-profile.snmp.scope-required' | translate }}
  33 + </mat-error>
  34 + </mat-form-field>
  35 + <mat-divider vertical></mat-divider>
  36 + <section fxFlex fxLayout="column">
  37 + <mat-form-field *ngIf="isShowFrequency(deviceProfileCommunication.get('spec').value)">
  38 + <mat-label translate>device-profile.snmp.querying-frequency</mat-label>
  39 + <input matInput formControlName="queryingFrequencyMs" type="number" min="0" required/>
  40 + <mat-error *ngIf="deviceProfileCommunication.get('queryingFrequencyMs').hasError('required')">
  41 + {{ 'device-profile.snmp.querying-frequency-required' | translate }}
  42 + </mat-error>
  43 + <mat-error *ngIf="deviceProfileCommunication.get('queryingFrequencyMs').hasError('pattern') ||
  44 + deviceProfileCommunication.get('queryingFrequencyMs').hasError('min')">
  45 + {{ 'device-profile.snmp.querying-frequency-invalid-format' | translate }}
  46 + </mat-error>
  47 + </mat-form-field>
  48 + <tb-snmp-device-profile-mapping formControlName="mappings">
  49 + </tb-snmp-device-profile-mapping>
  50 + </section>
  51 + </div>
  52 + <button *ngIf="!disabled"
  53 + mat-icon-button color="primary" style="min-width: 40px;"
  54 + type="button"
  55 + (click)="removeCommunicationConfig($index)"
  56 + matTooltip="{{ 'action.remove' | translate }}"
  57 + matTooltipPosition="above">
  58 + <mat-icon>remove_circle_outline</mat-icon>
  59 + </button>
  60 + </div>
  61 + <div *ngIf="!communicationConfigFormArray().controls.length && !disabled">
  62 + <span fxLayoutAlign="center center" class="tb-prompt required required-text" translate>device-profile.snmp.please-add-communication-config</span>
  63 + </div>
  64 + <div *ngIf="!disabled && isAddEnabled">
  65 + <button mat-stroked-button color="primary"
  66 + type="button"
  67 + (click)="addCommunicationConfig()">
  68 + <mat-icon class="button-icon">add_circle_outline</mat-icon>
  69 + {{ 'device-profile.snmp.add-communication-config' | translate }}
  70 + </button>
  71 + </div>
  72 +</div>
  1 +/**
  2 + * Copyright © 2016-2021 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 +:host {
  17 + .communication-config {
  18 + border: 2px groove rgba(0, 0, 0, 0.25);
  19 + border-radius: 4px;
  20 + padding: 8px;
  21 + min-width: 0;
  22 + }
  23 +
  24 + .scope-row {
  25 + padding-bottom: 8px;
  26 + }
  27 +
  28 + .required-text {
  29 + margin: 16px 0
  30 + }
  31 +}
  32 +
  33 +:host ::ng-deep {
  34 + .mat-form-field.spec {
  35 + .mat-form-field-infix {
  36 + width: 160px;
  37 + }
  38 + }
  39 + .button-icon{
  40 + font-size: 20px;
  41 + width: 20px;
  42 + height: 20px;
  43 + }
  44 +}
  1 +///
  2 +/// Copyright © 2016-2021 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 { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
  18 +import {
  19 + AbstractControl,
  20 + ControlValueAccessor,
  21 + FormArray,
  22 + FormBuilder,
  23 + FormGroup,
  24 + NG_VALIDATORS,
  25 + NG_VALUE_ACCESSOR,
  26 + Validator,
  27 + Validators
  28 +} from '@angular/forms';
  29 +import { SnmpCommunicationConfig, SnmpSpecType, SnmpSpecTypeTranslationMap } from '@shared/models/device.models';
  30 +import { Subject, Subscription } from 'rxjs';
  31 +import { isUndefinedOrNull } from '@core/utils';
  32 +import { takeUntil } from 'rxjs/operators';
  33 +
  34 +@Component({
  35 + selector: 'tb-snmp-device-profile-communication-config',
  36 + templateUrl: './snmp-device-profile-communication-config.component.html',
  37 + styleUrls: ['./snmp-device-profile-communication-config.component.scss'],
  38 + providers: [
  39 + {
  40 + provide: NG_VALUE_ACCESSOR,
  41 + useExisting: forwardRef(() => SnmpDeviceProfileCommunicationConfigComponent),
  42 + multi: true
  43 + },
  44 + {
  45 + provide: NG_VALIDATORS,
  46 + useExisting: forwardRef(() => SnmpDeviceProfileCommunicationConfigComponent),
  47 + multi: true
  48 + }]
  49 +})
  50 +export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
  51 +
  52 + snmpSpecTypes = Object.values(SnmpSpecType);
  53 + snmpSpecTypeTranslationMap = SnmpSpecTypeTranslationMap;
  54 +
  55 + deviceProfileCommunicationConfig: FormGroup;
  56 +
  57 + @Input()
  58 + disabled: boolean;
  59 +
  60 + private usedSpecType: SnmpSpecType[] = [];
  61 + private valueChange$: Subscription = null;
  62 + private destroy$ = new Subject();
  63 + private propagateChange = (v: any) => { };
  64 +
  65 + constructor(private fb: FormBuilder) { }
  66 +
  67 + ngOnInit(): void {
  68 + this.deviceProfileCommunicationConfig = this.fb.group({
  69 + communicationConfig: this.fb.array([])
  70 + });
  71 + }
  72 +
  73 + ngOnDestroy() {
  74 + if (this.valueChange$) {
  75 + this.valueChange$.unsubscribe();
  76 + }
  77 + this.destroy$.next();
  78 + this.destroy$.complete();
  79 + }
  80 +
  81 + communicationConfigFormArray(): FormArray {
  82 + return this.deviceProfileCommunicationConfig.get('communicationConfig') as FormArray;
  83 + }
  84 +
  85 + registerOnChange(fn: any): void {
  86 + this.propagateChange = fn;
  87 + }
  88 +
  89 + registerOnTouched(fn: any): void {
  90 + }
  91 +
  92 + setDisabledState(isDisabled: boolean) {
  93 + this.disabled = isDisabled;
  94 + if (this.disabled) {
  95 + this.deviceProfileCommunicationConfig.disable({emitEvent: false});
  96 + } else {
  97 + this.deviceProfileCommunicationConfig.enable({emitEvent: false});
  98 + }
  99 + }
  100 +
  101 + writeValue(communicationConfig: SnmpCommunicationConfig[]) {
  102 + if (this.valueChange$) {
  103 + this.valueChange$.unsubscribe();
  104 + }
  105 + const communicationConfigControl: Array<AbstractControl> = [];
  106 + if (communicationConfig) {
  107 + communicationConfig.forEach((config) => {
  108 + communicationConfigControl.push(this.createdFormGroup(config));
  109 + });
  110 + }
  111 + this.deviceProfileCommunicationConfig.setControl('communicationConfig', this.fb.array(communicationConfigControl));
  112 + if (!communicationConfig || !communicationConfig.length) {
  113 + this.addCommunicationConfig();
  114 + }
  115 + if (this.disabled) {
  116 + this.deviceProfileCommunicationConfig.disable({emitEvent: false});
  117 + } else {
  118 + this.deviceProfileCommunicationConfig.enable({emitEvent: false});
  119 + }
  120 + this.valueChange$ = this.deviceProfileCommunicationConfig.valueChanges.subscribe(() => {
  121 + this.updateModel();
  122 + });
  123 + this.updateUsedSpecType();
  124 + if (!this.disabled && !this.deviceProfileCommunicationConfig.valid) {
  125 + this.updateModel();
  126 + }
  127 + }
  128 +
  129 + public validate() {
  130 + return this.deviceProfileCommunicationConfig.valid && this.deviceProfileCommunicationConfig.value.communicationConfig.length ? null : {
  131 + communicationConfig: false
  132 + };
  133 + }
  134 +
  135 + public removeCommunicationConfig(index: number) {
  136 + this.communicationConfigFormArray().removeAt(index);
  137 + }
  138 +
  139 +
  140 + get isAddEnabled(): boolean {
  141 + return this.communicationConfigFormArray().length !== Object.keys(SnmpSpecType).length;
  142 + }
  143 +
  144 + public addCommunicationConfig() {
  145 + this.communicationConfigFormArray().push(this.createdFormGroup());
  146 + this.deviceProfileCommunicationConfig.updateValueAndValidity();
  147 + if (!this.deviceProfileCommunicationConfig.valid) {
  148 + this.updateModel();
  149 + }
  150 + }
  151 +
  152 + private getFirstUnusedSeverity(): SnmpSpecType {
  153 + for (const type of Object.values(SnmpSpecType)) {
  154 + if (this.usedSpecType.indexOf(type) === -1) {
  155 + return type;
  156 + }
  157 + }
  158 + return null;
  159 + }
  160 +
  161 + public isDisabledSeverity(type: SnmpSpecType, index: number): boolean {
  162 + const usedIndex = this.usedSpecType.indexOf(type);
  163 + return usedIndex > -1 && usedIndex !== index;
  164 + }
  165 +
  166 + public isShowFrequency(type: SnmpSpecType): boolean {
  167 + return type === SnmpSpecType.TELEMETRY_QUERYING || type === SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING;
  168 + }
  169 +
  170 + private updateUsedSpecType() {
  171 + this.usedSpecType = [];
  172 + const value: SnmpCommunicationConfig[] = this.deviceProfileCommunicationConfig.get('communicationConfig').value;
  173 + value.forEach((rule, index) => {
  174 + this.usedSpecType[index] = rule.spec;
  175 + });
  176 + }
  177 +
  178 + private createdFormGroup(value?: SnmpCommunicationConfig): FormGroup {
  179 + if (isUndefinedOrNull(value)) {
  180 + value = {
  181 + spec: this.getFirstUnusedSeverity(),
  182 + queryingFrequencyMs: 5000,
  183 + mappings: null
  184 + };
  185 + }
  186 + const form = this.fb.group({
  187 + spec: [value.spec, Validators.required],
  188 + mappings: [value.mappings]
  189 + });
  190 + if (this.isShowFrequency(value.spec)) {
  191 + form.addControl('queryingFrequencyMs',
  192 + this.fb.control(value.queryingFrequencyMs, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]));
  193 + }
  194 + form.get('spec').valueChanges.pipe(
  195 + takeUntil(this.destroy$)
  196 + ).subscribe(spec => {
  197 + if (this.isShowFrequency(spec)) {
  198 + form.addControl('queryingFrequencyMs',
  199 + this.fb.control(5000, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]));
  200 + } else {
  201 + form.removeControl('queryingFrequencyMs');
  202 + }
  203 + });
  204 + return form;
  205 + }
  206 +
  207 + private updateModel() {
  208 + const value: SnmpCommunicationConfig[] = this.deviceProfileCommunicationConfig.get('communicationConfig').value;
  209 + value.forEach(config => {
  210 + if (!this.isShowFrequency(config.spec)) {
  211 + delete config.queryingFrequencyMs;
  212 + }
  213 + });
  214 + this.updateUsedSpecType();
  215 + this.propagateChange(value);
  216 + }
  217 +
  218 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 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 +<div fxFlex fxLayout="column" class="mapping-config">
  19 + <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex="100">
  20 + <div fxFlex fxLayout="row" fxLayoutGap="8px">
  21 + <label fxFlex="26" class="tb-title no-padding" translate>device-profile.snmp.data-type</label>
  22 + <label fxFlex="37" class="tb-title no-padding" translate>device-profile.snmp.data-key</label>
  23 + <label fxFlex="37" class="tb-title no-padding" translate>device-profile.snmp.oid</label>
  24 + <span style="min-width: 40px" [fxShow]="!disabled"></span>
  25 + </div>
  26 + </div>
  27 + <mat-divider></mat-divider>
  28 + <div *ngFor="let mappingConfig of mappingsConfigFormArray().controls; let $index = index;
  29 + last as isLast;" fxLayout="row" fxLayoutAlign="start center"
  30 + fxLayoutGap="8px" [formGroup]="mappingConfig" class="mapping-list">
  31 + <div fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
  32 + <mat-form-field fxFlex="26" floatLabel="always" hideRequiredMarker>
  33 + <mat-label></mat-label>
  34 + <mat-select formControlName="dataType" required>
  35 + <mat-option *ngFor="let dataType of dataTypes" [value]="dataType">
  36 + {{ dataTypesTranslationMap.get(dataType) | translate }}
  37 + </mat-option>
  38 + </mat-select>
  39 + <mat-error *ngIf="mappingConfig.get('dataType').hasError('required')">
  40 + {{ 'device-profile.snmp.data-type-required' | translate }}
  41 + </mat-error>
  42 + </mat-form-field>
  43 + <mat-form-field floatLabel="always" hideRequiredMarker fxFlex="37">
  44 + <mat-label></mat-label>
  45 + <input matInput formControlName="key" required/>
  46 + <mat-error *ngIf="mappingConfig.get('key').hasError('required')">
  47 + {{ 'device-profile.snmp.data-key-required' | translate }}
  48 + </mat-error>
  49 + </mat-form-field>
  50 + <mat-form-field floatLabel="always" hideRequiredMarker fxFlex="37">
  51 + <mat-label></mat-label>
  52 + <input matInput formControlName="oid" required/>
  53 + <mat-error *ngIf="mappingConfig.get('oid').hasError('required')">
  54 + {{ 'device-profile.snmp.oid-required' | translate }}
  55 + </mat-error>
  56 + <mat-error *ngIf="mappingConfig.get('oid').hasError('pattern')">
  57 + {{ 'device-profile.snmp.oid-pattern' | translate }}
  58 + </mat-error>
  59 + </mat-form-field>
  60 + <button *ngIf="!disabled"
  61 + mat-icon-button color="primary"
  62 + type="button"
  63 + (click)="removeMappingConfig($index)"
  64 + matTooltip="{{ 'action.remove' | translate }}"
  65 + matTooltipPosition="above">
  66 + <mat-icon>close</mat-icon>
  67 + </button>
  68 + </div>
  69 + </div>
  70 + <div *ngIf="!mappingsConfigFormArray().controls.length && !disabled">
  71 + <span fxLayoutAlign="center center" class="tb-prompt required required-text" translate>device-profile.snmp.please-add-mapping-config</span>
  72 + </div>
  73 + <div *ngIf="!disabled">
  74 + <button mat-stroked-button color="primary"
  75 + type="button"
  76 + (click)="addMappingConfig()">
  77 + <mat-icon class="button-icon">add_circle_outline</mat-icon>
  78 + {{ 'device-profile.snmp.add-mapping' | translate }}
  79 + </button>
  80 + </div>
  81 +</div>
  1 +/**
  2 + * Copyright © 2016-2021 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 +:host {
  17 + .mapping-config {
  18 + min-width: 518px;
  19 + }
  20 + .mapping-list {
  21 + padding-bottom: 8px;
  22 + height: 46px;
  23 + }
  24 +
  25 + .required-text {
  26 + margin: 14px 0;
  27 + }
  28 +}
  29 +
  30 +:host ::ng-deep {
  31 + .mapping-list {
  32 + mat-form-field {
  33 + .mat-form-field-wrapper {
  34 + padding-bottom: 0;
  35 + .mat-form-field-infix {
  36 + border-top-width: 0.2em;
  37 + width: auto;
  38 + min-width: auto;
  39 + }
  40 + .mat-form-field-underline {
  41 + bottom: 0;
  42 + }
  43 + .mat-form-field-subscript-wrapper{
  44 + margin-top: 1.8em;
  45 + }
  46 + }
  47 + }
  48 + }
  49 +}
  1 +///
  2 +/// Copyright © 2016-2021 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 { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
  18 +import {
  19 + AbstractControl,
  20 + ControlValueAccessor,
  21 + FormArray,
  22 + FormBuilder,
  23 + FormGroup,
  24 + NG_VALIDATORS,
  25 + NG_VALUE_ACCESSOR,
  26 + ValidationErrors,
  27 + Validator,
  28 + Validators
  29 +} from '@angular/forms';
  30 +import { SnmpMapping } from '@shared/models/device.models';
  31 +import { Subscription } from 'rxjs';
  32 +import { DataType, DataTypeTranslationMap } from '@shared/models/constants';
  33 +import { isUndefinedOrNull } from '@core/utils';
  34 +
  35 +@Component({
  36 + selector: 'tb-snmp-device-profile-mapping',
  37 + templateUrl: './snmp-device-profile-mapping.component.html',
  38 + styleUrls: ['./snmp-device-profile-mapping.component.scss'],
  39 + providers: [
  40 + {
  41 + provide: NG_VALUE_ACCESSOR,
  42 + useExisting: forwardRef(() => SnmpDeviceProfileMappingComponent),
  43 + multi: true
  44 + },
  45 + {
  46 + provide: NG_VALIDATORS,
  47 + useExisting: forwardRef(() => SnmpDeviceProfileMappingComponent),
  48 + multi: true
  49 + }]
  50 +})
  51 +export class SnmpDeviceProfileMappingComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
  52 +
  53 + mappingsConfigForm: FormGroup;
  54 +
  55 + dataTypes = Object.values(DataType);
  56 + dataTypesTranslationMap = DataTypeTranslationMap;
  57 +
  58 + @Input()
  59 + disabled: boolean;
  60 +
  61 + private readonly oidPattern: RegExp = /^\.?([0-2])((\.0)|(\.[1-9][0-9]*))*$/;
  62 +
  63 + private valueChange$: Subscription = null;
  64 + private propagateChange = (v: any) => { };
  65 +
  66 + constructor(private fb: FormBuilder) { }
  67 +
  68 + ngOnInit() {
  69 + this.mappingsConfigForm = this.fb.group({
  70 + mappings: this.fb.array([])
  71 + });
  72 + }
  73 +
  74 + ngOnDestroy() {
  75 + if (this.valueChange$) {
  76 + this.valueChange$.unsubscribe();
  77 + }
  78 + }
  79 +
  80 + registerOnChange(fn: any) {
  81 + this.propagateChange = fn;
  82 + }
  83 +
  84 + registerOnTouched(fn: any) {
  85 + }
  86 +
  87 + setDisabledState(isDisabled: boolean) {
  88 + this.disabled = isDisabled;
  89 + if (this.disabled) {
  90 + this.mappingsConfigForm.disable({emitEvent: false});
  91 + } else {
  92 + this.mappingsConfigForm.enable({emitEvent: false});
  93 + }
  94 + }
  95 +
  96 + validate(): ValidationErrors | null {
  97 + return this.mappingsConfigForm.valid && this.mappingsConfigForm.value.mappings.length ? null : {
  98 + mapping: false
  99 + };
  100 + }
  101 +
  102 + writeValue(mappings: SnmpMapping[]) {
  103 + if (this.valueChange$) {
  104 + this.valueChange$.unsubscribe();
  105 + }
  106 + const mappingsControl: Array<AbstractControl> = [];
  107 + if (mappings) {
  108 + mappings.forEach((config) => {
  109 + mappingsControl.push(this.createdFormGroup(config));
  110 + });
  111 + }
  112 + this.mappingsConfigForm.setControl('mappings', this.fb.array(mappingsControl));
  113 + if (!mappings || !mappings.length) {
  114 + this.addMappingConfig();
  115 + }
  116 + if (this.disabled) {
  117 + this.mappingsConfigForm.disable({emitEvent: false});
  118 + } else {
  119 + this.mappingsConfigForm.enable({emitEvent: false});
  120 + }
  121 + this.valueChange$ = this.mappingsConfigForm.valueChanges.subscribe(() => {
  122 + this.updateModel();
  123 + });
  124 + if (!this.disabled && !this.mappingsConfigForm.valid) {
  125 + this.updateModel();
  126 + }
  127 + }
  128 +
  129 + mappingsConfigFormArray(): FormArray {
  130 + return this.mappingsConfigForm.get('mappings') as FormArray;
  131 + }
  132 +
  133 + public addMappingConfig() {
  134 + this.mappingsConfigFormArray().push(this.createdFormGroup());
  135 + this.mappingsConfigForm.updateValueAndValidity();
  136 + if (!this.mappingsConfigForm.valid) {
  137 + this.updateModel();
  138 + }
  139 + }
  140 +
  141 + public removeMappingConfig(index: number) {
  142 + this.mappingsConfigFormArray().removeAt(index);
  143 + }
  144 +
  145 + private createdFormGroup(value?: SnmpMapping): FormGroup {
  146 + if (isUndefinedOrNull(value)) {
  147 + value = {
  148 + dataType: DataType.STRING,
  149 + key: '',
  150 + oid: ''
  151 + };
  152 + }
  153 + return this.fb.group({
  154 + dataType: [value.dataType, Validators.required],
  155 + key: [value.key, Validators.required],
  156 + oid: [value.oid, [Validators.required, Validators.pattern(this.oidPattern)]]
  157 + });
  158 + }
  159 +
  160 + private updateModel() {
  161 + const value: SnmpMapping[] = this.mappingsConfigForm.get('mappings').value;
  162 + this.propagateChange(value);
  163 + }
  164 +
  165 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 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 +<form [formGroup]="snmpDeviceProfileTransportConfigurationFormGroup" style="padding: 8px 0 16px;">
  19 + <section fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column">
  20 + <mat-form-field fxFlex>
  21 + <mat-label translate>device-profile.snmp.timeout-ms</mat-label>
  22 + <input matInput formControlName="timeoutMs" type="number" min="0" required/>
  23 + <mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('required')">
  24 + {{ 'device-profile.snmp.timeout-ms-required' | translate }}
  25 + </mat-error>
  26 + <mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('pattern') ||
  27 + snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('min')">
  28 + {{ 'device-profile.snmp.timeout-ms-invalid-format' | translate }}
  29 + </mat-error>
  30 + </mat-form-field>
  31 + <mat-form-field fxFlex>
  32 + <mat-label translate>device-profile.snmp.retries</mat-label>
  33 + <input matInput formControlName="retries" type="number" min="0" required/>
  34 + <mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('required')">
  35 + {{ 'device-profile.snmp.retries-required' | translate }}
  36 + </mat-error>
  37 + <mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('pattern') ||
  38 + snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('min')">
  39 + {{ 'device-profile.snmp.retries-invalid-format' | translate }}
  40 + </mat-error>
  41 + </mat-form-field>
  42 + </section>
  43 + <div class="tb-small" style="padding-bottom: 8px" translate>device-profile.snmp.communication-configs</div>
  44 + <tb-snmp-device-profile-communication-config formControlName="communicationConfigs">
  45 + </tb-snmp-device-profile-communication-config>
  46 +</form>
ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport-configuration.component.ts renamed from ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.ts
@@ -15,9 +15,16 @@ @@ -15,9 +15,16 @@
15 /// 15 ///
16 16
17 import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; 17 import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
18 -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';  
19 -import { Store } from '@ngrx/store';  
20 -import { AppState } from '@app/core/core.state'; 18 +import {
  19 + ControlValueAccessor,
  20 + FormBuilder,
  21 + FormGroup,
  22 + NG_VALIDATORS,
  23 + NG_VALUE_ACCESSOR,
  24 + ValidationErrors,
  25 + Validator,
  26 + Validators
  27 +} from '@angular/forms';
21 import { coerceBooleanProperty } from '@angular/cdk/coercion'; 28 import { coerceBooleanProperty } from '@angular/cdk/coercion';
22 import { 29 import {
23 DeviceProfileTransportConfiguration, 30 DeviceProfileTransportConfiguration,
@@ -40,19 +47,24 @@ export interface OidMappingConfiguration { @@ -40,19 +47,24 @@ export interface OidMappingConfiguration {
40 selector: 'tb-snmp-device-profile-transport-configuration', 47 selector: 'tb-snmp-device-profile-transport-configuration',
41 templateUrl: './snmp-device-profile-transport-configuration.component.html', 48 templateUrl: './snmp-device-profile-transport-configuration.component.html',
42 styleUrls: [], 49 styleUrls: [],
43 - providers: [{  
44 - provide: NG_VALUE_ACCESSOR,  
45 - useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent),  
46 - multi: true  
47 - }] 50 + providers: [
  51 + {
  52 + provide: NG_VALUE_ACCESSOR,
  53 + useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent),
  54 + multi: true
  55 + },
  56 + {
  57 + provide: NG_VALIDATORS,
  58 + useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent),
  59 + multi: true
  60 + }]
48 }) 61 })
49 -export class SnmpDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy { 62 +export class SnmpDeviceProfileTransportConfigurationComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
50 63
51 snmpDeviceProfileTransportConfigurationFormGroup: FormGroup; 64 snmpDeviceProfileTransportConfigurationFormGroup: FormGroup;
52 65
53 private destroy$ = new Subject(); 66 private destroy$ = new Subject();
54 private requiredValue: boolean; 67 private requiredValue: boolean;
55 - private configuration = [];  
56 68
57 get required(): boolean { 69 get required(): boolean {
58 return this.requiredValue; 70 return this.requiredValue;
@@ -69,12 +81,14 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements Control @@ -69,12 +81,14 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements Control
69 private propagateChange = (v: any) => { 81 private propagateChange = (v: any) => {
70 } 82 }
71 83
72 - constructor(private store: Store<AppState>, private fb: FormBuilder) { 84 + constructor(private fb: FormBuilder) {
73 } 85 }
74 86
75 ngOnInit(): void { 87 ngOnInit(): void {
76 this.snmpDeviceProfileTransportConfigurationFormGroup = this.fb.group({ 88 this.snmpDeviceProfileTransportConfigurationFormGroup = this.fb.group({
77 - configuration: [null, Validators.required] 89 + timeoutMs: [500, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
  90 + retries: [0, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
  91 + communicationConfigs: [null, Validators.required],
78 }); 92 });
79 this.snmpDeviceProfileTransportConfigurationFormGroup.valueChanges.pipe( 93 this.snmpDeviceProfileTransportConfigurationFormGroup.valueChanges.pipe(
80 takeUntil(this.destroy$) 94 takeUntil(this.destroy$)
@@ -95,18 +109,33 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements Control @@ -95,18 +109,33 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements Control
95 registerOnTouched(fn: any): void { 109 registerOnTouched(fn: any): void {
96 } 110 }
97 111
  112 + setDisabledState(isDisabled: boolean) {
  113 + this.disabled = isDisabled;
  114 + if (this.disabled) {
  115 + this.snmpDeviceProfileTransportConfigurationFormGroup.disable({emitEvent: false});
  116 + } else {
  117 + this.snmpDeviceProfileTransportConfigurationFormGroup.enable({emitEvent: false});
  118 + }
  119 + }
  120 +
98 writeValue(value: SnmpDeviceProfileTransportConfiguration | null): void { 121 writeValue(value: SnmpDeviceProfileTransportConfiguration | null): void {
99 if (isDefinedAndNotNull(value)) { 122 if (isDefinedAndNotNull(value)) {
100 - this.snmpDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); 123 + this.snmpDeviceProfileTransportConfigurationFormGroup.patchValue(value, {emitEvent: !value.communicationConfigs});
101 } 124 }
102 } 125 }
103 126
104 private updateModel() { 127 private updateModel() {
105 let configuration: DeviceProfileTransportConfiguration = null; 128 let configuration: DeviceProfileTransportConfiguration = null;
106 if (this.snmpDeviceProfileTransportConfigurationFormGroup.valid) { 129 if (this.snmpDeviceProfileTransportConfigurationFormGroup.valid) {
107 - configuration = this.snmpDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration; 130 + configuration = this.snmpDeviceProfileTransportConfigurationFormGroup.getRawValue();
108 configuration.type = DeviceTransportType.SNMP; 131 configuration.type = DeviceTransportType.SNMP;
109 } 132 }
110 this.propagateChange(configuration); 133 this.propagateChange(configuration);
111 } 134 }
  135 +
  136 + validate(): ValidationErrors | null {
  137 + return this.snmpDeviceProfileTransportConfigurationFormGroup.valid ? null : {
  138 + snmpDeviceProfileTransportConfiguration: false
  139 + };
  140 + }
112 } 141 }
  1 +///
  2 +/// Copyright © 2016-2021 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 { NgModule } from '@angular/core';
  18 +import { SharedModule } from '@shared/shared.module';
  19 +import { CommonModule } from '@angular/common';
  20 +import { SnmpDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/snpm/snmp-device-profile-transport-configuration.component';
  21 +import { SnmpDeviceProfileCommunicationConfigComponent } from './snmp-device-profile-communication-config.component';
  22 +import { SnmpDeviceProfileMappingComponent } from './snmp-device-profile-mapping.component';
  23 +
  24 +@NgModule({
  25 + declarations: [
  26 + SnmpDeviceProfileTransportConfigurationComponent,
  27 + SnmpDeviceProfileCommunicationConfigComponent,
  28 + SnmpDeviceProfileMappingComponent
  29 + ],
  30 + imports: [
  31 + CommonModule,
  32 + SharedModule
  33 + ],
  34 + exports: [
  35 + SnmpDeviceProfileTransportConfigurationComponent
  36 + ]
  37 +})
  38 +export class SnmpDeviceProfileTransportModule { }
@@ -274,6 +274,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, @@ -274,6 +274,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
274 this.updateTitle(true); 274 this.updateTitle(true);
275 this.alarmsDatasource.updateAlarms(); 275 this.alarmsDatasource.updateAlarms();
276 this.clearCache(); 276 this.clearCache();
  277 + this.ctx.detectChanges();
277 } 278 }
278 279
279 public pageLinkSortDirection(): SortDirection { 280 public pageLinkSortDirection(): SortDirection {
@@ -443,7 +443,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O @@ -443,7 +443,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
443 const dataPageData = subscription.dataPages[0]; 443 const dataPageData = subscription.dataPages[0];
444 const childNodes: HierarchyNavTreeNode[] = []; 444 const childNodes: HierarchyNavTreeNode[] = [];
445 datasourcesPageData.data.forEach((childDatasource, index) => { 445 datasourcesPageData.data.forEach((childDatasource, index) => {
446 - childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, dataPageData.data[index])); 446 + childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, dataPageData.data[index], nodeCtx));
447 }); 447 });
448 nodeCtx.childrenNodesLoaded = true; 448 nodeCtx.childrenNodesLoaded = true;
449 childrenNodesLoadCb(this.prepareNodes(childNodes)); 449 childrenNodesLoadCb(this.prepareNodes(childNodes));
@@ -47,7 +47,6 @@ import { @@ -47,7 +47,6 @@ import {
47 isDefined, 47 isDefined,
48 isNumber, 48 isNumber,
49 isObject, 49 isObject,
50 - isString,  
51 isUndefined 50 isUndefined
52 } from '@core/utils'; 51 } from '@core/utils';
53 import cssjs from '@core/css/css'; 52 import cssjs from '@core/css/css';
@@ -236,6 +235,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -236,6 +235,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
236 this.updateTitle(true); 235 this.updateTitle(true);
237 this.entityDatasource.dataUpdated(); 236 this.entityDatasource.dataUpdated();
238 this.clearCache(); 237 this.clearCache();
  238 + this.ctx.detectChanges();
239 } 239 }
240 240
241 public pageLinkSortDirection(): SortDirection { 241 public pageLinkSortDirection(): SortDirection {
@@ -69,10 +69,10 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC @@ -69,10 +69,10 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
69 69
70 this.config.cellActionDescriptors.push( 70 this.config.cellActionDescriptors.push(
71 { 71 {
72 - name: this.translate.instant('resource.export'), 72 + name: this.translate.instant('resource.download'),
73 icon: 'file_download', 73 icon: 'file_download',
74 isEnabled: () => true, 74 isEnabled: () => true,
75 - onAction: ($event, entity) => this.exportResource($event, entity) 75 + onAction: ($event, entity) => this.downloadResource($event, entity)
76 } 76 }
77 ); 77 );
78 78
@@ -118,7 +118,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC @@ -118,7 +118,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
118 return this.config; 118 return this.config;
119 } 119 }
120 120
121 - exportResource($event: Event, resource: ResourceInfo) { 121 + downloadResource($event: Event, resource: ResourceInfo) {
122 if ($event) { 122 if ($event) {
123 $event.stopPropagation(); 123 $event.stopPropagation();
124 } 124 }
@@ -127,8 +127,8 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC @@ -127,8 +127,8 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
127 127
128 onResourceAction(action: EntityAction<ResourceInfo>): boolean { 128 onResourceAction(action: EntityAction<ResourceInfo>): boolean {
129 switch (action.action) { 129 switch (action.action) {
130 - case 'uploadResource':  
131 - this.exportResource(action.event, action.entity); 130 + case 'downloadResource':
  131 + this.downloadResource(action.event, action.entity);
132 return true; 132 return true;
133 } 133 }
134 return false; 134 return false;
@@ -18,16 +18,26 @@ @@ -18,16 +18,26 @@
18 <div class="tb-details-buttons" fxLayout.xs="column"> 18 <div class="tb-details-buttons" fxLayout.xs="column">
19 <button mat-raised-button color="primary" fxFlex.xs 19 <button mat-raised-button color="primary" fxFlex.xs
20 [disabled]="(isLoading$ | async)" 20 [disabled]="(isLoading$ | async)"
21 - (click)="onEntityAction($event, 'uploadResource')" 21 + (click)="onEntityAction($event, 'downloadResource')"
22 [fxShow]="!isEdit"> 22 [fxShow]="!isEdit">
23 - {{'resource.export' | translate }} 23 + {{ 'resource.download' | translate }}
24 </button> 24 </button>
25 <button mat-raised-button color="primary" fxFlex.xs 25 <button mat-raised-button color="primary" fxFlex.xs
26 [disabled]="(isLoading$ | async)" 26 [disabled]="(isLoading$ | async)"
27 (click)="onEntityAction($event, 'delete')" 27 (click)="onEntityAction($event, 'delete')"
28 [fxShow]="!hideDelete() && !isEdit"> 28 [fxShow]="!hideDelete() && !isEdit">
29 - {{'resource.delete' | translate }} 29 + {{ 'resource.delete' | translate }}
30 </button> 30 </button>
  31 + <div fxLayout="row" fxLayout.xs="column">
  32 + <button mat-raised-button
  33 + ngxClipboard
  34 + (cbOnSuccess)="onResourceIdCopied()"
  35 + [cbContent]="entity?.id?.id"
  36 + [fxShow]="!isEdit">
  37 + <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
  38 + <span translate>resource.copyId</span>
  39 + </button>
  40 + </div>
31 </div> 41 </div>
32 <div class="mat-padding" fxLayout="column"> 42 <div class="mat-padding" fxLayout="column">
33 <form [formGroup]="entityForm"> 43 <form [formGroup]="entityForm">
@@ -47,7 +57,7 @@ @@ -47,7 +57,7 @@
47 {{ 'resource.title-required' | translate }} 57 {{ 'resource.title-required' | translate }}
48 </mat-error> 58 </mat-error>
49 </mat-form-field> 59 </mat-form-field>
50 - <tb-file-input 60 + <tb-file-input *ngIf="isAdd"
51 formControlName="data" 61 formControlName="data"
52 required 62 required
53 [readAsBinary]="true" 63 [readAsBinary]="true"
@@ -59,6 +69,12 @@ @@ -59,6 +69,12 @@
59 [existingFileName]="entityForm.get('fileName')?.value" 69 [existingFileName]="entityForm.get('fileName')?.value"
60 (fileNameChanged)="entityForm?.get('fileName').patchValue($event)"> 70 (fileNameChanged)="entityForm?.get('fileName').patchValue($event)">
61 </tb-file-input> 71 </tb-file-input>
  72 + <div *ngIf="!isAdd" fxLayout="row" fxLayoutGap.gt-md="8px" fxLayoutGap.sm="8px" fxLayout.xs="column" fxLayout.md="column">
  73 + <mat-form-field fxFlex>
  74 + <mat-label translate>resource.file-name</mat-label>
  75 + <input matInput formControlName="fileName" type="text">
  76 + </mat-form-field>
  77 + </div>
62 </fieldset> 78 </fieldset>
63 </form> 79 </form>
64 </div> 80 </div>
@@ -30,6 +30,7 @@ import { @@ -30,6 +30,7 @@ import {
30 ResourceTypeTranslationMap 30 ResourceTypeTranslationMap
31 } from '@shared/models/resource.models'; 31 } from '@shared/models/resource.models';
32 import { pairwise, startWith, takeUntil } from 'rxjs/operators'; 32 import { pairwise, startWith, takeUntil } from 'rxjs/operators';
  33 +import { ActionNotificationShow } from "@core/notification/notification.actions";
33 34
34 @Component({ 35 @Component({
35 selector: 'tb-resources-library', 36 selector: 'tb-resources-library',
@@ -88,26 +89,29 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme @@ -88,26 +89,29 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
88 } 89 }
89 90
90 buildForm(entity: Resource): FormGroup { 91 buildForm(entity: Resource): FormGroup {
91 - return this.fb.group( 92 + const form = this.fb.group(
92 { 93 {
  94 + title: [entity ? entity.title : '', []],
93 resourceType: [{ 95 resourceType: [{
94 value: entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, 96 value: entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL,
95 - disabled: this.isEdit 97 + disabled: !this.isAdd
96 }, [Validators.required]], 98 }, [Validators.required]],
97 - data: [entity ? entity.data : null, [Validators.required]],  
98 fileName: [entity ? entity.fileName : null, [Validators.required]], 99 fileName: [entity ? entity.fileName : null, [Validators.required]],
99 - title: [entity ? entity.title : '', []]  
100 } 100 }
101 ); 101 );
  102 + if (this.isAdd) {
  103 + form.addControl('data', this.fb.control(null, Validators.required));
  104 + }
  105 + return form
102 } 106 }
103 107
104 updateForm(entity: Resource) { 108 updateForm(entity: Resource) {
105 - this.entityForm.patchValue({resourceType: entity.resourceType});  
106 if (this.isEdit) { 109 if (this.isEdit) {
107 this.entityForm.get('resourceType').disable({emitEvent: false}); 110 this.entityForm.get('resourceType').disable({emitEvent: false});
  111 + this.entityForm.get('fileName').disable({emitEvent: false});
108 } 112 }
109 this.entityForm.patchValue({ 113 this.entityForm.patchValue({
110 - data: entity.data, 114 + resourceType: entity.resourceType,
111 fileName: entity.fileName, 115 fileName: entity.fileName,
112 title: entity.title 116 title: entity.title
113 }); 117 });
@@ -132,4 +136,15 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme @@ -132,4 +136,15 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
132 convertToBase64File(data: string): string { 136 convertToBase64File(data: string): string {
133 return window.btoa(data); 137 return window.btoa(data);
134 } 138 }
  139 +
  140 + onResourceIdCopied() {
  141 + this.store.dispatch(new ActionNotificationShow(
  142 + {
  143 + message: this.translate.instant('resource.idCopiedMessage'),
  144 + type: 'success',
  145 + duration: 750,
  146 + verticalPosition: 'bottom',
  147 + horizontalPosition: 'right'
  148 + }));
  149 + }
135 } 150 }
@@ -98,7 +98,7 @@ const routes: Routes = [ @@ -98,7 +98,7 @@ const routes: Routes = [
98 } 98 }
99 }, 99 },
100 { 100 {
101 - path: ':customerId/edges', 101 + path: ':customerId/edgeInstances',
102 component: EntitiesTableComponent, 102 component: EntitiesTableComponent,
103 data: { 103 data: {
104 auth: [Authority.TENANT_ADMIN], 104 auth: [Authority.TENANT_ADMIN],
@@ -169,7 +169,7 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C @@ -169,7 +169,7 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
169 if ($event) { 169 if ($event) {
170 $event.stopPropagation(); 170 $event.stopPropagation();
171 } 171 }
172 - this.router.navigateByUrl(`customers/${customer.id.id}/edges`); 172 + this.router.navigateByUrl(`customers/${customer.id.id}/edgeInstances`);
173 } 173 }
174 174
175 onCustomerAction(action: EntityAction<Customer>): boolean { 175 onCustomerAction(action: EntityAction<Customer>): boolean {
@@ -362,7 +362,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig< @@ -362,7 +362,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
362 if (this.config.componentsData.dashboardScope === 'customer') { 362 if (this.config.componentsData.dashboardScope === 'customer') {
363 this.router.navigateByUrl(`customers/${this.config.componentsData.customerId}/dashboards/${dashboard.id.id}`); 363 this.router.navigateByUrl(`customers/${this.config.componentsData.customerId}/dashboards/${dashboard.id.id}`);
364 } else if (this.config.componentsData.dashboardScope === 'edge') { 364 } else if (this.config.componentsData.dashboardScope === 'edge') {
365 - this.router.navigateByUrl(`edges/${this.config.componentsData.edgeId}/dashboards/${dashboard.id.id}`); 365 + this.router.navigateByUrl(`edgeInstances/${this.config.componentsData.edgeId}/dashboards/${dashboard.id.id}`);
366 } else { 366 } else {
367 this.router.navigateByUrl(`dashboards/${dashboard.id.id}`); 367 this.router.navigateByUrl(`dashboards/${dashboard.id.id}`);
368 } 368 }
@@ -15,7 +15,16 @@ @@ -15,7 +15,16 @@
15 /// 15 ///
16 16
17 import { Component, forwardRef, Input, OnInit } from '@angular/core'; 17 import { Component, forwardRef, Input, OnInit } from '@angular/core';
18 -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; 18 +import {
  19 + ControlValueAccessor,
  20 + FormBuilder,
  21 + FormGroup,
  22 + NG_VALIDATORS,
  23 + NG_VALUE_ACCESSOR,
  24 + ValidationErrors,
  25 + Validator,
  26 + Validators
  27 +} from '@angular/forms';
19 import { Store } from '@ngrx/store'; 28 import { Store } from '@ngrx/store';
20 import { AppState } from '@app/core/core.state'; 29 import { AppState } from '@app/core/core.state';
21 import { coerceBooleanProperty } from '@angular/cdk/coercion'; 30 import { coerceBooleanProperty } from '@angular/cdk/coercion';
@@ -29,13 +38,20 @@ import { @@ -29,13 +38,20 @@ import {
29 selector: 'tb-device-data', 38 selector: 'tb-device-data',
30 templateUrl: './device-data.component.html', 39 templateUrl: './device-data.component.html',
31 styleUrls: [], 40 styleUrls: [],
32 - providers: [{  
33 - provide: NG_VALUE_ACCESSOR,  
34 - useExisting: forwardRef(() => DeviceDataComponent),  
35 - multi: true  
36 - }] 41 + providers: [
  42 + {
  43 + provide: NG_VALUE_ACCESSOR,
  44 + useExisting: forwardRef(() => DeviceDataComponent),
  45 + multi: true
  46 + },
  47 + {
  48 + provide: NG_VALIDATORS,
  49 + useExisting: forwardRef(() => DeviceDataComponent),
  50 + multi: true
  51 + },
  52 + ]
37 }) 53 })
38 -export class DeviceDataComponent implements ControlValueAccessor, OnInit { 54 +export class DeviceDataComponent implements ControlValueAccessor, OnInit, Validator {
39 55
40 deviceDataFormGroup: FormGroup; 56 deviceDataFormGroup: FormGroup;
41 57
@@ -97,6 +113,12 @@ export class DeviceDataComponent implements ControlValueAccessor, OnInit { @@ -97,6 +113,12 @@ export class DeviceDataComponent implements ControlValueAccessor, OnInit {
97 this.deviceDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false}); 113 this.deviceDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false});
98 } 114 }
99 115
  116 + validate(): ValidationErrors | null {
  117 + return this.deviceDataFormGroup.valid ? null : {
  118 + deviceDataForm: false
  119 + };
  120 + }
  121 +
100 private updateModel() { 122 private updateModel() {
101 let deviceData: DeviceData = null; 123 let deviceData: DeviceData = null;
102 if (this.deviceDataFormGroup.valid) { 124 if (this.deviceDataFormGroup.valid) {
@@ -15,27 +15,39 @@ @@ -15,27 +15,39 @@
15 /// 15 ///
16 16
17 import { Component, forwardRef, Input, OnInit } from '@angular/core'; 17 import { Component, forwardRef, Input, OnInit } from '@angular/core';
18 -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; 18 +import {
  19 + ControlValueAccessor,
  20 + FormBuilder,
  21 + FormGroup,
  22 + NG_VALIDATORS,
  23 + NG_VALUE_ACCESSOR,
  24 + ValidationErrors,
  25 + Validator,
  26 + Validators
  27 +} from '@angular/forms';
19 import { Store } from '@ngrx/store'; 28 import { Store } from '@ngrx/store';
20 import { AppState } from '@app/core/core.state'; 29 import { AppState } from '@app/core/core.state';
21 import { coerceBooleanProperty } from '@angular/cdk/coercion'; 30 import { coerceBooleanProperty } from '@angular/cdk/coercion';
22 -import {  
23 - DeviceTransportConfiguration,  
24 - DeviceTransportType  
25 -} from '@shared/models/device.models'; 31 +import { DeviceTransportConfiguration, DeviceTransportType } from '@shared/models/device.models';
26 import { deepClone } from '@core/utils'; 32 import { deepClone } from '@core/utils';
27 33
28 @Component({ 34 @Component({
29 selector: 'tb-device-transport-configuration', 35 selector: 'tb-device-transport-configuration',
30 templateUrl: './device-transport-configuration.component.html', 36 templateUrl: './device-transport-configuration.component.html',
31 styleUrls: [], 37 styleUrls: [],
32 - providers: [{  
33 - provide: NG_VALUE_ACCESSOR,  
34 - useExisting: forwardRef(() => DeviceTransportConfigurationComponent),  
35 - multi: true  
36 - }] 38 + providers: [
  39 + {
  40 + provide: NG_VALUE_ACCESSOR,
  41 + useExisting: forwardRef(() => DeviceTransportConfigurationComponent),
  42 + multi: true
  43 + },
  44 + {
  45 + provide: NG_VALIDATORS,
  46 + useExisting: forwardRef(() => DeviceTransportConfigurationComponent),
  47 + multi: true
  48 + }]
37 }) 49 })
38 -export class DeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { 50 +export class DeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit, Validator {
39 51
40 deviceTransportType = DeviceTransportType; 52 deviceTransportType = DeviceTransportType;
41 53
@@ -92,7 +104,15 @@ export class DeviceTransportConfigurationComponent implements ControlValueAccess @@ -92,7 +104,15 @@ export class DeviceTransportConfigurationComponent implements ControlValueAccess
92 if (configuration) { 104 if (configuration) {
93 delete configuration.type; 105 delete configuration.type;
94 } 106 }
95 - this.deviceTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); 107 + setTimeout(() => {
  108 + this.deviceTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false});
  109 + }, 0);
  110 + }
  111 +
  112 + validate(): ValidationErrors | null {
  113 + return this.deviceTransportConfigurationFormGroup.valid ? null : {
  114 + deviceTransportConfiguration: false
  115 + };
96 } 116 }
97 117
98 private updateModel() { 118 private updateModel() {
@@ -15,10 +15,119 @@ @@ -15,10 +15,119 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<form [formGroup]="snmpDeviceTransportConfigurationFormGroup" style="padding-bottom: 16px;">  
19 - <tb-json-object-edit  
20 - [required]="required"  
21 - label="{{ 'device-profile.transport-type-snmp-hint' | translate }}"  
22 - formControlName="configuration">  
23 - </tb-json-object-edit> 18 +<form [formGroup]="snmpDeviceTransportConfigurationFormGroup" style="padding-bottom: 16px;" fxLayoutGap="8px" fxLayout="column">
  19 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column">
  20 + <mat-form-field fxFlex>
  21 + <mat-label translate>device-profile.snmp.host</mat-label>
  22 + <input matInput formControlName="host" required>
  23 + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('host').hasError('required') ||
  24 + snmpDeviceTransportConfigurationFormGroup.get('host').hasError('pattern')">
  25 + {{ 'device-profile.snmp.host-required' | translate }}
  26 + </mat-error>
  27 + </mat-form-field>
  28 + <mat-form-field fxFlex>
  29 + <mat-label translate>device-profile.snmp.port</mat-label>
  30 + <input matInput formControlName="port" type="number" min="0" required>
  31 + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('port').hasError('required')">
  32 + {{ 'device-profile.snmp.port-required' | translate }}
  33 + </mat-error>
  34 + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('port').hasError('pattern') ||
  35 + snmpDeviceTransportConfigurationFormGroup.get('port').hasError('min')">
  36 + {{ 'device-profile.snmp.port-format' | translate }}
  37 + </mat-error>
  38 + </mat-form-field>
  39 + </div>
  40 + <mat-form-field class="mat-block" floatLabel="always" hideRequiredMarker>
  41 + <mat-label translate>device-profile.snmp.protocol-version</mat-label>
  42 + <mat-select formControlName="protocolVersion" required>
  43 + <mat-option *ngFor="let snmpDeviceProtocolVersion of snmpDeviceProtocolVersions" [value]="snmpDeviceProtocolVersion">
  44 + {{ snmpDeviceProtocolVersion | lowercase }}
  45 + </mat-option>
  46 + </mat-select>
  47 + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('protocolVersion').hasError('required')">
  48 + {{ 'device-profile.snmp.protocol-version-required' | translate }}
  49 + </mat-error>
  50 + </mat-form-field>
  51 + <section *ngIf="!isV3protocolVersion()">
  52 + <mat-form-field class="mat-block">
  53 + <mat-label translate>device-profile.snmp.community</mat-label>
  54 + <input matInput formControlName="community" required>
  55 + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('community').hasError('required') ||
  56 + snmpDeviceTransportConfigurationFormGroup.get('community').hasError('pattern')">
  57 + {{ 'device-profile.snmp.community-required' | translate }}
  58 + </mat-error>
  59 + </mat-form-field>
  60 + </section>
  61 + <section *ngIf="isV3protocolVersion()">
  62 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column">
  63 + <mat-form-field fxFlex>
  64 + <mat-label translate>device-profile.snmp.user-name</mat-label>
  65 + <input matInput formControlName="username" required>
  66 + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('username').hasError('required') ||
  67 + snmpDeviceTransportConfigurationFormGroup.get('username').hasError('pattern')">
  68 + {{ 'device-profile.snmp.user-name-required' | translate }}
  69 + </mat-error>
  70 + </mat-form-field>
  71 + <mat-form-field fxFlex>
  72 + <mat-label translate>device-profile.snmp.security-name</mat-label>
  73 + <input matInput formControlName="securityName" required>
  74 + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('securityName').hasError('required') ||
  75 + snmpDeviceTransportConfigurationFormGroup.get('securityName').hasError('pattern')">
  76 + {{ 'device-profile.snmp.security-name-required' | translate }}
  77 + </mat-error>
  78 + </mat-form-field>
  79 + </div>
  80 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column">
  81 + <mat-form-field fxFlex floatLabel="always" hideRequiredMarker>
  82 + <mat-label translate>device-profile.snmp.authentication-protocol</mat-label>
  83 + <mat-select formControlName="authenticationProtocol" required>
  84 + <mat-option *ngFor="let snmpAuthenticationProtocol of snmpAuthenticationProtocols" [value]="snmpAuthenticationProtocol">
  85 + {{ snmpAuthenticationProtocolTranslation.get(snmpAuthenticationProtocol) }}
  86 + </mat-option>
  87 + </mat-select>
  88 + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('authenticationProtocol').hasError('required')">
  89 + {{ 'device-profile.snmp.authentication-protocol-required' | translate }}
  90 + </mat-error>
  91 + </mat-form-field>
  92 + <mat-form-field fxFlex>
  93 + <mat-label translate>device-profile.snmp.authentication-passphrase</mat-label>
  94 + <input matInput formControlName="authenticationPassphrase" required>
  95 + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').hasError('required') ||
  96 + snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').hasError('pattern')">
  97 + {{ 'device-profile.snmp.authentication-passphrase-required' | translate }}
  98 + </mat-error>
  99 + </mat-form-field>
  100 + </div>
  101 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column">
  102 + <mat-form-field fxFlex floatLabel="always" hideRequiredMarker>
  103 + <mat-label translate>device-profile.snmp.privacy-protocol</mat-label>
  104 + <mat-select formControlName="privacyProtocol" required>
  105 + <mat-option *ngFor="let snmpPrivacyProtocol of snmpPrivacyProtocols" [value]="snmpPrivacyProtocol">
  106 + {{ snmpPrivacyProtocolTranslation.get(snmpPrivacyProtocol) }}
  107 + </mat-option>
  108 + </mat-select>
  109 + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('privacyProtocol').hasError('required')">
  110 + {{ 'device-profile.snmp.privacy-protocol-required' | translate }}
  111 + </mat-error>
  112 + </mat-form-field>
  113 + <mat-form-field fxFlex>
  114 + <mat-label translate>device-profile.snmp.privacy-passphrase</mat-label>
  115 + <input matInput formControlName="privacyPassphrase" required>
  116 + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').hasError('required') ||
  117 + snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').hasError('pattern')">
  118 + {{ 'device-profile.snmp.privacy-passphrase-required' | translate }}
  119 + </mat-error>
  120 + </mat-form-field>
  121 + </div>
  122 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column">
  123 + <mat-form-field fxFlex>
  124 + <mat-label translate>device-profile.snmp.context-name</mat-label>
  125 + <input matInput formControlName="contextName">
  126 + </mat-form-field>
  127 + <mat-form-field fxFlex>
  128 + <mat-label translate>device-profile.snmp.engine-id</mat-label>
  129 + <input matInput formControlName="engineId">
  130 + </mat-form-field>
  131 + </div>
  132 + </section>
24 </form> 133 </form>
@@ -14,31 +14,57 @@ @@ -14,31 +14,57 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import {Component, forwardRef, Input, OnInit} from '@angular/core';  
18 -import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators} from '@angular/forms';  
19 -import {Store} from '@ngrx/store';  
20 -import {AppState} from '@app/core/core.state';  
21 -import {coerceBooleanProperty} from '@angular/cdk/coercion'; 17 +import { Component, forwardRef, Input, OnInit } from '@angular/core';
  18 +import {
  19 + ControlValueAccessor,
  20 + FormBuilder,
  21 + FormGroup,
  22 + NG_VALIDATORS,
  23 + NG_VALUE_ACCESSOR,
  24 + ValidationErrors,
  25 + Validator,
  26 + Validators
  27 +} from '@angular/forms';
  28 +import { Store } from '@ngrx/store';
  29 +import { AppState } from '@app/core/core.state';
  30 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
22 import { 31 import {
23 DeviceTransportConfiguration, 32 DeviceTransportConfiguration,
24 DeviceTransportType, 33 DeviceTransportType,
25 - SnmpDeviceTransportConfiguration 34 + SnmpAuthenticationProtocol,
  35 + SnmpAuthenticationProtocolTranslationMap,
  36 + SnmpDeviceProtocolVersion,
  37 + SnmpDeviceTransportConfiguration,
  38 + SnmpPrivacyProtocol,
  39 + SnmpPrivacyProtocolTranslationMap
26 } from '@shared/models/device.models'; 40 } from '@shared/models/device.models';
  41 +import { isDefinedAndNotNull } from '@core/utils';
27 42
28 @Component({ 43 @Component({
29 selector: 'tb-snmp-device-transport-configuration', 44 selector: 'tb-snmp-device-transport-configuration',
30 templateUrl: './snmp-device-transport-configuration.component.html', 45 templateUrl: './snmp-device-transport-configuration.component.html',
31 styleUrls: [], 46 styleUrls: [],
32 - providers: [{  
33 - provide: NG_VALUE_ACCESSOR,  
34 - useExisting: forwardRef(() => SnmpDeviceTransportConfigurationComponent),  
35 - multi: true  
36 - }] 47 + providers: [
  48 + {
  49 + provide: NG_VALUE_ACCESSOR,
  50 + useExisting: forwardRef(() => SnmpDeviceTransportConfigurationComponent),
  51 + multi: true
  52 + }, {
  53 + provide: NG_VALIDATORS,
  54 + useExisting: forwardRef(() => SnmpDeviceTransportConfigurationComponent),
  55 + multi: true
  56 + }]
37 }) 57 })
38 -export class SnmpDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { 58 +export class SnmpDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit, Validator {
39 59
40 snmpDeviceTransportConfigurationFormGroup: FormGroup; 60 snmpDeviceTransportConfigurationFormGroup: FormGroup;
41 61
  62 + snmpDeviceProtocolVersions = Object.values(SnmpDeviceProtocolVersion);
  63 + snmpAuthenticationProtocols = Object.values(SnmpAuthenticationProtocol);
  64 + snmpAuthenticationProtocolTranslation = SnmpAuthenticationProtocolTranslationMap;
  65 + snmpPrivacyProtocols = Object.values(SnmpPrivacyProtocol);
  66 + snmpPrivacyProtocolTranslation = SnmpPrivacyProtocolTranslationMap;
  67 +
42 private requiredValue: boolean; 68 private requiredValue: boolean;
43 69
44 get required(): boolean { 70 get required(): boolean {
@@ -53,8 +79,7 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc @@ -53,8 +79,7 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc
53 @Input() 79 @Input()
54 disabled: boolean; 80 disabled: boolean;
55 81
56 - private propagateChange = (v: any) => {  
57 - }; 82 + private propagateChange = (v: any) => { };
58 83
59 constructor(private store: Store<AppState>, 84 constructor(private store: Store<AppState>,
60 private fb: FormBuilder) { 85 private fb: FormBuilder) {
@@ -69,13 +94,33 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc @@ -69,13 +94,33 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc
69 94
70 ngOnInit() { 95 ngOnInit() {
71 this.snmpDeviceTransportConfigurationFormGroup = this.fb.group({ 96 this.snmpDeviceTransportConfigurationFormGroup = this.fb.group({
72 - configuration: [null, Validators.required] 97 + host: ['', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]],
  98 + port: [null, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
  99 + protocolVersion: [SnmpDeviceProtocolVersion.V2C, Validators.required],
  100 + community: ['public', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]],
  101 + username: ['', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]],
  102 + securityName: ['public', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]],
  103 + contextName: [null],
  104 + authenticationProtocol: [SnmpAuthenticationProtocol.SHA_512, Validators.required],
  105 + authenticationPassphrase: ['', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]],
  106 + privacyProtocol: [SnmpPrivacyProtocol.DES, Validators.required],
  107 + privacyPassphrase: ['', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]],
  108 + engineId: ['']
  109 + });
  110 + this.snmpDeviceTransportConfigurationFormGroup.get('protocolVersion').valueChanges.subscribe((protocol: SnmpDeviceProtocolVersion) => {
  111 + this.updateDisabledFormValue(protocol);
73 }); 112 });
74 this.snmpDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { 113 this.snmpDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => {
75 this.updateModel(); 114 this.updateModel();
76 }); 115 });
77 } 116 }
78 117
  118 + validate(): ValidationErrors | null {
  119 + return this.snmpDeviceTransportConfigurationFormGroup.valid ? null : {
  120 + snmpDeviceTransportConfiguration: false
  121 + };
  122 + }
  123 +
79 setDisabledState(isDisabled: boolean): void { 124 setDisabledState(isDisabled: boolean): void {
80 this.disabled = isDisabled; 125 this.disabled = isDisabled;
81 if (this.disabled) { 126 if (this.disabled) {
@@ -86,13 +131,46 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc @@ -86,13 +131,46 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc
86 } 131 }
87 132
88 writeValue(value: SnmpDeviceTransportConfiguration | null): void { 133 writeValue(value: SnmpDeviceTransportConfiguration | null): void {
89 - this.snmpDeviceTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); 134 + if (isDefinedAndNotNull(value)) {
  135 + this.snmpDeviceTransportConfigurationFormGroup.patchValue(value, {emitEvent: false});
  136 + if (this.snmpDeviceTransportConfigurationFormGroup.enabled) {
  137 + this.updateDisabledFormValue(value.protocolVersion || SnmpDeviceProtocolVersion.V2C);
  138 + }
  139 + }
  140 + }
  141 +
  142 + isV3protocolVersion(): boolean {
  143 + return this.snmpDeviceTransportConfigurationFormGroup.get('protocolVersion').value === SnmpDeviceProtocolVersion.V3;
  144 + }
  145 +
  146 + private updateDisabledFormValue(protocol: SnmpDeviceProtocolVersion) {
  147 + if (protocol === SnmpDeviceProtocolVersion.V3) {
  148 + this.snmpDeviceTransportConfigurationFormGroup.get('community').disable({emitEvent: false});
  149 + this.snmpDeviceTransportConfigurationFormGroup.get('username').enable({emitEvent: false});
  150 + this.snmpDeviceTransportConfigurationFormGroup.get('securityName').enable({emitEvent: false});
  151 + this.snmpDeviceTransportConfigurationFormGroup.get('contextName').enable({emitEvent: false});
  152 + this.snmpDeviceTransportConfigurationFormGroup.get('authenticationProtocol').enable({emitEvent: false});
  153 + this.snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').enable({emitEvent: false});
  154 + this.snmpDeviceTransportConfigurationFormGroup.get('privacyProtocol').enable({emitEvent: false});
  155 + this.snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').enable({emitEvent: false});
  156 + this.snmpDeviceTransportConfigurationFormGroup.get('engineId').enable({emitEvent: false});
  157 + } else {
  158 + this.snmpDeviceTransportConfigurationFormGroup.get('community').enable({emitEvent: false});
  159 + this.snmpDeviceTransportConfigurationFormGroup.get('username').disable({emitEvent: false});
  160 + this.snmpDeviceTransportConfigurationFormGroup.get('securityName').disable({emitEvent: false});
  161 + this.snmpDeviceTransportConfigurationFormGroup.get('contextName').disable({emitEvent: false});
  162 + this.snmpDeviceTransportConfigurationFormGroup.get('authenticationProtocol').disable({emitEvent: false});
  163 + this.snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').disable({emitEvent: false});
  164 + this.snmpDeviceTransportConfigurationFormGroup.get('privacyProtocol').disable({emitEvent: false});
  165 + this.snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').disable({emitEvent: false});
  166 + this.snmpDeviceTransportConfigurationFormGroup.get('engineId').disable({emitEvent: false});
  167 + }
90 } 168 }
91 169
92 private updateModel() { 170 private updateModel() {
93 let configuration: DeviceTransportConfiguration = null; 171 let configuration: DeviceTransportConfiguration = null;
94 if (this.snmpDeviceTransportConfigurationFormGroup.valid) { 172 if (this.snmpDeviceTransportConfigurationFormGroup.valid) {
95 - configuration = this.snmpDeviceTransportConfigurationFormGroup.getRawValue().configuration; 173 + configuration = this.snmpDeviceTransportConfigurationFormGroup.value;
96 configuration.type = DeviceTransportType.SNMP; 174 configuration.type = DeviceTransportType.SNMP;
97 } 175 }
98 this.propagateChange(configuration); 176 this.propagateChange(configuration);
@@ -42,7 +42,7 @@ import { @@ -42,7 +42,7 @@ import {
42 42
43 const routes: Routes = [ 43 const routes: Routes = [
44 { 44 {
45 - path: 'edges', 45 + path: 'edgeInstances',
46 data: { 46 data: {
47 breadcrumb: { 47 breadcrumb: {
48 label: 'edge.edge-instances', 48 label: 'edge.edge-instances',
@@ -187,6 +187,24 @@ const routes: Routes = [ @@ -187,6 +187,24 @@ const routes: Routes = [
187 } 187 }
188 } 188 }
189 ] 189 ]
  190 + }
  191 + ]
  192 + },
  193 + {
  194 + path: 'edgeManagement',
  195 + data: {
  196 + breadcrumb: {
  197 + label: 'edge.management',
  198 + icon: 'settings_input_antenna'
  199 + }
  200 + },
  201 + children: [
  202 + {
  203 + path: '',
  204 + data: {
  205 + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
  206 + redirectTo: '/edgeManagement/ruleChains'
  207 + }
190 }, 208 },
191 { 209 {
192 path: 'ruleChains', 210 path: 'ruleChains',
@@ -417,7 +417,7 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI @@ -417,7 +417,7 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
417 suffix = 'ruleChains'; 417 suffix = 'ruleChains';
418 break; 418 break;
419 } 419 }
420 - this.router.navigateByUrl(`edges/${edge.id.id}/${suffix}`); 420 + this.router.navigateByUrl(`edgeInstances/${edge.id.id}/${suffix}`);
421 } 421 }
422 422
423 assignToCustomer($event: Event, edgesIds: Array<EdgeId>) { 423 assignToCustomer($event: Event, edgesIds: Array<EdgeId>) {
@@ -152,7 +152,7 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O @@ -152,7 +152,7 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
152 } 152 }
153 if (this.ruleNode.targetRuleChainId) { 153 if (this.ruleNode.targetRuleChainId) {
154 if (this.ruleChainType === RuleChainType.EDGE) { 154 if (this.ruleChainType === RuleChainType.EDGE) {
155 - this.router.navigateByUrl(`/edges/ruleChains/${this.ruleNode.targetRuleChainId}`); 155 + this.router.navigateByUrl(`/edgeManagement/ruleChains/${this.ruleNode.targetRuleChainId}`);
156 } else { 156 } else {
157 this.router.navigateByUrl(`/ruleChains/${this.ruleNode.targetRuleChainId}`); 157 this.router.navigateByUrl(`/ruleChains/${this.ruleNode.targetRuleChainId}`);
158 } 158 }
@@ -1289,7 +1289,7 @@ export class RuleChainPageComponent extends PageComponent @@ -1289,7 +1289,7 @@ export class RuleChainPageComponent extends PageComponent
1289 if (this.ruleChainType !== RuleChainType.EDGE) { 1289 if (this.ruleChainType !== RuleChainType.EDGE) {
1290 this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`); 1290 this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`);
1291 } else { 1291 } else {
1292 - this.router.navigateByUrl(`edges/ruleChains/${this.ruleChain.id.id}`); 1292 + this.router.navigateByUrl(`edgeManagement/ruleChains/${this.ruleChain.id.id}`);
1293 } 1293 }
1294 } else { 1294 } else {
1295 this.createRuleChainModel(); 1295 this.createRuleChainModel();
@@ -285,7 +285,7 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig< @@ -285,7 +285,7 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
285 if (ruleChainImport) { 285 if (ruleChainImport) {
286 this.itembuffer.storeRuleChainImport(ruleChainImport); 286 this.itembuffer.storeRuleChainImport(ruleChainImport);
287 if (this.config.componentsData.ruleChainScope === 'edges') { 287 if (this.config.componentsData.ruleChainScope === 'edges') {
288 - this.router.navigateByUrl(`edges/ruleChains/ruleChain/import`); 288 + this.router.navigateByUrl(`edgeManagement/ruleChains/ruleChain/import`);
289 } else { 289 } else {
290 this.router.navigateByUrl(`ruleChains/ruleChain/import`); 290 this.router.navigateByUrl(`ruleChains/ruleChain/import`);
291 } 291 }
@@ -298,9 +298,9 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig< @@ -298,9 +298,9 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
298 $event.stopPropagation(); 298 $event.stopPropagation();
299 } 299 }
300 if (this.config.componentsData.ruleChainScope === 'edges') { 300 if (this.config.componentsData.ruleChainScope === 'edges') {
301 - this.router.navigateByUrl(`edges/ruleChains/${ruleChain.id.id}`); 301 + this.router.navigateByUrl(`edgeManagement/ruleChains/${ruleChain.id.id}`);
302 } else if (this.config.componentsData.ruleChainScope === 'edge') { 302 } else if (this.config.componentsData.ruleChainScope === 'edge') {
303 - this.router.navigateByUrl(`edges/${this.config.componentsData.edgeId}/ruleChains/${ruleChain.id.id}`); 303 + this.router.navigateByUrl(`edgeInstances/${this.config.componentsData.edgeId}/ruleChains/${ruleChain.id.id}`);
304 } else { 304 } else {
305 this.router.navigateByUrl(`ruleChains/${ruleChain.id.id}`); 305 this.router.navigateByUrl(`ruleChains/${ruleChain.id.id}`);
306 } 306 }
@@ -50,7 +50,7 @@ export class RuleNodeComponent extends FcNodeComponent implements OnInit { @@ -50,7 +50,7 @@ export class RuleNodeComponent extends FcNodeComponent implements OnInit {
50 } 50 }
51 if (node.targetRuleChainId) { 51 if (node.targetRuleChainId) {
52 if (node.ruleChainType === RuleChainType.EDGE) { 52 if (node.ruleChainType === RuleChainType.EDGE) {
53 - this.router.navigateByUrl(`/edges/ruleChains/${node.targetRuleChainId}`); 53 + this.router.navigateByUrl(`/edgeManagement/ruleChains/${node.targetRuleChainId}`);
54 } else { 54 } else {
55 this.router.navigateByUrl(`/ruleChains/${node.targetRuleChainId}`); 55 this.router.navigateByUrl(`/ruleChains/${node.targetRuleChainId}`);
56 } 56 }
@@ -120,6 +120,7 @@ export const HelpLinks = { @@ -120,6 +120,7 @@ export const HelpLinks = {
120 entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views', 120 entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views',
121 entitiesImport: helpBaseUrl + '/docs/user-guide/bulk-provisioning', 121 entitiesImport: helpBaseUrl + '/docs/user-guide/bulk-provisioning',
122 rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains', 122 rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains',
  123 + resources: helpBaseUrl + '/docs/user-guide/ui/resources',
123 dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards', 124 dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards',
124 otaUpdates: helpBaseUrl + '/docs/user-guide/ui/ota-updates', 125 otaUpdates: helpBaseUrl + '/docs/user-guide/ui/ota-updates',
125 widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles', 126 widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles',
@@ -147,6 +148,22 @@ export enum ValueType { @@ -147,6 +148,22 @@ export enum ValueType {
147 JSON = 'JSON' 148 JSON = 'JSON'
148 } 149 }
149 150
  151 +export enum DataType {
  152 + STRING = 'STRING',
  153 + LONG = 'LONG',
  154 + BOOLEAN = 'BOOLEAN',
  155 + DOUBLE = 'DOUBLE',
  156 + JSON = 'JSON'
  157 +}
  158 +
  159 +export const DataTypeTranslationMap = new Map([
  160 + [DataType.STRING, 'value.string'],
  161 + [DataType.LONG, 'value.integer'],
  162 + [DataType.BOOLEAN, 'value.boolean'],
  163 + [DataType.DOUBLE, 'value.double'],
  164 + [DataType.JSON, 'value.json']
  165 +]);
  166 +
150 export const valueTypesMap = new Map<ValueType, ValueTypeData>( 167 export const valueTypesMap = new Map<ValueType, ValueTypeData>(
151 [ 168 [
152 [ 169 [
@@ -29,6 +29,7 @@ import * as _moment from 'moment'; @@ -29,6 +29,7 @@ import * as _moment from 'moment';
29 import { AbstractControl, ValidationErrors } from '@angular/forms'; 29 import { AbstractControl, ValidationErrors } from '@angular/forms';
30 import { OtaPackageId } from '@shared/models/id/ota-package-id'; 30 import { OtaPackageId } from '@shared/models/id/ota-package-id';
31 import { DashboardId } from '@shared/models/id/dashboard-id'; 31 import { DashboardId } from '@shared/models/id/dashboard-id';
  32 +import { DataType } from '@shared/models/constants';
32 33
33 export enum DeviceProfileType { 34 export enum DeviceProfileType {
34 DEFAULT = 'DEFAULT', 35 DEFAULT = 'DEFAULT',
@@ -257,7 +258,35 @@ export interface Lwm2mDeviceProfileTransportConfiguration { @@ -257,7 +258,35 @@ export interface Lwm2mDeviceProfileTransportConfiguration {
257 } 258 }
258 259
259 export interface SnmpDeviceProfileTransportConfiguration { 260 export interface SnmpDeviceProfileTransportConfiguration {
260 - [key: string]: any; 261 + timeoutMs?: number;
  262 + retries?: number;
  263 + communicationConfigs?: SnmpCommunicationConfig[];
  264 +}
  265 +
  266 +export enum SnmpSpecType {
  267 + TELEMETRY_QUERYING = 'TELEMETRY_QUERYING',
  268 + CLIENT_ATTRIBUTES_QUERYING = 'CLIENT_ATTRIBUTES_QUERYING',
  269 + SHARED_ATTRIBUTES_SETTING = 'SHARED_ATTRIBUTES_SETTING',
  270 + TO_DEVICE_RPC_REQUEST = 'TO_DEVICE_RPC_REQUEST'
  271 +}
  272 +
  273 +export const SnmpSpecTypeTranslationMap = new Map<SnmpSpecType, string>([
  274 + [SnmpSpecType.TELEMETRY_QUERYING, ' Telemetry'],
  275 + [SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING, 'Client attributes'],
  276 + [SnmpSpecType.SHARED_ATTRIBUTES_SETTING, 'Shared attributes'],
  277 + [SnmpSpecType.TO_DEVICE_RPC_REQUEST, 'RPC request']
  278 +]);
  279 +
  280 +export interface SnmpCommunicationConfig {
  281 + spec: SnmpSpecType;
  282 + mappings: SnmpMapping[];
  283 + queryingFrequencyMs?: number;
  284 +}
  285 +
  286 +export interface SnmpMapping {
  287 + oid: string;
  288 + key: string;
  289 + dataType: DataType;
261 } 290 }
262 291
263 export type DeviceProfileTransportConfigurations = DefaultDeviceProfileTransportConfiguration & 292 export type DeviceProfileTransportConfigurations = DefaultDeviceProfileTransportConfiguration &
@@ -332,7 +361,11 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT @@ -332,7 +361,11 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT
332 transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M}; 361 transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M};
333 break; 362 break;
334 case DeviceTransportType.SNMP: 363 case DeviceTransportType.SNMP:
335 - const snmpTransportConfiguration: SnmpDeviceProfileTransportConfiguration = {}; 364 + const snmpTransportConfiguration: SnmpDeviceProfileTransportConfiguration = {
  365 + timeoutMs: 500,
  366 + retries: 0,
  367 + communicationConfigs: null
  368 + };
336 transportConfiguration = {...snmpTransportConfiguration, type: DeviceTransportType.SNMP}; 369 transportConfiguration = {...snmpTransportConfiguration, type: DeviceTransportType.SNMP};
337 break; 370 break;
338 } 371 }
@@ -361,7 +394,12 @@ export function createDeviceTransportConfiguration(type: DeviceTransportType): D @@ -361,7 +394,12 @@ export function createDeviceTransportConfiguration(type: DeviceTransportType): D
361 transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M}; 394 transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M};
362 break; 395 break;
363 case DeviceTransportType.SNMP: 396 case DeviceTransportType.SNMP:
364 - const snmpTransportConfiguration: SnmpDeviceTransportConfiguration = {}; 397 + const snmpTransportConfiguration: SnmpDeviceTransportConfiguration = {
  398 + host: 'localhost',
  399 + port: 161,
  400 + protocolVersion: SnmpDeviceProtocolVersion.V2C,
  401 + community: 'public'
  402 + };
365 transportConfiguration = {...snmpTransportConfiguration, type: DeviceTransportType.SNMP}; 403 transportConfiguration = {...snmpTransportConfiguration, type: DeviceTransportType.SNMP};
366 break; 404 break;
367 } 405 }
@@ -539,8 +577,57 @@ export interface Lwm2mDeviceTransportConfiguration { @@ -539,8 +577,57 @@ export interface Lwm2mDeviceTransportConfiguration {
539 [key: string]: any; 577 [key: string]: any;
540 } 578 }
541 579
  580 +export enum SnmpDeviceProtocolVersion {
  581 + V1 = 'V1',
  582 + V2C = 'V2C',
  583 + V3 = 'V3'
  584 +}
  585 +
  586 +export enum SnmpAuthenticationProtocol {
  587 + SHA_1 = 'SHA_1',
  588 + SHA_224 = 'SHA_224',
  589 + SHA_256 = 'SHA_256',
  590 + SHA_384 = 'SHA_384',
  591 + SHA_512 = 'SHA_512',
  592 + MD5 = 'MD%'
  593 +}
  594 +
  595 +export const SnmpAuthenticationProtocolTranslationMap = new Map<SnmpAuthenticationProtocol, string>([
  596 + [SnmpAuthenticationProtocol.SHA_1, 'SHA-1'],
  597 + [SnmpAuthenticationProtocol.SHA_224, 'SHA-224'],
  598 + [SnmpAuthenticationProtocol.SHA_256, 'SHA-256'],
  599 + [SnmpAuthenticationProtocol.SHA_384, 'SHA-384'],
  600 + [SnmpAuthenticationProtocol.SHA_512, 'SHA-512'],
  601 + [SnmpAuthenticationProtocol.MD5, 'MD5']
  602 +]);
  603 +
  604 +export enum SnmpPrivacyProtocol {
  605 + DES = 'DES',
  606 + AES_128 = 'AES_128',
  607 + AES_192 = 'AES_192',
  608 + AES_256 = 'AES_256'
  609 +}
  610 +
  611 +export const SnmpPrivacyProtocolTranslationMap = new Map<SnmpPrivacyProtocol, string>([
  612 + [SnmpPrivacyProtocol.DES, 'DES'],
  613 + [SnmpPrivacyProtocol.AES_128, 'AES-128'],
  614 + [SnmpPrivacyProtocol.AES_192, 'AES-192'],
  615 + [SnmpPrivacyProtocol.AES_256, 'AES-256'],
  616 +]);
  617 +
542 export interface SnmpDeviceTransportConfiguration { 618 export interface SnmpDeviceTransportConfiguration {
543 - [key: string]: any; 619 + host?: string;
  620 + port?: number;
  621 + protocolVersion?: SnmpDeviceProtocolVersion;
  622 + community?: string;
  623 + username?: string;
  624 + securityName?: string;
  625 + contextName?: string;
  626 + authenticationProtocol?: SnmpAuthenticationProtocol;
  627 + authenticationPassphrase?: string;
  628 + privacyProtocol?: SnmpPrivacyProtocol;
  629 + privacyPassphrase?: string;
  630 + engineId?: string;
544 } 631 }
545 632
546 export type DeviceTransportConfigurations = DefaultDeviceTransportConfiguration & 633 export type DeviceTransportConfigurations = DefaultDeviceTransportConfiguration &
@@ -61,6 +61,6 @@ export interface Resource extends ResourceInfo { @@ -61,6 +61,6 @@ export interface Resource extends ResourceInfo {
61 } 61 }
62 62
63 export interface Resources extends ResourceInfo { 63 export interface Resources extends ResourceInfo {
64 - data: string|string[];  
65 - fileName: string|string[]; 64 + data: Array<string>;
  65 + fileName: Array<string>;
66 } 66 }
@@ -49,8 +49,10 @@ export interface DefaultTenantProfileConfiguration { @@ -49,8 +49,10 @@ export interface DefaultTenantProfileConfiguration {
49 maxRuleNodeExecutionsPerMessage: number; 49 maxRuleNodeExecutionsPerMessage: number;
50 maxEmails: number; 50 maxEmails: number;
51 maxSms: number; 51 maxSms: number;
  52 + maxCreatedAlarms: number;
52 53
53 defaultStorageTtlDays: number; 54 defaultStorageTtlDays: number;
  55 + alarmsTtlDays: number;
54 } 56 }
55 57
56 export type TenantProfileConfigurations = DefaultTenantProfileConfiguration; 58 export type TenantProfileConfigurations = DefaultTenantProfileConfiguration;
@@ -81,7 +83,9 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan @@ -81,7 +83,9 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
81 maxRuleNodeExecutionsPerMessage: 0, 83 maxRuleNodeExecutionsPerMessage: 0,
82 maxEmails: 0, 84 maxEmails: 0,
83 maxSms: 0, 85 maxSms: 0,
84 - defaultStorageTtlDays: 0 86 + maxCreatedAlarms: 0,
  87 + defaultStorageTtlDays: 0,
  88 + alarmsTtlDays: 0
85 }; 89 };
86 configuration = {...defaultConfiguration, type: TenantProfileType.DEFAULT}; 90 configuration = {...defaultConfiguration, type: TenantProfileType.DEFAULT};
87 break; 91 break;
@@ -1297,6 +1297,54 @@ @@ -1297,6 +1297,54 @@
1297 "sw-update-recourse": "Software update CoAP recourse", 1297 "sw-update-recourse": "Software update CoAP recourse",
1298 "sw-update-recourse-required": "Software update CoAP recourse is required.", 1298 "sw-update-recourse-required": "Software update CoAP recourse is required.",
1299 "config-json-tab": "Json Config Profile Device" 1299 "config-json-tab": "Json Config Profile Device"
  1300 + },
  1301 + "snmp": {
  1302 + "add-communication-config": "Add communication config",
  1303 + "add-mapping": "Add mapping",
  1304 + "authentication-passphrase": "Authentication passphrase",
  1305 + "authentication-passphrase-required": "Authentication passphrase is required.",
  1306 + "authentication-protocol": "Authentication protocol",
  1307 + "authentication-protocol-required": "Authentication protocol is required.",
  1308 + "communication-configs": "Communication configs",
  1309 + "community": "Community string",
  1310 + "community-required": "Community string is required.",
  1311 + "context-name": "Context name",
  1312 + "data-key": "Data key",
  1313 + "data-key-required": "Data key is required.",
  1314 + "data-type": "Data type",
  1315 + "data-type-required": "Data type is required.",
  1316 + "engine-id": "Engine ID",
  1317 + "host": "Host",
  1318 + "host-required": "Host is required.",
  1319 + "oid": "OID",
  1320 + "oid-pattern": "Invalid OID format.",
  1321 + "oid-required": "OID is required.",
  1322 + "please-add-communication-config": "Please add communication config",
  1323 + "please-add-mapping-config": "Please add mapping config",
  1324 + "port": "Port",
  1325 + "port-format": "Invalid port format.",
  1326 + "port-required": "Port is required.",
  1327 + "privacy-passphrase": "Privacy passphrase",
  1328 + "privacy-passphrase-required": "Privacy passphrase is required.",
  1329 + "privacy-protocol": "Privacy protocol",
  1330 + "privacy-protocol-required": "Privacy protocol is required.",
  1331 + "protocol-version": "Protocol version",
  1332 + "protocol-version-required": "Protocol version is required.",
  1333 + "querying-frequency": "Querying frequency, ms",
  1334 + "querying-frequency-invalid-format": "Querying frequency must be a positive integer.",
  1335 + "querying-frequency-required": "Querying frequency is required.",
  1336 + "retries": "Retries",
  1337 + "retries-invalid-format": "Retries must be a positive integer.",
  1338 + "retries-required": "Retries is required.",
  1339 + "scope": "Scope",
  1340 + "scope-required": "Scope is required.",
  1341 + "security-name": "Security name",
  1342 + "security-name-required": "Security name is required.",
  1343 + "timeout-ms": "Timeout, ms",
  1344 + "timeout-ms-invalid-format": "Timeout must be a positive integer.",
  1345 + "timeout-ms-required": "Timeout is required.",
  1346 + "user-name": "User name",
  1347 + "user-name-required": "User name is required."
1300 } 1348 }
1301 }, 1349 },
1302 "dialog": { 1350 "dialog": {
@@ -2309,20 +2357,23 @@ @@ -2309,20 +2357,23 @@
2309 }, 2357 },
2310 "resource": { 2358 "resource": {
2311 "add": "Add Resource", 2359 "add": "Add Resource",
  2360 + "copyId": "Copy resource Id",
2312 "delete": "Delete resource", 2361 "delete": "Delete resource",
2313 "delete-resource-text": "Be careful, after the confirmation the resource will become unrecoverable.", 2362 "delete-resource-text": "Be careful, after the confirmation the resource will become unrecoverable.",
2314 "delete-resource-title": "Are you sure you want to delete the resource '{{resourceTitle}}'?", 2363 "delete-resource-title": "Are you sure you want to delete the resource '{{resourceTitle}}'?",
2315 "delete-resources-action-title": "Delete { count, plural, 1 {1 resource} other {# resources} }", 2364 "delete-resources-action-title": "Delete { count, plural, 1 {1 resource} other {# resources} }",
2316 "delete-resources-text": "Be careful, after the confirmation all selected resources will be removed.", 2365 "delete-resources-text": "Be careful, after the confirmation all selected resources will be removed.",
2317 "delete-resources-title": "Are you sure you want to delete { count, plural, 1 {1 resource} other {# resources} }?", 2366 "delete-resources-title": "Are you sure you want to delete { count, plural, 1 {1 resource} other {# resources} }?",
  2367 + "download": "Download resource",
2318 "drop-file": "Drop a resource file or click to select a file to upload.", 2368 "drop-file": "Drop a resource file or click to select a file to upload.",
2319 "empty": "Resource is empty", 2369 "empty": "Resource is empty",
2320 - "export": "Export resource", 2370 + "file-name": "File name",
  2371 + "idCopiedMessage": "Resource Id has been copied to clipboard",
2321 "no-resource-matching": "No resource matching '{{widgetsBundle}}' were found.", 2372 "no-resource-matching": "No resource matching '{{widgetsBundle}}' were found.",
2322 "no-resource-text": "No resources found", 2373 "no-resource-text": "No resources found",
2323 "open-widgets-bundle": "Open widgets bundle", 2374 "open-widgets-bundle": "Open widgets bundle",
2324 "resource": "Resource", 2375 "resource": "Resource",
2325 - "resource-library-details": "Resource library details", 2376 + "resource-library-details": "Resource details",
2326 "resource-type": "Resource type", 2377 "resource-type": "Resource type",
2327 "resources-library": "Resources library", 2378 "resources-library": "Resources library",
2328 "search": "Search resources", 2379 "search": "Search resources",
@@ -1994,7 +1994,6 @@ @@ -1994,7 +1994,6 @@
1994 "delete-resources-title": "确定要删除 { count, plural, 1 {# 个资源} other {# 个资源} }?", 1994 "delete-resources-title": "确定要删除 { count, plural, 1 {# 个资源} other {# 个资源} }?",
1995 "drop-file": "拖拽资源文件或单击以选择要上传的文件。", 1995 "drop-file": "拖拽资源文件或单击以选择要上传的文件。",
1996 "empty": "资源为空", 1996 "empty": "资源为空",
1997 - "export": "导出资源",  
1998 "no-resource-matching": "找不到与 '{{widgetsBundle}}' 匹配的资源。", 1997 "no-resource-matching": "找不到与 '{{widgetsBundle}}' 匹配的资源。",
1999 "no-resource-text": "找不到资源", 1998 "no-resource-text": "找不到资源",
2000 "open-widgets-bundle": "打开部件库", 1999 "open-widgets-bundle": "打开部件库",