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 782 }
783 783
784 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 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 789 checkParameter("OtaPackageType", otaPackageType);
790 790 checkParameter("DeviceProfileId", deviceProfileId);
791 791 try {
792 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 796 } catch (Exception e) {
795 797 throw handleException(e);
796 798 }
... ...
... ... @@ -74,6 +74,7 @@ import java.util.List;
74 74 import java.util.Map;
75 75 import java.util.Set;
76 76 import java.util.concurrent.ConcurrentMap;
  77 +import java.util.concurrent.TimeUnit;
77 78 import java.util.stream.Collectors;
78 79
79 80 @Slf4j
... ... @@ -86,6 +87,7 @@ public class RuleChainController extends BaseController {
86 87 public static final String RULE_NODE_ID = "ruleNodeId";
87 88
88 89 private static final ObjectMapper objectMapper = new ObjectMapper();
  90 + public static final int TIMEOUT = 20;
89 91
90 92 @Autowired
91 93 private InstallScripts installScripts;
... ... @@ -388,25 +390,25 @@ public class RuleChainController extends BaseController {
388 390 TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data);
389 391 switch (scriptType) {
390 392 case "update":
391   - output = msgToOutput(engine.executeUpdate(inMsg));
  393 + output = msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
392 394 break;
393 395 case "generate":
394   - output = msgToOutput(engine.executeGenerate(inMsg));
  396 + output = msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
395 397 break;
396 398 case "filter":
397   - boolean result = engine.executeFilter(inMsg);
  399 + boolean result = engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
398 400 output = Boolean.toString(result);
399 401 break;
400 402 case "switch":
401   - Set<String> states = engine.executeSwitch(inMsg);
  403 + Set<String> states = engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
402 404 output = objectMapper.writeValueAsString(states);
403 405 break;
404 406 case "json":
405   - JsonNode json = engine.executeJson(inMsg);
  407 + JsonNode json = engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
406 408 output = objectMapper.writeValueAsString(json);
407 409 break;
408 410 case "string":
409   - output = engine.executeToString(inMsg);
  411 + output = engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
410 412 break;
411 413 default:
412 414 throw new IllegalArgumentException("Unsupported script type: " + scriptType);
... ...
... ... @@ -30,6 +30,7 @@ import java.util.UUID;
30 30 import java.util.concurrent.ConcurrentHashMap;
31 31 import java.util.concurrent.Executors;
32 32 import java.util.concurrent.ScheduledExecutorService;
  33 +import java.util.concurrent.TimeoutException;
33 34 import java.util.concurrent.atomic.AtomicInteger;
34 35
35 36 /**
... ... @@ -84,8 +85,10 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
84 85 apiUsageClient.report(tenantId, customerId, ApiUsageRecordKey.JS_EXEC_COUNT, 1);
85 86 return doInvokeFunction(scriptId, functionName, args);
86 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 93 } else {
91 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 120
118 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 130 private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) {
... ...
... ... @@ -160,7 +160,7 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer
160 160 return ((Invocable) engine).invokeFunction(functionName, args);
161 161 }
162 162 } catch (Exception e) {
163   - onScriptExecutionError(scriptId);
  163 + onScriptExecutionError(scriptId, e, functionName);
164 164 throw new ExecutionException(e);
165 165 }
166 166 });
... ...
... ... @@ -18,7 +18,6 @@ package org.thingsboard.server.service.script;
18 18 import com.google.common.util.concurrent.FutureCallback;
19 19 import com.google.common.util.concurrent.Futures;
20 20 import com.google.common.util.concurrent.ListenableFuture;
21   -import com.google.common.util.concurrent.MoreExecutors;
22 21 import lombok.Getter;
23 22 import lombok.extern.slf4j.Slf4j;
24 23 import org.springframework.beans.factory.annotation.Autowired;
... ... @@ -26,6 +25,7 @@ import org.springframework.beans.factory.annotation.Value;
26 25 import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
27 26 import org.springframework.scheduling.annotation.Scheduled;
28 27 import org.springframework.stereotype.Service;
  28 +import org.springframework.util.StopWatch;
29 29 import org.thingsboard.common.util.ThingsBoardThreadFactory;
30 30 import org.thingsboard.server.gen.js.JsInvokeProtos;
31 31 import org.thingsboard.server.queue.TbQueueRequestTemplate;
... ... @@ -161,7 +161,8 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
161 161
162 162 @Override
163 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 166 if (scriptBody == null) {
166 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 171 .setScriptIdLSB(scriptId.getLeastSignificantBits())
171 172 .setFunctionName(functionName)
172 173 .setTimeout((int) maxRequestsTimeout)
173   - .setScriptBody(scriptIdToBodysMap.get(scriptId));
  174 + .setScriptBody(scriptBody);
174 175
175 176 for (Object arg : args) {
176 177 jsRequestBuilder.addArgs(arg.toString());
... ... @@ -180,6 +181,9 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
180 181 .setInvokeRequest(jsRequestBuilder.build())
181 182 .build();
182 183
  184 + StopWatch stopWatch = new StopWatch();
  185 + stopWatch.start();
  186 +
183 187 ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(UUID.randomUUID(), jsRequestWrapper));
184 188 if (maxRequestsTimeout > 0) {
185 189 future = Futures.withTimeout(future, maxRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService);
... ... @@ -193,7 +197,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
193 197
194 198 @Override
195 199 public void onFailure(Throwable t) {
196   - onScriptExecutionError(scriptId);
  200 + onScriptExecutionError(scriptId, t, scriptBody);
197 201 if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) {
198 202 queueTimeoutMsgs.incrementAndGet();
199 203 }
... ... @@ -201,13 +205,16 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
201 205 }
202 206 }, callbackExecutor);
203 207 return Futures.transform(future, response -> {
  208 + stopWatch.stop();
  209 + log.trace("doInvokeFunction js-response took {}ms for uuid {}", stopWatch.getTotalTimeMillis(), response.getKey());
204 210 JsInvokeProtos.JsInvokeResponse invokeResult = response.getValue().getInvokeResponse();
205 211 if (invokeResult.getSuccess()) {
206 212 return invokeResult.getResult();
207 213 } else {
208   - onScriptExecutionError(scriptId);
  214 + final RuntimeException e = new RuntimeException(invokeResult.getErrorDetails());
  215 + onScriptExecutionError(scriptId, e, scriptBody);
209 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 219 }, callbackExecutor);
213 220 }
... ...
... ... @@ -18,12 +18,12 @@ package org.thingsboard.server.service.script;
18 18 import com.fasterxml.jackson.core.type.TypeReference;
19 19 import com.fasterxml.jackson.databind.JsonNode;
20 20 import com.fasterxml.jackson.databind.ObjectMapper;
21   -import com.google.common.collect.Sets;
22 21 import com.google.common.util.concurrent.Futures;
23 22 import com.google.common.util.concurrent.ListenableFuture;
24 23 import com.google.common.util.concurrent.MoreExecutors;
25 24 import lombok.extern.slf4j.Slf4j;
26 25 import org.apache.commons.lang3.StringUtils;
  26 +import org.thingsboard.server.common.data.id.CustomerId;
27 27 import org.thingsboard.server.common.data.id.EntityId;
28 28 import org.thingsboard.server.common.data.id.TenantId;
29 29 import org.thingsboard.server.common.msg.TbMsg;
... ... @@ -32,6 +32,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
32 32 import javax.script.ScriptException;
33 33 import java.util.ArrayList;
34 34 import java.util.Collections;
  35 +import java.util.HashSet;
35 36 import java.util.List;
36 37 import java.util.Map;
37 38 import java.util.Set;
... ... @@ -102,140 +103,115 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
102 103 String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType();
103 104 return TbMsg.transformMsg(msg, newMessageType, msg.getOriginator(), newMetadata, newData);
104 105 } catch (Throwable th) {
105   - th.printStackTrace();
106 106 throw new RuntimeException("Failed to unbind message data from javascript result", th);
107 107 }
108 108 }
109 109
110 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 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 138 if (!result.isObject()) {
147 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 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 147 return executeScriptAsync(msg);
161 148 }
162 149
163 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 165 @Override
184 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 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 186 for (JsonNode val : result) {
204 187 if (!val.isTextual()) {
205 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 190 } else {
208 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 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 215 o -> {
240 216 try {
241 217 return Futures.immediateFuture(mapper.readTree(o.toString()));
... ...
... ... @@ -332,6 +332,7 @@ actors:
332 332 cache:
333 333 # caffeine or redis
334 334 type: "${CACHE_TYPE:caffeine}"
  335 + maximumPoolSize: "${CACHE_MAXIMUM_POOL_SIZE:16}" # max pool size to process futures that calls the external cache
335 336 attributes:
336 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 338 enabled: "${CACHE_ATTRIBUTES_ENABLED:true}"
... ...
... ... @@ -51,6 +51,13 @@ public class SnmpDeviceTransportConfiguration implements DeviceTransportConfigur
51 51 private String privacyPassphrase;
52 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 61 @Override
55 62 public DeviceTransportType getType() {
56 63 return DeviceTransportType.SNMP;
... ... @@ -76,7 +83,7 @@ public class SnmpDeviceTransportConfiguration implements DeviceTransportConfigur
76 83 isValid = StringUtils.isNotBlank(username) && StringUtils.isNotBlank(securityName)
77 84 && contextName != null && authenticationProtocol != null
78 85 && StringUtils.isNotBlank(authenticationPassphrase)
79   - && privacyProtocol != null && privacyPassphrase != null && engineId != null;
  86 + && privacyProtocol != null && StringUtils.isNotBlank(privacyPassphrase) && engineId != null;
80 87 break;
81 88 }
82 89 }
... ...
... ... @@ -24,6 +24,8 @@ public interface TbQueueRequestTemplate<Request extends TbQueueMsg, Response ext
24 24
25 25 ListenableFuture<Response> send(Request request);
26 26
  27 + ListenableFuture<Response> send(Request request, long timeoutNs);
  28 +
27 29 void stop();
28 30
29 31 void setMessagesStats(MessagesStats messagesStats);
... ...
... ... @@ -19,7 +19,9 @@ import com.google.common.util.concurrent.Futures;
19 19 import com.google.common.util.concurrent.ListenableFuture;
20 20 import com.google.common.util.concurrent.SettableFuture;
21 21 import lombok.Builder;
  22 +import lombok.Getter;
22 23 import lombok.extern.slf4j.Slf4j;
  24 +import org.thingsboard.common.util.TbStopWatch;
23 25 import org.thingsboard.common.util.ThingsBoardThreadFactory;
24 26 import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
25 27 import org.thingsboard.server.queue.TbQueueAdmin;
... ... @@ -31,13 +33,17 @@ import org.thingsboard.server.queue.TbQueueProducer;
31 33 import org.thingsboard.server.queue.TbQueueRequestTemplate;
32 34 import org.thingsboard.server.common.stats.MessagesStats;
33 35
  36 +import javax.annotation.Nullable;
34 37 import java.util.List;
35 38 import java.util.UUID;
36 39 import java.util.concurrent.ConcurrentHashMap;
37   -import java.util.concurrent.ConcurrentMap;
38 40 import java.util.concurrent.ExecutorService;
39 41 import java.util.concurrent.Executors;
  42 +import java.util.concurrent.TimeUnit;
40 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 48 @Slf4j
43 49 public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response extends TbQueueMsg> extends AbstractTbQueueTemplate
... ... @@ -46,15 +52,15 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
46 52 private final TbQueueAdmin queueAdmin;
47 53 private final TbQueueProducer<Request> requestTemplate;
48 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 65 private MessagesStats messagesStats;
60 66
... ... @@ -65,79 +71,113 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
65 71 long maxRequestTimeout,
66 72 long maxPendingRequests,
67 73 long pollInterval,
68   - ExecutorService executor) {
  74 + @Nullable ExecutorService executor) {
69 75 this.queueAdmin = queueAdmin;
70 76 this.requestTemplate = requestTemplate;
71 77 this.responseTemplate = responseTemplate;
72   - this.pendingRequests = new ConcurrentHashMap<>();
73   - this.maxRequestTimeout = maxRequestTimeout;
  78 + this.maxRequestTimeoutNs = TimeUnit.MILLISECONDS.toNanos(maxRequestTimeout);
74 79 this.maxPendingRequests = maxPendingRequests;
75 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 89 @Override
86 90 public void init() {
87 91 queueAdmin.createTopicIfNotExists(responseTemplate.getTopic());
88   - this.requestTemplate.init();
89   - tickTs = System.currentTimeMillis();
  92 + requestTemplate.init();
90 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 183 @Override
... ... @@ -164,17 +204,48 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
164 204
165 205 @Override
166 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 214 return Futures.immediateFailedFuture(new RuntimeException("Pending request map is full!"));
169 215 }
170 216 UUID requestId = UUID.randomUUID();
171 217 request.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId));
172 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 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 249 if (messagesStats != null) {
179 250 messagesStats.incrementTotal();
180 251 }
... ... @@ -184,7 +255,7 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
184 255 if (messagesStats != null) {
185 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 261 @Override
... ... @@ -196,17 +267,32 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
196 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 276 private final long expTime;
204 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 282 this.expTime = ts;
208 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 21 import org.apache.kafka.clients.consumer.ConsumerRecord;
22 22 import org.apache.kafka.clients.consumer.ConsumerRecords;
23 23 import org.apache.kafka.clients.consumer.KafkaConsumer;
  24 +import org.springframework.util.StopWatch;
24 25 import org.thingsboard.server.queue.TbQueueAdmin;
25 26 import org.thingsboard.server.queue.TbQueueMsg;
26 27 import org.thingsboard.server.queue.common.AbstractTbQueueConsumerTemplate;
... ... @@ -82,7 +83,16 @@ public class TbKafkaConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQue
82 83
83 84 @Override
84 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 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 96 if (records.isEmpty()) {
87 97 return Collections.emptyList();
88 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 +}
\ No newline at end of file
... ...
... ... @@ -37,6 +37,10 @@
37 37
38 38 <dependencies>
39 39 <dependency>
  40 + <groupId>org.springframework</groupId>
  41 + <artifactId>spring-core</artifactId>
  42 + </dependency>
  43 + <dependency>
40 44 <groupId>com.google.guava</groupId>
41 45 <artifactId>guava</artifactId>
42 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 21 import lombok.extern.slf4j.Slf4j;
22 22 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
23 23 import org.springframework.cache.Cache;
24   -import org.springframework.cache.CacheManager;
25 24 import org.springframework.context.annotation.Primary;
26 25 import org.springframework.stereotype.Service;
27 26 import org.thingsboard.server.common.data.EntityType;
... ... @@ -32,6 +31,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
32 31 import org.thingsboard.server.common.data.kv.KvEntry;
33 32 import org.thingsboard.server.common.stats.DefaultCounter;
34 33 import org.thingsboard.server.common.stats.StatsFactory;
  34 +import org.thingsboard.server.dao.cache.CacheExecutorService;
35 35 import org.thingsboard.server.dao.service.Validator;
36 36
37 37 import java.util.ArrayList;
... ... @@ -45,7 +45,6 @@ import java.util.Optional;
45 45 import java.util.Set;
46 46 import java.util.stream.Collectors;
47 47
48   -import static org.thingsboard.server.common.data.CacheConstants.ATTRIBUTES_CACHE;
49 48 import static org.thingsboard.server.dao.attributes.AttributeUtils.validate;
50 49
51 50 @Service
... ... @@ -59,12 +58,15 @@ public class CachedAttributesService implements AttributesService {
59 58 private final AttributesCacheWrapper cacheWrapper;
60 59 private final DefaultCounter hitCounter;
61 60 private final DefaultCounter missCounter;
  61 + private final CacheExecutorService cacheExecutorService;
62 62
63 63 public CachedAttributesService(AttributesDao attributesDao,
64 64 AttributesCacheWrapper cacheWrapper,
65   - StatsFactory statsFactory) {
  65 + StatsFactory statsFactory,
  66 + CacheExecutorService cacheExecutorService) {
66 67 this.attributesDao = attributesDao;
67 68 this.cacheWrapper = cacheWrapper;
  69 + this.cacheExecutorService = cacheExecutorService;
68 70
69 71 this.hitCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "hit");
70 72 this.missCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "miss");
... ... @@ -88,7 +90,7 @@ public class CachedAttributesService implements AttributesService {
88 90 // TODO: think if it's a good idea to store 'empty' attributes
89 91 cacheWrapper.put(attributeCacheKey, foundAttrKvEntry.orElse(null));
90 92 return foundAttrKvEntry;
91   - }, MoreExecutors.directExecutor());
  93 + }, cacheExecutorService);
92 94 }
93 95 }
94 96
... ... @@ -111,7 +113,7 @@ public class CachedAttributesService implements AttributesService {
111 113 notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet());
112 114
113 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 171
170 172 // TODO: can do if (attributesCache.get() != null) attributesCache.put() instead, but will be more twice more requests to cache
171 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 175 return future;
174 176 }
175 177
... ... @@ -177,7 +179,7 @@ public class CachedAttributesService implements AttributesService {
177 179 public ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys) {
178 180 validate(entityId, scope);
179 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 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 10 audit-log.sink.type=none
11 11
12 12 cache.type=caffeine
  13 +cache.maximumPoolSize=16
13 14 #cache.type=redis
14 15
15 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 !`))
\ No newline at end of file
... ...
... ... @@ -25,6 +25,7 @@ const config = require('config'),
25 25 Utils = require('./utils'),
26 26 JsExecutor = require('./jsExecutor');
27 27
  28 +const statFrequency = Number(config.get('script.stat_print_frequency'));
28 29 const scriptBodyTraceFrequency = Number(config.get('script.script_body_trace_frequency'));
29 30 const useSandbox = config.get('script.use_sandbox') === 'true';
30 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 35 const {performance} = require('perf_hooks');
35 36
36 37 function JsInvokeMessageProcessor(producer) {
37   - console.log("Producer:", producer);
38 38 this.producer = producer;
39 39 this.executor = new JsExecutor(useSandbox);
40   - this.scriptMap = {};
  40 + this.scriptMap = new Map();
41 41 this.scriptIds = [];
42 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 47 var tStart = performance.now();
47 48 let requestId;
48 49 let responseTopic;
... ... @@ -77,13 +78,13 @@ JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) {
77 78 var tFinish = performance.now();
78 79 var tTook = tFinish - tStart;
79 80
80   - if ( tTook > slowQueryLogMs ) {
  81 + if (tTook > slowQueryLogMs) {
81 82 let functionName;
82 83 if (request.invokeRequest) {
83 84 try {
84 85 buf = Buffer.from(request.invokeRequest['functionName']);
85 86 functionName = buf.toString('utf8');
86   - } catch (err){
  87 + } catch (err) {
87 88 logger.error('[%s] Failed to read functionName from message header: %s', requestId, err.message);
88 89 logger.error(err.stack);
89 90 }
... ... @@ -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 101 var scriptId = getScriptId(compileRequest);
101 102 logger.debug('[%s] Processing compile request, scriptId: [%s]', requestId, scriptId);
102 103
... ... @@ -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 120 var scriptId = getScriptId(invokeRequest);
120 121 logger.debug('[%s] Processing invoke request, scriptId: [%s]', requestId, scriptId);
121 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 134 this.getOrCompileScript(scriptId, invokeRequest.scriptBody).then(
129 135 (script) => {
... ... @@ -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 164 var scriptId = getScriptId(releaseRequest);
159 165 logger.debug('[%s] Processing release request, scriptId: [%s]', requestId, scriptId);
160   - if (this.scriptMap[scriptId]) {
  166 + if (this.scriptMap.has(scriptId)) {
161 167 var index = this.scriptIds.indexOf(scriptId);
162 168 if (index > -1) {
163 169 this.scriptIds.splice(index, 1);
164 170 }
165   - delete this.scriptMap[scriptId];
  171 + this.scriptMap.delete(scriptId);
166 172 }
167 173 var releaseResponse = createReleaseResponse(scriptId, true);
168 174 logger.debug('[%s] Sending success release response, scriptId: [%s]', requestId, scriptId);
... ... @@ -173,6 +179,7 @@ JsInvokeMessageProcessor.prototype.sendResponse = function (requestId, responseT
173 179 var tStartSending = performance.now();
174 180 var remoteResponse = createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse);
175 181 var rawResponse = Buffer.from(JSON.stringify(remoteResponse), 'utf8');
  182 + logger.debug('[%s] Sending response to queue, scriptId: [%s]', requestId, scriptId);
176 183 this.producer.send(responseTopic, scriptId, rawResponse, headers).then(
177 184 () => {
178 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 193 );
187 194 }
188 195
189   -JsInvokeMessageProcessor.prototype.getOrCompileScript = function(scriptId, scriptBody) {
  196 +JsInvokeMessageProcessor.prototype.getOrCompileScript = function (scriptId, scriptBody) {
190 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 202 } else {
195 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 208 (err) => {
201 209 reject(err);
... ... @@ -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 218 this.scriptIds.push(scriptId);
211 219 while (this.scriptIds.length > maxActiveScripts) {
212 220 logger.info('Active scripts count [%s] exceeds maximum limit [%s]', this.scriptIds.length, maxActiveScripts);
213 221 const prevScriptId = this.scriptIds.shift();
214 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 230 function createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse) {
222 231 const requestIdBits = Utils.UUIDToBits(requestId);
223 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 241 function createCompileResponse(scriptId, success, errorCode, err) {
233 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 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 261 function createReleaseResponse(scriptId, success) {
253 262 const scriptIdBits = Utils.UUIDToBits(scriptId);
254 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 16
17 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 18 request_topic: "REMOTE_JS_EVAL_REQUEST_TOPIC"
  19 +http_port: "HTTP_PORT" # /livenessProbe
19 20
20 21 js:
21 22 response_poll_interval: "REMOTE_JS_RESPONSE_POLL_INTERVAL_MS"
... ... @@ -26,6 +27,9 @@ kafka:
26 27 servers: "TB_KAFKA_SERVERS"
27 28 replication_factor: "TB_QUEUE_KAFKA_REPLICATION_FACTOR"
28 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 33 requestTimeout: "TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS"
30 34 compression: "TB_QUEUE_KAFKA_COMPRESSION" # gzip or uncompressed
31 35 topic_properties: "TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES"
... ... @@ -70,6 +74,7 @@ logger:
70 74
71 75 script:
72 76 use_sandbox: "SCRIPT_USE_SANDBOX"
  77 + stat_print_frequency: "SCRIPT_STAT_PRINT_FREQUENCY"
73 78 script_body_trace_frequency: "SCRIPT_BODY_TRACE_FREQUENCY"
74 79 max_active_scripts: "MAX_ACTIVE_SCRIPTS"
75 80 slow_query_log_ms: "SLOW_QUERY_LOG_MS" #1.123456
... ...
... ... @@ -16,6 +16,7 @@
16 16
17 17 queue_type: "kafka"
18 18 request_topic: "js_eval.requests"
  19 +http_port: "8888" # /livenessProbe
19 20
20 21 js:
21 22 response_poll_interval: "25"
... ... @@ -26,6 +27,9 @@ kafka:
26 27 servers: "localhost:9092"
27 28 replication_factor: "1"
28 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 33 requestTimeout: "30000" # The default value in kafkajs is: 30000
30 34 compression: "gzip" # gzip or uncompressed
31 35 topic_properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100;min.insync.replicas:1"
... ... @@ -59,7 +63,8 @@ logger:
59 63
60 64 script:
61 65 use_sandbox: "true"
62   - script_body_trace_frequency: "1000"
  66 + script_body_trace_frequency: "10000"
  67 + stat_print_frequency: "10000"
63 68 max_active_scripts: "1000"
64   - slow_query_log_ms: "1.000000" #millis
  69 + slow_query_log_ms: "5.000000" #millis
65 70 slow_query_log_body: "false"
... ...
... ... @@ -18,6 +18,7 @@
18 18 "aws-sdk": "^2.741.0",
19 19 "azure-sb": "^0.11.1",
20 20 "config": "^3.3.1",
  21 + "express": "^4.17.1",
21 22 "js-yaml": "^3.14.0",
22 23 "kafkajs": "^1.15.0",
23 24 "long": "^4.0.0",
... ...
... ... @@ -23,8 +23,11 @@ const replicationFactor = Number(config.get('kafka.replication_factor'));
23 23 const topicProperties = config.get('kafka.topic_properties');
24 24 const kafkaClientId = config.get('kafka.client_id');
25 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 28 const requestTimeout = Number(config.get('kafka.requestTimeout'));
27 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 32 let kafkaClient;
30 33 let kafkaAdmin;
... ... @@ -33,22 +36,65 @@ let producer;
33 36
34 37 const configEntries = [];
35 38
  39 +let batchMessages = [];
  40 +let sendLoopInstance;
  41 +
36 42 function KafkaProducer() {
37 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 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 100 (async () => {
... ... @@ -64,8 +110,8 @@ function KafkaProducer() {
64 110
65 111 let kafkaConfig = {
66 112 brokers: kafkaBootstrapServers.split(','),
67   - logLevel: logLevel.INFO,
68   - logCreator: KafkaJsWinstonLogCreator
  113 + logLevel: logLevel.INFO,
  114 + logCreator: KafkaJsWinstonLogCreator
69 115 };
70 116
71 117 if (kafkaClientId) {
... ... @@ -114,14 +160,45 @@ function KafkaProducer() {
114 160
115 161 consumer = kafkaClient.consumer({groupId: 'js-executor-group'});
116 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 193 const messageProcessor = new JsInvokeMessageProcessor(new KafkaProducer());
118 194 await consumer.connect();
119 195 await producer.connect();
  196 + sendLoopWithLinger();
120 197 await consumer.subscribe({topic: requestTopic});
121 198
122 199 logger.info('Started ThingsBoard JavaScript Executor Microservice.');
123 200 await consumer.run({
124   - //partitionsConsumedConcurrently: 1, // Default: 1
  201 + partitionsConsumedConcurrently: partitionsConsumedConcurrently,
125 202 eachMessage: async ({topic, partition, message}) => {
126 203 let headers = message.headers;
127 204 let key = message.key;
... ... @@ -197,6 +274,9 @@ async function disconnectProducer() {
197 274 var _producer = producer;
198 275 producer = null;
199 276 try {
  277 + logger.info('Stopping loop...');
  278 + clearTimeout(sendLoopInstance);
  279 + await sendMessagesAsBatch();
200 280 await _producer.disconnect();
201 281 logger.info('Kafka Producer stopped.');
202 282 } catch (e) {
... ...
... ... @@ -51,3 +51,5 @@ switch (serviceType) {
51 51 process.exit(-1);
52 52 }
53 53
  54 +require('./api/httpServer');
  55 +
... ...
... ... @@ -418,6 +418,14 @@ abort-controller@^3.0.0:
418 418 dependencies:
419 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 429 agent-base@6:
422 430 version "6.0.1"
423 431 resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4"
... ... @@ -487,6 +495,11 @@ argparse@^1.0.7:
487 495 dependencies:
488 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 503 array-union@^2.1.0:
491 504 version "2.1.0"
492 505 resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
... ... @@ -621,6 +634,22 @@ bluebird@^3.5.2:
621 634 resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
622 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 653 boxen@^4.2.0:
625 654 version "4.2.0"
626 655 resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64"
... ... @@ -682,6 +711,11 @@ byline@^5.0.0:
682 711 resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1"
683 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 719 cacheable-request@^6.0.0:
686 720 version "6.1.0"
687 721 resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912"
... ... @@ -838,6 +872,28 @@ configstore@^5.0.1:
838 872 write-file-atomic "^3.0.0"
839 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 897 core-util-is@1.0.2, core-util-is@~1.0.0:
842 898 version "1.0.2"
843 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 923 dependencies:
868 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 933 debug@4, debug@^4.1.1:
871 934 version "4.1.1"
872 935 resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
... ... @@ -874,13 +937,6 @@ debug@4, debug@^4.1.1:
874 937 dependencies:
875 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 940 decamelize@^1.2.0:
885 941 version "1.2.0"
886 942 resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
... ... @@ -913,6 +969,16 @@ delayed-stream@~1.0.0:
913 969 resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
914 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 982 dir-glob@^3.0.1:
917 983 version "3.0.1"
918 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 1028 dependencies:
963 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 1036 emoji-regex@^7.0.1:
966 1037 version "7.0.3"
967 1038 resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
... ... @@ -977,6 +1048,11 @@ enabled@2.0.x:
977 1048 resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
978 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 1056 end-of-stream@^1.0.0, end-of-stream@^1.1.0:
981 1057 version "1.4.4"
982 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 1070 resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
995 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 1078 escodegen@^1.14.1:
998 1079 version "1.14.3"
999 1080 resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
... ... @@ -1021,6 +1102,11 @@ esutils@^2.0.2:
1021 1102 resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
1022 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 1110 event-target-shim@^5.0.0:
1025 1111 version "5.0.1"
1026 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 1127 resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
1042 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 1166 extend@^3.0.2, extend@~3.0.2:
1045 1167 version "3.0.2"
1046 1168 resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
... ... @@ -1119,6 +1241,19 @@ fill-range@^7.0.1:
1119 1241 dependencies:
1120 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 1257 find-up@^4.1.0:
1123 1258 version "4.1.0"
1124 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 1290 combined-stream "^1.0.6"
1156 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 1303 from2@^2.3.0:
1159 1304 version "2.3.0"
1160 1305 resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
... ... @@ -1384,6 +1529,28 @@ http-cache-semantics@^4.0.0:
1384 1529 resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
1385 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 1554 http-signature@~1.2.0:
1388 1555 version "1.2.0"
1389 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 1568 agent-base "6"
1402 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 1578 ieee754@1.1.13, ieee754@^1.1.4:
1405 1579 version "1.1.13"
1406 1580 resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
... ... @@ -1431,7 +1605,7 @@ inherits@2.0.3:
1431 1605 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
1432 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 1609 version "2.0.4"
1436 1610 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
1437 1611 integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
... ... @@ -1449,6 +1623,11 @@ into-stream@^5.1.1:
1449 1623 from2 "^2.3.0"
1450 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 1631 is-arrayish@^0.3.1:
1453 1632 version "0.3.2"
1454 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 1943 dependencies:
1765 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 1956 merge2@^1.3.0:
1768 1957 version "1.4.1"
1769 1958 resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
1770 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 1966 micromatch@^4.0.2:
1773 1967 version "4.0.2"
1774 1968 resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
... ... @@ -1782,6 +1976,11 @@ mime-db@1.44.0:
1782 1976 resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
1783 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 1984 mime-types@^2.1.12, mime-types@~2.1.19:
1786 1985 version "2.1.27"
1787 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 1988 dependencies:
1790 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 2003 mime@^2.2.0:
1793 2004 version "2.4.6"
1794 2005 resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
... ... @@ -1833,6 +2044,11 @@ ms@2.0.0:
1833 2044 resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
1834 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 2052 ms@^2.1.1:
1837 2053 version "2.1.2"
1838 2054 resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
... ... @@ -1846,6 +2062,11 @@ multistream@^2.1.1:
1846 2062 inherits "^2.0.1"
1847 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 2070 node-fetch@^2.3.0, node-fetch@^2.6.0:
1850 2071 version "2.6.0"
1851 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 2120 resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea"
1900 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 2130 once@^1.3.1, once@^1.4.0:
1903 2131 version "1.4.0"
1904 2132 resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
... ... @@ -1974,6 +2202,11 @@ package-json@^6.3.0:
1974 2202 registry-url "^5.0.0"
1975 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 2210 path-exists@^4.0.0:
1978 2211 version "4.0.0"
1979 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 2217 resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
1985 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 2225 path-type@^4.0.0:
1988 2226 version "4.0.0"
1989 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 2317 "@types/node" "^13.7.0"
2080 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 2328 psl@^1.1.28, psl@^1.1.33:
2083 2329 version "1.8.0"
2084 2330 resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
... ... @@ -2114,6 +2360,11 @@ pupa@^2.0.1:
2114 2360 dependencies:
2115 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 2368 qs@~6.5.2:
2118 2369 version "6.5.2"
2119 2370 resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
... ... @@ -2129,6 +2380,21 @@ querystringify@^2.1.1:
2129 2380 resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
2130 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 2398 rc@^1.2.8:
2133 2399 version "1.2.8"
2134 2400 resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
... ... @@ -2292,17 +2558,17 @@ run-parallel@^1.1.9:
2292 2558 resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
2293 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 2566 safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
2296 2567 version "5.2.1"
2297 2568 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
2298 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 2572 version "2.1.2"
2307 2573 resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
2308 2574 integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
... ... @@ -2339,11 +2605,45 @@ semver@^7.1.3:
2339 2605 resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
2340 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 2637 set-blocking@^2.0.0:
2343 2638 version "2.0.0"
2344 2639 resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
2345 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 2647 signal-exit@^3.0.2:
2348 2648 version "3.0.3"
2349 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 2691 resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
2392 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 2699 stream-browserify@^2.0.2:
2395 2700 version "2.0.2"
2396 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 2818 dependencies:
2514 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 2826 touch@^3.1.0:
2517 2827 version "3.1.0"
2518 2828 resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
... ... @@ -2581,6 +2891,14 @@ type-fest@^0.8.1:
2581 2891 resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
2582 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 2902 typedarray-to-buffer@^3.1.5:
2585 2903 version "3.1.5"
2586 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 2954 resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d"
2637 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 2962 update-notifier@^4.0.0:
2640 2963 version "4.1.1"
2641 2964 resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.1.tgz#895fc8562bbe666179500f9f2cebac4f26323746"
... ... @@ -2705,6 +3028,11 @@ util@^0.11.1:
2705 3028 dependencies:
2706 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 3036 uuid-parse@^1.1.0:
2709 3037 version "1.1.0"
2710 3038 resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b"
... ... @@ -2735,6 +3063,11 @@ validator@^9.4.1:
2735 3063 resolved "https://registry.yarnpkg.com/validator/-/validator-9.4.1.tgz#abf466d398b561cd243050112c6ff1de6cc12663"
2736 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 3071 verror@1.10.0:
2739 3072 version "1.10.0"
2740 3073 resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
... ...
... ... @@ -1010,6 +1010,11 @@
1010 1010 </exclusions>
1011 1011 </dependency>
1012 1012 <dependency>
  1013 + <groupId>org.springframework</groupId>
  1014 + <artifactId>spring-core</artifactId>
  1015 + <version>${spring.version}</version>
  1016 + </dependency>
  1017 + <dependency>
1013 1018 <groupId>org.springframework.boot</groupId>
1014 1019 <artifactId>spring-boot-starter-web</artifactId>
1015 1020 <version>${spring-boot.version}</version>
... ...
... ... @@ -104,6 +104,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
104 104 import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
105 105 import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
106 106 import org.thingsboard.server.common.data.oauth2.OAuth2Info;
  107 +import org.thingsboard.server.common.data.oauth2.PlatformType;
107 108 import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
108 109 import org.thingsboard.server.common.data.ota.OtaPackageType;
109 110 import org.thingsboard.server.common.data.page.PageData;
... ... @@ -1257,7 +1258,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
1257 1258 params.put("deviceProfileId", deviceProfileId.getId().toString());
1258 1259
1259 1260 return restTemplate.exchange(
1260   - baseURL + "/api/devices/count/{otaPackageType}?deviceProfileId={deviceProfileId}",
  1261 + baseURL + "/api/devices/count/{otaPackageType}/{deviceProfileId}",
1261 1262 HttpMethod.GET,
1262 1263 HttpEntity.EMPTY,
1263 1264 new ParameterizedTypeReference<Long>() {
... ... @@ -1772,7 +1773,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
1772 1773 }).getBody();
1773 1774 }
1774 1775
1775   - public List<OAuth2ClientInfo> getOAuth2Clients(String pkgName) {
  1776 + public List<OAuth2ClientInfo> getOAuth2Clients(String pkgName, PlatformType platformType) {
1776 1777 Map<String, String> params = new HashMap<>();
1777 1778 StringBuilder urlBuilder = new StringBuilder(baseURL);
1778 1779 urlBuilder.append("/api/noauth/oauth2Clients");
... ... @@ -1780,6 +1781,15 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
1780 1781 urlBuilder.append("?pkgName={pkgName}");
1781 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 1793 return restTemplate.exchange(
1784 1794 urlBuilder.toString(),
1785 1795 HttpMethod.POST,
... ...
... ... @@ -19,29 +19,22 @@ import com.fasterxml.jackson.databind.JsonNode;
19 19 import com.google.common.util.concurrent.ListenableFuture;
20 20 import org.thingsboard.server.common.msg.TbMsg;
21 21
22   -import javax.script.ScriptException;
23 22 import java.util.List;
24 23 import java.util.Set;
25 24
26 25 public interface ScriptEngine {
27 26
28   - List<TbMsg> executeUpdate(TbMsg msg) throws ScriptException;
29   -
30 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 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 39 void destroy();
47 40
... ...
... ... @@ -214,6 +214,10 @@ public interface TbContext {
214 214
215 215 EdgeEventService getEdgeEventService();
216 216
  217 + /**
  218 + * Js script executors call are completely asynchronous
  219 + * */
  220 + @Deprecated
217 221 ListeningExecutor getJsExecutor();
218 222
219 223 ListeningExecutor getMailExecutor();
... ...
... ... @@ -15,8 +15,11 @@
15 15 */
16 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 21 import lombok.extern.slf4j.Slf4j;
19   -import org.thingsboard.common.util.ListeningExecutor;
  22 +import org.checkerframework.checker.nullness.qual.Nullable;
20 23 import org.thingsboard.rule.engine.api.RuleNode;
21 24 import org.thingsboard.rule.engine.api.ScriptEngine;
22 25 import org.thingsboard.rule.engine.api.TbContext;
... ... @@ -55,18 +58,21 @@ public class TbLogNode implements TbNode {
55 58
56 59 @Override
57 60 public void onMsg(TbContext ctx, TbMsg msg) {
58   - ListeningExecutor jsExecutor = ctx.getJsExecutor();
59 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 78 @Override
... ...
... ... @@ -15,9 +15,12 @@
15 15 */
16 16 package org.thingsboard.rule.engine.debug;
17 17
  18 +import com.google.common.util.concurrent.Futures;
18 19 import com.google.common.util.concurrent.ListenableFuture;
  20 +import com.google.common.util.concurrent.MoreExecutors;
19 21 import lombok.extern.slf4j.Slf4j;
20 22 import org.springframework.util.StringUtils;
  23 +import org.thingsboard.common.util.TbStopWatch;
21 24 import org.thingsboard.rule.engine.api.RuleNode;
22 25 import org.thingsboard.rule.engine.api.ScriptEngine;
23 26 import org.thingsboard.rule.engine.api.TbContext;
... ... @@ -35,6 +38,7 @@ import org.thingsboard.server.common.msg.queue.ServiceQueue;
35 38
36 39 import java.util.UUID;
37 40 import java.util.concurrent.TimeUnit;
  41 +import java.util.concurrent.atomic.AtomicBoolean;
38 42
39 43 import static org.thingsboard.common.util.DonAsynchron.withCallback;
40 44 import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
... ... @@ -64,10 +68,11 @@ public class TbMsgGeneratorNode implements TbNode {
64 68 private EntityId originatorId;
65 69 private UUID nextTickId;
66 70 private TbMsg prevMsg;
67   - private volatile boolean initialized;
  71 + private final AtomicBoolean initialized = new AtomicBoolean(false);
68 72
69 73 @Override
70 74 public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
  75 + log.trace("init generator with config {}", configuration);
71 76 this.config = TbNodeUtils.convert(configuration, TbMsgGeneratorNodeConfiguration.class);
72 77 this.delay = TimeUnit.SECONDS.toMillis(config.getPeriodInSeconds());
73 78 this.currentMsgCount = 0;
... ... @@ -81,35 +86,39 @@ public class TbMsgGeneratorNode implements TbNode {
81 86
82 87 @Override
83 88 public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) {
  89 + log.trace("onPartitionChangeMsg, PartitionChangeMsg {}, config {}", msg, config);
84 90 updateGeneratorState(ctx);
85 91 }
86 92
87 93 private void updateGeneratorState(TbContext ctx) {
  94 + log.trace("updateGeneratorState, config {}", config);
88 95 if (ctx.isLocalEntity(originatorId)) {
89   - if (!initialized) {
90   - initialized = true;
  96 + if (initialized.compareAndSet(false, true)) {
91 97 this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType");
92 98 scheduleTickMsg(ctx);
93 99 }
94   - } else if (initialized) {
95   - initialized = false;
  100 + } else if (initialized.compareAndSet(true, false)) {
96 101 destroy();
97 102 }
98 103 }
99 104
100 105 @Override
101 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 110 withCallback(generate(ctx, msg),
104 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 114 ctx.enqueueForTellNext(m, SUCCESS);
107 115 scheduleTickMsg(ctx);
108 116 currentMsgCount++;
109 117 }
110 118 },
111 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 122 ctx.tellFailure(msg, t);
114 123 scheduleTickMsg(ctx);
115 124 currentMsgCount++;
... ... @@ -119,6 +128,7 @@ public class TbMsgGeneratorNode implements TbNode {
119 128 }
120 129
121 130 private void scheduleTickMsg(TbContext ctx) {
  131 + log.trace("scheduleTickMsg, config {}", config);
122 132 long curTs = System.currentTimeMillis();
123 133 if (lastScheduledTs == 0L) {
124 134 lastScheduledTs = curTs;
... ... @@ -131,22 +141,26 @@ public class TbMsgGeneratorNode implements TbNode {
131 141 }
132 142
133 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 152 ctx.logJsEvalResponse();
142 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 161 @Override
149 162 public void destroy() {
  163 + log.trace("destroy, config {}", config);
150 164 prevMsg = null;
151 165 if (jsEngine != null) {
152 166 jsEngine.destroy();
... ...
... ... @@ -15,7 +15,11 @@
15 15 */
16 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 21 import lombok.extern.slf4j.Slf4j;
  22 +import org.checkerframework.checker.nullness.qual.Nullable;
19 23 import org.thingsboard.common.util.ListeningExecutor;
20 24 import org.thingsboard.rule.engine.api.RuleNode;
21 25 import org.thingsboard.rule.engine.api.ScriptEngine;
... ... @@ -29,8 +33,6 @@ import org.thingsboard.server.common.msg.TbMsg;
29 33
30 34 import java.util.Set;
31 35
32   -import static org.thingsboard.common.util.DonAsynchron.withCallback;
33   -
34 36 @Slf4j
35 37 @RuleNode(
36 38 type = ComponentType.FILTER,
... ... @@ -58,17 +60,20 @@ public class TbJsSwitchNode implements TbNode {
58 60
59 61 @Override
60 62 public void onMsg(TbContext ctx, TbMsg msg) {
61   - ListeningExecutor jsExecutor = ctx.getJsExecutor();
62 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 79 private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) {
... ...
... ... @@ -64,7 +64,7 @@ public class TbJsSwitchNodeTest {
64 64 private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased());
65 65
66 66 @Test
67   - public void multipleRoutesAreAllowed() throws TbNodeException, ScriptException {
  67 + public void multipleRoutesAreAllowed() throws TbNodeException {
68 68 initWithScript();
69 69 TbMsgMetaData metaData = new TbMsgMetaData();
70 70 metaData.putValue("temp", "10");
... ... @@ -72,11 +72,9 @@ public class TbJsSwitchNodeTest {
72 72 String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
73 73
74 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 77 node.onMsg(ctx, msg);
79   - verify(ctx).getJsExecutor();
80 78 verify(ctx).tellNext(msg, Sets.newHashSet("one", "three"));
81 79 }
82 80
... ... @@ -92,19 +90,6 @@ public class TbJsSwitchNodeTest {
92 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 93 private void verifyError(TbMsg msg, String message, Class expectedClass) {
109 94 ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
110 95 verify(ctx).tellFailure(same(msg), captor.capture());
... ...
... ... @@ -133,7 +133,7 @@ export class OtaPackageService {
133 133 }
134 134
135 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 139 public confirmDialogUpdatePackage(entity: BaseData<EntityId>&OtaPagesIds,
... ...
... ... @@ -293,24 +293,24 @@ export class MenuService {
293 293 sections.push(
294 294 {
295 295 id: guid(),
  296 + name: 'edge.edge-instances',
  297 + type: 'link',
  298 + path: '/edgeInstances',
  299 + icon: 'router'
  300 + },
  301 + {
  302 + id: guid(),
296 303 name: 'edge.management',
297 304 type: 'toggle',
298   - path: '/edges',
299   - height: '80px',
300   - icon: 'router',
  305 + path: '/edgeManagement',
  306 + height: '40px',
  307 + icon: 'settings_input_antenna',
301 308 pages: [
302 309 {
303 310 id: guid(),
304   - name: 'edge.edge-instances',
305   - type: 'link',
306   - path: '/edges',
307   - icon: 'router'
308   - },
309   - {
310   - id: guid(),
311 311 name: 'edge.rulechain-templates',
312 312 type: 'link',
313   - path: '/edges/ruleChains',
  313 + path: '/edgeManagement/ruleChains',
314 314 icon: 'settings_ethernet'
315 315 }
316 316 ]
... ... @@ -448,12 +448,12 @@ export class MenuService {
448 448 {
449 449 name: 'edge.edge-instances',
450 450 icon: 'router',
451   - path: '/edges'
  451 + path: '/edgeInstances'
452 452 },
453 453 {
454 454 name: 'edge.rulechain-templates',
455 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 548 id: guid(),
549 549 name: 'edge.edge-instances',
550 550 type: 'link',
551   - path: '/edges',
  551 + path: '/edgeInstances',
552 552 icon: 'router'
553 553 }
554 554 );
... ... @@ -606,8 +606,8 @@ export class MenuService {
606 606 places: [
607 607 {
608 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 128 DashboardImageDialogComponent,
129 129 DashboardImageDialogData, DashboardImageDialogResult
130 130 } from '@home/components/dashboard-page/dashboard-image-dialog.component';
  131 +import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
131 132
132 133 // @dynamic
133 134 @Component({
... ... @@ -211,7 +212,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
211 212
212 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 218 dashboardCtx: DashboardContext = {
217 219 instanceId: this.utils.guid(),
... ... @@ -312,7 +314,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
312 314 private ngZone: NgZone,
313 315 private overlay: Overlay,
314 316 private viewContainerRef: ViewContainerRef,
315   - private cd: ChangeDetectorRef) {
  317 + private cd: ChangeDetectorRef,
  318 + private sanitizer: DomSanitizer) {
316 319 super(store);
317 320
318 321 }
... ... @@ -413,6 +416,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
413 416 private reset() {
414 417 this.dashboard = null;
415 418 this.dashboardConfiguration = null;
  419 + this.dashboardLogoCache = undefined;
416 420 this.prevDashboard = null;
417 421
418 422 this.widgetEditMode = false;
... ... @@ -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 585 public showRightLayoutSwitch(): boolean {
... ... @@ -702,6 +710,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
702 710 }).afterClosed().subscribe((data) => {
703 711 if (data) {
704 712 this.dashboard.configuration.settings = data.settings;
  713 + this.dashboardLogoCache = undefined;
705 714 const newGridSettings = data.gridSettings;
706 715 if (newGridSettings) {
707 716 const layout = this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout];
... ... @@ -855,11 +864,13 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
855 864 if (this.widgetEditMode) {
856 865 if (revert) {
857 866 this.dashboard = this.prevDashboard;
  867 + this.dashboardLogoCache = undefined;
858 868 }
859 869 } else {
860 870 this.resetHighlight();
861 871 if (revert) {
862 872 this.dashboard = this.prevDashboard;
  873 + this.dashboardLogoCache = undefined;
863 874 this.dashboardConfiguration = this.dashboard.configuration;
864 875 this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
865 876 this.entityAliasesUpdated();
... ...
... ... @@ -99,7 +99,6 @@ import { DeviceProfileDialogComponent } from '@home/components/profile/device-pr
99 99 import { DeviceProfileAutocompleteComponent } from '@home/components/profile/device-profile-autocomplete.component';
100 100 import { MqttDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/mqtt-device-profile-transport-configuration.component';
101 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 102 import { DeviceProfileAlarmsComponent } from '@home/components/profile/alarm/device-profile-alarms.component';
104 103 import { DeviceProfileAlarmComponent } from '@home/components/profile/alarm/device-profile-alarm.component';
105 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 142 import { SecurityConfigLwm2mServerComponent } from '@home/components/device/security-config-lwm2m-server.component';
144 143 import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component';
145 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 147 @NgModule({
148 148 declarations:
... ... @@ -228,7 +228,6 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain
228 228 DefaultDeviceProfileTransportConfigurationComponent,
229 229 MqttDeviceProfileTransportConfigurationComponent,
230 230 CoapDeviceProfileTransportConfigurationComponent,
231   - SnmpDeviceProfileTransportConfigurationComponent,
232 231 DeviceProfileTransportConfigurationComponent,
233 232 CreateAlarmRulesComponent,
234 233 AlarmRuleComponent,
... ... @@ -272,6 +271,7 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain
272 271 SharedModule,
273 272 SharedHomeComponentsModule,
274 273 Lwm2mProfileComponentsModule,
  274 + SnmpDeviceProfileTransportModule,
275 275 StatesControllerModule
276 276 ],
277 277 exports: [
... ... @@ -339,7 +339,6 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain
339 339 DefaultDeviceProfileTransportConfigurationComponent,
340 340 MqttDeviceProfileTransportConfigurationComponent,
341 341 CoapDeviceProfileTransportConfigurationComponent,
342   - SnmpDeviceProfileTransportConfigurationComponent,
343 342 DeviceProfileTransportConfigurationComponent,
344 343 CreateAlarmRulesComponent,
345 344 AlarmRuleComponent,
... ...
... ... @@ -89,7 +89,9 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu
89 89 if (configuration) {
90 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 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 15 ///
16 16
17 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 28 import { coerceBooleanProperty } from '@angular/cdk/coercion';
22 29 import {
23 30 DeviceProfileTransportConfiguration,
... ... @@ -40,19 +47,24 @@ export interface OidMappingConfiguration {
40 47 selector: 'tb-snmp-device-profile-transport-configuration',
41 48 templateUrl: './snmp-device-profile-transport-configuration.component.html',
42 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 64 snmpDeviceProfileTransportConfigurationFormGroup: FormGroup;
52 65
53 66 private destroy$ = new Subject();
54 67 private requiredValue: boolean;
55   - private configuration = [];
56 68
57 69 get required(): boolean {
58 70 return this.requiredValue;
... ... @@ -69,12 +81,14 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements Control
69 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 87 ngOnInit(): void {
76 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 93 this.snmpDeviceProfileTransportConfigurationFormGroup.valueChanges.pipe(
80 94 takeUntil(this.destroy$)
... ... @@ -95,18 +109,33 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements Control
95 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 121 writeValue(value: SnmpDeviceProfileTransportConfiguration | null): void {
99 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 127 private updateModel() {
105 128 let configuration: DeviceProfileTransportConfiguration = null;
106 129 if (this.snmpDeviceProfileTransportConfigurationFormGroup.valid) {
107   - configuration = this.snmpDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration;
  130 + configuration = this.snmpDeviceProfileTransportConfigurationFormGroup.getRawValue();
108 131 configuration.type = DeviceTransportType.SNMP;
109 132 }
110 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 274 this.updateTitle(true);
275 275 this.alarmsDatasource.updateAlarms();
276 276 this.clearCache();
  277 + this.ctx.detectChanges();
277 278 }
278 279
279 280 public pageLinkSortDirection(): SortDirection {
... ...
... ... @@ -443,7 +443,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
443 443 const dataPageData = subscription.dataPages[0];
444 444 const childNodes: HierarchyNavTreeNode[] = [];
445 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 448 nodeCtx.childrenNodesLoaded = true;
449 449 childrenNodesLoadCb(this.prepareNodes(childNodes));
... ...
... ... @@ -47,7 +47,6 @@ import {
47 47 isDefined,
48 48 isNumber,
49 49 isObject,
50   - isString,
51 50 isUndefined
52 51 } from '@core/utils';
53 52 import cssjs from '@core/css/css';
... ... @@ -236,6 +235,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
236 235 this.updateTitle(true);
237 236 this.entityDatasource.dataUpdated();
238 237 this.clearCache();
  238 + this.ctx.detectChanges();
239 239 }
240 240
241 241 public pageLinkSortDirection(): SortDirection {
... ...
... ... @@ -69,10 +69,10 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
69 69
70 70 this.config.cellActionDescriptors.push(
71 71 {
72   - name: this.translate.instant('resource.export'),
  72 + name: this.translate.instant('resource.download'),
73 73 icon: 'file_download',
74 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 118 return this.config;
119 119 }
120 120
121   - exportResource($event: Event, resource: ResourceInfo) {
  121 + downloadResource($event: Event, resource: ResourceInfo) {
122 122 if ($event) {
123 123 $event.stopPropagation();
124 124 }
... ... @@ -127,8 +127,8 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
127 127
128 128 onResourceAction(action: EntityAction<ResourceInfo>): boolean {
129 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 132 return true;
133 133 }
134 134 return false;
... ...
... ... @@ -18,16 +18,26 @@
18 18 <div class="tb-details-buttons" fxLayout.xs="column">
19 19 <button mat-raised-button color="primary" fxFlex.xs
20 20 [disabled]="(isLoading$ | async)"
21   - (click)="onEntityAction($event, 'uploadResource')"
  21 + (click)="onEntityAction($event, 'downloadResource')"
22 22 [fxShow]="!isEdit">
23   - {{'resource.export' | translate }}
  23 + {{ 'resource.download' | translate }}
24 24 </button>
25 25 <button mat-raised-button color="primary" fxFlex.xs
26 26 [disabled]="(isLoading$ | async)"
27 27 (click)="onEntityAction($event, 'delete')"
28 28 [fxShow]="!hideDelete() && !isEdit">
29   - {{'resource.delete' | translate }}
  29 + {{ 'resource.delete' | translate }}
30 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 41 </div>
32 42 <div class="mat-padding" fxLayout="column">
33 43 <form [formGroup]="entityForm">
... ... @@ -47,7 +57,7 @@
47 57 {{ 'resource.title-required' | translate }}
48 58 </mat-error>
49 59 </mat-form-field>
50   - <tb-file-input
  60 + <tb-file-input *ngIf="isAdd"
51 61 formControlName="data"
52 62 required
53 63 [readAsBinary]="true"
... ... @@ -59,6 +69,12 @@
59 69 [existingFileName]="entityForm.get('fileName')?.value"
60 70 (fileNameChanged)="entityForm?.get('fileName').patchValue($event)">
61 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 78 </fieldset>
63 79 </form>
64 80 </div>
... ...
... ... @@ -30,6 +30,7 @@ import {
30 30 ResourceTypeTranslationMap
31 31 } from '@shared/models/resource.models';
32 32 import { pairwise, startWith, takeUntil } from 'rxjs/operators';
  33 +import { ActionNotificationShow } from "@core/notification/notification.actions";
33 34
34 35 @Component({
35 36 selector: 'tb-resources-library',
... ... @@ -88,26 +89,29 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
88 89 }
89 90
90 91 buildForm(entity: Resource): FormGroup {
91   - return this.fb.group(
  92 + const form = this.fb.group(
92 93 {
  94 + title: [entity ? entity.title : '', []],
93 95 resourceType: [{
94 96 value: entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL,
95   - disabled: this.isEdit
  97 + disabled: !this.isAdd
96 98 }, [Validators.required]],
97   - data: [entity ? entity.data : null, [Validators.required]],
98 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 108 updateForm(entity: Resource) {
105   - this.entityForm.patchValue({resourceType: entity.resourceType});
106 109 if (this.isEdit) {
107 110 this.entityForm.get('resourceType').disable({emitEvent: false});
  111 + this.entityForm.get('fileName').disable({emitEvent: false});
108 112 }
109 113 this.entityForm.patchValue({
110   - data: entity.data,
  114 + resourceType: entity.resourceType,
111 115 fileName: entity.fileName,
112 116 title: entity.title
113 117 });
... ... @@ -132,4 +136,15 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
132 136 convertToBase64File(data: string): string {
133 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 98 }
99 99 },
100 100 {
101   - path: ':customerId/edges',
  101 + path: ':customerId/edgeInstances',
102 102 component: EntitiesTableComponent,
103 103 data: {
104 104 auth: [Authority.TENANT_ADMIN],
... ...
... ... @@ -169,7 +169,7 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
169 169 if ($event) {
170 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 175 onCustomerAction(action: EntityAction<Customer>): boolean {
... ...
... ... @@ -362,7 +362,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
362 362 if (this.config.componentsData.dashboardScope === 'customer') {
363 363 this.router.navigateByUrl(`customers/${this.config.componentsData.customerId}/dashboards/${dashboard.id.id}`);
364 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 366 } else {
367 367 this.router.navigateByUrl(`dashboards/${dashboard.id.id}`);
368 368 }
... ...
... ... @@ -15,7 +15,16 @@
15 15 ///
16 16
17 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 28 import { Store } from '@ngrx/store';
20 29 import { AppState } from '@app/core/core.state';
21 30 import { coerceBooleanProperty } from '@angular/cdk/coercion';
... ... @@ -29,13 +38,20 @@ import {
29 38 selector: 'tb-device-data',
30 39 templateUrl: './device-data.component.html',
31 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 56 deviceDataFormGroup: FormGroup;
41 57
... ... @@ -97,6 +113,12 @@ export class DeviceDataComponent implements ControlValueAccessor, OnInit {
97 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 122 private updateModel() {
101 123 let deviceData: DeviceData = null;
102 124 if (this.deviceDataFormGroup.valid) {
... ...
... ... @@ -15,27 +15,39 @@
15 15 ///
16 16
17 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 28 import { Store } from '@ngrx/store';
20 29 import { AppState } from '@app/core/core.state';
21 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 32 import { deepClone } from '@core/utils';
27 33
28 34 @Component({
29 35 selector: 'tb-device-transport-configuration',
30 36 templateUrl: './device-transport-configuration.component.html',
31 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 52 deviceTransportType = DeviceTransportType;
41 53
... ... @@ -92,7 +104,15 @@ export class DeviceTransportConfigurationComponent implements ControlValueAccess
92 104 if (configuration) {
93 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 118 private updateModel() {
... ...
... ... @@ -15,10 +15,119 @@
15 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 133 </form>
... ...
... ... @@ -14,31 +14,57 @@
14 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 31 import {
23 32 DeviceTransportConfiguration,
24 33 DeviceTransportType,
25   - SnmpDeviceTransportConfiguration
  34 + SnmpAuthenticationProtocol,
  35 + SnmpAuthenticationProtocolTranslationMap,
  36 + SnmpDeviceProtocolVersion,
  37 + SnmpDeviceTransportConfiguration,
  38 + SnmpPrivacyProtocol,
  39 + SnmpPrivacyProtocolTranslationMap
26 40 } from '@shared/models/device.models';
  41 +import { isDefinedAndNotNull } from '@core/utils';
27 42
28 43 @Component({
29 44 selector: 'tb-snmp-device-transport-configuration',
30 45 templateUrl: './snmp-device-transport-configuration.component.html',
31 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 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 68 private requiredValue: boolean;
43 69
44 70 get required(): boolean {
... ... @@ -53,8 +79,7 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc
53 79 @Input()
54 80 disabled: boolean;
55 81
56   - private propagateChange = (v: any) => {
57   - };
  82 + private propagateChange = (v: any) => { };
58 83
59 84 constructor(private store: Store<AppState>,
60 85 private fb: FormBuilder) {
... ... @@ -69,13 +94,33 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc
69 94
70 95 ngOnInit() {
71 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 113 this.snmpDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => {
75 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 124 setDisabledState(isDisabled: boolean): void {
80 125 this.disabled = isDisabled;
81 126 if (this.disabled) {
... ... @@ -86,13 +131,46 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc
86 131 }
87 132
88 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 170 private updateModel() {
93 171 let configuration: DeviceTransportConfiguration = null;
94 172 if (this.snmpDeviceTransportConfigurationFormGroup.valid) {
95   - configuration = this.snmpDeviceTransportConfigurationFormGroup.getRawValue().configuration;
  173 + configuration = this.snmpDeviceTransportConfigurationFormGroup.value;
96 174 configuration.type = DeviceTransportType.SNMP;
97 175 }
98 176 this.propagateChange(configuration);
... ...
... ... @@ -42,7 +42,7 @@ import {
42 42
43 43 const routes: Routes = [
44 44 {
45   - path: 'edges',
  45 + path: 'edgeInstances',
46 46 data: {
47 47 breadcrumb: {
48 48 label: 'edge.edge-instances',
... ... @@ -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 210 path: 'ruleChains',
... ...
... ... @@ -417,7 +417,7 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
417 417 suffix = 'ruleChains';
418 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 423 assignToCustomer($event: Event, edgesIds: Array<EdgeId>) {
... ...
... ... @@ -152,7 +152,7 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
152 152 }
153 153 if (this.ruleNode.targetRuleChainId) {
154 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 156 } else {
157 157 this.router.navigateByUrl(`/ruleChains/${this.ruleNode.targetRuleChainId}`);
158 158 }
... ...
... ... @@ -1289,7 +1289,7 @@ export class RuleChainPageComponent extends PageComponent
1289 1289 if (this.ruleChainType !== RuleChainType.EDGE) {
1290 1290 this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`);
1291 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 1294 } else {
1295 1295 this.createRuleChainModel();
... ...
... ... @@ -285,7 +285,7 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
285 285 if (ruleChainImport) {
286 286 this.itembuffer.storeRuleChainImport(ruleChainImport);
287 287 if (this.config.componentsData.ruleChainScope === 'edges') {
288   - this.router.navigateByUrl(`edges/ruleChains/ruleChain/import`);
  288 + this.router.navigateByUrl(`edgeManagement/ruleChains/ruleChain/import`);
289 289 } else {
290 290 this.router.navigateByUrl(`ruleChains/ruleChain/import`);
291 291 }
... ... @@ -298,9 +298,9 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
298 298 $event.stopPropagation();
299 299 }
300 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 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 304 } else {
305 305 this.router.navigateByUrl(`ruleChains/${ruleChain.id.id}`);
306 306 }
... ...
... ... @@ -50,7 +50,7 @@ export class RuleNodeComponent extends FcNodeComponent implements OnInit {
50 50 }
51 51 if (node.targetRuleChainId) {
52 52 if (node.ruleChainType === RuleChainType.EDGE) {
53   - this.router.navigateByUrl(`/edges/ruleChains/${node.targetRuleChainId}`);
  53 + this.router.navigateByUrl(`/edgeManagement/ruleChains/${node.targetRuleChainId}`);
54 54 } else {
55 55 this.router.navigateByUrl(`/ruleChains/${node.targetRuleChainId}`);
56 56 }
... ...
... ... @@ -120,6 +120,7 @@ export const HelpLinks = {
120 120 entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views',
121 121 entitiesImport: helpBaseUrl + '/docs/user-guide/bulk-provisioning',
122 122 rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains',
  123 + resources: helpBaseUrl + '/docs/user-guide/ui/resources',
123 124 dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards',
124 125 otaUpdates: helpBaseUrl + '/docs/user-guide/ui/ota-updates',
125 126 widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles',
... ... @@ -147,6 +148,22 @@ export enum ValueType {
147 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 167 export const valueTypesMap = new Map<ValueType, ValueTypeData>(
151 168 [
152 169 [
... ...
... ... @@ -29,6 +29,7 @@ import * as _moment from 'moment';
29 29 import { AbstractControl, ValidationErrors } from '@angular/forms';
30 30 import { OtaPackageId } from '@shared/models/id/ota-package-id';
31 31 import { DashboardId } from '@shared/models/id/dashboard-id';
  32 +import { DataType } from '@shared/models/constants';
32 33
33 34 export enum DeviceProfileType {
34 35 DEFAULT = 'DEFAULT',
... ... @@ -257,7 +258,35 @@ export interface Lwm2mDeviceProfileTransportConfiguration {
257 258 }
258 259
259 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 292 export type DeviceProfileTransportConfigurations = DefaultDeviceProfileTransportConfiguration &
... ... @@ -332,7 +361,11 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT
332 361 transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M};
333 362 break;
334 363 case DeviceTransportType.SNMP:
335   - const snmpTransportConfiguration: SnmpDeviceProfileTransportConfiguration = {};
  364 + const snmpTransportConfiguration: SnmpDeviceProfileTransportConfiguration = {
  365 + timeoutMs: 500,
  366 + retries: 0,
  367 + communicationConfigs: null
  368 + };
336 369 transportConfiguration = {...snmpTransportConfiguration, type: DeviceTransportType.SNMP};
337 370 break;
338 371 }
... ... @@ -361,7 +394,12 @@ export function createDeviceTransportConfiguration(type: DeviceTransportType): D
361 394 transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M};
362 395 break;
363 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 403 transportConfiguration = {...snmpTransportConfiguration, type: DeviceTransportType.SNMP};
366 404 break;
367 405 }
... ... @@ -539,8 +577,57 @@ export interface Lwm2mDeviceTransportConfiguration {
539 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 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 633 export type DeviceTransportConfigurations = DefaultDeviceTransportConfiguration &
... ...
... ... @@ -61,6 +61,6 @@ export interface Resource extends ResourceInfo {
61 61 }
62 62
63 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 49 maxRuleNodeExecutionsPerMessage: number;
50 50 maxEmails: number;
51 51 maxSms: number;
  52 + maxCreatedAlarms: number;
52 53
53 54 defaultStorageTtlDays: number;
  55 + alarmsTtlDays: number;
54 56 }
55 57
56 58 export type TenantProfileConfigurations = DefaultTenantProfileConfiguration;
... ... @@ -81,7 +83,9 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
81 83 maxRuleNodeExecutionsPerMessage: 0,
82 84 maxEmails: 0,
83 85 maxSms: 0,
84   - defaultStorageTtlDays: 0
  86 + maxCreatedAlarms: 0,
  87 + defaultStorageTtlDays: 0,
  88 + alarmsTtlDays: 0
85 89 };
86 90 configuration = {...defaultConfiguration, type: TenantProfileType.DEFAULT};
87 91 break;
... ...
... ... @@ -1297,6 +1297,54 @@
1297 1297 "sw-update-recourse": "Software update CoAP recourse",
1298 1298 "sw-update-recourse-required": "Software update CoAP recourse is required.",
1299 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 1350 "dialog": {
... ... @@ -2309,20 +2357,23 @@
2309 2357 },
2310 2358 "resource": {
2311 2359 "add": "Add Resource",
  2360 + "copyId": "Copy resource Id",
2312 2361 "delete": "Delete resource",
2313 2362 "delete-resource-text": "Be careful, after the confirmation the resource will become unrecoverable.",
2314 2363 "delete-resource-title": "Are you sure you want to delete the resource '{{resourceTitle}}'?",
2315 2364 "delete-resources-action-title": "Delete { count, plural, 1 {1 resource} other {# resources} }",
2316 2365 "delete-resources-text": "Be careful, after the confirmation all selected resources will be removed.",
2317 2366 "delete-resources-title": "Are you sure you want to delete { count, plural, 1 {1 resource} other {# resources} }?",
  2367 + "download": "Download resource",
2318 2368 "drop-file": "Drop a resource file or click to select a file to upload.",
2319 2369 "empty": "Resource is empty",
2320   - "export": "Export resource",
  2370 + "file-name": "File name",
  2371 + "idCopiedMessage": "Resource Id has been copied to clipboard",
2321 2372 "no-resource-matching": "No resource matching '{{widgetsBundle}}' were found.",
2322 2373 "no-resource-text": "No resources found",
2323 2374 "open-widgets-bundle": "Open widgets bundle",
2324 2375 "resource": "Resource",
2325   - "resource-library-details": "Resource library details",
  2376 + "resource-library-details": "Resource details",
2326 2377 "resource-type": "Resource type",
2327 2378 "resources-library": "Resources library",
2328 2379 "search": "Search resources",
... ...
... ... @@ -1994,7 +1994,6 @@
1994 1994 "delete-resources-title": "确定要删除 { count, plural, 1 {# 个资源} other {# 个资源} }?",
1995 1995 "drop-file": "拖拽资源文件或单击以选择要上传的文件。",
1996 1996 "empty": "资源为空",
1997   - "export": "导出资源",
1998 1997 "no-resource-matching": "找不到与 '{{widgetsBundle}}' 匹配的资源。",
1999 1998 "no-resource-text": "找不到资源",
2000 1999 "open-widgets-bundle": "打开部件库",
... ...