Browse Source

Merge branch 'develop/3.0' of https://github.com/thingsboard/thingsboard into map/3.0

pull/2738/head
Adsumus 6 years ago
parent
commit
746e63ab63
  1. 4
      application/pom.xml
  2. 4
      application/src/main/data/json/demo/rule_chains/root_rule_chain.json
  3. 4
      application/src/main/data/json/demo/rule_chains/thermostat_alarms.json
  4. 29
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  5. 12
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java
  6. 24
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  7. 13
      application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
  8. 2
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  9. 13
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  10. 18
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  11. 1
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  12. 5
      application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java
  13. 10
      application/src/main/java/org/thingsboard/server/service/queue/processing/BurstTbRuleEngineSubmitStrategy.java
  14. 8
      application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByEntityIdTbRuleEngineSubmitStrategy.java
  15. 1
      application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByTenantIdTbRuleEngineSubmitStrategy.java
  16. 6
      application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialTbRuleEngineSubmitStrategy.java
  17. 8
      application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java
  18. 7
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java
  19. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java
  20. 8
      application/src/main/resources/thingsboard.yml
  21. 12
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
  22. 12
      common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeException.java
  23. 1
      common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusAdmin.java
  24. 118
      common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusConsumerTemplate.java
  25. 7
      common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusProducerTemplate.java
  26. 53
      common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractParallelTbQueueConsumerTemplate.java
  27. 152
      common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java
  28. 107
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java
  29. 32
      common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubAdmin.java
  30. 105
      common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubConsumerTemplate.java
  31. 6
      common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubProducerTemplate.java
  32. 2
      common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqAdmin.java
  33. 107
      common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplate.java
  34. 14
      common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqProducerTemplate.java
  35. 139
      common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsConsumerTemplate.java
  36. 5
      common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsProducerTemplate.java
  37. 1
      common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsQueueAttributes.java
  38. 23
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
  39. 13
      common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
  40. 13
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportServiceCallback.java
  41. 4
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java
  42. 6
      docker/docker-compose.hybrid.yml
  43. 2
      docker/tb-js-executor.env
  44. 12
      msa/js-executor/config/custom-environment-variables.yml
  45. 12
      msa/js-executor/config/default.yml
  46. 1320
      msa/js-executor/package-lock.json
  47. 1
      msa/js-executor/package.json
  48. 38
      msa/js-executor/queue/awsSqsTemplate.js
  49. 2
      msa/js-executor/queue/kafkaTemplate.js
  50. 29
      msa/js-executor/queue/pubSubTemplate.js
  51. 26
      msa/js-executor/queue/rabbitmqTemplate.js
  52. 3
      msa/js-executor/queue/serviceBusTemplate.js
  53. 2
      msa/js-executor/server.js
  54. 38
      pom.xml
  55. 5
      rule-engine/rule-engine-api/pom.xml
  56. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
  57. 580
      ui-ngx/package-lock.json
  58. 38
      ui-ngx/package.json
  59. 4
      ui-ngx/src/app/core/api/datasource-subcription.ts
  60. 4
      ui-ngx/src/app/core/api/datasource.service.ts
  61. 58
      ui-ngx/src/app/core/services/utils.service.ts
  62. 223
      ui-ngx/src/app/core/utils.ts
  63. 4
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  64. 4
      ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts
  65. 4
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  66. 23
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts
  67. 6
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts
  68. 12
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
  69. 175
      ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts
  70. 7
      ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts
  71. 7
      ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts
  72. 4
      ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts
  73. 2
      ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts
  74. 2
      ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts
  75. 28
      ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts
  76. 4
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  77. 3
      ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts
  78. 2
      ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.html
  79. 27
      ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.ts
  80. 1
      ui-ngx/src/app/shared/pipe/public-api.ts
  81. 25
      ui-ngx/src/app/shared/pipe/template.pipe.ts
  82. 3
      ui-ngx/src/app/shared/shared.module.ts
  83. 6
      ui/src/app/locale/locale.constant-de_DE.json
  84. 4
      ui/src/app/locale/locale.constant-el_GR.json
  85. 4
      ui/src/app/locale/locale.constant-es_ES.json
  86. 6
      ui/src/app/locale/locale.constant-fr_FR.json
  87. 6
      ui/src/app/locale/locale.constant-it_IT.json
  88. 4
      ui/src/app/locale/locale.constant-ja_JA.json
  89. 4
      ui/src/app/locale/locale.constant-ko_KR.json
  90. 6
      ui/src/app/locale/locale.constant-ro_RO.json
  91. 6
      ui/src/app/locale/locale.constant-tr_TR.json

4
application/pom.xml

@ -193,8 +193,8 @@
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>

4
application/src/main/data/json/demo/rule_chains/root_rule_chain.json

@ -17,7 +17,7 @@
},
"type": "org.thingsboard.rule.engine.filter.TbJsFilterNode",
"name": "Is Thermostat?",
"debugMode": true,
"debugMode": false,
"configuration": {
"jsScript": "return msg.id.entityType === \"DEVICE\" && msg.type === \"thermostat\";"
}
@ -113,7 +113,7 @@
},
"type": "org.thingsboard.rule.engine.action.TbCreateRelationNode",
"name": "Relate to Asset",
"debugMode": true,
"debugMode": false,
"configuration": {
"direction": "FROM",
"relationType": "ToAlarmPropagationAsset",

4
application/src/main/data/json/demo/rule_chains/thermostat_alarms.json

@ -81,7 +81,7 @@
},
"type": "org.thingsboard.rule.engine.filter.TbJsSwitchNode",
"name": "Check Alarms",
"debugMode": true,
"debugMode": false,
"configuration": {
"jsScript": "var relations = [];\nif(metadata[\"ss_alarmTemperature\"] === \"true\"){\n if(msg.temperature > metadata[\"ss_thresholdTemperature\"]){\n relations.push(\"NewTempAlarm\");\n } else {\n relations.push(\"ClearTempAlarm\");\n }\n}\nif(metadata[\"ss_alarmHumidity\"] === \"true\"){\n if(msg.humidity < metadata[\"ss_thresholdHumidity\"]){\n relations.push(\"NewHumidityAlarm\");\n } else {\n relations.push(\"ClearHumidityAlarm\");\n }\n}\n\nreturn relations;"
}
@ -93,7 +93,7 @@
},
"type": "org.thingsboard.rule.engine.metadata.TbGetAttributesNode",
"name": "Fetch Configuration",
"debugMode": true,
"debugMode": false,
"configuration": {
"clientAttributeNames": [],
"sharedAttributeNames": [],

29
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java

@ -163,7 +163,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
String dispatcherName = tenantId.getId().equals(EntityId.NULL_UUID) ?
DefaultActorService.SYSTEM_RULE_DISPATCHER_NAME : DefaultActorService.TENANT_RULE_DISPATCHER_NAME;
return context.actorOf(
Props.create(new RuleNodeActor.ActorCreator(systemContext, tenantId, entityId, ruleNode.getId()))
Props.create(new RuleNodeActor.ActorCreator(systemContext, tenantId, entityId, ruleNode.getName(), ruleNode.getId()))
.withDispatcher(dispatcherName), ruleNode.getId().toString());
}
@ -200,7 +200,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
log.trace("[{}][{}] Processing message [{}]: {}", entityId, firstId, msg.getId(), msg);
if (envelope.getRelationTypes() == null || envelope.getRelationTypes().isEmpty()) {
try {
checkActive();
checkActive(envelope.getTbMsg());
RuleNodeId targetId = msg.getRuleNodeId();
RuleNodeCtx targetCtx;
if (targetId == null) {
@ -216,6 +216,8 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
log.trace("[{}][{}] Rule node does not exist. Probably old message", entityId, targetId);
msg.getCallback().onSuccess();
}
} catch (RuleNodeException rne) {
envelope.getTbMsg().getCallback().onFailure(rne);
} catch (Exception e) {
envelope.getTbMsg().getCallback().onFailure(new RuleEngineException(e.getMessage()));
}
@ -225,11 +227,15 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
}
void onRuleChainToRuleChainMsg(RuleChainToRuleChainMsg envelope) {
checkActive();
if (firstNode != null) {
pushMsgToNode(firstNode, envelope.getMsg(), envelope.getFromRelationType());
} else {
envelope.getMsg().getCallback().onSuccess();
try {
checkActive(envelope.getMsg());
if (firstNode != null) {
pushMsgToNode(firstNode, envelope.getMsg(), envelope.getFromRelationType());
} else {
envelope.getMsg().getCallback().onSuccess();
}
} catch (RuleNodeException e) {
log.debug("Rule Chain is not active. Current state [{}] for processor [{}][{}] tenant [{}]", state, entityId.getEntityType(), entityId, tenantId);
}
}
@ -239,7 +245,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
private void onTellNext(TbMsg msg, RuleNodeId originatorNodeId, Set<String> relationTypes, String failureMessage) {
try {
checkActive();
checkActive(msg);
EntityId entityId = msg.getOriginator();
TopicPartitionInfo tpi = systemContext.resolve(ServiceType.TB_RULE_ENGINE, tenantId, entityId);
List<RuleNodeRelation> relations = nodeRoutes.get(originatorNodeId).stream()
@ -272,6 +278,8 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
putToQueue(tpi, msg, callbackWrapper, target);
}
}
} catch (RuleNodeException rne) {
msg.getCallback().onFailure(rne);
} catch (Exception e) {
msg.getCallback().onFailure(new RuleEngineException("onTellNext - " + e.getMessage()));
}
@ -333,4 +341,9 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
}
}
@Override
protected RuleNodeException getInactiveException() {
RuleNode firstRuleNode = firstNode != null ? firstNode.getSelf() : null;
return new RuleNodeException("Rule Chain is not active! Failed to initialize.", ruleChainName, firstRuleNode);
}
}

12
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java

@ -27,12 +27,14 @@ import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
public class RuleNodeActor extends ComponentActor<RuleNodeId, RuleNodeActorMessageProcessor> {
private final String ruleChainName;
private final RuleChainId ruleChainId;
private RuleNodeActor(ActorSystemContext systemContext, TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
private RuleNodeActor(ActorSystemContext systemContext, TenantId tenantId, RuleChainId ruleChainId, String ruleChainName, RuleNodeId ruleNodeId) {
super(systemContext, tenantId, ruleNodeId);
this.ruleChainName = ruleChainName;
this.ruleChainId = ruleChainId;
setProcessor(new RuleNodeActorMessageProcessor(tenantId, ruleChainId, ruleNodeId, systemContext,
setProcessor(new RuleNodeActorMessageProcessor(tenantId, this.ruleChainName, ruleNodeId, systemContext,
context().parent(), context().self()));
}
@ -96,19 +98,21 @@ public class RuleNodeActor extends ComponentActor<RuleNodeId, RuleNodeActorMessa
private final TenantId tenantId;
private final RuleChainId ruleChainId;
private final String ruleChainName;
private final RuleNodeId ruleNodeId;
public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleChainId ruleChainId, String ruleChainName, RuleNodeId ruleNodeId) {
super(context);
this.tenantId = tenantId;
this.ruleChainId = ruleChainId;
this.ruleChainName = ruleChainName;
this.ruleNodeId = ruleNodeId;
}
@Override
public RuleNodeActor create() throws Exception {
return new RuleNodeActor(context, tenantId, ruleChainId, ruleNodeId);
return new RuleNodeActor(context, tenantId, ruleChainId, ruleChainName, ruleNodeId);
}
}

24
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java

@ -17,37 +17,33 @@ package org.thingsboard.server.actors.ruleChain;
import akka.actor.ActorContext;
import akka.actor.ActorRef;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.common.msg.queue.RuleNodeException;
/**
* @author Andrew Shvayka
*/
public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> {
private final ActorRef parent;
private final String ruleChainName;
private final ActorRef self;
private final RuleChainService service;
private RuleNode ruleNode;
private TbNode tbNode;
private DefaultTbContext defaultCtx;
RuleNodeActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId, ActorSystemContext systemContext
RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext
, ActorRef parent, ActorRef self) {
super(systemContext, tenantId, ruleNodeId);
this.parent = parent;
this.ruleChainName = ruleChainName;
this.self = self;
this.service = systemContext.getRuleChainService();
this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
this.defaultCtx = new DefaultTbContext(systemContext, new RuleNodeCtx(tenantId, parent, self, ruleNode));
}
@ -63,8 +59,8 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
@Override
public void onUpdate(ActorContext context) throws Exception {
RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
boolean restartRequired = !(ruleNode.getType().equals(newRuleNode.getType())
&& ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
boolean restartRequired = state != ComponentLifecycleState.ACTIVE ||
!(ruleNode.getType().equals(newRuleNode.getType()) && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
this.ruleNode = newRuleNode;
this.defaultCtx.updateSelf(newRuleNode);
if (restartRequired) {
@ -91,7 +87,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
}
public void onRuleToSelfMsg(RuleNodeToSelfMsg msg) throws Exception {
checkActive();
checkActive(msg.getMsg());
if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), "Self");
}
@ -103,7 +99,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
}
void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception {
checkActive();
checkActive(msg.getMsg());
if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
}
@ -129,4 +125,8 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
return tbNode;
}
@Override
protected RuleNodeException getInactiveException() {
return new RuleNodeException("Rule Node is not active! Failed to initialize.", ruleChainName, ruleNode);
}
}

13
application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java

@ -22,7 +22,10 @@ import org.thingsboard.server.actors.stats.StatsPersistTick;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.RuleNodeException;
@Slf4j
public abstract class ComponentMsgProcessor<T extends EntityId> extends AbstractContextAwareMsgProcessor {
@ -74,11 +77,17 @@ public abstract class ComponentMsgProcessor<T extends EntityId> extends Abstract
schedulePeriodicMsgWithDelay(context, new StatsPersistTick(), statsPersistFrequency, statsPersistFrequency);
}
protected void checkActive() {
protected void checkActive(TbMsg tbMsg) throws RuleNodeException {
if (state != ComponentLifecycleState.ACTIVE) {
log.debug("Component is not active. Current state [{}] for processor [{}][{}] tenant [{}]", state, entityId.getEntityType(), entityId, tenantId);
throw new IllegalStateException("Rule chain is not active! " + entityId + " - " + tenantId);
RuleNodeException ruleNodeException = getInactiveException();
if (tbMsg != null) {
tbMsg.getCallback().onFailure(ruleNodeException);
}
throw ruleNodeException;
}
}
abstract protected RuleNodeException getInactiveException();
}

2
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -338,7 +338,7 @@ public class AuthController extends BaseController {
@RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST)
@ResponseBody
public List<OAuth2ClientInfo> getOath2Clients() throws ThingsboardException {
public List<OAuth2ClientInfo> getOAuth2Clients() throws ThingsboardException {
try {
return oauth2Service.getOAuth2Clients();
} catch (Exception e) {

13
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -23,6 +23,7 @@ import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.RpcError;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
@ -45,6 +46,7 @@ import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.subscription.SubscriptionManagerService;
import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
@ -100,7 +102,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
}
@PreDestroy
public void destroy(){
public void destroy() {
super.destroy();
}
@ -143,8 +145,13 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
} else if (toCoreMsg.getToDeviceActorNotificationMsg() != null && !toCoreMsg.getToDeviceActorNotificationMsg().isEmpty()) {
Optional<TbActorMsg> actorMsg = encodingService.decode(toCoreMsg.getToDeviceActorNotificationMsg().toByteArray());
if (actorMsg.isPresent()) {
log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get());
actorContext.tell(actorMsg.get(), ActorRef.noSender());
TbActorMsg tbActorMsg = actorMsg.get();
if (tbActorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) {
tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) tbActorMsg);
} else {
log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get());
actorContext.tell(actorMsg.get(), ActorRef.noSender());
}
}
callback.onSuccess();
}

18
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.RpcError;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbActorMsg;
@ -31,6 +32,7 @@ import org.thingsboard.server.common.msg.queue.ServiceQueue;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg;
import org.thingsboard.server.queue.TbQueueConsumer;
@ -48,6 +50,9 @@ import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStr
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory;
import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy;
import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory;
import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
import org.thingsboard.server.service.stats.RuleEngineStatisticsService;
import javax.annotation.PostConstruct;
@ -81,6 +86,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
private final TbRuleEngineQueueFactory tbRuleEngineQueueFactory;
private final TbQueueRuleEngineSettings ruleEngineSettings;
private final RuleEngineStatisticsService statisticsService;
private final TbRuleEngineDeviceRpcService tbDeviceRpcService;
private final ConcurrentMap<String, TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>>> consumers = new ConcurrentHashMap<>();
private final ConcurrentMap<String, TbRuleEngineQueueConfiguration> consumerConfigurations = new ConcurrentHashMap<>();
private final ConcurrentMap<String, TbRuleEngineConsumerStats> consumerStats = new ConcurrentHashMap<>();
@ -90,13 +96,15 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
TbRuleEngineSubmitStrategyFactory submitStrategyFactory,
TbQueueRuleEngineSettings ruleEngineSettings,
TbRuleEngineQueueFactory tbRuleEngineQueueFactory, RuleEngineStatisticsService statisticsService,
ActorSystemContext actorContext, DataDecodingEncodingService encodingService) {
ActorSystemContext actorContext, DataDecodingEncodingService encodingService,
TbRuleEngineDeviceRpcService tbDeviceRpcService) {
super(actorContext, encodingService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer());
this.statisticsService = statisticsService;
this.ruleEngineSettings = ruleEngineSettings;
this.tbRuleEngineQueueFactory = tbRuleEngineQueueFactory;
this.submitStrategyFactory = submitStrategyFactory;
this.processingStrategyFactory = processingStrategyFactory;
this.tbDeviceRpcService = tbDeviceRpcService;
}
@PostConstruct
@ -227,7 +235,15 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
actorContext.tell(actorMsg.get(), ActorRef.noSender());
}
callback.onSuccess();
} else if (nfMsg.hasFromDeviceRpcResponse()) {
TransportProtos.FromDeviceRPCResponseProto proto = nfMsg.getFromDeviceRpcResponse();
RpcError error = proto.getError() > 0 ? RpcError.values()[proto.getError()] : null;
FromDeviceRpcResponse response = new FromDeviceRpcResponse(new UUID(proto.getRequestIdMSB(), proto.getRequestIdLSB())
, proto.getResponse(), error);
tbDeviceRpcService.processRpcResponseFromDevice(response);
callback.onSuccess();
} else {
log.trace("Received notification with missing handler");
callback.onSuccess();
}
}

1
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -23,7 +23,6 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionChangeEvent;

5
application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java

@ -23,7 +23,6 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
@ -77,8 +76,8 @@ public class BatchTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSubmitS
}
}
int submitSize = pendingPack.size();
if (log.isInfoEnabled() && submitSize > 0) {
log.info("[{}] submitting [{}] messages to rule engine", queueName, submitSize);
if (log.isDebugEnabled() && submitSize > 0) {
log.debug("[{}] submitting [{}] messages to rule engine", queueName, submitSize);
}
pendingPack.forEach(msgConsumer);
}

10
application/src/main/java/org/thingsboard/server/service/queue/processing/BurstTbRuleEngineSubmitStrategy.java

