Browse Source

Merge with origin

pull/1166/head
Andrew Shvayka 8 years ago
parent
commit
99ca18e0e7
  1. 3
      application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
  2. 14
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  3. 79
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  4. 6
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  5. 35
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  6. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  7. 13
      dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheConfiguration.java
  8. 7
      dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
  9. 3
      dao/src/main/java/org/thingsboard/server/dao/entityview/CassandraEntityViewDao.java
  10. 45
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
  11. 20
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  12. 4
      dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
  13. 51
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
  14. 2
      msa/docker/.env
  15. 3
      msa/docker/docker-compose.yml
  16. 129
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java
  17. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java
  18. 27
      ui/src/app/api/entity-view.service.js
  19. 4
      ui/src/app/api/entity.service.js
  20. 2
      ui/src/app/entity-view/add-entity-view.tpl.html
  21. 191
      ui/src/app/entity-view/entity-view-fieldset.tpl.html
  22. 28
      ui/src/app/entity-view/entity-view.directive.js
  23. 47
      ui/src/app/entity-view/entity-view.scss
  24. 14
      ui/src/app/locale/locale.constant-en_US.json
  25. 2
      ui/src/app/locale/locale.constant-es_ES.json

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

@ -80,7 +80,8 @@ public abstract class ComponentMsgProcessor<T extends EntityId> extends Abstract
protected void checkActive() { protected void checkActive() {
if (state != ComponentLifecycleState.ACTIVE) { if (state != ComponentLifecycleState.ACTIVE) {
throw new IllegalStateException("Rule chain is not active!"); logger.warning("Rule chain is not active. Current state [{}] for processor [{}] tenant [{}]", state, tenantId, entityId);
throw new IllegalStateException("Rule chain is not active! " + entityId + " - " + tenantId);
} }
} }

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

@ -51,6 +51,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg; import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.dashboard.DashboardService;
@ -70,6 +71,7 @@ import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import javax.mail.MessagingException; import javax.mail.MessagingException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -143,6 +145,12 @@ public abstract class BaseController {
@Autowired @Autowired
protected EntityViewService entityViewService; protected EntityViewService entityViewService;
@Autowired
protected TelemetrySubscriptionService tsSubService;
@Autowired
protected AttributesService attributesService;
@ExceptionHandler(ThingsboardException.class) @ExceptionHandler(ThingsboardException.class)
public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) { public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
errorResponseHandler.handle(ex, response); errorResponseHandler.handle(ex, response);
@ -605,6 +613,12 @@ public abstract class BaseController {
case ATTRIBUTES_DELETED: case ATTRIBUTES_DELETED:
msgType = DataConstants.ATTRIBUTES_DELETED; msgType = DataConstants.ATTRIBUTES_DELETED;
break; break;
case ALARM_ACK:
msgType = DataConstants.ALARM_ACK;
break;
case ALARM_CLEAR:
msgType = DataConstants.ALARM_CLEAR;
break;
} }
if (!StringUtils.isEmpty(msgType)) { if (!StringUtils.isEmpty(msgType)) {
try { try {

79
application/src/main/java/org/thingsboard/server/controller/EntityViewController.java

@ -15,7 +15,10 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -26,7 +29,9 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
@ -34,15 +39,24 @@ import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.controller.CustomerController.CUSTOMER_ID; import static org.thingsboard.server.controller.CustomerController.CUSTOMER_ID;
@ -52,6 +66,7 @@ import static org.thingsboard.server.controller.CustomerController.CUSTOMER_ID;
*/ */
@RestController @RestController
@RequestMapping("/api") @RequestMapping("/api")
@Slf4j
public class EntityViewController extends BaseController { public class EntityViewController extends BaseController {
public static final String ENTITY_VIEW_ID = "entityViewId"; public static final String ENTITY_VIEW_ID = "entityViewId";
@ -75,6 +90,20 @@ public class EntityViewController extends BaseController {
try { try {
entityView.setTenantId(getCurrentUser().getTenantId()); entityView.setTenantId(getCurrentUser().getTenantId());
EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView)); EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView));
List<ListenableFuture<List<Void>>> futures = new ArrayList<>();
if (savedEntityView.getKeys() != null && savedEntityView.getKeys().getAttributes() != null) {
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.CLIENT_SCOPE, savedEntityView.getKeys().getAttributes().getCs(), getCurrentUser()));
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SERVER_SCOPE, savedEntityView.getKeys().getAttributes().getSs(), getCurrentUser()));
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SHARED_SCOPE, savedEntityView.getKeys().getAttributes().getSh(), getCurrentUser()));
}
for (ListenableFuture<List<Void>> future : futures) {
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException("Failed to copy attributes to entity view", e);
}
}
logEntityAction(savedEntityView.getId(), savedEntityView, null, logEntityAction(savedEntityView.getId(), savedEntityView, null,
entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
return savedEntityView; return savedEntityView;
@ -85,6 +114,56 @@ public class EntityViewController extends BaseController {
} }
} }
private ListenableFuture<List<Void>> copyAttributesFromEntityToEntityView(EntityView entityView, String scope, Collection<String> keys, SecurityUser user) throws ThingsboardException {
EntityViewId entityId = entityView.getId();
if (keys != null && !keys.isEmpty()) {
ListenableFuture<List<AttributeKvEntry>> getAttrFuture = attributesService.find(entityView.getEntityId(), scope, keys);
return Futures.transform(getAttrFuture, attributeKvEntries -> {
List<AttributeKvEntry> attributes;
if (attributeKvEntries != null && !attributeKvEntries.isEmpty()) {
attributes =
attributeKvEntries.stream()
.filter(attributeKvEntry -> {
long startTime = entityView.getStartTimeMs();
long endTime = entityView.getEndTimeMs();
long lastUpdateTs = attributeKvEntry.getLastUpdateTs();
return startTime == 0 && endTime == 0 ||
(endTime == 0 && startTime < lastUpdateTs) ||
(startTime == 0 && endTime > lastUpdateTs)
? true : startTime < lastUpdateTs && endTime > lastUpdateTs;
}).collect(Collectors.toList());
tsSubService.saveAndNotify(entityId, scope, attributes, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void tmp) {
try {
logAttributesUpdated(user, entityId, scope, attributes, null);
} catch (ThingsboardException e) {
log.error("Failed to log attribute updates", e);
}
}
@Override
public void onFailure(Throwable t) {
try {
logAttributesUpdated(user, entityId, scope, attributes, t);
} catch (ThingsboardException e) {
log.error("Failed to log attribute updates", e);
}
}
});
}
return null;
});
} else {
return Futures.immediateFuture(null);
}
}
private void logAttributesUpdated(SecurityUser user, EntityId entityId, String scope, List<AttributeKvEntry> attributes, Throwable e) throws ThingsboardException {
logEntityAction(user, (UUIDBased & EntityId) entityId, null, null, ActionType.ATTRIBUTES_UPDATED, toException(e),
scope, attributes);
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.DELETE) @RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)

