Commit b0151caa17d06efb8f6096d3fd86aa99c692acb1

Authored by vparomskiy
2 parents e729494e bd343cf0

Merge remote-tracking branch 'origin/master'

@@ -51,6 +51,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -51,6 +51,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
51 import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg; 51 import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
52 import org.thingsboard.server.dao.alarm.AlarmService; 52 import org.thingsboard.server.dao.alarm.AlarmService;
53 import org.thingsboard.server.dao.asset.AssetService; 53 import org.thingsboard.server.dao.asset.AssetService;
  54 +import org.thingsboard.server.dao.attributes.AttributesService;
54 import org.thingsboard.server.dao.audit.AuditLogService; 55 import org.thingsboard.server.dao.audit.AuditLogService;
55 import org.thingsboard.server.dao.customer.CustomerService; 56 import org.thingsboard.server.dao.customer.CustomerService;
56 import org.thingsboard.server.dao.dashboard.DashboardService; 57 import org.thingsboard.server.dao.dashboard.DashboardService;
@@ -70,6 +71,7 @@ import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; @@ -70,6 +71,7 @@ import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
70 import org.thingsboard.server.service.component.ComponentDiscoveryService; 71 import org.thingsboard.server.service.component.ComponentDiscoveryService;
71 import org.thingsboard.server.service.security.model.SecurityUser; 72 import org.thingsboard.server.service.security.model.SecurityUser;
72 import org.thingsboard.server.service.state.DeviceStateService; 73 import org.thingsboard.server.service.state.DeviceStateService;
  74 +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
73 75
74 import javax.mail.MessagingException; 76 import javax.mail.MessagingException;
75 import javax.servlet.http.HttpServletRequest; 77 import javax.servlet.http.HttpServletRequest;
@@ -143,6 +145,12 @@ public abstract class BaseController { @@ -143,6 +145,12 @@ public abstract class BaseController {
143 @Autowired 145 @Autowired
144 protected EntityViewService entityViewService; 146 protected EntityViewService entityViewService;
145 147
  148 + @Autowired
  149 + protected TelemetrySubscriptionService tsSubService;
  150 +
  151 + @Autowired
  152 + protected AttributesService attributesService;
  153 +
146 @ExceptionHandler(ThingsboardException.class) 154 @ExceptionHandler(ThingsboardException.class)
147 public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) { 155 public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
148 errorResponseHandler.handle(ex, response); 156 errorResponseHandler.handle(ex, response);
@@ -15,7 +15,10 @@ @@ -15,7 +15,10 @@
15 */ 15 */
16 package org.thingsboard.server.controller; 16 package org.thingsboard.server.controller;
17 17
  18 +import com.google.common.util.concurrent.FutureCallback;
  19 +import com.google.common.util.concurrent.Futures;
18 import com.google.common.util.concurrent.ListenableFuture; 20 import com.google.common.util.concurrent.ListenableFuture;
  21 +import lombok.extern.slf4j.Slf4j;
19 import org.springframework.http.HttpStatus; 22 import org.springframework.http.HttpStatus;
20 import org.springframework.security.access.prepost.PreAuthorize; 23 import org.springframework.security.access.prepost.PreAuthorize;
21 import org.springframework.web.bind.annotation.PathVariable; 24 import org.springframework.web.bind.annotation.PathVariable;
@@ -26,7 +29,9 @@ import org.springframework.web.bind.annotation.RequestParam; @@ -26,7 +29,9 @@ import org.springframework.web.bind.annotation.RequestParam;
26 import org.springframework.web.bind.annotation.ResponseBody; 29 import org.springframework.web.bind.annotation.ResponseBody;
27 import org.springframework.web.bind.annotation.ResponseStatus; 30 import org.springframework.web.bind.annotation.ResponseStatus;
28 import org.springframework.web.bind.annotation.RestController; 31 import org.springframework.web.bind.annotation.RestController;
  32 +import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
29 import org.thingsboard.server.common.data.Customer; 33 import org.thingsboard.server.common.data.Customer;
  34 +import org.thingsboard.server.common.data.DataConstants;
30 import org.thingsboard.server.common.data.EntitySubtype; 35 import org.thingsboard.server.common.data.EntitySubtype;
31 import org.thingsboard.server.common.data.EntityType; 36 import org.thingsboard.server.common.data.EntityType;
32 import org.thingsboard.server.common.data.EntityView; 37 import org.thingsboard.server.common.data.EntityView;
@@ -34,15 +39,24 @@ import org.thingsboard.server.common.data.audit.ActionType; @@ -34,15 +39,24 @@ import org.thingsboard.server.common.data.audit.ActionType;
34 import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; 39 import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
35 import org.thingsboard.server.common.data.exception.ThingsboardException; 40 import org.thingsboard.server.common.data.exception.ThingsboardException;
36 import org.thingsboard.server.common.data.id.CustomerId; 41 import org.thingsboard.server.common.data.id.CustomerId;
  42 +import org.thingsboard.server.common.data.id.DeviceId;
  43 +import org.thingsboard.server.common.data.id.EntityId;
37 import org.thingsboard.server.common.data.id.EntityViewId; 44 import org.thingsboard.server.common.data.id.EntityViewId;
38 import org.thingsboard.server.common.data.id.TenantId; 45 import org.thingsboard.server.common.data.id.TenantId;
  46 +import org.thingsboard.server.common.data.id.UUIDBased;
  47 +import org.thingsboard.server.common.data.kv.AttributeKvEntry;
39 import org.thingsboard.server.common.data.page.TextPageData; 48 import org.thingsboard.server.common.data.page.TextPageData;
40 import org.thingsboard.server.common.data.page.TextPageLink; 49 import org.thingsboard.server.common.data.page.TextPageLink;
  50 +import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
41 import org.thingsboard.server.dao.exception.IncorrectParameterException; 51 import org.thingsboard.server.dao.exception.IncorrectParameterException;
42 import org.thingsboard.server.dao.model.ModelConstants; 52 import org.thingsboard.server.dao.model.ModelConstants;
43 import org.thingsboard.server.service.security.model.SecurityUser; 53 import org.thingsboard.server.service.security.model.SecurityUser;
44 54
  55 +import javax.annotation.Nullable;
  56 +import java.util.ArrayList;
  57 +import java.util.Collection;
45 import java.util.List; 58 import java.util.List;
  59 +import java.util.concurrent.ExecutionException;
46 import java.util.stream.Collectors; 60 import java.util.stream.Collectors;
47 61
48 import static org.thingsboard.server.controller.CustomerController.CUSTOMER_ID; 62 import static org.thingsboard.server.controller.CustomerController.CUSTOMER_ID;
@@ -52,6 +66,7 @@ import static org.thingsboard.server.controller.CustomerController.CUSTOMER_ID; @@ -52,6 +66,7 @@ import static org.thingsboard.server.controller.CustomerController.CUSTOMER_ID;
52 */ 66 */
53 @RestController 67 @RestController
54 @RequestMapping("/api") 68 @RequestMapping("/api")
  69 +@Slf4j
55 public class EntityViewController extends BaseController { 70 public class EntityViewController extends BaseController {
56 71
57 public static final String ENTITY_VIEW_ID = "entityViewId"; 72 public static final String ENTITY_VIEW_ID = "entityViewId";
@@ -75,6 +90,20 @@ public class EntityViewController extends BaseController { @@ -75,6 +90,20 @@ public class EntityViewController extends BaseController {
75 try { 90 try {
76 entityView.setTenantId(getCurrentUser().getTenantId()); 91 entityView.setTenantId(getCurrentUser().getTenantId());
77 EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView)); 92 EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView));
  93 + List<ListenableFuture<List<Void>>> futures = new ArrayList<>();
  94 + if (savedEntityView.getKeys() != null && savedEntityView.getKeys().getAttributes() != null) {
  95 + futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.CLIENT_SCOPE, savedEntityView.getKeys().getAttributes().getCs(), getCurrentUser()));
  96 + futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SERVER_SCOPE, savedEntityView.getKeys().getAttributes().getSs(), getCurrentUser()));
  97 + futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SHARED_SCOPE, savedEntityView.getKeys().getAttributes().getSh(), getCurrentUser()));
  98 + }
  99 + for (ListenableFuture<List<Void>> future : futures) {
  100 + try {
  101 + future.get();
  102 + } catch (InterruptedException | ExecutionException e) {
  103 + throw new RuntimeException("Failed to copy attributes to entity view", e);
  104 + }
  105 + }
  106 +