@ -19,14 +19,8 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j
public class BurstTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSubmitStrategy {
@ -37,8 +31,8 @@ public class BurstTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSubmitS
@Override
public void submitAttempt(BiConsumer<UUID, TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg>> msgConsumer) {
if (log.isInfoEnabled()) {
log.info("[{}] submitting [{}] messages to rule engine", queueName, orderedMsgList.size());
if (log.isDebugEnabled()) {
log.debug("[{}] submitting [{}] messages to rule engine", queueName, orderedMsgList.size());
}
orderedMsgList.forEach(pair -> msgConsumer.accept(pair.uuid, pair.msg));
}

8
application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByEntityIdTbRuleEngineSubmitStrategy.java

@ -15,26 +15,18 @@
*/
package org.thingsboard.server.service.queue.processing;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.gen.MsgProtos;
import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
@Slf4j
public abstract class SequentialByEntityIdTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSubmitStrategy {

1
application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByTenantIdTbRuleEngineSubmitStrategy.java

@ -30,6 +30,5 @@ public class SequentialByTenantIdTbRuleEngineSubmitStrategy extends SequentialBy
@Override
protected EntityId getEntityId(TransportProtos.ToRuleEngineMsg msg) {
return new TenantId(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB()));
}
}

6
application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialTbRuleEngineSubmitStrategy.java

@ -19,8 +19,6 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
@ -63,8 +61,8 @@ public class SequentialTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSu
if (idx < listSize) {
IdMsgPair pair = orderedMsgList.get(idx);
expectedMsgId = pair.uuid;
if (log.isInfoEnabled()) {
log.info("[{}] submitting [{}] message to rule engine", queueName, pair.msg);
if (log.isDebugEnabled()) {
log.debug("[{}] submitting [{}] message to rule engine", queueName, pair.msg);
}
msgConsumer.accept(pair.uuid, pair.msg);
}

8
application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java

@ -82,10 +82,10 @@ public class TbRuleEngineProcessingStrategyFactory {
retryCount++;
double failedCount = result.getFailedMap().size() + result.getPendingMap().size();
if (maxRetries > 0 && retryCount > maxRetries) {
log.info("[{}] Skip reprocess of the rule engine pack due to max retries", queueName);
log.debug("[{}] Skip reprocess of the rule engine pack due to max retries", queueName);
return new TbRuleEngineProcessingDecision(true, null);
} else if (maxAllowedFailurePercentage > 0 && (failedCount / initialTotalCount) > maxAllowedFailurePercentage) {
log.info("[{}] Skip reprocess of the rule engine pack due to max allowed failure percentage", queueName);
log.debug("[{}] Skip reprocess of the rule engine pack due to max allowed failure percentage", queueName);
return new TbRuleEngineProcessingDecision(true, null);
} else {
ConcurrentMap<UUID, TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg>> toReprocess = new ConcurrentHashMap<>(initialTotalCount);
@ -98,7 +98,7 @@ public class TbRuleEngineProcessingStrategyFactory {
if (retrySuccessful) {
result.getSuccessMap().forEach(toReprocess::put);
}
log.info("[{}] Going to reprocess {} messages", queueName, toReprocess.size());
log.debug("[{}] Going to reprocess {} messages", queueName, toReprocess.size());
if (log.isTraceEnabled()) {
toReprocess.forEach((id, msg) -> log.trace("Going to reprocess [{}]: {}", id, TbMsg.fromBytes(msg.getValue().getTbMsg().toByteArray(), TbMsgCallback.EMPTY)));
}
@ -126,7 +126,7 @@ public class TbRuleEngineProcessingStrategyFactory {
@Override
public TbRuleEngineProcessingDecision analyze(TbRuleEngineProcessingResult result) {
if (!result.isSuccess()) {
log.info("[{}] Reprocessing skipped for {} failed and {} timeout messages", queueName, result.getFailedMap().size(), result.getPendingMap().size());
log.debug("[{}] Reprocessing skipped for {} failed and {} timeout messages", queueName, result.getFailedMap().size(), result.getPendingMap().size());
}
if (log.isTraceEnabled()) {
result.getFailedMap().forEach((id, msg) -> log.trace("Failed messages [{}]: {}", id, TbMsg.fromBytes(msg.getValue().getTbMsg().toByteArray(), TbMsgCallback.EMPTY)));

7
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java

@ -51,9 +51,9 @@ public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implemen
String firstName = getStringAttributeByKey(attributes, config.getBasic().getFirstNameAttributeKey());
oauth2User.setFirstName(firstName);
}
if (!StringUtils.isEmpty(config.getBasic().getCustomerNameStrategyPattern())) {
if (!StringUtils.isEmpty(config.getBasic().getCustomerNamePattern())) {
StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
String customerName = sub.replace(config.getBasic().getCustomerNameStrategyPattern());
String customerName = sub.replace(config.getBasic().getCustomerNamePattern());
oauth2User.setCustomerName(customerName);
}
return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.getBasic().isAllowUserCreation());
@ -68,7 +68,7 @@ public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implemen
return email.substring(email .indexOf("@") + 1);
case CUSTOM_TENANT_STRATEGY:
StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
return sub.replace(config.getBasic().getTenantNameStrategyPattern());
return sub.replace(config.getBasic().getTenantNamePattern());
default:
throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!");
}
@ -78,7 +78,6 @@ public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implemen
String result = null;
try {
result = (String) attributes.get(key);
} catch (Exception e) {
log.warn("Can't convert attribute to String by key " + key);
}

2
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java

@ -41,7 +41,7 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.getBasic().isAllowUserCreation());
}
public OAuth2User getOAuth2User(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig.CustomOAuth2ClientMapperConfig custom) {
private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig.CustomOAuth2ClientMapperConfig custom) {
if (!StringUtils.isEmpty(custom.getUsername()) && !StringUtils.isEmpty(custom.getPassword())) {
restTemplateBuilder = restTemplateBuilder.basicAuthentication(custom.getUsername(), custom.getPassword());
}

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

@ -126,8 +126,8 @@ security:
firstNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_FIRST_NAME_ATTRIBUTE_KEY:}"
lastNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_LAST_NAME_ATTRIBUTE_KEY:}"
tenantNameStrategy: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_STRATEGY:domain}" # domain, email or custom
tenantNameStrategyPattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_STRATEGY_PATTERN:}"
customerNameStrategyPattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_CUSTOMER_NAME_STRATEGY_PATTERN:}"
tenantNamePattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_PATTERN:}" # %{attribute_key} as placeholder for attributes value by key
customerNamePattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_CUSTOMER_NAME_PATTERN:}" # %{attribute_key} as placeholder for attributes value by key
custom:
url: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_URL:}"
username: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_USERNAME:}"
@ -637,7 +637,7 @@ queue:
pack-processing-timeout: "${TB_QUEUE_CORE_PACK_PROCESSING_TIMEOUT_MS:60000}"
stats:
enabled: "${TB_QUEUE_CORE_STATS_ENABLED:true}"
print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:10000}"
print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:60000}"
js:
# JS Eval request topic
request_topic: "${REMOTE_JS_EVAL_REQUEST_TOPIC:js_eval.requests}"
@ -657,7 +657,7 @@ queue:
pack-processing-timeout: "${TB_QUEUE_RULE_ENGINE_PACK_PROCESSING_TIMEOUT_MS:60000}"
stats:
enabled: "${TB_QUEUE_RULE_ENGINE_STATS_ENABLED:true}"
print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:10000}"
print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:60000}"
queues:
- name: "${TB_QUEUE_RE_MAIN_QUEUE_NAME:Main}"
topic: "${TB_QUEUE_RE_MAIN_TOPIC:tb_rule_engine.main}"

12
common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.msg.gen.MsgProtos;
import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import java.io.IOException;
import java.io.Serializable;
import java.util.UUID;
@ -47,7 +48,7 @@ public final class TbMsg implements Serializable {
private final RuleChainId ruleChainId;
private final RuleNodeId ruleNodeId;
//This field is not serialized because we use queues and there is no need to do it
private final TbMsgCallback callback;
transient private final TbMsgCallback callback;
public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data) {
return new TbMsg(UUID.randomUUID(), type, originator, metaData.copy(), TbMsgDataType.JSON, data, null, null, TbMsgCallback.EMPTY);
@ -156,4 +157,13 @@ public final class TbMsg implements Serializable {
public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
return new TbMsg(this.id, this.type, this.originator, this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, callback);
}
public TbMsgCallback getCallback() {
//May be null in case of deserialization;
if (callback != null) {
return callback;
} else {
return TbMsgCallback.EMPTY;
}
}
}

12
common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeException.java