6
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -92,15 +92,9 @@ import java.util.stream.Collectors;
@Slf4j @Slf4j
public class TelemetryController extends BaseController { public class TelemetryController extends BaseController {
@Autowired
private AttributesService attributesService;
@Autowired @Autowired
private TimeseriesService tsService; private TimeseriesService tsService;
@Autowired
private TelemetrySubscriptionService tsSubService;
@Autowired @Autowired
private AccessValidator accessValidator; private AccessValidator accessValidator;

35
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java

@ -143,7 +143,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
public void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub) { public void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub) {
long startTime = 0L; long startTime = 0L;
long endTime = 0L; long endTime = 0L;
if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW) && TelemetryFeature.TIMESERIES.equals(sub.getType())) {
EntityView entityView = entityViewService.findEntityViewById(new EntityViewId(entityId.getId())); EntityView entityView = entityViewService.findEntityViewById(new EntityViewId(entityId.getId()));
entityId = entityView.getEntityId(); entityId = entityView.getEntityId();
startTime = entityView.getStartTimeMs(); startTime = entityView.getStartTimeMs();
@ -165,38 +165,15 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
} }
private SubscriptionState getUpdatedSubscriptionState(EntityId entityId, SubscriptionState sub, EntityView entityView) { private SubscriptionState getUpdatedSubscriptionState(EntityId entityId, SubscriptionState sub, EntityView entityView) {
boolean allKeys;
Map<String, Long> keyStates; Map<String, Long> keyStates;
if (sub.getType().equals(TelemetryFeature.TIMESERIES) && !entityView.getKeys().getTimeseries().isEmpty()) { if(sub.isAllKeys()) {
allKeys = false; keyStates = entityView.getKeys().getTimeseries().stream().collect(Collectors.toMap(k -> k, k -> 0L));
} else {
keyStates = sub.getKeyStates().entrySet() keyStates = sub.getKeyStates().entrySet()
.stream().filter(entry -> entityView.getKeys().getTimeseries().contains(entry.getKey())) .stream().filter(entry -> entityView.getKeys().getTimeseries().contains(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
} else if (sub.getType().equals(TelemetryFeature.ATTRIBUTES)) {
if (sub.getScope().equals(DataConstants.CLIENT_SCOPE) && !entityView.getKeys().getAttributes().getCs().isEmpty()) {
allKeys = false;
keyStates = filterMap(sub, entityView.getKeys().getAttributes().getCs());
} else if (sub.getScope().equals(DataConstants.SERVER_SCOPE) && !entityView.getKeys().getAttributes().getSs().isEmpty()) {
allKeys = false;
keyStates = filterMap(sub, entityView.getKeys().getAttributes().getSs());
} else if (sub.getScope().equals(DataConstants.SERVER_SCOPE) && !entityView.getKeys().getAttributes().getSh().isEmpty()) {
allKeys = false;
keyStates = filterMap(sub, entityView.getKeys().getAttributes().getSh());
} else {
allKeys = sub.isAllKeys();
keyStates = sub.getKeyStates();
}
} else {
allKeys = sub.isAllKeys();
keyStates = sub.getKeyStates();
} }
return new SubscriptionState(sub.getWsSessionId(), sub.getSubscriptionId(), entityId, sub.getType(), allKeys, keyStates, sub.getScope()); return new SubscriptionState(sub.getWsSessionId(), sub.getSubscriptionId(), entityId, sub.getType(), false, keyStates, sub.getScope());
}
private Map<String, Long> filterMap(SubscriptionState sub, List<String> allowedKeys) {
return sub.getKeyStates().entrySet()
.stream().filter(entry -> allowedKeys.contains(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
} }
@Override @Override
@ -467,7 +444,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
onLocalSubUpdate(entityId, s -> TelemetryFeature.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> { onLocalSubUpdate(entityId, s -> TelemetryFeature.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> {
List<TsKvEntry> subscriptionUpdate = null; List<TsKvEntry> subscriptionUpdate = null;
for (AttributeKvEntry kv : attributes) { for (AttributeKvEntry kv : attributes) {
if (isInTimeRange(s, kv.getLastUpdateTs()) && (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey()))) { if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
if (subscriptionUpdate == null) { if (subscriptionUpdate == null) {
subscriptionUpdate = new ArrayList<>(); subscriptionUpdate = new ArrayList<>();
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -57,6 +57,8 @@ public class DataConstants {
public static final String ENTITY_UNASSIGNED = "ENTITY_UNASSIGNED"; public static final String ENTITY_UNASSIGNED = "ENTITY_UNASSIGNED";
public static final String ATTRIBUTES_UPDATED = "ATTRIBUTES_UPDATED"; public static final String ATTRIBUTES_UPDATED = "ATTRIBUTES_UPDATED";
public static final String ATTRIBUTES_DELETED = "ATTRIBUTES_DELETED"; public static final String ATTRIBUTES_DELETED = "ATTRIBUTES_DELETED";
public static final String ALARM_ACK = "ALARM_ACK";
public static final String ALARM_CLEAR = "ALARM_CLEAR";
public static final String RPC_CALL_FROM_SERVER_TO_DEVICE = "RPC_CALL_FROM_SERVER_TO_DEVICE"; public static final String RPC_CALL_FROM_SERVER_TO_DEVICE = "RPC_CALL_FROM_SERVER_TO_DEVICE";

13
dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheConfiguration.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.cache;
import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause; import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.Ticker; import com.github.benmanes.caffeine.cache.Ticker;
import com.github.benmanes.caffeine.cache.Weigher;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -31,6 +32,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -64,8 +66,9 @@ public class CaffeineCacheConfiguration {
private CaffeineCache buildCache(String name, CacheSpecs cacheSpec) { private CaffeineCache buildCache(String name, CacheSpecs cacheSpec) {
final Caffeine<Object, Object> caffeineBuilder final Caffeine<Object, Object> caffeineBuilder
= Caffeine.newBuilder() = Caffeine.newBuilder()
.weigher(collectionSafeWeigher())
.maximumWeight(cacheSpec.getMaxSize())
.expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES) .expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES)
.maximumSize(cacheSpec.getMaxSize())
.ticker(ticker()); .ticker(ticker());
return new CaffeineCache(name, caffeineBuilder.build()); return new CaffeineCache(name, caffeineBuilder.build());
} }
@ -80,4 +83,12 @@ public class CaffeineCacheConfiguration {
return new PreviousDeviceCredentialsIdKeyGenerator(); return new PreviousDeviceCredentialsIdKeyGenerator();
} }
private Weigher<? super Object, ? super Object> collectionSafeWeigher() {
return (Weigher<Object, Object>) (key, value) -> {
if(value instanceof Collection) {
return ((Collection) value).size();
}
return 1;
};
}
} }

7
dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java

@ -29,6 +29,7 @@ import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
@ -46,6 +47,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
@Autowired @Autowired
private DeviceService deviceService; private DeviceService deviceService;
@Autowired
private EntityViewService entityViewService;
@Autowired @Autowired
private TenantService tenantService; private TenantService tenantService;
@ -81,6 +85,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
case DEVICE: case DEVICE:
hasName = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId())); hasName = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId()));
break; break;
case ENTITY_VIEW:
hasName = entityViewService.findEntityViewByIdAsync(new EntityViewId(entityId.getId()));
break;
case TENANT: case TENANT:
hasName = tenantService.findTenantByIdAsync(new TenantId(entityId.getId())); hasName = tenantService.findTenantByIdAsync(new TenantId(entityId.getId()));
break; break;

3
dao/src/main/java/org/thingsboard/server/dao/entityview/CassandraEntityViewDao.java

@ -84,8 +84,7 @@ public class CassandraEntityViewDao extends CassandraAbstractSearchTextDao<Entit
@Override @Override
public EntityView save(EntityView domain) { public EntityView save(EntityView domain) {
EntityView savedEntityView = super.save(domain); EntityView savedEntityView = super.save(domain);
EntitySubtype entitySubtype = new EntitySubtype(savedEntityView.getTenantId(), EntityType.ENTITY_VIEW, EntitySubtype entitySubtype = new EntitySubtype(savedEntityView.getTenantId(), EntityType.ENTITY_VIEW, savedEntityView.getType());
savedEntityView.getId().getEntityType().toString());
EntitySubtypeEntity entitySubtypeEntity = new EntitySubtypeEntity(entitySubtype); EntitySubtypeEntity entitySubtypeEntity = new EntitySubtypeEntity(entitySubtype);
Statement saveStatement = cluster.getMapper(EntitySubtypeEntity.class).saveQuery(entitySubtypeEntity); Statement saveStatement = cluster.getMapper(EntitySubtypeEntity.class).saveQuery(entitySubtypeEntity);
executeWrite(saveStatement); executeWrite(saveStatement);

45
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java

@ -105,21 +105,6 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
log.trace("Executing save entity view [{}]", entityView); log.trace("Executing save entity view [{}]", entityView);
entityViewValidator.validate(entityView); entityViewValidator.validate(entityView);
EntityView savedEntityView = entityViewDao.save(entityView); EntityView savedEntityView = entityViewDao.save(entityView);
List<ListenableFuture<List<Void>>> futures = new ArrayList<>();
if (savedEntityView.getKeys() != null) {
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.CLIENT_SCOPE, savedEntityView.getKeys().getAttributes().getCs()));
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SERVER_SCOPE, savedEntityView.getKeys().getAttributes().getSs()));
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SHARED_SCOPE, savedEntityView.getKeys().getAttributes().getSh()));
}
for (ListenableFuture<List<Void>> future : futures) {
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
log.error("Failed to copy attributes to entity view", e);
throw new RuntimeException("Failed to copy attributes to entity view", e);
}
}
return savedEntityView; return savedEntityView;
} }
@ -294,36 +279,6 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
}); });
} }
private ListenableFuture<List<Void>> copyAttributesFromEntityToEntityView(EntityView entityView, String scope, Collection<String> keys) {
if (keys != null && !keys.isEmpty()) {
ListenableFuture<List<AttributeKvEntry>> getAttrFuture = attributesService.find(entityView.getEntityId(), scope, keys);
return Futures.transform(getAttrFuture, attributeKvEntries -> {
List<AttributeKvEntry> filteredAttributes = new ArrayList<>();
if (attributeKvEntries != null && !attributeKvEntries.isEmpty()) {
filteredAttributes =
attributeKvEntries.stream()
.filter(attributeKvEntry -> {
long startTime = entityView.getStartTimeMs();
long endTime = entityView.getEndTimeMs();
long lastUpdateTs = attributeKvEntry.getLastUpdateTs();
return startTime == 0 && endTime == 0 ||
(endTime == 0 && startTime < lastUpdateTs) ||
(startTime == 0 && endTime > lastUpdateTs)
? true : startTime < lastUpdateTs && endTime > lastUpdateTs;
}).collect(Collectors.toList());
}
try {
return attributesService.save(entityView.getId(), scope, filteredAttributes).get();
} catch (InterruptedException | ExecutionException e) {
log.error("Failed to copy attributes to entity view", e);
throw new RuntimeException("Failed to copy attributes to entity view", e);
}
});
} else {
return Futures.immediateFuture(null);
}
}
private DataValidator<EntityView> entityViewValidator = private DataValidator<EntityView> entityViewValidator =
new DataValidator<EntityView>() { new DataValidator<EntityView>() {

20
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -79,12 +79,16 @@ public class BaseTimeseriesService implements TimeseriesService {
if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) {
EntityView entityView = entityViewService.findEntityViewById((EntityViewId) entityId); EntityView entityView = entityViewService.findEntityViewById((EntityViewId) entityId);
List<String> filteredKeys = new ArrayList<>(keys); List<String> filteredKeys = new ArrayList<>(keys);
if (!entityView.getKeys().getTimeseries().isEmpty()) { if (entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null &&
!entityView.getKeys().getTimeseries().isEmpty()) {
filteredKeys.retainAll(entityView.getKeys().getTimeseries()); filteredKeys.retainAll(entityView.getKeys().getTimeseries());
} }
List<ReadTsKvQuery> queries = List<ReadTsKvQuery> queries =
filteredKeys.stream() filteredKeys.stream()
.map(key -> new BaseReadTsKvQuery(key, entityView.getStartTimeMs(), entityView.getEndTimeMs(), 1, "ASC")) .map(key -> {
long endTs = entityView.getEndTimeMs() != 0 ? entityView.getEndTimeMs() : Long.MAX_VALUE;
return new BaseReadTsKvQuery(key, entityView.getStartTimeMs(), endTs, 1, "DESC");
})
.collect(Collectors.toList()); .collect(Collectors.toList());
if (queries.size() > 0) { if (queries.size() > 0) {
@ -100,7 +104,17 @@ public class BaseTimeseriesService implements TimeseriesService {
@Override @Override
public ListenableFuture<List<TsKvEntry>> findAllLatest(EntityId entityId) { public ListenableFuture<List<TsKvEntry>> findAllLatest(EntityId entityId) {
validate(entityId); validate(entityId);
return timeseriesDao.findAllLatest(entityId); if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) {
EntityView entityView = entityViewService.findEntityViewById((EntityViewId) entityId);
if (entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null &&
!entityView.getKeys().getTimeseries().isEmpty()) {
return findLatest(entityId, entityView.getKeys().getTimeseries());
} else {
return Futures.immediateFuture(new ArrayList<>());
}
} else {
return timeseriesDao.findAllLatest(entityId);
}
} }
@Override @Override

4
dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java

@ -45,6 +45,7 @@ import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
@ -88,6 +89,9 @@ public abstract class AbstractServiceTest {
@Autowired @Autowired
protected AssetService assetService; protected AssetService assetService;
@Autowired
protected EntityViewService entityViewService;
@Autowired @Autowired
protected DeviceCredentialsService deviceCredentialsService; protected DeviceCredentialsService deviceCredentialsService;

51
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java

@ -17,9 +17,15 @@ package org.thingsboard.server.dao.service.timeseries;
import com.datastax.driver.core.utils.UUIDs; import com.datastax.driver.core.utils.UUIDs;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
@ -30,6 +36,7 @@ import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.objects.TelemetryEntityView;
import org.thingsboard.server.dao.service.AbstractServiceTest; import org.thingsboard.server.dao.service.AbstractServiceTest;
import java.util.ArrayList; import java.util.ArrayList;
@ -61,6 +68,22 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
KvEntry doubleKvEntry = new DoubleDataEntry(DOUBLE_KEY, Double.MAX_VALUE); KvEntry doubleKvEntry = new DoubleDataEntry(DOUBLE_KEY, Double.MAX_VALUE);
KvEntry booleanKvEntry = new BooleanDataEntry(BOOLEAN_KEY, Boolean.TRUE); KvEntry booleanKvEntry = new BooleanDataEntry(BOOLEAN_KEY, Boolean.TRUE);
private TenantId tenantId;
@Before
public void before() {
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
Tenant savedTenant = tenantService.saveTenant(tenant);
Assert.assertNotNull(savedTenant);
tenantId = savedTenant.getId();
}
@After
public void after() {
tenantService.deleteTenant(tenantId);
}
@Test @Test
public void testFindAllLatest() throws Exception { public void testFindAllLatest() throws Exception {
DeviceId deviceId = new DeviceId(UUIDs.timeBased()); DeviceId deviceId = new DeviceId(UUIDs.timeBased());
@ -69,7 +92,15 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
saveEntries(deviceId, TS - 1); saveEntries(deviceId, TS - 1);
saveEntries(deviceId, TS); saveEntries(deviceId, TS);
List<TsKvEntry> tsList = tsService.findAllLatest(deviceId).get(); testLatestTsAndVerify(deviceId);
EntityView entityView = saveAndCreateEntityView(deviceId, Arrays.asList(STRING_KEY, DOUBLE_KEY, LONG_KEY, BOOLEAN_KEY));
testLatestTsAndVerify(entityView.getId());
}
private void testLatestTsAndVerify(EntityId entityId) throws ExecutionException, InterruptedException {
List<TsKvEntry> tsList = tsService.findAllLatest(entityId).get();
assertNotNull(tsList); assertNotNull(tsList);
assertEquals(4, tsList.size()); assertEquals(4, tsList.size());
@ -89,6 +120,18 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
assertEquals(expected, tsList); assertEquals(expected, tsList);
} }
private EntityView saveAndCreateEntityView(DeviceId deviceId, List<String> timeseries) {
EntityView entityView = new EntityView();
entityView.setName("entity_view_name");
entityView.setType("default");
entityView.setTenantId(tenantId);
TelemetryEntityView keys = new TelemetryEntityView();
keys.setTimeseries(timeseries);
entityView.setKeys(keys);
entityView.setEntityId(deviceId);
return entityViewService.saveEntityView(entityView);
}
@Test @Test
public void testFindLatest() throws Exception { public void testFindLatest() throws Exception {
DeviceId deviceId = new DeviceId(UUIDs.timeBased()); DeviceId deviceId = new DeviceId(UUIDs.timeBased());
@ -100,6 +143,12 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
List<TsKvEntry> entries = tsService.findLatest(deviceId, Collections.singleton(STRING_KEY)).get(); List<TsKvEntry> entries = tsService.findLatest(deviceId, Collections.singleton(STRING_KEY)).get();
Assert.assertEquals(1, entries.size()); Assert.assertEquals(1, entries.size());
Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0)); Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0));
EntityView entityView = saveAndCreateEntityView(deviceId, Arrays.asList(STRING_KEY));
entries = tsService.findLatest(entityView.getId(), Collections.singleton(STRING_KEY)).get();
Assert.assertEquals(1, entries.size());
Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0));
} }
@Test @Test