78 logEntityAction(savedEntityView.getId(), savedEntityView, null, 107 logEntityAction(savedEntityView.getId(), savedEntityView, null,
79 entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); 108 entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
80 return savedEntityView; 109 return savedEntityView;
@@ -85,6 +114,56 @@ public class EntityViewController extends BaseController { @@ -85,6 +114,56 @@ public class EntityViewController extends BaseController {
85 } 114 }
86 } 115 }
87 116
  117 + private ListenableFuture<List<Void>> copyAttributesFromEntityToEntityView(EntityView entityView, String scope, Collection<String> keys, SecurityUser user) throws ThingsboardException {
  118 + EntityViewId entityId = entityView.getId();
  119 + if (keys != null && !keys.isEmpty()) {
  120 + ListenableFuture<List<AttributeKvEntry>> getAttrFuture = attributesService.find(entityView.getEntityId(), scope, keys);
  121 + return Futures.transform(getAttrFuture, attributeKvEntries -> {
  122 + List<AttributeKvEntry> attributes;
  123 + if (attributeKvEntries != null && !attributeKvEntries.isEmpty()) {
  124 + attributes =
  125 + attributeKvEntries.stream()
  126 + .filter(attributeKvEntry -> {
  127 + long startTime = entityView.getStartTimeMs();
  128 + long endTime = entityView.getEndTimeMs();
  129 + long lastUpdateTs = attributeKvEntry.getLastUpdateTs();
  130 + return startTime == 0 && endTime == 0 ||
  131 + (endTime == 0 && startTime < lastUpdateTs) ||
  132 + (startTime == 0 && endTime > lastUpdateTs)
  133 + ? true : startTime < lastUpdateTs && endTime > lastUpdateTs;
  134 + }).collect(Collectors.toList());
  135 + tsSubService.saveAndNotify(entityId, scope, attributes, new FutureCallback<Void>() {
  136 + @Override
  137 + public void onSuccess(@Nullable Void tmp) {
  138 + try {
  139 + logAttributesUpdated(user, entityId, scope, attributes, null);
  140 + } catch (ThingsboardException e) {
  141 + log.error("Failed to log attribute updates", e);
  142 + }
  143 + }
  144 +
  145 + @Override
  146 + public void onFailure(Throwable t) {
  147 + try {
  148 + logAttributesUpdated(user, entityId, scope, attributes, t);
  149 + } catch (ThingsboardException e) {
  150 + log.error("Failed to log attribute updates", e);
  151 + }
  152 + }
  153 + });
  154 + }
  155 + return null;
  156 + });
  157 + } else {
  158 + return Futures.immediateFuture(null);
  159 + }
  160 + }
  161 +
  162 + private void logAttributesUpdated(SecurityUser user, EntityId entityId, String scope, List<AttributeKvEntry> attributes, Throwable e) throws ThingsboardException {
  163 + logEntityAction(user, (UUIDBased & EntityId) entityId, null, null, ActionType.ATTRIBUTES_UPDATED, toException(e),
  164 + scope, attributes);
  165 + }
  166 +
