Browse Source

Merge branch 'master' into lwm2m_coap_url_in_out

# Conflicts:
#	common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java
#	common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java
pull/4766/head
nickAS21 5 years ago
parent
commit
68ba6892d3
  1. 10
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  2. 14
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  3. 14
      application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java
  4. 2
      application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java
  5. 19
      application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java
  6. 158
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
  7. 1
      application/src/main/resources/thingsboard.yml
  8. 2
      application/src/test/java/org/thingsboard/server/transport/lwm2m/NoSecLwM2MIntegrationTest.java
  9. 7
      application/src/test/java/org/thingsboard/server/transport/lwm2m/X509LwM2MIntegrationTest.java
  10. 25
      common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java
  11. 9
      common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java
  12. 2
      common/queue/src/main/java/org/thingsboard/server/queue/TbQueueRequestTemplate.java
  13. 242
      common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java
  14. 10
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java
  15. 211
      common/queue/src/test/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplateTest.java
  16. 37
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java
  17. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java
  18. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/IdOrKeyRequest.java
  19. 1
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2MUplinkMsgHandler.java
  20. 4
      common/util/pom.xml
  21. 57
      common/util/src/main/java/org/thingsboard/common/util/TbStopWatch.java
  22. 16
      dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
  23. 33
      dao/src/main/java/org/thingsboard/server/dao/cache/CacheExecutorService.java
  24. 1
      dao/src/test/resources/application-test.properties
  25. 30
      msa/js-executor/api/httpServer.js
  26. 99
      msa/js-executor/api/jsInvokeMessageProcessor.js
  27. 5
      msa/js-executor/config/custom-environment-variables.yml
  28. 9
      msa/js-executor/config/default.yml
  29. 1
      msa/js-executor/package.json
  30. 110
      msa/js-executor/queue/kafkaTemplate.js
  31. 2
      msa/js-executor/server.js
  32. 361
      msa/js-executor/yarn.lock
  33. 5
      pom.xml
  34. 2
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  35. 15
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java
  36. 4
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  37. 30
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
  38. 50
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
  39. 29
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
  40. 19
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java
  41. 2
      ui-ngx/src/app/core/http/ota-package.service.ts
  42. 32
      ui-ngx/src/app/core/services/menu.service.ts
  43. 19
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  44. 5
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  45. 4
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts
  46. 23
      ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.html
  47. 72
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-communication-config.component.html
  48. 44
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-communication-config.component.scss
  49. 218
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-communication-config.component.ts
  50. 81
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-mapping.component.html
  51. 49
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-mapping.component.scss
  52. 165
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-mapping.component.ts
  53. 46
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport-configuration.component.html
  54. 57
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport-configuration.component.ts
  55. 38
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport.module.ts
  56. 1
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  57. 2
      ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts
  58. 2
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  59. 10
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
  60. 24
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html
  61. 27
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts
  62. 2
      ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts
  63. 2
      ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts
  64. 2
      ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts
  65. 36
      ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts
  66. 44
      ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts
  67. 121
      ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.html
  68. 112
      ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.ts
  69. 20
      ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts
  70. 2
      ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts
  71. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts
  72. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  73. 6
      ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts
  74. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts
  75. 17
      ui-ngx/src/app/shared/models/constants.ts
  76. 95
      ui-ngx/src/app/shared/models/device.models.ts
  77. 4
      ui-ngx/src/app/shared/models/resource.models.ts
  78. 6
      ui-ngx/src/app/shared/models/tenant.model.ts
  79. 55
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  80. 1
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json