2
msa/docker/.env

@ -8,4 +8,4 @@ MQTT_TRANSPORT_DOCKER_NAME=tb-mqtt-transport
TB_VERSION=2.2.0-SNAPSHOT TB_VERSION=2.2.0-SNAPSHOT
KAFKA_TOPICS=js.eval.requests:100:1,tb.transport.api.requests:30:1,tb.rule-engine:30:1 KAFKA_TOPICS=js.eval.requests:100:1:delete --config=retention.ms=60000 --config=retention.bytes=1073741824,tb.transport.api.requests:30:1:delete --config=retention.ms=60000 --config=retention.bytes=1073741824,tb.rule-engine:30:1

3
msa/docker/docker-compose.yml

@ -36,6 +36,9 @@ services:
KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE
KAFKA_CREATE_TOPICS: "${KAFKA_TOPICS}" KAFKA_CREATE_TOPICS: "${KAFKA_TOPICS}"
KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'false' KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'false'
KAFKA_LOG_RETENTION_BYTES: 1073741824
KAFKA_LOG_RETENTION_MS: 300000
KAFKA_LOG_CLEANUP_POLICY: delete
depends_on: depends_on:
- zookeeper - zookeeper
tb-js-executor: tb-js-executor:

129
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java

@ -17,7 +17,9 @@ package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.RuleNode;
@ -36,12 +38,11 @@ import org.thingsboard.server.common.msg.session.SessionMsgType;
import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
@Slf4j @Slf4j
@ -67,70 +68,88 @@ public class TbCopyAttributesToEntityViewNode implements TbNode {
} }
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { public void onMsg(TbContext ctx, TbMsg msg) {
if (!msg.getMetaData().getData().isEmpty()) { if (DataConstants.ATTRIBUTES_UPDATED.equals(msg.getType()) ||
long now = System.currentTimeMillis(); DataConstants.ATTRIBUTES_DELETED.equals(msg.getType()) ||
String scope = msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()) ? SessionMsgType.POST_ATTRIBUTES_REQUEST.name().equals(msg.getType())) {
DataConstants.CLIENT_SCOPE : msg.getMetaData().getValue("scope"); if (!msg.getMetaData().getData().isEmpty()) {
long now = System.currentTimeMillis();
String scope = msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()) ?
DataConstants.CLIENT_SCOPE : msg.getMetaData().getValue("scope");
ListenableFuture<List<EntityView>> entityViewsFuture = ListenableFuture<List<EntityView>> entityViewsFuture =
ctx.getEntityViewService().findEntityViewsByTenantIdAndEntityIdAsync(ctx.getTenantId(), msg.getOriginator()); ctx.getEntityViewService().findEntityViewsByTenantIdAndEntityIdAsync(ctx.getTenantId(), msg.getOriginator());
DonAsynchron.withCallback(entityViewsFuture, DonAsynchron.withCallback(entityViewsFuture,
entityViews -> { entityViews -> {
for (EntityView entityView : entityViews) { for (EntityView entityView : entityViews) {
long startTime = entityView.getStartTimeMs(); long startTime = entityView.getStartTimeMs();
long endTime = entityView.getEndTimeMs(); long endTime = entityView.getEndTimeMs();
if ((endTime != 0 && endTime > now && startTime < now) || (endTime == 0 && startTime < now)) { if ((endTime != 0 && endTime > now && startTime < now) || (endTime == 0 && startTime < now)) {
Set<AttributeKvEntry> attributes = if (DataConstants.ATTRIBUTES_UPDATED.equals(msg.getType()) ||
JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())); SessionMsgType.POST_ATTRIBUTES_REQUEST.name().equals(msg.getType())) {
List<AttributeKvEntry> filteredAttributes = Set<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData()));
attributes.stream() List<AttributeKvEntry> filteredAttributes =
.filter(attr -> { attributes.stream().filter(attr -> attributeContainsInEntityView(scope, attr.getKey(), entityView)).collect(Collectors.toList());
switch (scope) { ctx.getTelemetryService().saveAndNotify(entityView.getId(), scope, filteredAttributes,
case DataConstants.CLIENT_SCOPE: new FutureCallback<Void>() {
if (entityView.getKeys().getAttributes().getCs().isEmpty()) { @Override
return true; public void onSuccess(@Nullable Void result) {
} transformAndTellNext(ctx, msg, entityView);
return entityView.getKeys().getAttributes().getCs().contains(attr.getKey());
case DataConstants.SERVER_SCOPE:
if (entityView.getKeys().getAttributes().getSs().isEmpty()) {
return true;
}
return entityView.getKeys().getAttributes().getSs().contains(attr.getKey());
case DataConstants.SHARED_SCOPE:
if (entityView.getKeys().getAttributes().getSh().isEmpty()) {
return true;
}
return entityView.getKeys().getAttributes().getSh().contains(attr.getKey());
} }
return false;
}).collect(Collectors.toList());
ctx.getTelemetryService().saveAndNotify(entityView.getId(), scope, filteredAttributes, @Override
new FutureCallback<Void>() { public void onFailure(Throwable t) {
@Override ctx.tellFailure(msg, t);
public void onSuccess(@Nullable Void result) { }
TbMsg updMsg = ctx.transformMsg(msg, msg.getType(), entityView.getId(), msg.getMetaData(), msg.getData()); });
ctx.tellNext(updMsg, SUCCESS); } else if (DataConstants.ATTRIBUTES_DELETED.equals(msg.getType())) {
} List<String> attributes = new ArrayList<>();
for (JsonElement element : new JsonParser().parse(msg.getData()).getAsJsonObject().get("attributes").getAsJsonArray()) {
@Override if (element.isJsonPrimitive()) {
public void onFailure(Throwable t) { JsonPrimitive value = element.getAsJsonPrimitive();
ctx.tellFailure(msg, t); if (value.isString()) {
attributes.add(value.getAsString());
}
} }
}); }
List<String> filteredAttributes =
attributes.stream().filter(attr -> attributeContainsInEntityView(scope, attr, entityView)).collect(Collectors.toList());
if (filteredAttributes != null && !filteredAttributes.isEmpty()) {
ctx.getAttributesService().removeAll(entityView.getId(), scope, filteredAttributes);
transformAndTellNext(ctx, msg, entityView);
}
}
}
} }
} },
}, t -> ctx.tellFailure(msg, t));
t -> ctx.tellFailure(msg, t)); } else {
ctx.tellFailure(msg, new IllegalArgumentException("Message metadata is empty"));
}
} else { } else {
ctx.tellNext(msg, FAILURE); ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type [" + msg.getType() + "]"));
} }
} }
private void transformAndTellNext(TbContext ctx, TbMsg msg, EntityView entityView) {
TbMsg updMsg = ctx.transformMsg(msg, msg.getType(), entityView.getId(), msg.getMetaData(), msg.getData());
ctx.tellNext(updMsg, SUCCESS);
}
private boolean attributeContainsInEntityView(String scope, String attrKey, EntityView entityView) {
switch (scope) {
case DataConstants.CLIENT_SCOPE:
return entityView.getKeys().getAttributes().getCs().contains(attrKey);
case DataConstants.SERVER_SCOPE:
return entityView.getKeys().getAttributes().getSs().contains(attrKey);
case DataConstants.SHARED_SCOPE:
return entityView.getKeys().getAttributes().getSh().contains(attrKey);
}
return false;
}
@Override @Override
public void destroy() { public void destroy() {
} }
} }

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java