88 @PreAuthorize("hasAuthority('TENANT_ADMIN')") 167 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
89 @RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.DELETE) 168 @RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.DELETE)
90 @ResponseStatus(value = HttpStatus.OK) 169 @ResponseStatus(value = HttpStatus.OK)
@@ -94,15 +94,9 @@ import java.util.stream.Collectors; @@ -94,15 +94,9 @@ import java.util.stream.Collectors;
94 public class TelemetryController extends BaseController { 94 public class TelemetryController extends BaseController {
95 95
96 @Autowired 96 @Autowired
97 - private AttributesService attributesService;  
98 -  
99 - @Autowired  
100 private TimeseriesService tsService; 97 private TimeseriesService tsService;
101 98
102 @Autowired 99 @Autowired
103 - private TelemetrySubscriptionService tsSubService;  
104 -  
105 - @Autowired  
106 private AccessValidator accessValidator; 100 private AccessValidator accessValidator;
107 101
108 private ExecutorService executor; 102 private ExecutorService executor;
@@ -143,7 +143,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio @@ -143,7 +143,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
143 public void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub) { 143 public void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub) {
144 long startTime = 0L; 144 long startTime = 0L;
145 long endTime = 0L; 145 long endTime = 0L;
146 - if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { 146 + if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW) && TelemetryFeature.TIMESERIES.equals(sub.getType())) {
147 EntityView entityView = entityViewService.findEntityViewById(new EntityViewId(entityId.getId())); 147 EntityView entityView = entityViewService.findEntityViewById(new EntityViewId(entityId.getId()));
148 entityId = entityView.getEntityId(); 148 entityId = entityView.getEntityId();
149 startTime = entityView.getStartTimeMs(); 149 startTime = entityView.getStartTimeMs();
@@ -165,38 +165,15 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio @@ -165,38 +165,15 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
165 } 165 }
166 166
167 private SubscriptionState getUpdatedSubscriptionState(EntityId entityId, SubscriptionState sub, EntityView entityView) { 167 private SubscriptionState getUpdatedSubscriptionState(EntityId entityId, SubscriptionState sub, EntityView entityView) {
168 - boolean allKeys;  
169 Map<String, Long> keyStates; 168 Map<String, Long> keyStates;
170 - if (sub.getType().equals(TelemetryFeature.TIMESERIES) && !entityView.getKeys().getTimeseries().isEmpty()) {  
171 - allKeys = false; 169 + if(sub.isAllKeys()) {
  170 + keyStates = entityView.getKeys().getTimeseries().stream().collect(Collectors.toMap(k -> k, k -> 0L));
  171 + } else {
172 keyStates = sub.getKeyStates().entrySet() 172 keyStates = sub.getKeyStates().entrySet()
173 .stream().filter(entry -> entityView.getKeys().getTimeseries().contains(entry.getKey())) 173 .stream().filter(entry -> entityView.getKeys().getTimeseries().contains(entry.getKey()))
174 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 174 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
175 - } else if (sub.getType().equals(TelemetryFeature.ATTRIBUTES)) {  
176 - if (sub.getScope().equals(DataConstants.CLIENT_SCOPE) && !entityView.getKeys().getAttributes().getCs().isEmpty()) {  
177 - allKeys = false;  
178 - keyStates = filterMap(sub, entityView.getKeys().getAttributes().getCs());  
179 - } else if (sub.getScope().equals(DataConstants.SERVER_SCOPE) && !entityView.getKeys().getAttributes().getSs().isEmpty()) {  
180 - allKeys = false;  
181 - keyStates = filterMap(sub, entityView.getKeys().getAttributes().getSs());  
182 - } else if (sub.getScope().equals(DataConstants.SERVER_SCOPE) && !entityView.getKeys().getAttributes().getSh().isEmpty()) {  
183 - allKeys = false;  
184 - keyStates = filterMap(sub, entityView.getKeys().getAttributes().getSh());  
185 - } else {  
186 - allKeys = sub.isAllKeys();  
187 - keyStates = sub.getKeyStates();  
188 - }  
189 - } else {  
190 - allKeys = sub.isAllKeys();  
191 - keyStates = sub.getKeyStates();  
192 } 175 }
193 - return new SubscriptionState(sub.getWsSessionId(), sub.getSubscriptionId(), entityId, sub.getType(), allKeys, keyStates, sub.getScope());  
194 - }  
195 -  
196 - private Map<String, Long> filterMap(SubscriptionState sub, List<String> allowedKeys) {  
197 - return sub.getKeyStates().entrySet()  
198 - .stream().filter(entry -> allowedKeys.contains(entry.getKey()))  
199 - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 176 + return new SubscriptionState(sub.getWsSessionId(), sub.getSubscriptionId(), entityId, sub.getType(), false, keyStates, sub.getScope());
200 } 177 }
201 178
202 @Override 179 @Override
@@ -467,7 +444,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio @@ -467,7 +444,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
467 onLocalSubUpdate(entityId, s -> TelemetryFeature.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> { 444 onLocalSubUpdate(entityId, s -> TelemetryFeature.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> {
468 List<TsKvEntry> subscriptionUpdate = null; 445 List<TsKvEntry> subscriptionUpdate = null;
469 for (AttributeKvEntry kv : attributes) { 446 for (AttributeKvEntry kv : attributes) {
470 - if (isInTimeRange(s, kv.getLastUpdateTs()) && (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey()))) { 447 + if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
471 if (subscriptionUpdate == null) { 448 if (subscriptionUpdate == null) {
472 subscriptionUpdate = new ArrayList<>(); 449 subscriptionUpdate = new ArrayList<>();
473 } 450 }
@@ -29,6 +29,7 @@ import org.thingsboard.server.dao.asset.AssetService; @@ -29,6 +29,7 @@ import org.thingsboard.server.dao.asset.AssetService;
29 import org.thingsboard.server.dao.customer.CustomerService; 29 import org.thingsboard.server.dao.customer.CustomerService;
30 import org.thingsboard.server.dao.dashboard.DashboardService; 30 import org.thingsboard.server.dao.dashboard.DashboardService;
31 import org.thingsboard.server.dao.device.DeviceService; 31 import org.thingsboard.server.dao.device.DeviceService;
  32 +import org.thingsboard.server.dao.entityview.EntityViewService;
32 import org.thingsboard.server.dao.rule.RuleChainService; 33 import org.thingsboard.server.dao.rule.RuleChainService;
33 import org.thingsboard.server.dao.tenant.TenantService; 34 import org.thingsboard.server.dao.tenant.TenantService;
34 import org.thingsboard.server.dao.user.UserService; 35 import org.thingsboard.server.dao.user.UserService;
@@ -47,6 +48,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe @@ -47,6 +48,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
47 private DeviceService deviceService; 48 private DeviceService deviceService;
48 49
49 @Autowired 50 @Autowired
  51 + private EntityViewService entityViewService;
  52 +
  53 + @Autowired
50 private TenantService tenantService; 54 private TenantService tenantService;
51 55
52 @Autowired 56 @Autowired
@@ -81,6 +85,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe @@ -81,6 +85,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
81 case DEVICE: 85 case DEVICE:
82 hasName = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId())); 86 hasName = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId()));
83 break; 87 break;
  88 + case ENTITY_VIEW:
  89 + hasName = entityViewService.findEntityViewByIdAsync(new EntityViewId(entityId.getId()));
  90 + break;
84 case TENANT: 91 case TENANT:
85 hasName = tenantService.findTenantByIdAsync(new TenantId(entityId.getId())); 92 hasName = tenantService.findTenantByIdAsync(new TenantId(entityId.getId()));
86 break; 93 break;
@@ -84,8 +84,7 @@ public class CassandraEntityViewDao extends CassandraAbstractSearchTextDao<Entit @@ -84,8 +84,7 @@ public class CassandraEntityViewDao extends CassandraAbstractSearchTextDao<Entit
84 @Override 84 @Override
85 public EntityView save(EntityView domain) { 85 public EntityView save(EntityView domain) {
86 EntityView savedEntityView = super.save(domain); 86 EntityView savedEntityView = super.save(domain);
87 - EntitySubtype entitySubtype = new EntitySubtype(savedEntityView.getTenantId(), EntityType.ENTITY_VIEW,  
88 - savedEntityView.getId().getEntityType().toString()); 87 + EntitySubtype entitySubtype = new EntitySubtype(savedEntityView.getTenantId(), EntityType.ENTITY_VIEW, savedEntityView.getType());
89 EntitySubtypeEntity entitySubtypeEntity = new EntitySubtypeEntity(entitySubtype); 88 EntitySubtypeEntity entitySubtypeEntity = new EntitySubtypeEntity(entitySubtype);
90 Statement saveStatement = cluster.getMapper(EntitySubtypeEntity.class).saveQuery(entitySubtypeEntity); 89 Statement saveStatement = cluster.getMapper(EntitySubtypeEntity.class).saveQuery(entitySubtypeEntity);
91 executeWrite(saveStatement); 90 executeWrite(saveStatement);
@@ -105,21 +105,6 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti @@ -105,21 +105,6 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
105 log.trace("Executing save entity view [{}]", entityView); 105 log.trace("Executing save entity view [{}]", entityView);
106 entityViewValidator.validate(entityView); 106 entityViewValidator.validate(entityView);
107 EntityView savedEntityView = entityViewDao.save(entityView); 107 EntityView savedEntityView = entityViewDao.save(entityView);
108 -  
109 - List<ListenableFuture<List<Void>>> futures = new ArrayList<>();  
110 - if (savedEntityView.getKeys() != null) {  
111 - futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.CLIENT_SCOPE, savedEntityView.getKeys().getAttributes().getCs()));  
112 - futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SERVER_SCOPE, savedEntityView.getKeys().getAttributes().getSs()));  
113 - futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SHARED_SCOPE, savedEntityView.getKeys().getAttributes().getSh()));  
114 - }  
115 - for (ListenableFuture<List<Void>> future : futures) {  
116 - try {  
117 - future.get();  
118 - } catch (InterruptedException | ExecutionException e) {  
119 - log.error("Failed to copy attributes to entity view", e);  
120 - throw new RuntimeException("Failed to copy attributes to entity view", e);  
121 - }  
122 - }  
123 return savedEntityView; 108 return savedEntityView;
124 } 109 }
125 110
@@ -294,36 +279,6 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti @@ -294,36 +279,6 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
294 }); 279 });
295 } 280 }
296 281
297 - private ListenableFuture<List<Void>> copyAttributesFromEntityToEntityView(EntityView entityView, String scope, Collection<String> keys) {  
298 - if (keys != null && !keys.isEmpty()) {  
299 - ListenableFuture<List<AttributeKvEntry>> getAttrFuture = attributesService.find(entityView.getEntityId(), scope, keys);  
300 - return Futures.transform(getAttrFuture, attributeKvEntries -> {  
301 - List<AttributeKvEntry> filteredAttributes = new ArrayList<>();  
302 - if (attributeKvEntries != null && !attributeKvEntries.isEmpty()) {  
303 - filteredAttributes =  
304 - attributeKvEntries.stream()  
305 - .filter(attributeKvEntry -> {  
306 - long startTime = entityView.getStartTimeMs();  
307 - long endTime = entityView.getEndTimeMs();  
308 - long lastUpdateTs = attributeKvEntry.getLastUpdateTs();  
309 - return startTime == 0 && endTime == 0 ||  
310 - (endTime == 0 && startTime < lastUpdateTs) ||  
311 - (startTime == 0 && endTime > lastUpdateTs)  
312 - ? true : startTime < lastUpdateTs && endTime > lastUpdateTs;  
313 - }).collect(Collectors.toList());  
314 - }  
315 - try {  
316 - return attributesService.save(entityView.getId(), scope, filteredAttributes).get();  
317 - } catch (InterruptedException | ExecutionException e) {  
318 - log.error("Failed to copy attributes to entity view", e);  
319 - throw new RuntimeException("Failed to copy attributes to entity view", e);  
320 - }  
321 - });  
322 - } else {  
323 - return Futures.immediateFuture(null);  
324 - }  
325 - }  
326 -  
327 private DataValidator<EntityView> entityViewValidator = 282 private DataValidator<EntityView> entityViewValidator =
328 new DataValidator<EntityView>() { 283 new DataValidator<EntityView>() {
329 284
@@ -79,12 +79,16 @@ public class BaseTimeseriesService implements TimeseriesService { @@ -79,12 +79,16 @@ public class BaseTimeseriesService implements TimeseriesService {
79 if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { 79 if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) {
80 EntityView entityView = entityViewService.findEntityViewById((EntityViewId) entityId); 80 EntityView entityView = entityViewService.findEntityViewById((EntityViewId) entityId);
81 List<String> filteredKeys = new ArrayList<>(keys); 81 List<String> filteredKeys = new ArrayList<>(keys);
82 - if (!entityView.getKeys().getTimeseries().isEmpty()) { 82 + if (entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null &&
  83 + !entityView.getKeys().getTimeseries().isEmpty()) {
83 filteredKeys.retainAll(entityView.getKeys().getTimeseries()); 84 filteredKeys.retainAll(entityView.getKeys().getTimeseries());
84 } 85 }
85 List<ReadTsKvQuery> queries = 86 List<ReadTsKvQuery> queries =
86 filteredKeys.stream() 87 filteredKeys.stream()
87 - .map(key -> new BaseReadTsKvQuery(key, entityView.getStartTimeMs(), entityView.getEndTimeMs(), 1, "ASC")) 88 + .map(key -> {
  89 + long endTs = entityView.getEndTimeMs() != 0 ? entityView.getEndTimeMs() : Long.MAX_VALUE;
  90 + return new BaseReadTsKvQuery(key, entityView.getStartTimeMs(), endTs, 1, "DESC");
  91 + })
88 .collect(Collectors.toList()); 92 .collect(Collectors.toList());
89 93
90 if (queries.size() > 0) { 94 if (queries.size() > 0) {
@@ -100,7 +104,17 @@ public class BaseTimeseriesService implements TimeseriesService { @@ -100,7 +104,17 @@ public class BaseTimeseriesService implements TimeseriesService {
100 @Override 104 @Override
101 public ListenableFuture<List<TsKvEntry>> findAllLatest(EntityId entityId) { 105 public ListenableFuture<List<TsKvEntry>> findAllLatest(EntityId entityId) {
102 validate(entityId); 106 validate(entityId);
103 - return timeseriesDao.findAllLatest(entityId); 107 + if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) {
  108 + EntityView entityView = entityViewService.findEntityViewById((EntityViewId) entityId);
  109 + if (entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null &&
  110 + !entityView.getKeys().getTimeseries().isEmpty()) {
  111 + return findLatest(entityId, entityView.getKeys().getTimeseries());
  112 + } else {
  113 + return Futures.immediateFuture(new ArrayList<>());
  114 + }
  115 + } else {
  116 + return timeseriesDao.findAllLatest(entityId);
  117 + }
104 } 118 }
105 119
106 @Override 120 @Override
@@ -45,6 +45,7 @@ import org.thingsboard.server.dao.customer.CustomerService; @@ -45,6 +45,7 @@ import org.thingsboard.server.dao.customer.CustomerService;
45 import org.thingsboard.server.dao.dashboard.DashboardService; 45 import org.thingsboard.server.dao.dashboard.DashboardService;
46 import org.thingsboard.server.dao.device.DeviceCredentialsService; 46 import org.thingsboard.server.dao.device.DeviceCredentialsService;
47 import org.thingsboard.server.dao.device.DeviceService; 47 import org.thingsboard.server.dao.device.DeviceService;
  48 +import org.thingsboard.server.dao.entityview.EntityViewService;
48 import org.thingsboard.server.dao.event.EventService; 49 import org.thingsboard.server.dao.event.EventService;
49 import org.thingsboard.server.dao.relation.RelationService; 50 import org.thingsboard.server.dao.relation.RelationService;
50 import org.thingsboard.server.dao.rule.RuleChainService; 51 import org.thingsboard.server.dao.rule.RuleChainService;
@@ -89,6 +90,9 @@ public abstract class AbstractServiceTest { @@ -89,6 +90,9 @@ public abstract class AbstractServiceTest {
89 protected AssetService assetService; 90 protected AssetService assetService;
90 91
91 @Autowired 92 @Autowired
  93 + protected EntityViewService entityViewService;
  94 +
  95 + @Autowired
92 protected DeviceCredentialsService deviceCredentialsService; 96 protected DeviceCredentialsService deviceCredentialsService;
93 97
94 @Autowired 98 @Autowired
@@ -17,9 +17,15 @@ package org.thingsboard.server.dao.service.timeseries; @@ -17,9 +17,15 @@ package org.thingsboard.server.dao.service.timeseries;
17 17
18 import com.datastax.driver.core.utils.UUIDs; 18 import com.datastax.driver.core.utils.UUIDs;
19 import lombok.extern.slf4j.Slf4j; 19 import lombok.extern.slf4j.Slf4j;
  20 +import org.junit.After;
20 import org.junit.Assert; 21 import org.junit.Assert;
  22 +import org.junit.Before;
21 import org.junit.Test; 23 import org.junit.Test;
  24 +import org.thingsboard.server.common.data.EntityView;
  25 +import org.thingsboard.server.common.data.Tenant;
22 import org.thingsboard.server.common.data.id.DeviceId; 26 import org.thingsboard.server.common.data.id.DeviceId;
  27 +import org.thingsboard.server.common.data.id.EntityId;
  28 +import org.thingsboard.server.common.data.id.TenantId;
23 import org.thingsboard.server.common.data.kv.Aggregation; 29 import org.thingsboard.server.common.data.kv.Aggregation;
24 import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; 30 import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery;
25 import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; 31 import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
@@ -30,6 +36,7 @@ import org.thingsboard.server.common.data.kv.KvEntry; @@ -30,6 +36,7 @@ import org.thingsboard.server.common.data.kv.KvEntry;
30 import org.thingsboard.server.common.data.kv.LongDataEntry; 36 import org.thingsboard.server.common.data.kv.LongDataEntry;
31 import org.thingsboard.server.common.data.kv.StringDataEntry; 37 import org.thingsboard.server.common.data.kv.StringDataEntry;
32 import org.thingsboard.server.common.data.kv.TsKvEntry; 38 import org.thingsboard.server.common.data.kv.TsKvEntry;
  39 +import org.thingsboard.server.common.data.objects.TelemetryEntityView;
33 import org.thingsboard.server.dao.service.AbstractServiceTest; 40 import org.thingsboard.server.dao.service.AbstractServiceTest;
34 41
35 import java.util.ArrayList; 42 import java.util.ArrayList;
@@ -61,6 +68,22 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @@ -61,6 +68,22 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
61 KvEntry doubleKvEntry = new DoubleDataEntry(DOUBLE_KEY, Double.MAX_VALUE); 68 KvEntry doubleKvEntry = new DoubleDataEntry(DOUBLE_KEY, Double.MAX_VALUE);
62 KvEntry booleanKvEntry = new BooleanDataEntry(BOOLEAN_KEY, Boolean.TRUE); 69 KvEntry booleanKvEntry = new BooleanDataEntry(BOOLEAN_KEY, Boolean.TRUE);
63 70
  71 + private TenantId tenantId;
  72 +
  73 + @Before
  74 + public void before() {
  75 + Tenant tenant = new Tenant();
  76 + tenant.setTitle("My tenant");
  77 + Tenant savedTenant = tenantService.saveTenant(tenant);
  78 + Assert.assertNotNull(savedTenant);
  79 + tenantId = savedTenant.getId();
  80 + }
  81 +
  82 + @After
  83 + public void after() {
  84 + tenantService.deleteTenant(tenantId);
  85 + }
  86 +
64 @Test 87 @Test
65 public void testFindAllLatest() throws Exception { 88 public void testFindAllLatest() throws Exception {
66 DeviceId deviceId = new DeviceId(UUIDs.timeBased()); 89 DeviceId deviceId = new DeviceId(UUIDs.timeBased());
@@ -69,7 +92,15 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @@ -69,7 +92,15 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
69 saveEntries(deviceId, TS - 1); 92 saveEntries(deviceId, TS - 1);
70 saveEntries(deviceId, TS); 93 saveEntries(deviceId, TS);
71 94
72 - List<TsKvEntry> tsList = tsService.findAllLatest(deviceId).get(); 95 + testLatestTsAndVerify(deviceId);
  96 +
  97 + EntityView entityView = saveAndCreateEntityView(deviceId, Arrays.asList(STRING_KEY, DOUBLE_KEY, LONG_KEY, BOOLEAN_KEY));
  98 +
  99 + testLatestTsAndVerify(entityView.getId());
  100 + }
  101 +
  102 + private void testLatestTsAndVerify(EntityId entityId) throws ExecutionException, InterruptedException {
  103 + List<TsKvEntry> tsList = tsService.findAllLatest(entityId).get();
73 104
74 assertNotNull(tsList); 105 assertNotNull(tsList);
75 assertEquals(4, tsList.size()); 106 assertEquals(4, tsList.size());
@@ -89,6 +120,18 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @@ -89,6 +120,18 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
89 assertEquals(expected, tsList); 120 assertEquals(expected, tsList);
90 } 121 }
91 122
  123 + private EntityView saveAndCreateEntityView(DeviceId deviceId, List<String> timeseries) {
  124 + EntityView entityView = new EntityView();
  125 + entityView.setName("entity_view_name");
  126 + entityView.setType("default");
  127 + entityView.setTenantId(tenantId);
  128 + TelemetryEntityView keys = new TelemetryEntityView();
  129 + keys.setTimeseries(timeseries);
  130 + entityView.setKeys(keys);
  131 + entityView.setEntityId(deviceId);
  132 + return entityViewService.saveEntityView(entityView);
  133 + }
  134 +
92 @Test 135 @Test
93 public void testFindLatest() throws Exception { 136 public void testFindLatest() throws Exception {
94 DeviceId deviceId = new DeviceId(UUIDs.timeBased()); 137 DeviceId deviceId = new DeviceId(UUIDs.timeBased());
@@ -100,6 +143,12 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @@ -100,6 +143,12 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
100 List<TsKvEntry> entries = tsService.findLatest(deviceId, Collections.singleton(STRING_KEY)).get(); 143 List<TsKvEntry> entries = tsService.findLatest(deviceId, Collections.singleton(STRING_KEY)).get();
101 Assert.assertEquals(1, entries.size()); 144 Assert.assertEquals(1, entries.size());
102 Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0)); 145 Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0));
  146 +
  147 + EntityView entityView = saveAndCreateEntityView(deviceId, Arrays.asList(STRING_KEY));
  148 +
  149 + entries = tsService.findLatest(entityView.getId(), Collections.singleton(STRING_KEY)).get();
  150 + Assert.assertEquals(1, entries.size());
  151 + Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0));
103 } 152 }
104 153
105 @Test 154 @Test
@@ -7,4 +7,4 @@ WEB_UI_DOCKER_NAME=tb-web-ui @@ -7,4 +7,4 @@ WEB_UI_DOCKER_NAME=tb-web-ui
7 7
8 TB_VERSION=2.2.0-SNAPSHOT 8 TB_VERSION=2.2.0-SNAPSHOT
9 9
10 -KAFKA_TOPICS=js.eval.requests:100:1 10 +KAFKA_TOPICS=js.eval.requests:100:1:delete --config=retention.ms=60000 --config=retention.bytes=1073741824
@@ -36,6 +36,9 @@ services: @@ -36,6 +36,9 @@ services:
36 KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE 36 KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE
37 KAFKA_CREATE_TOPICS: "${KAFKA_TOPICS}" 37 KAFKA_CREATE_TOPICS: "${KAFKA_TOPICS}"
38 KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'false' 38 KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'false'
  39 + KAFKA_LOG_RETENTION_BYTES: 1073741824
  40 + KAFKA_LOG_RETENTION_MS: 300000
  41 + KAFKA_LOG_CLEANUP_POLICY: delete
39 depends_on: 42 depends_on:
40 - zookeeper 43 - zookeeper
41 tb-js-executor: 44 tb-js-executor:
@@ -17,7 +17,9 @@ package org.thingsboard.rule.engine.action; @@ -17,7 +17,9 @@ package org.thingsboard.rule.engine.action;
17 17
18 import com.google.common.util.concurrent.FutureCallback; 18 import com.google.common.util.concurrent.FutureCallback;
19 import com.google.common.util.concurrent.ListenableFuture; 19 import com.google.common.util.concurrent.ListenableFuture;
  20 +import com.google.gson.JsonElement;
20 import com.google.gson.JsonParser; 21 import com.google.gson.JsonParser;
  22 +import com.google.gson.JsonPrimitive;
21 import lombok.extern.slf4j.Slf4j; 23 import lombok.extern.slf4j.Slf4j;
22 import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; 24 import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
23 import org.thingsboard.rule.engine.api.RuleNode; 25 import org.thingsboard.rule.engine.api.RuleNode;
@@ -25,7 +27,6 @@ import org.thingsboard.rule.engine.api.TbContext; @@ -25,7 +27,6 @@ import org.thingsboard.rule.engine.api.TbContext;
25 import org.thingsboard.rule.engine.api.TbNode; 27 import org.thingsboard.rule.engine.api.TbNode;
26 import org.thingsboard.rule.engine.api.TbNodeConfiguration; 28 import org.thingsboard.rule.engine.api.TbNodeConfiguration;
27 import org.thingsboard.rule.engine.api.TbNodeException; 29 import org.thingsboard.rule.engine.api.TbNodeException;
28 -import org.thingsboard.rule.engine.api.TbRelationTypes;  
29 import org.thingsboard.rule.engine.api.util.DonAsynchron; 30 import org.thingsboard.rule.engine.api.util.DonAsynchron;
30 import org.thingsboard.rule.engine.api.util.TbNodeUtils; 31 import org.thingsboard.rule.engine.api.util.TbNodeUtils;
31 import org.thingsboard.server.common.data.DataConstants; 32 import org.thingsboard.server.common.data.DataConstants;
@@ -37,12 +38,11 @@ import org.thingsboard.server.common.msg.session.SessionMsgType; @@ -37,12 +38,11 @@ import org.thingsboard.server.common.msg.session.SessionMsgType;
37 import org.thingsboard.server.common.transport.adaptor.JsonConverter; 38 import org.thingsboard.server.common.transport.adaptor.JsonConverter;
38 39
39 import javax.annotation.Nullable; 40 import javax.annotation.Nullable;
  41 +import java.util.ArrayList;
40 import java.util.List; 42 import java.util.List;
41 import java.util.Set; 43 import java.util.Set;
42 -import java.util.concurrent.ExecutionException;  
43 import java.util.stream.Collectors; 44 import java.util.stream.Collectors;
44 45
45 -import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;  
46 import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; 46 import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
47 47
48 @Slf4j 48 @Slf4j
@@ -68,70 +68,89 @@ public class TbCopyAttributesToEntityViewNode implements TbNode { @@ -68,70 +68,89 @@ public class TbCopyAttributesToEntityViewNode implements TbNode {
68 } 68 }
69 69
70 @Override 70 @Override
71 - public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {  
72 - if (!msg.getMetaData().getData().isEmpty()) {  
73 - long now = System.currentTimeMillis();  
74 - String scope = msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()) ?  
75 - DataConstants.CLIENT_SCOPE : msg.getMetaData().getValue("scope"); 71 + public void onMsg(TbContext ctx, TbMsg msg) {
  72 + if (DataConstants.ATTRIBUTES_UPDATED.equals(msg.getType()) ||
  73 + DataConstants.ATTRIBUTES_DELETED.equals(msg.getType()) ||
  74 + SessionMsgType.POST_ATTRIBUTES_REQUEST.name().equals(msg.getType())) {
  75 + if (!msg.getMetaData().getData().isEmpty()) {
  76 + long now = System.currentTimeMillis();
  77 + String scope = msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()) ?
  78 + DataConstants.CLIENT_SCOPE : msg.getMetaData().getValue("scope");
76 79
77 - ListenableFuture<List<EntityView>> entityViewsFuture =  
78 - ctx.getEntityViewService().findEntityViewsByTenantIdAndEntityIdAsync(ctx.getTenantId(), msg.getOriginator()); 80 + ListenableFuture<List<EntityView>> entityViewsFuture =
  81 + ctx.getEntityViewService().findEntityViewsByTenantIdAndEntityIdAsync(ctx.getTenantId(), msg.getOriginator());
79 82
80 - DonAsynchron.withCallback(entityViewsFuture,  
81 - entityViews -> {  
82 - for (EntityView entityView : entityViews) {  
83 - long startTime = entityView.getStartTimeMs();  
84 - long endTime = entityView.getEndTimeMs();  
85 - if ((endTime != 0 && endTime > now && startTime < now) || (endTime == 0 && startTime < now)) {  
86 - Set<AttributeKvEntry> attributes =  
87 - JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())).getAttributes();  
88 - List<AttributeKvEntry> filteredAttributes =  
89 - attributes.stream()  
90 - .filter(attr -> {  
91 - switch (scope) {  
92 - case DataConstants.CLIENT_SCOPE:  
93 - if (entityView.getKeys().getAttributes().getCs().isEmpty()) {  
94 - return true;  
95 - }  
96 - return entityView.getKeys().getAttributes().getCs().contains(attr.getKey());  
97 - case DataConstants.SERVER_SCOPE:  
98 - if (entityView.getKeys().getAttributes().getSs().isEmpty()) {  
99 - return true;  
100 - }  
101 - return entityView.getKeys().getAttributes().getSs().contains(attr.getKey());  
102 - case DataConstants.SHARED_SCOPE:  
103 - if (entityView.getKeys().getAttributes().getSh().isEmpty()) {  
104 - return true;  
105 - }  
106 - return entityView.getKeys().getAttributes().getSh().contains(attr.getKey()); 83 + DonAsynchron.withCallback(entityViewsFuture,
  84 + entityViews -> {
  85 + for (EntityView entityView : entityViews) {
  86 + long startTime = entityView.getStartTimeMs();
  87 + long endTime = entityView.getEndTimeMs();
  88 + if ((endTime != 0 && endTime > now && startTime < now) || (endTime == 0 && startTime < now)) {
  89 + if (DataConstants.ATTRIBUTES_UPDATED.equals(msg.getType()) ||
  90 + SessionMsgType.POST_ATTRIBUTES_REQUEST.name().equals(msg.getType())) {
  91 + Set<AttributeKvEntry> attributes =
  92 + JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())).getAttributes();
  93 + List<AttributeKvEntry> filteredAttributes =
  94 + attributes.stream().filter(attr -> attributeContainsInEntityView(scope, attr.getKey(), entityView)).collect(Collectors.toList());
  95 + ctx.getTelemetryService().saveAndNotify(entityView.getId(), scope, filteredAttributes,
  96 + new FutureCallback<Void>() {
  97 + @Override
  98 + public void onSuccess(@Nullable Void result) {
  99 + transformAndTellNext(ctx, msg, entityView);
107 } 100 }
108 - return false;  
109 - }).collect(Collectors.toList());  
110 101
111 - ctx.getTelemetryService().saveAndNotify(entityView.getId(), scope, filteredAttributes,  
112 - new FutureCallback<Void>() {  
113 - @Override  
114 - public void onSuccess(@Nullable Void result) {  
115 - TbMsg updMsg = ctx.transformMsg(msg, msg.getType(), entityView.getId(), msg.getMetaData(), msg.getData());  
116 - ctx.tellNext(updMsg, SUCCESS);  
117 - }  
118 -  
119 - @Override  
120 - public void onFailure(Throwable t) {  
121 - ctx.tellFailure(msg, t); 102 + @Override
  103 + public void onFailure(Throwable t) {
  104 + ctx.tellFailure(msg, t);
  105 + }
  106 + });
  107 + } else if (DataConstants.ATTRIBUTES_DELETED.equals(msg.getType())) {
  108 + List<String> attributes = new ArrayList<>();
  109 + for (JsonElement element : new JsonParser().parse(msg.getData()).getAsJsonObject().get("attributes").getAsJsonArray()) {
  110 + if (element.isJsonPrimitive()) {
  111 + JsonPrimitive value = element.getAsJsonPrimitive();
  112 + if (value.isString()) {
  113 + attributes.add(value.getAsString());
  114 + }
122 } 115 }
123 - }); 116 + }
  117 + List<String> filteredAttributes =
  118 + attributes.stream().filter(attr -> attributeContainsInEntityView(scope, attr, entityView)).collect(Collectors.toList());
  119 + if (filteredAttributes != null && !filteredAttributes.isEmpty()) {
  120 + ctx.getAttributesService().removeAll(entityView.getId(), scope, filteredAttributes);
  121 + transformAndTellNext(ctx, msg, entityView);
  122 + }
  123 + }
  124 + }
124 } 125 }
125 - }  
126 - },  
127 - t -> ctx.tellFailure(msg, t)); 126 + },
  127 + t -> ctx.tellFailure(msg, t));
  128 + } else {
  129 + ctx.tellFailure(msg, new IllegalArgumentException("Message metadata is empty"));
  130 + }
128 } else { 131 } else {
129 - ctx.tellNext(msg, FAILURE); 132 + ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type [" + msg.getType() + "]"));
130 } 133 }
131 } 134 }
132 135
  136 + private void transformAndTellNext(TbContext ctx, TbMsg msg, EntityView entityView) {
  137 + TbMsg updMsg = ctx.transformMsg(msg, msg.getType(), entityView.getId(), msg.getMetaData(), msg.getData());
  138 + ctx.tellNext(updMsg, SUCCESS);
  139 + }
  140 +
  141 + private boolean attributeContainsInEntityView(String scope, String attrKey, EntityView entityView) {
  142 + switch (scope) {
  143 + case DataConstants.CLIENT_SCOPE:
  144 + return entityView.getKeys().getAttributes().getCs().contains(attrKey);
  145 + case DataConstants.SERVER_SCOPE:
  146 + return entityView.getKeys().getAttributes().getSs().contains(attrKey);
  147 + case DataConstants.SHARED_SCOPE:
  148 + return entityView.getKeys().getAttributes().getSh().contains(attrKey);
  149 + }
  150 + return false;
  151 + }
  152 +