@ -36,9 +36,15 @@ public class RuleNodeException extends RuleEngineException {
public RuleNodeException(String message, String ruleChainName, RuleNode ruleNode) {
super(message);
this.ruleChainName = ruleChainName;
this.ruleNodeName = ruleNode.getName();
this.ruleChainId = ruleNode.getRuleChainId();
this.ruleNodeId = ruleNode.getId();
if (ruleNode != null) {
this.ruleNodeName = ruleNode.getName();
this.ruleChainId = ruleNode.getRuleChainId();
this.ruleNodeId = ruleNode.getId();
} else {
ruleNodeName = "Unknown";
ruleChainId = new RuleChainId(RuleChainId.NULL_UUID);
ruleNodeId = new RuleNodeId(RuleNodeId.NULL_UUID);
}
}
public String toJsonString() {

1
common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusAdmin.java

@ -67,6 +67,7 @@ public class TbServiceBusAdmin implements TbQueueAdmin {
try {
QueueDescription queueDescription = new QueueDescription(topic);
queueDescription.setRequiresDuplicateDetection(false);
setQueueConfigs(queueDescription);
client.createQueue(queueDescription);

118
common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusConsumerTemplate.java

@ -31,9 +31,9 @@ import org.apache.qpid.proton.amqp.transport.SenderSettleMode;
import org.springframework.util.CollectionUtils;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.TbQueueMsgDecoder;
import org.thingsboard.server.queue.common.AbstractTbQueueConsumerTemplate;
import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
import java.time.Duration;
@ -50,102 +50,72 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
public class TbServiceBusConsumerTemplate<T extends TbQueueMsg> implements TbQueueConsumer<T> {
public class TbServiceBusConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQueueConsumerTemplate<MessageWithDeliveryTag, T> {
private final TbQueueAdmin admin;
private final String topic;
private final TbQueueMsgDecoder<T> decoder;
private final TbServiceBusSettings serviceBusSettings;
private final Gson gson = new Gson();
private Set<CoreMessageReceiver> receivers;
private volatile Set<TopicPartitionInfo> partitions;
private volatile boolean subscribed;
private volatile boolean stopped = false;
private Map<CoreMessageReceiver, Collection<MessageWithDeliveryTag>> pendingMessages = new ConcurrentHashMap<>();
private final Map<CoreMessageReceiver, Collection<MessageWithDeliveryTag>> pendingMessages = new ConcurrentHashMap<>();
private volatile int messagesPerQueue;
public TbServiceBusConsumerTemplate(TbQueueAdmin admin, TbServiceBusSettings serviceBusSettings, String topic, TbQueueMsgDecoder<T> decoder) {
super(topic);
this.admin = admin;
this.decoder = decoder;
this.topic = topic;
this.serviceBusSettings = serviceBusSettings;
}
@Override
public String getTopic() {
return topic;
protected List<MessageWithDeliveryTag> doPoll(long durationInMillis) {
List<CompletableFuture<Collection<MessageWithDeliveryTag>>> messageFutures =
receivers.stream()
.map(receiver -> receiver
.receiveAsync(messagesPerQueue, Duration.ofMillis(durationInMillis))
.whenComplete((messages, err) -> {
if (!CollectionUtils.isEmpty(messages)) {
pendingMessages.put(receiver, messages);
} else if (err != null) {
log.error("Failed to receive messages.", err);
}
}))
.collect(Collectors.toList());
try {
return fromList(messageFutures)
.get()
.stream()
.flatMap(messages -> CollectionUtils.isEmpty(messages) ? Stream.empty() : messages.stream())
.collect(Collectors.toList());
} catch (InterruptedException | ExecutionException e) {
if (stopped) {
log.info("[{}] Service Bus consumer is stopped.", getTopic());
} else {
log.error("Failed to receive messages", e);
}
return Collections.emptyList();
}
}
@Override
public void subscribe() {
partitions = Collections.singleton(new TopicPartitionInfo(topic, null, null, true));
subscribed = false;
protected void doSubscribe(List<String> topicNames) {
createReceivers();
messagesPerQueue = receivers.size() / partitions.size();
}
@Override
public void subscribe(Set<TopicPartitionInfo> partitions) {
this.partitions = partitions;
subscribed = false;
protected void doCommit() {
pendingMessages.forEach((receiver, msgs) ->
msgs.forEach(msg -> receiver.completeMessageAsync(msg.getDeliveryTag(), TransactionContext.NULL_TXN)));
pendingMessages.clear();
}
@Override
public void unsubscribe() {
stopped = true;
protected void doUnsubscribe() {
receivers.forEach(CoreMessageReceiver::closeAsync);
}
@Override
public List<T> poll(long durationInMillis) {
if (!subscribed && partitions == null) {
try {
Thread.sleep(durationInMillis);
} catch (InterruptedException e) {
log.debug("Failed to await subscription", e);
}
} else {
if (!subscribed) {
createReceivers();
messagesPerQueue = receivers.size() / partitions.size();
subscribed = true;
}
List<CompletableFuture<Collection<MessageWithDeliveryTag>>> messageFutures =
receivers.stream()
.map(receiver -> receiver
.receiveAsync(messagesPerQueue, Duration.ofMillis(durationInMillis))
.whenComplete((messages, err) -> {
if (!CollectionUtils.isEmpty(messages)) {
pendingMessages.put(receiver, messages);
} else if (err != null) {
log.error("Failed to receive messages.", err);
}
}))
.collect(Collectors.toList());
try {
return fromList(messageFutures)
.get()
.stream()
.flatMap(messages -> CollectionUtils.isEmpty(messages) ? Stream.empty() : messages.stream())
.map(message -> {
try {
return decode(message);
} catch (InvalidProtocolBufferException e) {
log.error("Failed to parse message.", e);
throw new RuntimeException("Failed to parse message.", e);
}
}).collect(Collectors.toList());
} catch (InterruptedException | ExecutionException e) {
if (stopped) {
log.info("[{}] Service Bus consumer is stopped.", topic);
} else {
log.error("Failed to receive messages", e);
}
}
}
return Collections.emptyList();
}
private void createReceivers() {
List<CompletableFuture<CoreMessageReceiver>> receiverFutures = partitions.stream()
.map(TopicPartitionInfo::getFullTopicName)
@ -167,7 +137,7 @@ public class TbServiceBusConsumerTemplate<T extends TbQueueMsg> implements TbQue
receivers = new HashSet<>(fromList(receiverFutures).get());
} catch (InterruptedException | ExecutionException e) {
if (stopped) {
log.info("[{}] Service Bus consumer is stopped.", topic);
log.info("[{}] Service Bus consumer is stopped.", getTopic());
} else {
log.error("Failed to create receivers", e);
}
@ -196,13 +166,7 @@ public class TbServiceBusConsumerTemplate<T extends TbQueueMsg> implements TbQue
}
@Override
public void commit() {
pendingMessages.forEach((receiver, msgs) ->
msgs.forEach(msg -> receiver.completeMessageAsync(msg.getDeliveryTag(), TransactionContext.NULL_TXN)));
pendingMessages.clear();
}
private T decode(MessageWithDeliveryTag data) throws InvalidProtocolBufferException {
protected T decode(MessageWithDeliveryTag data) throws InvalidProtocolBufferException {
DefaultTbQueueMsg msg = gson.fromJson(new String(((Data) data.getMessage().getBody()).getValue().getArray()), DefaultTbQueueMsg.class);
return decoder.decode(msg);
}

7
common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusProducerTemplate.java

@ -33,6 +33,7 @@ import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -42,14 +43,14 @@ public class TbServiceBusProducerTemplate<T extends TbQueueMsg> implements TbQue
private final Gson gson = new Gson();
private final TbQueueAdmin admin;
private final TbServiceBusSettings serviceBusSettings;
private final Map<String, QueueClient> clients = new HashMap<>();
private ExecutorService executorService;
private final Map<String, QueueClient> clients = new ConcurrentHashMap<>();
private final ExecutorService executorService;
public TbServiceBusProducerTemplate(TbQueueAdmin admin, TbServiceBusSettings serviceBusSettings, String defaultTopic) {
this.admin = admin;
this.defaultTopic = defaultTopic;
this.serviceBusSettings = serviceBusSettings;
executorService = Executors.newSingleThreadExecutor();
executorService = Executors.newCachedThreadPool();
}
@Override

53
common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractParallelTbQueueConsumerTemplate.java

@ -0,0 +1,53 @@
/**
* Copyright © 2016-2020 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 com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.queue.TbQueueMsg;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Slf4j
public abstract class AbstractParallelTbQueueConsumerTemplate<R, T extends TbQueueMsg> extends AbstractTbQueueConsumerTemplate<R, T> {
protected ListeningExecutorService consumerExecutor;
public AbstractParallelTbQueueConsumerTemplate(String topic) {
super(topic);
}
protected void initNewExecutor(int threadPoolSize) {
if (consumerExecutor != null) {
consumerExecutor.shutdown();
try {
consumerExecutor.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
log.trace("Interrupted while waiting for consumer executor to stop");
}
}
consumerExecutor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(threadPoolSize));
}
protected void shutdownExecutor() {
if (consumerExecutor != null) {
consumerExecutor.shutdownNow();
}
}
}

152
common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java

@ -0,0 +1,152 @@
/**
* Copyright © 2016-2020 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.Getter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueMsg;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
@Slf4j
public abstract class AbstractTbQueueConsumerTemplate<R, T extends TbQueueMsg> implements TbQueueConsumer<T> {
private volatile boolean subscribed;
protected volatile boolean stopped = false;
protected volatile Set<TopicPartitionInfo> partitions;
protected final Lock consumerLock = new ReentrantLock();
@Getter
private final String topic;
public AbstractTbQueueConsumerTemplate(String topic) {
this.topic = topic;
}
@Override
public void subscribe() {
consumerLock.lock();
try {
partitions = Collections.singleton(new TopicPartitionInfo(topic, null, null, true));
subscribed = false;
} finally {
consumerLock.unlock();
}
}
@Override
public void subscribe(Set<TopicPartitionInfo> partitions) {
consumerLock.lock();
try {
this.partitions = partitions;
subscribed = false;
} finally {
consumerLock.unlock();
}
}
@Override
public List<T> poll(long durationInMillis) {
if (!subscribed && partitions == null) {
try {
Thread.sleep(durationInMillis);
} catch (InterruptedException e) {
log.debug("Failed to await subscription", e);
}
} else {
long pollStartTs = System.currentTimeMillis();
consumerLock.lock();
try {
if (!subscribed) {
List<String> topicNames = partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList());
doSubscribe(topicNames);
subscribed = true;
}
List<R> records = doPoll(durationInMillis);
if (!records.isEmpty()) {
List<T> result = new ArrayList<>(records.size());
records.forEach(record -> {
try {
if (record != null) {
result.add(decode(record));
}
} catch (IOException e) {
log.error("Failed decode record: [{}]", record);
throw new RuntimeException("Failed to decode record: ", e);
}
});
return result;
} else {
long pollDuration = System.currentTimeMillis() - pollStartTs;
if (pollDuration < durationInMillis) {
try {
Thread.sleep(durationInMillis - pollDuration);
} catch (InterruptedException e) {
if (!stopped) {
log.error("Failed to wait.", e);
}
}
}
}
} finally {
consumerLock.unlock();
}
}
return Collections.emptyList();
}
@Override
public void commit() {
consumerLock.lock();
try {
doCommit();
} finally {
consumerLock.unlock();
}
}
@Override
public void unsubscribe() {
stopped = true;
consumerLock.lock();
try {
doUnsubscribe();
} finally {
consumerLock.unlock();
}
}
abstract protected List<R> doPoll(long durationInMillis);
abstract protected T decode(R record) throws IOException;
abstract protected void doSubscribe(List<String> topicNames);
abstract protected void doCommit();
abstract protected void doUnsubscribe();
}

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

@ -16,16 +16,14 @@
package org.thingsboard.server.queue.kafka;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.common.AbstractTbQueueConsumerTemplate;
import java.io.IOException;
import java.time.Duration;
@ -33,26 +31,16 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
/**
* Created by ashvayka on 24.09.18.
*/
@Slf4j
public class TbKafkaConsumerTemplate<T extends TbQueueMsg> implements TbQueueConsumer<T> {
public class TbKafkaConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQueueConsumerTemplate<ConsumerRecord<String, byte[]>, T> {
private final TbQueueAdmin admin;
private final KafkaConsumer<String, byte[]> consumer;
private final TbKafkaDecoder<T> decoder;
private volatile boolean subscribed;
private volatile Set<TopicPartitionInfo> partitions;
private final Lock consumerLock;
@Getter
private final String topic;
@Builder
private TbKafkaConsumerTemplate(TbKafkaSettings settings, TbKafkaDecoder<T> decoder,
@ -60,6 +48,7 @@ public class TbKafkaConsumerTemplate<T extends TbQueueMsg> implements TbQueueCon
boolean autoCommit, int autoCommitIntervalMs,
int maxPollRecords,
TbQueueAdmin admin) {
super(topic);
Properties props = settings.toProps();
props.put(ConsumerConfig.CLIENT_ID_CONFIG, clientId);
if (groupId != null) {
@ -75,94 +64,42 @@ public class TbKafkaConsumerTemplate<T extends TbQueueMsg> implements TbQueueCon
this.admin = admin;
this.consumer = new KafkaConsumer<>(props);
this.decoder = decoder;
this.topic = topic;
this.consumerLock = new ReentrantLock();
}
@Override
public void subscribe() {
consumerLock.lock();
try {
partitions = Collections.singleton(new TopicPartitionInfo(topic, null, null, true));
subscribed = false;
} finally {
consumerLock.unlock();
}
protected void doSubscribe(List<String> topicNames) {
topicNames.forEach(admin::createTopicIfNotExists);
consumer.subscribe(topicNames);
}
@Override
public void subscribe(Set<TopicPartitionInfo> partitions) {
consumerLock.lock();
try {
this.partitions = partitions;
subscribed = false;
} finally {
consumerLock.unlock();
protected List<ConsumerRecord<String, byte[]>> doPoll(long durationInMillis) {
ConsumerRecords<String, byte[]> records = consumer.poll(Duration.ofMillis(durationInMillis));
if (records.isEmpty()) {
return Collections.emptyList();
} else {
List<ConsumerRecord<String, byte[]>> recordList = new ArrayList<>(256);
records.forEach(recordList::add);
return recordList;
}
}
@Override
public List<T> poll(long durationInMillis) {
if (!subscribed && partitions == null) {
try {
Thread.sleep(durationInMillis);
} catch (InterruptedException e) {
log.debug("Failed to await subscription", e);
}
} else {
consumerLock.lock();
try {
if (!subscribed) {
List<String> topicNames = partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList());
topicNames.forEach(admin::createTopicIfNotExists);
consumer.subscribe(topicNames);
subscribed = true;
}
ConsumerRecords<String, byte[]> records = consumer.poll(Duration.ofMillis(durationInMillis));
if (records.count() > 0) {
List<T> result = new ArrayList<>();
records.forEach(record -> {
try {
result.add(decode(record));
} catch (IOException e) {
log.error("Failed decode record: [{}]", record);
}
});
return result;
}
} finally {
consumerLock.unlock();
}
}
return Collections.emptyList();
public T decode(ConsumerRecord<String, byte[]> record) throws IOException {
return decoder.decode(new KafkaTbQueueMsg(record));
}
@Override
public void commit() {
consumerLock.lock();
try {
consumer.commitAsync();
} finally {
consumerLock.unlock();
}
protected void doCommit() {
consumer.commitAsync();
}
@Override
public void unsubscribe() {
consumerLock.lock();
try {
if (consumer != null) {
consumer.unsubscribe();
consumer.close();
}
} finally {
consumerLock.unlock();
protected void doUnsubscribe() {
if (consumer != null) {
consumer.unsubscribe();
consumer.close();
}
}
public T decode(ConsumerRecord<String, byte[]> record) throws IOException {
return decoder.decode(new KafkaTbQueueMsg(record));
}
}

32
common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubAdmin.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.queue.pubsub;
import com.google.api.gax.rpc.AlreadyExistsException;
import com.google.cloud.pubsub.v1.SubscriptionAdminClient;
import com.google.cloud.pubsub.v1.SubscriptionAdminSettings;
import com.google.cloud.pubsub.v1.TopicAdminClient;
@ -24,9 +25,9 @@ import com.google.pubsub.v1.ListSubscriptionsRequest;
import com.google.pubsub.v1.ListTopicsRequest;
import com.google.pubsub.v1.ProjectName;
import com.google.pubsub.v1.ProjectSubscriptionName;
import com.google.pubsub.v1.ProjectTopicName;
import com.google.pubsub.v1.Subscription;
import com.google.pubsub.v1.Topic;
import com.google.pubsub.v1.TopicName;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.queue.TbQueueAdmin;
@ -103,7 +104,10 @@ public class TbPubSubAdmin implements TbQueueAdmin {
@Override
public void createTopicIfNotExists(String partition) {
ProjectTopicName topicName = ProjectTopicName.of(pubSubSettings.getProjectId(), partition);
TopicName topicName = TopicName.newBuilder()
.setTopic(partition)
.setProject(pubSubSettings.getProjectId())
.build();
if (topicSet.contains(topicName.toString())) {
createSubscriptionIfNotExists(partition, topicName);
@ -121,13 +125,18 @@ public class TbPubSubAdmin implements TbQueueAdmin {
}
}
topicAdminClient.createTopic(topicName);
topicSet.add(topicName.toString());
log.info("Created new topic: [{}]", topicName.toString());
try {
topicAdminClient.createTopic(topicName);
log.info("Created new topic: [{}]", topicName.toString());
} catch (AlreadyExistsException e) {
log.info("[{}] Topic already exist.", topicName.toString());
} finally {
topicSet.add(topicName.toString());
}
createSubscriptionIfNotExists(partition, topicName);
}
private void createSubscriptionIfNotExists(String partition, ProjectTopicName topicName) {
private void createSubscriptionIfNotExists(String partition, TopicName topicName) {
ProjectSubscriptionName subscriptionName =
ProjectSubscriptionName.of(pubSubSettings.getProjectId(), partition);
@ -153,9 +162,14 @@ public class TbPubSubAdmin implements TbQueueAdmin {
setAckDeadline(subscriptionBuilder);
setMessageRetention(subscriptionBuilder);
subscriptionAdminClient.createSubscription(subscriptionBuilder.build());
subscriptionSet.add(subscriptionName.toString());
log.info("Created new subscription: [{}]", subscriptionName.toString());
try {
subscriptionAdminClient.createSubscription(subscriptionBuilder.build());
log.info("Created new subscription: [{}]", subscriptionName.toString());
} catch (AlreadyExistsException e) {
log.info("[{}] Subscription already exist.", subscriptionName.toString());
} finally {
subscriptionSet.add(subscriptionName.toString());
}
}
private void setAckDeadline(Subscription.Builder builder) {

105
common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubConsumerTemplate.java

@ -30,27 +30,25 @@ import com.google.pubsub.v1.PullResponse;
import com.google.pubsub.v1.ReceivedMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.TbQueueMsgDecoder;
import org.thingsboard.server.queue.common.AbstractParallelTbQueueConsumerTemplate;
import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
@Slf4j
public class TbPubSubConsumerTemplate<T extends TbQueueMsg> implements TbQueueConsumer<T> {
public class TbPubSubConsumerTemplate<T extends TbQueueMsg> extends AbstractParallelTbQueueConsumerTemplate<PubsubMessage, T> {
private final Gson gson = new Gson();
private final TbQueueAdmin admin;
@ -58,23 +56,18 @@ public class TbPubSubConsumerTemplate<T extends TbQueueMsg> implements TbQueueCo
private final TbQueueMsgDecoder<T> decoder;
private final TbPubSubSettings pubSubSettings;
private volatile boolean subscribed;
private volatile Set<TopicPartitionInfo> partitions;
private volatile Set<String> subscriptionNames;
private final List<AcknowledgeRequest> acknowledgeRequests = new CopyOnWriteArrayList<>();
private ExecutorService consumerExecutor;
private final SubscriberStub subscriber;
private volatile boolean stopped;
private volatile int messagesPerTopic;
public TbPubSubConsumerTemplate(TbQueueAdmin admin, TbPubSubSettings pubSubSettings, String topic, TbQueueMsgDecoder<T> decoder) {
super(topic);
this.admin = admin;
this.pubSubSettings = pubSubSettings;
this.topic = topic;
this.decoder = decoder;
try {
SubscriberStubSettings subscriberStubSettings =
SubscriberStubSettings.newBuilder()
@ -84,89 +77,50 @@ public class TbPubSubConsumerTemplate<T extends TbQueueMsg> implements TbQueueCo
.setMaxInboundMessageSize(pubSubSettings.getMaxMsgSize())
.build())
.build();
this.subscriber = GrpcSubscriberStub.create(subscriberStubSettings);
} catch (IOException e) {
log.error("Failed to create subscriber.", e);
throw new RuntimeException("Failed to create subscriber.", e);
}
stopped = false;
}
@Override
public String getTopic() {
return topic;
protected List<PubsubMessage> doPoll(long durationInMillis) {
try {
List<ReceivedMessage> messages = receiveMessages();
if (!messages.isEmpty()) {
return messages.stream().map(ReceivedMessage::getMessage).collect(Collectors.toList());
}
} catch (ExecutionException | InterruptedException e) {
if (stopped) {
log.info("[{}] Pub/Sub consumer is stopped.", topic);
} else {
log.error("Failed to receive messages", e);
}
}
return Collections.emptyList();
}
@Override
public void subscribe() {
partitions = Collections.singleton(new TopicPartitionInfo(topic, null, null, true));
subscribed = false;
protected void doSubscribe(List<String> topicNames) {
subscriptionNames = new LinkedHashSet<>(topicNames);
subscriptionNames.forEach(admin::createTopicIfNotExists);
initNewExecutor(subscriptionNames.size() + 1);
messagesPerTopic = pubSubSettings.getMaxMessages() / subscriptionNames.size();
}
@Override
public void subscribe(Set<TopicPartitionInfo> partitions) {
this.partitions = partitions;
subscribed = false;
protected void doCommit() {
acknowledgeRequests.forEach(subscriber.acknowledgeCallable()::futureCall);
acknowledgeRequests.clear();
}
@Override
public void unsubscribe() {
stopped = true;
if (consumerExecutor != null) {
consumerExecutor.shutdownNow();
}
protected void doUnsubscribe() {
if (subscriber != null) {
subscriber.close();
}
}
@Override
public List<T> poll(long durationInMillis) {
if (!subscribed && partitions == null) {
try {
Thread.sleep(durationInMillis);
} catch (InterruptedException e) {
log.debug("Failed to await subscription", e);
}
} else {
if (!subscribed) {
subscriptionNames = partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toSet());
subscriptionNames.forEach(admin::createTopicIfNotExists);
consumerExecutor = Executors.newFixedThreadPool(subscriptionNames.size());
messagesPerTopic = pubSubSettings.getMaxMessages() / subscriptionNames.size();
subscribed = true;
}
List<ReceivedMessage> messages;
try {
messages = receiveMessages();
if (!messages.isEmpty()) {
List<T> result = new ArrayList<>();
messages.forEach(msg -> {
try {
result.add(decode(msg.getMessage()));
} catch (InvalidProtocolBufferException e) {
log.error("Failed decode record: [{}]", msg);
}
});
return result;
}
} catch (ExecutionException | InterruptedException e) {
if (stopped) {
log.info("[{}] Pub/Sub consumer is stopped.", topic);
} else {
log.error("Failed to receive messages", e);
}
}
}
return Collections.emptyList();
}
@Override
public void commit() {
acknowledgeRequests.forEach(subscriber.acknowledgeCallable()::futureCall);
acknowledgeRequests.clear();
shutdownExecutor();
}
private List<ReceivedMessage> receiveMessages() throws ExecutionException, InterruptedException {
@ -175,7 +129,7 @@ public class TbPubSubConsumerTemplate<T extends TbQueueMsg> implements TbQueueCo
PullRequest pullRequest =
PullRequest.newBuilder()
.setMaxMessages(messagesPerTopic)
.setReturnImmediately(false) // return immediately if messages are not available
// .setReturnImmediately(false) // return immediately if messages are not available
.setSubscription(subscriptionName)
.build();
@ -211,6 +165,7 @@ public class TbPubSubConsumerTemplate<T extends TbQueueMsg> implements TbQueueCo
return transform.get();
}
@Override
public T decode(PubsubMessage message) throws InvalidProtocolBufferException {
DefaultTbQueueMsg msg = gson.fromJson(message.getData().toStringUtf8(), DefaultTbQueueMsg.class);
return decoder.decode(msg);

6
common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubProducerTemplate.java

@ -49,7 +49,7 @@ public class TbPubSubProducerTemplate<T extends TbQueueMsg> implements TbQueuePr
private final Map<String, Publisher> publisherMap = new ConcurrentHashMap<>();
private ExecutorService pubExecutor = Executors.newCachedThreadPool();
private final ExecutorService pubExecutor = Executors.newCachedThreadPool();
public TbPubSubProducerTemplate(TbQueueAdmin admin, TbPubSubSettings pubSubSettings, String defaultTopic) {
this.defaultTopic = defaultTopic;
@ -124,8 +124,8 @@ public class TbPubSubProducerTemplate<T extends TbQueueMsg> implements TbQueuePr
publisherMap.put(topic, publisher);
return publisher;
} catch (IOException e) {
log.error("Failed to create topic [{}].", topic, e);
throw new RuntimeException("Failed to create topic.", e);
log.error("Failed to create Publisher for the topic [{}].", topic, e);
throw new RuntimeException("Failed to create Publisher for the topic.", e);
}
}

2
common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqAdmin.java

@ -27,13 +27,11 @@ import java.util.concurrent.TimeoutException;
@Slf4j
public class TbRabbitMqAdmin implements TbQueueAdmin {
private final TbRabbitMqSettings rabbitMqSettings;
private final Channel channel;
private final Connection connection;
private final Map<String, Object> arguments;
public TbRabbitMqAdmin(TbRabbitMqSettings rabbitMqSettings, Map<String, Object> arguments) {
this.rabbitMqSettings = rabbitMqSettings;
this.arguments = arguments;
try {

107
common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplate.java

@ -23,9 +23,9 @@ import com.rabbitmq.client.GetResponse;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.TbQueueMsgDecoder;
import org.thingsboard.server.queue.common.AbstractTbQueueConsumerTemplate;
import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
import java.io.IOException;
@ -37,33 +37,26 @@ import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
@Slf4j
public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> implements TbQueueConsumer<T> {
public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQueueConsumerTemplate<GetResponse, T> {
private final Gson gson = new Gson();
private final TbQueueAdmin admin;
private final String topic;
private final TbQueueMsgDecoder<T> decoder;
private final TbRabbitMqSettings rabbitMqSettings;
private final Channel channel;
private final Connection connection;
private volatile Set<TopicPartitionInfo> partitions;
private volatile boolean subscribed;
private volatile Set<String> queues;
private volatile boolean stopped;
public TbRabbitMqConsumerTemplate(TbQueueAdmin admin, TbRabbitMqSettings rabbitMqSettings, String topic, TbQueueMsgDecoder<T> decoder) {
super(topic);
this.admin = admin;
this.decoder = decoder;
this.topic = topic;
this.rabbitMqSettings = rabbitMqSettings;
try {
connection = rabbitMqSettings.getConnectionFactory().newConnection();
} catch (IOException | TimeoutException e) {
log.error("Failed to create connection.", e);
throw new RuntimeException("Failed to create connection.", e);
}
try {
channel = connection.createChannel();
} catch (IOException e) {
@ -74,25 +67,42 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> implements TbQueue
}
@Override
public String getTopic() {
return topic;
protected List<GetResponse> doPoll(long durationInMillis) {
List<GetResponse> result = queues.stream()
.map(queue -> {
try {
return channel.basicGet(queue, false);
} catch (IOException e) {
log.error("Failed to get messages from queue: [{}]", queue);
throw new RuntimeException("Failed to get messages from queue.", e);
}
}).filter(Objects::nonNull).collect(Collectors.toList());
if (result.size() > 0) {
return result;
} else {
return Collections.emptyList();
}
}
@Override
public void subscribe() {
partitions = Collections.singleton(new TopicPartitionInfo(topic, null, null, true));
subscribed = false;
protected void doSubscribe(List<String> topicNames) {
queues = partitions.stream()
.map(TopicPartitionInfo::getFullTopicName)
.collect(Collectors.toSet());
queues.forEach(admin::createTopicIfNotExists);
}
@Override
public void subscribe(Set<TopicPartitionInfo> partitions) {
this.partitions = partitions;
subscribed = false;
protected void doCommit() {
try {
channel.basicAck(0, true);
} catch (IOException e) {
log.error("Failed to ack messages.", e);
}
}
@Override
public void unsubscribe() {
stopped = true;
protected void doUnsubscribe() {
if (channel != null) {
try {
channel.close();
@ -109,63 +119,6 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> implements TbQueue
}
}
@Override
public List<T> poll(long durationInMillis) {
if (!subscribed && partitions == null) {
try {
Thread.sleep(durationInMillis);
} catch (InterruptedException e) {
log.debug("Failed to await subscription", e);
}
} else {
if (!subscribed) {
queues = partitions.stream()
.map(TopicPartitionInfo::getFullTopicName)
.collect(Collectors.toSet());
queues.forEach(admin::createTopicIfNotExists);
subscribed = true;
}
List<T> result = queues.stream()
.map(queue -> {
try {
return channel.basicGet(queue, false);
} catch (IOException e) {
log.error("Failed to get messages from queue: [{}]", queue);
throw new RuntimeException("Failed to get messages from queue.", e);
}
}).filter(Objects::nonNull).map(message -> {
try {
return decode(message);
} catch (InvalidProtocolBufferException e) {
log.error("Failed to decode message: [{}].", message);
throw new RuntimeException("Failed to decode message.", e);
}
}).collect(Collectors.toList());
if (result.size() > 0) {
return result;
}
}
try {
Thread.sleep(durationInMillis);
} catch (InterruptedException e) {
if (!stopped) {
log.error("Failed to wait.", e);
}
}
return Collections.emptyList();
}
@Override
public void commit() {
try {
channel.basicAck(0, true);
} catch (IOException e) {
log.error("Failed to ack messages.", e);
}
}
public T decode(GetResponse message) throws InvalidProtocolBufferException {
DefaultTbQueueMsg msg = gson.fromJson(new String(message.getBody()), DefaultTbQueueMsg.class);
return decoder.decode(msg);

14
common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqProducerTemplate.java

@ -30,6 +30,8 @@ import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
@ -39,10 +41,12 @@ public class TbRabbitMqProducerTemplate<T extends TbQueueMsg> implements TbQueue
private final Gson gson = new Gson();
private final TbQueueAdmin admin;
private final TbRabbitMqSettings rabbitMqSettings;
private ListeningExecutorService producerExecutor;
private final ListeningExecutorService producerExecutor;
private final Channel channel;
private final Connection connection;
private final Set<TopicPartitionInfo> topics = ConcurrentHashMap.newKeySet();
public TbRabbitMqProducerTemplate(TbQueueAdmin admin, TbRabbitMqSettings rabbitMqSettings, String defaultTopic) {
this.admin = admin;
this.defaultTopic = defaultTopic;
@ -75,6 +79,7 @@ public class TbRabbitMqProducerTemplate<T extends TbQueueMsg> implements TbQueue
@Override
public void send(TopicPartitionInfo tpi, T msg, TbQueueCallback callback) {
createTopicIfNotExist(tpi);
AMQP.BasicProperties properties = new AMQP.BasicProperties();
try {
channel.basicPublish(rabbitMqSettings.getExchangeName(), tpi.getFullTopicName(), properties, gson.toJson(new DefaultTbQueueMsg(msg)).getBytes());
@ -110,4 +115,11 @@ public class TbRabbitMqProducerTemplate<T extends TbQueueMsg> implements TbQueue
}
}
private void createTopicIfNotExist(TopicPartitionInfo tpi) {
if (topics.contains(tpi)) {
return;
}
admin.createTopicIfNotExists(tpi.getFullTopicName());
topics.add(tpi);
}
}

139
common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsConsumerTemplate.java

@ -25,21 +25,17 @@ import com.amazonaws.services.sqs.model.Message;
import com.amazonaws.services.sqs.model.ReceiveMessageRequest;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.gson.Gson;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.TbQueueMsgDecoder;
import org.thingsboard.server.queue.common.AbstractParallelTbQueueConsumerTemplate;
import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -47,34 +43,28 @@ import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
public class TbAwsSqsConsumerTemplate<T extends TbQueueMsg> implements TbQueueConsumer<T> {
public class TbAwsSqsConsumerTemplate<T extends TbQueueMsg> extends AbstractParallelTbQueueConsumerTemplate<Message, T> {
private static final int MAX_NUM_MSGS = 10;
private final Gson gson = new Gson();
private final TbQueueAdmin admin;
private final AmazonSQS sqsClient;
private final String topic;
private final TbQueueMsgDecoder<T> decoder;
private final TbAwsSqsSettings sqsSettings;
private final List<AwsSqsMsgWrapper> pendingMessages = new CopyOnWriteArrayList<>();
private volatile Set<String> queueUrls;
private volatile Set<TopicPartitionInfo> partitions;
private ListeningExecutorService consumerExecutor;
private volatile boolean subscribed;
private volatile boolean stopped = false;
public TbAwsSqsConsumerTemplate(TbQueueAdmin admin, TbAwsSqsSettings sqsSettings, String topic, TbQueueMsgDecoder<T> decoder) {
super(topic);
this.admin = admin;
this.decoder = decoder;
this.topic = topic;
this.sqsSettings = sqsSettings;
AWSCredentials awsCredentials = new BasicAWSCredentials(sqsSettings.getAccessKeyId(), sqsSettings.getSecretAccessKey());
@ -87,81 +77,60 @@ public class TbAwsSqsConsumerTemplate<T extends TbQueueMsg> implements TbQueueCo
}
@Override
public String getTopic() {
return topic;
protected void doSubscribe(List<String> topicNames) {
queueUrls = topicNames.stream().map(this::getQueueUrl).collect(Collectors.toSet());
initNewExecutor(queueUrls.size() * sqsSettings.getThreadsPerTopic() + 1);
}
@Override
public void subscribe() {
partitions = Collections.singleton(new TopicPartitionInfo(topic, null, null, true));
subscribed = false;
protected List<Message> doPoll(long durationInMillis) {
int duration = (int) TimeUnit.MILLISECONDS.toSeconds(durationInMillis);
List<ListenableFuture<List<Message>>> futureList = queueUrls
.stream()
.map(url -> poll(url, duration))
.collect(Collectors.toList());
ListenableFuture<List<List<Message>>> futureResult = Futures.allAsList(futureList);
try {
return futureResult.get().stream()
.flatMap(List::stream)
.filter(Objects::nonNull)
.collect(Collectors.toList());
} catch (InterruptedException | ExecutionException e) {
if (stopped) {
log.info("[{}] Aws SQS consumer is stopped.", getTopic());
} else {
log.error("Failed to pool messages.", e);
}
return Collections.emptyList();
}
}
@Override
public void subscribe(Set<TopicPartitionInfo> partitions) {
this.partitions = partitions;
subscribed = false;
public T decode(Message message) throws InvalidProtocolBufferException {
DefaultTbQueueMsg msg = gson.fromJson(message.getBody(), DefaultTbQueueMsg.class);
return decoder.decode(msg);
}
@Override
public void unsubscribe() {
stopped = true;
if (sqsClient != null) {
sqsClient.shutdown();
}
if (consumerExecutor != null) {
consumerExecutor.shutdownNow();
}
protected void doCommit() {
pendingMessages.forEach(msg ->
consumerExecutor.submit(() -> {
List<DeleteMessageBatchRequestEntry> entries = msg.getMessages()
.stream()
.map(message -> new DeleteMessageBatchRequestEntry(message.getMessageId(), message.getReceiptHandle()))
.collect(Collectors.toList());
sqsClient.deleteMessageBatch(msg.getUrl(), entries);
}));
pendingMessages.clear();
}
@Override
public List<T> poll(long durationInMillis) {
if (!subscribed && partitions == null) {
try {
Thread.sleep(durationInMillis);
} catch (InterruptedException e) {
log.debug("Failed to await subscription", e);
}
} else {
if (!subscribed) {
List<String> topicNames = partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList());
queueUrls = topicNames.stream().map(this::getQueueUrl).collect(Collectors.toSet());
consumerExecutor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(queueUrls.size() * sqsSettings.getThreadsPerTopic() + 1));
subscribed = true;
}
if (!pendingMessages.isEmpty()) {
log.warn("Present {} non committed messages.", pendingMessages.size());
return Collections.emptyList();
}
List<ListenableFuture<List<Message>>> futureList = queueUrls
.stream()
.map(url -> poll(url, (int) TimeUnit.MILLISECONDS.toSeconds(durationInMillis)))
.collect(Collectors.toList());
ListenableFuture<List<List<Message>>> futureResult = Futures.allAsList(futureList);
try {
return futureResult.get().stream()
.flatMap(List::stream)
.map(msg -> {
try {
return decode(msg);
} catch (IOException e) {
log.error("Failed to decode message: [{}]", msg);
return null;
}
}).filter(Objects::nonNull)
.collect(Collectors.toList());
} catch (InterruptedException | ExecutionException e) {
if (stopped) {
log.info("[{}] Aws SQS consumer is stopped.", topic);
} else {
log.error("Failed to pool messages.", e);
}
}
protected void doUnsubscribe() {
stopped = true;
if (sqsClient != null) {
sqsClient.shutdown();
}
return Collections.emptyList();
shutdownExecutor();
}
private ListenableFuture<List<Message>> poll(String url, int waitTimeSeconds) {
@ -172,7 +141,6 @@ public class TbAwsSqsConsumerTemplate<T extends TbQueueMsg> implements TbQueueCo
ReceiveMessageRequest request = new ReceiveMessageRequest();
request
.withWaitTimeSeconds(waitTimeSeconds)
.withMessageAttributeNames("headers")
.withQueueUrl(url)
.withMaxNumberOfMessages(MAX_NUM_MSGS);
return sqsClient.receiveMessage(request).getMessages();
@ -194,25 +162,6 @@ public class TbAwsSqsConsumerTemplate<T extends TbQueueMsg> implements TbQueueCo
}, consumerExecutor);
}
@Override
public void commit() {
pendingMessages.forEach(msg ->
consumerExecutor.submit(() -> {
List<DeleteMessageBatchRequestEntry> entries = msg.getMessages()
.stream()
.map(message -> new DeleteMessageBatchRequestEntry(message.getMessageId(), message.getReceiptHandle()))
.collect(Collectors.toList());
sqsClient.deleteMessageBatch(msg.getUrl(), entries);
}));
pendingMessages.clear();
}
public T decode(Message message) throws InvalidProtocolBufferException {
DefaultTbQueueMsg msg = gson.fromJson(message.getBody(), DefaultTbQueueMsg.class);
return decoder.decode(msg);
}
@Data
private static class AwsSqsMsgWrapper {
private final String url;

5
common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsProducerTemplate.java

@ -37,6 +37,7 @@ import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
@ -80,7 +81,9 @@ public class TbAwsSqsProducerTemplate<T extends TbQueueMsg> implements TbQueuePr
sendMsgRequest.withQueueUrl(getQueueUrl(tpi.getFullTopicName()));
sendMsgRequest.withMessageBody(gson.toJson(new DefaultTbQueueMsg(msg)));
sendMsgRequest.withMessageGroupId(msg.getKey().toString());
sendMsgRequest.withMessageGroupId(tpi.getTopic());
sendMsgRequest.withMessageDeduplicationId(UUID.randomUUID().toString());
ListenableFuture<SendMessageResult> future = producerExecutor.submit(() -> sqsClient.sendMessage(sendMsgRequest));
Futures.addCallback(future, new FutureCallback<SendMessageResult>() {

1
common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsQueueAttributes.java

@ -55,7 +55,6 @@ public class TbAwsSqsQueueAttributes {
@PostConstruct
private void init() {
defaultAttributes.put(QueueAttributeName.FifoQueue.toString(), "true");
defaultAttributes.put(QueueAttributeName.ContentBasedDeduplication.toString(), "true");
coreAttributes = getConfigs(coreProperties);
ruleEngineAttributes = getConfigs(ruleEngineProperties);

23
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java

@ -37,6 +37,7 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@ -55,6 +56,8 @@ public class CoapTransportResource extends CoapResource {
private final Field observerField;
private final long timeout;
private final ConcurrentMap<String, TransportProtos.SessionInfoProto> tokenToSessionIdMap = new ConcurrentHashMap<>();
private final Set<UUID> rpcSubscriptions = ConcurrentHashMap.newKeySet();
private final Set<UUID> attributeSubscriptions = ConcurrentHashMap.newKeySet();
public CoapTransportResource(CoapTransportContext context, String name) {
super(name);
@ -149,11 +152,13 @@ public class CoapTransportResource extends CoapResource {
transportService.process(sessionInfo,
transportContext.getAdaptor().convertToPostAttributes(sessionId, request),
new CoapOkCallback(exchange));
reportActivity(sessionId, sessionInfo);
break;
case POST_TELEMETRY_REQUEST:
transportService.process(sessionInfo,
transportContext.getAdaptor().convertToPostTelemetry(sessionId, request),
new CoapOkCallback(exchange));
reportActivity(sessionId, sessionInfo);
break;
case CLAIM_REQUEST:
transportService.process(sessionInfo,
@ -161,6 +166,7 @@ public class CoapTransportResource extends CoapResource {
new CoapOkCallback(exchange));
break;
case SUBSCRIBE_ATTRIBUTES_REQUEST:
attributeSubscriptions.add(sessionId);
advanced.setObserver(new CoapExchangeObserverProxy((ExchangeObserver) observerField.get(advanced),
registerAsyncCoapSession(exchange, request, sessionInfo, sessionId)));
transportService.process(sessionInfo,
@ -168,6 +174,7 @@ public class CoapTransportResource extends CoapResource {
new CoapNoOpCallback(exchange));
break;
case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
attributeSubscriptions.remove(sessionId);
TransportProtos.SessionInfoProto attrSession = lookupAsyncSessionInfo(request);
if (attrSession != null) {
transportService.process(attrSession,
@ -177,6 +184,7 @@ public class CoapTransportResource extends CoapResource {
}
break;
case SUBSCRIBE_RPC_COMMANDS_REQUEST:
rpcSubscriptions.add(sessionId);
advanced.setObserver(new CoapExchangeObserverProxy((ExchangeObserver) observerField.get(advanced),
registerAsyncCoapSession(exchange, request, sessionInfo, sessionId)));
transportService.process(sessionInfo,
@ -184,13 +192,13 @@ public class CoapTransportResource extends CoapResource {
new CoapNoOpCallback(exchange));
break;
case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
rpcSubscriptions.remove(sessionId);
TransportProtos.SessionInfoProto rpcSession = lookupAsyncSessionInfo(request);
if (rpcSession != null) {
transportService.process(rpcSession,
TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(),
new CoapOkCallback(exchange));
transportService.process(sessionInfo, getSessionEventMsg(TransportProtos.SessionEvent.CLOSED), null);
transportService.deregisterSession(rpcSession);
closeAndDeregister(sessionInfo);
}
break;
case TO_DEVICE_RPC_RESPONSE:
@ -221,6 +229,14 @@ public class CoapTransportResource extends CoapResource {
}));
}
private void reportActivity(UUID sessionId, TransportProtos.SessionInfoProto sessionInfo) {
transportContext.getTransportService().process(sessionInfo, TransportProtos.SubscriptionInfoProto.newBuilder()
.setAttributeSubscription(attributeSubscriptions.contains(sessionId))
.setRpcSubscription(rpcSubscriptions.contains(sessionId))
.setLastActivityTime(System.currentTimeMillis())
.build(), TransportServiceCallback.EMPTY);
}
private TransportProtos.SessionInfoProto lookupAsyncSessionInfo(Request request) {
String token = request.getSource().getHostAddress() + ":" + request.getSourcePort() + ":" + request.getTokenString();
return tokenToSessionIdMap.remove(token);
@ -438,6 +454,9 @@ public class CoapTransportResource extends CoapResource {
private void closeAndDeregister(TransportProtos.SessionInfoProto session) {
transportService.process(session, getSessionEventMsg(TransportProtos.SessionEvent.CLOSED), null);
transportService.deregisterSession(session);
UUID sessionId = new UUID(session.getSessionIdMSB(), session.getSessionIdLSB());
rpcSubscriptions.remove(sessionId);
attributeSubscriptions.remove(sessionId);
}
}

13
common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java

@ -36,6 +36,7 @@ import org.thingsboard.server.common.transport.TransportContext;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto;
import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg;
@ -102,6 +103,7 @@ public class DeviceApiController {
TransportService transportService = transportContext.getTransportService();
transportService.process(sessionInfo, JsonConverter.convertToAttributesProto(new JsonParser().parse(json)),
new HttpOkCallback(responseWriter));
reportActivity(sessionInfo);
}));
return responseWriter;
}
@ -115,6 +117,7 @@ public class DeviceApiController {
TransportService transportService = transportContext.getTransportService();
transportService.process(sessionInfo, JsonConverter.convertToTelemetryProto(new JsonParser().parse(json)),
new HttpOkCallback(responseWriter));
reportActivity(sessionInfo);
}));
return responseWriter;
}
@ -274,7 +277,6 @@ public class DeviceApiController {
}
}
private static class HttpSessionListener implements SessionMsgListener {
private final DeferredResult<ResponseEntity> responseWriter;
@ -308,4 +310,13 @@ public class DeviceApiController {
responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg).toString(), HttpStatus.OK));
}
}
private void reportActivity(SessionInfoProto sessionInfo) {
transportContext.getTransportService().process(sessionInfo, TransportProtos.SubscriptionInfoProto.newBuilder()
.setAttributeSubscription(false)
.setRpcSubscription(false)
.setLastActivityTime(System.currentTimeMillis())
.build(), TransportServiceCallback.EMPTY);
}
}

13
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportServiceCallback.java

@ -20,7 +20,20 @@ package org.thingsboard.server.common.transport;
*/
public interface TransportServiceCallback<T> {
TransportServiceCallback<Void> EMPTY = new TransportServiceCallback<Void>() {
@Override
public void onSuccess(Void msg) {
}
@Override
public void onError(Throwable e) {
}
};
void onSuccess(T msg);
void onError(Throwable e);
}

4
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java

@ -31,8 +31,8 @@ public class OAuth2ClientMapperConfig {
private String firstNameAttributeKey;
private String lastNameAttributeKey;
private String tenantNameStrategy;
private String tenantNameStrategyPattern;
private String customerNameStrategyPattern;
private String tenantNamePattern;
private String customerNamePattern;
}
@Data

6
docker/docker-compose.hybrid.yml

@ -44,17 +44,19 @@ services:
- cassandra
tb-core2:
env_file:
- tb-node.cassandra.env
- tb-node.hybrid.env
depends_on:
- kafka
- redis
- postgres
- cassandra
tb-rule-engine1:
env_file:
- tb-node.cassandra.env
- tb-node.hybrid.env
depends_on:
- kafka
- redis
- postgres
- cassandra
tb-rule-engine2:
env_file:

2
docker/tb-js-executor.env

@ -1,4 +1,4 @@
TB_QUEUE_TYPE=kafka
REMOTE_JS_EVAL_REQUEST_TOPIC=js_eval.requests
TB_KAFKA_SERVERS=kafka:9092
LOGGER_LEVEL=info

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

@ -14,7 +14,7 @@
# limitations under the License.
#
service-type: "TB_SERVICE_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"
js:
@ -25,18 +25,18 @@ kafka:
# Kafka Bootstrap Servers
servers: "TB_KAFKA_SERVERS"
replication_factor: "TB_QUEUE_KAFKA_REPLICATION_FACTOR"
topic-properties: "TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES"
topic_properties: "TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES"
pubsub:
project_id: "TB_QUEUE_PUBSUB_PROJECT_ID"
service_account: "TB_QUEUE_PUBSUB_SERVICE_ACCOUNT"
queue-properties: "TB_QUEUE_PUBSUB_JE_QUEUE_PROPERTIES"
queue_properties: "TB_QUEUE_PUBSUB_JE_QUEUE_PROPERTIES"
aws_sqs:
access_key_id: "TB_QUEUE_AWS_SQS_ACCESS_KEY_ID"
secret_access_key: "TB_QUEUE_AWS_SQS_SECRET_ACCESS_KEY"
region: "TB_QUEUE_AWS_SQS_REGION"
queue-properties: "TB_QUEUE_AWS_SQS_JE_QUEUE_PROPERTIES"
queue_properties: "TB_QUEUE_AWS_SQS_JE_QUEUE_PROPERTIES"
rabbitmq:
host: "TB_QUEUE_RABBIT_MQ_HOST"
@ -44,14 +44,14 @@ rabbitmq:
virtual_host: "TB_QUEUE_RABBIT_MQ_VIRTUAL_HOST"
username: "TB_QUEUE_RABBIT_MQ_USERNAME"
password: "TB_QUEUE_RABBIT_MQ_PASSWORD"
queue-properties: "TB_QUEUE_RABBIT_MQ_JE_QUEUE_PROPERTIES"
queue_properties: "TB_QUEUE_RABBIT_MQ_JE_QUEUE_PROPERTIES"
service_bus:
namespace_name: "TB_QUEUE_SERVICE_BUS_NAMESPACE_NAME"
sas_key_name: "TB_QUEUE_SERVICE_BUS_SAS_KEY_NAME"
sas_key: "TB_QUEUE_SERVICE_BUS_SAS_KEY"
max_messages: "TB_QUEUE_SERVICE_BUS_MAX_MESSAGES"
queue-properties: "TB_QUEUE_SERVICE_BUS_JE_QUEUE_PROPERTIES"
queue_properties: "TB_QUEUE_SERVICE_BUS_JE_QUEUE_PROPERTIES"
logger:
level: "LOGGER_LEVEL"

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

@ -14,7 +14,7 @@
# limitations under the License.
#
service-type: "kafka"
queue_type: "kafka"
request_topic: "js_eval.requests"
js:
@ -25,13 +25,13 @@ kafka:
# Kafka Bootstrap Servers
servers: "localhost:9092"
replication_factor: "1"
topic-properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600"
topic_properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600"
pubsub:
queue-properties: "ackDeadlineInSec:30;messageRetentionInSec:604800"
queue_properties: "ackDeadlineInSec:30;messageRetentionInSec:604800"
aws_sqs:
queue-properties: "VisibilityTimeout:30;MaximumMessageSize:262144;MessageRetentionPeriod:604800"
queue_properties: "VisibilityTimeout:30;MaximumMessageSize:262144;MessageRetentionPeriod:604800"
rabbitmq:
host: "localhost"
@ -39,10 +39,10 @@ rabbitmq:
virtual_host: "/"
username: "admin"
password: "password"
queue-properties: "x-max-length-bytes:1048576000;x-message-ttl:604800000"
queue_properties: "x-max-length-bytes:1048576000;x-message-ttl:604800000"
service_bus:
queue-properties: "lockDurationInSec:30;maxSizeInMb:1024;messageTimeToLiveInSec:604800"
queue_properties: "lockDurationInSec:30;maxSizeInMb:1024;messageTimeToLiveInSec:604800"
logger:
level: "info"

1320
msa/js-executor/package-lock.json

File diff suppressed because it is too large

1
msa/js-executor/package.json

@ -22,6 +22,7 @@
"azure-sb": "^0.11.1",
"long": "^4.0.0",
"uuid-parse": "^1.0.0",
"uuid-random": "^1.3.0",
"winston": "^3.0.0",
"winston-daily-rotate-file": "^3.2.1"
},

38
msa/js-executor/queue/awsSqsTemplate.js

@ -19,6 +19,7 @@
const config = require('config'),
JsInvokeMessageProcessor = require('../api/jsInvokeMessageProcessor'),
logger = require('../config/logger')._logger('awsSqsTemplate');
const uuid = require('uuid-random');
const requestTopic = config.get('request_topic');
@ -26,10 +27,10 @@ const accessKeyId = config.get('aws_sqs.access_key_id');
const secretAccessKey = config.get('aws_sqs.secret_access_key');
const region = config.get('aws_sqs.region');
const AWS = require('aws-sdk');
const queueProperties = config.get('aws_sqs.queue-properties');
const poolInterval = config.get('js.response_poll_interval');
const queueProperties = config.get('aws_sqs.queue_properties');
const pollInterval = config.get('js.response_poll_interval');
let queueAttributes = {FifoQueue: 'true', ContentBasedDeduplication: 'true'};
let queueAttributes = {FifoQueue: 'true'};
let sqsClient;
let requestQueueURL;
const queueUrls = new Map();
@ -51,7 +52,12 @@ function AwsSqsProducer() {
queueUrls.set(responseTopic, responseQueueUrl);
}
let params = {MessageBody: msgBody, QueueUrl: responseQueueUrl, MessageGroupId: scriptId};
let params = {
MessageBody: msgBody,
QueueUrl: responseQueueUrl,
MessageGroupId: 'js_eval',
MessageDeduplicationId: uuid()
};
return new Promise((resolve, reject) => {
sqsClient.sendMessage(params, function (err, data) {
@ -74,11 +80,13 @@ function AwsSqsProducer() {
const queues = await getQueues();
queues.forEach(queueUrl => {
const delimiterPosition = queueUrl.lastIndexOf('/');
const queueName = queueUrl.substring(delimiterPosition + 1);
queueUrls.set(queueName, queueUrl);
})
if (queues) {
queues.forEach(queueUrl => {
const delimiterPosition = queueUrl.lastIndexOf('/');
const queueName = queueUrl.substring(delimiterPosition + 1);
queueUrls.set(queueName, queueUrl);
});
}
parseQueueProperties();
@ -95,6 +103,7 @@ function AwsSqsProducer() {
WaitTimeSeconds: poolInterval / 1000
};
while (!stopped) {
let pollStartTs = new Date().getTime();
const messages = await new Promise((resolve, reject) => {
sqsClient.receiveMessage(params, function (err, data) {
if (err) {
@ -127,6 +136,11 @@ function AwsSqsProducer() {
//do nothing
}
});
} else {
let pollDuration = new Date().getTime() - pollStartTs;
if (pollDuration < pollInterval) {
await sleep(pollInterval - pollDuration);
}
}
}
} catch (e) {
@ -175,6 +189,12 @@ function parseQueueProperties() {
});
}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
process.on('exit', () => {
stopped = true;
logger.info('Aws Sqs client stopped.');

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

@ -20,7 +20,7 @@ const config = require('config'),
logger = require('../config/logger')._logger('kafkaTemplate'),
KafkaJsWinstonLogCreator = require('../config/logger').KafkaJsWinstonLogCreator;
const replicationFactor = config.get('kafka.replication_factor');
const topicProperties = config.get('kafka.topic-properties');
const topicProperties = config.get('kafka.topic_properties');
let kafkaClient;
let kafkaAdmin;

29
msa/js-executor/queue/pubSubTemplate.js

@ -24,7 +24,7 @@ const {PubSub} = require('@google-cloud/pubsub');
const projectId = config.get('pubsub.project_id');
const credentials = JSON.parse(config.get('pubsub.service_account'));
const requestTopic = config.get('request_topic');
const queueProperties = config.get('pubsub.queue-properties');
const queueProperties = config.get('pubsub.queue_properties');
let pubSubClient;
@ -98,23 +98,32 @@ function PubSubProducer() {
async function createTopic(topic) {
if (!topics.includes(topic)) {
await pubSubClient.createTopic(topic);
try {
await pubSubClient.createTopic(topic);
logger.info('Created new Pub/Sub topic: %s', topic);
} catch (e) {
logger.info('Pub/Sub topic already exists');
}
topics.push(topic);
logger.info('Created new Pub/Sub topic: %s', topic);
}
await createSubscription(topic)
}
async function createSubscription(topic) {
if (!subscriptions.includes(topic)) {
await pubSubClient.createSubscription(topic, topic, {
topic: topic,
subscription: topic,
ackDeadlineSeconds: queueProps['ackDeadlineInSec'],
messageRetentionDuration: {seconds: queueProps['messageRetentionInSec']}
});
try {
await pubSubClient.createSubscription(topic, topic, {
topic: topic,
subscription: topic,
ackDeadlineSeconds: queueProps['ackDeadlineInSec'],
messageRetentionDuration: {seconds: queueProps['messageRetentionInSec']}
});
logger.info('Created new Pub/Sub subscription: %s', topic);
} catch (e) {
logger.info('Pub/Sub subscription already exists.');
}
subscriptions.push(topic);
logger.info('Created new Pub/Sub subscription: %s', topic);
}
}

26
msa/js-executor/queue/rabbitmqTemplate.js

@ -26,23 +26,23 @@ const port = config.get('rabbitmq.port');
const vhost = config.get('rabbitmq.virtual_host');
const username = config.get('rabbitmq.username');
const password = config.get('rabbitmq.password');
const queueProperties = config.get('rabbitmq.queue-properties');
const poolInterval = config.get('js.response_poll_interval');
const queueProperties = config.get('rabbitmq.queue_properties');
const pollInterval = config.get('js.response_poll_interval');
const amqp = require('amqplib/callback_api');
let queueParams = {durable: false, exclusive: false, autoDelete: false};
let queueOptions = {durable: false, exclusive: false, autoDelete: false};
let connection;
let channel;
let stopped = false;
const responseTopics = [];
let queues = [];
function RabbitMqProducer() {
this.send = async (responseTopic, scriptId, rawResponse, headers) => {
if (!responseTopics.includes(responseTopic)) {
if (!queues.includes(responseTopic)) {
await createQueue(responseTopic);
responseTopics.push(responseTopic);
queues.push(responseTopic);
}
let data = JSON.stringify(
@ -98,6 +98,7 @@ function RabbitMqProducer() {
const messageProcessor = new JsInvokeMessageProcessor(new RabbitMqProducer());
while (!stopped) {
let pollStartTs = new Date().getTime();
let message = await new Promise((resolve, reject) => {
channel.get(requestTopic, {}, function (err, msg) {
if (err) {
@ -112,7 +113,10 @@ function RabbitMqProducer() {
messageProcessor.onJsInvokeMessage(JSON.parse(message.content.toString('utf8')));
channel.ack(message);
} else {
await sleep(poolInterval);
let pollDuration = new Date().getTime() - pollStartTs;
if (pollDuration < pollInterval) {
await sleep(pollInterval - pollDuration);
}
}
}
} catch (e) {
@ -123,16 +127,18 @@ function RabbitMqProducer() {
})();
function parseQueueProperties() {
let args = {};
const props = queueProperties.split(';');
props.forEach(p => {
const delimiterPosition = p.indexOf(':');
queueParams[p.substring(0, delimiterPosition)] = p.substring(delimiterPosition + 1);
args[p.substring(0, delimiterPosition)] = +p.substring(delimiterPosition + 1);
});
queueOptions['arguments'] = args;
}
function createQueue(topic) {
async function createQueue(topic) {
return new Promise((resolve, reject) => {
channel.assertQueue(topic, queueParams, function (err) {
channel.assertQueue(topic, queueOptions, function (err) {
if (err) {
reject(err);
} else {

3
msa/js-executor/queue/serviceBusTemplate.js

@ -26,7 +26,7 @@ const requestTopic = config.get('request_topic');
const namespaceName = config.get('service_bus.namespace_name');
const sasKeyName = config.get('service_bus.sas_key_name');
const sasKey = config.get('service_bus.sas_key');
const queueProperties = config.get('service_bus.queue-properties');
const queueProperties = config.get('service_bus.queue_properties');
let sbClient;
let receiverClient;
@ -140,6 +140,7 @@ function parseQueueProperties() {
properties[p.substring(0, delimiterPosition)] = p.substring(delimiterPosition + 1);
});
queueOptions = {
DuplicateDetection: 'false',
MaxSizeInMegabytes: properties['maxSizeInMb'],
DefaultMessageTimeToLive: `PT${properties['messageTimeToLiveInSec']}S`,
LockDuration: `PT${properties['lockDurationInSec']}S`

2
msa/js-executor/server.js

@ -16,7 +16,7 @@
const config = require('config'), logger = require('./config/logger')._logger('main');
const serviceType = config.get('service-type');
const serviceType = config.get('queue_type');
switch (serviceType) {
case 'kafka':
logger.info('Starting kafka template.');

38
pom.xml

@ -30,10 +30,10 @@
<properties>
<main.dir>${basedir}</main.dir>
<pkg.user>thingsboard</pkg.user>
<spring-boot.version>2.2.4.RELEASE</spring-boot.version>
<spring-boot.version>2.2.6.RELEASE</spring-boot.version>
<spring-oauth2.version>2.1.2.RELEASE</spring-oauth2.version>
<spring.version>5.2.2.RELEASE</spring.version>
<spring-security.version>5.2.2.RELEASE</spring-security.version>
<spring.version>5.2.6.RELEASE</spring.version>
<spring-security.version>5.2.3.RELEASE</spring-security.version>
<spring-data-redis.version>2.2.4.RELEASE</spring-data-redis.version>
<jedis.version>3.1.0</jedis.version>
<jjwt.version>0.7.0</jjwt.version>
@ -62,14 +62,14 @@
<gson.version>2.6.2</gson.version>
<velocity.version>1.7</velocity.version>
<velocity-tools.version>2.0</velocity-tools.version>
<mail.version>1.4.3</mail.version>
<mail.version>1.6.2</mail.version>
<curator.version>4.2.0</curator.version>
<zookeeper.version>3.5.5</zookeeper.version>
<protobuf.version>3.11.4</protobuf.version>
<grpc.version>1.22.1</grpc.version>
<lombok.version>1.16.18</lombok.version>
<paho.client.version>1.1.0</paho.client.version>
<netty.version>4.1.45.Final</netty.version>
<netty.version>4.1.49.Final</netty.version>
<os-maven-plugin.version>1.5.0</os-maven-plugin.version>
<rabbitmq.version>4.8.0</rabbitmq.version>
<surfire.version>2.19.1</surfire.version>
@ -96,7 +96,7 @@
<snakeyaml.version>1.25</snakeyaml.version>
<struts.version>1.3.10</struts.version>
<amazonaws.sqs.version>1.11.747</amazonaws.sqs.version>
<pubsub.client.version>1.84.0</pubsub.client.version>
<pubsub.client.version>1.105.0</pubsub.client.version>
<azure-servicebus.version>3.2.0</azure-servicebus.version>
<passay.version>1.5.0</passay.version>
<ua-parser.version>1.4.3</ua-parser.version>
@ -471,12 +471,12 @@
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
<version>${spring.version}</version>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
<version>${spring.version}</version>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@ -591,8 +591,8 @@
<version>${rabbitmq.version}</version>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>${mail.version}</version>
</dependency>
<dependency>
@ -610,6 +610,12 @@
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>${zookeeper.version}</version>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
@ -692,6 +698,12 @@
<groupId>com.github.fge</groupId>
<artifactId>json-schema-validator</artifactId>
<version>${json-schema-validator.version}</version>
<exclusions>
<exclusion>
<groupId>javax.mail</groupId>
<artifactId>mailapi</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
@ -928,12 +940,6 @@
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-servicebus</artifactId>
<version>${azure-servicebus.version}</version>
<exclusions>
<exclusion>
<groupId>com.microsoft.azure</groupId>
<artifactId>adal4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.passay</groupId>

5
rule-engine/rule-engine-api/pom.xml

@ -93,5 +93,10 @@
<artifactId>spring-data-redis</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java

@ -81,8 +81,8 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode<TbClearAlarmNodeConfig
}
alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK);
return Futures.immediateFuture(new AlarmResult(false, false, true, alarm));
}, MoreExecutors.directExecutor());
}, MoreExecutors.directExecutor());
}, ctx.getDbCallbackExecutor());
}, ctx.getDbCallbackExecutor());
}, ctx.getDbCallbackExecutor());
}
}

580
ui-ngx/package-lock.json

@ -40,21 +40,21 @@
}
},
"@angular-devkit/build-angular": {
"version": "0.901.3",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.901.3.tgz",
"integrity": "sha512-0ivl+rxiFWY/cWpAhflaJfUmTGNRZn3m4gyt0YNFyHh7oJcwKFVpd9lxJAM4t47Xfyj0ICpTL99UBi4aeo3lEg==",
"version": "0.901.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.901.4.tgz",
"integrity": "sha512-Vod4bc6d38fuvqauIDQxKMe9hKW9m4QNzPKxEc+Dv5Xkv95WaGzkbUVu8M8t4E//sDDMpmcPdYOXSdR27WBi2Q==",
"dev": true,
"requires": {
"@angular-devkit/architect": "0.901.3",
"@angular-devkit/build-optimizer": "0.901.3",
"@angular-devkit/build-webpack": "0.901.3",
"@angular-devkit/core": "9.1.3",
"@angular-devkit/architect": "0.901.4",
"@angular-devkit/build-optimizer": "0.901.4",
"@angular-devkit/build-webpack": "0.901.4",
"@angular-devkit/core": "9.1.4",
"@babel/core": "7.9.0",
"@babel/generator": "7.9.3",
"@babel/preset-env": "7.9.0",
"@babel/template": "7.8.6",
"@jsdevtools/coverage-istanbul-loader": "3.0.3",
"@ngtools/webpack": "9.1.3",
"@ngtools/webpack": "9.1.4",
"ajv": "6.12.0",
"autoprefixer": "9.7.4",
"babel-loader": "8.0.6",
@ -108,6 +108,29 @@
"worker-plugin": "4.0.2"
},
"dependencies": {
"@angular-devkit/architect": {
"version": "0.901.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.901.4.tgz",
"integrity": "sha512-w4RMj7eLhUSh70HUy5tW4EXjLQFXk0Lfr9WiSy5gvPGp+zzYxknI+Wn4Xid1wU/WS+4tuMv5nJIaNaH2sABESQ==",
"dev": true,
"requires": {
"@angular-devkit/core": "9.1.4",
"rxjs": "6.5.4"
}
},
"@angular-devkit/core": {
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-9.1.4.tgz",
"integrity": "sha512-OPFQDmT4XabLMSRDgmnzedlOrc83DzQIgLcfoh/UhZ7aJKf/2Vq4l09p/DkMNI36vN5BRL0zDZt7TjvKNgyYgA==",
"dev": true,
"requires": {
"ajv": "6.12.0",
"fast-json-stable-stringify": "2.1.0",
"magic-string": "0.25.7",
"rxjs": "6.5.4",
"source-map": "0.7.3"
}
},
"core-js": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz",
@ -138,9 +161,9 @@
}
},
"@angular-devkit/build-optimizer": {
"version": "0.901.3",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.901.3.tgz",
"integrity": "sha512-fLRiQ1egH1qsmj5GMJ4ZWawtUYLwpIz1hKJ3WwAIFEFJF2+CDWhO1feUaXZRxPf5qwoj8S+8cSWjdcnKLf/mBw==",
"version": "0.901.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.901.4.tgz",
"integrity": "sha512-iDMlNRwd8ICMBKnycfw55hdnL5kCiqUjQn+aK/4uOMJMz49tiYMNJAaznXX2BFKmYSmbapKjEbzx9yMYRi9Y7w==",
"dev": true,
"requires": {
"loader-utils": "2.0.0",
@ -159,16 +182,39 @@
}
},
"@angular-devkit/build-webpack": {
"version": "0.901.3",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.901.3.tgz",
"integrity": "sha512-tZOiVNCrKaEAo7SuyXPpOoaPy/pbX70G8NFtyhKvoGt5dPbgybWTlibACyzOciBI9wUlGSaJiGPZzKTgL4O5OA==",
"version": "0.901.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.901.4.tgz",
"integrity": "sha512-YBOufI4TGAtIDCS6erFygYJX/fkd8xwI58d+7iFIBmtIJC4/fpGjX6qkHMCBgh8HXAkEPSORBCYQn9O44J1ZXQ==",
"dev": true,
"requires": {
"@angular-devkit/architect": "0.901.3",
"@angular-devkit/core": "9.1.3",
"@angular-devkit/architect": "0.901.4",
"@angular-devkit/core": "9.1.4",
"rxjs": "6.5.4"
},
"dependencies": {
"@angular-devkit/architect": {
"version": "0.901.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.901.4.tgz",
"integrity": "sha512-w4RMj7eLhUSh70HUy5tW4EXjLQFXk0Lfr9WiSy5gvPGp+zzYxknI+Wn4Xid1wU/WS+4tuMv5nJIaNaH2sABESQ==",
"dev": true,
"requires": {
"@angular-devkit/core": "9.1.4",
"rxjs": "6.5.4"
}
},
"@angular-devkit/core": {
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-9.1.4.tgz",
"integrity": "sha512-OPFQDmT4XabLMSRDgmnzedlOrc83DzQIgLcfoh/UhZ7aJKf/2Vq4l09p/DkMNI36vN5BRL0zDZt7TjvKNgyYgA==",
"dev": true,
"requires": {
"ajv": "6.12.0",
"fast-json-stable-stringify": "2.1.0",
"magic-string": "0.25.7",
"rxjs": "6.5.4",
"source-map": "0.7.3"
}
},
"rxjs": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz",
@ -205,16 +251,29 @@
}
},
"@angular-devkit/schematics": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-9.1.3.tgz",
"integrity": "sha512-nNRoQPqoin5jzLKmTabIPLSqVw2Zjnk78XC4AhAM7rSisWMsZ6pw5+BNDITiKfq0LkSXfOs0DoJV/Vr9mLkn/A==",
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-9.1.4.tgz",
"integrity": "sha512-RAbdnUEZ3JTLmWSBiXT5trsVx8Fi72fxN9CiRaluM09Cytg6BUc1wC5XCO0YPvhI400+3Ro1nwjPXezjg7LXzQ==",
"dev": true,
"requires": {
"@angular-devkit/core": "9.1.3",
"@angular-devkit/core": "9.1.4",
"ora": "4.0.3",
"rxjs": "6.5.4"
},
"dependencies": {
"@angular-devkit/core": {
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-9.1.4.tgz",
"integrity": "sha512-OPFQDmT4XabLMSRDgmnzedlOrc83DzQIgLcfoh/UhZ7aJKf/2Vq4l09p/DkMNI36vN5BRL0zDZt7TjvKNgyYgA==",
"dev": true,
"requires": {
"ajv": "6.12.0",
"fast-json-stable-stringify": "2.1.0",
"magic-string": "0.25.7",
"rxjs": "6.5.4",
"source-map": "0.7.3"
}
},
"rxjs": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz",
@ -227,29 +286,29 @@
}
},
"@angular/animations": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-9.1.3.tgz",
"integrity": "sha512-QTQZSnXxr9SCgrN2SZsTMzS/UldrZ65y6Kuqs9yyghNc694eMbabgiPdeegjvcKlDhVPCN2x9Bjfb1vXPOfdvQ=="
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-9.1.4.tgz",
"integrity": "sha512-gMo/DbCm5BDArladMAeC7/75T2DvhLr4CSUGJt/P/aimTEG2ywoAALs3pzwSSe4qxrHiR0OIksVW3l4km3iXEw=="
},
"@angular/cdk": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.2.1.tgz",
"integrity": "sha512-aSG1UNPszkSnpNuDCNd7ZgT29oQ8vqHPmoqjvJI0JkEv3i6uEs5tRuhWl3TK39wDNuwdlq0AY47XTa/0Ppb5RQ==",
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.2.2.tgz",
"integrity": "sha512-VNd+KuMN6cBcy4/8OyMxqYaxdjPP6IyCqIVijB2JREkc5Sg4VWmPgx2L3rHt/DzjsVBVRgx35uqOMymDezG3jQ==",
"requires": {
"parse5": "^5.0.0"
}
},
"@angular/cli": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-9.1.3.tgz",
"integrity": "sha512-/7yHOuiyMgpcoBuADPrF4Eo9VDysA57fsyrMlOH2WZmKdsjW032StS9EIOue5RNQ7y0DwrtgtnkjbpZ6nYo3Pw==",
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-9.1.4.tgz",
"integrity": "sha512-H9MqoT4zyIv+Yo3cvRVkzafWGHsqt7jUvtvGwMHIDMTfEX+Q8yiYlDLL6WM3Eb6/hDmLcRGC/GI495sKS1z5qA==",
"dev": true,
"requires": {
"@angular-devkit/architect": "0.901.3",
"@angular-devkit/core": "9.1.3",
"@angular-devkit/schematics": "9.1.3",
"@schematics/angular": "9.1.3",
"@schematics/update": "0.901.3",
"@angular-devkit/architect": "0.901.4",
"@angular-devkit/core": "9.1.4",
"@angular-devkit/schematics": "9.1.4",
"@schematics/angular": "9.1.4",
"@schematics/update": "0.901.4",
"@yarnpkg/lockfile": "1.1.0",
"ansi-colors": "4.1.1",
"debug": "4.1.1",
@ -267,12 +326,44 @@
"uuid": "7.0.2"
},
"dependencies": {
"@angular-devkit/architect": {
"version": "0.901.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.901.4.tgz",
"integrity": "sha512-w4RMj7eLhUSh70HUy5tW4EXjLQFXk0Lfr9WiSy5gvPGp+zzYxknI+Wn4Xid1wU/WS+4tuMv5nJIaNaH2sABESQ==",
"dev": true,
"requires": {
"@angular-devkit/core": "9.1.4",
"rxjs": "6.5.4"
}
},
"@angular-devkit/core": {
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-9.1.4.tgz",
"integrity": "sha512-OPFQDmT4XabLMSRDgmnzedlOrc83DzQIgLcfoh/UhZ7aJKf/2Vq4l09p/DkMNI36vN5BRL0zDZt7TjvKNgyYgA==",
"dev": true,
"requires": {
"ajv": "6.12.0",
"fast-json-stable-stringify": "2.1.0",
"magic-string": "0.25.7",
"rxjs": "6.5.4",
"source-map": "0.7.3"
}
},
"ansi-colors": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
"integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
"dev": true
},
"rxjs": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz",
"integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==",
"dev": true,
"requires": {
"tslib": "^1.9.0"
}
},
"semver": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz",
@ -288,19 +379,19 @@
}
},
"@angular/common": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-9.1.3.tgz",
"integrity": "sha512-OGrd/x+WoceskeJWZI3ur+KqVoBHn2kmbg57RyJ8Ng/BkdDVYiXzQ/UvvX1WYGH/YzmiiVdlnVq55xkHkzGaXw=="
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-9.1.4.tgz",
"integrity": "sha512-JvCoCWVbx0tF7l/0WTi24ui/mc2SElyVSNchR4VK/FViARnkvnSBdI/Ef5QWXrsPyKU4PYBtnWWgyxRspH+FBA=="
},
"@angular/compiler": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-9.1.3.tgz",
"integrity": "sha512-NTqGZUwwdm/RIG1rWPIqQwNyyRcPOmNlv+t1KpydFtycpTut0lAh0shiu57h83Cl2LgaFV4veyiAAtNmxFmqXA=="
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-9.1.4.tgz",
"integrity": "sha512-B+f3lviFNEJtL9V9exSKYPSz2Ddb6dxgPzQR7GSjGikDo+fKMtC1PjNwgJooS9gavhQx30uwkEEMIPYQbM6nNA=="
},
"@angular/compiler-cli": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-9.1.3.tgz",
"integrity": "sha512-FqDPmEbJQmzzxqF08CJfqT9XKJFPYdyZuFFStKkJEZ5s1JrjrBlgQSqdF0qp5jWNsjTB87USLHC5zAlzJtMKxQ==",
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-9.1.4.tgz",
"integrity": "sha512-n3PzqNnPD7s/AF9mv5CnarK0sgfoq4txFncHjJWBSltuTQoz6BDZyjuEdqsSLUvgAZPeLsmohemOzEE38HYHZA==",
"dev": true,
"requires": {
"canonical-path": "1.0.0",
@ -501,9 +592,9 @@
}
},
"@angular/core": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-9.1.3.tgz",
"integrity": "sha512-r+GyeCuBw9SUBzpPWhKxzTOkNYotYp3gANSWndihLC9P6W5T8Hfyg8uYcxKy7L42bpx8eXZOXFq91cioVwZtqA=="
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-9.1.4.tgz",
"integrity": "sha512-ND240vncmVD2KVe/KSQU3d/DxxoRipFg1+jFOFZGt0n0orCBHk/V1fu9iaG1sRyldL0+rCQ+fTI+1N4DTmMnxA=="
},
"@angular/flex-layout": {
"version": "9.0.0-beta.29",
@ -511,35 +602,35 @@
"integrity": "sha512-93sxR+kYfYMOdnlWL0Q77FZ428gg8XnBu0YZm6GsCdkw/vLggIT/G1ZAqHlCPIODt6pxmCJ5KXh4ShvniIYDsA=="
},
"@angular/forms": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-9.1.3.tgz",
"integrity": "sha512-t3BexN5EHgCK6q0R04AuyNb6ZUP6sd9fJ+ER98yWws3HXV6FT+J5IB7dVnhelLoU1GTnql4TJSS+LTVradKZ7g=="
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-9.1.4.tgz",
"integrity": "sha512-Njt+pMLfPBchL0/ayIjJqXL6ZfM4Ccvf7KO1wS1HMzh3QlmfNa0JSgc4pfrbRJAMN9g7V/FYLyKejs1bJZkenA=="
},
"@angular/language-service": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-9.1.3.tgz",
"integrity": "sha512-mcZDO6C2BpDinMjDeMAdohTefpFxNu/S0JEqewYQIQ8TXtKm77xDPUdNdNb4qD4zYW+c46f7UXNMX/tbYCicQw==",
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-9.1.4.tgz",
"integrity": "sha512-eyVxxiegdb4ESdFGfkuDN+YfUbOVHRQLjIl6ACFJQDNHzVXzbmuqpyr5hIJANIVady103/7+dqRxxJo1DdIdTQ==",
"dev": true
},
"@angular/material": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-9.2.1.tgz",
"integrity": "sha512-nqn/0Eg04DxwnkRGSM1xnmGgtfHYOBcEPbFeTu8c1qAbjFEozd6tpw4y6dQrCCL/JLNIRQPsxsUsVnKeWDF/4Q=="
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-9.2.2.tgz",
"integrity": "sha512-gdQiMJ6PtW/5fd+0mglHFyzxULDCBGjn9RTET3sUq2rkc9+jBXr4OvnsUyBWSnqqv97XqotVDIx5JgE4/YX/Rw=="
},
"@angular/platform-browser": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-9.1.3.tgz",
"integrity": "sha512-ts6b7QBp4+UpuQS0Eb+FOT4MZVPUwWtWH4iIfBILJaG/0Bk4jHZeXuLFcpgeZkwMNCZEt9f0JrhnHfvuLCez5g=="
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-9.1.4.tgz",
"integrity": "sha512-mBCHfTl+5tQfaUiGlDujP7mFBzovFc54Zi2kcCE8DSdSSVQ2TPBo6hXa6y2cL3hJPFZzQ7mC4ORFrsGADhHn/w=="
},
"@angular/platform-browser-dynamic": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-9.1.3.tgz",
"integrity": "sha512-9U8PkBflnQtKIZ4t0AqYUaKLg7Kx1BvJ1D/S8FZo/V5kBJK6NkGVu58XddWVc+dtO3V+vCC/VLG7fb2PMf/V5g=="
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-9.1.4.tgz",
"integrity": "sha512-YtVbnxyS6FU7xNpA6A95tmSfrB8+WC7OH3mbP8M9NaGk0OYz8B/JOe1HByP4JRpEGCvBtXdJ2NSW/MpLIT8SiQ=="
},
"@angular/router": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-9.1.3.tgz",
"integrity": "sha512-NmOazeJ2WVT5eTyNHXYxBRAHic6i6Ku5DgsGWBgjUIR/Qmwb/i/xpagbA9WQQZ2OrGbp4ICEYgGkikeIrlQLPA=="
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-9.1.4.tgz",
"integrity": "sha512-yUyjCgG2P2Jh8MvoyC6yirmAtx1Qe7MKLuLvsa9WOB571QNEcNLTYMfAMHUKsQTcE/+o984QyLlneoibgI9wFA=="
},
"@auth0/angular-jwt": {
"version": "4.0.0",
@ -559,12 +650,12 @@
}
},
"@babel/compat-data": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.9.0.tgz",
"integrity": "sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g==",
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.9.6.tgz",
"integrity": "sha512-5QPTrNen2bm7RBc7dsOmcA5hbrS4O2Vhmk5XOL4zWW/zD/hV0iinpefDlkm+tBBy8kDtFaaeEvmAqt+nURAV2g==",
"dev": true,
"requires": {
"browserslist": "^4.9.1",
"browserslist": "^4.11.1",
"invariant": "^2.2.4",
"semver": "^5.5.0"
}
@ -641,13 +732,13 @@
}
},
"@babel/helper-compilation-targets": {
"version": "7.8.7",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.7.tgz",
"integrity": "sha512-4mWm8DCK2LugIS+p1yArqvG1Pf162upsIsjE7cNBjez+NjliQpVhj20obE520nao0o14DaTnFJv+Fw5a0JpoUw==",
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.9.6.tgz",
"integrity": "sha512-x2Nvu0igO0ejXzx09B/1fGBxY9NXQlBW2kZsSxCJft+KHN8t9XWzIvFxtPHnBOAXpVsdxZKZFbRUC8TsNKajMw==",
"dev": true,
"requires": {
"@babel/compat-data": "^7.8.6",
"browserslist": "^4.9.1",
"@babel/compat-data": "^7.9.6",
"browserslist": "^4.11.1",
"invariant": "^2.2.4",
"levenary": "^1.1.1",
"semver": "^5.5.0"
@ -785,15 +876,69 @@
}
},
"@babel/helper-replace-supers": {
"version": "7.8.6",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz",
"integrity": "sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==",
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.9.6.tgz",
"integrity": "sha512-qX+chbxkbArLyCImk3bWV+jB5gTNU/rsze+JlcF6Nf8tVTigPJSI1o1oBow/9Resa1yehUO9lIipsmu9oG4RzA==",
"dev": true,
"requires": {
"@babel/helper-member-expression-to-functions": "^7.8.3",
"@babel/helper-optimise-call-expression": "^7.8.3",
"@babel/traverse": "^7.8.6",
"@babel/types": "^7.8.6"
"@babel/traverse": "^7.9.6",
"@babel/types": "^7.9.6"
},
"dependencies": {
"@babel/generator": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.6.tgz",
"integrity": "sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ==",
"dev": true,
"requires": {
"@babel/types": "^7.9.6",
"jsesc": "^2.5.1",
"lodash": "^4.17.13",
"source-map": "^0.5.0"
}
},
"@babel/parser": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.6.tgz",
"integrity": "sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q==",
"dev": true
},
"@babel/traverse": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.6.tgz",
"integrity": "sha512-b3rAHSjbxy6VEAvlxM8OV/0X4XrG72zoxme6q1MOoe2vd0bEc+TwayhuC1+Dfgqh1QEG+pj7atQqvUprHIccsg==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.8.3",
"@babel/generator": "^7.9.6",
"@babel/helper-function-name": "^7.9.5",
"@babel/helper-split-export-declaration": "^7.8.3",
"@babel/parser": "^7.9.6",
"@babel/types": "^7.9.6",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.13"
}
},
"@babel/types": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.6.tgz",
"integrity": "sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.9.5",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true
}
}
},
"@babel/helper-simple-access": {
@ -834,14 +979,68 @@
}
},
"@babel/helpers": {
"version": "7.9.2",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.2.tgz",
"integrity": "sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA==",
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.6.tgz",
"integrity": "sha512-tI4bUbldloLcHWoRUMAj4g1bF313M/o6fBKhIsb3QnGVPwRm9JsNf/gqMkQ7zjqReABiffPV6RWj7hEglID5Iw==",
"dev": true,
"requires": {
"@babel/template": "^7.8.3",
"@babel/traverse": "^7.9.0",
"@babel/types": "^7.9.0"
"@babel/traverse": "^7.9.6",
"@babel/types": "^7.9.6"
},
"dependencies": {
"@babel/generator": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.6.tgz",
"integrity": "sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ==",
"dev": true,
"requires": {
"@babel/types": "^7.9.6",
"jsesc": "^2.5.1",
"lodash": "^4.17.13",
"source-map": "^0.5.0"
}
},
"@babel/parser": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.6.tgz",
"integrity": "sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q==",
"dev": true
},
"@babel/traverse": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.6.tgz",
"integrity": "sha512-b3rAHSjbxy6VEAvlxM8OV/0X4XrG72zoxme6q1MOoe2vd0bEc+TwayhuC1+Dfgqh1QEG+pj7atQqvUprHIccsg==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.8.3",
"@babel/generator": "^7.9.6",
"@babel/helper-function-name": "^7.9.5",
"@babel/helper-split-export-declaration": "^7.8.3",
"@babel/parser": "^7.9.6",
"@babel/types": "^7.9.6",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.13"
}
},
"@babel/types": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.6.tgz",
"integrity": "sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.9.5",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true
}
}
},
"@babel/highlight": {
@ -913,9 +1112,9 @@
}
},
"@babel/plugin-proposal-object-rest-spread": {
"version": "7.9.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.5.tgz",
"integrity": "sha512-VP2oXvAf7KCYTthbUHwBlewbl1Iq059f6seJGsxMizaCdgHIeczOr7FBqELhSqfkIl04Fi8okzWzl63UKbQmmg==",
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.6.tgz",
"integrity": "sha512-Ga6/fhGqA9Hj+y6whNpPv8psyaK5xzrQwSPsGPloVkvmH+PqW1ixdnfJ9uIO06OjQNYol3PMnfmJ8vfZtkzF+A==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.8.3",
@ -1174,38 +1373,38 @@
}
},
"@babel/plugin-transform-modules-amd": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.0.tgz",
"integrity": "sha512-vZgDDF003B14O8zJy0XXLnPH4sg+9X5hFBBGN1V+B2rgrB+J2xIypSN6Rk9imB2hSTHQi5OHLrFWsZab1GMk+Q==",
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.6.tgz",
"integrity": "sha512-zoT0kgC3EixAyIAU+9vfaUVKTv9IxBDSabgHoUCBP6FqEJ+iNiN7ip7NBKcYqbfUDfuC2mFCbM7vbu4qJgOnDw==",
"dev": true,
"requires": {
"@babel/helper-module-transforms": "^7.9.0",
"@babel/helper-plugin-utils": "^7.8.3",
"babel-plugin-dynamic-import-node": "^2.3.0"
"babel-plugin-dynamic-import-node": "^2.3.3"
}
},
"@babel/plugin-transform-modules-commonjs": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.0.tgz",
"integrity": "sha512-qzlCrLnKqio4SlgJ6FMMLBe4bySNis8DFn1VkGmOcxG9gqEyPIOzeQrA//u0HAKrWpJlpZbZMPB1n/OPa4+n8g==",
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.6.tgz",
"integrity": "sha512-7H25fSlLcn+iYimmsNe3uK1at79IE6SKW9q0/QeEHTMC9MdOZ+4bA+T1VFB5fgOqBWoqlifXRzYD0JPdmIrgSQ==",
"dev": true,
"requires": {
"@babel/helper-module-transforms": "^7.9.0",
"@babel/helper-plugin-utils": "^7.8.3",
"@babel/helper-simple-access": "^7.8.3",
"babel-plugin-dynamic-import-node": "^2.3.0"
"babel-plugin-dynamic-import-node": "^2.3.3"
}
},
"@babel/plugin-transform-modules-systemjs": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.0.tgz",
"integrity": "sha512-FsiAv/nao/ud2ZWy4wFacoLOm5uxl0ExSQ7ErvP7jpoihLR6Cq90ilOFyX9UXct3rbtKsAiZ9kFt5XGfPe/5SQ==",
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.6.tgz",
"integrity": "sha512-NW5XQuW3N2tTHim8e1b7qGy7s0kZ2OH3m5octc49K1SdAKGxYxeIx7hiIz05kS1R2R+hOWcsr1eYwcGhrdHsrg==",
"dev": true,
"requires": {
"@babel/helper-hoist-variables": "^7.8.3",
"@babel/helper-module-transforms": "^7.9.0",
"@babel/helper-plugin-utils": "^7.8.3",
"babel-plugin-dynamic-import-node": "^2.3.0"
"babel-plugin-dynamic-import-node": "^2.3.3"
}
},
"@babel/plugin-transform-modules-umd": {
@ -1572,14 +1771,14 @@
"integrity": "sha512-Eqy2vuhgTY+BeqjOiXBBmbGPRC4HTa4nBSo9NcyZ8Z0MoaKo9YCbqC4CNCEPYqRbJeDUeBwfejnHR94eVMB2cw=="
},
"@material-ui/core": {
"version": "4.9.12",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.9.12.tgz",
"integrity": "sha512-JtRm1iNw3PRg+bzULS1uRKhdIJ2jhKO3/5ptO6kTADARsv5KmhzMbM+PYmVS09qm9Yu3ilwka4dYrtjqea53Lw==",
"version": "4.9.13",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.9.13.tgz",
"integrity": "sha512-GEXNwUr+laZ0N+F1efmHB64Fyg+uQIRXLqbSejg3ebSXgLYNpIjnMOPRfWdu4rICq0dAIgvvNXGkKDMcf3AMpA==",
"requires": {
"@babel/runtime": "^7.4.4",
"@material-ui/react-transition-group": "^4.3.0",
"@material-ui/styles": "^4.9.10",
"@material-ui/system": "^4.9.10",
"@material-ui/styles": "^4.9.13",
"@material-ui/system": "^4.9.13",
"@material-ui/types": "^5.0.1",
"@material-ui/utils": "^4.9.12",
"@types/react-transition-group": "^4.2.0",
@ -1631,15 +1830,15 @@
}
},
"@material-ui/styles": {
"version": "4.9.10",
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.9.10.tgz",
"integrity": "sha512-EXIXlqVyFDnjXF6tj72y6ZxiSy+mHtrsCo3Srkm3XUeu3Z01aftDBy7ZSr3TQ02gXHTvDSBvegp3Le6p/tl7eA==",
"version": "4.9.13",
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.9.13.tgz",
"integrity": "sha512-lWlXJanBdHQ18jW/yphedRokHcvZD1GdGzUF/wQxKDsHwDDfO45ZkAxuSBI202dG+r1Ph483Z3pFykO2obeSRA==",
"requires": {
"@babel/runtime": "^7.4.4",
"@emotion/hash": "^0.8.0",
"@material-ui/types": "^5.0.1",
"@material-ui/utils": "^4.9.6",
"clsx": "^1.0.2",
"clsx": "^1.0.4",
"csstype": "^2.5.2",
"hoist-non-react-statics": "^3.3.2",
"jss": "^10.0.3",
@ -1654,9 +1853,9 @@
}
},
"@material-ui/system": {
"version": "4.9.10",
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.9.10.tgz",
"integrity": "sha512-E+t0baX2TBZk6ALm8twG6objpsxLdMM4MDm1++LMt2m7CetCAEc3aIAfDaprk4+tm5hFT1Cah5dRWk8EeIFQYw==",
"version": "4.9.13",
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.9.13.tgz",
"integrity": "sha512-6AlpvdW6KJJ5bF1Xo2OD13sCN8k+nlL36412/bWnWZOKIfIMo/Lb8c8d1DOIaT/RKWxTEUaWnKZjabVnA3eZjA==",
"requires": {
"@babel/runtime": "^7.4.4",
"@material-ui/utils": "^4.9.6",
@ -1694,17 +1893,30 @@
"integrity": "sha512-KDvuNBxfTgDHjESiouEoPXq8GCUYbHnA+jOs8MzzwVDmxNLeZuDy3amaVzKv9Q5nncI6mNGcXOzTKWzapET8Sw=="
},
"@ngtools/webpack": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-9.1.3.tgz",
"integrity": "sha512-yQ7LHqICC1Xf4E4Jw1jDCAJHYwPX7T7mRFxMd+R/0EBDcz1XBSh85+vnVs2PoJHVH+8ONOEkZKKSjABe1CZUvg==",
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-9.1.4.tgz",
"integrity": "sha512-CDlQzMnWraxf/dT3G5L+0N0VniWHWdzjLRP8pWBoyV0xlzWS1yL/lj8Mas2jEIuaUxZ8bi29mE7xa8prqewtBQ==",
"dev": true,
"requires": {
"@angular-devkit/core": "9.1.3",
"@angular-devkit/core": "9.1.4",
"enhanced-resolve": "4.1.1",
"rxjs": "6.5.4",
"webpack-sources": "1.4.3"
},
"dependencies": {
"@angular-devkit/core": {
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-9.1.4.tgz",
"integrity": "sha512-OPFQDmT4XabLMSRDgmnzedlOrc83DzQIgLcfoh/UhZ7aJKf/2Vq4l09p/DkMNI36vN5BRL0zDZt7TjvKNgyYgA==",
"dev": true,
"requires": {
"ajv": "6.12.0",
"fast-json-stable-stringify": "2.1.0",
"magic-string": "0.25.7",
"rxjs": "6.5.4",
"source-map": "0.7.3"
}
},
"rxjs": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz",
@ -1738,23 +1950,47 @@
}
},
"@schematics/angular": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-9.1.3.tgz",
"integrity": "sha512-3vxrPyvcIWvyWgbx/fpeQvMThXlmcnHTXVn2dpTcE2BzstAYdG17W3qouhvmlqd6m28orxkKKiZaJYMNTtnV/A==",
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-9.1.4.tgz",
"integrity": "sha512-7nbiYBRgXc+f0sa5iXTcF6/VMy/Xq0wyKKnDFiLCb2aFYR7EXRHWF2GuwDIg2bvFugLuCBDoNnXeddLE1TXGWg==",
"dev": true,
"requires": {
"@angular-devkit/core": "9.1.3",
"@angular-devkit/schematics": "9.1.3"
"@angular-devkit/core": "9.1.4",
"@angular-devkit/schematics": "9.1.4"
},
"dependencies": {
"@angular-devkit/core": {
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-9.1.4.tgz",
"integrity": "sha512-OPFQDmT4XabLMSRDgmnzedlOrc83DzQIgLcfoh/UhZ7aJKf/2Vq4l09p/DkMNI36vN5BRL0zDZt7TjvKNgyYgA==",
"dev": true,
"requires": {
"ajv": "6.12.0",
"fast-json-stable-stringify": "2.1.0",
"magic-string": "0.25.7",
"rxjs": "6.5.4",
"source-map": "0.7.3"
}
},
"rxjs": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz",
"integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==",
"dev": true,
"requires": {
"tslib": "^1.9.0"
}
}
}
},
"@schematics/update": {
"version": "0.901.3",
"resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.901.3.tgz",
"integrity": "sha512-kwfc9LMMi6ryN2oiat9vTqGMK1jKwI1c45Nm1xala35nsbQSQ12qeL3MrVmnSCeI14+cinFUbp8mWI9/3M+/Lg==",
"version": "0.901.4",
"resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.901.4.tgz",
"integrity": "sha512-jCtZY2Fbj502gKN5gdu1brnRy/ssyzTrWm69Ty73V+t8uL7nLr+/hny/VBJ8CiQnKQvxcgVl1xgvI1cXzpysVA==",
"dev": true,
"requires": {
"@angular-devkit/core": "9.1.3",
"@angular-devkit/schematics": "9.1.3",
"@angular-devkit/core": "9.1.4",
"@angular-devkit/schematics": "9.1.4",
"@yarnpkg/lockfile": "1.1.0",
"ini": "1.3.5",
"npm-package-arg": "^8.0.0",
@ -1764,6 +2000,19 @@
"semver-intersect": "1.4.0"
},
"dependencies": {
"@angular-devkit/core": {
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-9.1.4.tgz",
"integrity": "sha512-OPFQDmT4XabLMSRDgmnzedlOrc83DzQIgLcfoh/UhZ7aJKf/2Vq4l09p/DkMNI36vN5BRL0zDZt7TjvKNgyYgA==",
"dev": true,
"requires": {
"ajv": "6.12.0",
"fast-json-stable-stringify": "2.1.0",
"magic-string": "0.25.7",
"rxjs": "6.5.4",
"source-map": "0.7.3"
}
},
"rxjs": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz",
@ -1846,9 +2095,9 @@
}
},
"@types/jquery": {
"version": "3.3.36",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.36.tgz",
"integrity": "sha512-jHL8J5y5fJ0+C9zCTkeOvX4zqRnPug3r6JhAqAYl2YyBCYHiXTbZSH0MRCpayZADed5TigPjH92dEKczUFT2TQ==",
"version": "3.3.38",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.38.tgz",
"integrity": "sha512-nkDvmx7x/6kDM5guu/YpXkGZ/Xj/IwGiLDdKM99YA5Vag7pjGyTJ8BNUh/6hxEn/sEu5DKtyRgnONJ7EmOoKrA==",
"requires": {
"@types/sizzle": "*"
}
@ -3223,9 +3472,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001048",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001048.tgz",
"integrity": "sha512-g1iSHKVxornw0K8LG9LLdf+Fxnv7T1Z+mMsf0/YYLclQX4Cd522Ap0Lrw6NFqHgezit78dtyWxzlV2Xfc7vgRg==",
"version": "1.0.30001051",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001051.tgz",
"integrity": "sha512-sw8UUnTlRevawTMZKN7vpfwSjCBVoiMPlYd8oT2VwNylyPCBdMAUmLGUApnYYTtIm5JXsQegUAY7GPHqgfDzjw==",
"dev": true
},
"canonical-path": {
@ -4873,9 +5122,9 @@
"dev": true
},
"electron-to-chromium": {
"version": "1.3.421",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.421.tgz",
"integrity": "sha512-ogxgmvHGfDuLA+GtgfK0jkFWlBb4MCZK2U1MM+l98sf4U3Ixtrfw1iC9w4mQqNvo+lHgM4pR62TqoT4QrvKJCw==",
"version": "1.3.428",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.428.tgz",
"integrity": "sha512-u3+5jEfgLKq/hGO96YfAoOAM1tgFnRDTCD5mLuev44tttcXix+INtVegAkmGzUcfDsnzkPt51XXurXZLLwXt0w==",
"dev": true
},
"elliptic": {
@ -6084,13 +6333,33 @@
}
},
"hash-base": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
"integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
"integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
"dev": true,
"requires": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
"inherits": "^2.0.4",
"readable-stream": "^3.6.0",
"safe-buffer": "^5.2.0"
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"safe-buffer": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==",
"dev": true
}
}
},
"hash.js": {
@ -6900,10 +7169,13 @@
"dev": true
},
"is-wsl": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.1.1.tgz",
"integrity": "sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==",
"dev": true
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dev": true,
"requires": {
"is-docker": "^2.0.0"
}
},
"isarray": {
"version": "1.0.0",
@ -7172,9 +7444,9 @@
}
},
"jquery": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.0.tgz",
"integrity": "sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ=="
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
},
"jquery.terminal": {
"version": "2.15.4",
@ -9777,9 +10049,9 @@
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
},
"portfinder": {
"version": "1.0.25",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz",
"integrity": "sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==",
"version": "1.0.26",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz",
"integrity": "sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ==",
"dev": true,
"requires": {
"async": "^2.6.2",
@ -10942,9 +11214,9 @@
}
},
"rc-select": {
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/rc-select/-/rc-select-10.2.4.tgz",
"integrity": "sha512-PtfAHQRXof5gX1qEYQc8GIgroajtzU3mcMGbqM1aE1jqaZ4r+p0GJy3rabXoNANsKH7jUV5+zYJb5VhTxj2vmQ==",
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/rc-select/-/rc-select-10.2.5.tgz",
"integrity": "sha512-E5cdq+8kuTMdvDUmVXGdlHb4D0Effj/Xu988kUWSoALJtEAAdnUbuCJGhu04ehWtKMotGN5I9qlkLR4OdiLrgg==",
"requires": {
"classnames": "2.x",
"rc-animate": "^2.10.0",
@ -14526,9 +14798,9 @@
}
},
"mime": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
"integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==",
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.5.tgz",
"integrity": "sha512-3hQhEUF027BuxZjQA3s7rIv/7VCQPa27hN9u9g87sEkWaKwQPuXOkVKtOeiyUrnWqTDiOs8Ed2rwg733mB0R5w==",
"dev": true
},
"mkdirp": {

38
ui-ngx/package.json

@ -13,24 +13,24 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^9.1.2",
"@angular/cdk": "^9.1.3",
"@angular/common": "^9.1.2",
"@angular/compiler": "^9.1.2",
"@angular/core": "^9.1.2",
"@angular/animations": "^9.1.4",
"@angular/cdk": "^9.2.2",
"@angular/common": "^9.1.4",
"@angular/compiler": "^9.1.4",
"@angular/core": "^9.1.4",
"@angular/flex-layout": "^9.0.0-beta.29",
"@angular/forms": "^9.1.2",
"@angular/material": "^9.2.1",
"@angular/platform-browser": "^9.1.2",
"@angular/platform-browser-dynamic": "^9.1.2",
"@angular/router": "^9.1.2",
"@angular/forms": "^9.1.4",
"@angular/material": "^9.2.2",
"@angular/platform-browser": "^9.1.4",
"@angular/platform-browser-dynamic": "^9.1.4",
"@angular/router": "^9.1.4",
"@auth0/angular-jwt": "^4.0.0",
"@date-io/date-fns": "^2.6.1",
"@flowjs/flow.js": "^2.14.0",
"@flowjs/ngx-flow": "^0.4.3",
"@juggle/resize-observer": "^3.1.3",
"@mat-datetimepicker/core": "^4.1.0",
"@material-ui/core": "^4.9.11",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/pickers": "^3.2.10",
"@ngrx/effects": "^9.1.0",
@ -50,7 +50,7 @@
"flot": "git://github.com/thingsboard/flot.git#0.9-work",
"flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master",
"font-awesome": "^4.7.0",
"jquery": "^3.5.0",
"jquery": "^3.5.1",
"jquery.terminal": "^2.15.4",
"js-beautify": "^1.11.0",
"json-schema-defaults": "^0.4.0",
@ -75,7 +75,7 @@
"objectpath": "^2.0.0",
"prop-types": "^15.7.2",
"raphael": "^2.3.0",
"rc-select": "^10.2.4",
"rc-select": "^10.2.5",
"react": "^16.13.1",
"react-ace": "^8.1.0",
"react-dom": "^16.13.1",
@ -95,21 +95,21 @@
},
"devDependencies": {
"@angular-builders/custom-webpack": "^9.1.0",
"@angular-devkit/build-angular": "^0.901.1",
"@angular/cli": "^9.1.1",
"@angular/compiler-cli": "^9.1.2",
"@angular/language-service": "^9.1.2",
"@angular-devkit/build-angular": "^0.901.4",
"@angular/cli": "^9.1.4",
"@angular/compiler-cli": "^9.1.4",
"@angular/language-service": "^9.1.4",
"@types/canvas-gauges": "^2.1.2",
"@types/flot": "^0.0.31",
"@types/jasmine": "^3.5.10",
"@types/jasminewd2": "^2.0.8",
"@types/jquery": "^3.3.35",
"@types/jquery": "^3.3.38",
"@types/js-beautify": "^1.8.2",
"@types/jstree": "^3.3.39",
"@types/jszip": "^3.1.7",
"@types/leaflet": "^1.5.12",
"@types/leaflet-polylinedecorator": "^1.6.0",
"@types/leaflet-markercluster": "^1.0.3",
"@types/leaflet-polylinedecorator": "^1.6.0",
"@types/lodash": "^4.14.150",
"@types/raphael": "^2.3.0",
"@types/react": "^16.9.34",

4
ui-ngx/src/app/core/api/datasource-subcription.ts

@ -28,7 +28,7 @@ import {
} from '@shared/models/telemetry/telemetry.models';
import { DatasourceListener } from './datasource.service';
import { AggregationType, SubscriptionTimewindow, YEAR } from '@shared/models/time/time.models';
import { deepClone, isDefinedAndNotNull, isObject } from '@core/utils';
import { deepClone, isDefinedAndNotNull, isObject, objectHashCode } from '@core/utils';
import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models';
import { DataAggregator } from '@core/api/data-aggregator';
@ -120,7 +120,7 @@ export class DatasourceSubscription {
data: []
};
} else {
key = String(this.utils.objectHashCode(dataKey));
key = String(objectHashCode(dataKey));
this.datasourceData[key] = {
data: []
};

4
ui-ngx/src/app/core/api/datasource.service.ts

@ -25,7 +25,7 @@ import {
DatasourceSubscriptionOptions,
SubscriptionDataKey
} from '@core/api/datasource-subcription';
import { deepClone } from '@core/utils';
import { deepClone, objectHashCode } from '@core/utils';
export interface DatasourceListener {
subscriptionType: widgetType;
@ -79,7 +79,7 @@ export class DatasourceService {
datasourceSubscriptionOptions.entityType = listener.entityType;
datasourceSubscriptionOptions.entityId = listener.entityId;
}
listener.datasourceSubscriptionKey = this.utils.objectHashCode(datasourceSubscriptionOptions);
listener.datasourceSubscriptionKey = objectHashCode(datasourceSubscriptionOptions);
let subscription: DatasourceSubscription;
if (this.subscriptions[listener.datasourceSubscriptionKey]) {
subscription = this.subscriptions[listener.datasourceSubscriptionKey];

58
ui-ngx/src/app/core/services/utils.service.ts

@ -20,7 +20,15 @@
import { Inject, Injectable, NgZone } from '@angular/core';
import { WINDOW } from '@core/services/window.service';
import { ExceptionData } from '@app/shared/models/error.models';
import { deepClone, deleteNullProperties, guid, isDefined, isDefinedAndNotNull, isUndefined, createLabelFromDatasource } from '@core/utils';
import {
createLabelFromDatasource,
deepClone,
deleteNullProperties,
guid,
isDefined,
isDefinedAndNotNull,
isUndefined
} from '@core/utils';
import { WindowMessage } from '@shared/models/window-message.model';
import { TranslateService } from '@ngx-translate/core';
import { customTranslationsPrefix } from '@app/shared/models/constants';
@ -34,8 +42,6 @@ import jsonSchemaDefaults from 'json-schema-defaults';
import materialIconsCodepoints from '!raw-loader!material-design-icons/iconfont/codepoints';
import { Observable, of, ReplaySubject } from 'rxjs';
const varsRegex = /\$\{([^}]*)\}/g;
const predefinedFunctions: { [func: string]: string } = {
Sin: 'return Math.round(1000*Math.sin(time/5000));',
Cos: 'return Math.round(1000*Math.cos(time/5000));',
@ -163,32 +169,6 @@ export class UtilsService {
return obj;
}
public hashCode(str: string): number {
let hash = 0;
let i: number;
let char: number;
if (str.length === 0) {
return hash;
}
for (i = 0; i < str.length; i++) {
char = str.charCodeAt(i);
// tslint:disable-next-line:no-bitwise
hash = ((hash << 5) - hash) + char;
// tslint:disable-next-line:no-bitwise
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
public objectHashCode(obj: any): number {
let hash = 0;
if (obj) {
const str = JSON.stringify(obj);
hash = this.hashCode(str);
}
return hash;
}
public processWidgetException(exception: any): ExceptionData {
const data = this.parseException(exception, -6);
if (this.widgetEditMode) {
@ -241,7 +221,7 @@ export class UtilsService {
}
public customTranslation(translationValue: string, defaultValue: string): string {
let result = '';
let result: string;
const translationId = customTranslationsPrefix + translationValue;
const translation = this.translate.instant(translationId);
if (translation !== translationId) {
@ -252,20 +232,6 @@ export class UtilsService {
return result;
}
public insertVariable(pattern: string, name: string, value: any): string {
let result = deepClone(pattern);
let match = varsRegex.exec(pattern);
while (match !== null) {
const variable = match[0];
const variableName = match[1];
if (variableName === name) {
result = result.split(variable).join(value);
}
match = varsRegex.exec(pattern);
}
return result;
}
public guid(): string {
return guid();
}
@ -379,10 +345,6 @@ export class UtilsService {
return additionalDataKey;
}
public createLabelFromDatasource(datasource: Datasource, pattern: string) {
return createLabelFromDatasource(datasource, pattern);
}
public generateColors(datasources: Array<Datasource>) {
let index = 0;
datasources.forEach((datasource) => {

223
ui-ngx/src/app/core/utils.ts

@ -15,11 +15,12 @@
///
import _ from 'lodash';
import { Observable, Observer, of, Subject } from 'rxjs';
import { finalize, map, share } from 'rxjs/operators';
import { Observable, Subject } from 'rxjs';
import { finalize, share } from 'rxjs/operators';
import base64js from 'base64-js';
import { Datasource } from '@app/shared/models/widget.models';
import { FormattedData } from '@app/modules/home/components/widget/lib/maps/map-models';
const varsRegex = /\${([^}]*)}/g;
export function onParentScrollOrWindowResize(el: Node): Observable<Event> {
const scrollSubject = new Subject<Event>();
@ -33,7 +34,7 @@ export function onParentScrollOrWindowResize(el: Node): Observable<Event> {
scrollParentNode.addEventListener('scroll', eventListenerObject);
});
window.addEventListener('resize', eventListenerObject);
const shared = scrollSubject.pipe(
return scrollSubject.pipe(
finalize(() => {
scrollParentNodes.forEach((scrollParentNode) => {
scrollParentNode.removeEventListener('scroll', eventListenerObject);
@ -42,18 +43,13 @@ export function onParentScrollOrWindowResize(el: Node): Observable<Event> {
}),
share()
);
return shared;
}
export function isLocalUrl(url: string): boolean {
const parser = document.createElement('a');
parser.href = url;
const host = parser.hostname;
if (host === 'localhost' || host === '127.0.0.1') {
return true;
} else {
return false;
}
return host === 'localhost' || host === '127.0.0.1';
}
export function animatedScroll(element: HTMLElement, scrollTop: number, delay?: number) {
@ -68,8 +64,7 @@ export function animatedScroll(element: HTMLElement, scrollTop: number, delay?:
element.scrollTop = to;
} else {
currentTime += increment;
const val = easeInOut(currentTime, start, remaining, duration);
element.scrollTop = val;
element.scrollTop = easeInOut(currentTime, start, remaining, duration);
if (currentTime < duration) {
setTimeout(animateScroll, increment);
}
@ -110,6 +105,15 @@ export function isString(value: any): boolean {
return typeof value === 'string';
}
export function isEmpty(obj: any): boolean {
for (const key of Object.keys(obj)) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
return false;
}
}
return true;
}
export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined {
if (isDefined(value) &&
value !== null && isNumeric(value)) {
@ -118,7 +122,7 @@ export function formatValue(value: any, dec?: number, units?: string, showZeroDe
formatted = formatted.toFixed(dec);
}
if (!showZeroDecimals) {
formatted = (Number(formatted) * 1);
formatted = (Number(formatted));
}
formatted = formatted.toString();
if (isDefined(units) && units.length > 0) {
@ -154,15 +158,13 @@ export function deleteNullProperties(obj: any) {
export function objToBase64(obj: any): string {
const json = JSON.stringify(obj);
const encoded = utf8Encode(json);
const b64Encoded: string = base64js.fromByteArray(encoded);
return b64Encoded;
return base64js.fromByteArray(encoded);
}
export function base64toObj(b64Encoded: string): any {
const encoded: Uint8Array | number[] = base64js.toByteArray(b64Encoded);
const json = utf8Decode(encoded);
const obj = JSON.parse(json);
return obj;
return JSON.parse(json);
}
function utf8Encode(str: string): Uint8Array | number[] {
@ -223,11 +225,13 @@ function scrollParents(node: Node): Node[] {
return scrollParentNodes;
}
function hashCode(str) {
export function hashCode(str: string): number {
let hash = 0;
let i;
let char;
if (str.length === 0) return hash;
let i: number;
let char: number;
if (str.length === 0) {
return hash;
}
for (i = 0; i < str.length; i++) {
char = str.charCodeAt(i);
// tslint:disable-next-line:no-bitwise
@ -238,6 +242,15 @@ function hashCode(str) {
return hash;
}
export function objectHashCode(obj: any): number {
let hash = 0;
if (obj) {
const str = JSON.stringify(obj);
hash = hashCode(str);
}
return hash;
}
function easeInOut(
currentTime: number,
startTime: number,
@ -433,29 +446,21 @@ export function getDescendantProp(obj: any, path: string): any {
return path.split('.').reduce((acc, part) => acc && acc[part], obj);
}
export function imageLoader(imageUrl: string): Observable<HTMLImageElement> {
return new Observable((observer: Observer<HTMLImageElement>) => {
const image = new Image();
image.style.position = 'absolute';
image.style.left = '-99999px';
image.style.top = '-99999px';
image.onload = () => {
observer.next(image);
document.body.removeChild(image);
observer.complete();
};
image.onerror = err => {
observer.error(err);
document.body.removeChild(image);
observer.complete();
};
document.body.appendChild(image)
image.src = imageUrl;
});
export function insertVariable(pattern: string, name: string, value: any): string {
let result = pattern;
let match = varsRegex.exec(pattern);
while (match !== null) {
const variable = match[0];
const variableName = match[1];
if (variableName === name) {
result = result.split(variable).join(value);
}
match = varsRegex.exec(pattern);
}
return result;
}
export function createLabelFromDatasource(datasource: Datasource, pattern: string) {
const varsRegex = /\$\{([^}]*)\}/g;
let label = pattern;
if (!datasource) {
return label;
@ -482,142 +487,6 @@ export function createLabelFromDatasource(datasource: Datasource, pattern: strin
return label;
}
const imageAspectMap = {};
export function aspectCache(imageUrl: string): Observable<number> {
if (imageUrl?.length) {
const hash = hashCode(imageUrl);
let aspect = imageAspectMap[hash];
if (aspect) {
return of(aspect);
}
else return imageLoader(imageUrl).pipe(map(image => {
aspect = image.width / image.height;
imageAspectMap[hash] = aspect;
return aspect;
}))
}
}
export function parseArray(input: any[]): any[] {
return _(input).groupBy(el => el?.datasource?.entityName)
.values().value().map((entityArray, dsIndex) =>
entityArray[0].data.map((el, i) => {
const obj = {
entityName: entityArray[0]?.datasource?.entityName,
$datasource: entityArray[0]?.datasource,
dsIndex,
time: el[0],
deviceType: null
};
entityArray.filter(el => el.data.length).forEach(entity => {
obj[entity?.dataKey?.label] = entity?.data[i][1];
obj[entity?.dataKey?.label + '|ts'] = entity?.data[0][0];
if (entity?.dataKey?.label === 'type') {
obj.deviceType = entity?.data[0][1];
}
});
return obj;
})
);
}
export function parseData(input: any[]): FormattedData[] {
return _(input).groupBy(el => el?.datasource?.entityName)
.values().value().map((entityArray, i) => {
const obj = {
entityName: entityArray[0]?.datasource?.entityName,
$datasource: entityArray[0]?.datasource as Datasource,
dsIndex: i,
deviceType: null
};
entityArray.filter(el => el.data.length).forEach(el => {
obj[el?.dataKey?.label] = el?.data[0][1];
obj[el?.dataKey?.label + '|ts'] = el?.data[0][0];
if (el?.dataKey?.label === 'type') {
obj.deviceType = el?.data[0][1];
}
});
return obj;
});
}
export function safeExecute(func: Function, params = []) {
let res = null;
if (func && typeof (func) === 'function') {
try {
res = func(...params);
}
catch (err) {
console.log('error in external function:', err);
res = null;
}
}
return res;
}
export function parseFunction(source: any, params: string[] = ['def']): Function {
let res = null;
if (source?.length) {
try {
res = new Function(...params, source);
}
catch (err) {
res = null;
}
}
return res;
}
export function parseTemplate(template: string, data: { $datasource?: Datasource, [key: string]: any },
translateFn?: (key: string) => string) {
let res = '';
try {
if (template.match(/<link-act/g)) {
template = template.replace(/<link-act/g, '<a').replace(/link-act>/g, 'a>')
.replace(/name=(\'|")(.*?)(\'|")/g, `class='tb-custom-action' id='$2'`);
}
if (translateFn) {
template = translateFn(template);
}
template = createLabelFromDatasource(data.$datasource, template);
const formatted = template.match(/\$\{([^}]*)\:\d*\}/g);
if (formatted)
formatted.forEach(value => {
const [variable, digits] = value.replace('${', '').replace('}', '').split(':');
data[variable] = padValue(data[variable], +digits);
if (data[variable] === 'NaN') data[variable] = '';
template = template.replace(value, '${' + variable + '}');
});
const variables = template.match(/\$\{.*?\}/g);
if (variables) {
variables.forEach(variable => {
variable = variable.replace('${', '').replace('}', '');
if (!data[variable])
data[variable] = '';
})
}
const compiled = _.template(template);
res = compiled(data);
}
catch (ex) {
console.log(ex, template)
}
return res;
}
export let parseWithTranslation = {
translate(): string {
throw console.error('Translate not assigned');
},
parseTemplate(template: string, data: object, forceTranslate = false): string {
return parseTemplate(forceTranslate ? this.translate(template) : template, data, this?.translate);
},
setTranslate(translateFn: (key: string, defaultTranslation?: string) => string) {
this.translate = translateFn;
}
}
export function padValue(val: any, dec: number): string {
let strVal;
let n;

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

@ -33,7 +33,7 @@ import { Datasource, WidgetActionDescriptor, WidgetConfig } from '@shared/models
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { deepClone, isDefined, isNumber, createLabelFromDatasource } from '@core/utils';
import { deepClone, isDefined, isNumber, createLabelFromDatasource, hashCode } from '@core/utils';
import cssjs from '@core/css/css';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
@ -304,7 +304,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
const cssString = constructTableCssString(this.widgetConfig);
const cssParser = new cssjs();
cssParser.testMode = false;
const namespace = 'alarms-table-' + this.utils.hashCode(cssString);
const namespace = 'alarms-table-' + hashCode(cssString);
cssParser.cssPreviewNamespace = namespace;
cssParser.createStyleElement(namespace, cssString);
$(this.elementRef.nativeElement).addClass(namespace);

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

@ -37,7 +37,7 @@ import {
import { BaseData } from '@shared/models/base-data';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
import { deepClone } from '@core/utils';
import { deepClone, hashCode } from '@core/utils';
import {
defaultNodeIconFunction,
defaultNodeOpenedFunction,
@ -186,7 +186,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O
const cssString = constructTableCssString(this.widgetConfig);
const cssParser = new cssjs();
cssParser.testMode = false;
const namespace = 'entities-hierarchy-' + this.utils.hashCode(cssString);
const namespace = 'entities-hierarchy-' + hashCode(cssString);
cssParser.cssPreviewNamespace = namespace;
cssParser.createStyleElement(namespace, cssString);
$(this.elementRef.nativeElement).addClass(namespace);

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

@ -39,7 +39,7 @@ import {
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { deepClone, isDefined, isNumber, createLabelFromDatasource } from '@core/utils';
import { deepClone, isDefined, isNumber, createLabelFromDatasource, hashCode } from '@core/utils';
import cssjs from '@core/css/css';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
@ -226,7 +226,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
const cssString = constructTableCssString(this.widgetConfig);
const cssParser = new cssjs();
cssParser.testMode = false;
const namespace = 'entities-table-' + this.utils.hashCode(cssString);
const namespace = 'entities-table-' + hashCode(cssString);
cssParser.cssPreviewNamespace = namespace;
cssParser.createStyleElement(namespace, cssString);
$(this.elementRef.nativeElement).addClass(namespace);

23
ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts

@ -14,19 +14,26 @@
/// limitations under the License.
///
import L, { LatLngTuple, LatLngBounds, Point, MarkerClusterGroupOptions, markerClusterGroup } from 'leaflet';
import L, { LatLngBounds, LatLngTuple, markerClusterGroup, MarkerClusterGroupOptions } from 'leaflet';
import 'leaflet-providers';
import 'leaflet.markercluster/dist/leaflet.markercluster';
import { MapSettings, MarkerSettings, FormattedData, UnitedMapSettings, PolygonSettings, PolylineSettings } from './map-models';
import {
FormattedData,
MapSettings,
MarkerSettings,
PolygonSettings,
PolylineSettings,
UnitedMapSettings
} from './map-models';
import { Marker } from './markers';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter } from 'rxjs/operators';
import { Polyline } from './polyline';
import { Polygon } from './polygon';
import { DatasourceData } from '@app/shared/models/widget.models';
import { safeExecute } from '@app/core/utils';
import { safeExecute } from '@home/components/widget/lib/maps/maps-utils';
export default abstract class LeafletMap {
@ -38,13 +45,11 @@ export default abstract class LeafletMap {
map$: BehaviorSubject<L.Map> = new BehaviorSubject(null);
ready$: Observable<L.Map> = this.map$.pipe(filter(map => !!map));
options: UnitedMapSettings;
isMarketCluster: boolean;
bounds: L.LatLngBounds;
newMarker: L.Marker;
datasources: FormattedData[];
markersCluster;
constructor(public $container: HTMLElement, options: UnitedMapSettings) {
protected constructor(public $container: HTMLElement, options: UnitedMapSettings) {
this.options = options;
}
@ -125,7 +130,7 @@ export default abstract class LeafletMap {
addMarker.setPosition('topright')
}
L.Control.AddMarker = L.Control.extend({
onAdd(map) {
onAdd() {
const img = L.DomUtil.create('img') as any;
img.src = `assets/add_location.svg`;
img.style.width = '32px';
@ -138,7 +143,7 @@ export default abstract class LeafletMap {
draggableImg.on('dragend', dragListener)
return img;
},
onRemove(map) {
onRemove() {
},
dragMarker: this.dragMarker
} as any);
@ -166,7 +171,7 @@ export default abstract class LeafletMap {
this.datasources = dataSources;
}
public saveMarkerLocation(e) {
public saveMarkerLocation(_e) {
}

6
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { LatLngTuple, LeafletMouseEvent } from 'leaflet';
import { LatLngTuple } from 'leaflet';
import { Datasource } from '@app/shared/models/widget.models';
export type GenericFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string;
@ -116,7 +116,7 @@ export type PolygonSettings = {
autocloseTooltip: boolean;
tooltipFunction: GenericFunction;
showTooltipAction: string;
tooltipAction: object;
tooltipAction: { [name: string]: actionsHandler };
tooltipPattern: string;
useTooltipFunction: boolean;
polygonClick: { [name: string]: actionsHandler };
@ -152,6 +152,6 @@ export interface HistorySelectSettings {
buttonColor: string;
}
export type actionsHandler = ($event: Event | LeafletMouseEvent, datasource: Datasource) => void;
export type actionsHandler = ($event: Event, datasource: Datasource) => void;
export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings;

12
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts

@ -31,11 +31,10 @@ import {
} from './schemes';
import { MapWidgetStaticInterface, MapWidgetInterface } from './map-widget.interface';
import { OpenStreetMap, TencentMap, GoogleMap, HEREMap, ImageMap } from './providers';
import { parseFunction, parseArray, parseData, parseWithTranslation } from '@core/utils';
import { initSchema, addToSchema, mergeSchemes, addCondition, addGroupInfo } from '@core/schema-utils';
import { of, Subject } from 'rxjs';
import { WidgetContext } from '@app/modules/home/models/widget-component.models';
import { getDefCenterPosition } from './maps-utils';
import { getDefCenterPosition, parseArray, parseData, parseFunction, parseWithTranslation } from './maps-utils';
import { JsonSettingsSchema, WidgetActionDescriptor, DatasourceType, widgetType, Datasource } from '@shared/models/widget.models';
import { EntityId } from '@shared/models/id/entity-id';
import { AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
@ -138,7 +137,7 @@ export class MapWidgetController implements MapWidgetInterface {
else return '';
}
getDescriptors(name: string): { [name: string]: ($event: Event) => void } {
getDescriptors(name: string): { [name: string]: ($event: Event, datasource: Datasource) => void } {
const descriptors = this.ctx.actionsApi.getActionDescriptors(name);
const actions = {};
descriptors.forEach(descriptor => {
@ -150,9 +149,10 @@ export class MapWidgetController implements MapWidgetInterface {
onInit() {
}
private onCustomAction(descriptor: WidgetActionDescriptor, $event: any, entityInfo: Datasource) {
if ($event && $event.stopPropagation) {
$event?.stopPropagation();
private onCustomAction(descriptor: WidgetActionDescriptor, $event: Event, entityInfo: Datasource) {
if ($event) {
$event.preventDefault();
$event.stopPropagation();
}
const { entityId, entityName, entityLabel, entityType } = entityInfo;
this.ctx.actionsApi.handleWidgetAction($event, descriptor, {

175
ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts

@ -15,8 +15,12 @@
///
import L from 'leaflet';
import { MarkerSettings, PolygonSettings, PolylineSettings } from './map-models';
import { FormattedData, MarkerSettings, PolygonSettings, PolylineSettings } from './map-models';
import { Datasource } from '@app/shared/models/widget.models';
import _ from 'lodash';
import { Observable, Observer, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { createLabelFromDatasource, hashCode, padValue } from '@core/utils';
export function createTooltip(target: L.Layer,
settings: MarkerSettings | PolylineSettings | PolygonSettings,
@ -49,7 +53,7 @@ export function createTooltip(target: L.Layer,
export function getRatio(firsMoment: number, secondMoment: number, intermediateMoment: number): number {
return (intermediateMoment - firsMoment) / (secondMoment - firsMoment);
};
}
export function findAngle(startPoint, endPoint) {
let angle = -Math.atan2(endPoint.latitude - startPoint.latitude, endPoint.longitude - startPoint.longitude);
@ -65,3 +69,170 @@ export function getDefCenterPosition(position) {
return position;
return [0, 0];
}
const imageAspectMap = {};
function imageLoader(imageUrl: string): Observable<HTMLImageElement> {
return new Observable((observer: Observer<HTMLImageElement>) => {
const image = new Image();
image.style.position = 'absolute';
image.style.left = '-99999px';
image.style.top = '-99999px';
image.onload = () => {
observer.next(image);
document.body.removeChild(image);
observer.complete();
};
image.onerror = err => {
observer.error(err);
document.body.removeChild(image);
observer.complete();
};
document.body.appendChild(image)
image.src = imageUrl;
});
}
export function aspectCache(imageUrl: string): Observable<number> {
if (imageUrl?.length) {
const hash = hashCode(imageUrl);
let aspect = imageAspectMap[hash];
if (aspect) {
return of(aspect);
}
else return imageLoader(imageUrl).pipe(map(image => {
aspect = image.width / image.height;
imageAspectMap[hash] = aspect;
return aspect;
}))
}
}
function parseTemplate(template: string, data: { $datasource?: Datasource, [key: string]: any },
translateFn?: (key: string) => string) {
let res = '';
try {
if (template.match(/<link-act/g)) {
template = template.replace(/<link-act/g, '<a href="#"').replace(/link-act>/g, 'a>')
.replace(/name=(['"])(.*?)(['"])/g, `class='tb-custom-action' id='$2'`);
}
if (translateFn) {
template = translateFn(template);
}
template = createLabelFromDatasource(data.$datasource, template);
const formatted = template.match(/\${([^}]*):\d*}/g);
if (formatted)
formatted.forEach(value => {
const [variable, digits] = value.replace('${', '').replace('}', '').split(':');
data[variable] = padValue(data[variable], +digits);
if (data[variable] === 'NaN') data[variable] = '';
template = template.replace(value, '${' + variable + '}');
});
const variables = template.match(/\${.*?}/g);
if (variables) {
variables.forEach(variable => {
variable = variable.replace('${', '').replace('}', '');
if (!data[variable])
data[variable] = '';
})
}
const compiled = _.template(template);
res = compiled(data);
}
catch (ex) {
console.log(ex, template)
}
return res;
}
type TranslateFunc = (key: string, defaultTranslation?: string) => string;
export const parseWithTranslation = {
translateFn: null,
translate(key: string, defaultTranslation?: string): string {
if (this.translateFn) {
return this.translateFn(key, defaultTranslation);
} else {
throw console.error('Translate not assigned');
}
},
parseTemplate(template: string, data: object, forceTranslate = false): string {
return parseTemplate(forceTranslate ? this.translate(template) : template, data, this.translate.bind(this));
},
setTranslate(translateFn: TranslateFunc) {
this.translateFn = translateFn;
}
}
export function parseData(input: any[]): FormattedData[] {
return _(input).groupBy(el => el?.datasource?.entityName)
.values().value().map((entityArray, i) => {
const obj = {
entityName: entityArray[0]?.datasource?.entityName,
$datasource: entityArray[0]?.datasource as Datasource,
dsIndex: i,
deviceType: null
};
entityArray.filter(el => el.data.length).forEach(el => {
obj[el?.dataKey?.label] = el?.data[0][1];
obj[el?.dataKey?.label + '|ts'] = el?.data[0][0];
if (el?.dataKey?.label === 'type') {
obj.deviceType = el?.data[0][1];
}
});
return obj;
});
}
export function parseArray(input: any[]): any[] {
return _(input).groupBy(el => el?.datasource?.entityName)
.values().value().map((entityArray, dsIndex) =>
entityArray[0].data.map((el, i) => {
const obj = {
entityName: entityArray[0]?.datasource?.entityName,
$datasource: entityArray[0]?.datasource,
dsIndex,
time: el[0],
deviceType: null
};
entityArray.filter(e => e.data.length).forEach(entity => {
obj[entity?.dataKey?.label] = entity?.data[i][1];
obj[entity?.dataKey?.label + '|ts'] = entity?.data[0][0];
if (entity?.dataKey?.label === 'type') {
obj.deviceType = entity?.data[0][1];
}
});
return obj;
})
);
}
export function parseFunction(source: any, params: string[] = ['def']): (...args: any[]) => any {
let res = null;
if (source?.length) {
try {
res = new Function(...params, source);
}
catch (err) {
res = null;
}
}
return res;
}
export function safeExecute(func: (...args: any[]) => any, params = []) {
let res = null;
if (func && typeof (func) === 'function') {
try {
res = func(...params);
}
catch (err) {
console.log('error in external function:', err);
res = null;
}
}
return res;
}

7
ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts

@ -14,10 +14,9 @@
/// limitations under the License.
///
import { aspectCache, parseWithTranslation, safeExecute } from '@app/core/utils';
import L, { LeafletMouseEvent } from 'leaflet';
import { FormattedData, MarkerSettings } from './map-models';
import { createTooltip } from './maps-utils';
import { aspectCache, createTooltip, parseWithTranslation, safeExecute } from './maps-utils';
import tinycolor from 'tinycolor2';
export class Marker {
@ -50,7 +49,7 @@ export class Marker {
this.leafletMarker.on('click', (event: LeafletMouseEvent) => {
for (const action in this.settings.markerClick) {
if (typeof (this.settings.markerClick[action]) === 'function') {
this.settings.markerClick[action](event, this.data.$datasource);
this.settings.markerClick[action](event.originalEvent, this.data.$datasource);
}
}
});
@ -184,4 +183,4 @@ export class Marker {
setMarkerPosition(latLng) {
this.leafletMarker.setLatLng(latLng);
}
}
}

7
ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts

@ -15,10 +15,9 @@
///
import L, { LatLngExpression, LatLngTuple, LeafletMouseEvent } from 'leaflet';
import { createTooltip } from './maps-utils';
import { PolygonSettings, FormattedData } from './map-models';
import { createTooltip, parseWithTranslation, safeExecute } from './maps-utils';
import { PolygonSettings } from './map-models';
import { DatasourceData } from '@app/shared/models/widget.models';
import { safeExecute, parseWithTranslation } from '@app/core/utils';
export class Polygon {
@ -46,7 +45,7 @@ export class Polygon {
this.leafletPoly.on('click', (event: LeafletMouseEvent) => {
for (const action in this.settings.polygonClick) {
if (typeof (this.settings.polygonClick[action]) === 'function') {
this.settings.polygonClick[action](event, polyData.datasource);
this.settings.polygonClick[action](event.originalEvent, polyData.datasource);
}
}
});

4
ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts

@ -17,8 +17,8 @@
import L, { PolylineDecoratorOptions } from 'leaflet';
import 'leaflet-polylinedecorator';
import { safeExecute } from '@app/core/utils';
import { PolylineSettings } from './map-models';
import { safeExecute } from '@home/components/widget/lib/maps/maps-utils';
export class Polyline {
@ -95,4 +95,4 @@ export class Polyline {
setPolylineLatLngs(latLngs) {
this.leafletPoly.setLatLngs(latLngs);
}
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts

@ -29,7 +29,7 @@ export class GoogleMap extends LeafletMap {
super($container, options);
this.loadGoogle(() => {
const map = L.map($container).setView(options?.defaultCenterPosition, options?.defaultZoomLevel);
const roads = (L.gridLayer as any).googleMutant({
(L.gridLayer as any).googleMutant({
type: options?.gmDefaultMapType || 'roadmap'
}).addTo(map);
super.setMap(map);

2
ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts

@ -17,9 +17,9 @@
import L, { LatLngLiteral, LatLngBounds, LatLngTuple } from 'leaflet';
import LeafletMap from '../leaflet-map';
import { UnitedMapSettings } from '../map-models';
import { aspectCache, parseFunction } from '@app/core/utils';
import { Observable } from 'rxjs';
import { map, filter, switchMap } from 'rxjs/operators';
import { aspectCache, parseFunction } from '@home/components/widget/lib/maps/maps-utils';
const maxZoom = 4;// ?

28
ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts

@ -558,32 +558,6 @@ export const markerClusteringSettingsSchema =
]
};
export const markerClusteringSettingsSchemaGoogle =
{
schema: {
title: 'Marker Clustering Configuration Google',
type: 'object',
properties: {
gridSize: {
title: 'Maximum radius that a cluster will cover in pixels',
type: 'number',
default: 60
},
minimumClusterSize: {
title: 'The minimum number of markers in a cluster',
type: 'number'
}
},
required: []
},
form: [
'gridSize',
'minimumClusterSize'
]
};
export const markerClusteringSettingsSchemaLeaflet =
{
schema: {
@ -1271,4 +1245,4 @@ export const tripAnimationSchema = {
}
]
}]
}
}

4
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts

@ -40,7 +40,7 @@ import {
} from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { isDefined, isNumber } from '@core/utils';
import { hashCode, isDefined, isNumber } from '@core/utils';
import cssjs from '@core/css/css';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
@ -230,7 +230,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
const cssParser = new cssjs();
cssParser.testMode = false;
const namespace = 'ts-table-' + this.utils.hashCode(cssString);
const namespace = 'ts-table-' + hashCode(cssString);
cssParser.cssPreviewNamespace = namespace;
cssParser.createStyleElement(namespace, cssString);
$(this.elementRef.nativeElement).addClass(namespace);

3
ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts

@ -22,12 +22,11 @@ import { interpolateOnPointSegment } from 'leaflet-geometryutil';
import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, SecurityContext, ViewChild } from '@angular/core';
import { MapWidgetController, TbMapWidgetV2 } from '../lib/maps/map-widget2';
import { MapProviders } from '../lib/maps/map-models';
import { parseArray, parseWithTranslation, safeExecute, parseTemplate } from '@app/core/utils';
import { initSchema, addToSchema, addGroupInfo } from '@app/core/schema-utils';
import { tripAnimationSchema } from '../lib/maps/schemes';
import { DomSanitizer } from '@angular/platform-browser';
import { WidgetContext } from '@app/modules/home/models/widget-component.models';
import { findAngle, getRatio } from '../lib/maps/maps-utils';
import { findAngle, getRatio, parseArray, parseWithTranslation, safeExecute } from '../lib/maps/maps-utils';
import { JsonSettingsSchema, WidgetConfig } from '@shared/models/widget.models';
import moment from 'moment';

2
ui-ngx/src/app/modules/home/pages/dashboard/manage-dashboard-customers-dialog.component.html

@ -34,7 +34,7 @@
<span>{{ labelText | translate }}</span>
<tb-entity-list
formControlName="assignedCustomerIds"
required
[required]="data.actionType !== 'manage'"
[entityType]="entityType.CUSTOMER">
</tb-entity-list>
</fieldset>

27
ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.ts

@ -23,7 +23,7 @@ import { StateControllerComponent } from './state-controller.component';
import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service';
import { EntityId } from '@app/shared/models/id/entity-id';
import { UtilsService } from '@core/services/utils.service';
import { base64toObj, objToBase64 } from '@app/core/utils';
import { base64toObj, insertVariable, isEmpty, objToBase64 } from '@app/core/utils';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { EntityService } from '@core/http/entity.service';
import { EntityType } from '@shared/models/entity-type.models';
@ -123,11 +123,10 @@ export class EntityStateControllerComponent extends StateControllerComponent imp
if (this.states && this.states[id]) {
this.resolveEntity(params).subscribe(
() => {
const newState: StateObject = {
this.stateObject[this.stateObject.length - 1] = {
id,
params
};
this.stateObject[this.stateObject.length - 1] = newState;
this.gotoState(this.stateObject[this.stateObject.length - 1].id, true, openRightLayout);
}
);
@ -203,14 +202,14 @@ export class EntityStateControllerComponent extends StateControllerComponent imp
const targetParams = params && params.targetEntityParamName ? params[params.targetEntityParamName] : params;
const entityName = targetParams && targetParams.entityName ? targetParams.entityName : '';
const entityLabel = targetParams && targetParams.entityLabel ? targetParams.entityLabel : '';
result = this.utils.insertVariable(stateName, 'entityName', entityName);
result = this.utils.insertVariable(result, 'entityLabel', entityLabel);
result = insertVariable(stateName, 'entityName', entityName);
result = insertVariable(result, 'entityLabel', entityLabel);
for (const prop of Object.keys(params)) {
if (params[prop] && params[prop].entityName) {
result = this.utils.insertVariable(result, prop + ':entityName', params[prop].entityName);
result = insertVariable(result, prop + ':entityName', params[prop].entityName);
}
if (params[prop] && params[prop].entityLabel) {
result = this.utils.insertVariable(result, prop + ':entityLabel', params[prop].entityLabel);
result = insertVariable(result, prop + ':entityLabel', params[prop].entityLabel);
}
}
}
@ -276,20 +275,13 @@ export class EntityStateControllerComponent extends StateControllerComponent imp
if (this.stateObject.length === 1) {
const state = this.stateObject[0];
const rootStateId = this.dashboardUtils.getRootStateId(this.states);
if (state.id === rootStateId && (!state.params || this.isEmpty(state.params))) {
if (state.id === rootStateId && (!state.params || isEmpty(state.params))) {
return true;
}
}
return false;
}
private isEmpty(obj: any): boolean {
for (const key of Object.keys(obj)) {
return !Object.prototype.hasOwnProperty.call(obj, key);
}
return true;
}
private resolveEntity(params: StateParams): Observable<void> {
if (params && params.targetEntityParamName) {
params = params[params.targetEntityParamName];
@ -313,10 +305,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp
}
private isEntityResolved(params: StateParams): boolean {
if (!params.entityName || !params.entityName.length) {
return false;
}
return true;
return !(!params.entityName || !params.entityName.length);
}
private getStateObjById(id: string): StateObject {

1
ui-ngx/src/app/shared/pipe/public-api.ts

@ -20,4 +20,3 @@ export * from './keyboard-shortcut.pipe';
export * from './milliseconds-to-time-string.pipe';
export * from './nospace.pipe';
export * from './truncate.pipe';
export * from './template.pipe';

25
ui-ngx/src/app/shared/pipe/template.pipe.ts

@ -1,25 +0,0 @@
///
/// Copyright © 2016-2020 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 { Pipe, PipeTransform } from '@angular/core';
import { parseTemplate, parseWithTranslation } from '@app/core/utils';
@Pipe({ name: 'tbParseTemplate' })
export class TbTemplatePipe implements PipeTransform {
transform(template, data): string {
return parseWithTranslation.parseTemplate(template, data);
}
}

3
ui-ngx/src/app/shared/shared.module.ts

@ -131,7 +131,6 @@ import { LedLightComponent } from '@shared/components/led-light.component';
import { TbJsonToStringDirective } from '@shared/components/directives/tb-json-to-string.directive';
import { JsonObjectEditDialogComponent } from '@shared/components/dialog/json-object-edit-dialog.component';
import { HistorySelectorComponent } from './components/time/history-selector/history-selector.component';
import { TbTemplatePipe } from '@shared/pipe/template.pipe';
import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component';
import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component';
@ -215,7 +214,6 @@ import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list
HighlightPipe,
TruncatePipe,
TbJsonPipe,
TbTemplatePipe,
KeyboardShortcutPipe,
TbJsonToStringDirective,
JsonObjectEditDialogComponent,
@ -377,7 +375,6 @@ import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list
HighlightPipe,
TruncatePipe,
TbJsonPipe,
TbTemplatePipe,
KeyboardShortcutPipe,
TranslateModule,
JsonObjectEditDialogComponent,

6
ui/src/app/locale/locale.constant-de_DE.json

@ -1149,7 +1149,7 @@
"total": "Gesamt"
},
"login": {
"login": "Login",
"login": "Anmelden",
"request-password-reset": "Passwortzurücksetzung anfordern",
"reset-password": "Passwort zurücksetzen",
"create-password": "Passwort erstellen",
@ -1163,7 +1163,9 @@
"new-password": "Neues Passwort",
"new-password-again": "Neues Passwort wiederholen",
"password-link-sent-message": "Der Link zum Zurücksetzen des Passworts wurde erfolgreich versendet!",
"email": "E-Mail"
"email": "E-Mail",
"login-with": "Mit {{name}} anmelden",
"or": "oder"
},
"position": {
"top": "Oben",

4
ui/src/app/locale/locale.constant-el_GR.json

@ -1705,7 +1705,9 @@
"password-link-sent-message": "Ο σύνδεσμος επαναφοράς κωδικού πρόσβασης στάλθηκε με επιτυχία!",
"email": "Email",
"no-account": "Δεν έχετε λογαριασμό;",
"create-account": "Δημιουργία λογαριασμού"
"create-account": "Δημιουργία λογαριασμού",
"login-with": "Σύνδεση μέσω {{name}}",
"or": "ή"
},
"signup": {
"firstname": "Όνομα",

4
ui/src/app/locale/locale.constant-es_ES.json

@ -1229,7 +1229,9 @@
"new-password": "Nueva contraseña",
"new-password-again": "Repita la nueva contraseña",
"password-link-sent-message": "¡El enlace para el restablecer la contraseña fue enviado correctamente!",
"email": "Correo electrónico"
"email": "Correo electrónico",
"login-with": "Iniciar sesión con {{name}}",
"or": "o"
},
"position": {
"top": "Superior",

6
ui/src/app/locale/locale.constant-fr_FR.json

@ -1197,7 +1197,7 @@
"create-password": "Créer un mot de passe",
"email": "Email",
"forgot-password": "Mot de passe oublié?",
"login": "Login",
"login": "Connexion",
"new-password": "Nouveau mot de passe",
"new-password-again": "nouveau mot de passe",
"password-again": "Mot de passe à nouveau",
@ -1208,7 +1208,9 @@
"request-password-reset": "Demander la réinitialisation du mot de passe",
"reset-password": "Réinitialiser le mot de passe",
"sign-in": "Veuillez vous connecter",
"username": "Nom d'utilisateur (courriel)"
"username": "Nom d'utilisateur (courriel)",
"login-with": "Se connecter avec {{name}}",
"or": "ou"
},
"position": {
"bottom": "Bas",

6
ui/src/app/locale/locale.constant-it_IT.json

@ -1160,7 +1160,7 @@
"total": "totale"
},
"login": {
"login": "Login",
"login": "Accedi",
"request-password-reset": "Richiesta reset password",
"reset-password": "Reset Password",
"create-password": "Crea Password",
@ -1174,7 +1174,9 @@
"new-password": "Nuova password",
"new-password-again": "Ripeti nuova password",
"password-link-sent-message": "Link reset password inviato con successo!",
"email": "Email"
"email": "Email",
"login-with": "Accedi con {{name}}",
"or": "o"
},
"position": {
"top": "Alto",

4
ui/src/app/locale/locale.constant-ja_JA.json

@ -1024,7 +1024,9 @@
"new-password": "新しいパスワード",
"new-password-again": "新しいパスワードを再入力",
"password-link-sent-message": "パスワードリセットリンクが正常に送信されました!",
"email": "Eメール"
"email": "Eメール",
"login-with": "{{name}}でログイン",
"or": "または"
},
"position": {
"top": "上",

4
ui/src/app/locale/locale.constant-ko_KR.json

@ -934,7 +934,9 @@
"new-password": "새 비밀번호",
"new-password-again": "새 비밀번호 확인",
"password-link-sent-message": "비밀번호 재설정 링크가 성공적으로 전송되었습니다!",
"email": "이메일"
"email": "이메일",
"login-with": "{{name}}으로 로그인",
"or": "또는"
},
"position": {
"top": "상단",

6
ui/src/app/locale/locale.constant-ro_RO.json

@ -1230,7 +1230,7 @@
}
},
"login": {
"login": "Intră în Cont",
"login": "Conectare",
"request-password-reset": "Solicită Resetarea Parolei",
"reset-password": "Resetează Parolă",
"create-password": "Creează Parolă",
@ -1245,7 +1245,9 @@
"new-password": "Parolă nouă",
"new-password-again": "Verificare parolă nouă",
"password-link-sent-message": "Ți-am trimis pe eMail un link pentru resetarea parolei",
"email": "eMail"
"email": "eMail",
"login-with": "Conectare cu {{name}}",
"or": "sau"
},
"position": {
"top": "Sus",

6
ui/src/app/locale/locale.constant-tr_TR.json

@ -1090,7 +1090,7 @@
"total": "toplam"
},
"login": {
"login": "Oturum aç",
"login": "Giriş Yap",
"request-password-reset": "Parola Sıfırlama İsteği Gönder",
"reset-password": "Parola Sıfırla",
"create-password": "Parola Oluştur",
@ -1104,7 +1104,9 @@
"new-password": "Yeni parola",
"new-password-again": "Yeni parola tekrarı",
"password-link-sent-message": "Parola sıfırlama e-postası başarıyla gönderildi!",
"email": "E-posta"
"email": "E-posta",
"login-with": "{{name}} ile Giriş Yap",
"or": "ya da"
},
"position": {
"top": "Üst",

Loading…
Cancel
Save