Browse Source

Merged with develop

pull/789/head
Andrew Shvayka 8 years ago
parent
commit
e834dd3689
  1. 3
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 18
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  3. 8
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  4. 7
      application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
  5. 2
      application/src/main/java/org/thingsboard/server/actors/shared/EntityActorsManager.java
  6. 2
      application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java
  7. 6
      application/src/main/java/org/thingsboard/server/actors/shared/rulechain/SystemRuleChainManager.java
  8. 8
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  9. 13
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  10. 94
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  11. 102
      application/src/main/java/org/thingsboard/server/service/queue/DefaultMsgQueueService.java
  12. 32
      application/src/main/java/org/thingsboard/server/service/queue/MsgQueueService.java
  13. 14
      application/src/main/resources/thingsboard.yml
  14. 3
      application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
  15. 6
      application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
  16. 2
      application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
  17. 8
      dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
  18. 16
      dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
  19. 10
      dao/src/main/java/org/thingsboard/server/dao/queue/MsgQueue.java
  20. 4
      dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java
  21. 21
      dao/src/main/java/org/thingsboard/server/dao/queue/db/nosql/CassandraMsgQueue.java
  22. 2
      dao/src/main/java/org/thingsboard/server/dao/queue/db/nosql/MsgAck.java
  23. 4
      dao/src/main/java/org/thingsboard/server/dao/queue/db/nosql/QueuePartitioner.java
  24. 2
      dao/src/main/java/org/thingsboard/server/dao/queue/db/nosql/UnprocessedMsgFilter.java
  25. 6
      dao/src/main/java/org/thingsboard/server/dao/queue/db/nosql/repository/CassandraAckRepository.java
  26. 4
      dao/src/main/java/org/thingsboard/server/dao/queue/db/nosql/repository/CassandraMsgRepository.java
  27. 4
      dao/src/main/java/org/thingsboard/server/dao/queue/db/nosql/repository/CassandraProcessedPartitionRepository.java
  28. 4
      dao/src/main/java/org/thingsboard/server/dao/queue/db/repository/AckRepository.java
  29. 2
      dao/src/main/java/org/thingsboard/server/dao/queue/db/repository/MsgRepository.java
  30. 2
      dao/src/main/java/org/thingsboard/server/dao/queue/db/repository/ProcessedPartitionRepository.java
  31. 2
      dao/src/main/java/org/thingsboard/server/dao/queue/db/sql/SqlMsgQueue.java
  32. 2
      dao/src/main/java/org/thingsboard/server/dao/queue/memory/InMemoryMsgKey.java
  33. 118
      dao/src/main/java/org/thingsboard/server/dao/queue/memory/InMemoryMsgQueue.java
  34. 113
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
  35. 79
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  36. 11
      dao/src/main/java/org/thingsboard/server/dao/rule/CassandraRuleChainDao.java
  37. 9
      dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java
  38. 6
      dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java
  39. 114
      dao/src/main/java/org/thingsboard/server/dao/sql/queue/InMemoryMsgQueue.java
  40. 11
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java
  41. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java
  42. 5
      dao/src/test/java/org/thingsboard/server/dao/queue/db/nosql/QueuePartitionerTest.java
  43. 4
      dao/src/test/java/org/thingsboard/server/dao/queue/db/nosql/UnprocessedMsgFilterTest.java
  44. 5
      dao/src/test/java/org/thingsboard/server/dao/queue/db/nosql/repository/CassandraAckRepositoryTest.java
  45. 3
      dao/src/test/java/org/thingsboard/server/dao/queue/db/nosql/repository/CassandraMsgRepositoryTest.java
  46. 3
      dao/src/test/java/org/thingsboard/server/dao/queue/db/nosql/repository/CassandraProcessedPartitionRepositoryTest.java
  47. 93
      docker/k8s/redis.yaml
  48. 12
      docker/k8s/tb.yaml
  49. 6
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  50. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java
  51. 8
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
  52. 5
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
  53. 27
      ui/src/app/api/entity.service.js
  54. 218
      ui/src/app/api/plugin.service.js
  55. 52
      ui/src/app/api/rule-chain.service.js
  56. 186
      ui/src/app/api/rule.service.js
  57. 4
      ui/src/app/app.js
  58. 42
      ui/src/app/common/types.constant.js
  59. 105
      ui/src/app/component/component-dialog.controller.js
  60. 60
      ui/src/app/component/component-dialog.service.js
  61. 79
      ui/src/app/component/component-dialog.tpl.html
  62. 75
      ui/src/app/component/component.directive.js
  63. 58
      ui/src/app/component/component.tpl.html
  64. 28
      ui/src/app/component/index.js
  65. 115
      ui/src/app/components/plugin-select.directive.js
  66. 37
      ui/src/app/components/plugin-select.scss
  67. 44
      ui/src/app/components/plugin-select.tpl.html
  68. 12
      ui/src/app/entity/entity-autocomplete.directive.js
  69. 2
      ui/src/app/extension/extension-dialog.controller.js
  70. 1
      ui/src/app/extension/extension-dialog.tpl.html
  71. 140
      ui/src/app/extension/extensions-forms/extension-form-modbus.directive.js
  72. 774
      ui/src/app/extension/extensions-forms/extension-form-modbus.tpl.html
  73. 2
      ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
  74. 3
      ui/src/app/extension/index.js
  75. 122
      ui/src/app/import-export/import-export.service.js
  76. 4
      ui/src/app/layout/index.js
  77. 34
      ui/src/app/locale/locale.constant.js
  78. 48
      ui/src/app/plugin/add-plugin.tpl.html
  79. 36
      ui/src/app/plugin/index.js
  80. 19
      ui/src/app/plugin/plugin-card.tpl.html
  81. 90
      ui/src/app/plugin/plugin-fieldset.tpl.html
  82. 218
      ui/src/app/plugin/plugin.controller.js
  83. 94
      ui/src/app/plugin/plugin.directive.js
  84. 46
      ui/src/app/plugin/plugin.routes.js
  85. 18
      ui/src/app/plugin/plugin.scss
  86. 77
      ui/src/app/plugin/plugins.tpl.html
  87. 48
      ui/src/app/rule/add-rule.tpl.html
  88. 40
      ui/src/app/rule/index.js
  89. 19
      ui/src/app/rule/rule-card.tpl.html
  90. 219
      ui/src/app/rule/rule-fieldset.tpl.html
  91. 210
      ui/src/app/rule/rule.controller.js
  92. 191
      ui/src/app/rule/rule.directive.js
  93. 46
      ui/src/app/rule/rule.routes.js
  94. 55
      ui/src/app/rule/rule.scss
  95. 77
      ui/src/app/rule/rules.tpl.html
  96. 2
      ui/src/app/rulechain/rulechain-card.tpl.html
  97. 5
      ui/src/app/rulechain/rulechain-fieldset.tpl.html
  98. 6
      ui/src/app/rulechain/rulechain.controller.js
  99. 1
      ui/src/app/rulechain/rulechain.directive.js
  100. 2
      ui/src/app/rulechain/rulechain.routes.js