133 @Override 153 @Override
134 public void destroy() { 154 public void destroy() {
135 -  
136 } 155 }
137 } 156 }
@@ -27,7 +27,6 @@ function EntityViewService($http, $q, $window, userService, attributeService, cu @@ -27,7 +27,6 @@ function EntityViewService($http, $q, $window, userService, attributeService, cu
27 deleteEntityView: deleteEntityView, 27 deleteEntityView: deleteEntityView,
28 getCustomerEntityViews: getCustomerEntityViews, 28 getCustomerEntityViews: getCustomerEntityViews,
29 getEntityView: getEntityView, 29 getEntityView: getEntityView,
30 - getEntityViews: getEntityViews,  
31 getTenantEntityViews: getTenantEntityViews, 30 getTenantEntityViews: getTenantEntityViews,
32 saveEntityView: saveEntityView, 31 saveEntityView: saveEntityView,
33 unassignEntityViewFromCustomer: unassignEntityViewFromCustomer, 32 unassignEntityViewFromCustomer: unassignEntityViewFromCustomer,
@@ -126,32 +125,6 @@ function EntityViewService($http, $q, $window, userService, attributeService, cu @@ -126,32 +125,6 @@ function EntityViewService($http, $q, $window, userService, attributeService, cu
126 return deferred.promise; 125 return deferred.promise;
127 } 126 }
128 127
129 - function getEntityViews(entityViewIds, config) {  
130 - var deferred = $q.defer();  
131 - var ids = '';  
132 - for (var i=0;i<entityViewIds.length;i++) {  
133 - if (i>0) {  
134 - ids += ',';  
135 - }  
136 - ids += entityViewIds[i];  
137 - }  
138 - var url = '/api/entityViews?entityViewIds=' + ids;  
139 - $http.get(url, config).then(function success(response) {  
140 - var entityViews = response.data;  
141 - entityViews.sort(function (entityView1, entityView2) {  
142 - var id1 = entityView1.id.id;  
143 - var id2 = entityView2.id.id;  
144 - var index1 = entityViewIds.indexOf(id1);  
145 - var index2 = entityViewIds.indexOf(id2);  
146 - return index1 - index2;  
147 - });  
148 - deferred.resolve(entityViews);  
149 - }, function fail(response) {  
150 - deferred.reject(response.data);  
151 - });  
152 - return deferred.promise;  
153 - }  
154 -  
155 function saveEntityView(entityView) { 128 function saveEntityView(entityView) {
156 var deferred = $q.defer(); 129 var deferred = $q.defer();
157 var url = '/api/entityView'; 130 var url = '/api/entityView';
@@ -135,6 +135,10 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -135,6 +135,10 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
135 case types.entityType.asset: 135 case types.entityType.asset:
136 promise = assetService.getAssets(entityIds, config); 136 promise = assetService.getAssets(entityIds, config);
137 break; 137 break;
  138 + case types.entityType.entityView:
  139 + promise = getEntitiesByIdsPromise(
  140 + (id) => entityViewService.getEntityView(id, config), entityIds);
  141 + break;
138 case types.entityType.tenant: 142 case types.entityType.tenant:
139 promise = getEntitiesByIdsPromise( 143 promise = getEntitiesByIdsPromise(
140 (id) => tenantService.getTenant(id, config), entityIds); 144 (id) => tenantService.getTenant(id, config), entityIds);
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<md-dialog aria-label="{{ 'entity-view.add' | translate }}" tb-help="'entityViews'" help-container-id="help-container"> 18 +<md-dialog aria-label="{{ 'entity-view.add' | translate }}" style="width: 800px;" tb-help="'entityViews'" help-container-id="help-container">
19 <form name="theForm" ng-submit="vm.add()"> 19 <form name="theForm" ng-submit="vm.add()">
20 <md-toolbar> 20 <md-toolbar>
21 <div class="md-toolbar-tools"> 21 <div class="md-toolbar-tools">
@@ -60,7 +60,7 @@ @@ -60,7 +60,7 @@
60 entity-type="types.entityType.entityView"> 60 entity-type="types.entityType.entityView">
61 </tb-entity-subtype-autocomplete> 61 </tb-entity-subtype-autocomplete>
62 <section layout="column"> 62 <section layout="column">
63 - <label translate class="tb-title no-padding">entity-view.related-entity</label> 63 + <label translate class="tb-title no-padding">entity-view.target-entity</label>
64 <tb-entity-select flex ng-disabled="!isEdit" 64 <tb-entity-select flex ng-disabled="!isEdit"
65 the-form="theForm" 65 the-form="theForm"
66 tb-required="true" 66 tb-required="true"
@@ -68,62 +68,145 @@ @@ -68,62 +68,145 @@
68 ng-model="entityView.entityId"> 68 ng-model="entityView.entityId">
69 </tb-entity-select> 69 </tb-entity-select>
70 </section> 70 </section>
  71 + <md-expansion-panel-group class="tb-entity-view-panel-group" ng-class="{'disabled': $root.loading || !isEdit}"
  72 + auto-expand="true"
  73 + multiple="true"
  74 + md-component-id="attributesPanelGroup">
  75 + <md-expansion-panel md-component-id="{{attributesPanelId}}">
  76 + <md-expansion-panel-collapsed>
  77 + <div class="tb-panel-title">{{ 'entity-view.attributes-propagation' | translate }}</div>
  78 + <span flex></span>
  79 + <md-expansion-panel-icon></md-expansion-panel-icon>
  80 + </md-expansion-panel-collapsed>
  81 + <md-expansion-panel-expanded>
  82 + <md-expansion-panel-header ng-click="$mdExpansionPanel(attributesPanelId).collapse()">
  83 + <div class="tb-panel-title">{{ 'entity-view.attributes-propagation' | translate }}</div>
  84 + <span flex></span>
  85 + <md-expansion-panel-icon></md-expansion-panel-icon>
  86 + </md-expansion-panel-header>
  87 + <md-expansion-panel-content>
  88 + <div translate class="tb-hint">entity-view.attributes-propagation-hint</div>
  89 + <label translate class="tb-title no-padding">entity-view.client-attributes</label>
  90 + <md-chips style="padding-bottom: 15px;"
  91 + ng-required="false"
  92 + readonly="!isEdit"
  93 + ng-model="entityView.keys.attributes.cs"
  94 + placeholder="{{'entity-view.client-attributes-placeholder' | translate}}"
  95 + md-separator-keys="separatorKeys">
  96 + <md-autocomplete
  97 + md-no-cache="true"
  98 + id="ca_datakey"
  99 + md-selected-item="selectedAttributeDataKey"
  100 + md-search-text="attributeDataKeySearchText"
  101 + md-items="item in dataKeysSearch(attributeDataKeySearchText, types.dataKeyType.attribute)"
  102 + md-item-text="item.name"
  103 + md-min-length="0"
  104 + placeholder="{{'entity-view.client-attributes-placeholder' | translate }}"
  105 + md-menu-class="tb-attribute-datakey-autocomplete">
  106 + <span md-highlight-text="attributeDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
  107 + </md-autocomplete>
  108 + </md-chips>
  109 + <label translate class="tb-title no-padding">entity-view.shared-attributes</label>
  110 + <md-chips style="padding-bottom: 15px;"
  111 + ng-required="false"
  112 + readonly="!isEdit"
  113 + ng-model="entityView.keys.attributes.sh"
  114 + placeholder="{{'entity-view.shared-attributes-placeholder' | translate}}"
  115 + md-separator-keys="separatorKeys">
  116 + <md-autocomplete
  117 + md-no-cache="true"
  118 + id="sh_datakey"
  119 + md-selected-item="selectedAttributeDataKey"
  120 + md-search-text="attributeDataKeySearchText"
  121 + md-items="item in dataKeysSearch(attributeDataKeySearchText, types.dataKeyType.attribute)"
  122 + md-item-text="item.name"
  123 + md-min-length="0"
  124 + placeholder="{{'entity-view.server-attributes-placeholder' | translate }}"
  125 + md-menu-class="tb-attribute-datakey-autocomplete">
  126 + <span md-highlight-text="attributeDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
  127 + </md-autocomplete>
  128 + </md-chips>
  129 + <label translate class="tb-title no-padding">entity-view.server-attributes</label>
  130 + <md-chips style="padding-bottom: 15px;"
  131 + ng-required="false"
  132 + readonly="!isEdit"
  133 + ng-model="entityView.keys.attributes.ss"
  134 + placeholder="{{'entity-view.server-attributes-placeholder' | translate}}"
  135 + md-separator-keys="separatorKeys">
  136 + <md-autocomplete
  137 + md-no-cache="true"
  138 + id="ss_datakey"
  139 + md-selected-item="selectedAttributeDataKey"
  140 + md-search-text="attributeDataKeySearchText"
  141 + md-items="item in dataKeysSearch(attributeDataKeySearchText, types.dataKeyType.attribute)"
  142 + md-item-text="item.name"
  143 + md-min-length="0"
  144 + placeholder="{{'entity-view.server-attributes-placeholder' | translate }}"
  145 + md-menu-class="tb-attribute-datakey-autocomplete">
  146 + <span md-highlight-text="attributeDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
  147 + </md-autocomplete>
  148 + </md-chips>
  149 + </md-expansion-panel-content>
  150 + </md-expansion-panel-expanded>
  151 + </md-expansion-panel>
  152 + <md-expansion-panel md-component-id="{{timeseriesPanelId}}">
  153 + <md-expansion-panel-collapsed>
  154 + <div class="tb-panel-title">{{ 'entity-view.timeseries-data' | translate }}</div>
  155 + <span flex></span>
  156 + <md-expansion-panel-icon></md-expansion-panel-icon>
  157 + </md-expansion-panel-collapsed>
  158 + <md-expansion-panel-expanded>
  159 + <md-expansion-panel-header ng-click="$mdExpansionPanel(timeseriesPanelId).collapse()">
  160 + <div class="tb-panel-title">{{ 'entity-view.timeseries-data' | translate }}</div>
  161 + <span flex></span>
  162 + <md-expansion-panel-icon></md-expansion-panel-icon>
  163 + </md-expansion-panel-header>
  164 + <md-expansion-panel-content>
  165 + <div translate class="tb-hint">entity-view.timeseries-data-hint</div>
  166 + <label translate class="tb-title no-padding">entity-view.timeseries</label>
  167 + <md-chips ng-required="false"
  168 + readonly="!isEdit"
  169 + ng-model="entityView.keys.timeseries"
  170 + placeholder="{{'entity-view.timeseries-placeholder' | translate}}"
  171 + md-separator-keys="separatorKeys">
  172 + <md-autocomplete
  173 + md-no-cache="true"
  174 + id="timeseries_datakey"
  175 + md-selected-item="selectedTimeseriesDataKey"
  176 + md-search-text="timeseriesDataKeySearchText"
  177 + md-items="item in dataKeysSearch(timeseriesDataKeySearchText, types.dataKeyType.timeseries)"
  178 + md-item-text="item.name"
  179 + md-min-length="0"
  180 + placeholder="{{'entity-view.timeseries-placeholder' | translate }}"
  181 + md-menu-class="tb-timeseries-datakey-autocomplete">
  182 + <span md-highlight-text="timeseriesDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
  183 + </md-autocomplete>
  184 + </md-chips>
  185 + </md-expansion-panel-content>
  186 + </md-expansion-panel-expanded>
  187 + </md-expansion-panel>
  188 + </md-expansion-panel-group>
  189 + <section layout="row" layout-align="start start">
  190 + <mdp-date-picker ng-model="startTimeMs"
  191 + mdp-max-date="maxStartTimeMs"
  192 + mdp-placeholder="{{ 'entity-view.start-date' | translate }}"></mdp-date-picker>
  193 + <mdp-time-picker ng-model="startTimeMs"
  194 + mdp-max-date="maxStartTimeMs"
  195 + mdp-placeholder="{{ 'entity-view.start-ts' | translate }}"
  196 + mdp-auto-switch="true"></mdp-time-picker>
  197 + </section>
  198 + <section layout="row" layout-align="start start">
  199 + <mdp-date-picker ng-model="endTimeMs"
  200 + mdp-min-date="minEndTimeMs"
  201 + mdp-placeholder="{{ 'entity-view.end-date' | translate }}"></mdp-date-picker>
  202 + <mdp-time-picker ng-model="endTimeMs"
  203 + mdp-min-date="minEndTimeMs"
  204 + mdp-placeholder="{{ 'entity-view.end-ts' | translate }}"
  205 + mdp-auto-switch="true"></mdp-time-picker>
  206 + </section>
71 <md-input-container class="md-block"> 207 <md-input-container class="md-block">
72 <label translate>entity-view.description</label> 208 <label translate>entity-view.description</label>
73 <textarea ng-model="entityView.additionalInfo.description" rows="2"></textarea> 209 <textarea ng-model="entityView.additionalInfo.description" rows="2"></textarea>
74 </md-input-container> 210 </md-input-container>
75 - <section layout="column">  
76 - <label translate class="tb-title no-padding">entity-view.client-attributes</label>  
77 - <md-chips style="padding-bottom: 15px;"  
78 - ng-required="false"  
79 - readonly="!isEdit"  
80 - ng-model="entityView.keys.attributes.cs"  
81 - placeholder="{{'entity-view.client-attributes' | translate}}"  
82 - md-separator-keys="separatorKeys">  
83 - </md-chips>  
84 - <label translate class="tb-title no-padding">entity-view.shared-attributes</label>  
85 - <md-chips style="padding-bottom: 15px;"  
86 - ng-required="false"  
87 - readonly="!isEdit"  
88 - ng-model="entityView.keys.attributes.sh"  
89 - placeholder="{{'entity-view.shared-attributes' | translate}}"  
90 - md-separator-keys="separatorKeys">  
91 - </md-chips>  
92 - <label translate class="tb-title no-padding">entity-view.server-attributes</label>  
93 - <md-chips style="padding-bottom: 15px;"  
94 - ng-required="false"  
95 - readonly="!isEdit"  
96 - ng-model="entityView.keys.attributes.ss"  
97 - placeholder="{{'entity-view.server-attributes' | translate}}"  
98 - md-separator-keys="separatorKeys">  
99 - </md-chips>  
100 - <label translate class="tb-title no-padding">entity-view.latest-timeseries</label>  
101 - <md-chips ng-required="false"  
102 - readonly="!isEdit"  
103 - ng-model="entityView.keys.timeseries"  
104 - placeholder="{{'entity-view.latest-timeseries' | translate}}"  
105 - md-separator-keys="separatorKeys">  
106 - </md-chips>  
107 - </section>  
108 - <section layout="column">  
109 - <section layout="row" layout-align="start start">  
110 - <mdp-date-picker ng-model="startTimeMs"  
111 - mdp-max-date="maxStartTimeMs"  
112 - mdp-placeholder="{{ 'entity-view.start-ts' | translate }}"></mdp-date-picker>  
113 - <mdp-time-picker ng-model="startTimeMs"  
114 - mdp-max-date="maxStartTimeMs"  
115 - mdp-placeholder="{{ 'entity-view.start-ts' | translate }}"  
116 - mdp-auto-switch="true"></mdp-time-picker>  
117 - </section>  
118 - <section layout="row" layout-align="start start">  
119 - <mdp-date-picker ng-model="endTimeMs"  
120 - mdp-min-date="minEndTimeMs"  
121 - mdp-placeholder="{{ 'entity-view.end-ts' | translate }}"></mdp-date-picker>  
122 - <mdp-time-picker ng-model="endTimeMs"  
123 - mdp-min-date="minEndTimeMs"  
124 - mdp-placeholder="{{ 'entity-view.end-ts' | translate }}"  
125 - mdp-auto-switch="true"></mdp-time-picker>  
126 - </section>  
127 - </section>  
128 </fieldset> 211 </fieldset>
129 </md-content> 212 </md-content>
@@ -13,6 +13,9 @@ @@ -13,6 +13,9 @@
13 * See the License for the specific language governing permissions and 13 * See the License for the specific language governing permissions and
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
  16 +
  17 +import './entity-view.scss';
  18 +
16 /* eslint-disable import/no-unresolved, import/default */ 19 /* eslint-disable import/no-unresolved, import/default */
17 20
18 import entityViewFieldsetTemplate from './entity-view-fieldset.tpl.html'; 21 import entityViewFieldsetTemplate from './entity-view-fieldset.tpl.html';
@@ -20,12 +23,16 @@ import entityViewFieldsetTemplate from './entity-view-fieldset.tpl.html'; @@ -20,12 +23,16 @@ import entityViewFieldsetTemplate from './entity-view-fieldset.tpl.html';
20 /* eslint-enable import/no-unresolved, import/default */ 23 /* eslint-enable import/no-unresolved, import/default */
21 24
22 /*@ngInject*/ 25 /*@ngInject*/
23 -export default function EntityViewDirective($compile, $templateCache, $filter, toast, $translate, $mdConstant,  
24 - types, clipboardService, entityViewService, customerService) { 26 +export default function EntityViewDirective($q, $compile, $templateCache, $filter, toast, $translate, $mdConstant, $mdExpansionPanel,
  27 + types, clipboardService, entityViewService, customerService, entityService) {
25 var linker = function (scope, element) { 28 var linker = function (scope, element) {
26 var template = $templateCache.get(entityViewFieldsetTemplate); 29 var template = $templateCache.get(entityViewFieldsetTemplate);
27 element.html(template); 30 element.html(template);
28 31
  32 + scope.attributesPanelId = (Math.random()*1000).toFixed(0);
  33 + scope.timeseriesPanelId = (Math.random()*1000).toFixed(0);
  34 + scope.$mdExpansionPanel = $mdExpansionPanel;
  35 +
29 scope.types = types; 36 scope.types = types;
30 scope.isAssignedToCustomer = false; 37 scope.isAssignedToCustomer = false;
31 scope.isPublic = false; 38 scope.isPublic = false;
@@ -53,9 +60,13 @@ export default function EntityViewDirective($compile, $templateCache, $filter, t @@ -53,9 +60,13 @@ export default function EntityViewDirective($compile, $templateCache, $filter, t
53 } 60 }
54 if (scope.entityView.startTimeMs > 0) { 61 if (scope.entityView.startTimeMs > 0) {
55 scope.startTimeMs = new Date(scope.entityView.startTimeMs); 62 scope.startTimeMs = new Date(scope.entityView.startTimeMs);
  63 + } else {
  64 + scope.startTimeMs = null;
56 } 65 }
57 if (scope.entityView.endTimeMs > 0) { 66 if (scope.entityView.endTimeMs > 0) {
58 scope.endTimeMs = new Date(scope.entityView.endTimeMs); 67 scope.endTimeMs = new Date(scope.entityView.endTimeMs);
  68 + } else {
  69 + scope.endTimeMs = null;
59 } 70 }
60 if (!scope.entityView.keys) { 71 if (!scope.entityView.keys) {
61 scope.entityView.keys = {}; 72 scope.entityView.keys = {};
@@ -68,6 +79,19 @@ export default function EntityViewDirective($compile, $templateCache, $filter, t @@ -68,6 +79,19 @@ export default function EntityViewDirective($compile, $templateCache, $filter, t
68 } 79 }
69 }); 80 });
70 81
  82 + scope.dataKeysSearch = function (searchText, type) {
  83 + var deferred = $q.defer();
  84 + entityService.getEntityKeys(scope.entityView.entityId.entityType, scope.entityView.entityId.id, searchText, type, {ignoreLoading: true}).then(
  85 + function success(keys) {
  86 + deferred.resolve(keys);
  87 + },
  88 + function fail() {
  89 + deferred.resolve([]);
  90 + }
  91 + );
  92 + return deferred.promise;
  93 +
  94 + };
71 95
72 scope.$watch('startTimeMs', function (newDate) { 96 scope.$watch('startTimeMs', function (newDate) {
73 if (newDate) { 97 if (newDate) {
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +@import "../../scss/constants";
  18 +
  19 +.tb-entity-view-panel-group {
  20 + .tb-panel-title {
  21 + min-width: 90px;
  22 + user-select: none;
  23 +
  24 + @media (min-width: $layout-breakpoint-sm) {
  25 + min-width: 180px;
  26 + }
  27 + }
  28 +
  29 + .tb-panel-prompt {
  30 + overflow: hidden;
  31 + font-size: 14px;
  32 + color: rgba(0, 0, 0, .87);
  33 + text-overflow: ellipsis;
  34 + white-space: nowrap;
  35 + }
  36 +
  37 + &.disabled {
  38 + .tb-panel-title,
  39 + .tb-panel-prompt {
  40 + color: rgba(0, 0, 0, .38);
  41 + }
  42 + }
  43 +
  44 + md-icon.md-expansion-panel-icon {
  45 + margin-right: 0;
  46 + }
  47 +}
@@ -838,14 +838,24 @@ @@ -838,14 +838,24 @@
838 "unable-entity-view-device-alias-text": "Device alias '{{entityViewAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}", 838 "unable-entity-view-device-alias-text": "Device alias '{{entityViewAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
839 "select-entity-view": "Select entity view", 839 "select-entity-view": "Select entity view",
840 "make-public": "Make entity view public", 840 "make-public": "Make entity view public",
  841 + "start-date": "Start date",
841 "start-ts": "Start time", 842 "start-ts": "Start time",
  843 + "end-date": "End date",
842 "end-ts": "End time", 844 "end-ts": "End time",
843 "date-limits": "Date limits", 845 "date-limits": "Date limits",
844 "client-attributes": "Client attributes", 846 "client-attributes": "Client attributes",
845 "shared-attributes": "Shared attributes", 847 "shared-attributes": "Shared attributes",
846 "server-attributes": "Server attributes", 848 "server-attributes": "Server attributes",
847 - "latest-timeseries": "Latest timeseries",  
848 - "related-entity": "Related entity" 849 + "timeseries": "Timeseries",
  850 + "client-attributes-placeholder": "Client attributes",
  851 + "shared-attributes-placeholder": "Shared attributes",
  852 + "server-attributes-placeholder": "Server attributes",
  853 + "timeseries-placeholder": "Timeseries",
  854 + "target-entity": "Target entity",
  855 + "attributes-propagation": "Attributes propagation",
  856 + "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.",
  857 + "timeseries-data": "Timeseries data",
  858 + "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."
849 }, 859 },
850 "event": { 860 "event": {
851 "event-type": "Event type", 861 "event-type": "Event type",
@@ -839,7 +839,7 @@ @@ -839,7 +839,7 @@
839 "client-attributes": "Client attributes", 839 "client-attributes": "Client attributes",
840 "shared-attributes": "Shared attributes", 840 "shared-attributes": "Shared attributes",
841 "server-attributes": "Server attributes", 841 "server-attributes": "Server attributes",
842 - "latest-timeseries": "Latest timeseries" 842 + "timeseries": "Timeseries"
843 }, 843 },
844 "event": { 844 "event": {
845 "event-type": "Tipo de evento", 845 "event-type": "Tipo de evento",