10
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -782,15 +782,17 @@ public class DeviceController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/devices/count/{otaPackageType}", method = RequestMethod.GET)
@RequestMapping(value = "/devices/count/{otaPackageType}/{deviceProfileId}", method = RequestMethod.GET)
@ResponseBody
public Long countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(@PathVariable("otaPackageType") String otaPackageType,
@RequestParam String deviceProfileId) throws ThingsboardException {
public Long countByDeviceProfileAndEmptyOtaPackage(@PathVariable("otaPackageType") String otaPackageType,
@PathVariable("deviceProfileId") String deviceProfileId) throws ThingsboardException {
checkParameter("OtaPackageType", otaPackageType);
checkParameter("DeviceProfileId", deviceProfileId);
try {
return deviceService.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(
getCurrentUser().getTenantId(), new DeviceProfileId(UUID.fromString(deviceProfileId)), OtaPackageType.valueOf(otaPackageType));
getTenantId(),
new DeviceProfileId(UUID.fromString(deviceProfileId)),
OtaPackageType.valueOf(otaPackageType));
} catch (Exception e) {
throw handleException(e);
}

14
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -74,6 +74,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@ -86,6 +87,7 @@ public class RuleChainController extends BaseController {
public static final String RULE_NODE_ID = "ruleNodeId";
private static final ObjectMapper objectMapper = new ObjectMapper();
public static final int TIMEOUT = 20;
@Autowired
private InstallScripts installScripts;
@ -388,25 +390,25 @@ public class RuleChainController extends BaseController {
TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data);
switch (scriptType) {
case "update":
output = msgToOutput(engine.executeUpdate(inMsg));
output = msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
break;
case "generate":
output = msgToOutput(engine.executeGenerate(inMsg));
output = msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
break;
case "filter":
boolean result = engine.executeFilter(inMsg);
boolean result = engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
output = Boolean.toString(result);
break;
case "switch":
Set<String> states = engine.executeSwitch(inMsg);
Set<String> states = engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
output = objectMapper.writeValueAsString(states);
break;
case "json":
JsonNode json = engine.executeJson(inMsg);
JsonNode json = engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
output = objectMapper.writeValueAsString(json);
break;
case "string":
output = engine.executeToString(inMsg);
output = engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
break;
default:
throw new IllegalArgumentException("Unsupported script type: " + scriptType);

14
application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java

@ -30,6 +30,7 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
/**
@ -84,8 +85,10 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
apiUsageClient.report(tenantId, customerId, ApiUsageRecordKey.JS_EXEC_COUNT, 1);
return doInvokeFunction(scriptId, functionName, args);
} else {
return Futures.immediateFailedFuture(
new RuntimeException("Script invocation is blocked due to maximum error count " + getMaxErrors() + "!"));
String message = "Script invocation is blocked due to maximum error count "
+ getMaxErrors() + ", scriptId " + scriptId + "!";
log.warn(message);
return Futures.immediateFailedFuture(new RuntimeException(message));
}
} else {
return Futures.immediateFailedFuture(new RuntimeException("JS Execution is disabled due to API limits!"));
@ -117,8 +120,11 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
protected abstract long getMaxBlacklistDuration();
protected void onScriptExecutionError(UUID scriptId) {
disabledFunctions.computeIfAbsent(scriptId, key -> new DisableListInfo()).incrementAndGet();
protected void onScriptExecutionError(UUID scriptId, Throwable t, String scriptBody) {
DisableListInfo disableListInfo = disabledFunctions.computeIfAbsent(scriptId, key -> new DisableListInfo());
log.warn("Script has exception and will increment counter {} on disabledFunctions for id {}, exception {}, cause {}, scriptBody {}",
disableListInfo.get(), scriptId, t, t.getCause(), scriptBody);
disableListInfo.incrementAndGet();
}
private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) {

2
application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java

@ -160,7 +160,7 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer
return ((Invocable) engine).invokeFunction(functionName, args);
}
} catch (Exception e) {
onScriptExecutionError(scriptId);
onScriptExecutionError(scriptId, e, functionName);
throw new ExecutionException(e);
}
});

19
application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java

@ -18,7 +18,6 @@ package org.thingsboard.server.service.script;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -26,6 +25,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.queue.TbQueueRequestTemplate;
@ -161,7 +161,8 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
@Override
protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, String functionName, Object[] args) {
String scriptBody = scriptIdToBodysMap.get(scriptId);
log.trace("doInvokeFunction js-request for uuid {} with timeout {}ms", scriptId, maxRequestsTimeout);
final String scriptBody = scriptIdToBodysMap.get(scriptId);
if (scriptBody == null) {
return Futures.immediateFailedFuture(new RuntimeException("No script body found for scriptId: [" + scriptId + "]!"));
}
@ -170,7 +171,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
.setScriptIdLSB(scriptId.getLeastSignificantBits())
.setFunctionName(functionName)
.setTimeout((int) maxRequestsTimeout)
.setScriptBody(scriptIdToBodysMap.get(scriptId));
.setScriptBody(scriptBody);
for (Object arg : args) {
jsRequestBuilder.addArgs(arg.toString());
@ -180,6 +181,9 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
.setInvokeRequest(jsRequestBuilder.build())
.build();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(UUID.randomUUID(), jsRequestWrapper));
if (maxRequestsTimeout > 0) {
future = Futures.withTimeout(future, maxRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService);
@ -193,7 +197,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
@Override
public void onFailure(Throwable t) {
onScriptExecutionError(scriptId);
onScriptExecutionError(scriptId, t, scriptBody);
if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) {
queueTimeoutMsgs.incrementAndGet();
}
@ -201,13 +205,16 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
}
}, callbackExecutor);
return Futures.transform(future, response -> {
stopWatch.stop();
log.trace("doInvokeFunction js-response took {}ms for uuid {}", stopWatch.getTotalTimeMillis(), response.getKey());
JsInvokeProtos.JsInvokeResponse invokeResult = response.getValue().getInvokeResponse();
if (invokeResult.getSuccess()) {
return invokeResult.getResult();
} else {
onScriptExecutionError(scriptId);
final RuntimeException e = new RuntimeException(invokeResult.getErrorDetails());
onScriptExecutionError(scriptId, e, scriptBody);
log.debug("[{}] Failed to compile script due to [{}]: {}", scriptId, invokeResult.getErrorCode().name(), invokeResult.getErrorDetails());
throw new RuntimeException(invokeResult.getErrorDetails());
throw e;
}
}, callbackExecutor);
}

158
application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java

@ -18,12 +18,12 @@ package org.thingsboard.server.service.script;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
@ -32,6 +32,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import javax.script.ScriptException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -102,140 +103,115 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType();
return TbMsg.transformMsg(msg, newMessageType, msg.getOriginator(), newMetadata, newData);
} catch (Throwable th) {
th.printStackTrace();
throw new RuntimeException("Failed to unbind message data from javascript result", th);
}
}
@Override
public List<TbMsg> executeUpdate(TbMsg msg) throws ScriptException {
JsonNode result = executeScript(msg);
if (result.isObject()) {
return Collections.singletonList(unbindMsg(result, msg));
} else if (result.isArray()){
List<TbMsg> res = new ArrayList<>(result.size());
result.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg)));
return res;
} else {
log.warn("Wrong result type: {}", result.getNodeType());
throw new ScriptException("Wrong result type: " + result.getNodeType());
public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) {
ListenableFuture<JsonNode> result = executeScriptAsync(msg);
return Futures.transformAsync(result,
json -> executeUpdateTransform(msg, json),
MoreExecutors.directExecutor());
}
ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, JsonNode json) {
if (json.isObject()) {
return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg)));
} else if (json.isArray()) {
List<TbMsg> res = new ArrayList<>(json.size());
json.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg)));
return Futures.immediateFuture(res);
}
log.warn("Wrong result type: {}", json.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
}
@Override
public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) {
ListenableFuture<JsonNode> result = executeScriptAsync(msg);
return Futures.transformAsync(result, json -> {
if (json.isObject()) {
return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg)));
} else if (json.isArray()){
List<TbMsg> res = new ArrayList<>(json.size());
json.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg)));
return Futures.immediateFuture(res);
}
else{
log.warn("Wrong result type: {}", json.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
}
}, MoreExecutors.directExecutor());
public ListenableFuture<TbMsg> executeGenerateAsync(TbMsg prevMsg) {
return Futures.transformAsync(executeScriptAsync(prevMsg),
result -> executeGenerateTransform(prevMsg, result),
MoreExecutors.directExecutor());
}
@Override
public TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException {
JsonNode result = executeScript(prevMsg);
ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, JsonNode result) {
if (!result.isObject()) {
log.warn("Wrong result type: {}", result.getNodeType());
throw new ScriptException("Wrong result type: " + result.getNodeType());
Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
}
return unbindMsg(result, prevMsg);
return Futures.immediateFuture(unbindMsg(result, prevMsg));
}
@Override
public JsonNode executeJson(TbMsg msg) throws ScriptException {
return executeScript(msg);
}
@Override
public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) throws ScriptException {
public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) {
return executeScriptAsync(msg);
}
@Override
public String executeToString(TbMsg msg) throws ScriptException {
JsonNode result = executeScript(msg);
if (!result.isTextual()) {
log.warn("Wrong result type: {}", result.getNodeType());
throw new ScriptException("Wrong result type: " + result.getNodeType());
}
return result.asText();
public ListenableFuture<String> executeToStringAsync(TbMsg msg) {
return Futures.transformAsync(executeScriptAsync(msg),
this::executeToStringTransform,
MoreExecutors.directExecutor());
}
@Override
public boolean executeFilter(TbMsg msg) throws ScriptException {
JsonNode result = executeScript(msg);
if (!result.isBoolean()) {
log.warn("Wrong result type: {}", result.getNodeType());
throw new ScriptException("Wrong result type: " + result.getNodeType());
ListenableFuture<String> executeToStringTransform(JsonNode result) {
if (result.isTextual()) {
return Futures.immediateFuture(result.asText());
}
return result.asBoolean();
log.warn("Wrong result type: {}", result.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
}
@Override
public ListenableFuture<Boolean> executeFilterAsync(TbMsg msg) {
ListenableFuture<JsonNode> result = executeScriptAsync(msg);
return Futures.transformAsync(result, json -> {
if (!json.isBoolean()) {
log.warn("Wrong result type: {}", json.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
} else {
return Futures.immediateFuture(json.asBoolean());
}
}, MoreExecutors.directExecutor());
return Futures.transformAsync(executeScriptAsync(msg),
this::executeFilterTransform,
MoreExecutors.directExecutor());
}
@Override
public Set<String> executeSwitch(TbMsg msg) throws ScriptException {
JsonNode result = executeScript(msg);
ListenableFuture<Boolean> executeFilterTransform(JsonNode json) {
if (json.isBoolean()) {
return Futures.immediateFuture(json.asBoolean());
}
log.warn("Wrong result type: {}", json.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
}
ListenableFuture<Set<String>> executeSwitchTransform(JsonNode result) {
if (result.isTextual()) {
return Collections.singleton(result.asText());
} else if (result.isArray()) {
Set<String> nextStates = Sets.newHashSet();
return Futures.immediateFuture(Collections.singleton(result.asText()));
}
if (result.isArray()) {
Set<String> nextStates = new HashSet<>();
for (JsonNode val : result) {
if (!val.isTextual()) {
log.warn("Wrong result type: {}", val.getNodeType());
throw new ScriptException("Wrong result type: " + val.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + val.getNodeType()));
} else {
nextStates.add(val.asText());
}
}
return nextStates;
} else {
log.warn("Wrong result type: {}", result.getNodeType());
throw new ScriptException("Wrong result type: " + result.getNodeType());
return Futures.immediateFuture(nextStates);
}
log.warn("Wrong result type: {}", result.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
}
private JsonNode executeScript(TbMsg msg) throws ScriptException {
try {
String[] inArgs = prepareArgs(msg);
String eval = sandboxService.invokeFunction(tenantId, msg.getCustomerId(), this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString();
return mapper.readTree(eval);
} catch (ExecutionException e) {
if (e.getCause() instanceof ScriptException) {
throw (ScriptException) e.getCause();
} else if (e.getCause() instanceof RuntimeException) {
throw new ScriptException(e.getCause().getMessage());
} else {
throw new ScriptException(e);
}
} catch (Exception e) {
throw new ScriptException(e);
}
@Override
public ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg) {
return Futures.transformAsync(executeScriptAsync(msg),
this::executeSwitchTransform,
MoreExecutors.directExecutor()); //usually runs in a callbackExecutor
}
private ListenableFuture<JsonNode> executeScriptAsync(TbMsg msg) {
ListenableFuture<JsonNode> executeScriptAsync(TbMsg msg) {
log.trace("execute script async, msg {}", msg);
String[] inArgs = prepareArgs(msg);
return Futures.transformAsync(sandboxService.invokeFunction(tenantId, msg.getCustomerId(), this.scriptId, inArgs[0], inArgs[1], inArgs[2]),
return executeScriptAsync(msg.getCustomerId(), inArgs[0], inArgs[1], inArgs[2]);
}
ListenableFuture<JsonNode> executeScriptAsync(CustomerId customerId, Object... args) {
return Futures.transformAsync(sandboxService.invokeFunction(tenantId, customerId, this.scriptId, args),
o -> {
try {
return Futures.immediateFuture(mapper.readTree(o.toString()));

1
application/src/main/resources/thingsboard.yml

@ -332,6 +332,7 @@ actors:
cache:
# caffeine or redis
type: "${CACHE_TYPE:caffeine}"
maximumPoolSize: "${CACHE_MAXIMUM_POOL_SIZE:16}" # max pool size to process futures that calls the external cache
attributes:
# 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'
enabled: "${CACHE_ATTRIBUTES_ENABLED:true}"

2
application/src/test/java/org/thingsboard/server/transport/lwm2m/NoSecLwM2MIntegrationTest.java

@ -53,7 +53,7 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
private final int PORT = 5685;
private final Security SECURITY = noSec("coap://localhost:" + PORT, 123);
private final NetworkConfig COAP_CONFIG = new NetworkConfig().setString("COAP_PORT", Integer.toString(PORT));
private final String ENDPOINT = "deviceAEndpoint";
private final String ENDPOINT = "noSecEndpoint";
private Device createDevice() throws Exception {
Device device = new Device();

7
application/src/test/java/org/thingsboard/server/transport/lwm2m/X509LwM2MIntegrationTest.java

@ -19,6 +19,7 @@ import org.eclipse.californium.core.network.config.NetworkConfig;
import org.eclipse.leshan.client.object.Security;
import org.jetbrains.annotations.NotNull;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
@ -74,11 +75,13 @@ public class X509LwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
return device;
}
//TODO: use different endpoints to isolate tests.
@Ignore()
@Test
public void testConnectAndObserveTelemetry() throws Exception {
createDeviceProfile(TRANSPORT_CONFIGURATION);
X509ClientCredentials credentials = new X509ClientCredentials();
credentials.setEndpoint(endpoint);
credentials.setEndpoint(endpoint+1);
Device device = createDevice(credentials);
SingleEntityFilter sef = new SingleEntityFilter();
@ -96,7 +99,7 @@ public class X509LwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
wsClient.waitForReply();
wsClient.registerWaitForUpdate();
LwM2MTestClient client = new LwM2MTestClient(executor, endpoint);
LwM2MTestClient client = new LwM2MTestClient(executor, endpoint+1);
Security security = x509(serverUri, 123, clientX509Cert.getEncoded(), clientPrivateKeyFromCert.getEncoded(), serverX509Cert.getEncoded());
client.init(security, coapConfig);
String msg = wsClient.waitForUpdate();

25
common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java

@ -93,21 +93,16 @@ public class DefaultCoapServerService implements CoapServerService {
InetAddress addr = InetAddress.getByName(coapServerContext.getHost());
InetSocketAddress sockAddr = new InetSocketAddress(addr, coapServerContext.getPort());
noSecCoapEndpointBuilder.setInetSocketAddress(sockAddr);
NetworkConfig coapConfig = new NetworkConfig();
coapConfig.setInt(NetworkConfig.Keys.COAP_PORT, 5683);
coapConfig.setInt(NetworkConfig.Keys.COAP_SECURE_PORT, 5684);
coapConfig.setBoolean(NetworkConfig.Keys.BLOCKWISE_STRICT_BLOCK2_OPTION, true);
coapConfig.setBoolean(NetworkConfig.Keys.BLOCKWISE_ENTITY_TOO_LARGE_AUTO_FAILOVER, true);
coapConfig.setLong(NetworkConfig.Keys.BLOCKWISE_STATUS_LIFETIME, DEFAULT_BLOCKWISE_STATUS_LIFETIME);
coapConfig.setInt(NetworkConfig.Keys.MAX_RESOURCE_BODY_SIZE, 256 * 1024 * 1024);
coapConfig.setString(NetworkConfig.Keys.RESPONSE_MATCHING, "RELAXED");
coapConfig.setInt(NetworkConfig.Keys.PREFERRED_BLOCK_SIZE, 1024);
coapConfig.setInt(NetworkConfig.Keys.MAX_MESSAGE_SIZE, 1024);
coapConfig.setInt(NetworkConfig.Keys.MAX_RETRANSMIT, 10);
noSecCoapEndpointBuilder.setNetworkConfig(coapConfig);
NetworkConfig networkConfig = new NetworkConfig();
networkConfig.setBoolean(NetworkConfig.Keys.BLOCKWISE_STRICT_BLOCK2_OPTION, true);
networkConfig.setBoolean(NetworkConfig.Keys.BLOCKWISE_ENTITY_TOO_LARGE_AUTO_FAILOVER, true);
networkConfig.setLong(NetworkConfig.Keys.BLOCKWISE_STATUS_LIFETIME, DEFAULT_BLOCKWISE_STATUS_LIFETIME);
networkConfig.setInt(NetworkConfig.Keys.MAX_RESOURCE_BODY_SIZE, 256 * 1024 * 1024);
networkConfig.setString(NetworkConfig.Keys.RESPONSE_MATCHING, "RELAXED");
networkConfig.setInt(NetworkConfig.Keys.PREFERRED_BLOCK_SIZE, 1024);
networkConfig.setInt(NetworkConfig.Keys.MAX_MESSAGE_SIZE, 1024);
networkConfig.setInt(NetworkConfig.Keys.MAX_RETRANSMIT, 4);
noSecCoapEndpointBuilder.setNetworkConfig(networkConfig);
CoapEndpoint noSecCoapEndpoint = noSecCoapEndpointBuilder.build();
server.addEndpoint(noSecCoapEndpoint);

9
common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java

@ -51,6 +51,13 @@ public class SnmpDeviceTransportConfiguration implements DeviceTransportConfigur
private String privacyPassphrase;
private String engineId;
public SnmpDeviceTransportConfiguration() {
this.host = "localhost";
this.port = 161;
this.protocolVersion = SnmpProtocolVersion.V2C;
this.community = "public";
}
@Override
public DeviceTransportType getType() {
return DeviceTransportType.SNMP;
@ -76,7 +83,7 @@ public class SnmpDeviceTransportConfiguration implements DeviceTransportConfigur
isValid = StringUtils.isNotBlank(username) && StringUtils.isNotBlank(securityName)
&& contextName != null && authenticationProtocol != null
&& StringUtils.isNotBlank(authenticationPassphrase)
&& privacyProtocol != null && privacyPassphrase != null && engineId != null;
&& privacyProtocol != null && StringUtils.isNotBlank(privacyPassphrase) && engineId != null;
break;
}
}

2
common/queue/src/main/java/org/thingsboard/server/queue/TbQueueRequestTemplate.java

@ -24,6 +24,8 @@ public interface TbQueueRequestTemplate<Request extends TbQueueMsg, Response ext
ListenableFuture<Response> send(Request request);
ListenableFuture<Response> send(Request request, long timeoutNs);
void stop();
void setMessagesStats(MessagesStats messagesStats);

242
common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java

@ -19,7 +19,9 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.TbStopWatch;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin;
@ -31,13 +33,17 @@ import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.TbQueueRequestTemplate;
import org.thingsboard.server.common.stats.MessagesStats;
import javax.annotation.Nullable;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response extends TbQueueMsg> extends AbstractTbQueueTemplate
@ -46,15 +52,15 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
private final TbQueueAdmin queueAdmin;
private final TbQueueProducer<Request> requestTemplate;
private final TbQueueConsumer<Response> responseTemplate;
private final ConcurrentMap<UUID, DefaultTbQueueRequestTemplate.ResponseMetaData<Response>> pendingRequests;
private final boolean internalExecutor;
private final ExecutorService executor;
private final long maxRequestTimeout;
private final long maxPendingRequests;
private final long pollInterval;
private volatile long tickTs = 0L;
private volatile long tickSize = 0L;
private volatile boolean stopped = false;
final ConcurrentHashMap<UUID, DefaultTbQueueRequestTemplate.ResponseMetaData<Response>> pendingRequests = new ConcurrentHashMap<>();
final boolean internalExecutor;
final ExecutorService executor;
final long maxRequestTimeoutNs;
final long maxPendingRequests;
final long pollInterval;
volatile boolean stopped = false;
long nextCleanupNs = 0L;
private final Lock cleanerLock = new ReentrantLock();
private MessagesStats messagesStats;
@ -65,79 +71,113 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
long maxRequestTimeout,
long maxPendingRequests,
long pollInterval,
ExecutorService executor) {
@Nullable ExecutorService executor) {
this.queueAdmin = queueAdmin;
this.requestTemplate = requestTemplate;
this.responseTemplate = responseTemplate;
this.pendingRequests = new ConcurrentHashMap<>();
this.maxRequestTimeout = maxRequestTimeout;
this.maxRequestTimeoutNs = TimeUnit.MILLISECONDS.toNanos(maxRequestTimeout);
this.maxPendingRequests = maxPendingRequests;
this.pollInterval = pollInterval;
if (executor != null) {
internalExecutor = false;
this.executor = executor;
} else {
internalExecutor = true;
this.executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-queue-request-template-" + responseTemplate.getTopic()));
}
this.internalExecutor = (executor == null);
this.executor = internalExecutor ? createExecutor() : executor;
}
ExecutorService createExecutor() {
return Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-queue-request-template-" + responseTemplate.getTopic()));
}
@Override
public void init() {
queueAdmin.createTopicIfNotExists(responseTemplate.getTopic());
this.requestTemplate.init();
tickTs = System.currentTimeMillis();
requestTemplate.init();
responseTemplate.subscribe();
executor.submit(() -> {
long nextCleanupMs = 0L;
while (!stopped) {
try {
List<Response> responses = responseTemplate.poll(pollInterval);
if (responses.size() > 0) {
log.trace("Polling responses completed, consumer records count [{}]", responses.size());
}
responses.forEach(response -> {
byte[] requestIdHeader = response.getHeaders().get(REQUEST_ID_HEADER);
UUID requestId;
if (requestIdHeader == null) {
log.error("[{}] Missing requestId in header and body", response);
} else {
requestId = bytesToUuid(requestIdHeader);
log.trace("[{}] Response received: {}", requestId, response);
ResponseMetaData<Response> expectedResponse = pendingRequests.remove(requestId);
if (expectedResponse == null) {
log.trace("[{}] Invalid or stale request", requestId);
} else {
expectedResponse.future.set(response);
}
executor.submit(this::mainLoop);
}
void mainLoop() {
while (!stopped) {
TbStopWatch sw = TbStopWatch.startNew();
try {
fetchAndProcessResponses();
} catch (Throwable e) {
long sleepNanos = TimeUnit.MILLISECONDS.toNanos(this.pollInterval) - sw.stopAndGetTotalTimeNanos();
log.warn("Failed to obtain and process responses from queue. Going to sleep " + sleepNanos + "ns", e);
sleep(sleepNanos);
}
}
}
void fetchAndProcessResponses() {
final long pendingRequestsCount = pendingRequests.mappingCount();
log.trace("Starting template pool topic {}, for pendingRequests {}", responseTemplate.getTopic(), pendingRequestsCount);
List<Response> responses = doPoll(); //poll js responses
log.trace("Completed template poll topic {}, for pendingRequests [{}], received [{}] responses", responseTemplate.getTopic(), pendingRequestsCount, responses.size());
responses.forEach(this::processResponse); //this can take a long time
responseTemplate.commit();
tryCleanStaleRequests();
}
private boolean tryCleanStaleRequests() {
if (!cleanerLock.tryLock()) {
return false;
}
try {
log.trace("tryCleanStaleRequest...");
final long currentNs = getCurrentClockNs();
if (nextCleanupNs < currentNs) {
pendingRequests.forEach((key, value) -> {
if (value.expTime < currentNs) {
ResponseMetaData<Response> staleRequest = pendingRequests.remove(key);
if (staleRequest != null) {
setTimeoutException(key, staleRequest, currentNs);
}
});
responseTemplate.commit();
tickTs = System.currentTimeMillis();
tickSize = pendingRequests.size();
if (nextCleanupMs < tickTs) {
//cleanup;
pendingRequests.forEach((key, value) -> {
if (value.expTime < tickTs) {
ResponseMetaData<Response> staleRequest = pendingRequests.remove(key);
if (staleRequest != null) {
log.trace("[{}] Request timeout detected, expTime [{}], tickTs [{}]", key, staleRequest.expTime, tickTs);
staleRequest.future.setException(new TimeoutException());
}
}
});
nextCleanupMs = tickTs + maxRequestTimeout;
}
} catch (Throwable e) {
log.warn("Failed to obtain responses from queue.", e);
try {
Thread.sleep(pollInterval);
} catch (InterruptedException e2) {
log.trace("Failed to wait until the server has capacity to handle new responses", e2);
}
}
});
setupNextCleanup();
}
});
} finally {
cleanerLock.unlock();
}
return true;
}
void setupNextCleanup() {
nextCleanupNs = getCurrentClockNs() + maxRequestTimeoutNs;
log.trace("setupNextCleanup {}", nextCleanupNs);
}
List<Response> doPoll() {
return responseTemplate.poll(pollInterval);
}
void sleep(long nanos) {
LockSupport.parkNanos(nanos);
}
void setTimeoutException(UUID key, ResponseMetaData<Response> staleRequest, long currentNs) {
if (currentNs >= staleRequest.getSubmitTime() + staleRequest.getTimeout()) {
log.warn("Request timeout detected, currentNs [{}], {}, key [{}]", currentNs, staleRequest, key);
} else {
log.error("Request timeout detected, currentNs [{}], {}, key [{}]", currentNs, staleRequest, key);
}
staleRequest.future.setException(new TimeoutException());
}
void processResponse(Response response) {
byte[] requestIdHeader = response.getHeaders().get(REQUEST_ID_HEADER);
UUID requestId;
if (requestIdHeader == null) {
log.error("[{}] Missing requestId in header and body", response);
} else {
requestId = bytesToUuid(requestIdHeader);
log.trace("[{}] Response received: {}", requestId, String.valueOf(response).replace("\n", " ")); //TODO remove overhead
ResponseMetaData<Response> expectedResponse = pendingRequests.remove(requestId);
if (expectedResponse == null) {
log.warn("[{}] Invalid or stale request, response: {}", requestId, String.valueOf(response).replace("\n", " "));
} else {
expectedResponse.future.set(response);
}
}
}
@Override
@ -164,17 +204,48 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
@Override
public ListenableFuture<Response> send(Request request) {
if (tickSize > maxPendingRequests) {
return send(request, this.maxRequestTimeoutNs);
}
@Override
public ListenableFuture<Response> send(Request request, long requestTimeoutNs) {
if (pendingRequests.mappingCount() >= maxPendingRequests) {
log.warn("Pending request map is full [{}]! Consider to increase maxPendingRequests or increase processing performance", maxPendingRequests);
return Futures.immediateFailedFuture(new RuntimeException("Pending request map is full!"));
}
UUID requestId = UUID.randomUUID();
request.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId));
request.getHeaders().put(RESPONSE_TOPIC_HEADER, stringToBytes(responseTemplate.getTopic()));
request.getHeaders().put(REQUEST_TIME, longToBytes(System.currentTimeMillis()));
request.getHeaders().put(REQUEST_TIME, longToBytes(getCurrentTimeMs()));
long currentClockNs = getCurrentClockNs();
SettableFuture<Response> future = SettableFuture.create();
ResponseMetaData<Response> responseMetaData = new ResponseMetaData<>(tickTs + maxRequestTimeout, future);
pendingRequests.putIfAbsent(requestId, responseMetaData);
log.trace("[{}] Sending request, key [{}], expTime [{}]", requestId, request.getKey(), responseMetaData.expTime);
ResponseMetaData<Response> responseMetaData = new ResponseMetaData<>(currentClockNs + requestTimeoutNs, future, currentClockNs, requestTimeoutNs);
log.trace("pending {}", responseMetaData);
if (pendingRequests.putIfAbsent(requestId, responseMetaData) != null) {
log.warn("Pending request already exists [{}]!", maxPendingRequests);
return Futures.immediateFailedFuture(new RuntimeException("Pending request already exists !" + requestId));
}
sendToRequestTemplate(request, requestId, future, responseMetaData);
return future;
}
/**
* MONOTONIC clock instead jumping wall clock.
* Wrapped into the method for the test purposes to travel through the time
* */
long getCurrentClockNs() {
return System.nanoTime();
}
/**
* Wall clock to send timestamp to an external service
* */
long getCurrentTimeMs() {
return System.currentTimeMillis();
}
void sendToRequestTemplate(Request request, UUID requestId, SettableFuture<Response> future, ResponseMetaData<Response> responseMetaData) {
log.trace("[{}] Sending request, key [{}], expTime [{}], request {}", requestId, request.getKey(), responseMetaData.expTime, request);
if (messagesStats != null) {
messagesStats.incrementTotal();
}
@ -184,7 +255,7 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
if (messagesStats != null) {
messagesStats.incrementSuccessful();
}
log.trace("[{}] Request sent: {}", requestId, metadata);
log.trace("[{}] Request sent: {}, request {}", requestId, metadata, request);
}
@Override
@ -196,17 +267,32 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
future.setException(t);
}
});
return future;
}
private static class ResponseMetaData<T> {
@Getter
static class ResponseMetaData<T> {
private final long submitTime;
private final long timeout;
private final long expTime;
private final SettableFuture<T> future;
ResponseMetaData(long ts, SettableFuture<T> future) {
ResponseMetaData(long ts, SettableFuture<T> future, long submitTime, long timeout) {
this.submitTime = submitTime;
this.timeout = timeout;
this.expTime = ts;
this.future = future;
}
@Override
public String toString() {
return "ResponseMetaData{" +
"submitTime=" + submitTime +
", calculatedExpTime=" + (submitTime + timeout) +
", deltaMs=" + (expTime - submitTime) +
", expTime=" + expTime +
", future=" + future +
'}';
}
}
}

10
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java

@ -21,6 +21,7 @@ import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.springframework.util.StopWatch;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.common.AbstractTbQueueConsumerTemplate;
@ -82,7 +83,16 @@ public class TbKafkaConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQue
@Override
protected List<ConsumerRecord<String, byte[]>> doPoll(long durationInMillis) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
log.trace("poll topic {} maxDuration {}", getTopic(), durationInMillis);
ConsumerRecords<String, byte[]> records = consumer.poll(Duration.ofMillis(durationInMillis));
stopWatch.stop();
log.trace("poll topic {} took {}ms", getTopic(), stopWatch.getTotalTimeMillis());
if (records.isEmpty()) {
return Collections.emptyList();
} else {

211
common/queue/src/test/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplateTest.java

@ -0,0 +1,211 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.queue.common;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.TbQueueProducer;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.BDDMockito.willDoNothing;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.hamcrest.MockitoHamcrest.longThat;
@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class DefaultTbQueueRequestTemplateTest {
@Mock
TbQueueAdmin queueAdmin;
@Mock
TbQueueProducer<TbQueueMsg> requestTemplate;
@Mock
TbQueueConsumer<TbQueueMsg> responseTemplate;
@Mock
ExecutorService executorMock;
ExecutorService executor;
String topic = "js-responses-tb-node-0";
long maxRequestTimeout = 10;
long maxPendingRequests = 32;
long pollInterval = 5;
DefaultTbQueueRequestTemplate inst;
@Before
public void setUp() throws Exception {
willReturn(topic).given(responseTemplate).getTopic();
inst = spy(new DefaultTbQueueRequestTemplate(
queueAdmin, requestTemplate, responseTemplate,
maxRequestTimeout, maxPendingRequests, pollInterval, executorMock));
}
@After
public void tearDown() throws Exception {
if (executor != null) {
executor.shutdownNow();
}
}
@Test
public void givenInstance_whenVerifyInitialParameters_thenOK() {
assertThat(inst.maxPendingRequests, equalTo(maxPendingRequests));
assertThat(inst.maxRequestTimeoutNs, equalTo(TimeUnit.MILLISECONDS.toNanos(maxRequestTimeout)));
assertThat(inst.pollInterval, equalTo(pollInterval));
assertThat(inst.executor, is(executorMock));
assertThat(inst.stopped, is(false));
assertThat(inst.internalExecutor, is(false));
}
@Test
public void givenExternalExecutor_whenInitStop_thenOK() {
inst.init();
assertThat(inst.nextCleanupNs, equalTo(0L));
verify(queueAdmin, times(1)).createTopicIfNotExists(topic);
verify(requestTemplate, times(1)).init();
verify(responseTemplate, times(1)).subscribe();
verify(executorMock, times(1)).submit(any(Runnable.class));
inst.stop();
assertThat(inst.stopped, is(true));
verify(responseTemplate, times(1)).unsubscribe();
verify(requestTemplate, times(1)).stop();
verify(executorMock, never()).shutdownNow();
}
@Test
public void givenMainLoop_whenLoopFewTimes_thenVerifyInvocationCount() throws InterruptedException {
executor = inst.createExecutor();
CountDownLatch latch = new CountDownLatch(5);
willDoNothing().given(inst).sleep(anyLong());
willAnswer(invocation -> {
if (latch.getCount() == 1) {
inst.stop(); //stop the loop in natural way
}
if (latch.getCount() == 3 || latch.getCount() == 4) {
latch.countDown();
throw new RuntimeException("test catch block");
}
latch.countDown();
return null;
}).given(inst).fetchAndProcessResponses();
executor.submit(inst::mainLoop);
latch.await(10, TimeUnit.SECONDS);
verify(inst, times(5)).fetchAndProcessResponses();
verify(inst, times(2)).sleep(longThat(lessThan(TimeUnit.MILLISECONDS.toNanos(inst.pollInterval))));
}
@Test
public void givenMessages_whenSend_thenOK() {
willDoNothing().given(inst).sendToRequestTemplate(any(), any(), any(), any());
inst.init();
final int msgCount = 10;
for (int i = 0; i < msgCount; i++) {
inst.send(getRequestMsgMock());
}
assertThat(inst.pendingRequests.mappingCount(), equalTo((long) msgCount));
verify(inst, times(msgCount)).sendToRequestTemplate(any(), any(), any(), any());
}
@Test
public void givenMessagesOverMaxPendingRequests_whenSend_thenImmediateFailedFutureForTheOfRequests() {
willDoNothing().given(inst).sendToRequestTemplate(any(), any(), any(), any());
inst.init();
int msgOverflowCount = 10;
for (int i = 0; i < inst.maxPendingRequests; i++) {
assertThat(inst.send(getRequestMsgMock()).isDone(), is(false)); //SettableFuture future - pending only
}
for (int i = 0; i < msgOverflowCount; i++) {
assertThat("max pending requests overflow", inst.send(getRequestMsgMock()).isDone(), is(true)); //overflow, immediate failed future
}
assertThat(inst.pendingRequests.mappingCount(), equalTo(inst.maxPendingRequests));
verify(inst, times((int) inst.maxPendingRequests)).sendToRequestTemplate(any(), any(), any(), any());
}
@Test
public void givenNothing_whenSendAndFetchAndProcessResponsesWithTimeout_thenFail() {
//given
AtomicLong currentTime = new AtomicLong();
willAnswer(x -> {
log.info("currentTime={}", currentTime.get());
return currentTime.get();
}).given(inst).getCurrentClockNs();
inst.init();
inst.setupNextCleanup();
willReturn(Collections.emptyList()).given(inst).doPoll();
//when
long stepNs = TimeUnit.MILLISECONDS.toNanos(1);
for (long i = 0; i <= inst.maxRequestTimeoutNs * 2; i = i + stepNs) {
currentTime.addAndGet(stepNs);
assertThat(inst.send(getRequestMsgMock()).isDone(), is(false)); //SettableFuture future - pending only
if (i % (inst.maxRequestTimeoutNs * 3 / 2) == 0) {
inst.fetchAndProcessResponses();
}
}
//then
ArgumentCaptor<DefaultTbQueueRequestTemplate.ResponseMetaData> argumentCaptorResp = ArgumentCaptor.forClass(DefaultTbQueueRequestTemplate.ResponseMetaData.class);
ArgumentCaptor<UUID> argumentCaptorUUID = ArgumentCaptor.forClass(UUID.class);
ArgumentCaptor<Long> argumentCaptorLong = ArgumentCaptor.forClass(Long.class);
verify(inst, atLeastOnce()).setTimeoutException(argumentCaptorUUID.capture(), argumentCaptorResp.capture(), argumentCaptorLong.capture());
List<DefaultTbQueueRequestTemplate.ResponseMetaData> responseMetaDataList = argumentCaptorResp.getAllValues();
List<Long> tickTsList = argumentCaptorLong.getAllValues();
for (int i = 0; i < responseMetaDataList.size(); i++) {
assertThat("tickTs >= calculatedExpTime", tickTsList.get(i), greaterThanOrEqualTo(responseMetaDataList.get(i).getSubmitTime() + responseMetaDataList.get(i).getTimeout()));
}
}
TbQueueMsg getRequestMsgMock() {
return mock(TbQueueMsg.class, RETURNS_DEEP_STUBS);
}
}

37
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java

@ -17,12 +17,13 @@ package org.thingsboard.server.transport.coap;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.network.Exchange;
import org.eclipse.californium.core.observe.ObserveRelation;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.eclipse.californium.core.server.resources.Resource;
import org.eclipse.californium.core.server.resources.ResourceObserver;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.StringUtils;
@ -139,7 +140,7 @@ public class OtaPackageTransportResource extends AbstractCoapTransportResource {
Response response = new Response(CoAP.ResponseCode.CONTENT);
if (data != null && data.length > 0) {
response.setPayload(data);
response.getOptions().setAccept(MediaTypeRegistry.APPLICATION_OCTET_STREAM);
// response.getOptions().setAccept(MediaTypeRegistry.APPLICATION_OCTET_STREAM);
if (exchange.getRequestOptions().getBlock2() != null) {
int chunkSize = exchange.getRequestOptions().getBlock2().getSzx();
boolean lastFlag = data.length <= chunkSize;
@ -150,4 +151,36 @@ public class OtaPackageTransportResource extends AbstractCoapTransportResource {
}
}
public class CoapResourceObserver implements ResourceObserver {
@Override
public void changedName(String old) {
}
@Override
public void changedPath(String old) {
}
@Override
public void addedChild(Resource child) {
}
@Override
public void removedChild(Resource child) {
}
@Override
public void addedObserveRelation(ObserveRelation relation) {
}
@Override
public void removedObserveRelation(ObserveRelation relation) {
}
}
}

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java

@ -206,7 +206,7 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
}
private void sendWriteReplaceRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) {
RpcWriteUpdateRequest requestBody = JacksonUtil.fromString(requestMsg.getParams(), RpcWriteUpdateRequest.class);
RpcWriteReplaceRequest requestBody = JacksonUtil.fromString(requestMsg.getParams(), RpcWriteReplaceRequest.class);
TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(versionedId)
.value(requestBody.getValue())
.timeout(this.config.getTimeout()).build();

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/IdOrKeyRequest.java

@ -15,9 +15,11 @@
*/
package org.thingsboard.server.transport.lwm2m.server.rpc;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class IdOrKeyRequest {
private String key;

1
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2MUplinkMsgHandler.java

@ -256,6 +256,7 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl
LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint());
try {
log.warn("[{}] [{{}] Client: update after Registration", registration.getEndpoint(), registration.getId());
logService.log(lwM2MClient, String.format("[%s][%s] Updated registration.", registration.getId(), registration.getSocketAddress()));
clientContext.updateRegistration(lwM2MClient, registration);
TransportProtos.SessionInfoProto sessionInfo = lwM2MClient.getSession();
this.reportActivityAndRegister(sessionInfo);

4
common/util/pom.xml

@ -36,6 +36,10 @@
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>

57
common/util/src/main/java/org/thingsboard/common/util/TbStopWatch.java

@ -0,0 +1,57 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.common.util;
import org.springframework.util.StopWatch;
/**
* Utility method that extends Spring Framework StopWatch
* It is a MONOTONIC time stopwatch.
* It is a replacement for any measurements with a wall-clock like System.currentTimeMillis()
* It is not affected by leap second, day-light saving and wall-clock adjustments by manual or network time synchronization
* The main features is a single call for common use cases:
* - create and start: TbStopWatch sw = TbStopWatch.startNew()
* - stop and get: sw.stopAndGetTotalTimeMillis() or sw.stopAndGetLastTaskTimeMillis()
* */
public class TbStopWatch extends StopWatch {
public static TbStopWatch startNew(){
TbStopWatch stopWatch = new TbStopWatch();
stopWatch.start();
return stopWatch;
}
public long stopAndGetTotalTimeMillis(){
stop();
return getTotalTimeMillis();
}
public long stopAndGetTotalTimeNanos(){
stop();
return getLastTaskTimeNanos();
}
public long stopAndGetLastTaskTimeMillis(){
stop();
return getLastTaskTimeMillis();
}
public long stopAndGetLastTaskTimeNanos(){
stop();
return getLastTaskTimeNanos();
}
}

16
dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java

@ -21,7 +21,6 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
@ -32,6 +31,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.stats.DefaultCounter;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.cache.CacheExecutorService;
import org.thingsboard.server.dao.service.Validator;
import java.util.ArrayList;
@ -45,7 +45,6 @@ import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.CacheConstants.ATTRIBUTES_CACHE;
import static org.thingsboard.server.dao.attributes.AttributeUtils.validate;
@Service
@ -59,12 +58,15 @@ public class CachedAttributesService implements AttributesService {
private final AttributesCacheWrapper cacheWrapper;
private final DefaultCounter hitCounter;
private final DefaultCounter missCounter;
private final CacheExecutorService cacheExecutorService;
public CachedAttributesService(AttributesDao attributesDao,
AttributesCacheWrapper cacheWrapper,
StatsFactory statsFactory) {
StatsFactory statsFactory,
CacheExecutorService cacheExecutorService) {
this.attributesDao = attributesDao;
this.cacheWrapper = cacheWrapper;
this.cacheExecutorService = cacheExecutorService;
this.hitCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "hit");
this.missCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "miss");
@ -88,7 +90,7 @@ public class CachedAttributesService implements AttributesService {
// TODO: think if it's a good idea to store 'empty' attributes
cacheWrapper.put(attributeCacheKey, foundAttrKvEntry.orElse(null));
return foundAttrKvEntry;
}, MoreExecutors.directExecutor());
}, cacheExecutorService);
}
}
@ -111,7 +113,7 @@ public class CachedAttributesService implements AttributesService {
notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet());
ListenableFuture<List<AttributeKvEntry>> result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys);
return Futures.transform(result, foundInDbAttributes -> mergeDbAndCacheAttributes(entityId, scope, cachedAttributes, notFoundAttributeKeys, foundInDbAttributes), MoreExecutors.directExecutor());
return Futures.transform(result, foundInDbAttributes -> mergeDbAndCacheAttributes(entityId, scope, cachedAttributes, notFoundAttributeKeys, foundInDbAttributes), cacheExecutorService);
}
@ -169,7 +171,7 @@ public class CachedAttributesService implements AttributesService {
// TODO: can do if (attributesCache.get() != null) attributesCache.put() instead, but will be more twice more requests to cache
List<String> attributeKeys = attributes.stream().map(KvEntry::getKey).collect(Collectors.toList());
future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), MoreExecutors.directExecutor());
future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), cacheExecutorService);
return future;
}
@ -177,7 +179,7 @@ public class CachedAttributesService implements AttributesService {
public ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys) {
validate(entityId, scope);
ListenableFuture<List<Void>> future = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys);
future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), MoreExecutors.directExecutor());
future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), cacheExecutorService);
return future;
}