@ -30,7 +30,7 @@ import org.thingsboard.server.common.msg.session.SessionMsgType;
configClazz = EmptyNodeConfiguration.class, configClazz = EmptyNodeConfiguration.class,
relationTypes = {"Post attributes", "Post telemetry", "RPC Request from Device", "RPC Request to Device", "Activity Event", "Inactivity Event", relationTypes = {"Post attributes", "Post telemetry", "RPC Request from Device", "RPC Request to Device", "Activity Event", "Inactivity Event",
"Connect Event", "Disconnect Event", "Entity Created", "Entity Updated", "Entity Deleted", "Entity Assigned", "Connect Event", "Disconnect Event", "Entity Created", "Entity Updated", "Entity Deleted", "Entity Assigned",
"Entity Unassigned", "Attributes Updated", "Attributes Deleted", "Other"}, "Entity Unassigned", "Attributes Updated", "Attributes Deleted", "Alarm Acknowledged", "Alarm Cleared", "Other"},
nodeDescription = "Route incoming messages by Message Type", nodeDescription = "Route incoming messages by Message Type",
nodeDetails = "Sends messages with message types <b>\"Post attributes\", \"Post telemetry\", \"RPC Request\"</b> etc. via corresponding chain, otherwise <b>Other</b> chain is used.", nodeDetails = "Sends messages with message types <b>\"Post attributes\", \"Post telemetry\", \"RPC Request\"</b> etc. via corresponding chain, otherwise <b>Other</b> chain is used.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
@ -75,6 +75,10 @@ public class TbMsgTypeSwitchNode implements TbNode {
relationType = "Attributes Updated"; relationType = "Attributes Updated";
} else if (msg.getType().equals(DataConstants.ATTRIBUTES_DELETED)) { } else if (msg.getType().equals(DataConstants.ATTRIBUTES_DELETED)) {
relationType = "Attributes Deleted"; relationType = "Attributes Deleted";
} else if (msg.getType().equals(DataConstants.ALARM_ACK)) {
relationType = "Alarm Acknowledged";
} else if (msg.getType().equals(DataConstants.ALARM_CLEAR)) {
relationType = "Alarm Cleared";
} else if (msg.getType().equals(DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE)) { } else if (msg.getType().equals(DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE)) {
relationType = "RPC Request to Device"; relationType = "RPC Request to Device";
} else { } else {

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

@ -27,7 +27,6 @@ function EntityViewService($http, $q, $window, userService, attributeService, cu
deleteEntityView: deleteEntityView, deleteEntityView: deleteEntityView,
getCustomerEntityViews: getCustomerEntityViews, getCustomerEntityViews: getCustomerEntityViews,
getEntityView: getEntityView, getEntityView: getEntityView,
getEntityViews: getEntityViews,
getTenantEntityViews: getTenantEntityViews, getTenantEntityViews: getTenantEntityViews,
saveEntityView: saveEntityView, saveEntityView: saveEntityView,
unassignEntityViewFromCustomer: unassignEntityViewFromCustomer, unassignEntityViewFromCustomer: unassignEntityViewFromCustomer,
@ -126,32 +125,6 @@ function EntityViewService($http, $q, $window, userService, attributeService, cu
return deferred.promise; return deferred.promise;
} }
function getEntityViews(entityViewIds, config) {
var deferred = $q.defer();
var ids = '';
for (var i=0;i<entityViewIds.length;i++) {
if (i>0) {
ids += ',';
}
ids += entityViewIds[i];
}
var url = '/api/entityViews?entityViewIds=' + ids;
$http.get(url, config).then(function success(response) {
var entityViews = response.data;
entityViews.sort(function (entityView1, entityView2) {
var id1 = entityView1.id.id;
var id2 = entityView2.id.id;
var index1 = entityViewIds.indexOf(id1);
var index2 = entityViewIds.indexOf(id2);
return index1 - index2;
});
deferred.resolve(entityViews);
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
function saveEntityView(entityView) { function saveEntityView(entityView) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/entityView'; var url = '/api/entityView';

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

@ -135,6 +135,10 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
case types.entityType.asset: case types.entityType.asset:
promise = assetService.getAssets(entityIds, config); promise = assetService.getAssets(entityIds, config);
break; break;
case types.entityType.entityView:
promise = getEntitiesByIdsPromise(
(id) => entityViewService.getEntityView(id, config), entityIds);
break;
case types.entityType.tenant: case types.entityType.tenant:
promise = getEntitiesByIdsPromise( promise = getEntitiesByIdsPromise(
(id) => tenantService.getTenant(id, config), entityIds); (id) => tenantService.getTenant(id, config), entityIds);

2
ui/src/app/entity-view/add-entity-view.tpl.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<md-dialog aria-label="{{ 'entity-view.add' | translate }}" tb-help="'entityViews'" help-container-id="help-container"> <md-dialog aria-label="{{ 'entity-view.add' | translate }}" style="width: 800px;" tb-help="'entityViews'" help-container-id="help-container">
<form name="theForm" ng-submit="vm.add()"> <form name="theForm" ng-submit="vm.add()">
<md-toolbar> <md-toolbar>
<div class="md-toolbar-tools"> <div class="md-toolbar-tools">

191
ui/src/app/entity-view/entity-view-fieldset.tpl.html

@ -60,7 +60,7 @@
entity-type="types.entityType.entityView"> entity-type="types.entityType.entityView">
</tb-entity-subtype-autocomplete> </tb-entity-subtype-autocomplete>
<section layout="column"> <section layout="column">
<label translate class="tb-title no-padding">entity-view.related-entity</label> <label translate class="tb-title no-padding">entity-view.target-entity</label>
<tb-entity-select flex ng-disabled="!isEdit" <tb-entity-select flex ng-disabled="!isEdit"
the-form="theForm" the-form="theForm"
tb-required="true" tb-required="true"
@ -68,62 +68,145 @@
ng-model="entityView.entityId"> ng-model="entityView.entityId">
</tb-entity-select> </tb-entity-select>
</section> </section>
<md-expansion-panel-group class="tb-entity-view-panel-group" ng-class="{'disabled': $root.loading || !isEdit}"
auto-expand="true"
multiple="true"
md-component-id="attributesPanelGroup">
<md-expansion-panel md-component-id="{{attributesPanelId}}">
<md-expansion-panel-collapsed>
<div class="tb-panel-title">{{ 'entity-view.attributes-propagation' | translate }}</div>
<span flex></span>
<md-expansion-panel-icon></md-expansion-panel-icon>
</md-expansion-panel-collapsed>
<md-expansion-panel-expanded>
<md-expansion-panel-header ng-click="$mdExpansionPanel(attributesPanelId).collapse()">
<div class="tb-panel-title">{{ 'entity-view.attributes-propagation' | translate }}</div>
<span flex></span>
<md-expansion-panel-icon></md-expansion-panel-icon>
</md-expansion-panel-header>
<md-expansion-panel-content>
<div translate class="tb-hint">entity-view.attributes-propagation-hint</div>
<label translate class="tb-title no-padding">entity-view.client-attributes</label>
<md-chips style="padding-bottom: 15px;"
ng-required="false"
readonly="!isEdit"
ng-model="entityView.keys.attributes.cs"
placeholder="{{'entity-view.client-attributes-placeholder' | translate}}"
md-separator-keys="separatorKeys">
<md-autocomplete
md-no-cache="true"
id="ca_datakey"
md-selected-item="selectedAttributeDataKey"
md-search-text="attributeDataKeySearchText"
md-items="item in dataKeysSearch(attributeDataKeySearchText, types.dataKeyType.attribute)"
md-item-text="item.name"
md-min-length="0"
placeholder="{{'entity-view.client-attributes-placeholder' | translate }}"
md-menu-class="tb-attribute-datakey-autocomplete">
<span md-highlight-text="attributeDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
</md-autocomplete>
</md-chips>
<label translate class="tb-title no-padding">entity-view.shared-attributes</label>
<md-chips style="padding-bottom: 15px;"
ng-required="false"
readonly="!isEdit"
ng-model="entityView.keys.attributes.sh"
placeholder="{{'entity-view.shared-attributes-placeholder' | translate}}"
md-separator-keys="separatorKeys">
<md-autocomplete
md-no-cache="true"
id="sh_datakey"
md-selected-item="selectedAttributeDataKey"
md-search-text="attributeDataKeySearchText"
md-items="item in dataKeysSearch(attributeDataKeySearchText, types.dataKeyType.attribute)"
md-item-text="item.name"
md-min-length="0"
placeholder="{{'entity-view.server-attributes-placeholder' | translate }}"
md-menu-class="tb-attribute-datakey-autocomplete">
<span md-highlight-text="attributeDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
</md-autocomplete>
</md-chips>
<label translate class="tb-title no-padding">entity-view.server-attributes</label>
<md-chips style="padding-bottom: 15px;"
ng-required="false"
readonly="!isEdit"
ng-model="entityView.keys.attributes.ss"
placeholder="{{'entity-view.server-attributes-placeholder' | translate}}"
md-separator-keys="separatorKeys">
<md-autocomplete
md-no-cache="true"
id="ss_datakey"
md-selected-item="selectedAttributeDataKey"
md-search-text="attributeDataKeySearchText"
md-items="item in dataKeysSearch(attributeDataKeySearchText, types.dataKeyType.attribute)"
md-item-text="item.name"
md-min-length="0"
placeholder="{{'entity-view.server-attributes-placeholder' | translate }}"
md-menu-class="tb-attribute-datakey-autocomplete">
<span md-highlight-text="attributeDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
</md-autocomplete>
</md-chips>
</md-expansion-panel-content>
</md-expansion-panel-expanded>
</md-expansion-panel>
<md-expansion-panel md-component-id="{{timeseriesPanelId}}">
<md-expansion-panel-collapsed>
<div class="tb-panel-title">{{ 'entity-view.timeseries-data' | translate }}</div>
<span flex></span>
<md-expansion-panel-icon></md-expansion-panel-icon>
</md-expansion-panel-collapsed>
<md-expansion-panel-expanded>
<md-expansion-panel-header ng-click="$mdExpansionPanel(timeseriesPanelId).collapse()">
<div class="tb-panel-title">{{ 'entity-view.timeseries-data' | translate }}</div>
<span flex></span>
<md-expansion-panel-icon></md-expansion-panel-icon>
</md-expansion-panel-header>
<md-expansion-panel-content>
<div translate class="tb-hint">entity-view.timeseries-data-hint</div>
<label translate class="tb-title no-padding">entity-view.timeseries</label>
<md-chips ng-required="false"
readonly="!isEdit"
ng-model="entityView.keys.timeseries"
placeholder="{{'entity-view.timeseries-placeholder' | translate}}"
md-separator-keys="separatorKeys">
<md-autocomplete
md-no-cache="true"
id="timeseries_datakey"
md-selected-item="selectedTimeseriesDataKey"
md-search-text="timeseriesDataKeySearchText"
md-items="item in dataKeysSearch(timeseriesDataKeySearchText, types.dataKeyType.timeseries)"
md-item-text="item.name"
md-min-length="0"
placeholder="{{'entity-view.timeseries-placeholder' | translate }}"
md-menu-class="tb-timeseries-datakey-autocomplete">
<span md-highlight-text="timeseriesDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
</md-autocomplete>
</md-chips>
</md-expansion-panel-content>
</md-expansion-panel-expanded>
</md-expansion-panel>
</md-expansion-panel-group>
<section layout="row" layout-align="start start">
<mdp-date-picker ng-model="startTimeMs"
mdp-max-date="maxStartTimeMs"
mdp-placeholder="{{ 'entity-view.start-date' | translate }}"></mdp-date-picker>
<mdp-time-picker ng-model="startTimeMs"
mdp-max-date="maxStartTimeMs"
mdp-placeholder="{{ 'entity-view.start-ts' | translate }}"
mdp-auto-switch="true"></mdp-time-picker>
</section>
<section layout="row" layout-align="start start">
<mdp-date-picker ng-model="endTimeMs"
mdp-min-date="minEndTimeMs"
mdp-placeholder="{{ 'entity-view.end-date' | translate }}"></mdp-date-picker>
<mdp-time-picker ng-model="endTimeMs"
mdp-min-date="minEndTimeMs"
mdp-placeholder="{{ 'entity-view.end-ts' | translate }}"
mdp-auto-switch="true"></mdp-time-picker>
</section>
<md-input-container class="md-block"> <md-input-container class="md-block">
<label translate>entity-view.description</label> <label translate>entity-view.description</label>
<textarea ng-model="entityView.additionalInfo.description" rows="2"></textarea> <textarea ng-model="entityView.additionalInfo.description" rows="2"></textarea>
</md-input-container> </md-input-container>
<section layout="column">
<label translate class="tb-title no-padding">entity-view.client-attributes</label>
<md-chips style="padding-bottom: 15px;"
ng-required="false"
readonly="!isEdit"
ng-model="entityView.keys.attributes.cs"
placeholder="{{'entity-view.client-attributes' | translate}}"
md-separator-keys="separatorKeys">
</md-chips>
<label translate class="tb-title no-padding">entity-view.shared-attributes</label>
<md-chips style="padding-bottom: 15px;"
ng-required="false"
readonly="!isEdit"
ng-model="entityView.keys.attributes.sh"
placeholder="{{'entity-view.shared-attributes' | translate}}"
md-separator-keys="separatorKeys">
</md-chips>
<label translate class="tb-title no-padding">entity-view.server-attributes</label>
<md-chips style="padding-bottom: 15px;"
ng-required="false"
readonly="!isEdit"
ng-model="entityView.keys.attributes.ss"
placeholder="{{'entity-view.server-attributes' | translate}}"
md-separator-keys="separatorKeys">
</md-chips>
<label translate class="tb-title no-padding">entity-view.latest-timeseries</label>
<md-chips ng-required="false"
readonly="!isEdit"
ng-model="entityView.keys.timeseries"
placeholder="{{'entity-view.latest-timeseries' | translate}}"
md-separator-keys="separatorKeys">
</md-chips>
</section>
<section layout="column">
<section layout="row" layout-align="start start">
<mdp-date-picker ng-model="startTimeMs"
mdp-max-date="maxStartTimeMs"
mdp-placeholder="{{ 'entity-view.start-ts' | translate }}"></mdp-date-picker>
<mdp-time-picker ng-model="startTimeMs"
mdp-max-date="maxStartTimeMs"
mdp-placeholder="{{ 'entity-view.start-ts' | translate }}"
mdp-auto-switch="true"></mdp-time-picker>
</section>
<section layout="row" layout-align="start start">
<mdp-date-picker ng-model="endTimeMs"
mdp-min-date="minEndTimeMs"
mdp-placeholder="{{ 'entity-view.end-ts' | translate }}"></mdp-date-picker>
<mdp-time-picker ng-model="endTimeMs"
mdp-min-date="minEndTimeMs"
mdp-placeholder="{{ 'entity-view.end-ts' | translate }}"
mdp-auto-switch="true"></mdp-time-picker>
</section>
</section>
</fieldset> </fieldset>
</md-content> </md-content>

28
ui/src/app/entity-view/entity-view.directive.js

@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import './entity-view.scss';
/* eslint-disable import/no-unresolved, import/default */ /* eslint-disable import/no-unresolved, import/default */
import entityViewFieldsetTemplate from './entity-view-fieldset.tpl.html'; import entityViewFieldsetTemplate from './entity-view-fieldset.tpl.html';
@ -20,12 +23,16 @@ import entityViewFieldsetTemplate from './entity-view-fieldset.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@ngInject*/
export default function EntityViewDirective($compile, $templateCache, $filter, toast, $translate, $mdConstant, export default function EntityViewDirective($q, $compile, $templateCache, $filter, toast, $translate, $mdConstant, $mdExpansionPanel,
types, clipboardService, entityViewService, customerService) { types, clipboardService, entityViewService, customerService, entityService) {
var linker = function (scope, element) { var linker = function (scope, element) {
var template = $templateCache.get(entityViewFieldsetTemplate); var template = $templateCache.get(entityViewFieldsetTemplate);
element.html(template); element.html(template);
scope.attributesPanelId = (Math.random()*1000).toFixed(0);
scope.timeseriesPanelId = (Math.random()*1000).toFixed(0);
scope.$mdExpansionPanel = $mdExpansionPanel;
scope.types = types; scope.types = types;
scope.isAssignedToCustomer = false; scope.isAssignedToCustomer = false;
scope.isPublic = false; scope.isPublic = false;
@ -53,9 +60,13 @@ export default function EntityViewDirective($compile, $templateCache, $filter, t
} }
if (scope.entityView.startTimeMs > 0) { if (scope.entityView.startTimeMs > 0) {
scope.startTimeMs = new Date(scope.entityView.startTimeMs); scope.startTimeMs = new Date(scope.entityView.startTimeMs);
} else {
scope.startTimeMs = null;
} }
if (scope.entityView.endTimeMs > 0) { if (scope.entityView.endTimeMs > 0) {
scope.endTimeMs = new Date(scope.entityView.endTimeMs); scope.endTimeMs = new Date(scope.entityView.endTimeMs);
} else {
scope.endTimeMs = null;
} }
if (!scope.entityView.keys) { if (!scope.entityView.keys) {
scope.entityView.keys = {}; scope.entityView.keys = {};
@ -68,6 +79,19 @@ export default function EntityViewDirective($compile, $templateCache, $filter, t
} }
}); });
scope.dataKeysSearch = function (searchText, type) {
var deferred = $q.defer();
entityService.getEntityKeys(scope.entityView.entityId.entityType, scope.entityView.entityId.id, searchText, type, {ignoreLoading: true}).then(
function success(keys) {
deferred.resolve(keys);
},
function fail() {
deferred.resolve([]);
}
);
return deferred.promise;
};
scope.$watch('startTimeMs', function (newDate) { scope.$watch('startTimeMs', function (newDate) {
if (newDate) { if (newDate) {

47
ui/src/app/entity-view/entity-view.scss

@ -0,0 +1,47 @@
/**
* 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/constants";
.tb-entity-view-panel-group {
.tb-panel-title {
min-width: 90px;
user-select: none;
@media (min-width: $layout-breakpoint-sm) {
min-width: 180px;
}
}
.tb-panel-prompt {
overflow: hidden;
font-size: 14px;
color: rgba(0, 0, 0, .87);
text-overflow: ellipsis;
white-space: nowrap;
}
&.disabled {
.tb-panel-title,
.tb-panel-prompt {
color: rgba(0, 0, 0, .38);
}
}
md-icon.md-expansion-panel-icon {
margin-right: 0;
}
}

14
ui/src/app/locale/locale.constant-en_US.json

@ -838,14 +838,24 @@
"unable-entity-view-device-alias-text": "Device alias '{{entityViewAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}", "unable-entity-view-device-alias-text": "Device alias '{{entityViewAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
"select-entity-view": "Select entity view", "select-entity-view": "Select entity view",
"make-public": "Make entity view public", "make-public": "Make entity view public",
"start-date": "Start date",
"start-ts": "Start time", "start-ts": "Start time",
"end-date": "End date",
"end-ts": "End time", "end-ts": "End time",
"date-limits": "Date limits", "date-limits": "Date limits",
"client-attributes": "Client attributes", "client-attributes": "Client attributes",
"shared-attributes": "Shared attributes", "shared-attributes": "Shared attributes",
"server-attributes": "Server attributes", "server-attributes": "Server attributes",
"latest-timeseries": "Latest timeseries", "timeseries": "Timeseries",
"related-entity": "Related entity" "client-attributes-placeholder": "Client attributes",
"shared-attributes-placeholder": "Shared attributes",
"server-attributes-placeholder": "Server attributes",
"timeseries-placeholder": "Timeseries",
"target-entity": "Target entity",
"attributes-propagation": "Attributes propagation",
"attributes-propagation-hint": "Entity View will automatically copy specified attributes from Target Entity each time you save or update this entity view. For performance reasons target entity attributes are not propagated to entity view on each attribute change. You can enable automatic propagation by configuring \"copy to view\" rule node in your rule chain and linking \"Post attributes\" and \"Attributes Updated\" messages to the new rule node.",
"timeseries-data": "Timeseries data",
"timeseries-data-hint": "Configure timeseries data keys of the target entity that will be accessible to the entity view. This timeseries data is read-only."
}, },
"event": { "event": {
"event-type": "Event type", "event-type": "Event type",

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

@ -839,7 +839,7 @@
"client-attributes": "Client attributes", "client-attributes": "Client attributes",
"shared-attributes": "Shared attributes", "shared-attributes": "Shared attributes",
"server-attributes": "Server attributes", "server-attributes": "Server attributes",
"latest-timeseries": "Latest timeseries" "timeseries": "Timeseries"
}, },
"event": { "event": {
"event-type": "Tipo de evento", "event-type": "Tipo de evento",

Loading…
Cancel
Save