Browse Source

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

pull/4770/head
ShvaykaD 5 years ago
parent
commit
37407c4214
  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. 9
      common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java
  9. 2
      common/queue/src/main/java/org/thingsboard/server/queue/TbQueueRequestTemplate.java
  10. 242
      common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java
  11. 10
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java
  12. 211
      common/queue/src/test/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplateTest.java
  13. 4
      common/util/pom.xml
  14. 57
      common/util/src/main/java/org/thingsboard/common/util/TbStopWatch.java
  15. 16
      dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
  16. 33
      dao/src/main/java/org/thingsboard/server/dao/cache/CacheExecutorService.java
  17. 1
      dao/src/test/resources/application-test.properties
  18. 30
      msa/js-executor/api/httpServer.js
  19. 99
      msa/js-executor/api/jsInvokeMessageProcessor.js
  20. 5
      msa/js-executor/config/custom-environment-variables.yml
  21. 9
      msa/js-executor/config/default.yml
  22. 1
      msa/js-executor/package.json
  23. 110
      msa/js-executor/queue/kafkaTemplate.js
  24. 2
      msa/js-executor/server.js
  25. 361
      msa/js-executor/yarn.lock
  26. 5
      pom.xml
  27. 14
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  28. 15
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java
  29. 4
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  30. 30
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
  31. 50
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
  32. 29
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
  33. 19
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java
  34. 2
      ui-ngx/src/app/core/http/ota-package.service.ts
  35. 32
      ui-ngx/src/app/core/services/menu.service.ts
  36. 19
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  37. 5
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  38. 4
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts
  39. 23
      ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.html
  40. 72
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-communication-config.component.html
  41. 44
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-communication-config.component.scss
  42. 218
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-communication-config.component.ts
  43. 81
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-mapping.component.html
  44. 49
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-mapping.component.scss
  45. 165
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-mapping.component.ts
  46. 46
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport-configuration.component.html
  47. 57
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport-configuration.component.ts
  48. 38
      ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport.module.ts
  49. 1
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  50. 2
      ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts
  51. 2
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  52. 10
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
  53. 24
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html
  54. 27
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts
  55. 2
      ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts
  56. 2
      ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts
  57. 2
      ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts
  58. 36
      ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts
  59. 44
      ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts
  60. 121
      ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.html
  61. 112
      ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.ts
  62. 20
      ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts
  63. 2
      ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts
  64. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts
  65. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  66. 6
      ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts
  67. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts
  68. 17
      ui-ngx/src/app/shared/models/constants.ts
  69. 95
      ui-ngx/src/app/shared/models/device.models.ts
  70. 4
      ui-ngx/src/app/shared/models/resource.models.ts
  71. 6
      ui-ngx/src/app/shared/models/tenant.model.ts
  72. 55
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  73. 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')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/devices/count/{otaPackageType}", method = RequestMethod.GET) @RequestMapping(value = "/devices/count/{otaPackageType}/{deviceProfileId}", method = RequestMethod.GET)
@ResponseBody @ResponseBody
public Long countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(@PathVariable("otaPackageType") String otaPackageType, public Long countByDeviceProfileAndEmptyOtaPackage(@PathVariable("otaPackageType") String otaPackageType,
@RequestParam String deviceProfileId) throws ThingsboardException { @PathVariable("deviceProfileId") String deviceProfileId) throws ThingsboardException {
checkParameter("OtaPackageType", otaPackageType); checkParameter("OtaPackageType", otaPackageType);
checkParameter("DeviceProfileId", deviceProfileId); checkParameter("DeviceProfileId", deviceProfileId);
try { try {
return deviceService.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage( 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) { } catch (Exception e) {
throw handleException(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.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@ -86,6 +87,7 @@ public class RuleChainController extends BaseController {
public static final String RULE_NODE_ID = "ruleNodeId"; public static final String RULE_NODE_ID = "ruleNodeId";
private static final ObjectMapper objectMapper = new ObjectMapper(); private static final ObjectMapper objectMapper = new ObjectMapper();
public static final int TIMEOUT = 20;
@Autowired @Autowired
private InstallScripts installScripts; private InstallScripts installScripts;
@ -388,25 +390,25 @@ public class RuleChainController extends BaseController {
TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data); TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data);
switch (scriptType) { switch (scriptType) {
case "update": case "update":
output = msgToOutput(engine.executeUpdate(inMsg)); output = msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
break; break;
case "generate": case "generate":
output = msgToOutput(engine.executeGenerate(inMsg)); output = msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
break; break;
case "filter": case "filter":
boolean result = engine.executeFilter(inMsg); boolean result = engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
output = Boolean.toString(result); output = Boolean.toString(result);
break; break;
case "switch": case "switch":
Set<String> states = engine.executeSwitch(inMsg); Set<String> states = engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
output = objectMapper.writeValueAsString(states); output = objectMapper.writeValueAsString(states);
break; break;
case "json": case "json":
JsonNode json = engine.executeJson(inMsg); JsonNode json = engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
output = objectMapper.writeValueAsString(json); output = objectMapper.writeValueAsString(json);
break; break;
case "string": case "string":
output = engine.executeToString(inMsg); output = engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
break; break;
default: default:
throw new IllegalArgumentException("Unsupported script type: " + scriptType); 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.ConcurrentHashMap;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger; 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); apiUsageClient.report(tenantId, customerId, ApiUsageRecordKey.JS_EXEC_COUNT, 1);
return doInvokeFunction(scriptId, functionName, args); return doInvokeFunction(scriptId, functionName, args);
} else { } else {
return Futures.immediateFailedFuture( String message = "Script invocation is blocked due to maximum error count "
new RuntimeException("Script invocation is blocked due to maximum error count " + getMaxErrors() + "!")); + getMaxErrors() + ", scriptId " + scriptId + "!";
log.warn(message);
return Futures.immediateFailedFuture(new RuntimeException(message));
} }
} else { } else {
return Futures.immediateFailedFuture(new RuntimeException("JS Execution is disabled due to API limits!")); 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 abstract long getMaxBlacklistDuration();
protected void onScriptExecutionError(UUID scriptId) { protected void onScriptExecutionError(UUID scriptId, Throwable t, String scriptBody) {
disabledFunctions.computeIfAbsent(scriptId, key -> new DisableListInfo()).incrementAndGet(); 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) { 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); return ((Invocable) engine).invokeFunction(functionName, args);
} }
} catch (Exception e) { } catch (Exception e) {
onScriptExecutionError(scriptId); onScriptExecutionError(scriptId, e, functionName);
throw new ExecutionException(e); 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.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; 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.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.TbQueueRequestTemplate;
@ -161,7 +161,8 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
@Override @Override
protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, String functionName, Object[] args) { 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) { if (scriptBody == null) {
return Futures.immediateFailedFuture(new RuntimeException("No script body found for scriptId: [" + scriptId + "]!")); return Futures.immediateFailedFuture(new RuntimeException("No script body found for scriptId: [" + scriptId + "]!"));
} }
@ -170,7 +171,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
.setScriptIdLSB(scriptId.getLeastSignificantBits()) .setScriptIdLSB(scriptId.getLeastSignificantBits())
.setFunctionName(functionName) .setFunctionName(functionName)
.setTimeout((int) maxRequestsTimeout) .setTimeout((int) maxRequestsTimeout)
.setScriptBody(scriptIdToBodysMap.get(scriptId)); .setScriptBody(scriptBody);
for (Object arg : args) { for (Object arg : args) {
jsRequestBuilder.addArgs(arg.toString()); jsRequestBuilder.addArgs(arg.toString());
@ -180,6 +181,9 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
.setInvokeRequest(jsRequestBuilder.build()) .setInvokeRequest(jsRequestBuilder.build())
.build(); .build();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(UUID.randomUUID(), jsRequestWrapper)); ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(UUID.randomUUID(), jsRequestWrapper));
if (maxRequestsTimeout > 0) { if (maxRequestsTimeout > 0) {
future = Futures.withTimeout(future, maxRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService); future = Futures.withTimeout(future, maxRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService);
@ -193,7 +197,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
onScriptExecutionError(scriptId); onScriptExecutionError(scriptId, t, scriptBody);
if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) { if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) {
queueTimeoutMsgs.incrementAndGet(); queueTimeoutMsgs.incrementAndGet();
} }
@ -201,13 +205,16 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
} }
}, callbackExecutor); }, callbackExecutor);
return Futures.transform(future, response -> { 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(); JsInvokeProtos.JsInvokeResponse invokeResult = response.getValue().getInvokeResponse();
if (invokeResult.getSuccess()) { if (invokeResult.getSuccess()) {
return invokeResult.getResult(); return invokeResult.getResult();
} else { } 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()); log.debug("[{}] Failed to compile script due to [{}]: {}", scriptId, invokeResult.getErrorCode().name(), invokeResult.getErrorDetails());
throw new RuntimeException(invokeResult.getErrorDetails()); throw e;
} }
}, callbackExecutor); }, 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.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; 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.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; 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.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -32,6 +32,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import javax.script.ScriptException; import javax.script.ScriptException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; 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(); String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType();
return TbMsg.transformMsg(msg, newMessageType, msg.getOriginator(), newMetadata, newData); return TbMsg.transformMsg(msg, newMessageType, msg.getOriginator(), newMetadata, newData);
} catch (Throwable th) { } catch (Throwable th) {
th.printStackTrace();
throw new RuntimeException("Failed to unbind message data from javascript result", th); throw new RuntimeException("Failed to unbind message data from javascript result", th);
} }
} }
@Override @Override
public List<TbMsg> executeUpdate(TbMsg msg) throws ScriptException { public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) {
JsonNode result = executeScript(msg); ListenableFuture<JsonNode> result = executeScriptAsync(msg);
if (result.isObject()) { return Futures.transformAsync(result,
return Collections.singletonList(unbindMsg(result, msg)); json -> executeUpdateTransform(msg, json),
} else if (result.isArray()){ MoreExecutors.directExecutor());
List<TbMsg> res = new ArrayList<>(result.size()); }
result.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg)));
return res; ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, JsonNode json) {
} else { if (json.isObject()) {
log.warn("Wrong result type: {}", result.getNodeType()); return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg)));
throw new ScriptException("Wrong result type: " + result.getNodeType()); } 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 @Override
public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) { public ListenableFuture<TbMsg> executeGenerateAsync(TbMsg prevMsg) {
ListenableFuture<JsonNode> result = executeScriptAsync(msg); return Futures.transformAsync(executeScriptAsync(prevMsg),
return Futures.transformAsync(result, json -> { result -> executeGenerateTransform(prevMsg, result),
if (json.isObject()) { MoreExecutors.directExecutor());
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());
} }
@Override ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, JsonNode result) {
public TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException {
JsonNode result = executeScript(prevMsg);
if (!result.isObject()) { if (!result.isObject()) {
log.warn("Wrong result type: {}", result.getNodeType()); 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 @Override
public JsonNode executeJson(TbMsg msg) throws ScriptException { public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) {
return executeScript(msg);
}
@Override
public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) throws ScriptException {
return executeScriptAsync(msg); return executeScriptAsync(msg);
} }
@Override @Override
public String executeToString(TbMsg msg) throws ScriptException { public ListenableFuture<String> executeToStringAsync(TbMsg msg) {
JsonNode result = executeScript(msg); return Futures.transformAsync(executeScriptAsync(msg),
if (!result.isTextual()) { this::executeToStringTransform,
log.warn("Wrong result type: {}", result.getNodeType()); MoreExecutors.directExecutor());
throw new ScriptException("Wrong result type: " + result.getNodeType());
}
return result.asText();
} }
@Override ListenableFuture<String> executeToStringTransform(JsonNode result) {
public boolean executeFilter(TbMsg msg) throws ScriptException { if (result.isTextual()) {
JsonNode result = executeScript(msg); return Futures.immediateFuture(result.asText());
if (!result.isBoolean()) {
log.warn("Wrong result type: {}", result.getNodeType());
throw new ScriptException("Wrong result type: " + result.getNodeType());
} }
return result.asBoolean(); log.warn("Wrong result type: {}", result.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
} }
@Override @Override
public ListenableFuture<Boolean> executeFilterAsync(TbMsg msg) { public ListenableFuture<Boolean> executeFilterAsync(TbMsg msg) {
ListenableFuture<JsonNode> result = executeScriptAsync(msg); return Futures.transformAsync(executeScriptAsync(msg),
return Futures.transformAsync(result, json -> { this::executeFilterTransform,
if (!json.isBoolean()) { MoreExecutors.directExecutor());
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());
} }
@Override ListenableFuture<Boolean> executeFilterTransform(JsonNode json) {
public Set<String> executeSwitch(TbMsg msg) throws ScriptException { if (json.isBoolean()) {
JsonNode result = executeScript(msg); 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()) { if (result.isTextual()) {
return Collections.singleton(result.asText()); return Futures.immediateFuture(Collections.singleton(result.asText()));
} else if (result.isArray()) { }
Set<String> nextStates = Sets.newHashSet(); if (result.isArray()) {
Set<String> nextStates = new HashSet<>();
for (JsonNode val : result) { for (JsonNode val : result) {
if (!val.isTextual()) { if (!val.isTextual()) {
log.warn("Wrong result type: {}", val.getNodeType()); 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 { } else {
nextStates.add(val.asText()); nextStates.add(val.asText());
} }
} }
return nextStates; return Futures.immediateFuture(nextStates);
} else {
log.warn("Wrong result type: {}", result.getNodeType());
throw new ScriptException("Wrong result type: " + result.getNodeType());
} }
log.warn("Wrong result type: {}", result.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
} }
private JsonNode executeScript(TbMsg msg) throws ScriptException { @Override
try { public ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg) {
String[] inArgs = prepareArgs(msg); return Futures.transformAsync(executeScriptAsync(msg),
String eval = sandboxService.invokeFunction(tenantId, msg.getCustomerId(), this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString(); this::executeSwitchTransform,
return mapper.readTree(eval); MoreExecutors.directExecutor()); //usually runs in a callbackExecutor
} 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);
}
} }
private ListenableFuture<JsonNode> executeScriptAsync(TbMsg msg) { ListenableFuture<JsonNode> executeScriptAsync(TbMsg msg) {
log.trace("execute script async, msg {}", msg);
String[] inArgs = prepareArgs(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 -> { o -> {
try { try {
return Futures.immediateFuture(mapper.readTree(o.toString())); return Futures.immediateFuture(mapper.readTree(o.toString()));

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

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

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 privacyPassphrase;
private String engineId; private String engineId;
public SnmpDeviceTransportConfiguration() {
this.host = "localhost";
this.port = 161;
this.protocolVersion = SnmpProtocolVersion.V2C;
this.community = "public";
}
@Override @Override
public DeviceTransportType getType() { public DeviceTransportType getType() {
return DeviceTransportType.SNMP; return DeviceTransportType.SNMP;
@ -76,7 +83,7 @@ public class SnmpDeviceTransportConfiguration implements DeviceTransportConfigur
isValid = StringUtils.isNotBlank(username) && StringUtils.isNotBlank(securityName) isValid = StringUtils.isNotBlank(username) && StringUtils.isNotBlank(securityName)
&& contextName != null && authenticationProtocol != null && contextName != null && authenticationProtocol != null
&& StringUtils.isNotBlank(authenticationPassphrase) && StringUtils.isNotBlank(authenticationPassphrase)
&& privacyProtocol != null && privacyPassphrase != null && engineId != null; && privacyProtocol != null && StringUtils.isNotBlank(privacyPassphrase) && engineId != null;
break; 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);
ListenableFuture<Response> send(Request request, long timeoutNs);
void stop(); void stop();
void setMessagesStats(MessagesStats messagesStats); 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.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import lombok.Builder; import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.TbStopWatch;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin; 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.queue.TbQueueRequestTemplate;
import org.thingsboard.server.common.stats.MessagesStats; import org.thingsboard.server.common.stats.MessagesStats;
import javax.annotation.Nullable;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j @Slf4j
public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response extends TbQueueMsg> extends AbstractTbQueueTemplate 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 TbQueueAdmin queueAdmin;
private final TbQueueProducer<Request> requestTemplate; private final TbQueueProducer<Request> requestTemplate;
private final TbQueueConsumer<Response> responseTemplate; private final TbQueueConsumer<Response> responseTemplate;
private final ConcurrentMap<UUID, DefaultTbQueueRequestTemplate.ResponseMetaData<Response>> pendingRequests; final ConcurrentHashMap<UUID, DefaultTbQueueRequestTemplate.ResponseMetaData<Response>> pendingRequests = new ConcurrentHashMap<>();
private final boolean internalExecutor; final boolean internalExecutor;
private final ExecutorService executor; final ExecutorService executor;
private final long maxRequestTimeout; final long maxRequestTimeoutNs;
private final long maxPendingRequests; final long maxPendingRequests;
private final long pollInterval; final long pollInterval;
private volatile long tickTs = 0L; volatile boolean stopped = false;
private volatile long tickSize = 0L; long nextCleanupNs = 0L;
private volatile boolean stopped = false; private final Lock cleanerLock = new ReentrantLock();
private MessagesStats messagesStats; private MessagesStats messagesStats;
@ -65,79 +71,113 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
long maxRequestTimeout, long maxRequestTimeout,
long maxPendingRequests, long maxPendingRequests,
long pollInterval, long pollInterval,
ExecutorService executor) { @Nullable ExecutorService executor) {
this.queueAdmin = queueAdmin; this.queueAdmin = queueAdmin;
this.requestTemplate = requestTemplate; this.requestTemplate = requestTemplate;
this.responseTemplate = responseTemplate; this.responseTemplate = responseTemplate;
this.pendingRequests = new ConcurrentHashMap<>(); this.maxRequestTimeoutNs = TimeUnit.MILLISECONDS.toNanos(maxRequestTimeout);
this.maxRequestTimeout = maxRequestTimeout;
this.maxPendingRequests = maxPendingRequests; this.maxPendingRequests = maxPendingRequests;
this.pollInterval = pollInterval; this.pollInterval = pollInterval;
if (executor != null) { this.internalExecutor = (executor == null);
internalExecutor = false; this.executor = internalExecutor ? createExecutor() : executor;
this.executor = executor; }
} else {
internalExecutor = true; ExecutorService createExecutor() {
this.executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-queue-request-template-" + responseTemplate.getTopic())); return Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-queue-request-template-" + responseTemplate.getTopic()));
}
} }
@Override @Override
public void init() { public void init() {
queueAdmin.createTopicIfNotExists(responseTemplate.getTopic()); queueAdmin.createTopicIfNotExists(responseTemplate.getTopic());
this.requestTemplate.init(); requestTemplate.init();
tickTs = System.currentTimeMillis();
responseTemplate.subscribe(); responseTemplate.subscribe();
executor.submit(() -> { executor.submit(this::mainLoop);
long nextCleanupMs = 0L; }
while (!stopped) {
try { void mainLoop() {
List<Response> responses = responseTemplate.poll(pollInterval); while (!stopped) {
if (responses.size() > 0) { TbStopWatch sw = TbStopWatch.startNew();
log.trace("Polling responses completed, consumer records count [{}]", responses.size()); try {
} fetchAndProcessResponses();
responses.forEach(response -> { } catch (Throwable e) {
byte[] requestIdHeader = response.getHeaders().get(REQUEST_ID_HEADER); long sleepNanos = TimeUnit.MILLISECONDS.toNanos(this.pollInterval) - sw.stopAndGetTotalTimeNanos();
UUID requestId; log.warn("Failed to obtain and process responses from queue. Going to sleep " + sleepNanos + "ns", e);
if (requestIdHeader == null) { sleep(sleepNanos);
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); void fetchAndProcessResponses() {
if (expectedResponse == null) { final long pendingRequestsCount = pendingRequests.mappingCount();
log.trace("[{}] Invalid or stale request", requestId); log.trace("Starting template pool topic {}, for pendingRequests {}", responseTemplate.getTopic(), pendingRequestsCount);
} else { List<Response> responses = doPoll(); //poll js responses
expectedResponse.future.set(response); 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 @Override
@ -164,17 +204,48 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
@Override @Override
public ListenableFuture<Response> send(Request request) { 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!")); return Futures.immediateFailedFuture(new RuntimeException("Pending request map is full!"));
} }
UUID requestId = UUID.randomUUID(); UUID requestId = UUID.randomUUID();
request.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId)); request.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId));
request.getHeaders().put(RESPONSE_TOPIC_HEADER, stringToBytes(responseTemplate.getTopic())); 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(); SettableFuture<Response> future = SettableFuture.create();
ResponseMetaData<Response> responseMetaData = new ResponseMetaData<>(tickTs + maxRequestTimeout, future); ResponseMetaData<Response> responseMetaData = new ResponseMetaData<>(currentClockNs + requestTimeoutNs, future, currentClockNs, requestTimeoutNs);
pendingRequests.putIfAbsent(requestId, responseMetaData); log.trace("pending {}", responseMetaData);
log.trace("[{}] Sending request, key [{}], expTime [{}]", requestId, request.getKey(), responseMetaData.expTime); 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) { if (messagesStats != null) {
messagesStats.incrementTotal(); messagesStats.incrementTotal();
} }
@ -184,7 +255,7 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
if (messagesStats != null) { if (messagesStats != null) {
messagesStats.incrementSuccessful(); messagesStats.incrementSuccessful();
} }
log.trace("[{}] Request sent: {}", requestId, metadata); log.trace("[{}] Request sent: {}, request {}", requestId, metadata, request);
} }
@Override @Override
@ -196,17 +267,32 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
future.setException(t); 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 long expTime;
private final SettableFuture<T> future; 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.expTime = ts;
this.future = future; 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.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.springframework.util.StopWatch;
import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.common.AbstractTbQueueConsumerTemplate; import org.thingsboard.server.queue.common.AbstractTbQueueConsumerTemplate;
@ -82,7 +83,16 @@ public class TbKafkaConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQue
@Override @Override
protected List<ConsumerRecord<String, byte[]>> doPoll(long durationInMillis) { 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)); ConsumerRecords<String, byte[]> records = consumer.poll(Duration.ofMillis(durationInMillis));
stopWatch.stop();
log.trace("poll topic {} took {}ms", getTopic(), stopWatch.getTotalTimeMillis());
if (records.isEmpty()) { if (records.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();
} else { } 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);
}
}

4
common/util/pom.xml

@ -36,6 +36,10 @@
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <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 lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.Cache; import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType; 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.data.kv.KvEntry;
import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.DefaultCounter;
import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.cache.CacheExecutorService;
import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.Validator;
import java.util.ArrayList; import java.util.ArrayList;
@ -45,7 +45,6 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.CacheConstants.ATTRIBUTES_CACHE;
import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; import static org.thingsboard.server.dao.attributes.AttributeUtils.validate;
@Service @Service
@ -59,12 +58,15 @@ public class CachedAttributesService implements AttributesService {
private final AttributesCacheWrapper cacheWrapper; private final AttributesCacheWrapper cacheWrapper;
private final DefaultCounter hitCounter; private final DefaultCounter hitCounter;
private final DefaultCounter missCounter; private final DefaultCounter missCounter;
private final CacheExecutorService cacheExecutorService;
public CachedAttributesService(AttributesDao attributesDao, public CachedAttributesService(AttributesDao attributesDao,
AttributesCacheWrapper cacheWrapper, AttributesCacheWrapper cacheWrapper,
StatsFactory statsFactory) { StatsFactory statsFactory,
CacheExecutorService cacheExecutorService) {
this.attributesDao = attributesDao; this.attributesDao = attributesDao;
this.cacheWrapper = cacheWrapper; this.cacheWrapper = cacheWrapper;
this.cacheExecutorService = cacheExecutorService;
this.hitCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "hit"); this.hitCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "hit");
this.missCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "miss"); 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 // TODO: think if it's a good idea to store 'empty' attributes
cacheWrapper.put(attributeCacheKey, foundAttrKvEntry.orElse(null)); cacheWrapper.put(attributeCacheKey, foundAttrKvEntry.orElse(null));
return foundAttrKvEntry; return foundAttrKvEntry;
}, MoreExecutors.directExecutor()); }, cacheExecutorService);
} }
} }
@ -111,7 +113,7 @@ public class CachedAttributesService implements AttributesService {
notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet()); notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet());
ListenableFuture<List<AttributeKvEntry>> result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); 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 // 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()); 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; 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) { public ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys) {
validate(entityId, scope); validate(entityId, scope);
ListenableFuture<List<Void>> future = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys); 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; 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 audit-log.sink.type=none
cache.type=caffeine cache.type=caffeine
cache.maximumPoolSize=16
#cache.type=redis #cache.type=redis
caffeine.specs.relations.timeToLiveInMinutes=1440 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'), Utils = require('./utils'),
JsExecutor = require('./jsExecutor'); JsExecutor = require('./jsExecutor');
const statFrequency = Number(config.get('script.stat_print_frequency'));
const scriptBodyTraceFrequency = Number(config.get('script.script_body_trace_frequency')); const scriptBodyTraceFrequency = Number(config.get('script.script_body_trace_frequency'));
const useSandbox = config.get('script.use_sandbox') === 'true'; const useSandbox = config.get('script.use_sandbox') === 'true';
const maxActiveScripts = Number(config.get('script.max_active_scripts')); 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'); const {performance} = require('perf_hooks');
function JsInvokeMessageProcessor(producer) { function JsInvokeMessageProcessor(producer) {
console.log("Producer:", producer);
this.producer = producer; this.producer = producer;
this.executor = new JsExecutor(useSandbox); this.executor = new JsExecutor(useSandbox);
this.scriptMap = {}; this.scriptMap = new Map();
this.scriptIds = []; this.scriptIds = [];
this.executedScriptsCounter = 0; this.executedScriptsCounter = 0;
this.lastStatTime = performance.now();
} }
JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) { JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function (message) {
var tStart = performance.now(); var tStart = performance.now();
let requestId; let requestId;
let responseTopic; let responseTopic;
@ -77,13 +78,13 @@ JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) {
var tFinish = performance.now(); var tFinish = performance.now();
var tTook = tFinish - tStart; var tTook = tFinish - tStart;
if ( tTook > slowQueryLogMs ) { if (tTook > slowQueryLogMs) {
let functionName; let functionName;
if (request.invokeRequest) { if (request.invokeRequest) {
try { try {
buf = Buffer.from(request.invokeRequest['functionName']); buf = Buffer.from(request.invokeRequest['functionName']);
functionName = buf.toString('utf8'); functionName = buf.toString('utf8');
} catch (err){ } catch (err) {
logger.error('[%s] Failed to read functionName from message header: %s', requestId, err.message); logger.error('[%s] Failed to read functionName from message header: %s', requestId, err.message);
logger.error(err.stack); 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); var scriptId = getScriptId(compileRequest);
logger.debug('[%s] Processing compile request, scriptId: [%s]', requestId, scriptId); 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); var scriptId = getScriptId(invokeRequest);
logger.debug('[%s] Processing invoke request, scriptId: [%s]', requestId, scriptId); logger.debug('[%s] Processing invoke request, scriptId: [%s]', requestId, scriptId);
this.executedScriptsCounter++; this.executedScriptsCounter++;
if ( this.executedScriptsCounter >= scriptBodyTraceFrequency ) { if (this.executedScriptsCounter % statFrequency == 0) {
this.executedScriptsCounter = 0; const nowMs = performance.now();
if (logger.levels[logger.level] >= logger.levels['debug']) { const msSinceLastStat = nowMs - this.lastStatTime;
logger.debug('[%s] Executing script body: [%s]', scriptId, invokeRequest.scriptBody); 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( this.getOrCompileScript(scriptId, invokeRequest.scriptBody).then(
(script) => { (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); var scriptId = getScriptId(releaseRequest);
logger.debug('[%s] Processing release request, scriptId: [%s]', requestId, scriptId); 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); var index = this.scriptIds.indexOf(scriptId);
if (index > -1) { if (index > -1) {
this.scriptIds.splice(index, 1); this.scriptIds.splice(index, 1);
} }
delete this.scriptMap[scriptId]; this.scriptMap.delete(scriptId);
} }
var releaseResponse = createReleaseResponse(scriptId, true); var releaseResponse = createReleaseResponse(scriptId, true);
logger.debug('[%s] Sending success release response, scriptId: [%s]', requestId, scriptId); 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 tStartSending = performance.now();
var remoteResponse = createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse); var remoteResponse = createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse);
var rawResponse = Buffer.from(JSON.stringify(remoteResponse), 'utf8'); 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( 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); 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; var self = this;
return new Promise(function(resolve, reject) { return new Promise(function (resolve, reject) {
if (self.scriptMap[scriptId]) { const script = self.scriptMap.get(scriptId);
resolve(self.scriptMap[scriptId]); if (script) {
resolve(script);
} else { } else {
self.executor.compileScript(scriptBody).then( self.executor.compileScript(scriptBody).then(
(script) => { (compiledScript) => {
self.cacheScript(scriptId, script); self.cacheScript(scriptId, compiledScript);
resolve(script); resolve(compiledScript);
}, },
(err) => { (err) => {
reject(err); reject(err);
@ -205,56 +213,57 @@ JsInvokeMessageProcessor.prototype.getOrCompileScript = function(scriptId, scrip
}); });
} }
JsInvokeMessageProcessor.prototype.cacheScript = function(scriptId, script) { JsInvokeMessageProcessor.prototype.cacheScript = function (scriptId, script) {
if (!this.scriptMap[scriptId]) { if (!this.scriptMap.has(scriptId)) {
this.scriptIds.push(scriptId); this.scriptIds.push(scriptId);
while (this.scriptIds.length > maxActiveScripts) { while (this.scriptIds.length > maxActiveScripts) {
logger.info('Active scripts count [%s] exceeds maximum limit [%s]', this.scriptIds.length, maxActiveScripts); logger.info('Active scripts count [%s] exceeds maximum limit [%s]', this.scriptIds.length, maxActiveScripts);
const prevScriptId = this.scriptIds.shift(); const prevScriptId = this.scriptIds.shift();
logger.info('Removing active script with id [%s]', prevScriptId); 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) { function createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse) {
const requestIdBits = Utils.UUIDToBits(requestId); const requestIdBits = Utils.UUIDToBits(requestId);
return { return {
requestIdMSB: requestIdBits[0], requestIdMSB: requestIdBits[0],
requestIdLSB: requestIdBits[1], requestIdLSB: requestIdBits[1],
compileResponse: compileResponse, compileResponse: compileResponse,
invokeResponse: invokeResponse, invokeResponse: invokeResponse,
releaseResponse: releaseResponse releaseResponse: releaseResponse
}; };
} }
function createCompileResponse(scriptId, success, errorCode, err) { function createCompileResponse(scriptId, success, errorCode, err) {
const scriptIdBits = Utils.UUIDToBits(scriptId); const scriptIdBits = Utils.UUIDToBits(scriptId);
return { return {
errorCode: errorCode, errorCode: errorCode,
success: success, success: success,
errorDetails: parseJsErrorDetails(err), errorDetails: parseJsErrorDetails(err),
scriptIdMSB: scriptIdBits[0], scriptIdMSB: scriptIdBits[0],
scriptIdLSB: scriptIdBits[1] scriptIdLSB: scriptIdBits[1]
}; };
} }
function createInvokeResponse(result, success, errorCode, err) { function createInvokeResponse(result, success, errorCode, err) {
return { return {
errorCode: errorCode, errorCode: errorCode,
success: success, success: success,
errorDetails: parseJsErrorDetails(err), errorDetails: parseJsErrorDetails(err),
result: result result: result
}; };
} }
function createReleaseResponse(scriptId, success) { function createReleaseResponse(scriptId, success) {
const scriptIdBits = Utils.UUIDToBits(scriptId); const scriptIdBits = Utils.UUIDToBits(scriptId);
return { return {
success: success, success: success,
scriptIdMSB: scriptIdBits[0], scriptIdMSB: scriptIdBits[0],
scriptIdLSB: scriptIdBits[1] 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) 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" request_topic: "REMOTE_JS_EVAL_REQUEST_TOPIC"
http_port: "HTTP_PORT" # /livenessProbe
js: js:
response_poll_interval: "REMOTE_JS_RESPONSE_POLL_INTERVAL_MS" response_poll_interval: "REMOTE_JS_RESPONSE_POLL_INTERVAL_MS"
@ -26,6 +27,9 @@ kafka:
servers: "TB_KAFKA_SERVERS" servers: "TB_KAFKA_SERVERS"
replication_factor: "TB_QUEUE_KAFKA_REPLICATION_FACTOR" replication_factor: "TB_QUEUE_KAFKA_REPLICATION_FACTOR"
acks: "TB_KAFKA_ACKS" # -1 = all; 0 = no acknowledgments; 1 = only waits for the leader to acknowledge 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" requestTimeout: "TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS"
compression: "TB_QUEUE_KAFKA_COMPRESSION" # gzip or uncompressed compression: "TB_QUEUE_KAFKA_COMPRESSION" # gzip or uncompressed
topic_properties: "TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES" topic_properties: "TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES"
@ -70,6 +74,7 @@ logger:
script: script:
use_sandbox: "SCRIPT_USE_SANDBOX" use_sandbox: "SCRIPT_USE_SANDBOX"
stat_print_frequency: "SCRIPT_STAT_PRINT_FREQUENCY"
script_body_trace_frequency: "SCRIPT_BODY_TRACE_FREQUENCY" script_body_trace_frequency: "SCRIPT_BODY_TRACE_FREQUENCY"
max_active_scripts: "MAX_ACTIVE_SCRIPTS" max_active_scripts: "MAX_ACTIVE_SCRIPTS"
slow_query_log_ms: "SLOW_QUERY_LOG_MS" #1.123456 slow_query_log_ms: "SLOW_QUERY_LOG_MS" #1.123456

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

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

1
msa/js-executor/package.json

@ -18,6 +18,7 @@
"aws-sdk": "^2.741.0", "aws-sdk": "^2.741.0",
"azure-sb": "^0.11.1", "azure-sb": "^0.11.1",
"config": "^3.3.1", "config": "^3.3.1",
"express": "^4.17.1",
"js-yaml": "^3.14.0", "js-yaml": "^3.14.0",
"kafkajs": "^1.15.0", "kafkajs": "^1.15.0",
"long": "^4.0.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 topicProperties = config.get('kafka.topic_properties');
const kafkaClientId = config.get('kafka.client_id'); const kafkaClientId = config.get('kafka.client_id');
const acks = Number(config.get('kafka.acks')); 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 requestTimeout = Number(config.get('kafka.requestTimeout'));
const compressionType = (config.get('kafka.compression') === "gzip") ? CompressionTypes.GZIP : CompressionTypes.None; const compressionType = (config.get('kafka.compression') === "gzip") ? CompressionTypes.GZIP : CompressionTypes.None;
const partitionsConsumedConcurrently = Number(config.get('kafka.partitions_consumed_concurrently'));
let kafkaClient; let kafkaClient;
let kafkaAdmin; let kafkaAdmin;
@ -33,22 +36,65 @@ let producer;
const configEntries = []; const configEntries = [];
let batchMessages = [];
let sendLoopInstance;
function KafkaProducer() { function KafkaProducer() {
this.send = async (responseTopic, scriptId, rawResponse, headers) => { this.send = async (responseTopic, scriptId, rawResponse, headers) => {
return producer.send( logger.debug('Pending queue response, scriptId: [%s]', scriptId);
{ const message = {
topic: responseTopic, 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, acks: acks,
compression: compressionType, compression: compressionType
messages: [ })
{ logger.debug('Response batch sent to kafka, length: [%s]', messagesToSend.length);
key: scriptId, } catch(err) {
value: rawResponse, logger.error('Failed batch send to kafka, length: [%s], pending to reprocess msgs', messagesToSend.length);
headers: headers.data logger.error(err.stack);
} batchMessages = messagesToSend.concat(batchMessages);
] }
});
} }
sendLoopWithLinger();
} }
(async () => { (async () => {
@ -64,8 +110,8 @@ function KafkaProducer() {
let kafkaConfig = { let kafkaConfig = {
brokers: kafkaBootstrapServers.split(','), brokers: kafkaBootstrapServers.split(','),
logLevel: logLevel.INFO, logLevel: logLevel.INFO,
logCreator: KafkaJsWinstonLogCreator logCreator: KafkaJsWinstonLogCreator
}; };
if (kafkaClientId) { if (kafkaClientId) {
@ -114,14 +160,45 @@ function KafkaProducer() {
consumer = kafkaClient.consumer({groupId: 'js-executor-group'}); consumer = kafkaClient.consumer({groupId: 'js-executor-group'});
producer = kafkaClient.producer(); 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()); const messageProcessor = new JsInvokeMessageProcessor(new KafkaProducer());
await consumer.connect(); await consumer.connect();
await producer.connect(); await producer.connect();
sendLoopWithLinger();
await consumer.subscribe({topic: requestTopic}); await consumer.subscribe({topic: requestTopic});
logger.info('Started ThingsBoard JavaScript Executor Microservice.'); logger.info('Started ThingsBoard JavaScript Executor Microservice.');
await consumer.run({ await consumer.run({
//partitionsConsumedConcurrently: 1, // Default: 1 partitionsConsumedConcurrently: partitionsConsumedConcurrently,
eachMessage: async ({topic, partition, message}) => { eachMessage: async ({topic, partition, message}) => {
let headers = message.headers; let headers = message.headers;
let key = message.key; let key = message.key;
@ -197,6 +274,9 @@ async function disconnectProducer() {
var _producer = producer; var _producer = producer;
producer = null; producer = null;
try { try {
logger.info('Stopping loop...');
clearTimeout(sendLoopInstance);
await sendMessagesAsBatch();
await _producer.disconnect(); await _producer.disconnect();
logger.info('Kafka Producer stopped.'); logger.info('Kafka Producer stopped.');
} catch (e) { } catch (e) {

2
msa/js-executor/server.js

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

361
msa/js-executor/yarn.lock

@ -418,6 +418,14 @@ abort-controller@^3.0.0:
dependencies: dependencies:
event-target-shim "^5.0.0" 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: agent-base@6:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4"
@ -487,6 +495,11 @@ argparse@^1.0.7:
dependencies: dependencies:
sprintf-js "~1.0.2" 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: array-union@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" 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" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== 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: boxen@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" 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" resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1"
integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= 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: cacheable-request@^6.0.0:
version "6.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" 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" write-file-atomic "^3.0.0"
xdg-basedir "^4.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: core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 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: dependencies:
ms "^2.1.1" 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: debug@4, debug@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
@ -874,13 +937,6 @@ debug@4, debug@^4.1.1:
dependencies: dependencies:
ms "^2.1.1" 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: decamelize@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 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" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= 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: dir-glob@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" 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: dependencies:
safe-buffer "^5.0.1" 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: emoji-regex@^7.0.1:
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" 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" resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== 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: end-of-stream@^1.0.0, end-of-stream@^1.1.0:
version "1.4.4" version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" 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" resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== 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: escodegen@^1.14.1:
version "1.14.3" version "1.14.3"
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" 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" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== 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: event-target-shim@^5.0.0:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" 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" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== 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: extend@^3.0.2, extend@~3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
@ -1119,6 +1241,19 @@ fill-range@^7.0.1:
dependencies: dependencies:
to-regex-range "^5.0.1" 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: find-up@^4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" 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" combined-stream "^1.0.6"
mime-types "^2.1.12" 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: from2@^2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" 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" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== 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: http-signature@~1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" 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" agent-base "6"
debug "4" 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: ieee754@1.1.13, ieee754@^1.1.4:
version "1.1.13" version "1.1.13"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" 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" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= 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" version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@ -1449,6 +1623,11 @@ into-stream@^5.1.1:
from2 "^2.3.0" from2 "^2.3.0"
p-is-promise "^3.0.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: is-arrayish@^0.3.1:
version "0.3.2" version "0.3.2"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
@ -1764,11 +1943,26 @@ make-dir@^3.0.0:
dependencies: dependencies:
semver "^6.0.0" 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: merge2@^1.3.0:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== 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: micromatch@^4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" 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" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== 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: mime-types@^2.1.12, mime-types@~2.1.19:
version "2.1.27" version "2.1.27"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" 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: dependencies:
mime-db "1.44.0" 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: mime@^2.2.0:
version "2.4.6" version "2.4.6"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" 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" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 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: ms@^2.1.1:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@ -1846,6 +2062,11 @@ multistream@^2.1.1:
inherits "^2.0.1" inherits "^2.0.1"
readable-stream "^2.0.5" 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: node-fetch@^2.3.0, node-fetch@^2.6.0:
version "2.6.0" version "2.6.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" 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" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea"
integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg== 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: once@^1.3.1, once@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 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" registry-url "^5.0.0"
semver "^6.2.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: path-exists@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 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" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== 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: path-type@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" 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" "@types/node" "^13.7.0"
long "^4.0.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: psl@^1.1.28, psl@^1.1.33:
version "1.8.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
@ -2114,6 +2360,11 @@ pupa@^2.0.1:
dependencies: dependencies:
escape-goat "^2.0.0" 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: qs@~6.5.2:
version "6.5.2" version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" 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" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== 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: rc@^1.2.8:
version "1.2.8" version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" 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" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== 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: safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
version "5.2.1" version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@~5.1.2: "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
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:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@ -2339,11 +2605,45 @@ semver@^7.1.3:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== 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: set-blocking@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= 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: signal-exit@^3.0.2:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" 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" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= 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: stream-browserify@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" 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: dependencies:
is-number "^7.0.0" 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: touch@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" 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" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== 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: typedarray-to-buffer@^3.1.5:
version "3.1.5" version "3.1.5"
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" 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" resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d"
integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== 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: update-notifier@^4.0.0:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.1.tgz#895fc8562bbe666179500f9f2cebac4f26323746" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.1.tgz#895fc8562bbe666179500f9f2cebac4f26323746"
@ -2705,6 +3028,11 @@ util@^0.11.1:
dependencies: dependencies:
inherits "2.0.3" 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: uuid-parse@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b" 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" resolved "https://registry.yarnpkg.com/validator/-/validator-9.4.1.tgz#abf466d398b561cd243050112c6ff1de6cc12663"
integrity sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA== 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: verror@1.10.0:
version "1.10.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"

5
pom.xml

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

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

@ -104,6 +104,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import org.thingsboard.server.common.data.oauth2.OAuth2Info; import org.thingsboard.server.common.data.oauth2.OAuth2Info;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
@ -1257,7 +1258,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
params.put("deviceProfileId", deviceProfileId.getId().toString()); params.put("deviceProfileId", deviceProfileId.getId().toString());
return restTemplate.exchange( return restTemplate.exchange(
baseURL + "/api/devices/count/{otaPackageType}?deviceProfileId={deviceProfileId}", baseURL + "/api/devices/count/{otaPackageType}/{deviceProfileId}",
HttpMethod.GET, HttpMethod.GET,
HttpEntity.EMPTY, HttpEntity.EMPTY,
new ParameterizedTypeReference<Long>() { new ParameterizedTypeReference<Long>() {
@ -1772,7 +1773,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}).getBody(); }).getBody();
} }
public List<OAuth2ClientInfo> getOAuth2Clients(String pkgName) { public List<OAuth2ClientInfo> getOAuth2Clients(String pkgName, PlatformType platformType) {
Map<String, String> params = new HashMap<>(); Map<String, String> params = new HashMap<>();
StringBuilder urlBuilder = new StringBuilder(baseURL); StringBuilder urlBuilder = new StringBuilder(baseURL);
urlBuilder.append("/api/noauth/oauth2Clients"); urlBuilder.append("/api/noauth/oauth2Clients");
@ -1780,6 +1781,15 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
urlBuilder.append("?pkgName={pkgName}"); urlBuilder.append("?pkgName={pkgName}");
params.put("pkgName", pkgName); params.put("pkgName", pkgName);
} }
if (platformType != null) {
if (pkgName != null) {
urlBuilder.append("&");
} else {
urlBuilder.append("?");
}
urlBuilder.append("platform={platform}");
params.put("platform", platformType.name());
}
return restTemplate.exchange( return restTemplate.exchange(
urlBuilder.toString(), urlBuilder.toString(),
HttpMethod.POST, HttpMethod.POST,

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 com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import javax.script.ScriptException;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
public interface ScriptEngine { public interface ScriptEngine {
List<TbMsg> executeUpdate(TbMsg msg) throws ScriptException;
ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg); ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg);
TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException; ListenableFuture<TbMsg> executeGenerateAsync(TbMsg prevMsg);
boolean executeFilter(TbMsg msg) throws ScriptException;
ListenableFuture<Boolean> executeFilterAsync(TbMsg msg); ListenableFuture<Boolean> executeFilterAsync(TbMsg msg);
Set<String> executeSwitch(TbMsg msg) throws ScriptException; ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg);
JsonNode executeJson(TbMsg msg) throws ScriptException;
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(); 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(); EdgeEventService getEdgeEventService();
/**
* Js script executors call are completely asynchronous
* */
@Deprecated
ListeningExecutor getJsExecutor(); ListeningExecutor getJsExecutor();
ListeningExecutor getMailExecutor(); 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; 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 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.RuleNode;
import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
@ -55,18 +58,21 @@ public class TbLogNode implements TbNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
ListeningExecutor jsExecutor = ctx.getJsExecutor();
ctx.logJsEvalRequest(); ctx.logJsEvalRequest();
withCallback(jsExecutor.executeAsync(() -> jsEngine.executeToString(msg)), Futures.addCallback(jsEngine.executeToStringAsync(msg), new FutureCallback<String>() {
toString -> { @Override
ctx.logJsEvalResponse(); public void onSuccess(@Nullable String result) {
log.info(toString); ctx.logJsEvalResponse();
ctx.tellSuccess(msg); log.info(result);
}, ctx.tellSuccess(msg);
t -> { }
ctx.logJsEvalResponse();
ctx.tellFailure(msg, t); @Override
}); public void onFailure(Throwable t) {
ctx.logJsEvalResponse();
ctx.tellFailure(msg, t);
}
}, MoreExecutors.directExecutor()); //usually js responses runs on js callback executor
} }
@Override @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; 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.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.thingsboard.common.util.TbStopWatch;
import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.rule.engine.api.TbContext; 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.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.thingsboard.common.util.DonAsynchron.withCallback; import static org.thingsboard.common.util.DonAsynchron.withCallback;
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
@ -64,10 +68,11 @@ public class TbMsgGeneratorNode implements TbNode {
private EntityId originatorId; private EntityId originatorId;
private UUID nextTickId; private UUID nextTickId;
private TbMsg prevMsg; private TbMsg prevMsg;
private volatile boolean initialized; private final AtomicBoolean initialized = new AtomicBoolean(false);
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
log.trace("init generator with config {}", configuration);
this.config = TbNodeUtils.convert(configuration, TbMsgGeneratorNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbMsgGeneratorNodeConfiguration.class);
this.delay = TimeUnit.SECONDS.toMillis(config.getPeriodInSeconds()); this.delay = TimeUnit.SECONDS.toMillis(config.getPeriodInSeconds());
this.currentMsgCount = 0; this.currentMsgCount = 0;
@ -81,35 +86,39 @@ public class TbMsgGeneratorNode implements TbNode {
@Override @Override
public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) { public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) {
log.trace("onPartitionChangeMsg, PartitionChangeMsg {}, config {}", msg, config);
updateGeneratorState(ctx); updateGeneratorState(ctx);
} }
private void updateGeneratorState(TbContext ctx) { private void updateGeneratorState(TbContext ctx) {
log.trace("updateGeneratorState, config {}", config);
if (ctx.isLocalEntity(originatorId)) { if (ctx.isLocalEntity(originatorId)) {
if (!initialized) { if (initialized.compareAndSet(false, true)) {
initialized = true;
this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType"); this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType");
scheduleTickMsg(ctx); scheduleTickMsg(ctx);
} }
} else if (initialized) { } else if (initialized.compareAndSet(true, false)) {
initialized = false;
destroy(); destroy();
} }
} }
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { 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), withCallback(generate(ctx, msg),
m -> { 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); ctx.enqueueForTellNext(m, SUCCESS);
scheduleTickMsg(ctx); scheduleTickMsg(ctx);
currentMsgCount++; currentMsgCount++;
} }
}, },
t -> { 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); ctx.tellFailure(msg, t);
scheduleTickMsg(ctx); scheduleTickMsg(ctx);
currentMsgCount++; currentMsgCount++;
@ -119,6 +128,7 @@ public class TbMsgGeneratorNode implements TbNode {
} }
private void scheduleTickMsg(TbContext ctx) { private void scheduleTickMsg(TbContext ctx) {
log.trace("scheduleTickMsg, config {}", config);
long curTs = System.currentTimeMillis(); long curTs = System.currentTimeMillis();
if (lastScheduledTs == 0L) { if (lastScheduledTs == 0L) {
lastScheduledTs = curTs; lastScheduledTs = curTs;
@ -131,22 +141,26 @@ public class TbMsgGeneratorNode implements TbNode {
} }
private ListenableFuture<TbMsg> generate(TbContext ctx, TbMsg msg) { private ListenableFuture<TbMsg> generate(TbContext ctx, TbMsg msg) {
return ctx.getJsExecutor().executeAsync(() -> { log.trace("generate, config {}", config);
if (prevMsg == null) { if (prevMsg == null) {
prevMsg = ctx.newMsg(ServiceQueue.MAIN, "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}"); prevMsg = ctx.newMsg(ServiceQueue.MAIN, "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}");
} }
if (initialized) { if (initialized.get()) {
ctx.logJsEvalRequest(); ctx.logJsEvalRequest();
TbMsg generated = jsEngine.executeGenerate(prevMsg); return Futures.transformAsync(jsEngine.executeGenerateAsync(prevMsg), generated -> {
log.trace("generate process response, generated {}, config {}", generated, config);
ctx.logJsEvalResponse(); ctx.logJsEvalResponse();
prevMsg = ctx.newMsg(ServiceQueue.MAIN, generated.getType(), originatorId, msg.getCustomerId(), generated.getMetaData(), generated.getData()); prevMsg = ctx.newMsg(ServiceQueue.MAIN, generated.getType(), originatorId, msg.getCustomerId(), generated.getMetaData(), generated.getData());
} return Futures.immediateFuture(prevMsg);
return prevMsg; }, MoreExecutors.directExecutor()); //usually it runs on js-executor-remote-callback thread pool
}); }
return Futures.immediateFuture(prevMsg);
} }
@Override @Override
public void destroy() { public void destroy() {
log.trace("destroy, config {}", config);
prevMsg = null; prevMsg = null;
if (jsEngine != null) { if (jsEngine != null) {
jsEngine.destroy(); 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; 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 lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.ScriptEngine;
@ -29,8 +33,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.Set; import java.util.Set;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.FILTER, type = ComponentType.FILTER,
@ -58,17 +60,20 @@ public class TbJsSwitchNode implements TbNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
ListeningExecutor jsExecutor = ctx.getJsExecutor();
ctx.logJsEvalRequest(); ctx.logJsEvalRequest();
withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(msg)), Futures.addCallback(jsEngine.executeSwitchAsync(msg), new FutureCallback<Set<String>>() {
result -> { @Override
ctx.logJsEvalResponse(); public void onSuccess(@Nullable Set<String> result) {
processSwitch(ctx, msg, result); ctx.logJsEvalResponse();
}, processSwitch(ctx, msg, result);
t -> { }
ctx.logJsEvalFailure();
ctx.tellFailure(msg, t); @Override
}, ctx.getDbCallbackExecutor()); 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) { 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()); private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased());
@Test @Test
public void multipleRoutesAreAllowed() throws TbNodeException, ScriptException { public void multipleRoutesAreAllowed() throws TbNodeException {
initWithScript(); initWithScript();
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10"); metaData.putValue("temp", "10");
@ -72,11 +72,9 @@ public class TbJsSwitchNodeTest {
String rawJson = "{\"name\": \"Vit\", \"passed\": 5}"; String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId);
mockJsExecutor(); when(scriptEngine.executeSwitchAsync(msg)).thenReturn(Futures.immediateFuture(Sets.newHashSet("one", "three")));
when(scriptEngine.executeSwitch(msg)).thenReturn(Sets.newHashSet("one", "three"));
node.onMsg(ctx, msg); node.onMsg(ctx, msg);
verify(ctx).getJsExecutor();
verify(ctx).tellNext(msg, Sets.newHashSet("one", "three")); verify(ctx).tellNext(msg, Sets.newHashSet("one", "three"));
} }
@ -92,19 +90,6 @@ public class TbJsSwitchNodeTest {
node.init(ctx, nodeConfiguration); 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) { private void verifyError(TbMsg msg, String message, Class expectedClass) {
ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class); ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
verify(ctx).tellFailure(same(msg), captor.capture()); 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> { 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, 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) { if (authState.edgesSupportEnabled) {
sections.push( sections.push(
{
id: guid(),
name: 'edge.edge-instances',
type: 'link',
path: '/edgeInstances',
icon: 'router'
},
{ {
id: guid(), id: guid(),
name: 'edge.management', name: 'edge.management',
type: 'toggle', type: 'toggle',
path: '/edges', path: '/edgeManagement',
height: '80px', height: '40px',
icon: 'router', icon: 'settings_input_antenna',
pages: [ pages: [
{
id: guid(),
name: 'edge.edge-instances',
type: 'link',
path: '/edges',
icon: 'router'
},
{ {
id: guid(), id: guid(),
name: 'edge.rulechain-templates', name: 'edge.rulechain-templates',
type: 'link', type: 'link',
path: '/edges/ruleChains', path: '/edgeManagement/ruleChains',
icon: 'settings_ethernet' icon: 'settings_ethernet'
} }
] ]
@ -448,12 +448,12 @@ export class MenuService {
{ {
name: 'edge.edge-instances', name: 'edge.edge-instances',
icon: 'router', icon: 'router',
path: '/edges' path: '/edgeInstances'
}, },
{ {
name: 'edge.rulechain-templates', name: 'edge.rulechain-templates',
icon: 'settings_ethernet', icon: 'settings_ethernet',
path: '/edges/ruleChains' path: '/edgeManagement/ruleChains'
} }
] ]
} }
@ -548,7 +548,7 @@ export class MenuService {
id: guid(), id: guid(),
name: 'edge.edge-instances', name: 'edge.edge-instances',
type: 'link', type: 'link',
path: '/edges', path: '/edgeInstances',
icon: 'router' icon: 'router'
} }
); );
@ -606,8 +606,8 @@ export class MenuService {
places: [ places: [
{ {
name: 'edge.edge-instances', name: 'edge.edge-instances',
icon: 'router', icon: 'settings_input_antenna',
path: '/edges' path: '/edgeInstances'
} }
] ]
} }

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

@ -128,6 +128,7 @@ import {
DashboardImageDialogComponent, DashboardImageDialogComponent,
DashboardImageDialogData, DashboardImageDialogResult DashboardImageDialogData, DashboardImageDialogResult
} from '@home/components/dashboard-page/dashboard-image-dialog.component'; } from '@home/components/dashboard-page/dashboard-image-dialog.component';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
// @dynamic // @dynamic
@Component({ @Component({
@ -211,7 +212,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
addingLayoutCtx: DashboardPageLayoutContext; addingLayoutCtx: DashboardPageLayoutContext;
logo = 'assets/logo_title_white.svg'; private dashboardLogoCache: SafeUrl;
private defaultDashboardLogo = 'assets/logo_title_white.svg';
dashboardCtx: DashboardContext = { dashboardCtx: DashboardContext = {
instanceId: this.utils.guid(), instanceId: this.utils.guid(),
@ -312,7 +314,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private ngZone: NgZone, private ngZone: NgZone,
private overlay: Overlay, private overlay: Overlay,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef,
private sanitizer: DomSanitizer) {
super(store); super(store);
} }
@ -413,6 +416,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private reset() { private reset() {
this.dashboard = null; this.dashboard = null;
this.dashboardConfiguration = null; this.dashboardConfiguration = null;
this.dashboardLogoCache = undefined;
this.prevDashboard = null; this.prevDashboard = null;
this.widgetEditMode = false; this.widgetEditMode = false;
@ -570,8 +574,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
} }
public get dashboardLogo(): string { public get dashboardLogo(): SafeUrl {
return this.dashboard.configuration.settings.dashboardLogoUrl || this.logo; if (!this.dashboardLogoCache) {
const logo = this.dashboard.configuration.settings.dashboardLogoUrl || this.defaultDashboardLogo;
this.dashboardLogoCache = this.sanitizer.bypassSecurityTrustUrl(logo);
}
return this.dashboardLogoCache;
} }
public showRightLayoutSwitch(): boolean { public showRightLayoutSwitch(): boolean {
@ -702,6 +710,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}).afterClosed().subscribe((data) => { }).afterClosed().subscribe((data) => {
if (data) { if (data) {
this.dashboard.configuration.settings = data.settings; this.dashboard.configuration.settings = data.settings;
this.dashboardLogoCache = undefined;
const newGridSettings = data.gridSettings; const newGridSettings = data.gridSettings;
if (newGridSettings) { if (newGridSettings) {
const layout = this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout]; 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 (this.widgetEditMode) {
if (revert) { if (revert) {
this.dashboard = this.prevDashboard; this.dashboard = this.prevDashboard;
this.dashboardLogoCache = undefined;
} }
} else { } else {
this.resetHighlight(); this.resetHighlight();
if (revert) { if (revert) {
this.dashboard = this.prevDashboard; this.dashboard = this.prevDashboard;
this.dashboardLogoCache = undefined;
this.dashboardConfiguration = this.dashboard.configuration; this.dashboardConfiguration = this.dashboard.configuration;
this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
this.entityAliasesUpdated(); 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 { DeviceProfileAutocompleteComponent } from '@home/components/profile/device-profile-autocomplete.component';
import { MqttDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/mqtt-device-profile-transport-configuration.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 { 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 { DeviceProfileAlarmsComponent } from '@home/components/profile/alarm/device-profile-alarms.component';
import { DeviceProfileAlarmComponent } from '@home/components/profile/alarm/device-profile-alarm.component'; import { DeviceProfileAlarmComponent } from '@home/components/profile/alarm/device-profile-alarm.component';
import { CreateAlarmRulesComponent } from '@home/components/profile/alarm/create-alarm-rules.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 { SecurityConfigLwm2mServerComponent } from '@home/components/device/security-config-lwm2m-server.component';
import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component'; import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component';
import { WidgetContainerComponent } from '@home/components/widget/widget-container.component'; import { WidgetContainerComponent } from '@home/components/widget/widget-container.component';
import { SnmpDeviceProfileTransportModule } from '@home/components/profile/device/snpm/snmp-device-profile-transport.module';
@NgModule({ @NgModule({
declarations: declarations:
@ -228,7 +228,6 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain
DefaultDeviceProfileTransportConfigurationComponent, DefaultDeviceProfileTransportConfigurationComponent,
MqttDeviceProfileTransportConfigurationComponent, MqttDeviceProfileTransportConfigurationComponent,
CoapDeviceProfileTransportConfigurationComponent, CoapDeviceProfileTransportConfigurationComponent,
SnmpDeviceProfileTransportConfigurationComponent,
DeviceProfileTransportConfigurationComponent, DeviceProfileTransportConfigurationComponent,
CreateAlarmRulesComponent, CreateAlarmRulesComponent,
AlarmRuleComponent, AlarmRuleComponent,
@ -272,6 +271,7 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain
SharedModule, SharedModule,
SharedHomeComponentsModule, SharedHomeComponentsModule,
Lwm2mProfileComponentsModule, Lwm2mProfileComponentsModule,
SnmpDeviceProfileTransportModule,
StatesControllerModule StatesControllerModule
], ],
exports: [ exports: [
@ -339,7 +339,6 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain
DefaultDeviceProfileTransportConfigurationComponent, DefaultDeviceProfileTransportConfigurationComponent,
MqttDeviceProfileTransportConfigurationComponent, MqttDeviceProfileTransportConfigurationComponent,
CoapDeviceProfileTransportConfigurationComponent, CoapDeviceProfileTransportConfigurationComponent,
SnmpDeviceProfileTransportConfigurationComponent,
DeviceProfileTransportConfigurationComponent, DeviceProfileTransportConfigurationComponent,
CreateAlarmRulesComponent, CreateAlarmRulesComponent,
AlarmRuleComponent, 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) { if (configuration) {
delete configuration.type; delete configuration.type;
} }
this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); setTimeout(() => {
this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false});
});
} }
private updateModel() { 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 { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import {
import { Store } from '@ngrx/store'; ControlValueAccessor,
import { AppState } from '@app/core/core.state'; FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { import {
DeviceProfileTransportConfiguration, DeviceProfileTransportConfiguration,
@ -40,19 +47,24 @@ export interface OidMappingConfiguration {
selector: 'tb-snmp-device-profile-transport-configuration', selector: 'tb-snmp-device-profile-transport-configuration',
templateUrl: './snmp-device-profile-transport-configuration.component.html', templateUrl: './snmp-device-profile-transport-configuration.component.html',
styleUrls: [], styleUrls: [],
providers: [{ providers: [
provide: NG_VALUE_ACCESSOR, {
useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent), provide: NG_VALUE_ACCESSOR,
multi: true 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; snmpDeviceProfileTransportConfigurationFormGroup: FormGroup;
private destroy$ = new Subject(); private destroy$ = new Subject();
private requiredValue: boolean; private requiredValue: boolean;
private configuration = [];
get required(): boolean { get required(): boolean {
return this.requiredValue; return this.requiredValue;
@ -69,12 +81,14 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements Control
private propagateChange = (v: any) => { private propagateChange = (v: any) => {
} }
constructor(private store: Store<AppState>, private fb: FormBuilder) { constructor(private fb: FormBuilder) {
} }
ngOnInit(): void { ngOnInit(): void {
this.snmpDeviceProfileTransportConfigurationFormGroup = this.fb.group({ 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( this.snmpDeviceProfileTransportConfigurationFormGroup.valueChanges.pipe(
takeUntil(this.destroy$) takeUntil(this.destroy$)
@ -95,18 +109,33 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements Control
registerOnTouched(fn: any): void { 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 { writeValue(value: SnmpDeviceProfileTransportConfiguration | null): void {
if (isDefinedAndNotNull(value)) { if (isDefinedAndNotNull(value)) {
this.snmpDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); this.snmpDeviceProfileTransportConfigurationFormGroup.patchValue(value, {emitEvent: !value.communicationConfigs});
} }
} }
private updateModel() { private updateModel() {
let configuration: DeviceProfileTransportConfiguration = null; let configuration: DeviceProfileTransportConfiguration = null;
if (this.snmpDeviceProfileTransportConfigurationFormGroup.valid) { if (this.snmpDeviceProfileTransportConfigurationFormGroup.valid) {
configuration = this.snmpDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration; configuration = this.snmpDeviceProfileTransportConfigurationFormGroup.getRawValue();
configuration.type = DeviceTransportType.SNMP; configuration.type = DeviceTransportType.SNMP;
} }
this.propagateChange(configuration); 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.updateTitle(true);
this.alarmsDatasource.updateAlarms(); this.alarmsDatasource.updateAlarms();
this.clearCache(); this.clearCache();
this.ctx.detectChanges();
} }
public pageLinkSortDirection(): SortDirection { 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 dataPageData = subscription.dataPages[0];
const childNodes: HierarchyNavTreeNode[] = []; const childNodes: HierarchyNavTreeNode[] = [];
datasourcesPageData.data.forEach((childDatasource, index) => { 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; nodeCtx.childrenNodesLoaded = true;
childrenNodesLoadCb(this.prepareNodes(childNodes)); 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, isDefined,
isNumber, isNumber,
isObject, isObject,
isString,
isUndefined isUndefined
} from '@core/utils'; } from '@core/utils';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
@ -236,6 +235,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.updateTitle(true); this.updateTitle(true);
this.entityDatasource.dataUpdated(); this.entityDatasource.dataUpdated();
this.clearCache(); this.clearCache();
this.ctx.detectChanges();
} }
public pageLinkSortDirection(): SortDirection { 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( this.config.cellActionDescriptors.push(
{ {
name: this.translate.instant('resource.export'), name: this.translate.instant('resource.download'),
icon: 'file_download', icon: 'file_download',
isEnabled: () => true, 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; return this.config;
} }
exportResource($event: Event, resource: ResourceInfo) { downloadResource($event: Event, resource: ResourceInfo) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -127,8 +127,8 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
onResourceAction(action: EntityAction<ResourceInfo>): boolean { onResourceAction(action: EntityAction<ResourceInfo>): boolean {
switch (action.action) { switch (action.action) {
case 'uploadResource': case 'downloadResource':
this.exportResource(action.event, action.entity); this.downloadResource(action.event, action.entity);
return true; return true;
} }
return false; 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"> <div class="tb-details-buttons" fxLayout.xs="column">
<button mat-raised-button color="primary" fxFlex.xs <button mat-raised-button color="primary" fxFlex.xs
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'uploadResource')" (click)="onEntityAction($event, 'downloadResource')"
[fxShow]="!isEdit"> [fxShow]="!isEdit">
{{'resource.export' | translate }} {{ 'resource.download' | translate }}
</button> </button>
<button mat-raised-button color="primary" fxFlex.xs <button mat-raised-button color="primary" fxFlex.xs
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')" (click)="onEntityAction($event, 'delete')"
[fxShow]="!hideDelete() && !isEdit"> [fxShow]="!hideDelete() && !isEdit">
{{'resource.delete' | translate }} {{ 'resource.delete' | translate }}
</button> </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>
<div class="mat-padding" fxLayout="column"> <div class="mat-padding" fxLayout="column">
<form [formGroup]="entityForm"> <form [formGroup]="entityForm">
@ -47,7 +57,7 @@
{{ 'resource.title-required' | translate }} {{ 'resource.title-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<tb-file-input <tb-file-input *ngIf="isAdd"
formControlName="data" formControlName="data"
required required
[readAsBinary]="true" [readAsBinary]="true"
@ -59,6 +69,12 @@
[existingFileName]="entityForm.get('fileName')?.value" [existingFileName]="entityForm.get('fileName')?.value"
(fileNameChanged)="entityForm?.get('fileName').patchValue($event)"> (fileNameChanged)="entityForm?.get('fileName').patchValue($event)">
</tb-file-input> </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> </fieldset>
</form> </form>
</div> </div>

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

@ -30,6 +30,7 @@ import {
ResourceTypeTranslationMap ResourceTypeTranslationMap
} from '@shared/models/resource.models'; } from '@shared/models/resource.models';
import { pairwise, startWith, takeUntil } from 'rxjs/operators'; import { pairwise, startWith, takeUntil } from 'rxjs/operators';
import { ActionNotificationShow } from "@core/notification/notification.actions";
@Component({ @Component({
selector: 'tb-resources-library', selector: 'tb-resources-library',
@ -88,26 +89,29 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
} }
buildForm(entity: Resource): FormGroup { buildForm(entity: Resource): FormGroup {
return this.fb.group( const form = this.fb.group(
{ {
title: [entity ? entity.title : '', []],
resourceType: [{ resourceType: [{
value: entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, value: entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL,
disabled: this.isEdit disabled: !this.isAdd
}, [Validators.required]], }, [Validators.required]],
data: [entity ? entity.data : null, [Validators.required]],
fileName: [entity ? entity.fileName : 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) { updateForm(entity: Resource) {
this.entityForm.patchValue({resourceType: entity.resourceType});
if (this.isEdit) { if (this.isEdit) {
this.entityForm.get('resourceType').disable({emitEvent: false}); this.entityForm.get('resourceType').disable({emitEvent: false});
this.entityForm.get('fileName').disable({emitEvent: false});
} }
this.entityForm.patchValue({ this.entityForm.patchValue({
data: entity.data, resourceType: entity.resourceType,
fileName: entity.fileName, fileName: entity.fileName,
title: entity.title title: entity.title
}); });
@ -132,4 +136,15 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
convertToBase64File(data: string): string { convertToBase64File(data: string): string {
return window.btoa(data); 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, component: EntitiesTableComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN], 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) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
this.router.navigateByUrl(`customers/${customer.id.id}/edges`); this.router.navigateByUrl(`customers/${customer.id.id}/edgeInstances`);
} }
onCustomerAction(action: EntityAction<Customer>): boolean { 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') { if (this.config.componentsData.dashboardScope === 'customer') {
this.router.navigateByUrl(`customers/${this.config.componentsData.customerId}/dashboards/${dashboard.id.id}`); this.router.navigateByUrl(`customers/${this.config.componentsData.customerId}/dashboards/${dashboard.id.id}`);
} else if (this.config.componentsData.dashboardScope === 'edge') { } 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 { } else {
this.router.navigateByUrl(`dashboards/${dashboard.id.id}`); 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 { 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 { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state'; import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
@ -29,13 +38,20 @@ import {
selector: 'tb-device-data', selector: 'tb-device-data',
templateUrl: './device-data.component.html', templateUrl: './device-data.component.html',
styleUrls: [], styleUrls: [],
providers: [{ providers: [
provide: NG_VALUE_ACCESSOR, {
useExisting: forwardRef(() => DeviceDataComponent), provide: NG_VALUE_ACCESSOR,
multi: true 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; deviceDataFormGroup: FormGroup;
@ -97,6 +113,12 @@ export class DeviceDataComponent implements ControlValueAccessor, OnInit {
this.deviceDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false}); this.deviceDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false});
} }
validate(): ValidationErrors | null {
return this.deviceDataFormGroup.valid ? null : {
deviceDataForm: false
};
}
private updateModel() { private updateModel() {
let deviceData: DeviceData = null; let deviceData: DeviceData = null;
if (this.deviceDataFormGroup.valid) { 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 { 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 { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state'; import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { import { DeviceTransportConfiguration, DeviceTransportType } from '@shared/models/device.models';
DeviceTransportConfiguration,
DeviceTransportType
} from '@shared/models/device.models';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
@Component({ @Component({
selector: 'tb-device-transport-configuration', selector: 'tb-device-transport-configuration',
templateUrl: './device-transport-configuration.component.html', templateUrl: './device-transport-configuration.component.html',
styleUrls: [], styleUrls: [],
providers: [{ providers: [
provide: NG_VALUE_ACCESSOR, {
useExisting: forwardRef(() => DeviceTransportConfigurationComponent), provide: NG_VALUE_ACCESSOR,
multi: true 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; deviceTransportType = DeviceTransportType;
@ -92,7 +104,15 @@ export class DeviceTransportConfigurationComponent implements ControlValueAccess
if (configuration) { if (configuration) {
delete configuration.type; 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() { 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. limitations under the License.
--> -->
<form [formGroup]="snmpDeviceTransportConfigurationFormGroup" style="padding-bottom: 16px;"> <form [formGroup]="snmpDeviceTransportConfigurationFormGroup" style="padding-bottom: 16px;" fxLayoutGap="8px" fxLayout="column">
<tb-json-object-edit <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column">
[required]="required" <mat-form-field fxFlex>
label="{{ 'device-profile.transport-type-snmp-hint' | translate }}" <mat-label translate>device-profile.snmp.host</mat-label>
formControlName="configuration"> <input matInput formControlName="host" required>
</tb-json-object-edit> <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> </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. /// limitations under the License.
/// ///
import {Component, forwardRef, Input, OnInit} from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators} from '@angular/forms'; import {
import {Store} from '@ngrx/store'; ControlValueAccessor,
import {AppState} from '@app/core/core.state'; FormBuilder,
import {coerceBooleanProperty} from '@angular/cdk/coercion'; 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 { import {
DeviceTransportConfiguration, DeviceTransportConfiguration,
DeviceTransportType, DeviceTransportType,
SnmpDeviceTransportConfiguration SnmpAuthenticationProtocol,
SnmpAuthenticationProtocolTranslationMap,
SnmpDeviceProtocolVersion,
SnmpDeviceTransportConfiguration,
SnmpPrivacyProtocol,
SnmpPrivacyProtocolTranslationMap
} from '@shared/models/device.models'; } from '@shared/models/device.models';
import { isDefinedAndNotNull } from '@core/utils';
@Component({ @Component({
selector: 'tb-snmp-device-transport-configuration', selector: 'tb-snmp-device-transport-configuration',
templateUrl: './snmp-device-transport-configuration.component.html', templateUrl: './snmp-device-transport-configuration.component.html',
styleUrls: [], styleUrls: [],
providers: [{ providers: [
provide: NG_VALUE_ACCESSOR, {
useExisting: forwardRef(() => SnmpDeviceTransportConfigurationComponent), provide: NG_VALUE_ACCESSOR,
multi: true 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; snmpDeviceTransportConfigurationFormGroup: FormGroup;
snmpDeviceProtocolVersions = Object.values(SnmpDeviceProtocolVersion);
snmpAuthenticationProtocols = Object.values(SnmpAuthenticationProtocol);
snmpAuthenticationProtocolTranslation = SnmpAuthenticationProtocolTranslationMap;
snmpPrivacyProtocols = Object.values(SnmpPrivacyProtocol);
snmpPrivacyProtocolTranslation = SnmpPrivacyProtocolTranslationMap;
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {
@ -53,8 +79,7 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc
@Input() @Input()
disabled: boolean; disabled: boolean;
private propagateChange = (v: any) => { private propagateChange = (v: any) => { };
};
constructor(private store: Store<AppState>, constructor(private store: Store<AppState>,
private fb: FormBuilder) { private fb: FormBuilder) {
@ -69,13 +94,33 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc
ngOnInit() { ngOnInit() {
this.snmpDeviceTransportConfigurationFormGroup = this.fb.group({ 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.snmpDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => {
this.updateModel(); this.updateModel();
}); });
} }
validate(): ValidationErrors | null {
return this.snmpDeviceTransportConfigurationFormGroup.valid ? null : {
snmpDeviceTransportConfiguration: false
};
}
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) { if (this.disabled) {
@ -86,13 +131,46 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc
} }
writeValue(value: SnmpDeviceTransportConfiguration | null): void { 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() { private updateModel() {
let configuration: DeviceTransportConfiguration = null; let configuration: DeviceTransportConfiguration = null;
if (this.snmpDeviceTransportConfigurationFormGroup.valid) { if (this.snmpDeviceTransportConfigurationFormGroup.valid) {
configuration = this.snmpDeviceTransportConfigurationFormGroup.getRawValue().configuration; configuration = this.snmpDeviceTransportConfigurationFormGroup.value;
configuration.type = DeviceTransportType.SNMP; configuration.type = DeviceTransportType.SNMP;
} }
this.propagateChange(configuration); this.propagateChange(configuration);

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

@ -42,7 +42,7 @@ import {
const routes: Routes = [ const routes: Routes = [
{ {
path: 'edges', path: 'edgeInstances',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'edge.edge-instances', 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', 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'; suffix = 'ruleChains';
break; break;
} }
this.router.navigateByUrl(`edges/${edge.id.id}/${suffix}`); this.router.navigateByUrl(`edgeInstances/${edge.id.id}/${suffix}`);
} }
assignToCustomer($event: Event, edgesIds: Array<EdgeId>) { 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.ruleNode.targetRuleChainId) {
if (this.ruleChainType === RuleChainType.EDGE) { if (this.ruleChainType === RuleChainType.EDGE) {
this.router.navigateByUrl(`/edges/ruleChains/${this.ruleNode.targetRuleChainId}`); this.router.navigateByUrl(`/edgeManagement/ruleChains/${this.ruleNode.targetRuleChainId}`);
} else { } else {
this.router.navigateByUrl(`/ruleChains/${this.ruleNode.targetRuleChainId}`); 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) { if (this.ruleChainType !== RuleChainType.EDGE) {
this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`); this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`);
} else { } else {
this.router.navigateByUrl(`edges/ruleChains/${this.ruleChain.id.id}`); this.router.navigateByUrl(`edgeManagement/ruleChains/${this.ruleChain.id.id}`);
} }
} else { } else {
this.createRuleChainModel(); 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) { if (ruleChainImport) {
this.itembuffer.storeRuleChainImport(ruleChainImport); this.itembuffer.storeRuleChainImport(ruleChainImport);
if (this.config.componentsData.ruleChainScope === 'edges') { if (this.config.componentsData.ruleChainScope === 'edges') {
this.router.navigateByUrl(`edges/ruleChains/ruleChain/import`); this.router.navigateByUrl(`edgeManagement/ruleChains/ruleChain/import`);
} else { } else {
this.router.navigateByUrl(`ruleChains/ruleChain/import`); this.router.navigateByUrl(`ruleChains/ruleChain/import`);
} }
@ -298,9 +298,9 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
$event.stopPropagation(); $event.stopPropagation();
} }
if (this.config.componentsData.ruleChainScope === 'edges') { 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') { } 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 { } else {
this.router.navigateByUrl(`ruleChains/${ruleChain.id.id}`); 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.targetRuleChainId) {
if (node.ruleChainType === RuleChainType.EDGE) { if (node.ruleChainType === RuleChainType.EDGE) {
this.router.navigateByUrl(`/edges/ruleChains/${node.targetRuleChainId}`); this.router.navigateByUrl(`/edgeManagement/ruleChains/${node.targetRuleChainId}`);
} else { } else {
this.router.navigateByUrl(`/ruleChains/${node.targetRuleChainId}`); 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', entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views',
entitiesImport: helpBaseUrl + '/docs/user-guide/bulk-provisioning', entitiesImport: helpBaseUrl + '/docs/user-guide/bulk-provisioning',
rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains', rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains',
resources: helpBaseUrl + '/docs/user-guide/ui/resources',
dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards', dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards',
otaUpdates: helpBaseUrl + '/docs/user-guide/ui/ota-updates', otaUpdates: helpBaseUrl + '/docs/user-guide/ui/ota-updates',
widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles', widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles',
@ -147,6 +148,22 @@ export enum ValueType {
JSON = 'JSON' 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>( 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 { AbstractControl, ValidationErrors } from '@angular/forms';
import { OtaPackageId } from '@shared/models/id/ota-package-id'; import { OtaPackageId } from '@shared/models/id/ota-package-id';
import { DashboardId } from '@shared/models/id/dashboard-id'; import { DashboardId } from '@shared/models/id/dashboard-id';
import { DataType } from '@shared/models/constants';
export enum DeviceProfileType { export enum DeviceProfileType {
DEFAULT = 'DEFAULT', DEFAULT = 'DEFAULT',
@ -257,7 +258,35 @@ export interface Lwm2mDeviceProfileTransportConfiguration {
} }
export interface SnmpDeviceProfileTransportConfiguration { 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 & export type DeviceProfileTransportConfigurations = DefaultDeviceProfileTransportConfiguration &
@ -332,7 +361,11 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT
transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M}; transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M};
break; break;
case DeviceTransportType.SNMP: case DeviceTransportType.SNMP:
const snmpTransportConfiguration: SnmpDeviceProfileTransportConfiguration = {}; const snmpTransportConfiguration: SnmpDeviceProfileTransportConfiguration = {
timeoutMs: 500,
retries: 0,
communicationConfigs: null
};
transportConfiguration = {...snmpTransportConfiguration, type: DeviceTransportType.SNMP}; transportConfiguration = {...snmpTransportConfiguration, type: DeviceTransportType.SNMP};
break; break;
} }
@ -361,7 +394,12 @@ export function createDeviceTransportConfiguration(type: DeviceTransportType): D
transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M}; transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M};
break; break;
case DeviceTransportType.SNMP: case DeviceTransportType.SNMP:
const snmpTransportConfiguration: SnmpDeviceTransportConfiguration = {}; const snmpTransportConfiguration: SnmpDeviceTransportConfiguration = {
host: 'localhost',
port: 161,
protocolVersion: SnmpDeviceProtocolVersion.V2C,
community: 'public'
};
transportConfiguration = {...snmpTransportConfiguration, type: DeviceTransportType.SNMP}; transportConfiguration = {...snmpTransportConfiguration, type: DeviceTransportType.SNMP};
break; break;
} }
@ -539,8 +577,57 @@ export interface Lwm2mDeviceTransportConfiguration {
[key: string]: any; [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 { 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 & 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 { export interface Resources extends ResourceInfo {
data: string|string[]; data: Array<string>;
fileName: string|string[]; fileName: Array<string>;
} }

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

@ -49,8 +49,10 @@ export interface DefaultTenantProfileConfiguration {
maxRuleNodeExecutionsPerMessage: number; maxRuleNodeExecutionsPerMessage: number;
maxEmails: number; maxEmails: number;
maxSms: number; maxSms: number;
maxCreatedAlarms: number;
defaultStorageTtlDays: number; defaultStorageTtlDays: number;
alarmsTtlDays: number;
} }
export type TenantProfileConfigurations = DefaultTenantProfileConfiguration; export type TenantProfileConfigurations = DefaultTenantProfileConfiguration;
@ -81,7 +83,9 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
maxRuleNodeExecutionsPerMessage: 0, maxRuleNodeExecutionsPerMessage: 0,
maxEmails: 0, maxEmails: 0,
maxSms: 0, maxSms: 0,
defaultStorageTtlDays: 0 maxCreatedAlarms: 0,
defaultStorageTtlDays: 0,
alarmsTtlDays: 0
}; };
configuration = {...defaultConfiguration, type: TenantProfileType.DEFAULT}; configuration = {...defaultConfiguration, type: TenantProfileType.DEFAULT};
break; 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": "Software update CoAP recourse",
"sw-update-recourse-required": "Software update CoAP recourse is required.", "sw-update-recourse-required": "Software update CoAP recourse is required.",
"config-json-tab": "Json Config Profile Device" "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": { "dialog": {
@ -2309,20 +2357,23 @@
}, },
"resource": { "resource": {
"add": "Add Resource", "add": "Add Resource",
"copyId": "Copy resource Id",
"delete": "Delete resource", "delete": "Delete resource",
"delete-resource-text": "Be careful, after the confirmation the resource will become unrecoverable.", "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-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-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-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} }?", "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.", "drop-file": "Drop a resource file or click to select a file to upload.",
"empty": "Resource is empty", "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-matching": "No resource matching '{{widgetsBundle}}' were found.",
"no-resource-text": "No resources found", "no-resource-text": "No resources found",
"open-widgets-bundle": "Open widgets bundle", "open-widgets-bundle": "Open widgets bundle",
"resource": "Resource", "resource": "Resource",
"resource-library-details": "Resource library details", "resource-library-details": "Resource details",
"resource-type": "Resource type", "resource-type": "Resource type",
"resources-library": "Resources library", "resources-library": "Resources library",
"search": "Search resources", "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 {# 个资源} }?", "delete-resources-title": "确定要删除 { count, plural, 1 {# 个资源} other {# 个资源} }?",
"drop-file": "拖拽资源文件或单击以选择要上传的文件。", "drop-file": "拖拽资源文件或单击以选择要上传的文件。",
"empty": "资源为空", "empty": "资源为空",
"export": "导出资源",
"no-resource-matching": "找不到与 '{{widgetsBundle}}' 匹配的资源。", "no-resource-matching": "找不到与 '{{widgetsBundle}}' 匹配的资源。",
"no-resource-text": "找不到资源", "no-resource-text": "找不到资源",
"open-widgets-bundle": "打开部件库", "open-widgets-bundle": "打开部件库",

Loading…
Cancel
Save