33
dao/src/main/java/org/thingsboard/server/dao/cache/CacheExecutorService.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.cache;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.AbstractListeningExecutor;
@Component
public class CacheExecutorService extends AbstractListeningExecutor {
@Value("${cache.maximumPoolSize}")
private int poolSize;
@Override
protected int getThreadPollSize() {
return poolSize;
}
}

1
dao/src/test/resources/application-test.properties

@ -10,6 +10,7 @@ audit-log.default_query_period=30
audit-log.sink.type=none
cache.type=caffeine
cache.maximumPoolSize=16
#cache.type=redis
caffeine.specs.relations.timeToLiveInMinutes=1440

30
msa/js-executor/api/httpServer.js

@ -0,0 +1,30 @@
/*
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const config = require('config'),
logger = require('../config/logger')._logger('httpServer'),
express = require('express');
const httpPort = Number(config.get('http_port'));
const app = express();
app.get('/livenessProbe', async (req, res) => {
const date = new Date();
const message = { now: date.toISOString() };
res.send(message);
})
app.listen(httpPort, () => logger.info(`Started http endpoint on port ${httpPort}. Please, use /livenessProbe !`))

99
msa/js-executor/api/jsInvokeMessageProcessor.js

@ -25,6 +25,7 @@ const config = require('config'),
Utils = require('./utils'),
JsExecutor = require('./jsExecutor');
const statFrequency = Number(config.get('script.stat_print_frequency'));
const scriptBodyTraceFrequency = Number(config.get('script.script_body_trace_frequency'));
const useSandbox = config.get('script.use_sandbox') === 'true';
const maxActiveScripts = Number(config.get('script.max_active_scripts'));
@ -34,15 +35,15 @@ const slowQueryLogBody = config.get('script.slow_query_log_body') === 'true';
const {performance} = require('perf_hooks');
function JsInvokeMessageProcessor(producer) {
console.log("Producer:", producer);
this.producer = producer;
this.executor = new JsExecutor(useSandbox);
this.scriptMap = {};
this.scriptMap = new Map();
this.scriptIds = [];
this.executedScriptsCounter = 0;
this.lastStatTime = performance.now();
}
JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) {
JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function (message) {
var tStart = performance.now();
let requestId;
let responseTopic;
@ -77,13 +78,13 @@ JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) {
var tFinish = performance.now();
var tTook = tFinish - tStart;
if ( tTook > slowQueryLogMs ) {
if (tTook > slowQueryLogMs) {
let functionName;
if (request.invokeRequest) {
try {
buf = Buffer.from(request.invokeRequest['functionName']);
functionName = buf.toString('utf8');
} catch (err){
} catch (err) {
logger.error('[%s] Failed to read functionName from message header: %s', requestId, err.message);
logger.error(err.stack);
}
@ -96,7 +97,7 @@ JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) {
}
JsInvokeMessageProcessor.prototype.processCompileRequest = function(requestId, responseTopic, headers, compileRequest) {
JsInvokeMessageProcessor.prototype.processCompileRequest = function (requestId, responseTopic, headers, compileRequest) {
var scriptId = getScriptId(compileRequest);
logger.debug('[%s] Processing compile request, scriptId: [%s]', requestId, scriptId);
@ -115,15 +116,20 @@ JsInvokeMessageProcessor.prototype.processCompileRequest = function(requestId, r
);
}
JsInvokeMessageProcessor.prototype.processInvokeRequest = function(requestId, responseTopic, headers, invokeRequest) {
JsInvokeMessageProcessor.prototype.processInvokeRequest = function (requestId, responseTopic, headers, invokeRequest) {
var scriptId = getScriptId(invokeRequest);
logger.debug('[%s] Processing invoke request, scriptId: [%s]', requestId, scriptId);
this.executedScriptsCounter++;
if ( this.executedScriptsCounter >= scriptBodyTraceFrequency ) {
this.executedScriptsCounter = 0;
if (logger.levels[logger.level] >= logger.levels['debug']) {
logger.debug('[%s] Executing script body: [%s]', scriptId, invokeRequest.scriptBody);
}
if (this.executedScriptsCounter % statFrequency == 0) {
const nowMs = performance.now();
const msSinceLastStat = nowMs - this.lastStatTime;
const requestsPerSec = msSinceLastStat == 0 ? statFrequency : statFrequency / msSinceLastStat * 1000;
this.lastStatTime = nowMs;
logger.info('STAT[%s]: requests [%s], took [%s]ms, request/s [%s]', this.executedScriptsCounter, statFrequency, msSinceLastStat, requestsPerSec);
}
if (this.executedScriptsCounter % scriptBodyTraceFrequency == 0) {
logger.info('[%s] Executing script body: [%s]', scriptId, invokeRequest.scriptBody);
}
this.getOrCompileScript(scriptId, invokeRequest.scriptBody).then(
(script) => {
@ -154,15 +160,15 @@ JsInvokeMessageProcessor.prototype.processInvokeRequest = function(requestId, re
);
}
JsInvokeMessageProcessor.prototype.processReleaseRequest = function(requestId, responseTopic, headers, releaseRequest) {
JsInvokeMessageProcessor.prototype.processReleaseRequest = function (requestId, responseTopic, headers, releaseRequest) {
var scriptId = getScriptId(releaseRequest);
logger.debug('[%s] Processing release request, scriptId: [%s]', requestId, scriptId);
if (this.scriptMap[scriptId]) {
if (this.scriptMap.has(scriptId)) {
var index = this.scriptIds.indexOf(scriptId);
if (index > -1) {
this.scriptIds.splice(index, 1);
}
delete this.scriptMap[scriptId];
this.scriptMap.delete(scriptId);
}
var releaseResponse = createReleaseResponse(scriptId, true);
logger.debug('[%s] Sending success release response, scriptId: [%s]', requestId, scriptId);
@ -173,6 +179,7 @@ JsInvokeMessageProcessor.prototype.sendResponse = function (requestId, responseT
var tStartSending = performance.now();
var remoteResponse = createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse);
var rawResponse = Buffer.from(JSON.stringify(remoteResponse), 'utf8');
logger.debug('[%s] Sending response to queue, scriptId: [%s]', requestId, scriptId);
this.producer.send(responseTopic, scriptId, rawResponse, headers).then(
() => {
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
);
}
JsInvokeMessageProcessor.prototype.getOrCompileScript = function(scriptId, scriptBody) {
JsInvokeMessageProcessor.prototype.getOrCompileScript = function (scriptId, scriptBody) {
var self = this;
return new Promise(function(resolve, reject) {
if (self.scriptMap[scriptId]) {
resolve(self.scriptMap[scriptId]);
return new Promise(function (resolve, reject) {
const script = self.scriptMap.get(scriptId);
if (script) {
resolve(script);
} else {
self.executor.compileScript(scriptBody).then(
(script) => {
self.cacheScript(scriptId, script);
resolve(script);
(compiledScript) => {
self.cacheScript(scriptId, compiledScript);
resolve(compiledScript);
},
(err) => {
reject(err);
@ -205,56 +213,57 @@ JsInvokeMessageProcessor.prototype.getOrCompileScript = function(scriptId, scrip
});
}
JsInvokeMessageProcessor.prototype.cacheScript = function(scriptId, script) {
if (!this.scriptMap[scriptId]) {
JsInvokeMessageProcessor.prototype.cacheScript = function (scriptId, script) {
if (!this.scriptMap.has(scriptId)) {
this.scriptIds.push(scriptId);
while (this.scriptIds.length > maxActiveScripts) {
logger.info('Active scripts count [%s] exceeds maximum limit [%s]', this.scriptIds.length, maxActiveScripts);
const prevScriptId = this.scriptIds.shift();
logger.info('Removing active script with id [%s]', prevScriptId);
delete this.scriptMap[prevScriptId];
this.scriptMap.delete(prevScriptId);
}
}
this.scriptMap[scriptId] = script;
this.scriptMap.set(scriptId, script);
logger.info("scriptMap size is [%s]", this.scriptMap.size);
}
function createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse) {
const requestIdBits = Utils.UUIDToBits(requestId);
return {
requestIdMSB: requestIdBits[0],
requestIdLSB: requestIdBits[1],
compileResponse: compileResponse,
invokeResponse: invokeResponse,
releaseResponse: releaseResponse
requestIdMSB: requestIdBits[0],
requestIdLSB: requestIdBits[1],
compileResponse: compileResponse,
invokeResponse: invokeResponse,
releaseResponse: releaseResponse
};
}
function createCompileResponse(scriptId, success, errorCode, err) {
const scriptIdBits = Utils.UUIDToBits(scriptId);
return {
errorCode: errorCode,
success: success,
errorDetails: parseJsErrorDetails(err),
scriptIdMSB: scriptIdBits[0],
scriptIdLSB: scriptIdBits[1]
return {
errorCode: errorCode,
success: success,
errorDetails: parseJsErrorDetails(err),
scriptIdMSB: scriptIdBits[0],
scriptIdLSB: scriptIdBits[1]
};
}
function createInvokeResponse(result, success, errorCode, err) {
return {
errorCode: errorCode,
success: success,
errorDetails: parseJsErrorDetails(err),
result: result
return {
errorCode: errorCode,
success: success,
errorDetails: parseJsErrorDetails(err),
result: result
};
}
function createReleaseResponse(scriptId, success) {
const scriptIdBits = Utils.UUIDToBits(scriptId);
return {
success: success,
scriptIdMSB: scriptIdBits[0],
scriptIdLSB: scriptIdBits[1]
success: success,
scriptIdMSB: scriptIdBits[0],
scriptIdLSB: scriptIdBits[1]
};
}

5
msa/js-executor/config/custom-environment-variables.yml

@ -16,6 +16,7 @@
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)
request_topic: "REMOTE_JS_EVAL_REQUEST_TOPIC"
http_port: "HTTP_PORT" # /livenessProbe
js:
response_poll_interval: "REMOTE_JS_RESPONSE_POLL_INTERVAL_MS"
@ -26,6 +27,9 @@ kafka:
servers: "TB_KAFKA_SERVERS"
replication_factor: "TB_QUEUE_KAFKA_REPLICATION_FACTOR"
acks: "TB_KAFKA_ACKS" # -1 = all; 0 = no acknowledgments; 1 = only waits for the leader to acknowledge
batch_size: "TB_KAFKA_BATCH_SIZE" # for producer
linger_ms: "TB_KAFKA_LINGER_MS" # for producer
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
requestTimeout: "TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS"
compression: "TB_QUEUE_KAFKA_COMPRESSION" # gzip or uncompressed
topic_properties: "TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES"
@ -70,6 +74,7 @@ logger:
script:
use_sandbox: "SCRIPT_USE_SANDBOX"
stat_print_frequency: "SCRIPT_STAT_PRINT_FREQUENCY"
script_body_trace_frequency: "SCRIPT_BODY_TRACE_FREQUENCY"
max_active_scripts: "MAX_ACTIVE_SCRIPTS"
slow_query_log_ms: "SLOW_QUERY_LOG_MS" #1.123456

9
msa/js-executor/config/default.yml

@ -16,6 +16,7 @@
queue_type: "kafka"
request_topic: "js_eval.requests"
http_port: "8888" # /livenessProbe
js:
response_poll_interval: "25"
@ -26,6 +27,9 @@ kafka:
servers: "localhost:9092"
replication_factor: "1"
acks: "1" # -1 = all; 0 = no acknowledgments; 1 = only waits for the leader to acknowledge
batch_size: "128" # for producer
linger_ms: "1" # for producer
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
requestTimeout: "30000" # The default value in kafkajs is: 30000
compression: "gzip" # gzip or uncompressed
topic_properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100;min.insync.replicas:1"
@ -59,7 +63,8 @@ logger:
script:
use_sandbox: "true"
script_body_trace_frequency: "1000"
script_body_trace_frequency: "10000"
stat_print_frequency: "10000"
max_active_scripts: "1000"
slow_query_log_ms: "1.000000" #millis
slow_query_log_ms: "5.000000" #millis
slow_query_log_body: "false"

1
msa/js-executor/package.json

@ -18,6 +18,7 @@
"aws-sdk": "^2.741.0",
"azure-sb": "^0.11.1",
"config": "^3.3.1",
"express": "^4.17.1",
"js-yaml": "^3.14.0",
"kafkajs": "^1.15.0",
"long": "^4.0.0",

110
msa/js-executor/queue/kafkaTemplate.js

@ -23,8 +23,11 @@ const replicationFactor = Number(config.get('kafka.replication_factor'));
const topicProperties = config.get('kafka.topic_properties');
const kafkaClientId = config.get('kafka.client_id');
const acks = Number(config.get('kafka.acks'));
const maxBatchSize = Number(config.get('kafka.batch_size'));
const linger = Number(config.get('kafka.linger_ms'));
const requestTimeout = Number(config.get('kafka.requestTimeout'));
const compressionType = (config.get('kafka.compression') === "gzip") ? CompressionTypes.GZIP : CompressionTypes.None;
const partitionsConsumedConcurrently = Number(config.get('kafka.partitions_consumed_concurrently'));
let kafkaClient;
let kafkaAdmin;
@ -33,22 +36,65 @@ let producer;
const configEntries = [];
let batchMessages = [];
let sendLoopInstance;
function KafkaProducer() {
this.send = async (responseTopic, scriptId, rawResponse, headers) => {
return producer.send(
{
topic: responseTopic,
logger.debug('Pending queue response, scriptId: [%s]', scriptId);
const message = {
topic: responseTopic,
messages: [{
key: scriptId,
value: rawResponse,
headers: headers.data
}]
};
await pushMessageToSendLater(message);
}
}
async function pushMessageToSendLater(message) {
batchMessages.push(message);
if (batchMessages.length >= maxBatchSize) {
await sendMessagesAsBatch(true);
}
}
function sendLoopWithLinger() {
if (sendLoopInstance) {
clearTimeout(sendLoopInstance);
} else {
logger.debug("Starting new send loop with linger [%s]", linger)
}
sendLoopInstance = setTimeout(sendMessagesAsBatch, linger);
}
async function sendMessagesAsBatch(isImmediately) {
if (sendLoopInstance) {
logger.debug("sendMessagesAsBatch: Clear sendLoop scheduler. Starting new send loop with linger [%s]", linger);
clearTimeout(sendLoopInstance);
}
sendLoopInstance = null;
if (batchMessages.length > 0) {
logger.debug('sendMessagesAsBatch, length: [%s], %s', batchMessages.length, isImmediately ? 'immediately' : '');
const messagesToSend = batchMessages;
batchMessages = [];
try {
await producer.sendBatch({
topicMessages: messagesToSend,
acks: acks,
compression: compressionType,
messages: [
{
key: scriptId,
value: rawResponse,
headers: headers.data
}
]
});
compression: compressionType
})
logger.debug('Response batch sent to kafka, length: [%s]', messagesToSend.length);
} catch(err) {
logger.error('Failed batch send to kafka, length: [%s], pending to reprocess msgs', messagesToSend.length);
logger.error(err.stack);
batchMessages = messagesToSend.concat(batchMessages);
}
}
sendLoopWithLinger();
}
(async () => {
@ -64,8 +110,8 @@ function KafkaProducer() {
let kafkaConfig = {
brokers: kafkaBootstrapServers.split(','),
logLevel: logLevel.INFO,
logCreator: KafkaJsWinstonLogCreator
logLevel: logLevel.INFO,
logCreator: KafkaJsWinstonLogCreator
};
if (kafkaClientId) {
@ -114,14 +160,45 @@ function KafkaProducer() {
consumer = kafkaClient.consumer({groupId: 'js-executor-group'});
producer = kafkaClient.producer();
/*
//producer event instrumentation to debug
const { CONNECT } = producer.events;
const removeListenerC = producer.on(CONNECT, e => logger.info(`producer CONNECT`));
const { DISCONNECT } = producer.events;
const removeListenerD = producer.on(DISCONNECT, e => logger.info(`producer DISCONNECT`));
const { REQUEST } = producer.events;
const removeListenerR = producer.on(REQUEST, e => logger.info(`producer REQUEST ${e.payload.broker}`));
const { REQUEST_TIMEOUT } = producer.events;
const removeListenerRT = producer.on(REQUEST_TIMEOUT, e => logger.info(`producer REQUEST_TIMEOUT ${e.payload.broker}`));
const { REQUEST_QUEUE_SIZE } = producer.events;
const removeListenerRQS = producer.on(REQUEST_QUEUE_SIZE, e => logger.info(`producer REQUEST_QUEUE_SIZE ${e.payload.broker} size ${e.queueSize}`));
*/
/*
//consumer event instrumentation to debug
const removeListeners = {}
const { FETCH_START } = consumer.events;
removeListeners[FETCH_START] = consumer.on(FETCH_START, e => logger.info(`consumer FETCH_START`));
const { FETCH } = consumer.events;
removeListeners[FETCH] = consumer.on(FETCH, e => logger.info(`consumer FETCH numberOfBatches ${e.payload.numberOfBatches} duration ${e.payload.duration}`));
const { START_BATCH_PROCESS } = consumer.events;
removeListeners[START_BATCH_PROCESS] = consumer.on(START_BATCH_PROCESS, e => logger.info(`consumer START_BATCH_PROCESS topic ${e.payload.topic} batchSize ${e.payload.batchSize}`));
const { END_BATCH_PROCESS } = consumer.events;
removeListeners[END_BATCH_PROCESS] = consumer.on(END_BATCH_PROCESS, e => logger.info(`consumer END_BATCH_PROCESS topic ${e.payload.topic} batchSize ${e.payload.batchSize}`));
const { COMMIT_OFFSETS } = consumer.events;
removeListeners[COMMIT_OFFSETS] = consumer.on(COMMIT_OFFSETS, e => logger.info(`consumer COMMIT_OFFSETS topics ${e.payload.topics}`));
*/
const messageProcessor = new JsInvokeMessageProcessor(new KafkaProducer());
await consumer.connect();
await producer.connect();
sendLoopWithLinger();
await consumer.subscribe({topic: requestTopic});
logger.info('Started ThingsBoard JavaScript Executor Microservice.');
await consumer.run({
//partitionsConsumedConcurrently: 1, // Default: 1
partitionsConsumedConcurrently: partitionsConsumedConcurrently,
eachMessage: async ({topic, partition, message}) => {
let headers = message.headers;
let key = message.key;
@ -197,6 +274,9 @@ async function disconnectProducer() {
var _producer = producer;
producer = null;
try {
logger.info('Stopping loop...');
clearTimeout(sendLoopInstance);
await sendMessagesAsBatch();
await _producer.disconnect();
logger.info('Kafka Producer stopped.');
} catch (e) {

2
msa/js-executor/server.js

@ -51,3 +51,5 @@ switch (serviceType) {
process.exit(-1);
}
require('./api/httpServer');

361
msa/js-executor/yarn.lock

@ -418,6 +418,14 @@ abort-controller@^3.0.0:
dependencies:
event-target-shim "^5.0.0"
accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
dependencies:
mime-types "~2.1.24"
negotiator "0.6.2"
agent-base@6:
version "6.0.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4"
@ -487,6 +495,11 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
@ -621,6 +634,22 @@ bluebird@^3.5.2:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
body-parser@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
dependencies:
bytes "3.1.0"
content-type "~1.0.4"
debug "2.6.9"
depd "~1.1.2"
http-errors "1.7.2"
iconv-lite "0.4.24"
on-finished "~2.3.0"
qs "6.7.0"
raw-body "2.4.0"
type-is "~1.6.17"
boxen@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64"
@ -682,6 +711,11 @@ byline@^5.0.0:
resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1"
integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
cacheable-request@^6.0.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912"
@ -838,6 +872,28 @@ configstore@^5.0.1:
write-file-atomic "^3.0.0"
xdg-basedir "^4.0.0"
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
dependencies:
safe-buffer "5.1.2"
content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
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:
dependencies:
ms "^2.1.1"
debug@2.6.9, debug@^2.2.0, debug@~2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
debug@4, debug@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
@ -874,13 +937,6 @@ debug@4, debug@^4.1.1:
dependencies:
ms "^2.1.1"
debug@^2.2.0, debug@~2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@ -913,6 +969,16 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
dir-glob@^3.0.1:
version "3.0.1"
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:
dependencies:
safe-buffer "^5.0.1"
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
emoji-regex@^7.0.1:
version "7.0.3"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
@ -977,6 +1048,11 @@ enabled@2.0.x:
resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
end-of-stream@^1.0.0, end-of-stream@^1.1.0:
version "1.4.4"
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:
resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
escodegen@^1.14.1:
version "1.14.3"
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
@ -1021,6 +1102,11 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
event-target-shim@^5.0.0:
version "5.0.1"
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:
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
dependencies:
accepts "~1.3.7"
array-flatten "1.1.1"
body-parser "1.19.0"
content-disposition "0.5.3"
content-type "~1.0.4"
cookie "0.4.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~1.1.2"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "~1.1.2"
fresh "0.5.2"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.3"
path-to-regexp "0.1.7"
proxy-addr "~2.0.5"
qs "6.7.0"
range-parser "~1.2.1"
safe-buffer "5.1.2"
send "0.17.1"
serve-static "1.14.1"
setprototypeof "1.1.1"
statuses "~1.5.0"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
extend@^3.0.2, extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
@ -1119,6 +1241,19 @@ fill-range@^7.0.1:
dependencies:
to-regex-range "^5.0.1"
finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "~2.3.0"
parseurl "~1.3.3"
statuses "~1.5.0"
unpipe "~1.0.0"
find-up@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
@ -1155,6 +1290,16 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
forwarded@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
from2@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
@ -1384,6 +1529,28 @@ http-cache-semantics@^4.0.0:
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
http-errors@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-errors@~1.7.2:
version "1.7.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
dependencies:
depd "~1.1.2"
inherits "2.0.4"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-signature@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
@ -1401,6 +1568,13 @@ https-proxy-agent@^5.0.0:
agent-base "6"
debug "4"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
ieee754@1.1.13, ieee754@^1.1.4:
version "1.1.13"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
@ -1431,7 +1605,7 @@ inherits@2.0.3:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@ -1449,6 +1623,11 @@ into-stream@^5.1.1:
from2 "^2.3.0"
p-is-promise "^3.0.0"
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
is-arrayish@^0.3.1:
version "0.3.2"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
@ -1764,11 +1943,26 @@ make-dir@^3.0.0:
dependencies:
semver "^6.0.0"
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
merge2@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
micromatch@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
@ -1782,6 +1976,11 @@ mime-db@1.44.0:
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
mime-db@1.48.0:
version "1.48.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d"
integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==
mime-types@^2.1.12, mime-types@~2.1.19:
version "2.1.27"
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:
dependencies:
mime-db "1.44.0"
mime-types@~2.1.24:
version "2.1.31"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b"
integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==
dependencies:
mime-db "1.48.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mime@^2.2.0:
version "2.4.6"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
@ -1833,6 +2044,11 @@ ms@2.0.0:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
ms@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@ -1846,6 +2062,11 @@ multistream@^2.1.1:
inherits "^2.0.1"
readable-stream "^2.0.5"
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
node-fetch@^2.3.0, node-fetch@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
@ -1899,6 +2120,13 @@ object-hash@^2.0.1:
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea"
integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
dependencies:
ee-first "1.1.1"
once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@ -1974,6 +2202,11 @@ package-json@^6.3.0:
registry-url "^5.0.0"
semver "^6.2.0"
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@ -1984,6 +2217,11 @@ path-parse@^1.0.6:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-type@^4.0.0:
version "4.0.0"
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:
"@types/node" "^13.7.0"
long "^4.0.0"
proxy-addr@~2.0.5:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
dependencies:
forwarded "0.2.0"
ipaddr.js "1.9.1"
psl@^1.1.28, psl@^1.1.33:
version "1.8.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
@ -2114,6 +2360,11 @@ pupa@^2.0.1:
dependencies:
escape-goat "^2.0.0"
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@ -2129,6 +2380,21 @@ querystringify@^2.1.1:
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
dependencies:
bytes "3.1.0"
http-errors "1.7.2"
iconv-lite "0.4.24"
unpipe "1.0.0"
rc@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
@ -2292,17 +2558,17 @@ run-parallel@^1.1.9:
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@ -2339,11 +2605,45 @@ semver@^7.1.3:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
dependencies:
debug "2.6.9"
depd "~1.1.2"
destroy "~1.0.4"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "~1.7.2"
mime "1.6.0"
ms "2.1.1"
on-finished "~2.3.0"
range-parser "~1.2.1"
statuses "~1.5.0"
serve-static@1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.17.1"
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
setprototypeof@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
signal-exit@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
@ -2391,6 +2691,11 @@ stack-trace@0.0.x:
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
stream-browserify@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"
@ -2513,6 +2818,11 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
touch@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
@ -2581,6 +2891,14 @@ type-fest@^0.8.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.24"
typedarray-to-buffer@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
@ -2636,6 +2954,11 @@ universalify@^1.0.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d"
integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
update-notifier@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.1.tgz#895fc8562bbe666179500f9f2cebac4f26323746"
@ -2705,6 +3028,11 @@ util@^0.11.1:
dependencies:
inherits "2.0.3"
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid-parse@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b"
@ -2735,6 +3063,11 @@ validator@^9.4.1:
resolved "https://registry.yarnpkg.com/validator/-/validator-9.4.1.tgz#abf466d398b561cd243050112c6ff1de6cc12663"
integrity sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
verror@1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"

5
pom.xml

@ -1009,6 +1009,11 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>

2
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -1257,7 +1257,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
params.put("deviceProfileId", deviceProfileId.getId().toString());
return restTemplate.exchange(
baseURL + "/api/devices/count/{otaPackageType}?deviceProfileId={deviceProfileId}",
baseURL + "/api/devices/count/{otaPackageType}/{deviceProfileId}",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<Long>() {

15
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java

@ -19,29 +19,22 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.msg.TbMsg;
import javax.script.ScriptException;
import java.util.List;
import java.util.Set;
public interface ScriptEngine {
List<TbMsg> executeUpdate(TbMsg msg) throws ScriptException;
ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg);
TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException;
boolean executeFilter(TbMsg msg) throws ScriptException;
ListenableFuture<TbMsg> executeGenerateAsync(TbMsg prevMsg);
ListenableFuture<Boolean> executeFilterAsync(TbMsg msg);
Set<String> executeSwitch(TbMsg msg) throws ScriptException;
JsonNode executeJson(TbMsg msg) throws ScriptException;
ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg);
ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) throws ScriptException;
ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg);
String executeToString(TbMsg msg) throws ScriptException;
ListenableFuture<String> executeToStringAsync(TbMsg msg);
void destroy();

4
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -214,6 +214,10 @@ public interface TbContext {
EdgeEventService getEdgeEventService();
/**
* Js script executors call are completely asynchronous
* */
@Deprecated
ListeningExecutor getJsExecutor();
ListeningExecutor getMailExecutor();

30
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java

@ -15,8 +15,11 @@
*/
package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ListeningExecutor;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.rule.engine.api.TbContext;
@ -55,18 +58,21 @@ public class TbLogNode implements TbNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
ListeningExecutor jsExecutor = ctx.getJsExecutor();
ctx.logJsEvalRequest();
withCallback(jsExecutor.executeAsync(() -> jsEngine.executeToString(msg)),
toString -> {
ctx.logJsEvalResponse();
log.info(toString);
ctx.tellSuccess(msg);
},
t -> {
ctx.logJsEvalResponse();
ctx.tellFailure(msg, t);
});
Futures.addCallback(jsEngine.executeToStringAsync(msg), new FutureCallback<String>() {
@Override
public void onSuccess(@Nullable String result) {
ctx.logJsEvalResponse();
log.info(result);
ctx.tellSuccess(msg);
}
@Override
public void onFailure(Throwable t) {
ctx.logJsEvalResponse();
ctx.tellFailure(msg, t);
}
}, MoreExecutors.directExecutor()); //usually js responses runs on js callback executor
}
@Override

50
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java

@ -15,9 +15,12 @@
*/
package org.thingsboard.rule.engine.debug;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.thingsboard.common.util.TbStopWatch;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.rule.engine.api.TbContext;
@ -35,6 +38,7 @@ import org.thingsboard.server.common.msg.queue.ServiceQueue;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
@ -64,10 +68,11 @@ public class TbMsgGeneratorNode implements TbNode {
private EntityId originatorId;
private UUID nextTickId;
private TbMsg prevMsg;
private volatile boolean initialized;
private final AtomicBoolean initialized = new AtomicBoolean(false);
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
log.trace("init generator with config {}", configuration);
this.config = TbNodeUtils.convert(configuration, TbMsgGeneratorNodeConfiguration.class);
this.delay = TimeUnit.SECONDS.toMillis(config.getPeriodInSeconds());
this.currentMsgCount = 0;
@ -81,35 +86,39 @@ public class TbMsgGeneratorNode implements TbNode {
@Override
public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) {
log.trace("onPartitionChangeMsg, PartitionChangeMsg {}, config {}", msg, config);
updateGeneratorState(ctx);
}
private void updateGeneratorState(TbContext ctx) {
log.trace("updateGeneratorState, config {}", config);
if (ctx.isLocalEntity(originatorId)) {
if (!initialized) {
initialized = true;
if (initialized.compareAndSet(false, true)) {
this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType");
scheduleTickMsg(ctx);
}
} else if (initialized) {
initialized = false;
} else if (initialized.compareAndSet(true, false)) {
destroy();
}
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
if (initialized && msg.getType().equals(TB_MSG_GENERATOR_NODE_MSG) && msg.getId().equals(nextTickId)) {
log.trace("onMsg, config {}, msg {}", config, msg);
if (initialized.get() && msg.getType().equals(TB_MSG_GENERATOR_NODE_MSG) && msg.getId().equals(nextTickId)) {
TbStopWatch sw = TbStopWatch.startNew();
withCallback(generate(ctx, msg),
m -> {
if (initialized && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) {
log.trace("onMsg onSuccess callback, took {}ms, config {}, msg {}", sw.stopAndGetTotalTimeMillis(), config, msg);
if (initialized.get() && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) {
ctx.enqueueForTellNext(m, SUCCESS);
scheduleTickMsg(ctx);
currentMsgCount++;
}
},
t -> {
if (initialized && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) {
log.warn("onMsg onFailure callback, took {}ms, config {}, msg {}, exception {}", sw.stopAndGetTotalTimeMillis(), config, msg, t);
if (initialized.get() && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) {
ctx.tellFailure(msg, t);
scheduleTickMsg(ctx);
currentMsgCount++;
@ -119,6 +128,7 @@ public class TbMsgGeneratorNode implements TbNode {
}
private void scheduleTickMsg(TbContext ctx) {
log.trace("scheduleTickMsg, config {}", config);
long curTs = System.currentTimeMillis();
if (lastScheduledTs == 0L) {
lastScheduledTs = curTs;
@ -131,22 +141,26 @@ public class TbMsgGeneratorNode implements TbNode {
}
private ListenableFuture<TbMsg> generate(TbContext ctx, TbMsg msg) {
return ctx.getJsExecutor().executeAsync(() -> {
if (prevMsg == null) {
prevMsg = ctx.newMsg(ServiceQueue.MAIN, "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}");
}
if (initialized) {
ctx.logJsEvalRequest();
TbMsg generated = jsEngine.executeGenerate(prevMsg);
log.trace("generate, config {}", config);
if (prevMsg == null) {
prevMsg = ctx.newMsg(ServiceQueue.MAIN, "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}");
}
if (initialized.get()) {
ctx.logJsEvalRequest();
return Futures.transformAsync(jsEngine.executeGenerateAsync(prevMsg), generated -> {
log.trace("generate process response, generated {}, config {}", generated, config);
ctx.logJsEvalResponse();
prevMsg = ctx.newMsg(ServiceQueue.MAIN, generated.getType(), originatorId, msg.getCustomerId(), generated.getMetaData(), generated.getData());
}
return prevMsg;
});
return Futures.immediateFuture(prevMsg);
}, MoreExecutors.directExecutor()); //usually it runs on js-executor-remote-callback thread pool
}
return Futures.immediateFuture(prevMsg);
}
@Override
public void destroy() {
log.trace("destroy, config {}", config);
prevMsg = null;
if (jsEngine != null) {
jsEngine.destroy();

29
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java

@ -15,7 +15,11 @@
*/
package org.thingsboard.rule.engine.filter;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.ScriptEngine;
@ -29,8 +33,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.Set;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
@ -58,17 +60,20 @@ public class TbJsSwitchNode implements TbNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
ListeningExecutor jsExecutor = ctx.getJsExecutor();
ctx.logJsEvalRequest();
withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(msg)),
result -> {
ctx.logJsEvalResponse();
processSwitch(ctx, msg, result);
},
t -> {
ctx.logJsEvalFailure();
ctx.tellFailure(msg, t);
}, ctx.getDbCallbackExecutor());
Futures.addCallback(jsEngine.executeSwitchAsync(msg), new FutureCallback<Set<String>>() {
@Override
public void onSuccess(@Nullable Set<String> result) {
ctx.logJsEvalResponse();
processSwitch(ctx, msg, result);
}
@Override
public void onFailure(Throwable t) {
ctx.logJsEvalFailure();
ctx.tellFailure(msg, t);
}
}, MoreExecutors.directExecutor()); //usually runs in a callbackExecutor
}
private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) {

19
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java

@ -64,7 +64,7 @@ public class TbJsSwitchNodeTest {
private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased());
@Test
public void multipleRoutesAreAllowed() throws TbNodeException, ScriptException {
public void multipleRoutesAreAllowed() throws TbNodeException {
initWithScript();
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10");
@ -72,11 +72,9 @@ public class TbJsSwitchNodeTest {
String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId);
mockJsExecutor();
when(scriptEngine.executeSwitch(msg)).thenReturn(Sets.newHashSet("one", "three"));
when(scriptEngine.executeSwitchAsync(msg)).thenReturn(Futures.immediateFuture(Sets.newHashSet("one", "three")));
node.onMsg(ctx, msg);
verify(ctx).getJsExecutor();
verify(ctx).tellNext(msg, Sets.newHashSet("one", "three"));
}
@ -92,19 +90,6 @@ public class TbJsSwitchNodeTest {
node.init(ctx, nodeConfiguration);
}
@SuppressWarnings("unchecked")
private void mockJsExecutor() {
when(ctx.getJsExecutor()).thenReturn(executor);
doAnswer((Answer<ListenableFuture<Set<String>>>) invocationOnMock -> {
try {
Callable task = (Callable) (invocationOnMock.getArguments())[0];
return Futures.immediateFuture((Set<String>) task.call());
} catch (Throwable th) {
return Futures.immediateFailedFuture(th);
}
}).when(executor).executeAsync(ArgumentMatchers.any(Callable.class));
}
private void verifyError(TbMsg msg, String message, Class expectedClass) {
ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
verify(ctx).tellFailure(same(msg), captor.capture());

2
ui-ngx/src/app/core/http/ota-package.service.ts

@ -133,7 +133,7 @@ export class OtaPackageService {
}
public countUpdateDeviceAfterChangePackage(type: OtaUpdateType, entityId: EntityId, config?: RequestConfig): Observable<number> {
return this.http.get<number>(`/api/devices/count/${type}?deviceProfileId=${entityId.id}`, defaultHttpOptionsFromConfig(config));
return this.http.get<number>(`/api/devices/count/${type}/${entityId.id}`, defaultHttpOptionsFromConfig(config));
}
public confirmDialogUpdatePackage(entity: BaseData<EntityId>&OtaPagesIds,

32
ui-ngx/src/app/core/services/menu.service.ts

@ -291,26 +291,26 @@ export class MenuService {
);
if (authState.edgesSupportEnabled) {
sections.push(
{
id: guid(),
name: 'edge.edge-instances',
type: 'link',
path: '/edgeInstances',
icon: 'router'
},
{
id: guid(),
name: 'edge.management',
type: 'toggle',
path: '/edges',
height: '80px',
icon: 'router',
path: '/edgeManagement',
height: '40px',
icon: 'settings_input_antenna',
pages: [
{
id: guid(),
name: 'edge.edge-instances',
type: 'link',
path: '/edges',
icon: 'router'
},
{
id: guid(),
name: 'edge.rulechain-templates',
type: 'link',
path: '/edges/ruleChains',
path: '/edgeManagement/ruleChains',
icon: 'settings_ethernet'
}
]
@ -448,12 +448,12 @@ export class MenuService {
{
name: 'edge.edge-instances',
icon: 'router',
path: '/edges'
path: '/edgeInstances'
},
{
name: 'edge.rulechain-templates',
icon: 'settings_ethernet',
path: '/edges/ruleChains'
path: '/edgeManagement/ruleChains'
}
]
}
@ -548,7 +548,7 @@ export class MenuService {
id: guid(),
name: 'edge.edge-instances',
type: 'link',
path: '/edges',
path: '/edgeInstances',
icon: 'router'
}
);
@ -606,8 +606,8 @@ export class MenuService {
places: [
{
name: 'edge.edge-instances',
icon: 'router',
path: '/edges'
icon: 'settings_input_antenna',
path: '/edgeInstances'
}
]
}

19
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -128,6 +128,7 @@ import {
DashboardImageDialogComponent,
DashboardImageDialogData, DashboardImageDialogResult
} from '@home/components/dashboard-page/dashboard-image-dialog.component';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
// @dynamic
@Component({
@ -211,7 +212,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
addingLayoutCtx: DashboardPageLayoutContext;
logo = 'assets/logo_title_white.svg';
private dashboardLogoCache: SafeUrl;
private defaultDashboardLogo = 'assets/logo_title_white.svg';
dashboardCtx: DashboardContext = {
instanceId: this.utils.guid(),
@ -312,7 +314,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private ngZone: NgZone,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef) {
private cd: ChangeDetectorRef,
private sanitizer: DomSanitizer) {
super(store);
}
@ -413,6 +416,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private reset() {
this.dashboard = null;
this.dashboardConfiguration = null;
this.dashboardLogoCache = undefined;
this.prevDashboard = null;
this.widgetEditMode = false;
@ -570,8 +574,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
}
public get dashboardLogo(): string {
return this.dashboard.configuration.settings.dashboardLogoUrl || this.logo;
public get dashboardLogo(): SafeUrl {
if (!this.dashboardLogoCache) {
const logo = this.dashboard.configuration.settings.dashboardLogoUrl || this.defaultDashboardLogo;
this.dashboardLogoCache = this.sanitizer.bypassSecurityTrustUrl(logo);
}
return this.dashboardLogoCache;
}
public showRightLayoutSwitch(): boolean {
@ -702,6 +710,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}).afterClosed().subscribe((data) => {
if (data) {
this.dashboard.configuration.settings = data.settings;
this.dashboardLogoCache = undefined;
const newGridSettings = data.gridSettings;
if (newGridSettings) {
const layout = this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout];
@ -855,11 +864,13 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if (this.widgetEditMode) {
if (revert) {
this.dashboard = this.prevDashboard;
this.dashboardLogoCache = undefined;
}
} else {
this.resetHighlight();
if (revert) {
this.dashboard = this.prevDashboard;
this.dashboardLogoCache = undefined;
this.dashboardConfiguration = this.dashboard.configuration;
this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
this.entityAliasesUpdated();

5
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -99,7 +99,6 @@ import { DeviceProfileDialogComponent } from '@home/components/profile/device-pr
import { DeviceProfileAutocompleteComponent } from '@home/components/profile/device-profile-autocomplete.component';
import { MqttDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/mqtt-device-profile-transport-configuration.component';
import { CoapDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/coap-device-profile-transport-configuration.component';
import { SnmpDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/snmp-device-profile-transport-configuration.component';
import { DeviceProfileAlarmsComponent } from '@home/components/profile/alarm/device-profile-alarms.component';
import { DeviceProfileAlarmComponent } from '@home/components/profile/alarm/device-profile-alarm.component';
import { CreateAlarmRulesComponent } from '@home/components/profile/alarm/create-alarm-rules.component';
@ -143,6 +142,7 @@ import { SecurityConfigLwm2mComponent } from '@home/components/device/security-c
import { SecurityConfigLwm2mServerComponent } from '@home/components/device/security-config-lwm2m-server.component';
import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component';
import { WidgetContainerComponent } from '@home/components/widget/widget-container.component';
import { SnmpDeviceProfileTransportModule } from '@home/components/profile/device/snpm/snmp-device-profile-transport.module';
@NgModule({
declarations:
@ -228,7 +228,6 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain
DefaultDeviceProfileTransportConfigurationComponent,
MqttDeviceProfileTransportConfigurationComponent,
CoapDeviceProfileTransportConfigurationComponent,
SnmpDeviceProfileTransportConfigurationComponent,
DeviceProfileTransportConfigurationComponent,
CreateAlarmRulesComponent,
AlarmRuleComponent,
@ -272,6 +271,7 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain
SharedModule,
SharedHomeComponentsModule,
Lwm2mProfileComponentsModule,
SnmpDeviceProfileTransportModule,
StatesControllerModule
],
exports: [
@ -339,7 +339,6 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain
DefaultDeviceProfileTransportConfigurationComponent,
MqttDeviceProfileTransportConfigurationComponent,
CoapDeviceProfileTransportConfigurationComponent,
SnmpDeviceProfileTransportConfigurationComponent,
DeviceProfileTransportConfigurationComponent,
CreateAlarmRulesComponent,
AlarmRuleComponent,

4
ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts

@ -89,7 +89,9 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu
if (configuration) {
delete configuration.type;
}
this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false});
setTimeout(() => {
this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false});
});
}
private updateModel() {

23
ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.html

@ -1,23 +0,0 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="snmpDeviceProfileTransportConfigurationFormGroup" style="padding-bottom: 16px;">
<tb-json-object-edit
required
formControlName="configuration">
</tb-json-object-edit>
</form>

72
ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-communication-config.component.html

@ -0,0 +1,72 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div fxLayout="column">
<div *ngFor="let deviceProfileCommunication of communicationConfigFormArray().controls; let $index = index;
last as isLast;" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px" class="scope-row" [formGroup]="deviceProfileCommunication">
<div class="communication-config" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
<mat-form-field class="spec mat-block" floatLabel="always" hideRequiredMarker>
<mat-label translate>device-profile.snmp.scope</mat-label>
<mat-select formControlName="spec" required>
<mat-option *ngFor="let snmpSpecType of snmpSpecTypes" [value]="snmpSpecType"
[disabled]="isDisabledSeverity(snmpSpecType, $index)">
{{ snmpSpecTypeTranslationMap.get(snmpSpecType) }}
</mat-option>
</mat-select>
<mat-error *ngIf="deviceProfileCommunication.get('spec').hasError('required')">
{{ 'device-profile.snmp.scope-required' | translate }}
</mat-error>
</mat-form-field>
<mat-divider vertical></mat-divider>
<section fxFlex fxLayout="column">
<mat-form-field *ngIf="isShowFrequency(deviceProfileCommunication.get('spec').value)">
<mat-label translate>device-profile.snmp.querying-frequency</mat-label>
<input matInput formControlName="queryingFrequencyMs" type="number" min="0" required/>
<mat-error *ngIf="deviceProfileCommunication.get('queryingFrequencyMs').hasError('required')">
{{ 'device-profile.snmp.querying-frequency-required' | translate }}
</mat-error>
<mat-error *ngIf="deviceProfileCommunication.get('queryingFrequencyMs').hasError('pattern') ||
deviceProfileCommunication.get('queryingFrequencyMs').hasError('min')">
{{ 'device-profile.snmp.querying-frequency-invalid-format' | translate }}
</mat-error>
</mat-form-field>
<tb-snmp-device-profile-mapping formControlName="mappings">
</tb-snmp-device-profile-mapping>
</section>
</div>
<button *ngIf="!disabled"
mat-icon-button color="primary" style="min-width: 40px;"
type="button"
(click)="removeCommunicationConfig($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>remove_circle_outline</mat-icon>
</button>
</div>
<div *ngIf="!communicationConfigFormArray().controls.length && !disabled">
<span fxLayoutAlign="center center" class="tb-prompt required required-text" translate>device-profile.snmp.please-add-communication-config</span>
</div>
<div *ngIf="!disabled && isAddEnabled">
<button mat-stroked-button color="primary"
type="button"
(click)="addCommunicationConfig()">
<mat-icon class="button-icon">add_circle_outline</mat-icon>
{{ 'device-profile.snmp.add-communication-config' | translate }}
</button>
</div>
</div>

44
ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-communication-config.component.scss

@ -0,0 +1,44 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.communication-config {
border: 2px groove rgba(0, 0, 0, 0.25);
border-radius: 4px;
padding: 8px;
min-width: 0;
}
.scope-row {
padding-bottom: 8px;
}
.required-text {
margin: 16px 0
}
}
:host ::ng-deep {
.mat-form-field.spec {
.mat-form-field-infix {
width: 160px;
}
}
.button-icon{
font-size: 20px;
width: 20px;
height: 20px;
}
}

218
ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-communication-config.component.ts

@ -0,0 +1,218 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormArray,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator,
Validators
} from '@angular/forms';
import { SnmpCommunicationConfig, SnmpSpecType, SnmpSpecTypeTranslationMap } from '@shared/models/device.models';
import { Subject, Subscription } from 'rxjs';
import { isUndefinedOrNull } from '@core/utils';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-snmp-device-profile-communication-config',
templateUrl: './snmp-device-profile-communication-config.component.html',
styleUrls: ['./snmp-device-profile-communication-config.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SnmpDeviceProfileCommunicationConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => SnmpDeviceProfileCommunicationConfigComponent),
multi: true
}]
})
export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
snmpSpecTypes = Object.values(SnmpSpecType);
snmpSpecTypeTranslationMap = SnmpSpecTypeTranslationMap;
deviceProfileCommunicationConfig: FormGroup;
@Input()
disabled: boolean;
private usedSpecType: SnmpSpecType[] = [];
private valueChange$: Subscription = null;
private destroy$ = new Subject();
private propagateChange = (v: any) => { };
constructor(private fb: FormBuilder) { }
ngOnInit(): void {
this.deviceProfileCommunicationConfig = this.fb.group({
communicationConfig: this.fb.array([])
});
}
ngOnDestroy() {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
this.destroy$.next();
this.destroy$.complete();
}
communicationConfigFormArray(): FormArray {
return this.deviceProfileCommunicationConfig.get('communicationConfig') as FormArray;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
if (this.disabled) {
this.deviceProfileCommunicationConfig.disable({emitEvent: false});
} else {
this.deviceProfileCommunicationConfig.enable({emitEvent: false});
}
}
writeValue(communicationConfig: SnmpCommunicationConfig[]) {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
const communicationConfigControl: Array<AbstractControl> = [];
if (communicationConfig) {
communicationConfig.forEach((config) => {
communicationConfigControl.push(this.createdFormGroup(config));
});
}
this.deviceProfileCommunicationConfig.setControl('communicationConfig', this.fb.array(communicationConfigControl));
if (!communicationConfig || !communicationConfig.length) {
this.addCommunicationConfig();
}
if (this.disabled) {
this.deviceProfileCommunicationConfig.disable({emitEvent: false});
} else {
this.deviceProfileCommunicationConfig.enable({emitEvent: false});
}
this.valueChange$ = this.deviceProfileCommunicationConfig.valueChanges.subscribe(() => {
this.updateModel();
});
this.updateUsedSpecType();
if (!this.disabled && !this.deviceProfileCommunicationConfig.valid) {
this.updateModel();
}
}
public validate() {
return this.deviceProfileCommunicationConfig.valid && this.deviceProfileCommunicationConfig.value.communicationConfig.length ? null : {
communicationConfig: false
};
}
public removeCommunicationConfig(index: number) {
this.communicationConfigFormArray().removeAt(index);
}
get isAddEnabled(): boolean {
return this.communicationConfigFormArray().length !== Object.keys(SnmpSpecType).length;
}
public addCommunicationConfig() {
this.communicationConfigFormArray().push(this.createdFormGroup());
this.deviceProfileCommunicationConfig.updateValueAndValidity();
if (!this.deviceProfileCommunicationConfig.valid) {
this.updateModel();
}
}
private getFirstUnusedSeverity(): SnmpSpecType {
for (const type of Object.values(SnmpSpecType)) {
if (this.usedSpecType.indexOf(type) === -1) {
return type;
}
}
return null;
}
public isDisabledSeverity(type: SnmpSpecType, index: number): boolean {
const usedIndex = this.usedSpecType.indexOf(type);
return usedIndex > -1 && usedIndex !== index;
}
public isShowFrequency(type: SnmpSpecType): boolean {
return type === SnmpSpecType.TELEMETRY_QUERYING || type === SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING;
}
private updateUsedSpecType() {
this.usedSpecType = [];
const value: SnmpCommunicationConfig[] = this.deviceProfileCommunicationConfig.get('communicationConfig').value;
value.forEach((rule, index) => {
this.usedSpecType[index] = rule.spec;
});
}
private createdFormGroup(value?: SnmpCommunicationConfig): FormGroup {
if (isUndefinedOrNull(value)) {
value = {
spec: this.getFirstUnusedSeverity(),
queryingFrequencyMs: 5000,
mappings: null
};
}
const form = this.fb.group({
spec: [value.spec, Validators.required],
mappings: [value.mappings]
});
if (this.isShowFrequency(value.spec)) {
form.addControl('queryingFrequencyMs',
this.fb.control(value.queryingFrequencyMs, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]));
}
form.get('spec').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(spec => {
if (this.isShowFrequency(spec)) {
form.addControl('queryingFrequencyMs',
this.fb.control(5000, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]));
} else {
form.removeControl('queryingFrequencyMs');
}
});
return form;
}
private updateModel() {
const value: SnmpCommunicationConfig[] = this.deviceProfileCommunicationConfig.get('communicationConfig').value;
value.forEach(config => {
if (!this.isShowFrequency(config.spec)) {
delete config.queryingFrequencyMs;
}
});
this.updateUsedSpecType();
this.propagateChange(value);
}
}

81
ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-mapping.component.html

@ -0,0 +1,81 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div fxFlex fxLayout="column" class="mapping-config">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex="100">
<div fxFlex fxLayout="row" fxLayoutGap="8px">
<label fxFlex="26" class="tb-title no-padding" translate>device-profile.snmp.data-type</label>
<label fxFlex="37" class="tb-title no-padding" translate>device-profile.snmp.data-key</label>
<label fxFlex="37" class="tb-title no-padding" translate>device-profile.snmp.oid</label>
<span style="min-width: 40px" [fxShow]="!disabled"></span>
</div>
</div>
<mat-divider></mat-divider>
<div *ngFor="let mappingConfig of mappingsConfigFormArray().controls; let $index = index;
last as isLast;" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px" [formGroup]="mappingConfig" class="mapping-list">
<div fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
<mat-form-field fxFlex="26" floatLabel="always" hideRequiredMarker>
<mat-label></mat-label>
<mat-select formControlName="dataType" required>
<mat-option *ngFor="let dataType of dataTypes" [value]="dataType">
{{ dataTypesTranslationMap.get(dataType) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="mappingConfig.get('dataType').hasError('required')">
{{ 'device-profile.snmp.data-type-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex="37">
<mat-label></mat-label>
<input matInput formControlName="key" required/>
<mat-error *ngIf="mappingConfig.get('key').hasError('required')">
{{ 'device-profile.snmp.data-key-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex="37">
<mat-label></mat-label>
<input matInput formControlName="oid" required/>
<mat-error *ngIf="mappingConfig.get('oid').hasError('required')">
{{ 'device-profile.snmp.oid-required' | translate }}
</mat-error>
<mat-error *ngIf="mappingConfig.get('oid').hasError('pattern')">
{{ 'device-profile.snmp.oid-pattern' | translate }}
</mat-error>
</mat-form-field>
<button *ngIf="!disabled"
mat-icon-button color="primary"
type="button"
(click)="removeMappingConfig($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
<div *ngIf="!mappingsConfigFormArray().controls.length && !disabled">
<span fxLayoutAlign="center center" class="tb-prompt required required-text" translate>device-profile.snmp.please-add-mapping-config</span>
</div>
<div *ngIf="!disabled">
<button mat-stroked-button color="primary"
type="button"
(click)="addMappingConfig()">
<mat-icon class="button-icon">add_circle_outline</mat-icon>
{{ 'device-profile.snmp.add-mapping' | translate }}
</button>
</div>
</div>

49
ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-mapping.component.scss

@ -0,0 +1,49 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.mapping-config {
min-width: 518px;
}
.mapping-list {
padding-bottom: 8px;
height: 46px;
}
.required-text {
margin: 14px 0;
}
}
:host ::ng-deep {
.mapping-list {
mat-form-field {
.mat-form-field-wrapper {
padding-bottom: 0;
.mat-form-field-infix {
border-top-width: 0.2em;
width: auto;
min-width: auto;
}
.mat-form-field-underline {
bottom: 0;
}
.mat-form-field-subscript-wrapper{
margin-top: 1.8em;
}
}
}
}
}

165
ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-mapping.component.ts

@ -0,0 +1,165 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormArray,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { SnmpMapping } from '@shared/models/device.models';
import { Subscription } from 'rxjs';
import { DataType, DataTypeTranslationMap } from '@shared/models/constants';
import { isUndefinedOrNull } from '@core/utils';
@Component({
selector: 'tb-snmp-device-profile-mapping',
templateUrl: './snmp-device-profile-mapping.component.html',
styleUrls: ['./snmp-device-profile-mapping.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SnmpDeviceProfileMappingComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => SnmpDeviceProfileMappingComponent),
multi: true
}]
})
export class SnmpDeviceProfileMappingComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
mappingsConfigForm: FormGroup;
dataTypes = Object.values(DataType);
dataTypesTranslationMap = DataTypeTranslationMap;
@Input()
disabled: boolean;
private readonly oidPattern: RegExp = /^\.?([0-2])((\.0)|(\.[1-9][0-9]*))*$/;
private valueChange$: Subscription = null;
private propagateChange = (v: any) => { };
constructor(private fb: FormBuilder) { }
ngOnInit() {
this.mappingsConfigForm = this.fb.group({
mappings: this.fb.array([])
});
}
ngOnDestroy() {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
}
registerOnChange(fn: any) {
this.propagateChange = fn;
}
registerOnTouched(fn: any) {
}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
if (this.disabled) {
this.mappingsConfigForm.disable({emitEvent: false});
} else {
this.mappingsConfigForm.enable({emitEvent: false});
}
}
validate(): ValidationErrors | null {
return this.mappingsConfigForm.valid && this.mappingsConfigForm.value.mappings.length ? null : {
mapping: false
};
}
writeValue(mappings: SnmpMapping[]) {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
const mappingsControl: Array<AbstractControl> = [];
if (mappings) {
mappings.forEach((config) => {
mappingsControl.push(this.createdFormGroup(config));
});
}
this.mappingsConfigForm.setControl('mappings', this.fb.array(mappingsControl));
if (!mappings || !mappings.length) {
this.addMappingConfig();
}
if (this.disabled) {
this.mappingsConfigForm.disable({emitEvent: false});
} else {
this.mappingsConfigForm.enable({emitEvent: false});
}
this.valueChange$ = this.mappingsConfigForm.valueChanges.subscribe(() => {
this.updateModel();
});
if (!this.disabled && !this.mappingsConfigForm.valid) {
this.updateModel();
}
}
mappingsConfigFormArray(): FormArray {
return this.mappingsConfigForm.get('mappings') as FormArray;
}
public addMappingConfig() {
this.mappingsConfigFormArray().push(this.createdFormGroup());
this.mappingsConfigForm.updateValueAndValidity();
if (!this.mappingsConfigForm.valid) {
this.updateModel();
}
}
public removeMappingConfig(index: number) {
this.mappingsConfigFormArray().removeAt(index);
}
private createdFormGroup(value?: SnmpMapping): FormGroup {
if (isUndefinedOrNull(value)) {
value = {
dataType: DataType.STRING,
key: '',
oid: ''
};
}
return this.fb.group({
dataType: [value.dataType, Validators.required],
key: [value.key, Validators.required],
oid: [value.oid, [Validators.required, Validators.pattern(this.oidPattern)]]
});
}
private updateModel() {
const value: SnmpMapping[] = this.mappingsConfigForm.get('mappings').value;
this.propagateChange(value);
}
}

46
ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport-configuration.component.html

@ -0,0 +1,46 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="snmpDeviceProfileTransportConfigurationFormGroup" style="padding: 8px 0 16px;">
<section fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column">
<mat-form-field fxFlex>
<mat-label translate>device-profile.snmp.timeout-ms</mat-label>
<input matInput formControlName="timeoutMs" type="number" min="0" required/>
<mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('required')">
{{ 'device-profile.snmp.timeout-ms-required' | translate }}
</mat-error>
<mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('pattern') ||
snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('min')">
{{ 'device-profile.snmp.timeout-ms-invalid-format' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>device-profile.snmp.retries</mat-label>
<input matInput formControlName="retries" type="number" min="0" required/>
<mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('required')">
{{ 'device-profile.snmp.retries-required' | translate }}
</mat-error>
<mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('pattern') ||
snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('min')">
{{ 'device-profile.snmp.retries-invalid-format' | translate }}
</mat-error>
</mat-form-field>
</section>
<div class="tb-small" style="padding-bottom: 8px" translate>device-profile.snmp.communication-configs</div>
<tb-snmp-device-profile-communication-config formControlName="communicationConfigs">
</tb-snmp-device-profile-communication-config>
</form>

57
ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.ts → ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport-configuration.component.ts

@ -15,9 +15,16 @@
///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
DeviceProfileTransportConfiguration,
@ -40,19 +47,24 @@ export interface OidMappingConfiguration {
selector: 'tb-snmp-device-profile-transport-configuration',
templateUrl: './snmp-device-profile-transport-configuration.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent),
multi: true
}]
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent),
multi: true
}]
})
export class SnmpDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy {
export class SnmpDeviceProfileTransportConfigurationComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
snmpDeviceProfileTransportConfigurationFormGroup: FormGroup;
private destroy$ = new Subject();
private requiredValue: boolean;
private configuration = [];
get required(): boolean {
return this.requiredValue;
@ -69,12 +81,14 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements Control
private propagateChange = (v: any) => {
}
constructor(private store: Store<AppState>, private fb: FormBuilder) {
constructor(private fb: FormBuilder) {
}
ngOnInit(): void {
this.snmpDeviceProfileTransportConfigurationFormGroup = this.fb.group({
configuration: [null, Validators.required]
timeoutMs: [500, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
retries: [0, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
communicationConfigs: [null, Validators.required],
});
this.snmpDeviceProfileTransportConfigurationFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
@ -95,18 +109,33 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements Control
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
if (this.disabled) {
this.snmpDeviceProfileTransportConfigurationFormGroup.disable({emitEvent: false});
} else {
this.snmpDeviceProfileTransportConfigurationFormGroup.enable({emitEvent: false});
}
}
writeValue(value: SnmpDeviceProfileTransportConfiguration | null): void {
if (isDefinedAndNotNull(value)) {
this.snmpDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false});
this.snmpDeviceProfileTransportConfigurationFormGroup.patchValue(value, {emitEvent: !value.communicationConfigs});
}
}
private updateModel() {
let configuration: DeviceProfileTransportConfiguration = null;
if (this.snmpDeviceProfileTransportConfigurationFormGroup.valid) {
configuration = this.snmpDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration;
configuration = this.snmpDeviceProfileTransportConfigurationFormGroup.getRawValue();
configuration.type = DeviceTransportType.SNMP;
}
this.propagateChange(configuration);
}
validate(): ValidationErrors | null {
return this.snmpDeviceProfileTransportConfigurationFormGroup.valid ? null : {
snmpDeviceProfileTransportConfiguration: false
};
}
}

38
ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport.module.ts

@ -0,0 +1,38 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { SnmpDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/snpm/snmp-device-profile-transport-configuration.component';
import { SnmpDeviceProfileCommunicationConfigComponent } from './snmp-device-profile-communication-config.component';
import { SnmpDeviceProfileMappingComponent } from './snmp-device-profile-mapping.component';
@NgModule({
declarations: [
SnmpDeviceProfileTransportConfigurationComponent,
SnmpDeviceProfileCommunicationConfigComponent,
SnmpDeviceProfileMappingComponent
],
imports: [
CommonModule,
SharedModule
],
exports: [
SnmpDeviceProfileTransportConfigurationComponent
]
})
export class SnmpDeviceProfileTransportModule { }

1
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts

@ -274,6 +274,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.updateTitle(true);
this.alarmsDatasource.updateAlarms();
this.clearCache();
this.ctx.detectChanges();
}
public pageLinkSortDirection(): SortDirection {

2
ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts

@ -443,7 +443,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
const dataPageData = subscription.dataPages[0];
const childNodes: HierarchyNavTreeNode[] = [];
datasourcesPageData.data.forEach((childDatasource, index) => {
childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, dataPageData.data[index]));
childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, dataPageData.data[index], nodeCtx));
});
nodeCtx.childrenNodesLoaded = true;
childrenNodesLoadCb(this.prepareNodes(childNodes));

2
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts

@ -47,7 +47,6 @@ import {
isDefined,
isNumber,
isObject,
isString,
isUndefined
} from '@core/utils';
import cssjs from '@core/css/css';
@ -236,6 +235,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.updateTitle(true);
this.entityDatasource.dataUpdated();
this.clearCache();
this.ctx.detectChanges();
}
public pageLinkSortDirection(): SortDirection {

10
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts

@ -69,10 +69,10 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
this.config.cellActionDescriptors.push(
{
name: this.translate.instant('resource.export'),
name: this.translate.instant('resource.download'),
icon: 'file_download',
isEnabled: () => true,
onAction: ($event, entity) => this.exportResource($event, entity)
onAction: ($event, entity) => this.downloadResource($event, entity)
}
);
@ -118,7 +118,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
return this.config;
}
exportResource($event: Event, resource: ResourceInfo) {
downloadResource($event: Event, resource: ResourceInfo) {
if ($event) {
$event.stopPropagation();
}
@ -127,8 +127,8 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
onResourceAction(action: EntityAction<ResourceInfo>): boolean {
switch (action.action) {
case 'uploadResource':
this.exportResource(action.event, action.entity);
case 'downloadResource':
this.downloadResource(action.event, action.entity);
return true;
}
return false;

24
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html

@ -18,16 +18,26 @@
<div class="tb-details-buttons" fxLayout.xs="column">
<button mat-raised-button color="primary" fxFlex.xs
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'uploadResource')"
(click)="onEntityAction($event, 'downloadResource')"
[fxShow]="!isEdit">
{{'resource.export' | translate }}
{{ 'resource.download' | translate }}
</button>
<button mat-raised-button color="primary" fxFlex.xs
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[fxShow]="!hideDelete() && !isEdit">
{{'resource.delete' | translate }}
{{ 'resource.delete' | translate }}
</button>
<div fxLayout="row" fxLayout.xs="column">
<button mat-raised-button
ngxClipboard
(cbOnSuccess)="onResourceIdCopied()"
[cbContent]="entity?.id?.id"
[fxShow]="!isEdit">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
<span translate>resource.copyId</span>
</button>
</div>
</div>
<div class="mat-padding" fxLayout="column">
<form [formGroup]="entityForm">
@ -47,7 +57,7 @@
{{ 'resource.title-required' | translate }}
</mat-error>
</mat-form-field>
<tb-file-input
<tb-file-input *ngIf="isAdd"
formControlName="data"
required
[readAsBinary]="true"
@ -59,6 +69,12 @@
[existingFileName]="entityForm.get('fileName')?.value"
(fileNameChanged)="entityForm?.get('fileName').patchValue($event)">
</tb-file-input>
<div *ngIf="!isAdd" fxLayout="row" fxLayoutGap.gt-md="8px" fxLayoutGap.sm="8px" fxLayout.xs="column" fxLayout.md="column">
<mat-form-field fxFlex>
<mat-label translate>resource.file-name</mat-label>
<input matInput formControlName="fileName" type="text">
</mat-form-field>
</div>
</fieldset>
</form>
</div>

27
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts

@ -30,6 +30,7 @@ import {
ResourceTypeTranslationMap
} from '@shared/models/resource.models';
import { pairwise, startWith, takeUntil } from 'rxjs/operators';
import { ActionNotificationShow } from "@core/notification/notification.actions";
@Component({
selector: 'tb-resources-library',
@ -88,26 +89,29 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
}
buildForm(entity: Resource): FormGroup {
return this.fb.group(
const form = this.fb.group(
{
title: [entity ? entity.title : '', []],
resourceType: [{
value: entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL,
disabled: this.isEdit
disabled: !this.isAdd
}, [Validators.required]],
data: [entity ? entity.data : null, [Validators.required]],
fileName: [entity ? entity.fileName : null, [Validators.required]],
title: [entity ? entity.title : '', []]
}
);
if (this.isAdd) {
form.addControl('data', this.fb.control(null, Validators.required));
}
return form
}
updateForm(entity: Resource) {
this.entityForm.patchValue({resourceType: entity.resourceType});
if (this.isEdit) {
this.entityForm.get('resourceType').disable({emitEvent: false});
this.entityForm.get('fileName').disable({emitEvent: false});
}
this.entityForm.patchValue({
data: entity.data,
resourceType: entity.resourceType,
fileName: entity.fileName,
title: entity.title
});
@ -132,4 +136,15 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
convertToBase64File(data: string): string {
return window.btoa(data);
}
onResourceIdCopied() {
this.store.dispatch(new ActionNotificationShow(
{
message: this.translate.instant('resource.idCopiedMessage'),
type: 'success',
duration: 750,
verticalPosition: 'bottom',
horizontalPosition: 'right'
}));
}
}

2
ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts

@ -98,7 +98,7 @@ const routes: Routes = [
}
},
{
path: ':customerId/edges',
path: ':customerId/edgeInstances',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],

2
ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts

@ -169,7 +169,7 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
if ($event) {
$event.stopPropagation();
}
this.router.navigateByUrl(`customers/${customer.id.id}/edges`);
this.router.navigateByUrl(`customers/${customer.id.id}/edgeInstances`);
}
onCustomerAction(action: EntityAction<Customer>): boolean {

2
ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts

@ -362,7 +362,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
if (this.config.componentsData.dashboardScope === 'customer') {
this.router.navigateByUrl(`customers/${this.config.componentsData.customerId}/dashboards/${dashboard.id.id}`);
} else if (this.config.componentsData.dashboardScope === 'edge') {
this.router.navigateByUrl(`edges/${this.config.componentsData.edgeId}/dashboards/${dashboard.id.id}`);
this.router.navigateByUrl(`edgeInstances/${this.config.componentsData.edgeId}/dashboards/${dashboard.id.id}`);
} else {
this.router.navigateByUrl(`dashboards/${dashboard.id.id}`);
}

36
ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts

@ -15,7 +15,16 @@
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
@ -29,13 +38,20 @@ import {
selector: 'tb-device-data',
templateUrl: './device-data.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceDataComponent),
multi: true
}]
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceDataComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DeviceDataComponent),
multi: true
},
]
})
export class DeviceDataComponent implements ControlValueAccessor, OnInit {
export class DeviceDataComponent implements ControlValueAccessor, OnInit, Validator {
deviceDataFormGroup: FormGroup;
@ -97,6 +113,12 @@ export class DeviceDataComponent implements ControlValueAccessor, OnInit {
this.deviceDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false});
}
validate(): ValidationErrors | null {
return this.deviceDataFormGroup.valid ? null : {
deviceDataForm: false
};
}
private updateModel() {
let deviceData: DeviceData = null;
if (this.deviceDataFormGroup.valid) {

44
ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts

@ -15,27 +15,39 @@
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
DeviceTransportConfiguration,
DeviceTransportType
} from '@shared/models/device.models';
import { DeviceTransportConfiguration, DeviceTransportType } from '@shared/models/device.models';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-device-transport-configuration',
templateUrl: './device-transport-configuration.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceTransportConfigurationComponent),
multi: true
}]
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceTransportConfigurationComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DeviceTransportConfigurationComponent),
multi: true
}]
})
export class DeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit {
export class DeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit, Validator {
deviceTransportType = DeviceTransportType;
@ -92,7 +104,15 @@ export class DeviceTransportConfigurationComponent implements ControlValueAccess
if (configuration) {
delete configuration.type;
}
this.deviceTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false});
setTimeout(() => {
this.deviceTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false});
}, 0);
}
validate(): ValidationErrors | null {
return this.deviceTransportConfigurationFormGroup.valid ? null : {
deviceTransportConfiguration: false
};
}
private updateModel() {

121
ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.html

@ -15,10 +15,119 @@
limitations under the License.
-->
<form [formGroup]="snmpDeviceTransportConfigurationFormGroup" style="padding-bottom: 16px;">
<tb-json-object-edit
[required]="required"
label="{{ 'device-profile.transport-type-snmp-hint' | translate }}"
formControlName="configuration">
</tb-json-object-edit>
<form [formGroup]="snmpDeviceTransportConfigurationFormGroup" style="padding-bottom: 16px;" fxLayoutGap="8px" fxLayout="column">
<div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column">
<mat-form-field fxFlex>
<mat-label translate>device-profile.snmp.host</mat-label>
<input matInput formControlName="host" required>
<mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('host').hasError('required') ||
snmpDeviceTransportConfigurationFormGroup.get('host').hasError('pattern')">
{{ 'device-profile.snmp.host-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>device-profile.snmp.port</mat-label>
<input matInput formControlName="port" type="number" min="0" required>
<mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('port').hasError('required')">
{{ 'device-profile.snmp.port-required' | translate }}
</mat-error>
<mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('port').hasError('pattern') ||
snmpDeviceTransportConfigurationFormGroup.get('port').hasError('min')">
{{ 'device-profile.snmp.port-format' | translate }}
</mat-error>
</mat-form-field>
</div>
<mat-form-field class="mat-block" floatLabel="always" hideRequiredMarker>
<mat-label translate>device-profile.snmp.protocol-version</mat-label>
<mat-select formControlName="protocolVersion" required>
<mat-option *ngFor="let snmpDeviceProtocolVersion of snmpDeviceProtocolVersions" [value]="snmpDeviceProtocolVersion">
{{ snmpDeviceProtocolVersion | lowercase }}
</mat-option>
</mat-select>
<mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('protocolVersion').hasError('required')">
{{ 'device-profile.snmp.protocol-version-required' | translate }}
</mat-error>
</mat-form-field>
<section *ngIf="!isV3protocolVersion()">
<mat-form-field class="mat-block">
<mat-label translate>device-profile.snmp.community</mat-label>
<input matInput formControlName="community" required>
<mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('community').hasError('required') ||
snmpDeviceTransportConfigurationFormGroup.get('community').hasError('pattern')">
{{ 'device-profile.snmp.community-required' | translate }}
</mat-error>
</mat-form-field>
</section>
<section *ngIf="isV3protocolVersion()">
<div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column">
<mat-form-field fxFlex>
<mat-label translate>device-profile.snmp.user-name</mat-label>
<input matInput formControlName="username" required>
<mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('username').hasError('required') ||
snmpDeviceTransportConfigurationFormGroup.get('username').hasError('pattern')">
{{ 'device-profile.snmp.user-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>device-profile.snmp.security-name</mat-label>
<input matInput formControlName="securityName" required>
<mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('securityName').hasError('required') ||
snmpDeviceTransportConfigurationFormGroup.get('securityName').hasError('pattern')">
{{ 'device-profile.snmp.security-name-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column">
<mat-form-field fxFlex floatLabel="always" hideRequiredMarker>
<mat-label translate>device-profile.snmp.authentication-protocol</mat-label>
<mat-select formControlName="authenticationProtocol" required>
<mat-option *ngFor="let snmpAuthenticationProtocol of snmpAuthenticationProtocols" [value]="snmpAuthenticationProtocol">
{{ snmpAuthenticationProtocolTranslation.get(snmpAuthenticationProtocol) }}
</mat-option>
</mat-select>
<mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('authenticationProtocol').hasError('required')">
{{ 'device-profile.snmp.authentication-protocol-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>device-profile.snmp.authentication-passphrase</mat-label>
<input matInput formControlName="authenticationPassphrase" required>
<mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').hasError('required') ||
snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').hasError('pattern')">
{{ 'device-profile.snmp.authentication-passphrase-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column">
<mat-form-field fxFlex floatLabel="always" hideRequiredMarker>
<mat-label translate>device-profile.snmp.privacy-protocol</mat-label>
<mat-select formControlName="privacyProtocol" required>
<mat-option *ngFor="let snmpPrivacyProtocol of snmpPrivacyProtocols" [value]="snmpPrivacyProtocol">
{{ snmpPrivacyProtocolTranslation.get(snmpPrivacyProtocol) }}
</mat-option>
</mat-select>
<mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('privacyProtocol').hasError('required')">
{{ 'device-profile.snmp.privacy-protocol-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>device-profile.snmp.privacy-passphrase</mat-label>
<input matInput formControlName="privacyPassphrase" required>
<mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').hasError('required') ||
snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').hasError('pattern')">
{{ 'device-profile.snmp.privacy-passphrase-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column">
<mat-form-field fxFlex>
<mat-label translate>device-profile.snmp.context-name</mat-label>
<input matInput formControlName="contextName">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>device-profile.snmp.engine-id</mat-label>
<input matInput formControlName="engineId">
</mat-form-field>
</div>
</section>
</form>

112
ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.ts

@ -14,31 +14,57 @@
/// limitations under the License.
///
import {Component, forwardRef, Input, OnInit} from '@angular/core';
import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators} from '@angular/forms';
import {Store} from '@ngrx/store';
import {AppState} from '@app/core/core.state';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
DeviceTransportConfiguration,
DeviceTransportType,
SnmpDeviceTransportConfiguration
SnmpAuthenticationProtocol,
SnmpAuthenticationProtocolTranslationMap,
SnmpDeviceProtocolVersion,
SnmpDeviceTransportConfiguration,
SnmpPrivacyProtocol,
SnmpPrivacyProtocolTranslationMap
} from '@shared/models/device.models';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-snmp-device-transport-configuration',
templateUrl: './snmp-device-transport-configuration.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SnmpDeviceTransportConfigurationComponent),
multi: true
}]
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SnmpDeviceTransportConfigurationComponent),
multi: true
}, {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => SnmpDeviceTransportConfigurationComponent),
multi: true
}]
})
export class SnmpDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit {
export class SnmpDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit, Validator {
snmpDeviceTransportConfigurationFormGroup: FormGroup;
snmpDeviceProtocolVersions = Object.values(SnmpDeviceProtocolVersion);
snmpAuthenticationProtocols = Object.values(SnmpAuthenticationProtocol);
snmpAuthenticationProtocolTranslation = SnmpAuthenticationProtocolTranslationMap;
snmpPrivacyProtocols = Object.values(SnmpPrivacyProtocol);
snmpPrivacyProtocolTranslation = SnmpPrivacyProtocolTranslationMap;
private requiredValue: boolean;
get required(): boolean {
@ -53,8 +79,7 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc
@Input()
disabled: boolean;
private propagateChange = (v: any) => {
};
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private fb: FormBuilder) {
@ -69,13 +94,33 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc
ngOnInit() {
this.snmpDeviceTransportConfigurationFormGroup = this.fb.group({
configuration: [null, Validators.required]
host: ['', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]],
port: [null, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]],
protocolVersion: [SnmpDeviceProtocolVersion.V2C, Validators.required],
community: ['public', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]],
username: ['', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]],
securityName: ['public', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]],
contextName: [null],
authenticationProtocol: [SnmpAuthenticationProtocol.SHA_512, Validators.required],
authenticationPassphrase: ['', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]],
privacyProtocol: [SnmpPrivacyProtocol.DES, Validators.required],
privacyPassphrase: ['', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]],
engineId: ['']
});
this.snmpDeviceTransportConfigurationFormGroup.get('protocolVersion').valueChanges.subscribe((protocol: SnmpDeviceProtocolVersion) => {
this.updateDisabledFormValue(protocol);
});
this.snmpDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
validate(): ValidationErrors | null {
return this.snmpDeviceTransportConfigurationFormGroup.valid ? null : {
snmpDeviceTransportConfiguration: false
};
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
@ -86,13 +131,46 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc
}
writeValue(value: SnmpDeviceTransportConfiguration | null): void {
this.snmpDeviceTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false});
if (isDefinedAndNotNull(value)) {
this.snmpDeviceTransportConfigurationFormGroup.patchValue(value, {emitEvent: false});
if (this.snmpDeviceTransportConfigurationFormGroup.enabled) {
this.updateDisabledFormValue(value.protocolVersion || SnmpDeviceProtocolVersion.V2C);
}
}
}
isV3protocolVersion(): boolean {
return this.snmpDeviceTransportConfigurationFormGroup.get('protocolVersion').value === SnmpDeviceProtocolVersion.V3;
}
private updateDisabledFormValue(protocol: SnmpDeviceProtocolVersion) {
if (protocol === SnmpDeviceProtocolVersion.V3) {
this.snmpDeviceTransportConfigurationFormGroup.get('community').disable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('username').enable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('securityName').enable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('contextName').enable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('authenticationProtocol').enable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').enable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('privacyProtocol').enable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').enable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('engineId').enable({emitEvent: false});
} else {
this.snmpDeviceTransportConfigurationFormGroup.get('community').enable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('username').disable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('securityName').disable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('contextName').disable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('authenticationProtocol').disable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').disable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('privacyProtocol').disable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').disable({emitEvent: false});
this.snmpDeviceTransportConfigurationFormGroup.get('engineId').disable({emitEvent: false});
}
}
private updateModel() {
let configuration: DeviceTransportConfiguration = null;
if (this.snmpDeviceTransportConfigurationFormGroup.valid) {
configuration = this.snmpDeviceTransportConfigurationFormGroup.getRawValue().configuration;
configuration = this.snmpDeviceTransportConfigurationFormGroup.value;
configuration.type = DeviceTransportType.SNMP;
}
this.propagateChange(configuration);

20
ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts

@ -42,7 +42,7 @@ import {
const routes: Routes = [
{
path: 'edges',
path: 'edgeInstances',
data: {
breadcrumb: {
label: 'edge.edge-instances',
@ -187,6 +187,24 @@ const routes: Routes = [
}
}
]
}
]
},
{
path: 'edgeManagement',
data: {
breadcrumb: {
label: 'edge.management',
icon: 'settings_input_antenna'
}
},
children: [
{
path: '',
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
redirectTo: '/edgeManagement/ruleChains'
}
},
{
path: 'ruleChains',

2
ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts

@ -417,7 +417,7 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
suffix = 'ruleChains';
break;
}
this.router.navigateByUrl(`edges/${edge.id.id}/${suffix}`);
this.router.navigateByUrl(`edgeInstances/${edge.id.id}/${suffix}`);
}
assignToCustomer($event: Event, edgesIds: Array<EdgeId>) {

2
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts

@ -152,7 +152,7 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
}
if (this.ruleNode.targetRuleChainId) {
if (this.ruleChainType === RuleChainType.EDGE) {
this.router.navigateByUrl(`/edges/ruleChains/${this.ruleNode.targetRuleChainId}`);
this.router.navigateByUrl(`/edgeManagement/ruleChains/${this.ruleNode.targetRuleChainId}`);
} else {
this.router.navigateByUrl(`/ruleChains/${this.ruleNode.targetRuleChainId}`);
}

2
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts

@ -1289,7 +1289,7 @@ export class RuleChainPageComponent extends PageComponent
if (this.ruleChainType !== RuleChainType.EDGE) {
this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`);
} else {
this.router.navigateByUrl(`edges/ruleChains/${this.ruleChain.id.id}`);
this.router.navigateByUrl(`edgeManagement/ruleChains/${this.ruleChain.id.id}`);
}
} else {
this.createRuleChainModel();

6
ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts

@ -285,7 +285,7 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
if (ruleChainImport) {
this.itembuffer.storeRuleChainImport(ruleChainImport);
if (this.config.componentsData.ruleChainScope === 'edges') {
this.router.navigateByUrl(`edges/ruleChains/ruleChain/import`);
this.router.navigateByUrl(`edgeManagement/ruleChains/ruleChain/import`);
} else {
this.router.navigateByUrl(`ruleChains/ruleChain/import`);
}
@ -298,9 +298,9 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
$event.stopPropagation();
}
if (this.config.componentsData.ruleChainScope === 'edges') {
this.router.navigateByUrl(`edges/ruleChains/${ruleChain.id.id}`);
this.router.navigateByUrl(`edgeManagement/ruleChains/${ruleChain.id.id}`);
} else if (this.config.componentsData.ruleChainScope === 'edge') {
this.router.navigateByUrl(`edges/${this.config.componentsData.edgeId}/ruleChains/${ruleChain.id.id}`);
this.router.navigateByUrl(`edgeInstances/${this.config.componentsData.edgeId}/ruleChains/${ruleChain.id.id}`);
} else {
this.router.navigateByUrl(`ruleChains/${ruleChain.id.id}`);
}

2
ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts

@ -50,7 +50,7 @@ export class RuleNodeComponent extends FcNodeComponent implements OnInit {
}
if (node.targetRuleChainId) {
if (node.ruleChainType === RuleChainType.EDGE) {
this.router.navigateByUrl(`/edges/ruleChains/${node.targetRuleChainId}`);
this.router.navigateByUrl(`/edgeManagement/ruleChains/${node.targetRuleChainId}`);
} else {
this.router.navigateByUrl(`/ruleChains/${node.targetRuleChainId}`);
}

17
ui-ngx/src/app/shared/models/constants.ts

@ -120,6 +120,7 @@ export const HelpLinks = {
entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views',
entitiesImport: helpBaseUrl + '/docs/user-guide/bulk-provisioning',
rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains',
resources: helpBaseUrl + '/docs/user-guide/ui/resources',
dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards',
otaUpdates: helpBaseUrl + '/docs/user-guide/ui/ota-updates',
widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles',
@ -147,6 +148,22 @@ export enum ValueType {
JSON = 'JSON'
}
export enum DataType {
STRING = 'STRING',
LONG = 'LONG',
BOOLEAN = 'BOOLEAN',
DOUBLE = 'DOUBLE',
JSON = 'JSON'
}
export const DataTypeTranslationMap = new Map([
[DataType.STRING, 'value.string'],
[DataType.LONG, 'value.integer'],
[DataType.BOOLEAN, 'value.boolean'],
[DataType.DOUBLE, 'value.double'],
[DataType.JSON, 'value.json']
]);
export const valueTypesMap = new Map<ValueType, ValueTypeData>(
[
[

95
ui-ngx/src/app/shared/models/device.models.ts

@ -29,6 +29,7 @@ import * as _moment from 'moment';
import { AbstractControl, ValidationErrors } from '@angular/forms';
import { OtaPackageId } from '@shared/models/id/ota-package-id';
import { DashboardId } from '@shared/models/id/dashboard-id';
import { DataType } from '@shared/models/constants';
export enum DeviceProfileType {
DEFAULT = 'DEFAULT',
@ -257,7 +258,35 @@ export interface Lwm2mDeviceProfileTransportConfiguration {
}
export interface SnmpDeviceProfileTransportConfiguration {
[key: string]: any;
timeoutMs?: number;
retries?: number;
communicationConfigs?: SnmpCommunicationConfig[];
}
export enum SnmpSpecType {
TELEMETRY_QUERYING = 'TELEMETRY_QUERYING',
CLIENT_ATTRIBUTES_QUERYING = 'CLIENT_ATTRIBUTES_QUERYING',
SHARED_ATTRIBUTES_SETTING = 'SHARED_ATTRIBUTES_SETTING',
TO_DEVICE_RPC_REQUEST = 'TO_DEVICE_RPC_REQUEST'
}
export const SnmpSpecTypeTranslationMap = new Map<SnmpSpecType, string>([
[SnmpSpecType.TELEMETRY_QUERYING, ' Telemetry'],
[SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING, 'Client attributes'],
[SnmpSpecType.SHARED_ATTRIBUTES_SETTING, 'Shared attributes'],
[SnmpSpecType.TO_DEVICE_RPC_REQUEST, 'RPC request']
]);
export interface SnmpCommunicationConfig {
spec: SnmpSpecType;
mappings: SnmpMapping[];
queryingFrequencyMs?: number;
}
export interface SnmpMapping {
oid: string;
key: string;
dataType: DataType;
}
export type DeviceProfileTransportConfigurations = DefaultDeviceProfileTransportConfiguration &
@ -332,7 +361,11 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT
transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M};
break;
case DeviceTransportType.SNMP:
const snmpTransportConfiguration: SnmpDeviceProfileTransportConfiguration = {};
const snmpTransportConfiguration: SnmpDeviceProfileTransportConfiguration = {
timeoutMs: 500,
retries: 0,
communicationConfigs: null
};
transportConfiguration = {...snmpTransportConfiguration, type: DeviceTransportType.SNMP};
break;
}
@ -361,7 +394,12 @@ export function createDeviceTransportConfiguration(type: DeviceTransportType): D
transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M};
break;
case DeviceTransportType.SNMP:
const snmpTransportConfiguration: SnmpDeviceTransportConfiguration = {};
const snmpTransportConfiguration: SnmpDeviceTransportConfiguration = {
host: 'localhost',
port: 161,
protocolVersion: SnmpDeviceProtocolVersion.V2C,
community: 'public'
};
transportConfiguration = {...snmpTransportConfiguration, type: DeviceTransportType.SNMP};
break;
}
@ -539,8 +577,57 @@ export interface Lwm2mDeviceTransportConfiguration {
[key: string]: any;
}
export enum SnmpDeviceProtocolVersion {
V1 = 'V1',
V2C = 'V2C',
V3 = 'V3'
}
export enum SnmpAuthenticationProtocol {
SHA_1 = 'SHA_1',
SHA_224 = 'SHA_224',
SHA_256 = 'SHA_256',
SHA_384 = 'SHA_384',
SHA_512 = 'SHA_512',
MD5 = 'MD%'
}
export const SnmpAuthenticationProtocolTranslationMap = new Map<SnmpAuthenticationProtocol, string>([
[SnmpAuthenticationProtocol.SHA_1, 'SHA-1'],
[SnmpAuthenticationProtocol.SHA_224, 'SHA-224'],
[SnmpAuthenticationProtocol.SHA_256, 'SHA-256'],
[SnmpAuthenticationProtocol.SHA_384, 'SHA-384'],
[SnmpAuthenticationProtocol.SHA_512, 'SHA-512'],
[SnmpAuthenticationProtocol.MD5, 'MD5']
]);
export enum SnmpPrivacyProtocol {
DES = 'DES',
AES_128 = 'AES_128',
AES_192 = 'AES_192',
AES_256 = 'AES_256'
}
export const SnmpPrivacyProtocolTranslationMap = new Map<SnmpPrivacyProtocol, string>([
[SnmpPrivacyProtocol.DES, 'DES'],
[SnmpPrivacyProtocol.AES_128, 'AES-128'],
[SnmpPrivacyProtocol.AES_192, 'AES-192'],
[SnmpPrivacyProtocol.AES_256, 'AES-256'],
]);
export interface SnmpDeviceTransportConfiguration {
[key: string]: any;
host?: string;
port?: number;
protocolVersion?: SnmpDeviceProtocolVersion;
community?: string;
username?: string;
securityName?: string;
contextName?: string;
authenticationProtocol?: SnmpAuthenticationProtocol;
authenticationPassphrase?: string;
privacyProtocol?: SnmpPrivacyProtocol;
privacyPassphrase?: string;
engineId?: string;
}
export type DeviceTransportConfigurations = DefaultDeviceTransportConfiguration &

4
ui-ngx/src/app/shared/models/resource.models.ts

@ -61,6 +61,6 @@ export interface Resource extends ResourceInfo {
}
export interface Resources extends ResourceInfo {
data: string|string[];
fileName: string|string[];
data: Array<string>;
fileName: Array<string>;
}

6
ui-ngx/src/app/shared/models/tenant.model.ts

@ -49,8 +49,10 @@ export interface DefaultTenantProfileConfiguration {
maxRuleNodeExecutionsPerMessage: number;
maxEmails: number;
maxSms: number;
maxCreatedAlarms: number;
defaultStorageTtlDays: number;
alarmsTtlDays: number;
}
export type TenantProfileConfigurations = DefaultTenantProfileConfiguration;
@ -81,7 +83,9 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
maxRuleNodeExecutionsPerMessage: 0,
maxEmails: 0,
maxSms: 0,
defaultStorageTtlDays: 0
maxCreatedAlarms: 0,
defaultStorageTtlDays: 0,
alarmsTtlDays: 0
};
configuration = {...defaultConfiguration, type: TenantProfileType.DEFAULT};
break;

55
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1297,6 +1297,54 @@
"sw-update-recourse": "Software update CoAP recourse",
"sw-update-recourse-required": "Software update CoAP recourse is required.",
"config-json-tab": "Json Config Profile Device"
},
"snmp": {
"add-communication-config": "Add communication config",
"add-mapping": "Add mapping",
"authentication-passphrase": "Authentication passphrase",
"authentication-passphrase-required": "Authentication passphrase is required.",
"authentication-protocol": "Authentication protocol",
"authentication-protocol-required": "Authentication protocol is required.",
"communication-configs": "Communication configs",
"community": "Community string",
"community-required": "Community string is required.",
"context-name": "Context name",
"data-key": "Data key",
"data-key-required": "Data key is required.",
"data-type": "Data type",
"data-type-required": "Data type is required.",
"engine-id": "Engine ID",
"host": "Host",
"host-required": "Host is required.",
"oid": "OID",
"oid-pattern": "Invalid OID format.",
"oid-required": "OID is required.",
"please-add-communication-config": "Please add communication config",
"please-add-mapping-config": "Please add mapping config",
"port": "Port",
"port-format": "Invalid port format.",
"port-required": "Port is required.",
"privacy-passphrase": "Privacy passphrase",
"privacy-passphrase-required": "Privacy passphrase is required.",
"privacy-protocol": "Privacy protocol",
"privacy-protocol-required": "Privacy protocol is required.",
"protocol-version": "Protocol version",
"protocol-version-required": "Protocol version is required.",
"querying-frequency": "Querying frequency, ms",
"querying-frequency-invalid-format": "Querying frequency must be a positive integer.",
"querying-frequency-required": "Querying frequency is required.",
"retries": "Retries",
"retries-invalid-format": "Retries must be a positive integer.",
"retries-required": "Retries is required.",
"scope": "Scope",
"scope-required": "Scope is required.",
"security-name": "Security name",
"security-name-required": "Security name is required.",
"timeout-ms": "Timeout, ms",
"timeout-ms-invalid-format": "Timeout must be a positive integer.",
"timeout-ms-required": "Timeout is required.",
"user-name": "User name",
"user-name-required": "User name is required."
}
},
"dialog": {
@ -2309,20 +2357,23 @@
},
"resource": {
"add": "Add Resource",
"copyId": "Copy resource Id",
"delete": "Delete resource",
"delete-resource-text": "Be careful, after the confirmation the resource will become unrecoverable.",
"delete-resource-title": "Are you sure you want to delete the resource '{{resourceTitle}}'?",
"delete-resources-action-title": "Delete { count, plural, 1 {1 resource} other {# resources} }",
"delete-resources-text": "Be careful, after the confirmation all selected resources will be removed.",
"delete-resources-title": "Are you sure you want to delete { count, plural, 1 {1 resource} other {# resources} }?",
"download": "Download resource",
"drop-file": "Drop a resource file or click to select a file to upload.",
"empty": "Resource is empty",
"export": "Export resource",
"file-name": "File name",
"idCopiedMessage": "Resource Id has been copied to clipboard",
"no-resource-matching": "No resource matching '{{widgetsBundle}}' were found.",
"no-resource-text": "No resources found",
"open-widgets-bundle": "Open widgets bundle",
"resource": "Resource",
"resource-library-details": "Resource library details",
"resource-library-details": "Resource details",
"resource-type": "Resource type",
"resources-library": "Resources library",
"search": "Search resources",

1
ui-ngx/src/assets/locale/locale.constant-zh_CN.json

@ -1994,7 +1994,6 @@
"delete-resources-title": "确定要删除 { count, plural, 1 {# 个资源} other {# 个资源} }?",
"drop-file": "拖拽资源文件或单击以选择要上传的文件。",
"empty": "资源为空",
"export": "导出资源",
"no-resource-matching": "找不到与 '{{widgetsBundle}}' 匹配的资源。",
"no-resource-text": "找不到资源",
"open-widgets-bundle": "打开部件库",

Loading…
Cancel
Save