3
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -64,6 +64,7 @@ import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.executors.ExternalCallExecutorService;
import org.thingsboard.server.service.mail.MailExecutorService;
import org.thingsboard.server.service.queue.MsgQueueService;
import org.thingsboard.server.service.rpc.DeviceRpcService;
import org.thingsboard.server.service.script.JsExecutorService;
import org.thingsboard.server.service.state.DeviceStateService;
@ -201,7 +202,7 @@ public class ActorSystemContext {
@Autowired
@Getter
private MsgQueue msgQueue;
private MsgQueueService msgQueueService;
@Autowired
@Getter

18
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -84,21 +84,6 @@ class DefaultTbContext implements TbContext {
mainCtx.getScheduler().scheduleOnce(Duration.create(delayInMs, TimeUnit.MILLISECONDS), target, msg, mainCtx.getActorSystem().dispatcher(), nodeCtx.getSelfActor());
}
@Override
public void tellOthers(TbMsg msg) {
throw new RuntimeException("Not Implemented!");
}
@Override
public void tellSibling(TbMsg msg, ServerAddress address) {
throw new RuntimeException("Not Implemented!");
}
@Override
public void ack(TbMsg msg) {
}
@Override
public void tellError(TbMsg msg, Throwable th) {
if (nodeCtx.getSelf().isDebugMode()) {
@ -119,7 +104,7 @@ class DefaultTbContext implements TbContext {
@Override
public TbMsg transformMsg(TbMsg origMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) {
return new TbMsg(origMsg.getId(), type, originator, metaData.copy(), data, nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId(), mainCtx.getQueuePartitionId());
return new TbMsg(origMsg.getId(), type, originator, metaData.copy(), data, origMsg.getRuleChainId(), origMsg.getRuleNodeId(), mainCtx.getQueuePartitionId());
}
@Override
@ -134,6 +119,7 @@ class DefaultTbContext implements TbContext {
@Override
public void tellNext(TbMsg msg, Set<String> relationTypes) {
//TODO: fix this to send set of relations instead of loop.
relationTypes.forEach(type -> tellNext(msg, type));
}

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

@ -95,12 +95,12 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
private void reprocess(List<RuleNode> ruleNodeList) {
for (RuleNode ruleNode : ruleNodeList) {
for (TbMsg tbMsg : queue.findUnprocessed(ruleNode.getId().getId(), systemContext.getQueuePartitionId())) {
for (TbMsg tbMsg : queue.findUnprocessed(tenantId, ruleNode.getId().getId(), systemContext.getQueuePartitionId())) {
pushMsgToNode(nodeActors.get(ruleNode.getId()), tbMsg, "");
}
}
if (firstNode != null) {
for (TbMsg tbMsg : queue.findUnprocessed(entityId.getId(), systemContext.getQueuePartitionId())) {
for (TbMsg tbMsg : queue.findUnprocessed(tenantId, entityId.getId(), systemContext.getQueuePartitionId())) {
pushMsgToNode(firstNode, tbMsg, "");
}
}
@ -215,7 +215,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
int relationsCount = relations.size();
EntityId ackId = msg.getRuleNodeId() != null ? msg.getRuleNodeId() : msg.getRuleChainId();
if (relationsCount == 0) {
queue.ack(msg, ackId.getId(), msg.getClusterPartition());
queue.ack(tenantId, msg, ackId.getId(), msg.getClusterPartition());
} else if (relationsCount == 1) {
for (RuleNodeRelation relation : relations) {
pushToTarget(msg, relation.getOut(), relation.getType());
@ -233,7 +233,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
}
}
//TODO: Ideally this should happen in async way when all targets confirm that the copied messages are successfully written to corresponding target queues.
queue.ack(msg, ackId.getId(), msg.getClusterPartition());
queue.ack(tenantId, msg, ackId.getId(), msg.getClusterPartition());
}
}

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

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.dao.queue.MsgQueue;
import org.thingsboard.server.service.queue.MsgQueueService;
import javax.annotation.Nullable;
import java.util.function.Consumer;
@ -35,14 +36,14 @@ public abstract class ComponentMsgProcessor<T extends EntityId> extends Abstract
protected final TenantId tenantId;
protected final T entityId;
protected final MsgQueue queue;
protected final MsgQueueService queue;
protected ComponentLifecycleState state;
protected ComponentMsgProcessor(ActorSystemContext systemContext, LoggingAdapter logger, TenantId tenantId, T id) {
super(systemContext, logger);
this.tenantId = tenantId;
this.entityId = id;
this.queue = systemContext.getMsgQueue();
this.queue = systemContext.getMsgQueueService();
}
public abstract void start(ActorContext context) throws Exception;
@ -88,7 +89,7 @@ public abstract class ComponentMsgProcessor<T extends EntityId> extends Abstract
protected void putToQueue(final TbMsg tbMsg, final Consumer<TbMsg> onSuccess) {
EntityId entityId = tbMsg.getRuleNodeId() != null ? tbMsg.getRuleNodeId() : tbMsg.getRuleChainId();
Futures.addCallback(queue.put(tbMsg, entityId.getId(), tbMsg.getClusterPartition()), new FutureCallback<Void>() {
Futures.addCallback(queue.put(this.tenantId, tbMsg, entityId.getId(), tbMsg.getClusterPartition()), new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
onSuccess.accept(tbMsg);

2
application/src/main/java/org/thingsboard/server/actors/shared/EntityActorsManager.java

@ -67,7 +67,7 @@ public abstract class EntityActorsManager<T extends EntityId, A extends UntypedA
}
}
protected void visit(M entity, ActorRef actorRef) {}
public void visit(M entity, ActorRef actorRef) {}
public ActorRef getOrCreateActor(ActorContext context, T entityId) {
return actors.computeIfAbsent(entityId, eId ->

2
application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java

@ -49,7 +49,7 @@ public abstract class RuleChainManager extends EntityActorsManager<RuleChainId,
}
@Override
protected void visit(RuleChain entity, ActorRef actorRef) {
public void visit(RuleChain entity, ActorRef actorRef) {
if (entity.isRoot()) {
rootChain = entity;
rootChainActor = actorRef;

6
application/src/main/java/org/thingsboard/server/actors/shared/rulechain/SystemRuleChainManager.java

@ -20,10 +20,14 @@ import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.actors.shared.plugin.PluginManager;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.plugin.PluginMetaData;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.dao.plugin.BasePluginService;
import java.util.Collections;
public class SystemRuleChainManager extends RuleChainManager {
public SystemRuleChainManager(ActorSystemContext systemContext) {
@ -32,7 +36,7 @@ public class SystemRuleChainManager extends RuleChainManager {
@Override
protected FetchFunction<RuleChain> getFetchEntitiesFunction() {
return service::findSystemRuleChains;
return link -> new TextPageData<>(Collections.emptyList(), link);
}
@Override

8
application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java

@ -30,8 +30,11 @@ import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.actors.shared.plugin.TenantPluginManager;
import org.thingsboard.server.actors.shared.rulechain.TenantRuleChainManager;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
import org.thingsboard.server.common.msg.device.DeviceToDeviceActorMsg;
@ -130,6 +133,11 @@ public class TenantActor extends RuleChainManagerActor {
private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
ActorRef target = getEntityActorRef(msg.getEntityId());
if (target != null) {
if (msg.getEntityId().getEntityType() == EntityType.RULE_CHAIN) {
RuleChain ruleChain = systemContext.getRuleChainService().
findRuleChainById(new RuleChainId(msg.getEntityId().getId()));
ruleChainManager.visit(ruleChain, target);
}
target.tell(msg, ActorRef.noSender());
} else {
logger.debug("Invalid component lifecycle msg: {}", msg);

13
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -558,15 +558,10 @@ public abstract class BaseController {
SecurityUser authUser = getCurrentUser();
TenantId tenantId = ruleChain.getTenantId();
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
if (authUser.getAuthority() != Authority.SYS_ADMIN) {
if (authUser.getTenantId() == null ||
!tenantId.getId().equals(ModelConstants.NULL_UUID) && !authUser.getTenantId().equals(tenantId)) {
throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION,
ThingsboardErrorCode.PERMISSION_DENIED);
} else if (tenantId.getId().equals(ModelConstants.NULL_UUID)) {
ruleChain.setConfiguration(null);
}
if (authUser.getAuthority() != Authority.TENANT_ADMIN ||
!authUser.getTenantId().equals(tenantId)) {
throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION,
ThingsboardErrorCode.PERMISSION_DENIED);
}
return ruleChain;
}

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

@ -64,7 +64,7 @@ public class RuleChainController extends BaseController {
@Autowired
private EventService eventService;
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET)
@ResponseBody
public RuleChain getRuleChainById(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
@ -77,7 +77,7 @@ public class RuleChainController extends BaseController {
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/metadata", method = RequestMethod.GET)
@ResponseBody
public RuleChainMetaData getRuleChainMetaData(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
@ -92,7 +92,7 @@ public class RuleChainController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain", method = RequestMethod.POST)
@ResponseBody
public RuleChain saveRuleChain(@RequestBody RuleChain ruleChain) throws ThingsboardException {
@ -118,7 +118,46 @@ public class RuleChainController extends BaseController {
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/root", method = RequestMethod.POST)
@ResponseBody
public RuleChain setRootRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
checkParameter(RULE_CHAIN_ID, strRuleChainId);
try {
RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
RuleChain ruleChain = checkRuleChain(ruleChainId);
TenantId tenantId = getCurrentUser().getTenantId();
RuleChain previousRootRuleChain = ruleChainService.getRootTenantRuleChain(tenantId);
if (ruleChainService.setRootRuleChain(ruleChainId)) {
previousRootRuleChain = ruleChainService.findRuleChainById(previousRootRuleChain.getId());
actorService.onEntityStateChange(previousRootRuleChain.getTenantId(), previousRootRuleChain.getId(),
ComponentLifecycleEvent.UPDATED);
logEntityAction(previousRootRuleChain.getId(), previousRootRuleChain,
null, ActionType.UPDATED, null);
ruleChain = ruleChainService.findRuleChainById(ruleChainId);
actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(),
ComponentLifecycleEvent.UPDATED);
logEntityAction(ruleChain.getId(), ruleChain,
null, ActionType.UPDATED, null);
}
return ruleChain;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.RULE_CHAIN),
null,
null,
ActionType.UPDATED, e, strRuleChainId);
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/metadata", method = RequestMethod.POST)
@ResponseBody
public RuleChainMetaData saveRuleChainMetaData(@RequestBody RuleChainMetaData ruleChainMetaData) throws ThingsboardException {
@ -142,7 +181,7 @@ public class RuleChainController extends BaseController {
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChains", params = {"limit"}, method = RequestMethod.GET)
@ResponseBody
public TextPageData<RuleChain> getRuleChains(
@ -150,48 +189,6 @@ public class RuleChainController extends BaseController {
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String idOffset,
@RequestParam(required = false) String textOffset) throws ThingsboardException {
try {
TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN) {
return checkNotNull(ruleChainService.findSystemRuleChains(pageLink));
} else {
TenantId tenantId = getCurrentUser().getTenantId();
TextPageData<RuleChain> ruleChainsData = checkNotNull(ruleChainService.findAllTenantRuleChainsByTenantIdAndPageLink(tenantId, pageLink));
List<RuleChain> ruleChains = ruleChainsData.getData();
ruleChains.stream()
.filter(ruleChain -> ruleChain.getTenantId().getId().equals(ModelConstants.NULL_UUID))
.forEach(ruleChain -> ruleChain.setConfiguration(null));
return ruleChainsData;
}
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/system/ruleChains", params = {"limit"}, method = RequestMethod.GET)
@ResponseBody
public TextPageData<RuleChain> getSystemRuleChains(
@RequestParam int limit,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String idOffset,
@RequestParam(required = false) String textOffset) throws ThingsboardException {
try {
TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
return checkNotNull(ruleChainService.findSystemRuleChains(pageLink));
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/ruleChains", params = {"limit"}, method = RequestMethod.GET)
@ResponseBody
public TextPageData<RuleChain> getTenantRuleChains(
@RequestParam int limit,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String idOffset,
@RequestParam(required = false) String textOffset) throws ThingsboardException {
try {
TenantId tenantId = getCurrentUser().getTenantId();
TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
@ -201,7 +198,7 @@ public class RuleChainController extends BaseController {
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void deleteRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
@ -209,6 +206,7 @@ public class RuleChainController extends BaseController {
try {
RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
RuleChain ruleChain = checkRuleChain(ruleChainId);
ruleChainService.deleteRuleChainById(ruleChainId);
actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.DELETED);

102
application/src/main/java/org/thingsboard/server/service/queue/DefaultMsgQueueService.java

@ -0,0 +1,102 @@
/**
* Copyright © 2016-2018 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.service.queue;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.dao.queue.MsgQueue;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@Service
@Slf4j
public class DefaultMsgQueueService implements MsgQueueService {
@Value("${rule.queue.max_size}")
private long queueMaxSize;
@Value("${rule.queue.cleanup_period}")
private long queueCleanUpPeriod;
@Autowired
private MsgQueue msgQueue;
private ScheduledExecutorService cleanupExecutor;
private Map<TenantId, AtomicLong> pendingCountPerTenant = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
if (queueCleanUpPeriod > 0) {
cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
cleanupExecutor.scheduleAtFixedRate(() -> cleanup(),
queueCleanUpPeriod, queueCleanUpPeriod, TimeUnit.SECONDS);
}
}
@PreDestroy
public void stop() {
if (cleanupExecutor != null) {
cleanupExecutor.shutdownNow();
}
}
@Override
public ListenableFuture<Void> put(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition) {
AtomicLong pendingMsgCount = pendingCountPerTenant.computeIfAbsent(tenantId, key -> new AtomicLong());
if (pendingMsgCount.incrementAndGet() < queueMaxSize) {
return msgQueue.put(tenantId, msg, nodeId, clusterPartition);
} else {
pendingMsgCount.decrementAndGet();
return Futures.immediateFailedFuture(new RuntimeException("Message queue is full!"));
}
}
@Override
public ListenableFuture<Void> ack(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition) {
ListenableFuture<Void> result = msgQueue.ack(tenantId, msg, nodeId, clusterPartition);
AtomicLong pendingMsgCount = pendingCountPerTenant.computeIfAbsent(tenantId, key -> new AtomicLong());
pendingMsgCount.decrementAndGet();
return result;
}
@Override
public Iterable<TbMsg> findUnprocessed(TenantId tenantId, UUID nodeId, long clusterPartition) {
return msgQueue.findUnprocessed(tenantId, nodeId, clusterPartition);
}
private void cleanup() {
pendingCountPerTenant.forEach((tenantId, pendingMsgCount) -> {
pendingMsgCount.set(0);
msgQueue.cleanUp(tenantId);
});
}
}

32
application/src/main/java/org/thingsboard/server/service/queue/MsgQueueService.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2018 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.service.queue;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.UUID;
public interface MsgQueueService {
ListenableFuture<Void> put(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition);
ListenableFuture<Void> ack(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition);
Iterable<TbMsg> findUnprocessed(TenantId tenantId, UUID nodeId, long clusterPartition);
}

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

@ -89,6 +89,7 @@ mqtt:
leak_detector_level: "${NETTY_LEASK_DETECTOR_LVL:DISABLED}"
boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"
worker_group_thread_count: "${NETTY_WORKER_GROUP_THREADS:12}"
max_payload_size: "${NETTY_MAX_PAYLOAD_SIZE:65536}"
# MQTT SSL configuration
ssl:
# Enable/disable SSL support
@ -306,18 +307,21 @@ spring:
rule:
queue:
type: "memory"
max_size: 10000
#Message queue type (memory or db)
type: "${RULE_QUEUE_TYPE:memory}"
#Message queue maximum size (per tenant)
max_size: "${RULE_QUEUE_MAX_SIZE:100}"
#Message queue cleanup period in seconds
cleanup_period: "${RULE_QUEUE_CLEANUP_PERIOD:3600}"
# PostgreSQL DAO Configuration
#spring:
# data:
# jpa:
# sql:
# repositories:
# enabled: "true"
# jpa:
# sql:
# hibernate:
# ddl-auto: "validate"
# database-platform: "${SPRING_JPA_DATABASE_PLATFORM:org.hibernate.dialect.PostgreSQLDialect}"

3
application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.dao.queue.MsgQueue;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.service.queue.MsgQueueService;
import java.io.IOException;
@ -41,7 +42,7 @@ public class AbstractRuleEngineControllerTest extends AbstractControllerTest {
protected RuleChainService ruleChainService;
@Autowired
protected MsgQueue msgQueue;
protected MsgQueueService msgQueueService;
protected RuleChain saveRuleChain(RuleChain ruleChain) throws Exception {
return doPost("/api/ruleChain", ruleChain, RuleChain.class);

6
application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java

@ -189,7 +189,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
Assert.assertEquals("serverAttributeValue1", getMetadata(outEvent).get("ss_serverAttributeKey1").asText());
Assert.assertEquals("serverAttributeValue2", getMetadata(outEvent).get("ss_serverAttributeKey2").asText());
List<TbMsg> unAckMsgList = Lists.newArrayList(msgQueue.findUnprocessed(ruleChain.getId().getId(), 0L));
List<TbMsg> unAckMsgList = Lists.newArrayList(msgQueueService.findUnprocessed(savedTenant.getId(), ruleChain.getId().getId(), 0L));
Assert.assertEquals(0, unAckMsgList.size());
}
@ -306,10 +306,10 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
Assert.assertEquals("serverAttributeValue1", getMetadata(outEvent).get("ss_serverAttributeKey1").asText());
Assert.assertEquals("serverAttributeValue2", getMetadata(outEvent).get("ss_serverAttributeKey2").asText());
List<TbMsg> unAckMsgList = Lists.newArrayList(msgQueue.findUnprocessed(rootRuleChain.getId().getId(), 0L));
List<TbMsg> unAckMsgList = Lists.newArrayList(msgQueueService.findUnprocessed(savedTenant.getId(), rootRuleChain.getId().getId(), 0L));
Assert.assertEquals(0, unAckMsgList.size());
unAckMsgList = Lists.newArrayList(msgQueue.findUnprocessed(secondaryRuleChain.getId().getId(), 0L));
unAckMsgList = Lists.newArrayList(msgQueueService.findUnprocessed(savedTenant.getId(), secondaryRuleChain.getId().getId(), 0L));
Assert.assertEquals(0, unAckMsgList.size());
}

2
application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java

@ -186,7 +186,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac
new TbMsgMetaData(),
"{}",
ruleChain.getId(), null, 0L);
msgQueue.put(tbMsg, ruleChain.getId().getId(), 0L);
msgQueueService.put(device.getTenantId(), tbMsg, ruleChain.getId().getId(), 0L);
Thread.sleep(1000);

8
dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java

@ -67,6 +67,10 @@ public abstract class AbstractCassandraCluster {
private long initTimeout;
@Value("${cassandra.init_retry_interval_ms}")
private long initRetryInterval;
@Value("${cassandra.max_requests_per_connection_local:128}")
private int max_requests_local;
@Value("${cassandra.max_requests_per_connection_remote:128}")
private int max_requests_remote;
@Autowired
private CassandraSocketOptions socketOpts;
@ -97,8 +101,8 @@ public abstract class AbstractCassandraCluster {
.withClusterName(clusterName)
.withSocketOptions(socketOpts.getOpts())
.withPoolingOptions(new PoolingOptions()
.setMaxRequestsPerConnection(HostDistance.LOCAL, 32768)
.setMaxRequestsPerConnection(HostDistance.REMOTE, 32768));
.setMaxRequestsPerConnection(HostDistance.LOCAL, max_requests_local)
.setMaxRequestsPerConnection(HostDistance.REMOTE, max_requests_remote));
this.clusterBuilder.withQueryOptions(queryOpts.getOpts());
this.clusterBuilder.withCompression(StringUtils.isEmpty(compression) ? Compression.NONE : Compression.valueOf(compression.toUpperCase()));
if (ssl) {

16
dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java

@ -88,15 +88,27 @@ public abstract class CassandraAbstractDao {
}
private ResultSet execute(Statement statement, ConsistencyLevel level) {
log.debug("Execute cassandra statement {}", statement);
if (log.isDebugEnabled()) {
log.debug("Execute cassandra statement {}", statementToString(statement));
}
return executeAsync(statement, level).getUninterruptibly();
}
private ResultSetFuture executeAsync(Statement statement, ConsistencyLevel level) {
log.debug("Execute cassandra async statement {}", statement);
if (log.isDebugEnabled()) {
log.debug("Execute cassandra async statement {}", statementToString(statement));
}
if (statement.getConsistencyLevel() == null) {
statement.setConsistencyLevel(level);
}
return new RateLimitedResultSetFuture(getSession(), rateLimiter, statement);
}
private static String statementToString(Statement statement) {
if (statement instanceof BoundStatement) {
return ((BoundStatement)statement).preparedStatement().getQueryString();
} else {
return statement.toString();
}
}
}

10
dao/src/main/java/org/thingsboard/server/dao/queue/MsgQueue.java

@ -16,15 +16,19 @@
package org.thingsboard.server.dao.queue;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.UUID;
public interface MsgQueue {
ListenableFuture<Void> put(TbMsg msg, UUID nodeId, long clusterPartition);
ListenableFuture<Void> put(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition);
ListenableFuture<Void> ack(TbMsg msg, UUID nodeId, long clusterPartition);
ListenableFuture<Void> ack(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition);
Iterable<TbMsg> findUnprocessed(TenantId tenantId, UUID nodeId, long clusterPartition);
ListenableFuture<Void> cleanUp(TenantId tenantId);
Iterable<TbMsg> findUnprocessed(UUID nodeId, long clusterPartition);
}

4
dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java

@ -28,8 +28,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.Bean;
import org.thingsboard.server.common.data.id.EntityId;
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.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -80,7 +82,7 @@ public class QueueBenchmark implements CommandLineRunner {
try {
TbMsg msg = randomMsg();
UUID nodeId = UUIDs.timeBased();
ListenableFuture<Void> put = msgQueue.put(msg, nodeId, 100L);
ListenableFuture<Void> put = msgQueue.put(new TenantId(EntityId.NULL_UUID), msg, nodeId, 100L);
// ListenableFuture<Void> put = msgQueue.ack(msg, nodeId, 100L);
Futures.addCallback(put, new FutureCallback<Void>() {
@Override

21
dao/src/main/java/org/thingsboard/server/dao/service/queue/cassandra/CassandraMsgQueue.java → dao/src/main/java/org/thingsboard/server/dao/queue/db/nosql/CassandraMsgQueue.java

@ -13,24 +13,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.queue.cassandra;
package org.thingsboard.server.dao.queue.db.nosql;
import com.datastax.driver.core.utils.UUIDs;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.dao.queue.MsgQueue;
import org.thingsboard.server.dao.service.queue.cassandra.repository.AckRepository;
import org.thingsboard.server.dao.service.queue.cassandra.repository.MsgRepository;
import org.thingsboard.server.dao.queue.db.repository.AckRepository;
import org.thingsboard.server.dao.queue.db.repository.MsgRepository;
import org.thingsboard.server.dao.util.NoSqlDao;
import java.util.List;
import java.util.UUID;
@Component
@ConditionalOnProperty(prefix = "rule.queue", value = "type", havingValue = "db")
@Slf4j
@NoSqlDao
public class CassandraMsgQueue implements MsgQueue {
@ -45,21 +49,21 @@ public class CassandraMsgQueue implements MsgQueue {
private QueuePartitioner queuePartitioner;
@Override
public ListenableFuture<Void> put(TbMsg msg, UUID nodeId, long clusterPartition) {
public ListenableFuture<Void> put(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition) {
long msgTime = getMsgTime(msg);
long tsPartition = queuePartitioner.getPartition(msgTime);
return msgRepository.save(msg, nodeId, clusterPartition, tsPartition, msgTime);
}
@Override
public ListenableFuture<Void> ack(TbMsg msg, UUID nodeId, long clusterPartition) {
public ListenableFuture<Void> ack(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition) {
long tsPartition = queuePartitioner.getPartition(getMsgTime(msg));
MsgAck ack = new MsgAck(msg.getId(), nodeId, clusterPartition, tsPartition);
return ackRepository.ack(ack);
}
@Override
public Iterable<TbMsg> findUnprocessed(UUID nodeId, long clusterPartition) {
public Iterable<TbMsg> findUnprocessed(TenantId tenantId, UUID nodeId, long clusterPartition) {
List<TbMsg> unprocessedMsgs = Lists.newArrayList();
for (Long tsPartition : queuePartitioner.findUnprocessedPartitions(nodeId, clusterPartition)) {
List<TbMsg> msgs = msgRepository.findMsgs(nodeId, clusterPartition, tsPartition);
@ -69,6 +73,11 @@ public class CassandraMsgQueue implements MsgQueue {
return unprocessedMsgs;
}
@Override
public ListenableFuture<Void> cleanUp(TenantId tenantId) {
return Futures.immediateFuture(null);
}
private long getMsgTime(TbMsg msg) {
return UUIDs.unixTimestamp(msg.getId());
}

2
dao/src/main/java/org/thingsboard/server/dao/service/queue/cassandra/MsgAck.java → dao/src/main/java/org/thingsboard/server/dao/queue/db/nosql/MsgAck.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.queue.cassandra;
package org.thingsboard.server.dao.queue.db.nosql;
import lombok.Data;
import lombok.EqualsAndHashCode;

4
dao/src/main/java/org/thingsboard/server/dao/service/queue/cassandra/QueuePartitioner.java → dao/src/main/java/org/thingsboard/server/dao/queue/db/nosql/QueuePartitioner.java

@ -13,13 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.queue.cassandra;
package org.thingsboard.server.dao.queue.db.nosql;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.server.dao.service.queue.cassandra.repository.ProcessedPartitionRepository;
import org.thingsboard.server.dao.queue.db.repository.ProcessedPartitionRepository;
import org.thingsboard.server.dao.timeseries.TsPartitionDate;
import org.thingsboard.server.dao.util.NoSqlDao;

2
dao/src/main/java/org/thingsboard/server/dao/service/queue/cassandra/UnprocessedMsgFilter.java → dao/src/main/java/org/thingsboard/server/dao/queue/db/nosql/UnprocessedMsgFilter.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.queue.cassandra;
package org.thingsboard.server.dao.queue.db.nosql;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.msg.TbMsg;

6
dao/src/main/java/org/thingsboard/server/dao/service/queue/cassandra/repository/impl/CassandraAckRepository.java → dao/src/main/java/org/thingsboard/server/dao/queue/db/nosql/repository/CassandraAckRepository.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.queue.cassandra.repository.impl;
package org.thingsboard.server.dao.queue.db.nosql.repository;
import com.datastax.driver.core.*;
import com.google.common.base.Function;
@ -22,8 +22,8 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.server.dao.nosql.CassandraAbstractDao;
import org.thingsboard.server.dao.service.queue.cassandra.MsgAck;
import org.thingsboard.server.dao.service.queue.cassandra.repository.AckRepository;
import org.thingsboard.server.dao.queue.db.nosql.MsgAck;
import org.thingsboard.server.dao.queue.db.repository.AckRepository;
import org.thingsboard.server.dao.util.NoSqlDao;
import java.util.ArrayList;

4
dao/src/main/java/org/thingsboard/server/dao/service/queue/cassandra/repository/impl/CassandraMsgRepository.java → dao/src/main/java/org/thingsboard/server/dao/queue/db/nosql/repository/CassandraMsgRepository.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.queue.cassandra.repository.impl;
package org.thingsboard.server.dao.queue.db.nosql.repository;
import com.datastax.driver.core.*;
import com.google.common.base.Function;
@ -23,7 +23,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.dao.nosql.CassandraAbstractDao;
import org.thingsboard.server.dao.service.queue.cassandra.repository.MsgRepository;
import org.thingsboard.server.dao.queue.db.repository.MsgRepository;
import org.thingsboard.server.dao.util.NoSqlDao;
import java.util.ArrayList;

4
dao/src/main/java/org/thingsboard/server/dao/service/queue/cassandra/repository/impl/CassandraProcessedPartitionRepository.java → dao/src/main/java/org/thingsboard/server/dao/queue/db/nosql/repository/CassandraProcessedPartitionRepository.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.queue.cassandra.repository.impl;
package org.thingsboard.server.dao.queue.db.nosql.repository;
import com.datastax.driver.core.*;
import com.google.common.base.Function;
@ -22,7 +22,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.server.dao.nosql.CassandraAbstractDao;
import org.thingsboard.server.dao.service.queue.cassandra.repository.ProcessedPartitionRepository;
import org.thingsboard.server.dao.queue.db.repository.ProcessedPartitionRepository;
import org.thingsboard.server.dao.util.NoSqlDao;
import java.util.Optional;

4
dao/src/main/java/org/thingsboard/server/dao/service/queue/cassandra/repository/AckRepository.java → dao/src/main/java/org/thingsboard/server/dao/queue/db/repository/AckRepository.java

@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.queue.cassandra.repository;
package org.thingsboard.server.dao.queue.db.repository;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.dao.service.queue.cassandra.MsgAck;
import org.thingsboard.server.dao.queue.db.nosql.MsgAck;
import java.util.List;
import java.util.UUID;

2
dao/src/main/java/org/thingsboard/server/dao/service/queue/cassandra/repository/MsgRepository.java → dao/src/main/java/org/thingsboard/server/dao/queue/db/repository/MsgRepository.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.queue.cassandra.repository;
package org.thingsboard.server.dao.queue.db.repository;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.msg.TbMsg;

2
dao/src/main/java/org/thingsboard/server/dao/service/queue/cassandra/repository/ProcessedPartitionRepository.java → dao/src/main/java/org/thingsboard/server/dao/queue/db/repository/ProcessedPartitionRepository.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.queue.cassandra.repository;
package org.thingsboard.server.dao.queue.db.repository;
import com.google.common.util.concurrent.ListenableFuture;

2
dao/src/main/java/org/thingsboard/server/dao/queue/jpa/SqlMsgQueue.java → dao/src/main/java/org/thingsboard/server/dao/queue/db/sql/SqlMsgQueue.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.queue.jpa;
package org.thingsboard.server.dao.queue.db.sql;
//@todo-vp: implement
public class SqlMsgQueue {

2
dao/src/main/java/org/thingsboard/server/dao/sql/queue/InMemoryMsgKey.java → dao/src/main/java/org/thingsboard/server/dao/queue/memory/InMemoryMsgKey.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.queue;
package org.thingsboard.server.dao.queue.memory;
import lombok.Data;

118
dao/src/main/java/org/thingsboard/server/dao/queue/memory/InMemoryMsgQueue.java

@ -0,0 +1,118 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.queue.memory;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.dao.queue.MsgQueue;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
/**
* Created by ashvayka on 27.04.18.
*/
@Component
@ConditionalOnProperty(prefix = "rule.queue", value = "type", havingValue = "memory", matchIfMissing = true)
@Slf4j
public class InMemoryMsgQueue implements MsgQueue {
private ListeningExecutorService queueExecutor;
private Map<TenantId, Map<InMemoryMsgKey, Map<UUID, TbMsg>>> data = new HashMap<>();
@PostConstruct
public void init() {
// Should be always single threaded due to absence of locks.
queueExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
}
@PreDestroy
public void stop() {
if (queueExecutor != null) {
queueExecutor.shutdownNow();
}
}
@Override
public ListenableFuture<Void> put(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition) {
return queueExecutor.submit(() -> {
data.computeIfAbsent(tenantId, key -> new HashMap<>()).
computeIfAbsent(new InMemoryMsgKey(nodeId, clusterPartition), key -> new HashMap<>()).put(msg.getId(), msg);
return null;
});
}
@Override
public ListenableFuture<Void> ack(TenantId tenantId, TbMsg msg, UUID nodeId, long clusterPartition) {
return queueExecutor.submit(() -> {
Map<InMemoryMsgKey, Map<UUID, TbMsg>> tenantMap = data.get(tenantId);
if (tenantMap != null) {
InMemoryMsgKey key = new InMemoryMsgKey(nodeId, clusterPartition);
Map<UUID, TbMsg> map = tenantMap.get(key);
if (map != null) {
map.remove(msg.getId());
if (map.isEmpty()) {
tenantMap.remove(key);
}
}
if (tenantMap.isEmpty()) {
data.remove(tenantId);
}
}
return null;
});
}
@Override
public Iterable<TbMsg> findUnprocessed(TenantId tenantId, UUID nodeId, long clusterPartition) {
ListenableFuture<List<TbMsg>> list = queueExecutor.submit(() -> {
Map<InMemoryMsgKey, Map<UUID, TbMsg>> tenantMap = data.get(tenantId);
if (tenantMap != null) {
InMemoryMsgKey key = new InMemoryMsgKey(nodeId, clusterPartition);
Map<UUID, TbMsg> map = tenantMap.get(key);
if (map != null) {
return new ArrayList<>(map.values());
} else {
return Collections.emptyList();
}
} else {
return Collections.emptyList();
}
});
try {
return list.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
@Override
public ListenableFuture<Void> cleanUp(TenantId tenantId) {
return queueExecutor.submit(() -> {
data.remove(tenantId);
return null;
});
}
}

113
dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java

@ -16,7 +16,6 @@
package org.thingsboard.server.dao.relation;
import com.google.common.base.Function;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
@ -29,12 +28,23 @@ import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.*;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import javax.annotation.Nullable;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
@ -170,12 +180,12 @@ public class BaseRelationService implements RelationService {
Cache cache = cacheManager.getCache(RELATIONS_CACHE);
log.trace("Executing deleteEntityRelations [{}]", entity);
validate(entity);
List<ListenableFuture<List<EntityRelation>>> inboundRelationsListTo = new ArrayList<>();
List<ListenableFuture<List<EntityRelation>>> inboundRelationsList = new ArrayList<>();
for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
inboundRelationsListTo.add(relationDao.findAllByTo(entity, typeGroup));
inboundRelationsList.add(relationDao.findAllByTo(entity, typeGroup));
}
ListenableFuture<List<List<EntityRelation>>> inboundRelationsTo = Futures.allAsList(inboundRelationsListTo);
ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelationsTo, (List<List<EntityRelation>> relations) ->
ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations, relations ->
getBooleans(relations, cache, true));
ListenableFuture<Boolean> inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction());
@ -186,13 +196,12 @@ public class BaseRelationService implements RelationService {
log.error("Error deleting entity inbound relations", e);
}
List<ListenableFuture<List<EntityRelation>>> inboundRelationsListFrom = new ArrayList<>();
List<ListenableFuture<List<EntityRelation>>> outboundRelationsList = new ArrayList<>();
for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
inboundRelationsListFrom.add(relationDao.findAllByFrom(entity, typeGroup));
outboundRelationsList.add(relationDao.findAllByFrom(entity, typeGroup));
}
ListenableFuture<List<List<EntityRelation>>> inboundRelationsFrom = Futures.allAsList(inboundRelationsListFrom);
Futures.transform(inboundRelationsFrom, (Function<List<List<EntityRelation>>, List<Boolean>>) relations ->
getBooleans(relations, cache, false));
ListenableFuture<List<List<EntityRelation>>> outboundRelations = Futures.allAsList(outboundRelationsList);
Futures.transform(outboundRelations, relations -> getBooleans(relations, cache, false));
boolean outboundDeleteResult = relationDao.deleteOutboundRelations(entity);
return inboundDeleteResult && outboundDeleteResult;
@ -201,9 +210,7 @@ public class BaseRelationService implements RelationService {
private List<Boolean> getBooleans(List<List<EntityRelation>> relations, Cache cache, boolean isRemove) {
List<Boolean> results = new ArrayList<>();
for (List<EntityRelation> relationList : relations) {
relationList.stream().forEach(relation -> {
checkFromDeleteSync(cache, results, relation, isRemove);
});
relationList.forEach(relation -> checkFromDeleteSync(cache, results, relation, isRemove));
}
return results;
}
@ -211,10 +218,8 @@ public class BaseRelationService implements RelationService {
private void checkFromDeleteSync(Cache cache, List<Boolean> results, EntityRelation relation, boolean isRemove) {
if (isRemove) {
results.add(relationDao.deleteRelation(relation));
cacheEviction(relation, false, cache);
} else {
cacheEviction(relation, true, cache);
}
cacheEviction(relation, cache);
}
@Override
@ -222,12 +227,12 @@ public class BaseRelationService implements RelationService {
Cache cache = cacheManager.getCache(RELATIONS_CACHE);
log.trace("Executing deleteEntityRelationsAsync [{}]", entity);
validate(entity);
List<ListenableFuture<List<EntityRelation>>> inboundRelationsListTo = new ArrayList<>();
List<ListenableFuture<List<EntityRelation>>> inboundRelationsList = new ArrayList<>();
for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
inboundRelationsListTo.add(relationDao.findAllByTo(entity, typeGroup));
inboundRelationsList.add(relationDao.findAllByTo(entity, typeGroup));
}
ListenableFuture<List<List<EntityRelation>>> inboundRelationsTo = Futures.allAsList(inboundRelationsListTo);
ListenableFuture<List<Boolean>> inboundDeletions = Futures.transformAsync(inboundRelationsTo,
ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
ListenableFuture<List<Boolean>> inboundDeletions = Futures.transformAsync(inboundRelations,
relations -> {
List<ListenableFuture<Boolean>> results = getListenableFutures(relations, cache, true);
return Futures.allAsList(results);
@ -235,12 +240,12 @@ public class BaseRelationService implements RelationService {
ListenableFuture<Boolean> inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction());
List<ListenableFuture<List<EntityRelation>>> inboundRelationsListFrom = new ArrayList<>();
List<ListenableFuture<List<EntityRelation>>> outboundRelationsList = new ArrayList<>();
for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
inboundRelationsListFrom.add(relationDao.findAllByTo(entity, typeGroup));
outboundRelationsList.add(relationDao.findAllByFrom(entity, typeGroup));
}
ListenableFuture<List<List<EntityRelation>>> inboundRelationsFrom = Futures.allAsList(inboundRelationsListFrom);
Futures.transformAsync(inboundRelationsFrom, relations -> {
ListenableFuture<List<List<EntityRelation>>> outboundRelations = Futures.allAsList(outboundRelationsList);
Futures.transformAsync(outboundRelations, relations -> {
List<ListenableFuture<Boolean>> results = getListenableFutures(relations, cache, false);
return Futures.allAsList(results);
});
@ -252,9 +257,7 @@ public class BaseRelationService implements RelationService {
private List<ListenableFuture<Boolean>> getListenableFutures(List<List<EntityRelation>> relations, Cache cache, boolean isRemove) {
List<ListenableFuture<Boolean>> results = new ArrayList<>();
for (List<EntityRelation> relationList : relations) {
relationList.forEach(relation -> {
checkFromDeleteAsync(cache, results, relation, isRemove);
});
relationList.forEach(relation -> checkFromDeleteAsync(cache, results, relation, isRemove));
}
return results;
}
@ -262,13 +265,11 @@ public class BaseRelationService implements RelationService {
private void checkFromDeleteAsync(Cache cache, List<ListenableFuture<Boolean>> results, EntityRelation relation, boolean isRemove) {
if (isRemove) {
results.add(relationDao.deleteRelationAsync(relation));
cacheEviction(relation, false, cache);
} else {
cacheEviction(relation, true, cache);
}
cacheEviction(relation, cache);
}
private void cacheEviction(EntityRelation relation, boolean outboundOnly, Cache cache) {
private void cacheEviction(EntityRelation relation, Cache cache) {
List<Object> fromToTypeAndTypeGroup = new ArrayList<>();
fromToTypeAndTypeGroup.add(relation.getFrom());
fromToTypeAndTypeGroup.add(relation.getTo());
@ -287,18 +288,16 @@ public class BaseRelationService implements RelationService {
fromAndTypeGroup.add(relation.getTypeGroup());
cache.evict(fromAndTypeGroup);
if (!outboundOnly) {
List<Object> toAndTypeGroup = new ArrayList<>();
toAndTypeGroup.add(relation.getTo());
toAndTypeGroup.add(relation.getTypeGroup());
cache.evict(toAndTypeGroup);
List<Object> toTypeAndTypeGroup = new ArrayList<>();
fromTypeAndTypeGroup.add(relation.getTo());
fromTypeAndTypeGroup.add(relation.getType());
fromTypeAndTypeGroup.add(relation.getTypeGroup());
cache.evict(toTypeAndTypeGroup);
}
List<Object> toAndTypeGroup = new ArrayList<>();
toAndTypeGroup.add(relation.getTo());
toAndTypeGroup.add(relation.getTypeGroup());
cache.evict(toAndTypeGroup);
List<Object> toTypeAndTypeGroup = new ArrayList<>();
fromTypeAndTypeGroup.add(relation.getTo());
fromTypeAndTypeGroup.add(relation.getType());
fromTypeAndTypeGroup.add(relation.getTypeGroup());
cache.evict(toTypeAndTypeGroup);
}
@Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}")
@ -383,10 +382,10 @@ public class BaseRelationService implements RelationService {
return Futures.transformAsync(relations,
relations1 -> {
List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
relations1.stream().forEach(relation ->
relations1.forEach(relation ->
futures.add(fetchRelationInfoAsync(relation,
relation2 -> relation2.getFrom(),
(EntityRelationInfo relationInfo, String entityName) -> relationInfo.setFromName(entityName)))
EntityRelation::getFrom,
EntityRelationInfo::setFromName))
);
return Futures.successfulAsList(futures);
});
@ -396,13 +395,11 @@ public class BaseRelationService implements RelationService {
Function<EntityRelation, EntityId> entityIdGetter,
BiConsumer<EntityRelationInfo, String> entityNameSetter) {
ListenableFuture<String> entityName = entityService.fetchEntityNameAsync(entityIdGetter.apply(relation));
ListenableFuture<EntityRelationInfo> entityRelationInfo =
Futures.transform(entityName, (Function<String, EntityRelationInfo>) entityName1 -> {
EntityRelationInfo entityRelationInfo1 = new EntityRelationInfo(relation);
entityNameSetter.accept(entityRelationInfo1, entityName1);
return entityRelationInfo1;
});
return entityRelationInfo;
return Futures.transform(entityName, entityName1 -> {
EntityRelationInfo entityRelationInfo1 = new EntityRelationInfo(relation);
entityNameSetter.accept(entityRelationInfo1, entityName1);
return entityRelationInfo1;
});
}
@Cacheable(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup}")
@ -437,7 +434,7 @@ public class BaseRelationService implements RelationService {
try {
ListenableFuture<Set<EntityRelation>> relationSet = findRelationsRecursively(params.getEntityId(), params.getDirection(), maxLvl, new ConcurrentHashMap<>());
return Futures.transform(relationSet, (Function<Set<EntityRelation>, List<EntityRelation>>) input -> {
return Futures.transform(relationSet, input -> {
List<EntityRelation> relations = new ArrayList<>();
if (filters == null || filters.isEmpty()) {
relations.addAll(input);
@ -464,7 +461,7 @@ public class BaseRelationService implements RelationService {
return Futures.transformAsync(relations,
relations1 -> {
List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
relations1.stream().forEach(relation ->
relations1.forEach(relation ->
futures.add(fetchRelationInfoAsync(relation,
relation2 -> direction == EntitySearchDirection.FROM ? relation2.getTo() : relation2.getFrom(),
(EntityRelationInfo relationInfo, String entityName) -> {
@ -582,7 +579,7 @@ public class BaseRelationService implements RelationService {
}
//TODO: try to remove this blocking operation
List<Set<EntityRelation>> relations = Futures.successfulAsList(futures).get();
relations.forEach(r -> r.forEach(d -> children.add(d)));
relations.forEach(r -> r.forEach(children::add));
return Futures.immediateFuture(children);
}

79
dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java

@ -57,8 +57,6 @@ import java.util.stream.Collectors;
@Slf4j
public class BaseRuleChainService extends AbstractEntityService implements RuleChainService {
public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
@Autowired
private RuleChainDao ruleChainDao;
@ -71,12 +69,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
@Override
public RuleChain saveRuleChain(RuleChain ruleChain) {
ruleChainValidator.validate(ruleChain);
if (ruleChain.getTenantId() == null) {
log.trace("Save system rule chain with predefined id {}", SYSTEM_TENANT);
ruleChain.setTenantId(SYSTEM_TENANT);
}
RuleChain savedRuleChain = ruleChainDao.save(ruleChain);
if (ruleChain.isRoot() && ruleChain.getTenantId() != null && ruleChain.getId() == null) {
if (ruleChain.isRoot() && ruleChain.getId() == null) {
try {
createRelation(new EntityRelation(savedRuleChain.getTenantId(), savedRuleChain.getId(),
EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN));
@ -89,6 +83,31 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
return savedRuleChain;
}
@Override
public boolean setRootRuleChain(RuleChainId ruleChainId) {
RuleChain ruleChain = ruleChainDao.findById(ruleChainId.getId());
if (!ruleChain.isRoot()) {
RuleChain previousRootRuleChain = getRootTenantRuleChain(ruleChain.getTenantId());
if (!previousRootRuleChain.getId().equals(ruleChain.getId())) {
try {
deleteRelation(new EntityRelation(previousRootRuleChain.getTenantId(), previousRootRuleChain.getId(),
EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN));
previousRootRuleChain.setRoot(false);
ruleChainDao.save(previousRootRuleChain);
createRelation(new EntityRelation(ruleChain.getTenantId(), ruleChain.getId(),
EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN));
ruleChain.setRoot(true);
ruleChainDao.save(ruleChain);
return true;
} catch (ExecutionException | InterruptedException e) {
log.warn("[{}] Failed to set root rule chain, ruleChainId: [{}]", ruleChainId);
throw new RuntimeException(e);
}
}
}
return false;
}
@Override
public RuleChainMetaData saveRuleChainMetaData(RuleChainMetaData ruleChainMetaData) {
Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id.");
@ -266,13 +285,6 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
return relationService.findByFrom(ruleNodeId, RelationTypeGroup.RULE_NODE);
}
@Override
public TextPageData<RuleChain> findSystemRuleChains(TextPageLink pageLink) {
Validator.validatePageLink(pageLink, "Incorrect PageLink object for search system rule chain request.");
List<RuleChain> ruleChains = ruleChainDao.findRuleChainsByTenantId(SYSTEM_TENANT.getId(), pageLink);
return new TextPageData<>(ruleChains, pageLink);
}
@Override
public TextPageData<RuleChain> findTenantRuleChains(TenantId tenantId, TextPageLink pageLink) {
Validator.validateId(tenantId, "Incorrect tenant id for search rule chain request.");
@ -281,18 +293,13 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
return new TextPageData<>(ruleChains, pageLink);
}
@Override
public TextPageData<RuleChain> findAllTenantRuleChainsByTenantIdAndPageLink(TenantId tenantId, TextPageLink pageLink) {
log.trace("Executing findAllTenantRuleChainsByTenantIdAndPageLink, tenantId [{}], pageLink [{}]", tenantId, pageLink);
Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink);
List<RuleChain> ruleChains = ruleChainDao.findAllRuleChainsByTenantId(tenantId.getId(), pageLink);
return new TextPageData<>(ruleChains, pageLink);
}
@Override
public void deleteRuleChainById(RuleChainId ruleChainId) {
Validator.validateId(ruleChainId, "Incorrect rule chain id for delete request.");
RuleChain ruleChain = ruleChainDao.findById(ruleChainId.getId());
if (ruleChain != null && ruleChain.isRoot()) {
throw new DataValidationException("Deletion of Root Tenant Rule Chain is prohibited!");
}
checkRuleNodesAndDelete(ruleChainId);
}
@ -325,6 +332,11 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
relationService.saveRelation(relation);
}
private void deleteRelation(EntityRelation relation) throws ExecutionException, InterruptedException {
log.debug("Deleting relation: {}", relation);
relationService.deleteRelation(relation);
}
private DataValidator<RuleChain> ruleChainValidator =
new DataValidator<RuleChain>() {
@Override
@ -332,16 +344,17 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
if (StringUtils.isEmpty(ruleChain.getName())) {
throw new DataValidationException("Rule chain name should be specified!.");
}
if (ruleChain.getTenantId() != null && !ruleChain.getTenantId().isNullUid()) {
Tenant tenant = tenantDao.findById(ruleChain.getTenantId().getId());
if (tenant == null) {
throw new DataValidationException("Rule chain is referencing to non-existent tenant!");
}
if (ruleChain.isRoot()) {
RuleChain rootRuleChain = getRootTenantRuleChain(ruleChain.getTenantId());
if (rootRuleChain != null && !rootRuleChain.getId().equals(ruleChain.getId())) {
throw new DataValidationException("Another root rule chain is present in scope of current tenant!");
}
if (ruleChain.getTenantId() == null || ruleChain.getTenantId().isNullUid()) {
throw new DataValidationException("Rule chain should be assigned to tenant!");
}
Tenant tenant = tenantDao.findById(ruleChain.getTenantId().getId());
if (tenant == null) {
throw new DataValidationException("Rule chain is referencing to non-existent tenant!");
}
if (ruleChain.isRoot()) {
RuleChain rootRuleChain = getRootTenantRuleChain(ruleChain.getTenantId());
if (rootRuleChain != null && !rootRuleChain.getId().equals(ruleChain.getId())) {
throw new DataValidationException("Another root rule chain is present in scope of current tenant!");
}
}
}

11
dao/src/main/java/org/thingsboard/server/dao/rule/CassandraRuleChainDao.java

@ -60,15 +60,4 @@ public class CassandraRuleChainDao extends CassandraAbstractSearchTextDao<RuleCh
return DaoUtil.convertDataList(ruleChainEntities);
}
@Override
public List<RuleChain> findAllRuleChainsByTenantId(UUID tenantId, TextPageLink pageLink) {
log.debug("Try to find all rule chains by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
List<RuleChainEntity> ruleChainEntities = findPageWithTextSearch(RULE_CHAIN_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
Arrays.asList(in(ModelConstants.RULE_CHAIN_TENANT_ID_PROPERTY, Arrays.asList(NULL_UUID, tenantId))),
pageLink);
log.trace("Found rule chains [{}] by tenantId [{}] and pageLink [{}]", ruleChainEntities, tenantId, pageLink);
return DaoUtil.convertDataList(ruleChainEntities);
}
}

9
dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java

@ -37,13 +37,4 @@ public interface RuleChainDao extends Dao<RuleChain> {
*/
List<RuleChain> findRuleChainsByTenantId(UUID tenantId, TextPageLink pageLink);
/**
* Find all rule chains by tenantId and page link.
*
* @param tenantId the tenantId
* @param pageLink the page link
* @return the list of rule chain objects
*/
List<RuleChain> findAllRuleChainsByTenantId(UUID tenantId, TextPageLink pageLink);
}

6
dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java

@ -36,6 +36,8 @@ public interface RuleChainService {
RuleChain saveRuleChain(RuleChain ruleChain);
boolean setRootRuleChain(RuleChainId ruleChainId);
RuleChainMetaData saveRuleChainMetaData(RuleChainMetaData ruleChainMetaData);
RuleChainMetaData loadRuleChainMetaData(RuleChainId ruleChainId);
@ -54,12 +56,8 @@ public interface RuleChainService {
List<EntityRelation> getRuleNodeRelations(RuleNodeId ruleNodeId);
TextPageData<RuleChain> findSystemRuleChains(TextPageLink pageLink);
TextPageData<RuleChain> findTenantRuleChains(TenantId tenantId, TextPageLink pageLink);
TextPageData<RuleChain> findAllTenantRuleChainsByTenantIdAndPageLink(TenantId tenantId, TextPageLink pageLink);
void deleteRuleChainById(RuleChainId ruleChainId);
void deleteRuleChainsByTenantId(TenantId tenantId);

114
dao/src/main/java/org/thingsboard/server/dao/sql/queue/InMemoryMsgQueue.java

@ -1,114 +0,0 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.queue;
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 lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.dao.queue.MsgQueue;
import org.thingsboard.server.dao.util.SqlDao;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
/**
* Created by ashvayka on 27.04.18.
*/
@Component
//@ConditionalOnProperty(prefix = "rule.queue", value = "type", havingValue = "memory", matchIfMissing = true)
@Slf4j
@SqlDao
public class InMemoryMsgQueue implements MsgQueue {
@Value("${rule.queue.max_size}")
@Getter
private long maxSize;
private ListeningExecutorService queueExecutor;
private AtomicLong pendingMsgCount = new AtomicLong();
private Map<InMemoryMsgKey, Map<UUID, TbMsg>> data = new HashMap<>();
@PostConstruct
public void init() {
// Should be always single threaded due to absence of locks.
queueExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
}
@PreDestroy
public void stop() {
if (queueExecutor == null) {
queueExecutor.shutdownNow();
}
}
@Override
public ListenableFuture<Void> put(TbMsg msg, UUID nodeId, long clusterPartition) {
if (pendingMsgCount.incrementAndGet() < maxSize) {
return queueExecutor.submit(() -> {
data.computeIfAbsent(new InMemoryMsgKey(nodeId, clusterPartition), key -> new HashMap<>()).put(msg.getId(), msg);
return null;
});
} else {
pendingMsgCount.decrementAndGet();
return Futures.immediateFailedFuture(new RuntimeException("Message queue is full!"));
}
}
@Override
public ListenableFuture<Void> ack(TbMsg msg, UUID nodeId, long clusterPartition) {
return queueExecutor.submit(() -> {
InMemoryMsgKey key = new InMemoryMsgKey(nodeId, clusterPartition);
Map<UUID, TbMsg> map = data.get(key);
if (map != null) {
if (map.remove(msg.getId()) != null) {
pendingMsgCount.decrementAndGet();
}
if (map.isEmpty()) {
data.remove(key);
}
}
return null;
});
}
@Override
public Iterable<TbMsg> findUnprocessed(UUID nodeId, long clusterPartition) {
ListenableFuture<List<TbMsg>> list = queueExecutor.submit(() -> {
InMemoryMsgKey key = new InMemoryMsgKey(nodeId, clusterPartition);
Map<UUID, TbMsg> map = data.get(key);
if (map != null) {
return new ArrayList<>(map.values());
} else {
return Collections.emptyList();
}
});
try {
return list.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
}

11
dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java

@ -63,15 +63,4 @@ public class JpaRuleChainDao extends JpaAbstractSearchTextDao<RuleChainEntity, R
new PageRequest(0, pageLink.getLimit())));
}
@Override
public List<RuleChain> findAllRuleChainsByTenantId(UUID tenantId, TextPageLink pageLink) {
return DaoUtil.convertDataList(ruleChainRepository
.findAllTenantRuleChainsByTenantId(
UUIDConverter.fromTimeUUID(tenantId),
NULL_UUID_STR,
Objects.toString(pageLink.getTextSearch(), ""),
pageLink.getIdOffset() == null ? NULL_UUID_STR : UUIDConverter.fromTimeUUID(pageLink.getIdOffset()),
new PageRequest(0, pageLink.getLimit())));
}
}

8
dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java

@ -35,12 +35,4 @@ public interface RuleChainRepository extends CrudRepository<RuleChainEntity, Str
@Param("idOffset") String idOffset,
Pageable pageable);
@Query("SELECT rc FROM RuleChainEntity rc WHERE rc.tenantId IN (:tenantId, :nullTenantId) " +
"AND LOWER(rc.searchText) LIKE LOWER(CONCAT(:searchText, '%')) " +
"AND rc.id > :idOffset ORDER BY rc.id")
List<RuleChainEntity> findAllTenantRuleChainsByTenantId(@Param("tenantId") String tenantId,
@Param("nullTenantId") String nullTenantId,
@Param("searchText") String searchText,
@Param("idOffset") String idOffset,
Pageable pageable);
}

5
dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/QueuePartitionerTest.java → dao/src/test/java/org/thingsboard/server/dao/queue/db/nosql/QueuePartitionerTest.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.queue.cassandra;
package org.thingsboard.server.dao.queue.db.nosql;
import org.junit.Before;
@ -21,7 +21,8 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.thingsboard.server.dao.service.queue.cassandra.repository.ProcessedPartitionRepository;
import org.thingsboard.server.dao.queue.db.nosql.QueuePartitioner;
import org.thingsboard.server.dao.queue.db.repository.ProcessedPartitionRepository;
import java.time.Clock;
import java.time.Instant;

4
dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/UnprocessedMsgFilterTest.java → dao/src/test/java/org/thingsboard/server/dao/queue/db/nosql/UnprocessedMsgFilterTest.java

@ -13,11 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.queue.cassandra;
package org.thingsboard.server.dao.queue.db.nosql;
import com.google.common.collect.Lists;
import org.junit.Test;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.dao.queue.db.nosql.MsgAck;
import org.thingsboard.server.dao.queue.db.nosql.UnprocessedMsgFilter;
import java.util.Collection;
import java.util.List;

5
dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/repository/impl/CassandraAckRepositoryTest.java → dao/src/test/java/org/thingsboard/server/dao/queue/db/nosql/repository/CassandraAckRepositoryTest.java

@ -13,18 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.queue.cassandra.repository.impl;
package org.thingsboard.server.dao.queue.db.nosql.repository;
import com.datastax.driver.core.utils.UUIDs;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.dao.service.AbstractServiceTest;
import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.queue.cassandra.MsgAck;
import org.thingsboard.server.dao.queue.db.nosql.MsgAck;
import java.util.List;
import java.util.UUID;

3
dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/repository/impl/CassandraMsgRepositoryTest.java → dao/src/test/java/org/thingsboard/server/dao/queue/db/nosql/repository/CassandraMsgRepositoryTest.java

@ -13,13 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.queue.cassandra.repository.impl;
package org.thingsboard.server.dao.queue.db.nosql.repository;
//import static org.junit.jupiter.api.Assertions.*;
import com.datastax.driver.core.utils.UUIDs;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.util.ReflectionTestUtils;

3
dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/repository/impl/CassandraProcessedPartitionRepositoryTest.java → dao/src/test/java/org/thingsboard/server/dao/queue/db/nosql/repository/CassandraProcessedPartitionRepositoryTest.java

@ -13,12 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.queue.cassandra.repository.impl;
package org.thingsboard.server.dao.queue.db.nosql.repository;
import com.datastax.driver.core.utils.UUIDs;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.util.ReflectionTestUtils;

93
docker/k8s/redis.yaml

@ -0,0 +1,93 @@
#
# Copyright © 2016-2018 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.
#
---
apiVersion: v1
kind: Service
metadata:
labels:
name: redis-service
name: redis-service
spec:
ports:
- name: redis-service
protocol: TCP
port: 6379
targetPort: 6379
selector:
app: redis
---
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-conf
data:
redis.conf: |
appendonly yes
protected-mode no
bind 0.0.0.0
port 6379
dir /var/lib/redis
---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: redis-service
replicas: 1
template:
metadata:
labels:
app: redis
spec:
terminationGracePeriodSeconds: 10
containers:
- name: redis
image: redis:4.0.9
command:
- redis-server
args:
- /etc/redis/redis.conf
resources:
requests:
cpu: 100m
memory: 100Mi
ports:
- containerPort: 6379
name: redis
volumeMounts:
- name: redis-data
mountPath: /var/lib/redis
- name: redis-conf
mountPath: /etc/redis
volumes:
- name: redis-conf
configMap:
name: redis-conf
items:
- key: redis.conf
path: redis.conf
volumeClaimTemplates:
- metadata:
name: redis-data
annotations:
volume.beta.kubernetes.io/storage-class: fast
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi

12
docker/k8s/tb.yaml

@ -54,6 +54,8 @@ data:
cassandra.host: "cassandra-headless"
cassandra.port: "9042"
database.type: "cassandra"
cache.type: "redis"
redis.host: "redis-service"
---
apiVersion: apps/v1beta1
kind: StatefulSet
@ -127,6 +129,16 @@ spec:
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: CACHE_TYPE
valueFrom:
configMapKeyRef:
name: tb-config
key: cache.type
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: tb-config
key: redis.host
command:
- sh
- -c

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

@ -48,12 +48,6 @@ public interface TbContext {
void tellSelf(TbMsg msg, long delayMs);
void tellOthers(TbMsg msg);
void tellSibling(TbMsg msg, ServerAddress address);
void ack(TbMsg msg);
void tellError(TbMsg msg, Throwable th);
void updateSelf(RuleNode self);

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java

@ -69,13 +69,13 @@ public abstract class TbAbstractAlarmNode<C extends TbAbstractAlarmNodeConfigura
protected ListenableFuture<JsonNode> buildAlarmDetails(TbContext ctx, TbMsg msg, JsonNode previousDetails) {
return ctx.getJsExecutor().executeAsync(() -> {
TbMsg theMsg = msg;
TbMsg dummyMsg = msg;
if (previousDetails != null) {
TbMsgMetaData metaData = msg.getMetaData().copy();
metaData.putValue(PREV_ALARM_DETAILS, mapper.writeValueAsString(previousDetails));
theMsg = ctx.newMsg(msg.getType(), msg.getOriginator(), metaData, msg.getData());
dummyMsg = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), metaData, msg.getData());
}
return buildDetailsJsEngine.executeJson(theMsg);
return buildDetailsJsEngine.executeJson(dummyMsg);
});
}

8
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java

@ -33,8 +33,6 @@ import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
*/
public class MqttTransportServerInitializer extends ChannelInitializer<SocketChannel> {
private static final int MAX_PAYLOAD_SIZE = 64 * 1024 * 1024;
private final SessionMsgProcessor processor;
private final DeviceService deviceService;
private final DeviceAuthService authService;
@ -42,10 +40,11 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
private final MqttTransportAdaptor adaptor;
private final MqttSslHandlerProvider sslHandlerProvider;
private final QuotaService quotaService;
private final int maxPayloadSize;
public MqttTransportServerInitializer(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService,
MqttTransportAdaptor adaptor, MqttSslHandlerProvider sslHandlerProvider,
QuotaService quotaService) {
QuotaService quotaService, int maxPayloadSize) {
this.processor = processor;
this.deviceService = deviceService;
this.authService = authService;
@ -53,6 +52,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
this.adaptor = adaptor;
this.sslHandlerProvider = sslHandlerProvider;
this.quotaService = quotaService;
this.maxPayloadSize = maxPayloadSize;
}
@Override
@ -63,7 +63,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
sslHandler = sslHandlerProvider.getSslHandler();
pipeline.addLast(sslHandler);
}
pipeline.addLast("decoder", new MqttDecoder(MAX_PAYLOAD_SIZE));
pipeline.addLast("decoder", new MqttDecoder(maxPayloadSize));
pipeline.addLast("encoder", MqttEncoder.INSTANCE);
MqttTransportHandler handler = new MqttTransportHandler(processor, deviceService, authService, relationService,

5
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java

@ -82,7 +82,8 @@ public class MqttTransportService {
private Integer bossGroupThreadCount;
@Value("${mqtt.netty.worker_group_thread_count}")
private Integer workerGroupThreadCount;
@Value("${mqtt.netty.max_payload_size}")
private Integer maxPayloadSize;
private MqttTransportAdaptor adaptor;
@ -106,7 +107,7 @@ public class MqttTransportService {
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MqttTransportServerInitializer(processor, deviceService, authService, relationService,
adaptor, sslHandlerProvider, quotaService));
adaptor, sslHandlerProvider, quotaService, maxPayloadSize));
serverChannel = b.bind(host, port).sync().channel();
log.info("Mqtt transport started!");

27
ui/src/app/api/entity.service.js

@ -21,8 +21,7 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes])
/*@ngInject*/
function EntityService($http, $q, $filter, $translate, $log, userService, deviceService,
assetService, tenantService, customerService,
ruleService, pluginService, ruleChainService, dashboardService, entityRelationService, attributeService, types, utils) {
assetService, tenantService, customerService, ruleChainService, dashboardService, entityRelationService, attributeService, types, utils) {
var service = {
getEntity: getEntity,
getEntities: getEntities,
@ -61,12 +60,6 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
case types.entityType.customer:
promise = customerService.getCustomer(entityId, config);
break;
case types.entityType.rule:
promise = ruleService.getRule(entityId, config);
break;
case types.entityType.plugin:
promise = pluginService.getPlugin(entityId, config);
break;
case types.entityType.dashboard:
promise = dashboardService.getDashboardInfo(entityId, config);
break;
@ -146,14 +139,6 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
promise = getEntitiesByIdsPromise(
(id) => customerService.getCustomer(id, config), entityIds);
break;
case types.entityType.rule:
promise = getEntitiesByIdsPromise(
(id) => ruleService.getRule(id, config), entityIds);
break;
case types.entityType.plugin:
promise = getEntitiesByIdsPromise(
(id) => pluginService.getPlugin(id, config), entityIds);
break;
case types.entityType.dashboard:
promise = getEntitiesByIdsPromise(
(id) => dashboardService.getDashboardInfo(id, config), entityIds);
@ -268,12 +253,6 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
promise = customerService.getCustomers(pageLink, config);
}
break;
case types.entityType.rule:
promise = ruleService.getAllRules(pageLink, config);
break;
case types.entityType.plugin:
promise = pluginService.getAllPlugins(pageLink, config);
break;
case types.entityType.rulechain:
promise = ruleChainService.getRuleChains(pageLink, config);
break;
@ -742,16 +721,12 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
switch(authority) {
case 'SYS_ADMIN':
entityTypes.tenant = types.entityType.tenant;
entityTypes.rule = types.entityType.rule;
entityTypes.plugin = types.entityType.plugin;
break;
case 'TENANT_ADMIN':
entityTypes.device = types.entityType.device;
entityTypes.asset = types.entityType.asset;
entityTypes.tenant = types.entityType.tenant;
entityTypes.customer = types.entityType.customer;
entityTypes.rule = types.entityType.rule;
entityTypes.plugin = types.entityType.plugin;
entityTypes.dashboard = types.entityType.dashboard;
if (useAliasEntityTypes) {
entityTypes.current_customer = types.aliasEntityType.current_customer;

218
ui/src/app/api/plugin.service.js

@ -1,218 +0,0 @@
/*
* Copyright © 2016-2018 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.
*/
export default angular.module('thingsboard.api.plugin', [])
.factory('pluginService', PluginService).name;
/*@ngInject*/
function PluginService($http, $q, $rootScope, $filter, componentDescriptorService, types, utils) {
var allPlugins = undefined;
var allActionPlugins = undefined;
var systemPlugins = undefined;
var tenantPlugins = undefined;
$rootScope.pluginServiceStateChangeStartHandle = $rootScope.$on('$stateChangeStart', function () {
invalidatePluginsCache();
});
var service = {
getSystemPlugins: getSystemPlugins,
getTenantPlugins: getTenantPlugins,
getAllPlugins: getAllPlugins,
getAllActionPlugins: getAllActionPlugins,
getPluginByToken: getPluginByToken,
getPlugin: getPlugin,
deletePlugin: deletePlugin,
savePlugin: savePlugin,
activatePlugin: activatePlugin,
suspendPlugin: suspendPlugin
}
return service;
function invalidatePluginsCache() {
allPlugins = undefined;
allActionPlugins = undefined;
systemPlugins = undefined;
tenantPlugins = undefined;
}
function loadPluginsCache(config) {
var deferred = $q.defer();
if (!allPlugins) {
var url = '/api/plugins';
$http.get(url, config).then(function success(response) {
componentDescriptorService.getComponentDescriptorsByType(types.componentType.plugin).then(
function success(pluginComponents) {
allPlugins = response.data;
allActionPlugins = [];
systemPlugins = [];
tenantPlugins = [];
allPlugins = $filter('orderBy')(allPlugins, ['+name', '-createdTime']);
var pluginHasActionsByClazz = {};
for (var index in pluginComponents) {
pluginHasActionsByClazz[pluginComponents[index].clazz] =
(pluginComponents[index].actions != null && pluginComponents[index].actions.length > 0);
}
for (var i = 0; i < allPlugins.length; i++) {
var plugin = allPlugins[i];
if (pluginHasActionsByClazz[plugin.clazz] === true) {
allActionPlugins.push(plugin);
}
if (plugin.tenantId.id === types.id.nullUid) {
systemPlugins.push(plugin);
} else {
tenantPlugins.push(plugin);
}
}
deferred.resolve();
},
function fail() {
deferred.reject();
}
);
}, function fail() {
deferred.reject();
});
} else {
deferred.resolve();
}
return deferred.promise;
}
function getSystemPlugins(pageLink, config) {
var deferred = $q.defer();
loadPluginsCache(config).then(
function success() {
utils.filterSearchTextEntities(systemPlugins, 'name', pageLink, deferred);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function getTenantPlugins(pageLink, config) {
var deferred = $q.defer();
loadPluginsCache(config).then(
function success() {
utils.filterSearchTextEntities(tenantPlugins, 'name', pageLink, deferred);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function getAllActionPlugins(pageLink, config) {
var deferred = $q.defer();
loadPluginsCache(config).then(
function success() {
utils.filterSearchTextEntities(allActionPlugins, 'name', pageLink, deferred);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function getAllPlugins(pageLink, config) {
var deferred = $q.defer();
loadPluginsCache(config).then(
function success() {
utils.filterSearchTextEntities(allPlugins, 'name', pageLink, deferred);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function getPluginByToken(pluginToken) {
var deferred = $q.defer();
var url = '/api/plugin/token/' + pluginToken;
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function getPlugin(pluginId, config) {
var deferred = $q.defer();
var url = '/api/plugin/' + pluginId;
$http.get(url, config).then(function success(response) {
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
function savePlugin(plugin) {
var deferred = $q.defer();
var url = '/api/plugin';
$http.post(url, plugin).then(function success(response) {
invalidatePluginsCache();
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
function deletePlugin(pluginId) {
var deferred = $q.defer();
var url = '/api/plugin/' + pluginId;
$http.delete(url).then(function success() {
invalidatePluginsCache();
deferred.resolve();
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
function activatePlugin(pluginId) {
var deferred = $q.defer();
var url = '/api/plugin/' + pluginId + '/activate';
$http.post(url, null).then(function success(response) {
invalidatePluginsCache();
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
function suspendPlugin(pluginId) {
var deferred = $q.defer();
var url = '/api/plugin/' + pluginId + '/suspend';
$http.post(url, null).then(function success(response) {
invalidatePluginsCache();
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
}

52
ui/src/app/api/rule-chain.service.js

@ -22,11 +22,10 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
var ruleNodeComponents = null;
var service = {
getSystemRuleChains: getSystemRuleChains,
getTenantRuleChains: getTenantRuleChains,
getRuleChains: getRuleChains,
getRuleChain: getRuleChain,
saveRuleChain: saveRuleChain,
setRootRuleChain: setRootRuleChain,
deleteRuleChain: deleteRuleChain,
getRuleChainMetaData: getRuleChainMetaData,
saveRuleChainMetaData: saveRuleChainMetaData,
@ -40,29 +39,9 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
return service;
function getSystemRuleChains (pageLink, config) {
var deferred = $q.defer();
var url = '/api/system/ruleChains?limit=' + pageLink.limit;
if (angular.isDefined(pageLink.textSearch)) {
url += '&textSearch=' + pageLink.textSearch;
}
if (angular.isDefined(pageLink.idOffset)) {
url += '&idOffset=' + pageLink.idOffset;
}
if (angular.isDefined(pageLink.textOffset)) {
url += '&textOffset=' + pageLink.textOffset;
}
$http.get(url, config).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function getTenantRuleChains (pageLink, config) {
function getRuleChains (pageLink, config) {
var deferred = $q.defer();
var url = '/api/tenant/ruleChains?limit=' + pageLink.limit;
var url = '/api/ruleChains?limit=' + pageLink.limit;
if (angular.isDefined(pageLink.textSearch)) {
url += '&textSearch=' + pageLink.textSearch;
}
@ -80,18 +59,9 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
return deferred.promise;
}
function getRuleChains (pageLink, config) {
function getRuleChain(ruleChainId, config) {
var deferred = $q.defer();
var url = '/api/ruleChains?limit=' + pageLink.limit;
if (angular.isDefined(pageLink.textSearch)) {
url += '&textSearch=' + pageLink.textSearch;
}
if (angular.isDefined(pageLink.idOffset)) {
url += '&idOffset=' + pageLink.idOffset;
}
if (angular.isDefined(pageLink.textOffset)) {
url += '&textOffset=' + pageLink.textOffset;
}
var url = '/api/ruleChain/' + ruleChainId;
$http.get(url, config).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
@ -100,10 +70,10 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
return deferred.promise;
}
function getRuleChain(ruleChainId, config) {
function saveRuleChain(ruleChain) {
var deferred = $q.defer();
var url = '/api/ruleChain/' + ruleChainId;
$http.get(url, config).then(function success(response) {
var url = '/api/ruleChain';
$http.post(url, ruleChain).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
@ -111,10 +81,10 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
return deferred.promise;
}
function saveRuleChain(ruleChain) {
function setRootRuleChain(ruleChainId) {
var deferred = $q.defer();
var url = '/api/ruleChain';
$http.post(url, ruleChain).then(function success(response) {
var url = '/api/ruleChain/' + ruleChainId + '/root';
$http.post(url).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();

186
ui/src/app/api/rule.service.js

@ -1,186 +0,0 @@
/*
* Copyright © 2016-2018 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.
*/
export default angular.module('thingsboard.api.rule', [])
.factory('ruleService', RuleService).name;
/*@ngInject*/
function RuleService($http, $q, $rootScope, $filter, types, utils) {
var allRules = undefined;
var systemRules = undefined;
var tenantRules = undefined;
$rootScope.ruleServiceStateChangeStartHandle = $rootScope.$on('$stateChangeStart', function () {
invalidateRulesCache();
});
var service = {
getSystemRules: getSystemRules,
getTenantRules: getTenantRules,
getAllRules: getAllRules,
getRulesByPluginToken: getRulesByPluginToken,
getRule: getRule,
deleteRule: deleteRule,
saveRule: saveRule,
activateRule: activateRule,
suspendRule: suspendRule
}
return service;
function invalidateRulesCache() {
allRules = undefined;
systemRules = undefined;
tenantRules = undefined;
}
function loadRulesCache(config) {
var deferred = $q.defer();
if (!allRules) {
var url = '/api/rules';
$http.get(url, config).then(function success(response) {
allRules = response.data;
systemRules = [];
tenantRules = [];
allRules = $filter('orderBy')(allRules, ['+name', '-createdTime']);
for (var i = 0; i < allRules.length; i++) {
var rule = allRules[i];
if (rule.tenantId.id === types.id.nullUid) {
systemRules.push(rule);
} else {
tenantRules.push(rule);
}
}
deferred.resolve();
}, function fail() {
deferred.reject();
});
} else {
deferred.resolve();
}
return deferred.promise;
}
function getSystemRules(pageLink) {
var deferred = $q.defer();
loadRulesCache().then(
function success() {
utils.filterSearchTextEntities(systemRules, 'name', pageLink, deferred);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function getTenantRules(pageLink) {
var deferred = $q.defer();
loadRulesCache().then(
function success() {
utils.filterSearchTextEntities(tenantRules, 'name', pageLink, deferred);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function getAllRules(pageLink, config) {
var deferred = $q.defer();
loadRulesCache(config).then(
function success() {
utils.filterSearchTextEntities(allRules, 'name', pageLink, deferred);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function getRulesByPluginToken(pluginToken) {
var deferred = $q.defer();
var url = '/api/rule/token/' + pluginToken;
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function getRule(ruleId, config) {
var deferred = $q.defer();
var url = '/api/rule/' + ruleId;
$http.get(url, config).then(function success(response) {
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
function saveRule(rule) {
var deferred = $q.defer();
var url = '/api/rule';
$http.post(url, rule).then(function success(response) {
invalidateRulesCache();
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
function deleteRule(ruleId) {
var deferred = $q.defer();
var url = '/api/rule/' + ruleId;
$http.delete(url).then(function success() {
invalidateRulesCache();
deferred.resolve();
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
function activateRule(ruleId) {
var deferred = $q.defer();
var url = '/api/rule/' + ruleId + '/activate';
$http.post(url, null).then(function success(response) {
invalidateRulesCache();
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
function suspendRule(ruleId) {
var deferred = $q.defer();
var url = '/api/rule/' + ruleId + '/suspend';
$http.post(url, null).then(function success(response) {
invalidateRulesCache();
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
}

4
ui/src/app/app.js

@ -74,6 +74,7 @@ import thingsboardApiAttribute from './api/attribute.service';
import thingsboardApiEntity from './api/entity.service';
import thingsboardApiAlarm from './api/alarm.service';
import thingsboardApiAuditLog from './api/audit-log.service';
import thingsboardApiComponentDescriptor from './api/component-descriptor.service';
import thingsboardApiRuleChain from './api/rule-chain.service';
import 'typeface-roboto';
@ -81,7 +82,7 @@ import 'font-awesome/css/font-awesome.min.css';
import 'angular-material/angular-material.min.css';
import 'angular-material-icons/angular-material-icons.css';
import 'angular-gridster/dist/angular-gridster.min.css';
import 'v-accordion/dist/v-accordion.min.css'
import 'v-accordion/dist/v-accordion.min.css';
import 'md-color-picker/dist/mdColorPicker.min.css';
import 'mdPickers/dist/mdPickers.min.css';
import 'angular-hotkeys/build/hotkeys.min.css';
@ -139,6 +140,7 @@ angular.module('thingsboard', [
thingsboardApiEntity,
thingsboardApiAlarm,
thingsboardApiAuditLog,
thingsboardApiComponentDescriptor,
thingsboardApiRuleChain,
uiRouter])
.config(AppConfig)

42
ui/src/app/common/types.constant.js

@ -297,16 +297,15 @@ export default angular.module('thingsboard.types', [])
}
},
componentType: {
enrichment: "ENRICHMENT",
filter: "FILTER",
processor: "PROCESSOR",
transformation: "TRANSFORMATION",
action: "ACTION",
plugin: "PLUGIN"
external: "EXTERNAL"
},
entityType: {
device: "DEVICE",
asset: "ASSET",
rule: "RULE",
plugin: "PLUGIN",
tenant: "TENANT",
customer: "CUSTOMER",
user: "USER",
@ -331,18 +330,6 @@ export default angular.module('thingsboard.types', [])
list: 'entity.list-of-assets',
nameStartsWith: 'entity.asset-name-starts-with'
},
"RULE": {
type: 'entity.type-rule',
typePlural: 'entity.type-rules',
list: 'entity.list-of-rules',
nameStartsWith: 'entity.rule-name-starts-with'
},
"PLUGIN": {
type: 'entity.type-plugin',
typePlural: 'entity.type-plugins',
list: 'entity.list-of-plugins',
nameStartsWith: 'entity.plugin-name-starts-with'
},
"TENANT": {
type: 'entity.type-tenant',
typePlural: 'entity.type-tenants',
@ -419,7 +406,8 @@ export default angular.module('thingsboard.types', [])
extensionType: {
http: "HTTP",
mqtt: "MQTT",
opc: "OPC UA"
opc: "OPC UA",
modbus: "MODBUS"
},
extensionValueType: {
string: 'value.string',
@ -463,6 +451,26 @@ export default angular.module('thingsboard.types', [])
PKCS12: "PKCS12",
JKS: "JKS"
},
extensionModbusFunctionCodes: {
1: "Read Coils (1)",
2: "Read Discrete Inputs (2)",
3: "Read Multiple Holding Registers (3)",
4: "Read Input Registers (4)"
},
extensionModbusTransports: {
tcp: "TCP",
udp: "UDP",
rtu: "RTU"
},
extensionModbusRtuParities: {
none: "none",
even: "even",
odd: "odd"
},
extensionModbusRtuEncodings: {
ascii: "ascii",
rtu: "rtu"
},
latestTelemetry: {
value: "LATEST_TELEMETRY",
name: "attribute.scope-latest-telemetry",

105
ui/src/app/component/component-dialog.controller.js

@ -1,105 +0,0 @@
/*
* Copyright © 2016-2018 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.
*/
/*@ngInject*/
export default function ComponentDialogController($mdDialog, $q, $scope, componentDescriptorService, types, utils, helpLinks, isAdd, isReadOnly, componentInfo) {
var vm = this;
vm.isReadOnly = isReadOnly;
vm.isAdd = isAdd;
vm.componentInfo = componentInfo;
if (isAdd) {
vm.componentInfo.component = {};
}
vm.componentHasSchema = false;
vm.componentDescriptors = [];
if (vm.componentInfo.component && !vm.componentInfo.component.configuration) {
vm.componentInfo.component.configuration = {};
}
vm.helpLinkIdForComponent = helpLinkIdForComponent;
vm.save = save;
vm.cancel = cancel;
$scope.$watch("vm.componentInfo.component.clazz", function (newValue, prevValue) {
if (newValue != prevValue) {
if (newValue && prevValue) {
vm.componentInfo.component.configuration = {};
}
loadComponentDescriptor();
}
});
var componentDescriptorsPromise =
vm.componentInfo.type === types.componentType.action
? componentDescriptorService.getPluginActionsByPluginClazz(vm.componentInfo.pluginClazz)
: componentDescriptorService.getComponentDescriptorsByType(vm.componentInfo.type);
componentDescriptorsPromise.then(
function success(componentDescriptors) {
vm.componentDescriptors = componentDescriptors;
if (vm.componentDescriptors.length === 1 && isAdd && !vm.componentInfo.component.clazz) {
vm.componentInfo.component.clazz = vm.componentDescriptors[0].clazz;
}
},
function fail() {
}
);
loadComponentDescriptor();
function loadComponentDescriptor () {
if (vm.componentInfo.component.clazz) {
componentDescriptorService.getComponentDescriptorByClazz(vm.componentInfo.component.clazz).then(
function success(componentDescriptor) {
vm.componentDescriptor = componentDescriptor;
vm.componentHasSchema = utils.isDescriptorSchemaNotEmpty(vm.componentDescriptor.configurationDescriptor);
},
function fail() {
}
);
} else {
vm.componentHasSchema = false;
}
}
function helpLinkIdForComponent() {
switch (vm.componentInfo.type) {
case types.componentType.filter: {
return helpLinks.getFilterLink(vm.componentInfo.component);
}
case types.componentType.processor: {
return helpLinks.getProcessorLink(vm.componentInfo.component);
}
case types.componentType.action: {
return helpLinks.getPluginActionLink(vm.componentInfo.component);
}
}
}
function cancel () {
$mdDialog.cancel();
}
function save () {
$mdDialog.hide(vm.componentInfo.component);
}
}

60
ui/src/app/component/component-dialog.service.js

@ -1,60 +0,0 @@
/*
* Copyright © 2016-2018 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.
*/
/* eslint-disable import/no-unresolved, import/default */
import componentDialogTemplate from './component-dialog.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function ComponentDialogService($mdDialog, $document, $q) {
var service = {
openComponentDialog: openComponentDialog
}
return service;
function openComponentDialog($event, isAdd, readOnly, title, type, pluginClazz, component) {
var deferred = $q.defer();
var componentInfo = {
title: title,
type: type,
pluginClazz: pluginClazz
};
if (component) {
componentInfo.component = angular.copy(component);
}
$mdDialog.show({
controller: 'ComponentDialogController',
controllerAs: 'vm',
templateUrl: componentDialogTemplate,
locals: {isAdd: isAdd,
isReadOnly: readOnly,
componentInfo: componentInfo},
parent: angular.element($document[0].body),
fullscreen: true,
targetEvent: $event,
skipHide: true
}).then(function (component) {
deferred.resolve(component);
}, function () {
deferred.reject();
});
return deferred.promise;
}
}

79
ui/src/app/component/component-dialog.tpl.html

@ -1,79 +0,0 @@
<!--
Copyright © 2016-2018 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.
-->
<md-dialog aria-label="{{ vm.componentInfo.title | translate }}" tb-help="vm.helpLinkIdForComponent()" help-container-id="help-container">
<form name="theForm" ng-submit="vm.save()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>{{ vm.componentInfo.title }}</h2>
<span flex></span>
<div id="help-container"></div>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
<md-dialog-content>
<div class="md-dialog-content tb-filter">
<fieldset ng-disabled="$root.loading || vm.isReadOnly">
<section flex layout="row">
<md-input-container flex class="md-block">
<label translate>rule.component-name</label>
<input required name="componentName" ng-model="vm.componentInfo.component.name">
<div ng-messages="theForm.componentName.$error">
<div translate ng-message="required">rule.component-name-required</div>
</div>
</md-input-container>
<md-input-container flex class="md-block">
<label translate>rule.component-type</label>
<md-select required name="componentType" ng-model="vm.componentInfo.component.clazz" ng-disabled="$root.loading || vm.isReadOnly">
<md-option ng-repeat="componentDescriptor in vm.componentDescriptors" ng-value="componentDescriptor.clazz">
{{componentDescriptor.name}}
</md-option>
</md-select>
<div ng-messages="theForm.componentType.$error">
<div translate ng-message="required">rule.component-type-required</div>
</div>
</md-input-container>
</section>
<md-card flex class="plugin-config" ng-if="vm.componentHasSchema">
<md-card-content>
<tb-json-form schema="vm.componentDescriptor.configurationDescriptor.schema"
form="vm.componentDescriptor.configurationDescriptor.form"
model="vm.componentInfo.component.configuration"
readonly="$root.loading || vm.isReadOnly"
form-control="theForm">
</tb-json-form>
</md-card-content>
</md-card>
</fieldset>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-if="!vm.isReadOnly" ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
class="md-raised md-primary">
{{ (vm.isAdd ? 'action.add' : 'action.save') | translate }}
</md-button>
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
translate }}
</md-button>
</md-dialog-actions>
</form>
</md-dialog>

75
ui/src/app/component/component.directive.js

@ -1,75 +0,0 @@
/*
* Copyright © 2016-2018 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.
*/
/* eslint-disable import/no-unresolved, import/default */
import componentTemplate from './component.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function ComponentDirective($compile, $templateCache, $document, $mdDialog, componentDialogService, componentDescriptorService) {
var linker = function (scope, element) {
var template = $templateCache.get(componentTemplate);
element.html(template);
scope.componentTypeName = '';
scope.loadComponentTypeName = function () {
componentDescriptorService.getComponentDescriptorByClazz(scope.component.clazz).then(
function success(component) {
scope.componentTypeName = component.name;
},
function fail() {}
);
}
scope.$watch('component', function(newVal) {
if (newVal) {
scope.loadComponentTypeName();
}
}
);
scope.openComponent = function($event) {
componentDialogService.openComponentDialog($event, false,
scope.readOnly, scope.title, scope.type, scope.pluginClazz,
angular.copy(scope.component)).then(
function success(component) {
scope.component = component;
},
function fail() {}
);
}
$compile(element.contents())(scope);
}
return {
restrict: "E",
link: linker,
scope: {
component: '=',
type: '=',
pluginClazz: '=',
title: '@',
readOnly: '=',
onRemoveComponent: '&'
}
};
}

58
ui/src/app/component/component.tpl.html

@ -1,58 +0,0 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="md-whiteframe-4dp" flex layout="row" layout-align="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<span flex="50">
{{ component.name }}
</span>
<span flex="50">
{{ componentTypeName }}
</span>
<span ng-if="readOnly" style="min-width: 40px; min-height: 40px; margin: 0 6px;"></br></span>
<md-button ng-disabled="$root.loading" class="md-icon-button md-primary"
style="min-width: 40px;"
ng-click="openComponent($event)"
aria-label="{{ (readOnly ? 'action.view' : 'action.edit') | translate }}">
<md-tooltip ng-if="readOnly" md-direction="top">
{{ 'action.view' | translate }}
</md-tooltip>
<md-tooltip ng-if="!readOnly" md-direction="top">
{{ 'action.edit' | translate }}
</md-tooltip>
<md-icon ng-if="readOnly" aria-label="{{ 'action.view' | translate }}"
class="material-icons">
more_horiz
</md-icon>
<md-icon ng-if="!readOnly" aria-label="{{ 'action.edit' | translate }}"
class="material-icons">
edit
</md-icon>
</md-button>
<md-button ng-if="!readOnly" ng-disabled="$root.loading" class="md-icon-button md-primary"
style="min-width: 40px;"
ng-click="onRemoveComponent({event: $event})"
aria-label="{{ 'action.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.delete' | translate }}"
class="material-icons">
close
</md-icon>
</md-button>
</div>

28
ui/src/app/component/index.js

@ -1,28 +0,0 @@
/*
* Copyright © 2016-2018 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 thingsboardApiComponentDescriptor from '../api/component-descriptor.service';
import ComponentDialogService from './component-dialog.service';
import ComponentDialogController from './component-dialog.controller';
import ComponentDirective from './component.directive';
export default angular.module('thingsboard.component', [
thingsboardApiComponentDescriptor
])
.factory('componentDialogService', ComponentDialogService)
.controller('ComponentDialogController', ComponentDialogController)
.directive('tbComponent', ComponentDirective)
.name;

115
ui/src/app/components/plugin-select.directive.js

@ -1,115 +0,0 @@
/*
* Copyright © 2016-2018 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 './plugin-select.scss';
import thingsboardApiPlugin from '../api/plugin.service';
/* eslint-disable import/no-unresolved, import/default */
import pluginSelectTemplate from './plugin-select.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
export default angular.module('thingsboard.directives.pluginSelect', [thingsboardApiPlugin])
.directive('tbPluginSelect', PluginSelect)
.name;
/*@ngInject*/
function PluginSelect($compile, $templateCache, $q, pluginService, types) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(pluginSelectTemplate);
element.html(template);
scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
scope.plugin = null;
scope.pluginSearchText = '';
scope.searchTextChanged = false;
scope.pluginFetchFunction = pluginService.getAllPlugins;
if (angular.isDefined(scope.pluginsScope)) {
if (scope.pluginsScope === 'action') {
scope.pluginFetchFunction = pluginService.getAllActionPlugins;
} else if (scope.pluginsScope === 'system') {
scope.pluginFetchFunction = pluginService.getSystemPlugins;
} else if (scope.pluginsScope === 'tenant') {
scope.pluginFetchFunction = pluginService.getTenantPlugins;
}
}
scope.fetchPlugins = function(searchText) {
var pageLink = {limit: 10, textSearch: searchText};
var deferred = $q.defer();
scope.pluginFetchFunction(pageLink, {ignoreLoading: true}).then(function success(result) {
deferred.resolve(result.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
scope.pluginSearchTextChanged = function() {
scope.searchTextChanged = true;
}
scope.isSystem = function(item) {
return item && item.tenantId.id === types.id.nullUid;
}
scope.updateView = function () {
ngModelCtrl.$setViewValue(scope.plugin);
}
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
scope.plugin = ngModelCtrl.$viewValue;
}
}
scope.$watch('plugin', function () {
scope.updateView();
})
if (scope.selectFirstPlugin) {
var pageLink = {limit: 1, textSearch: ''};
scope.pluginFetchFunction(pageLink, {ignoreLoading: true}).then(function success(result) {
var plugins = result.data;
if (plugins.length > 0) {
scope.plugin = plugins[0];
}
}, function fail() {
});
}
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
link: linker,
scope: {
pluginsScope: '@',
theForm: '=?',
tbRequired: '=?',
selectFirstPlugin: '='
}
};
}

37
ui/src/app/components/plugin-select.scss

@ -1,37 +0,0 @@
/**
* Copyright © 2016-2018 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 "../../scss/mixins";
.tb-plugin-autocomplete {
.tb-not-found {
display: block;
line-height: 1.5;
height: 48px;
}
.tb-plugin-item {
display: block;
height: 48px;
.tb-plugin-system {
font-size: 0.8rem;
opacity: 0.8;
float: right;
}
}
li {
height: auto !important;
white-space: normal !important;
}
}

44
ui/src/app/components/plugin-select.tpl.html

@ -1,44 +0,0 @@
<!--
Copyright © 2016-2018 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.
-->
<md-autocomplete ng-required="tbRequired"
md-input-name="plugin"
ng-model="plugin"
md-selected-item="plugin"
md-search-text="pluginSearchText"
md-search-text-change="pluginSearchTextChanged()"
md-items="item in fetchPlugins(pluginSearchText)"
md-item-text="item.name"
md-min-length="0"
placeholder="{{ 'plugin.select-plugin' | translate }}"
md-menu-class="tb-plugin-autocomplete">
<md-item-template>
<div class="tb-plugin-item">
<span md-highlight-text="pluginSearchText" md-highlight-flags="^i">{{item.name}}</span>
<span translate class="tb-plugin-system" ng-if="isSystem(item)">plugin.system</span>
</div>
</md-item-template>
<md-not-found>
<div class="tb-not-found">
<span translate translate-values='{ entity: pluginSearchText }'>plugin.no-plugins-matching</span>
</div>
</md-not-found>
</md-autocomplete>
<div ng-if="searchTextChanged" class="tb-error-messages" ng-messages="theForm.plugin.$error" role="alert">
<div translate ng-message="required" class="tb-error-message">plugin.plugin-required</div>
<div translate ng-message="md-require-match" class="tb-error-message">plugin.plugin-require-match</div>
</div>

12
ui/src/app/entity/entity-autocomplete.directive.js

@ -131,18 +131,6 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter
scope.noEntitiesMatchingText = 'device.no-devices-matching';
scope.entityRequiredText = 'device.device-required';
break;
case types.entityType.rule:
scope.selectEntityText = 'rule.select-rule';
scope.entityText = 'rule.rule';
scope.noEntitiesMatchingText = 'rule.no-rules-matching';
scope.entityRequiredText = 'rule.rule-required';
break;
case types.entityType.plugin:
scope.selectEntityText = 'plugin.select-plugin';
scope.entityText = 'plugin.plugin';
scope.noEntitiesMatchingText = 'plugin.no-plugins-matching';
scope.entityRequiredText = 'plugin.plugin-required';
break;
case types.entityType.rulechain:
scope.selectEntityText = 'rulechain.select-rulechain';
scope.entityText = 'rulechain.rulechain';

2
ui/src/app/extension/extension-dialog.controller.js

@ -49,7 +49,7 @@ export default function ExtensionDialogController($scope, $mdDialog, $translate,
"brokers": []
};
}
if (vm.extension.type === "OPC UA") {
if (vm.extension.type === "OPC UA" || vm.extension.type === "MODBUS") {
vm.extension.configuration = {
"servers": []
};

1
ui/src/app/extension/extension-dialog.tpl.html

@ -62,6 +62,7 @@
<div tb-extension-form-http config="vm.extension.configuration" is-add="vm.isAdd" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.http"></div>
<div tb-extension-form-mqtt config="vm.extension.configuration" is-add="vm.isAdd" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.mqtt"></div>
<div tb-extension-form-opc configuration="vm.extension.configuration" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.opc"></div>
<div tb-extension-form-modbus configuration="vm.extension.configuration" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.modbus"></div>
</fieldset>
</md-content>
</div>

140
ui/src/app/extension/extensions-forms/extension-form-modbus.directive.js

@ -0,0 +1,140 @@
/*
* Copyright © 2016-2018 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 'brace/ext/language_tools';
import 'brace/mode/json';
import 'brace/theme/github';
import './extension-form.scss';
/* eslint-disable angular/log */
import extensionFormModbusTemplate from './extension-form-modbus.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function ExtensionFormModbusDirective($compile, $templateCache, $translate, types) {
var linker = function(scope, element) {
function Server() {
this.transport = {
"type": "tcp",
"host": "localhost",
"port": 502,
"timeout": 3000
};
this.devices = []
}
function Device() {
this.unitId = 1;
this.deviceName = "";
this.attributesPollPeriod = 1000;
this.timeseriesPollPeriod = 1000;
this.attributes = [];
this.timeseries = [];
}
function Tag(globalPollPeriod) {
this.tag = "";
this.type = "long";
this.pollPeriod = globalPollPeriod;
this.functionCode = 3;
this.address = 0;
this.registerCount = 1;
this.bit = 0;
this.byteOrder = "BIG";
}
var template = $templateCache.get(extensionFormModbusTemplate);
element.html(template);
scope.types = types;
scope.theForm = scope.$parent.theForm;
if (!scope.configuration.servers.length) {
scope.configuration.servers.push(new Server());
}
scope.addServer = function(serversList) {
serversList.push(new Server());
scope.theForm.$setDirty();
};
scope.addDevice = function(deviceList) {
deviceList.push(new Device());
scope.theForm.$setDirty();
};
scope.addNewAttribute = function(device) {
device.attributes.push(new Tag(device.attributesPollPeriod));
scope.theForm.$setDirty();
};
scope.addNewTimeseries = function(device) {
device.timeseries.push(new Tag(device.timeseriesPollPeriod));
scope.theForm.$setDirty();
};
scope.removeItem = (item, itemList) => {
var index = itemList.indexOf(item);
if (index > -1) {
itemList.splice(index, 1);
}
scope.theForm.$setDirty();
};
scope.onTransportChanged = function(server) {
var type = server.transport.type;
server.transport = {};
server.transport.type = type;
server.transport.timeout = 3000;
scope.theForm.$setDirty();
};
$compile(element.contents())(scope);
scope.collapseValidation = function(index, id) {
var invalidState = angular.element('#'+id+':has(.ng-invalid)');
if(invalidState.length) {
invalidState.addClass('inner-invalid');
}
};
scope.expandValidation = function (index, id) {
var invalidState = angular.element('#'+id);
invalidState.removeClass('inner-invalid');
};
};
return {
restrict: "A",
link: linker,
scope: {
configuration: "=",
isAdd: "="
}
}
}

774
ui/src/app/extension/extensions-forms/extension-form-modbus.tpl.html

@ -0,0 +1,774 @@
<!--
Copyright © 2016-2018 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.
-->
<md-card class="extension-form extension-modbus">
<md-card-title>
<md-card-title-text>
<span translate class="md-headline">extension.configuration</span>
</md-card-title-text>
</md-card-title>
<md-card-content>
<v-accordion id="modbus-server-configs-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="modbus-servers-pane" expanded="true">
<v-pane-header>
{{ 'extension.modbus-server' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="configuration.servers.length === 0">
<span translate layout-align="center center" class="tb-prompt">extension.modbus-add-server-prompt</span>
</div>
<div ng-if="configuration.servers.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(serverIndex, server) in configuration.servers">
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(server, configuration.servers)"
ng-hide="configuration.servers.length < 2"
>
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<div layout="row">
<md-input-container flex="50" class="md-block tb-container-for-select">
<label translate>extension.modbus-transport</label>
<md-select required
name="transportType_{{serverIndex}}"
ng-model="server.transport.type"
ng-change="onTransportChanged(server)"
>
<md-option ng-value="transportType"
ng-repeat="(transportType, transportValue) in types.extensionModbusTransports"
><span ng-bind="transportValue"></span></md-option>
</md-select>
<div ng-messages="theForm['transportType_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<div layout="row" ng-if="server.transport.type == 'tcp' || server.transport.type == 'udp'">
<md-input-container flex="33" class="md-block">
<label translate>extension.host</label>
<input required name="transportHost_{{serverIndex}}" ng-model="server.transport.host">
<div ng-messages="theForm['transportHost_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="33" class="md-block">
<label translate>extension.port</label>
<input type="number"
required
name="transportPort_{{serverIndex}}"
ng-model="server.transport.port"
min="1"
max="65535"
>
<div ng-messages="theForm['transportPort_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.port-range</div>
<div translate
ng-message="max"
>extension.port-range</div>
</div>
</md-input-container>
<md-input-container flex="33" class="md-block">
<label translate>extension.timeout</label>
<input type="number"
required name="transportTimeout_{{serverIndex}}"
ng-model="server.transport.timeout"
>
<div ng-messages="theForm['transportTimeout_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<div ng-if="server.transport.type == 'rtu'">
<div layout="row">
<md-input-container flex="70" class="md-block">
<label translate>extension.modbus-port-name</label>
<input required name="transportPortName_{{serverIndex}}" ng-model="server.transport.portName">
<div ng-messages="theForm['transportPortName_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="30" class="md-block">
<label translate>extension.timeout</label>
<input type="number"
required name="transportTimeout_{{serverIndex}}"
ng-model="server.transport.timeout"
>
<div ng-messages="theForm['transportTimeout_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<div layout="row">
<md-input-container flex="50" class="md-block tb-container-for-select">
<label translate>extension.modbus-encoding</label>
<md-select required
name="transportEncoding_{{serverIndex}}"
ng-model="server.transport.encoding"
>
<md-option ng-value="encodingType"
ng-repeat="(encodingType, encodingValue) in types.extensionModbusRtuEncodings"
><span ng-bind="encodingValue"></span></md-option>
</md-select>
<div ng-messages="theForm['transportEncoding_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block tb-container-for-select">
<label translate>extension.modbus-parity</label>
<md-select name="transportParity_{{serverIndex}}" ng-model="server.transport.parity">
<md-option ng-repeat="(parityKey, parityValue) in types.extensionModbusRtuParities"
ng-value="parityValue"
>
{{parityValue}}
</md-option>
</md-select>
<div ng-messages="theForm['transportParity_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<div layout="row">
<md-input-container flex="33" class="md-block">
<label translate>extension.modbus-baudrate</label>
<input type="number"
required name="transportBaudRate_{{serverIndex}}"
ng-model="server.transport.baudRate"
>
<div ng-messages="theForm['transportBaudRate_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="33" class="md-block">
<label translate>extension.modbus-databits</label>
<input type="number"
required
name="transportDataBits_{{serverIndex}}"
ng-model="server.transport.dataBits"
min="7"
max="8"
>
<div ng-messages="theForm['transportDataBits_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-databits-range</div>
<div translate
ng-message="max"
>extension.modbus-databits-range</div>
</div>
</md-input-container>
<md-input-container flex="33" class="md-block">
<label translate>extension.modbus-stopbits</label>
<input type="number"
required
name="transportStopBits_{{serverIndex}}"
ng-model="server.transport.stopBits"
min="1"
max="2"
>
<div ng-messages="theForm['transportStopBits_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-stopbits-range</div>
<div translate
ng-message="max"
>extension.modbus-stopbits-range</div>
</div>
</md-input-container>
</div>
</div>
<v-accordion id="modbus-mapping-accordion"
class="vAccordion--default"
onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="modbus-mapping-pane_{{serverIndex}}">
<v-pane-header>
{{ 'extension.mapping' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="server.devices.length > 0">
<ol class="list-group">
<li class="list-group-item"
ng-repeat="(deviceIndex, device) in server.devices"
>
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(device, server.devices)"
>
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<div flex layout="row">
<md-input-container flex="80" class="md-block">
<label translate>extension.modbus-device-name</label>
<input required
name="deviceName_{{serverIndex}}{{deviceIndex}}"
ng-model="device.deviceName"
>
<div ng-messages="theForm['deviceName_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="20" class="md-block">
<label translate>extension.modbus-unit-id</label>
<input type="number"
required
name="unitId_{{serverIndex}}{{deviceIndex}}"
ng-model="device.unitId"
min="1"
max="247"
>
<div ng-messages="theForm['unitId_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-unit-id-range</div>
<div translate
ng-message="max"
>extension.modbus-unit-id-range</div>
</div>
</md-input-container>
</div>
<div flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.modbus-attributes-poll-period</label>
<input type="number"
required
name="attributesPollPeriod_{{serverIndex}}{{deviceIndex}}"
ng-model="device.attributesPollPeriod"
min="1"
>
<div ng-messages="theForm['attributesPollPeriod_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-poll-period-range</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.modbus-timeseries-poll-period</label>
<input type="number"
required
name="timeseriesPollPeriod_{{serverIndex}}{{deviceIndex}}"
ng-model="device.timeseriesPollPeriod"
min="1"
>
<div ng-messages="theForm['timeseriesPollPeriod_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-poll-period-range</div>
</div>
</md-input-container>
</div>
<v-accordion id="modbus-attributes-accordion"
class="vAccordion--default"
onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="modbus-attributes-pane_{{serverIndex}}{{deviceIndex}}">
<v-pane-header>
{{ 'extension.attributes' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-show="device.attributes.length > 0">
<ol class="list-group">
<li class="list-group-item"
ng-repeat="(attributeIndex, attribute) in device.attributes"
>
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(attribute, device.attributes)">
<ng-md-icon icon="close"
aria-label="{{ 'action.remove' | translate }}"
></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.modbus-tag</label>
<input required
name="modbusAttributeTag_{{serverIndex}}{{deviceIndex}}{{attributeIndex}}"
ng-model="attribute.tag"
>
<div ng-messages="theForm['modbusAttributeTag_' + serverIndex + deviceIndex + attributeIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="30" class="md-block tb-container-for-select">
<label translate>extension.type</label>
<md-select required name="modbusAttributeType_{{serverIndex}}{{deviceIndex}}{{attributeIndex}}"
ng-model="attribute.type"
>
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
ng-value="attrType"
>
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['modbusAttributeType_' + serverIndex + deviceIndex + attributeIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="20" class="md-block">
<label translate>extension.modbus-poll-period</label>
<input type="number"
name="pollPeriod_{{serverIndex}}{{deviceIndex}}"
ng-model="attribute.pollPeriod"
min="1"
>
<div ng-messages="theForm['pollPeriod_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="min"
>extension.modbus-poll-period-range</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="50" class="md-block tb-container-for-select">
<label translate>extension.type</label>
<md-select required name="modbusAttributeFunctionCode_{{serverIndex}}{{deviceIndex}}{{attributeIndex}}"
ng-model="attribute.functionCode"
>
<md-option ng-repeat="(functionCode, functionName) in types.extensionModbusFunctionCodes"
ng-value="functionCode"
>
{{functionName}}
</md-option>
</md-select>
<div ng-messages="theForm['modbusAttributeFunctionCode_' + serverIndex + deviceIndex + attributeIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.modbus-register-address</label>
<input type="number"
required
name="address_{{serverIndex}}{{deviceIndex}}"
ng-model="attribute.address"
min="0"
max="65535"
>
<div ng-messages="theForm['address_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-register-address-range</div>
<div translate
ng-message="max"
>extension.modbus-register-address-range</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="50" class="md-block" ng-if="attribute.type != 'boolean'">
<label translate>extension.modbus-register-count</label>
<input type="number"
name="registerCount_{{serverIndex}}{{deviceIndex}}"
ng-model="attribute.registerCount"
min="1"
>
<div ng-messages="theForm['registerCount_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="min"
>extension.modbus-register-count-range</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block" ng-if="attribute.type == 'boolean' && attribute.functionCode >= 3">
<label translate>extension.modbus-register-bit-index</label>
<input type="number"
required
name="bit_{{serverIndex}}{{deviceIndex}}"
ng-model="attribute.bit"
min="0"
max="15"
>
<div ng-messages="theForm['bit_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-register-bit-index-range</div>
<div translate
ng-message="max"
>extension.modbus-register-bit-index-range</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="100" class="md-block" ng-if="attribute.functionCode >= 3">
<label translate>extension.modbus-byte-order</label>
<input required
name="modbusByteOrder_{{serverIndex}}{{deviceIndex}}{{attributeIndex}}"
ng-model="attribute.byteOrder"
>
<div ng-messages="theForm['modbusByteOrder_' + serverIndex + deviceIndex + attributeIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</section>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addNewAttribute(device)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-attribute</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="modbus-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="modbus-timeseries-pane_{{serverIndex}}{{deviceIndex}}">
<v-pane-header>
{{ 'extension.timeseries' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-show="device.timeseries.length > 0">
<ol class="list-group">
<li class="list-group-item"
ng-repeat="(timeseriesIndex, timeserie) in device.timeseries"
>
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(timeserie, device.timeseries)">
<ng-md-icon icon="close"
aria-label="{{ 'action.remove' | translate }}"
></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.modbus-tag</label>
<input required
name="modbusTimeserieTag_{{serverIndex}}{{deviceIndex}}{{timeseriesIndex}}"
ng-model="timeserie.tag"
>
<div ng-messages="theForm['modbusTimeserieTag_' + serverIndex + deviceIndex + timeseriesIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="30" class="md-block tb-container-for-select">
<label translate>extension.type</label>
<md-select required name="modbusTimeserieType_{{serverIndex}}{{deviceIndex}}{{timeseriesIndex}}"
ng-model="timeserie.type"
>
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
ng-value="attrType"
>
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['modbusTimeserieType_' + serverIndex + deviceIndex + timeseriesIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="20" class="md-block">
<label translate>extension.modbus-poll-period</label>
<input type="number"
name="pollPeriod_{{serverIndex}}{{deviceIndex}}"
ng-model="timeserie.pollPeriod"
min="1"
>
<div ng-messages="theForm['pollPeriod_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="min"
>extension.modbus-poll-period-range</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="50" class="md-block tb-container-for-select">
<label translate>extension.type</label>
<md-select required name="modbusTimeserieFunctionCode_{{serverIndex}}{{deviceIndex}}{{timeseriesIndex}}"
ng-model="timeserie.functionCode"
>
<md-option ng-repeat="(functionCode, functionName) in types.extensionModbusFunctionCodes"
ng-value="functionCode"
>
{{functionName}}
</md-option>
</md-select>
<div ng-messages="theForm['modbusTimeserieFunctionCode_' + serverIndex + deviceIndex + timeseriesIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.modbus-register-address</label>
<input type="number"
required
name="address_{{serverIndex}}{{deviceIndex}}"
ng-model="timeserie.address"
min="0"
max="65535"
>
<div ng-messages="theForm['address_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-register-address-range</div>
<div translate
ng-message="max"
>extension.modbus-register-address-range</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="50" class="md-block" ng-if="timeserie.type != 'boolean'">
<label translate>extension.modbus-register-count</label>
<input type="number"
name="registerCount_{{serverIndex}}{{deviceIndex}}"
ng-model="timeserie.registerCount"
min="1"
>
<div ng-messages="theForm['registerCount_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="min"
>extension.modbus-register-count-range</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block" ng-if="timeserie.type == 'boolean' && timeserie.functionCode >= 3">
<label translate>extension.modbus-register-bit-index</label>
<input type="number"
required
name="bit_{{serverIndex}}{{deviceIndex}}"
ng-model="timeserie.bit"
min="0"
max="15"
>
<div ng-messages="theForm['bit_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-register-bit-index-range</div>
<div translate
ng-message="max"
>extension.modbus-register-bit-index-range</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="100" class="md-block" ng-if="timeserie.functionCode >= 3">
<label translate>extension.modbus-byte-order</label>
<input required
name="modbusByteOrder_{{serverIndex}}{{deviceIndex}}{{timeseriesIndex}}"
ng-model="timeserie.byteOrder"
>
<div ng-messages="theForm['modbusByteOrder_' + serverIndex + deviceIndex + timeseriesIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</section>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addNewTimeseries(device)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-timeseries</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex
layout="row"
layout-align="start center"
>
<md-button class="md-primary md-raised"
ng-click="addDevice(server.devices)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-device</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
<div flex
layout="row"
layout-align="start center"
>
<md-button class="md-primary md-raised"
ng-click="addServer(configuration.servers)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>extension.modbus-add-server</span>
</md-button>
</div>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<!--{{config}}-->
</md-card-content>
</md-card>

2
ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html

@ -194,7 +194,7 @@
</md-input-container>
</div>
<v-accordion id="opc-keystore-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-accordion id="opc-keystore-accordion" class="vAccordion--default" ng-if="server.security != 'None'" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="opc-keystore-pane__{{serverIndex}}" expanded="true">
<v-pane-header>
{{ 'extension.opc-keystore' | translate }}

3
ui/src/app/extension/index.js

@ -17,6 +17,8 @@ import ExtensionTableDirective from './extension-table.directive';
import ExtensionFormHttpDirective from './extensions-forms/extension-form-http.directive';
import ExtensionFormMqttDirective from './extensions-forms/extension-form-mqtt.directive'
import ExtensionFormOpcDirective from './extensions-forms/extension-form-opc.directive';
import ExtensionFormModbusDirective from './extensions-forms/extension-form-modbus.directive';
import {ParseToNull} from './extension-dialog.controller';
export default angular.module('thingsboard.extension', [])
@ -24,5 +26,6 @@ export default angular.module('thingsboard.extension', [])
.directive('tbExtensionFormHttp', ExtensionFormHttpDirective)
.directive('tbExtensionFormMqtt', ExtensionFormMqttDirective)
.directive('tbExtensionFormOpc', ExtensionFormOpcDirective)
.directive('tbExtensionFormModbus', ExtensionFormModbusDirective)
.directive('parseToNull', ParseToNull)
.name;

122
ui/src/app/import-export/import-export.service.js

@ -25,8 +25,7 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
/*@ngInject*/
export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types,
dashboardUtils, entityService, dashboardService, pluginService, ruleService,
ruleChainService, widgetService, toast, attributeService) {
dashboardUtils, entityService, dashboardService, ruleChainService, widgetService, toast, attributeService) {
var service = {
@ -34,10 +33,6 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
importDashboard: importDashboard,
exportWidget: exportWidget,
importWidget: importWidget,
exportPlugin: exportPlugin,
importPlugin: importPlugin,
exportRule: exportRule,
importRule: importRule,
exportRuleChain: exportRuleChain,
importRuleChain: importRuleChain,
exportWidgetType: exportWidgetType,
@ -221,62 +216,6 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return true;
}
// Rule functions
function exportRule(ruleId) {
ruleService.getRule(ruleId).then(
function success(rule) {
var name = rule.name;
name = name.toLowerCase().replace(/\W/g,"_");
exportToPc(prepareExport(rule), name + '.json');
},
function fail(rejection) {
var message = rejection;
if (!message) {
message = $translate.instant('error.unknown-error');
}
toast.showError($translate.instant('rule.export-failed-error', {error: message}));
}
);
}
function importRule($event) {
var deferred = $q.defer();
openImportDialog($event, 'rule.import', 'rule.rule-file').then(
function success(rule) {
if (!validateImportedRule(rule)) {
toast.showError($translate.instant('rule.invalid-rule-file-error'));
deferred.reject();
} else {
rule.state = 'SUSPENDED';
ruleService.saveRule(rule).then(
function success() {
deferred.resolve();
},
function fail() {
deferred.reject();
}
);
}
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function validateImportedRule(rule) {
if (angular.isUndefined(rule.name)
|| angular.isUndefined(rule.pluginToken)
|| angular.isUndefined(rule.filters)
|| angular.isUndefined(rule.action))
{
return false;
}
return true;
}
// Rule chain functions
function exportRuleChain(ruleChainId) {
@ -361,65 +300,6 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return true;
}
// Plugin functions
function exportPlugin(pluginId) {
pluginService.getPlugin(pluginId).then(
function success(plugin) {
if (!plugin.configuration || plugin.configuration === null) {
plugin.configuration = {};
}
var name = plugin.name;
name = name.toLowerCase().replace(/\W/g,"_");
exportToPc(prepareExport(plugin), name + '.json');
},
function fail(rejection) {
var message = rejection;
if (!message) {
message = $translate.instant('error.unknown-error');
}
toast.showError($translate.instant('plugin.export-failed-error', {error: message}));
}
);
}
function importPlugin($event) {
var deferred = $q.defer();
openImportDialog($event, 'plugin.import', 'plugin.plugin-file').then(
function success(plugin) {
if (!validateImportedPlugin(plugin)) {
toast.showError($translate.instant('plugin.invalid-plugin-file-error'));
deferred.reject();
} else {
plugin.state = 'SUSPENDED';
pluginService.savePlugin(plugin).then(
function success() {
deferred.resolve();
},
function fail() {
deferred.reject();
}
);
}
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function validateImportedPlugin(plugin) {
if (angular.isUndefined(plugin.name)
|| angular.isUndefined(plugin.clazz)
|| angular.isUndefined(plugin.apiToken)
|| angular.isUndefined(plugin.configuration))
{
return false;
}
return true;
}
// Widget functions
function exportWidget(dashboard, sourceState, sourceLayout, widget) {

4
ui/src/app/layout/index.js

@ -50,8 +50,6 @@ import thingsboardAsset from '../asset';
import thingsboardDevice from '../device';
import thingsboardWidgetLibrary from '../widget';
import thingsboardDashboard from '../dashboard';
import thingsboardPlugin from '../plugin';
import thingsboardRule from '../rule';
import thingsboardRuleChain from '../rulechain';
import thingsboardJsonForm from '../jsonform';
@ -83,8 +81,6 @@ export default angular.module('thingsboard.home', [
thingsboardDevice,
thingsboardWidgetLibrary,
thingsboardDashboard,
thingsboardPlugin,
thingsboardRule,
thingsboardRuleChain,
thingsboardJsonForm,
thingsboardApiDevice,

34
ui/src/app/locale/locale.constant.js

@ -886,8 +886,10 @@ export default angular.module('thingsboard.locale', [])
"response-timeout": "Response timeout in milliseconds",
"topic-expression": "Topic expression",
"client-scope": "Client scope",
"add-device": "Add device",
"opc-server": "Servers",
"opc-add-server": "Add server",
"opc-add-server-prompt": "Please add server",
"opc-application-name": "Application name",
"opc-application-uri": "Application uri",
"opc-scan-period-in-seconds": "Scan period in seconds",
@ -902,6 +904,34 @@ export default angular.module('thingsboard.locale', [])
"opc-keystore-key-password":"Key password",
"opc-device-node-pattern":"Device node pattern",
"opc-device-name-pattern":"Device name pattern",
"modbus-server": "Servers/slaves",
"modbus-add-server": "Add server/slave",
"modbus-add-server-prompt": "Please add server/slave",
"modbus-transport": "Transport",
"modbus-port-name": "Serial port name",
"modbus-encoding": "Encoding",
"modbus-parity": "Parity",
"modbus-baudrate": "Baud rate",
"modbus-databits": "Data bits",
"modbus-stopbits": "Stop bits",
"modbus-databits-range": "Data bits should be in a range from 7 to 8.",
"modbus-stopbits-range": "Stop bits should be in a range from 1 to 2.",
"modbus-unit-id": "Unit ID",
"modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.",
"modbus-device-name":"Device name",
"modbus-poll-period": "Poll period (ms)",
"modbus-attributes-poll-period": "Attributes poll period (ms)",
"modbus-timeseries-poll-period": "Timeseries poll period (ms)",
"modbus-poll-period-range": "Poll period should be positive value.",
"modbus-tag": "Tag",
"modbus-function": "Function",
"modbus-register-address": "Register address",
"modbus-register-address-range": "Register address should be in a range from 0 to 65535.",
"modbus-register-bit-index": "Bit index",
"modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.",
"modbus-register-count": "Register count",
"modbus-register-count-range": "Register count should be a positive value.",
"modbus-byte-order": "Byte order",
"sync": {
"status": "Status",
@ -1163,11 +1193,15 @@ export default angular.module('thingsboard.locale', [])
"rulechain": {
"rulechain": "Rule chain",
"rulechains": "Rule chains",
"root": "Root",
"delete": "Delete rule chain",
"name": "Name",
"name-required": "Name is required.",
"description": "Description",
"add": "Add Rule Chain",
"set-root": "Make rule chain root",
"set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?",
"set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.",
"delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?",
"delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.",
"delete-rulechains-title": "Are you sure you want to delete { count, select, 1 {1 rule chain} other {# rule chains} }?",

48
ui/src/app/plugin/add-plugin.tpl.html

@ -1,48 +0,0 @@
<!--
Copyright © 2016-2018 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.
-->
<md-dialog aria-label="{{ 'plugin.add' | translate }}" tb-help="vm.helpLinks.getPluginLink(vm.item)" help-container-id="help-container">
<form name="theForm" ng-submit="vm.add()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>plugin.add</h2>
<span flex></span>
<div id="help-container"></div>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
<md-dialog-content>
<div class="md-dialog-content">
<tb-plugin plugin="vm.item" is-edit="true" the-form="theForm"></tb-plugin>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
class="md-raised md-primary">
{{ 'action.add' | translate }}
</md-button>
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
translate }}
</md-button>
</md-dialog-actions>
</form>
</md-dialog>

36
ui/src/app/plugin/index.js

@ -1,36 +0,0 @@
/*
* Copyright © 2016-2018 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 uiRouter from 'angular-ui-router';
import thingsboardGrid from '../components/grid.directive';
import thingsboardJsonForm from '../components/json-form.directive';
import thingsboardApiPlugin from '../api/plugin.service';
import thingsboardApiComponentDescriptor from '../api/component-descriptor.service';
import PluginRoutes from './plugin.routes';
import PluginController from './plugin.controller';
import PluginDirective from './plugin.directive';
export default angular.module('thingsboard.plugin', [
uiRouter,
thingsboardGrid,
thingsboardJsonForm,
thingsboardApiPlugin,
thingsboardApiComponentDescriptor
])
.config(PluginRoutes)
.controller('PluginController', PluginController)
.directive('tbPlugin', PluginDirective)
.name;

19
ui/src/app/plugin/plugin-card.tpl.html

@ -1,19 +0,0 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-uppercase" ng-if="item && parentCtl.types.id.nullUid === item.tenantId.id" translate>plugin.system</div>
<div class="tb-uppercase" translate>{{item && item.state === 'ACTIVE' ? 'plugin.active' : 'plugin.suspended'}}</div>

90
ui/src/app/plugin/plugin-fieldset.tpl.html

@ -1,90 +0,0 @@
<!--
Copyright © 2016-2018 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.
-->
<md-button ng-click="onActivatePlugin({event: $event})"
ng-show="!isEdit && !isReadOnly && plugin.state === 'SUSPENDED'"
class="md-raised md-primary">{{ 'plugin.activate' | translate }}</md-button>
<md-button ng-click="onSuspendPlugin({event: $event})"
ng-show="!isEdit && !isReadOnly && plugin.state === 'ACTIVE'"
class="md-raised md-primary">{{ 'plugin.suspend' | translate }}</md-button>
<md-button ng-click="onExportPlugin({event: $event})"
ng-show="!isEdit"
class="md-raised md-primary">{{ 'plugin.export' | translate }}</md-button>
<md-button ng-click="onDeletePlugin({event: $event})"
ng-show="!isEdit && !isReadOnly"
class="md-raised md-primary">{{ 'plugin.delete' | translate }}</md-button>
<div layout="row">
<md-button ngclipboard data-clipboard-action="copy"
ngclipboard-success="onPluginIdCopied(e)"
data-clipboard-text="{{plugin.id.id}}" ng-show="!isEdit"
class="md-raised">
<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
<span translate>plugin.copyId</span>
</md-button>
</div>
<md-content class="md-padding" layout="column" style="overflow-x: hidden">
<fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
<md-input-container class="md-block">
<label translate>plugin.name</label>
<input required name="name" ng-model="plugin.name">
<div ng-messages="theForm.name.$error">
<div translate ng-message="required">plugin.name-required</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>plugin.description</label>
<textarea ng-model="plugin.additionalInfo.description" rows="2"></textarea>
</md-input-container>
<section flex layout="row">
<md-input-container flex class="md-block">
<label translate>plugin.api-token</label>
<input required name="apiToken" ng-model="plugin.apiToken">
<div ng-messages="theForm.apiToken.$error">
<div translate ng-message="required">plugin.api-token-required</div>
</div>
</md-input-container>
<md-input-container flex class="md-block">
<label translate>plugin.type</label>
<md-select required name="pluginType" ng-model="plugin.clazz" ng-disabled="$root.loading || !isEdit">
<md-option ng-repeat="component in pluginComponents" ng-value="component.clazz">
{{component.name}}
</md-option>
</md-select>
<div ng-messages="theForm.pluginType.$error">
<div translate ng-message="required">plugin.type-required</div>
</div>
</md-input-container>
</section>
<md-card flex class="plugin-config" ng-if="showPluginConfig">
<md-card-title>
<md-card-title-text>
<span translate class="md-headline" ng-class="{'tb-readonly-label' : ($root.loading || !isEdit || isReadOnly)}">plugin.configuration</span>
</md-card-title-text>
</md-card-title>
<md-card-content>
<tb-json-form schema="pluginComponent.configurationDescriptor.schema"
form="pluginComponent.configurationDescriptor.form"
model="pluginConfiguration.data"
readonly="$root.loading || !isEdit || isReadOnly"
form-control="theForm">
</tb-json-form>
</md-card-content>
</md-card>
</fieldset>
</md-content>

218
ui/src/app/plugin/plugin.controller.js

@ -1,218 +0,0 @@
/*
* Copyright © 2016-2018 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.
*/
/* eslint-disable import/no-unresolved, import/default */
import addPluginTemplate from './add-plugin.tpl.html';
import pluginCard from './plugin-card.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function PluginController(pluginService, userService, importExport, $state, $stateParams, $filter, $translate, types, helpLinks) {
var pluginActionsList = [
{
onAction: function ($event, item) {
exportPlugin($event, item);
},
name: function() { $translate.instant('action.export') },
details: function() { return $translate.instant('plugin.export') },
icon: "file_download"
},
{
onAction: function ($event, item) {
activatePlugin($event, item);
},
name: function() { return $translate.instant('action.activate') },
details: function() { return $translate.instant('plugin.activate') },
icon: "play_arrow",
isEnabled: function(plugin) {
return isPluginEditable(plugin) && plugin && plugin.state === 'SUSPENDED';
}
},
{
onAction: function ($event, item) {
suspendPlugin($event, item);
},
name: function() { return $translate.instant('action.suspend') },
details: function() { return $translate.instant('plugin.suspend') },
icon: "pause",
isEnabled: function(plugin) {
return isPluginEditable(plugin) && plugin && plugin.state === 'ACTIVE';
}
},
{
onAction: function ($event, item) {
vm.grid.deleteItem($event, item);
},
name: function() { return $translate.instant('action.delete') },
details: function() { return $translate.instant('plugin.delete') },
icon: "delete",
isEnabled: isPluginEditable
}
];
var pluginAddItemActionsList = [
{
onAction: function ($event) {
vm.grid.addItem($event);
},
name: function() { return $translate.instant('action.create') },
details: function() { return $translate.instant('plugin.create-new-plugin') },
icon: "insert_drive_file"
},
{
onAction: function ($event) {
importExport.importPlugin($event).then(
function() {
vm.grid.refreshList();
}
);
},
name: function() { return $translate.instant('action.import') },
details: function() { return $translate.instant('plugin.import') },
icon: "file_upload"
}
];
var vm = this;
vm.types = types;
vm.helpLinkIdForPlugin = helpLinkIdForPlugin;
vm.pluginGridConfig = {
refreshParamsFunc: null,
deleteItemTitleFunc: deletePluginTitle,
deleteItemContentFunc: deletePluginText,
deleteItemsTitleFunc: deletePluginsTitle,
deleteItemsActionTitleFunc: deletePluginsActionTitle,
deleteItemsContentFunc: deletePluginsText,
fetchItemsFunc: fetchPlugins,
saveItemFunc: savePlugin,
deleteItemFunc: deletePlugin,
getItemTitleFunc: getPluginTitle,
itemCardTemplateUrl: pluginCard,
parentCtl: vm,
actionsList: pluginActionsList,
addItemActions: pluginAddItemActionsList,
onGridInited: gridInited,
addItemTemplateUrl: addPluginTemplate,
addItemText: function() { return $translate.instant('plugin.add-plugin-text') },
noItemsText: function() { return $translate.instant('plugin.no-plugins-text') },
itemDetailsText: function() { return $translate.instant('plugin.plugin-details') },
isSelectionEnabled: isPluginEditable,
isDetailsReadOnly: function(plugin) {
return !isPluginEditable(plugin);
}
};
if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
vm.pluginGridConfig.items = $stateParams.items;
}
if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
vm.pluginGridConfig.topIndex = $stateParams.topIndex;
}
vm.isPluginEditable = isPluginEditable;
vm.activatePlugin = activatePlugin;
vm.suspendPlugin = suspendPlugin;
vm.exportPlugin = exportPlugin;
function helpLinkIdForPlugin() {
return helpLinks.getPluginLink(vm.grid.operatingItem());
}
function deletePluginTitle(plugin) {
return $translate.instant('plugin.delete-plugin-title', {pluginName: plugin.name});
}
function deletePluginText() {
return $translate.instant('plugin.delete-plugin-text');
}
function deletePluginsTitle(selectedCount) {
return $translate.instant('plugin.delete-plugins-title', {count: selectedCount}, 'messageformat');
}
function deletePluginsActionTitle(selectedCount) {
return $translate.instant('plugin.delete-plugins-action-title', {count: selectedCount}, 'messageformat');
}
function deletePluginsText() {
return $translate.instant('plugin.delete-plugins-text');
}
function gridInited(grid) {
vm.grid = grid;
}
function fetchPlugins(pageLink) {
return pluginService.getAllPlugins(pageLink);
}
function savePlugin(plugin) {
return pluginService.savePlugin(plugin);
}
function deletePlugin(pluginId) {
return pluginService.deletePlugin(pluginId);
}
function getPluginTitle(plugin) {
return plugin ? plugin.name : '';
}
function isPluginEditable(plugin) {
if (userService.getAuthority() === 'TENANT_ADMIN') {
return plugin && plugin.tenantId.id != types.id.nullUid;
} else {
return userService.getAuthority() === 'SYS_ADMIN';
}
}
function exportPlugin($event, plugin) {
$event.stopPropagation();
importExport.exportPlugin(plugin.id.id);
}
function activatePlugin(event, plugin) {
pluginService.activatePlugin(plugin.id.id).then(function () {
vm.grid.refreshList();
}, function () {
});
}
function suspendPlugin(event, plugin) {
pluginService.suspendPlugin(plugin.id.id).then(function () {
vm.grid.refreshList();
}, function () {
});
}
}

94
ui/src/app/plugin/plugin.directive.js

@ -1,94 +0,0 @@
/*
* Copyright © 2016-2018 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 './plugin.scss';
/* eslint-disable import/no-unresolved, import/default */
import pluginFieldsetTemplate from './plugin-fieldset.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function PluginDirective($compile, $templateCache, $translate, types, toast, utils, userService, componentDescriptorService) {
var linker = function (scope, element) {
var template = $templateCache.get(pluginFieldsetTemplate);
element.html(template);
scope.showPluginConfig = false;
scope.pluginConfiguration = {
data: null
};
if (scope.plugin && !scope.plugin.configuration) {
scope.plugin.configuration = {};
}
scope.$watch("plugin.clazz", function (newValue, prevValue) {
if (newValue != prevValue) {
scope.pluginConfiguration.data = null;
if (scope.plugin) {
componentDescriptorService.getComponentDescriptorByClazz(scope.plugin.clazz).then(
function success(component) {
scope.pluginComponent = component;
scope.showPluginConfig = !(userService.getAuthority() === 'TENANT_ADMIN'
&& scope.plugin.tenantId
&& scope.plugin.tenantId.id === types.id.nullUid)
&& utils.isDescriptorSchemaNotEmpty(scope.pluginComponent.configurationDescriptor);
scope.pluginConfiguration.data = angular.copy(scope.plugin.configuration);
},
function fail() {
}
);
}
}
});
scope.$watch("pluginConfiguration.data", function (newValue, prevValue) {
if (newValue && !angular.equals(newValue, prevValue)) {
scope.plugin.configuration = angular.copy(scope.pluginConfiguration.data);
}
}, true);
scope.onPluginIdCopied = function() {
toast.showSuccess($translate.instant('plugin.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
};
componentDescriptorService.getComponentDescriptorsByType(types.componentType.plugin).then(
function success(components) {
scope.pluginComponents = components;
},
function fail() {
}
);
$compile(element.contents())(scope);
}
return {
restrict: "E",
link: linker,
scope: {
plugin: '=',
isEdit: '=',
isReadOnly: '=',
theForm: '=',
onActivatePlugin: '&',
onSuspendPlugin: '&',
onExportPlugin: '&',
onDeletePlugin: '&'
}
};
}

46
ui/src/app/plugin/plugin.routes.js

@ -1,46 +0,0 @@
/*
* Copyright © 2016-2018 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.
*/
/* eslint-disable import/no-unresolved, import/default */
import pluginsTemplate from './plugins.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function PluginRoutes($stateProvider) {
$stateProvider
.state('home.plugins', {
url: '/plugins',
params: {'topIndex': 0},
module: 'private',
auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
views: {
"content@home": {
templateUrl: pluginsTemplate,
controllerAs: 'vm',
controller: 'PluginController'
}
},
data: {
searchEnabled: true,
pageTitle: 'plugin.plugins'
},
ncyBreadcrumb: {
label: '{"icon": "extension", "label": "plugin.plugins"}'
}
});
}

18
ui/src/app/plugin/plugin.scss

@ -1,18 +0,0 @@
/**
* Copyright © 2016-2018 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.
*/
.plugin-config {
min-width: 500px;
}

77
ui/src/app/plugin/plugins.tpl.html

@ -1,77 +0,0 @@
<!--
Copyright © 2016-2018 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.
-->
<tb-grid grid-configuration="vm.pluginGridConfig">
<details-buttons tb-help="vm.helpLinkIdForPlugin()" help-container-id="help-container">
<div id="help-container"></div>
</details-buttons>
<md-tabs ng-class="{'tb-headless': (vm.grid.detailsConfig.isDetailsEditMode || !vm.isPluginEditable(vm.grid.operatingItem()))}"
id="tabs" md-border-bottom flex class="tb-absolute-fill">
<md-tab label="{{ 'plugin.details' | translate }}">
<tb-plugin plugin="vm.grid.operatingItem()"
is-edit="vm.grid.detailsConfig.isDetailsEditMode"
is-read-only="vm.grid.isDetailsReadOnly(vm.grid.operatingItem())"
the-form="vm.grid.detailsForm"
on-activate-plugin="vm.activatePlugin(event, vm.grid.detailsConfig.currentItem)"
on-suspend-plugin="vm.suspendPlugin(event, vm.grid.detailsConfig.currentItem)"
on-export-plugin="vm.exportPlugin(event, vm.grid.detailsConfig.currentItem)"
on-delete-plugin="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-plugin>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.plugin}}"
entity-name="vm.grid.operatingItem().name"
default-attribute-scope="{{vm.types.attributesScope.server.value}}">
</tb-attribute-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.plugin}}"
entity-name="vm.grid.operatingItem().name"
default-attribute-scope="{{vm.types.latestTelemetry.value}}"
disable-attribute-scope-selection="true">
</tb-attribute-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
<tb-alarm-table flex entity-type="vm.types.entityType.plugin"
entity-id="vm.grid.operatingItem().id.id">
</tb-alarm-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'plugin.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.plugin"
entity-id="vm.grid.operatingItem().id.id"
tenant-id="vm.grid.operatingItem().tenantId.id"
default-event-type="{{vm.types.eventType.lcEvent.value}}">
</tb-event-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
<tb-relation-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.plugin}}">
</tb-relation-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table flex entity-type="vm.types.entityType.plugin"
entity-id="vm.grid.operatingItem().id.id"
audit-log-mode="{{vm.types.auditLogMode.entity}}">
</tb-audit-log-table>
</md-tab>
</md-tabs>
</tb-grid>

48
ui/src/app/rule/add-rule.tpl.html

@ -1,48 +0,0 @@
<!--
Copyright © 2016-2018 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.
-->
<md-dialog aria-label="{{ 'rule.add' | translate }}" tb-help="'rules'" help-container-id="help-container">
<form name="theForm" ng-submit="vm.add()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>rule.add</h2>
<span flex></span>
<div id="help-container"></div>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
<md-dialog-content>
<div class="md-dialog-content">
<tb-rule rule="vm.item" is-edit="true" the-form="theForm"></tb-rule>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
class="md-raised md-primary">
{{ 'action.add' | translate }}
</md-button>
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
translate }}
</md-button>
</md-dialog-actions>
</form>
</md-dialog>

40
ui/src/app/rule/index.js

@ -1,40 +0,0 @@
/*
* Copyright © 2016-2018 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 uiRouter from 'angular-ui-router';
import thingsboardGrid from '../components/grid.directive';
import thingsboardPluginSelect from '../components/plugin-select.directive';
import thingsboardComponent from '../component';
import thingsboardApiRule from '../api/rule.service';
import thingsboardApiPlugin from '../api/plugin.service';
import thingsboardApiComponentDescriptor from '../api/component-descriptor.service';
import RuleRoutes from './rule.routes';
import RuleController from './rule.controller';
import RuleDirective from './rule.directive';
export default angular.module('thingsboard.rule', [
uiRouter,
thingsboardGrid,
thingsboardPluginSelect,
thingsboardComponent,
thingsboardApiRule,
thingsboardApiPlugin,
thingsboardApiComponentDescriptor
])
.config(RuleRoutes)
.controller('RuleController', RuleController)
.directive('tbRule', RuleDirective)
.name;

19
ui/src/app/rule/rule-card.tpl.html

@ -1,19 +0,0 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-uppercase" ng-if="item && parentCtl.types.id.nullUid === item.tenantId.id" translate>rule.system</div>
<div class="tb-uppercase" translate>{{item && item.state === 'ACTIVE' ? 'rule.active' : 'rule.suspended'}}</div>

219
ui/src/app/rule/rule-fieldset.tpl.html

@ -1,219 +0,0 @@
<!--
Copyright © 2016-2018 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.
-->
<md-button ng-click="onActivateRule({event: $event})"
ng-show="!isEdit && !isReadOnly && rule.state === 'SUSPENDED'"
class="md-raised md-primary">{{ 'rule.activate' | translate }}</md-button>
<md-button ng-click="onSuspendRule({event: $event})"
ng-show="!isEdit && !isReadOnly && rule.state === 'ACTIVE'"
class="md-raised md-primary">{{ 'rule.suspend' | translate }}</md-button>
<md-button ng-click="onExportRule({event: $event})"
ng-show="!isEdit"
class="md-raised md-primary">{{ 'rule.export' | translate }}</md-button>
<md-button ng-click="onDeleteRule({event: $event})"
ng-show="!isEdit && !isReadOnly"
class="md-raised md-primary">{{ 'rule.delete' | translate }}</md-button>
<div layout="row">
<md-button ngclipboard data-clipboard-action="copy"
ngclipboard-success="onRuleIdCopied(e)"
data-clipboard-text="{{rule.id.id}}" ng-show="!isEdit"
class="md-raised">
<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
<span translate>rule.copyId</span>
</md-button>
</div>
<md-content class="md-padding tb-rule" layout="column">
<fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
<md-input-container class="md-block">
<label translate>rule.name</label>
<input required name="name" ng-model="rule.name">
<div ng-messages="theForm.name.$error">
<div translate ng-message="required">rule.name-required</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>rule.description</label>
<textarea ng-model="rule.additionalInfo.description" rows="2"></textarea>
</md-input-container>
</fieldset>
<v-accordion id="filters-accordion" class="vAccordion--default"
ng-class="{'tb-readonly-label' : (!isEdit || isReadOnly)}">
<v-pane id="filters-pane" expanded="true">
<v-pane-header>
{{ 'rule.filters' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="rule.filters.length === 0">
<span translate layout-align="center center"
class="tb-prompt">rule.add-filter-prompt</span>
</div>
<div ng-if="rule.filters.length > 0">
<div flex layout="row" layout-align="start center">
<span ng-if="isEdit && !isReadOnly" style="min-width: 40px; margin: 0 6px;"></br></span>
<span flex="5"></span>
<div flex layout="row" layout-align="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<span translate flex="50">rule.filter-name</span>
<span translate flex="50">rule.filter-type</span>
<span style="min-width: 80px; margin: 0 12px;"></br></span>
</div>
</div>
<div class="tb-filters" style="max-height: 300px; overflow: auto; padding-bottom: 15px;">
<ul dnd-list="filters" dnd-disable-if="!isEdit || isReadOnly">
<li ng-repeat="filter in filters"
dnd-draggable="filter"
dnd-moved="filters.splice($index, 1)"
dnd-disable-if="!isEdit || isReadOnly"
dnd-effect-allowed="move">
<div flex layout="row" layout-align="start center">
<md-button ng-if="isEdit && !isReadOnly" dnd-handle class="md-icon-button md-primary handle"
style="min-width: 40px;"
aria-label="{{ 'action.drag' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.drag' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.drag' | translate }}"
class="material-icons">
drag_handle
</md-icon>
</md-button>
<dnd-nodrag flex layout="row" layout-align="start center">
<span flex="5">{{$index + 1}}.</span>
<tb-component flex
component="filter.value"
type="types.componentType.filter"
title="rule.filter"
read-only="!isEdit || isReadOnly"
on-remove-component="removeFilter(event, filter)">
</tb-component>
</dnd-nodrag>
</div>
</li>
</ul>
</div>
</div>
<div ng-if="isEdit && !isReadOnly" flex layout="row" layout-align="start center">
<md-button ng-disabled="$root.loading" class="md-primary md-raised"
ng-click="addFilter($event)" aria-label="{{ 'action.add' | translate }}">
<md-tooltip md-direction="top">
{{ 'rule.add-filter' | translate }}
</md-tooltip>
<md-icon class="material-icons">add</md-icon>
<span translate>action.add</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="processor-accordion" class="vAccordion--default"
ng-class="{'tb-readonly-label' : (!isEdit || isReadOnly)}">
<v-pane id="processor-pane" expanded="true">
<v-pane-header>
{{ 'rule.processor' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="rule.processor && rule.processor != null">
<div flex layout="row" layout-align="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<span translate flex="50">rule.processor-name</span>
<span translate flex="50">rule.processor-type</span>
<span style="min-width: 80px; margin: 0 12px;"></br></span>
</div>
<div flex layout="row" layout-align="start center" style="padding-bottom: 15px;">
<tb-component flex
component="rule.processor"
type="types.componentType.processor"
title="rule.processor"
read-only="!isEdit || isReadOnly"
on-remove-component="removeProcessor(event)">
</tb-component>
</div>
</div>
<div ng-if="!rule.processor || rule.processor == null">
<span ng-if="!isEdit || isReadOnly" translate layout-align="center center"
class="tb-prompt">rule.no-processor-configured</span>
<div ng-if="isEdit && !isReadOnly" flex layout="row" layout-align="start center">
<md-button ng-disabled="$root.loading" class="md-primary md-raised"
ng-click="addProcessor($event)" aria-label="{{ 'action.create' | translate }}">
<md-tooltip md-direction="top">
{{ 'rule.create-processor' | translate }}
</md-tooltip>
<md-icon class="material-icons">add</md-icon>
<span translate>action.create</span>
</md-button>
</div>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
<md-input-container ng-if="!isEdit || isReadOnly" flex class="md-block">
<label translate>plugin.plugin</label>
<input name="name" ng-model="plugin.name">
</md-input-container>
<tb-plugin-select ng-show="isEdit && !isReadOnly" flex
ng-model="plugin"
tb-required="false"
the-form="theForm"
plugins-scope="action">
</tb-plugin-select>
</fieldset>
<v-accordion ng-if="plugin != null" id="plugin-action-accordion" class="vAccordion--default"
ng-class="{'tb-readonly-label' : (!isEdit || isReadOnly)}">
<v-pane id="plugin-action-pane" expanded="true">
<v-pane-header>
{{ 'rule.plugin-action' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="rule.action && rule.action != null">
<div flex layout="row" layout-align="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<span translate flex="50">rule.action-name</span>
<span translate flex="50">rule.action-type</span>
<span style="min-width: 80px; margin: 0 12px;"></br></span>
</div>
<div flex layout="row" layout-align="start center" style="padding-bottom: 15px;">
<tb-component flex
component="rule.action"
type="types.componentType.action"
plugin-clazz="plugin.clazz"
title="rule.plugin-action"
read-only="!isEdit || isReadOnly"
on-remove-component="removeAction(event)">
</tb-component>
</div>
</div>
<div ng-if="!rule.action || rule.action == null">
<span translate layout-align="center center"
class="tb-prompt">rule.create-action-prompt</span>
<div ng-if="isEdit && !isReadOnly" flex layout="row" layout-align="start center">
<md-button ng-disabled="$root.loading" class="md-primary md-raised"
ng-click="addAction($event)" aria-label="{{ 'action.create' | translate }}">
<md-tooltip md-direction="top">
{{ 'rule.create-action' | translate }}
</md-tooltip>
<md-icon class="material-icons">add</md-icon>
<span translate>action.create</span>
</md-button>
</div>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-content>

210
ui/src/app/rule/rule.controller.js

@ -1,210 +0,0 @@
/*
* Copyright © 2016-2018 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.
*/
/* eslint-disable import/no-unresolved, import/default */
import addRuleTemplate from './add-rule.tpl.html';
import ruleCard from './rule-card.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function RuleController(ruleService, userService, importExport, $state, $stateParams, $filter, $translate, types) {
var ruleActionsList = [
{
onAction: function ($event, item) {
exportRule($event, item);
},
name: function() { $translate.instant('action.export') },
details: function() { return $translate.instant('rule.export') },
icon: "file_download"
},
{
onAction: function ($event, item) {
activateRule($event, item);
},
name: function() { return $translate.instant('action.activate') },
details: function() { return $translate.instant('rule.activate') },
icon: "play_arrow",
isEnabled: function(rule) {
return isRuleEditable(rule) && rule && rule.state === 'SUSPENDED';
}
},
{
onAction: function ($event, item) {
suspendRule($event, item);
},
name: function() { return $translate.instant('action.suspend') },
details: function() { return $translate.instant('rule.suspend') },
icon: "pause",
isEnabled: function(rule) {
return isRuleEditable(rule) && rule.state === 'ACTIVE';
}
},
{
onAction: function ($event, item) {
vm.grid.deleteItem($event, item);
},
name: function() { return $translate.instant('action.delete') },
details: function() { return $translate.instant('rule.delete') },
icon: "delete",
isEnabled: isRuleEditable
}
];
var ruleAddItemActionsList = [
{
onAction: function ($event) {
vm.grid.addItem($event);
},
name: function() { return $translate.instant('action.create') },
details: function() { return $translate.instant('rule.create-new-rule') },
icon: "insert_drive_file"
},
{
onAction: function ($event) {
importExport.importRule($event).then(
function() {
vm.grid.refreshList();
}
);
},
name: function() { return $translate.instant('action.import') },
details: function() { return $translate.instant('rule.import') },
icon: "file_upload"
}
];
var vm = this;
vm.types = types;
vm.ruleGridConfig = {
refreshParamsFunc: null,
deleteItemTitleFunc: deleteRuleTitle,
deleteItemContentFunc: deleteRuleText,
deleteItemsTitleFunc: deleteRulesTitle,
deleteItemsActionTitleFunc: deleteRulesActionTitle,
deleteItemsContentFunc: deleteRulesText,
fetchItemsFunc: fetchRules,
saveItemFunc: saveRule,
deleteItemFunc: deleteRule,
getItemTitleFunc: getRuleTitle,
itemCardTemplateUrl: ruleCard,
parentCtl: vm,
actionsList: ruleActionsList,
addItemActions: ruleAddItemActionsList,
onGridInited: gridInited,
addItemTemplateUrl: addRuleTemplate,
addItemText: function() { return $translate.instant('rule.add-rule-text') },
noItemsText: function() { return $translate.instant('rule.no-rules-text') },
itemDetailsText: function() { return $translate.instant('rule.rule-details') },
isSelectionEnabled: isRuleEditable,
isDetailsReadOnly: function(rule) {
return !isRuleEditable(rule);
}
};
if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
vm.ruleGridConfig.items = $stateParams.items;
}
if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
vm.ruleGridConfig.topIndex = $stateParams.topIndex;
}
vm.isRuleEditable = isRuleEditable;
vm.activateRule = activateRule;
vm.suspendRule = suspendRule;
vm.exportRule = exportRule;
function deleteRuleTitle(rule) {
return $translate.instant('rule.delete-rule-title', {ruleName: rule.name});
}
function deleteRuleText() {
return $translate.instant('rule.delete-rule-text');
}
function deleteRulesTitle(selectedCount) {
return $translate.instant('rule.delete-rules-title', {count: selectedCount}, 'messageformat');
}
function deleteRulesActionTitle(selectedCount) {
return $translate.instant('rule.delete-rules-action-title', {count: selectedCount}, 'messageformat');
}
function deleteRulesText() {
return $translate.instant('rule.delete-rules-text');
}
function gridInited(grid) {
vm.grid = grid;
}
function fetchRules(pageLink) {
return ruleService.getAllRules(pageLink);
}
function saveRule(rule) {
return ruleService.saveRule(rule);
}
function deleteRule(ruleId) {
return ruleService.deleteRule(ruleId);
}
function getRuleTitle(rule) {
return rule ? rule.name : '';
}
function isRuleEditable(rule) {
if (userService.getAuthority() === 'TENANT_ADMIN') {
return rule && rule.tenantId.id != types.id.nullUid;
} else {
return userService.getAuthority() === 'SYS_ADMIN';
}
}
function exportRule($event, rule) {
$event.stopPropagation();
importExport.exportRule(rule.id.id);
}
function activateRule(event, rule) {
ruleService.activateRule(rule.id.id).then(function () {
vm.grid.refreshList();
}, function () {
});
}
function suspendRule(event, rule) {
ruleService.suspendRule(rule.id.id).then(function () {
vm.grid.refreshList();
}, function () {
});
}
}

191
ui/src/app/rule/rule.directive.js

@ -1,191 +0,0 @@
/*
* Copyright © 2016-2018 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 './rule.scss';
/* eslint-disable import/no-unresolved, import/default */
import ruleFieldsetTemplate from './rule-fieldset.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function RuleDirective($compile, $templateCache, $mdDialog, $document, $q, $translate, pluginService,
componentDialogService, componentDescriptorService, types, toast) {
var linker = function (scope, element) {
var template = $templateCache.get(ruleFieldsetTemplate);
element.html(template);
scope.plugin = null;
scope.types = types;
scope.filters = [];
scope.addFilter = function($event) {
componentDialogService.openComponentDialog($event, true, false,
'rule.filter', types.componentType.filter).then(
function success(filter) {
scope.filters.push({ value: filter });
},
function fail() {}
);
}
scope.removeFilter = function ($event, filter) {
var index = scope.filters.indexOf(filter);
if (index > -1) {
scope.filters.splice(index, 1);
}
};
scope.addProcessor = function($event) {
componentDialogService.openComponentDialog($event, true, false,
'rule.processor', types.componentType.processor).then(
function success(processor) {
scope.rule.processor = processor;
},
function fail() {}
);
}
scope.removeProcessor = function() {
if (scope.rule.processor) {
scope.rule.processor = null;
}
}
scope.addAction = function($event) {
componentDialogService.openComponentDialog($event, true, false,
'rule.plugin-action', types.componentType.action, scope.plugin.clazz).then(
function success(action) {
scope.rule.action = action;
},
function fail() {}
);
}
scope.removeAction = function() {
if (scope.rule.action) {
scope.rule.action = null;
}
}
scope.updateValidity = function () {
if (scope.rule) {
var valid = scope.rule.filters && scope.rule.filters.length > 0;
scope.theForm.$setValidity('filters', valid);
var processorDefined = angular.isDefined(scope.rule.processor) && scope.rule.processor != null;
var pluginDefined = angular.isDefined(scope.rule.pluginToken) && scope.rule.pluginToken != null;
var pluginActionDefined = angular.isDefined(scope.rule.action) && scope.rule.action != null;
valid = processorDefined && !pluginDefined || (pluginDefined && pluginActionDefined);
scope.theForm.$setValidity('processorOrPlugin', valid);
}
};
scope.onRuleIdCopied = function() {
toast.showSuccess($translate.instant('rule.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
};
scope.$watch('rule', function(newVal, prevVal) {
if (newVal) {
if (!scope.rule.filters) {
scope.rule.filters = [];
}
if (!angular.equals(newVal, prevVal)) {
if (scope.rule.pluginToken) {
pluginService.getPluginByToken(scope.rule.pluginToken).then(
function success(plugin) {
scope.plugin = plugin;
},
function fail() {}
);
} else {
scope.plugin = null;
}
if (scope.filters) {
scope.filters.splice(0, scope.filters.length);
} else {
scope.filters = [];
}
if (scope.rule.filters) {
for (var i in scope.rule.filters) {
scope.filters.push({value: scope.rule.filters[i]});
}
}
}
scope.updateValidity();
}
}
);
scope.$watch('filters', function (newVal, prevVal) {
if (scope.rule && scope.isEdit && !angular.equals(newVal, prevVal)) {
if (scope.rule.filters) {
scope.rule.filters.splice(0, scope.rule.filters.length);
} else {
scope.rule.filters = [];
}
if (scope.filters) {
for (var i in scope.filters) {
scope.rule.filters.push(scope.filters[i].value);
}
}
scope.theForm.$setDirty();
scope.updateValidity();
}
}, true);
scope.$watch('plugin', function(newVal, prevVal) {
if (scope.rule && scope.isEdit && !angular.equals(newVal, prevVal)) {
if (newVal) {
scope.rule.pluginToken = scope.plugin.apiToken;
} else {
scope.rule.pluginToken = null;
}
scope.rule.action = null;
scope.updateValidity();
}
}, true);
scope.$watch('rule.processor', function(newVal, prevVal) {
if (scope.rule && scope.isEdit && !angular.equals(newVal, prevVal)) {
scope.theForm.$setDirty();
scope.updateValidity();
}
}, true);
scope.$watch('rule.action', function(newVal, prevVal) {
if (scope.rule && scope.isEdit && !angular.equals(newVal, prevVal)) {
scope.theForm.$setDirty();
scope.updateValidity();
}
}, true);
$compile(element.contents())(scope);
}
return {
restrict: "E",
link: linker,
scope: {
rule: '=',
isEdit: '=',
isReadOnly: '=',
theForm: '=',
onActivateRule: '&',
onSuspendRule: '&',
onExportRule: '&',
onDeleteRule: '&'
}
};
}

46
ui/src/app/rule/rule.routes.js

@ -1,46 +0,0 @@
/*
* Copyright © 2016-2018 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.
*/
/* eslint-disable import/no-unresolved, import/default */
import rulesTemplate from './rules.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function RuleRoutes($stateProvider) {
$stateProvider
.state('home.rules', {
url: '/rules',
params: {'topIndex': 0},
module: 'private',
auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
views: {
"content@home": {
templateUrl: rulesTemplate,
controllerAs: 'vm',
controller: 'RuleController'
}
},
data: {
searchEnabled: true,
pageTitle: 'rule.rules'
},
ncyBreadcrumb: {
label: '{"icon": "settings_ethernet", "label": "rule.rules"}'
}
});
}

55
ui/src/app/rule/rule.scss

@ -1,55 +0,0 @@
/**
* Copyright © 2016-2018 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.
*/
.tb-rule {
min-width: 600px;
tb-plugin-select {
padding: 2px;
margin: 18px 0;
}
}
.tb-filter {
min-width: 500px;
min-height: 300px;
}
.tb-filters ul[dnd-list],
.tb-filters ul[dnd-list] > li {
position: relative;
}
.tb-filters ul[dnd-list] {
min-height: 42px;
padding-left: 0px;
}
.tb-filters ul[dnd-list] .dndDraggingSource {
display: none;
}
.tb-filters ul[dnd-list] .dndPlaceholder {
display: block;
background-color: #ddd;
min-height: 42px;
}
.tb-filters ul[dnd-list] li {
display: block;
}
.tb-filters .handle {
cursor: move;
}

77
ui/src/app/rule/rules.tpl.html

@ -1,77 +0,0 @@
<!--
Copyright © 2016-2018 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.
-->
<tb-grid grid-configuration="vm.ruleGridConfig">
<details-buttons tb-help="'rules'" help-container-id="help-container">
<div id="help-container"></div>
</details-buttons>
<md-tabs ng-class="{'tb-headless': (vm.grid.detailsConfig.isDetailsEditMode || !vm.isRuleEditable(vm.grid.operatingItem()))}"
id="tabs" md-border-bottom flex class="tb-absolute-fill">
<md-tab label="{{ 'rule.details' | translate }}">
<tb-rule rule="vm.grid.operatingItem()"
is-edit="vm.grid.detailsConfig.isDetailsEditMode"
is-read-only="vm.grid.isDetailsReadOnly(vm.grid.operatingItem())"
the-form="vm.grid.detailsForm"
on-activate-rule="vm.activateRule(event, vm.grid.detailsConfig.currentItem)"
on-suspend-rule="vm.suspendRule(event, vm.grid.detailsConfig.currentItem)"
on-export-rule="vm.exportRule(event, vm.grid.detailsConfig.currentItem)"
on-delete-rule="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-rule>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.rule}}"
entity-name="vm.grid.operatingItem().name"
default-attribute-scope="{{vm.types.attributesScope.server.value}}">
</tb-attribute-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.rule}}"
entity-name="vm.grid.operatingItem().name"
default-attribute-scope="{{vm.types.latestTelemetry.value}}"
disable-attribute-scope-selection="true">
</tb-attribute-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
<tb-alarm-table flex entity-type="vm.types.entityType.rule"
entity-id="vm.grid.operatingItem().id.id">
</tb-alarm-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'rule.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.rule"
entity-id="vm.grid.operatingItem().id.id"
tenant-id="vm.grid.operatingItem().tenantId.id"
default-event-type="{{vm.types.eventType.lcEvent.value}}">
</tb-event-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
<tb-relation-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.rule}}">
</tb-relation-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table flex entity-type="vm.types.entityType.rule"
entity-id="vm.grid.operatingItem().id.id"
audit-log-mode="{{vm.types.auditLogMode.entity}}">
</tb-audit-log-table>
</md-tab>
</md-tabs>
</tb-grid>

2
ui/src/app/rulechain/rulechain-card.tpl.html

@ -15,4 +15,4 @@
limitations under the License.
-->
<div class="tb-uppercase" ng-if="item && parentCtl.types.id.nullUid === item.tenantId.id" translate>rulechain.system</div>
<div ng-if="item && item.root" translate>rulechain.root</div>

5
ui/src/app/rulechain/rulechain-fieldset.tpl.html

@ -18,8 +18,11 @@
<md-button ng-click="onExportRuleChain({event: $event})"
ng-show="!isEdit"
class="md-raised md-primary">{{ 'rulechain.export' | translate }}</md-button>
<md-button ng-click="onSetRootRuleChain({event: $event})"
ng-show="!isEdit && !ruleChain.root"
class="md-raised md-primary">{{ 'rulechain.set-root' | translate }}</md-button>
<md-button ng-click="onDeleteRuleChain({event: $event})"
ng-show="!isEdit && !isReadOnly"
ng-show="!isEdit && !ruleChain.root"
class="md-raised md-primary">{{ 'rulechain.delete' | translate }}</md-button>
<div layout="row">

6
ui/src/app/rulechain/rulechain.controller.js

@ -598,9 +598,15 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
edgeDoubleClick: function (event, edge) {
openLinkDetails(edge);
},
edgeEdit: function(event, edge) {
openLinkDetails(edge);
},
nodeCallbacks: {
'doubleClick': function (event, node) {
openNodeDetails(node);
},
'nodeEdit': function (event, node) {
openNodeDetails(node);
}
},
isValidEdge: function (source, destination) {

1
ui/src/app/rulechain/rulechain.directive.js

@ -40,6 +40,7 @@ export default function RuleChainDirective($compile, $templateCache, $mdDialog,
isEdit: '=',
isReadOnly: '=',
theForm: '=',
onSetRootRuleChain: '&',
onExportRuleChain: '&',
onDeleteRuleChain: '&'
}

2
ui/src/app/rulechain/rulechain.routes.js

@ -81,7 +81,7 @@ export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider
pageTitle: 'rulechain.rulechain'
},
ncyBreadcrumb: {
label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name }}", "translate": "false"}'
label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name + (vm.ruleChain.root ? (\' (\' + (\'rulechain.root\' | translate) + \')\') : \'\') }}", "translate": "false"}'
}
}).state('home.ruleChains.importRuleChain', {
url: '/ruleChain/import',

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save