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 51 import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
52 52 import org.thingsboard.server.dao.alarm.AlarmService;
53 53 import org.thingsboard.server.dao.asset.AssetService;
  54 +import org.thingsboard.server.dao.attributes.AttributesService;
54 55 import org.thingsboard.server.dao.audit.AuditLogService;
55 56 import org.thingsboard.server.dao.customer.CustomerService;
56 57 import org.thingsboard.server.dao.dashboard.DashboardService;
... ... @@ -70,6 +71,7 @@ import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
70 71 import org.thingsboard.server.service.component.ComponentDiscoveryService;
71 72 import org.thingsboard.server.service.security.model.SecurityUser;
72 73 import org.thingsboard.server.service.state.DeviceStateService;
  74 +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
73 75
74 76 import javax.mail.MessagingException;
75 77 import javax.servlet.http.HttpServletRequest;
... ... @@ -143,6 +145,12 @@ public abstract class BaseController {
143 145 @Autowired
144 146 protected EntityViewService entityViewService;
145 147
  148 + @Autowired
  149 + protected TelemetrySubscriptionService tsSubService;
  150 +
  151 + @Autowired
  152 + protected AttributesService attributesService;
  153 +
146 154 @ExceptionHandler(ThingsboardException.class)
147 155 public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
148 156 errorResponseHandler.handle(ex, response);
... ...
... ... @@ -15,7 +15,10 @@
15 15 */
16 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 20 import com.google.common.util.concurrent.ListenableFuture;
  21 +import lombok.extern.slf4j.Slf4j;
19 22 import org.springframework.http.HttpStatus;
20 23 import org.springframework.security.access.prepost.PreAuthorize;
21 24 import org.springframework.web.bind.annotation.PathVariable;
... ... @@ -26,7 +29,9 @@ import org.springframework.web.bind.annotation.RequestParam;
26 29 import org.springframework.web.bind.annotation.ResponseBody;
27 30 import org.springframework.web.bind.annotation.ResponseStatus;
28 31 import org.springframework.web.bind.annotation.RestController;
  32 +import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
29 33 import org.thingsboard.server.common.data.Customer;
  34 +import org.thingsboard.server.common.data.DataConstants;
30 35 import org.thingsboard.server.common.data.EntitySubtype;
31 36 import org.thingsboard.server.common.data.EntityType;
32 37 import org.thingsboard.server.common.data.EntityView;
... ... @@ -34,15 +39,24 @@ import org.thingsboard.server.common.data.audit.ActionType;
34 39 import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
35 40 import org.thingsboard.server.common.data.exception.ThingsboardException;
36 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 44 import org.thingsboard.server.common.data.id.EntityViewId;
38 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 48 import org.thingsboard.server.common.data.page.TextPageData;
40 49 import org.thingsboard.server.common.data.page.TextPageLink;
  50 +import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
41 51 import org.thingsboard.server.dao.exception.IncorrectParameterException;
42 52 import org.thingsboard.server.dao.model.ModelConstants;
43 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 58 import java.util.List;
  59 +import java.util.concurrent.ExecutionException;
46 60 import java.util.stream.Collectors;
47 61
48 62 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 67 @RestController
54 68 @RequestMapping("/api")
  69 +@Slf4j
55 70 public class EntityViewController extends BaseController {
56 71
57 72 public static final String ENTITY_VIEW_ID = "entityViewId";
... ... @@ -75,6 +90,20 @@ public class EntityViewController extends BaseController {
75 90 try {
76 91 entityView.setTenantId(getCurrentUser().getTenantId());
77 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 107 logEntityAction(savedEntityView.getId(), savedEntityView, null,
79 108 entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
80 109 return savedEntityView;
... ... @@ -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 167 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
89 168 @RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.DELETE)
90 169 @ResponseStatus(value = HttpStatus.OK)
... ...
... ... @@ -94,15 +94,9 @@ import java.util.stream.Collectors;
94 94 public class TelemetryController extends BaseController {
95 95
96 96 @Autowired
97   - private AttributesService attributesService;
98   -
99   - @Autowired
100 97 private TimeseriesService tsService;
101 98
102 99 @Autowired
103   - private TelemetrySubscriptionService tsSubService;
104   -
105   - @Autowired
106 100 private AccessValidator accessValidator;
107 101
108 102 private ExecutorService executor;
... ...
... ... @@ -143,7 +143,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
143 143 public void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub) {
144 144 long startTime = 0L;
145 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 147 EntityView entityView = entityViewService.findEntityViewById(new EntityViewId(entityId.getId()));
148 148 entityId = entityView.getEntityId();
149 149 startTime = entityView.getStartTimeMs();
... ... @@ -165,38 +165,15 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
165 165 }
166 166
167 167 private SubscriptionState getUpdatedSubscriptionState(EntityId entityId, SubscriptionState sub, EntityView entityView) {
168   - boolean allKeys;
169 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 172 keyStates = sub.getKeyStates().entrySet()
173 173 .stream().filter(entry -> entityView.getKeys().getTimeseries().contains(entry.getKey()))
174 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 179 @Override
... ... @@ -467,7 +444,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
467 444 onLocalSubUpdate(entityId, s -> TelemetryFeature.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> {
468 445 List<TsKvEntry> subscriptionUpdate = null;
469 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 448 if (subscriptionUpdate == null) {
472 449 subscriptionUpdate = new ArrayList<>();
473 450 }
... ...
... ... @@ -29,6 +29,7 @@ import org.thingsboard.server.dao.asset.AssetService;
29 29 import org.thingsboard.server.dao.customer.CustomerService;
30 30 import org.thingsboard.server.dao.dashboard.DashboardService;
31 31 import org.thingsboard.server.dao.device.DeviceService;
  32 +import org.thingsboard.server.dao.entityview.EntityViewService;
32 33 import org.thingsboard.server.dao.rule.RuleChainService;
33 34 import org.thingsboard.server.dao.tenant.TenantService;
34 35 import org.thingsboard.server.dao.user.UserService;
... ... @@ -47,6 +48,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
47 48 private DeviceService deviceService;
48 49
49 50 @Autowired
  51 + private EntityViewService entityViewService;
  52 +
  53 + @Autowired
50 54 private TenantService tenantService;
51 55
52 56 @Autowired
... ... @@ -81,6 +85,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
81 85 case DEVICE:
82 86 hasName = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId()));
83 87 break;
  88 + case ENTITY_VIEW:
  89 + hasName = entityViewService.findEntityViewByIdAsync(new EntityViewId(entityId.getId()));
  90 + break;
84 91 case TENANT:
85 92 hasName = tenantService.findTenantByIdAsync(new TenantId(entityId.getId()));
86 93 break;
... ...
... ... @@ -84,8 +84,7 @@ public class CassandraEntityViewDao extends CassandraAbstractSearchTextDao<Entit
84 84 @Override
85 85 public EntityView save(EntityView domain) {
86 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 88 EntitySubtypeEntity entitySubtypeEntity = new EntitySubtypeEntity(entitySubtype);
90 89 Statement saveStatement = cluster.getMapper(EntitySubtypeEntity.class).saveQuery(entitySubtypeEntity);
91 90 executeWrite(saveStatement);
... ...
... ... @@ -105,21 +105,6 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti
105 105 log.trace("Executing save entity view [{}]", entityView);
106 106 entityViewValidator.validate(entityView);
107 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 108 return savedEntityView;
124 109 }
125 110
... ... @@ -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 282 private DataValidator<EntityView> entityViewValidator =
328 283 new DataValidator<EntityView>() {
329 284
... ...
... ... @@ -79,12 +79,16 @@ public class BaseTimeseriesService implements TimeseriesService {
79 79 if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) {
80 80 EntityView entityView = entityViewService.findEntityViewById((EntityViewId) entityId);
81 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 84 filteredKeys.retainAll(entityView.getKeys().getTimeseries());
84 85 }
85 86 List<ReadTsKvQuery> queries =
86 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 92 .collect(Collectors.toList());
89 93
90 94 if (queries.size() > 0) {
... ... @@ -100,7 +104,17 @@ public class BaseTimeseriesService implements TimeseriesService {
100 104 @Override
101 105 public ListenableFuture<List<TsKvEntry>> findAllLatest(EntityId entityId) {
102 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 120 @Override
... ...
... ... @@ -45,6 +45,7 @@ import org.thingsboard.server.dao.customer.CustomerService;
45 45 import org.thingsboard.server.dao.dashboard.DashboardService;
46 46 import org.thingsboard.server.dao.device.DeviceCredentialsService;
47 47 import org.thingsboard.server.dao.device.DeviceService;
  48 +import org.thingsboard.server.dao.entityview.EntityViewService;
48 49 import org.thingsboard.server.dao.event.EventService;
49 50 import org.thingsboard.server.dao.relation.RelationService;
50 51 import org.thingsboard.server.dao.rule.RuleChainService;
... ... @@ -89,6 +90,9 @@ public abstract class AbstractServiceTest {
89 90 protected AssetService assetService;
90 91
91 92 @Autowired
  93 + protected EntityViewService entityViewService;
  94 +
  95 + @Autowired
92 96 protected DeviceCredentialsService deviceCredentialsService;
93 97
94 98 @Autowired
... ...
... ... @@ -17,9 +17,15 @@ package org.thingsboard.server.dao.service.timeseries;
17 17
18 18 import com.datastax.driver.core.utils.UUIDs;
19 19 import lombok.extern.slf4j.Slf4j;
  20 +import org.junit.After;
20 21 import org.junit.Assert;
  22 +import org.junit.Before;
21 23 import org.junit.Test;
  24 +import org.thingsboard.server.common.data.EntityView;
  25 +import org.thingsboard.server.common.data.Tenant;
22 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 29 import org.thingsboard.server.common.data.kv.Aggregation;
24 30 import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery;
25 31 import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
... ... @@ -30,6 +36,7 @@ import org.thingsboard.server.common.data.kv.KvEntry;
30 36 import org.thingsboard.server.common.data.kv.LongDataEntry;
31 37 import org.thingsboard.server.common.data.kv.StringDataEntry;
32 38 import org.thingsboard.server.common.data.kv.TsKvEntry;
  39 +import org.thingsboard.server.common.data.objects.TelemetryEntityView;
33 40 import org.thingsboard.server.dao.service.AbstractServiceTest;
34 41
35 42 import java.util.ArrayList;
... ... @@ -61,6 +68,22 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
61 68 KvEntry doubleKvEntry = new DoubleDataEntry(DOUBLE_KEY, Double.MAX_VALUE);
62 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 87 @Test
65 88 public void testFindAllLatest() throws Exception {
66 89 DeviceId deviceId = new DeviceId(UUIDs.timeBased());
... ... @@ -69,7 +92,15 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
69 92 saveEntries(deviceId, TS - 1);
70 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 105 assertNotNull(tsList);
75 106 assertEquals(4, tsList.size());
... ... @@ -89,6 +120,18 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
89 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 135 @Test
93 136 public void testFindLatest() throws Exception {
94 137 DeviceId deviceId = new DeviceId(UUIDs.timeBased());
... ... @@ -100,6 +143,12 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
100 143 List<TsKvEntry> entries = tsService.findLatest(deviceId, Collections.singleton(STRING_KEY)).get();
101 144 Assert.assertEquals(1, entries.size());
102 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 154 @Test
... ...
... ... @@ -7,4 +7,4 @@ WEB_UI_DOCKER_NAME=tb-web-ui
7 7
8 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 36 KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE
37 37 KAFKA_CREATE_TOPICS: "${KAFKA_TOPICS}"
38 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 42 depends_on:
40 43 - zookeeper
41 44 tb-js-executor:
... ...
... ... @@ -17,7 +17,9 @@ package org.thingsboard.rule.engine.action;
17 17
18 18 import com.google.common.util.concurrent.FutureCallback;
19 19 import com.google.common.util.concurrent.ListenableFuture;
  20 +import com.google.gson.JsonElement;
20 21 import com.google.gson.JsonParser;
  22 +import com.google.gson.JsonPrimitive;
21 23 import lombok.extern.slf4j.Slf4j;
22 24 import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
23 25 import org.thingsboard.rule.engine.api.RuleNode;
... ... @@ -25,7 +27,6 @@ import org.thingsboard.rule.engine.api.TbContext;
25 27 import org.thingsboard.rule.engine.api.TbNode;
26 28 import org.thingsboard.rule.engine.api.TbNodeConfiguration;
27 29 import org.thingsboard.rule.engine.api.TbNodeException;
28   -import org.thingsboard.rule.engine.api.TbRelationTypes;
29 30 import org.thingsboard.rule.engine.api.util.DonAsynchron;
30 31 import org.thingsboard.rule.engine.api.util.TbNodeUtils;
31 32 import org.thingsboard.server.common.data.DataConstants;
... ... @@ -37,12 +38,11 @@ import org.thingsboard.server.common.msg.session.SessionMsgType;
37 38 import org.thingsboard.server.common.transport.adaptor.JsonConverter;
38 39
39 40 import javax.annotation.Nullable;
  41 +import java.util.ArrayList;
40 42 import java.util.List;
41 43 import java.util.Set;
42   -import java.util.concurrent.ExecutionException;
43 44 import java.util.stream.Collectors;
44 45
45   -import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
46 46 import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
47 47
48 48 @Slf4j
... ... @@ -68,70 +68,89 @@ public class TbCopyAttributesToEntityViewNode implements TbNode {
68 68 }
69 69
70 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 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 153 @Override
134 154 public void destroy() {
135   -
136 155 }
137 156 }
... ...
... ... @@ -27,7 +27,6 @@ function EntityViewService($http, $q, $window, userService, attributeService, cu
27 27 deleteEntityView: deleteEntityView,
28 28 getCustomerEntityViews: getCustomerEntityViews,
29 29 getEntityView: getEntityView,
30   - getEntityViews: getEntityViews,
31 30 getTenantEntityViews: getTenantEntityViews,
32 31 saveEntityView: saveEntityView,
33 32 unassignEntityViewFromCustomer: unassignEntityViewFromCustomer,
... ... @@ -126,32 +125,6 @@ function EntityViewService($http, $q, $window, userService, attributeService, cu
126 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 128 function saveEntityView(entityView) {
156 129 var deferred = $q.defer();
157 130 var url = '/api/entityView';
... ...
... ... @@ -135,6 +135,10 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
135 135 case types.entityType.asset:
136 136 promise = assetService.getAssets(entityIds, config);
137 137 break;
  138 + case types.entityType.entityView:
  139 + promise = getEntitiesByIdsPromise(
  140 + (id) => entityViewService.getEntityView(id, config), entityIds);
  141 + break;
138 142 case types.entityType.tenant:
139 143 promise = getEntitiesByIdsPromise(
140 144 (id) => tenantService.getTenant(id, config), entityIds);
... ...
... ... @@ -15,7 +15,7 @@
15 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 19 <form name="theForm" ng-submit="vm.add()">
20 20 <md-toolbar>
21 21 <div class="md-toolbar-tools">
... ...
... ... @@ -60,7 +60,7 @@
60 60 entity-type="types.entityType.entityView">
61 61 </tb-entity-subtype-autocomplete>
62 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 64 <tb-entity-select flex ng-disabled="!isEdit"
65 65 the-form="theForm"
66 66 tb-required="true"
... ... @@ -68,62 +68,145 @@
68 68 ng-model="entityView.entityId">
69 69 </tb-entity-select>
70 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 207 <md-input-container class="md-block">
72 208 <label translate>entity-view.description</label>
73 209 <textarea ng-model="entityView.additionalInfo.description" rows="2"></textarea>
74 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 211 </fieldset>
129 212 </md-content>
... ...
... ... @@ -13,6 +13,9 @@
13 13 * See the License for the specific language governing permissions and
14 14 * limitations under the License.
15 15 */
  16 +
  17 +import './entity-view.scss';
  18 +
16 19 /* eslint-disable import/no-unresolved, import/default */
17 20
18 21 import entityViewFieldsetTemplate from './entity-view-fieldset.tpl.html';
... ... @@ -20,12 +23,16 @@ import entityViewFieldsetTemplate from './entity-view-fieldset.tpl.html';
20 23 /* eslint-enable import/no-unresolved, import/default */
21 24
22 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 28 var linker = function (scope, element) {
26 29 var template = $templateCache.get(entityViewFieldsetTemplate);
27 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 36 scope.types = types;
30 37 scope.isAssignedToCustomer = false;
31 38 scope.isPublic = false;
... ... @@ -53,9 +60,13 @@ export default function EntityViewDirective($compile, $templateCache, $filter, t
53 60 }
54 61 if (scope.entityView.startTimeMs > 0) {
55 62 scope.startTimeMs = new Date(scope.entityView.startTimeMs);
  63 + } else {
  64 + scope.startTimeMs = null;
56 65 }
57 66 if (scope.entityView.endTimeMs > 0) {
58 67 scope.endTimeMs = new Date(scope.entityView.endTimeMs);
  68 + } else {
  69 + scope.endTimeMs = null;
59 70 }
60 71 if (!scope.entityView.keys) {
61 72 scope.entityView.keys = {};
... ... @@ -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 96 scope.$watch('startTimeMs', function (newDate) {
73 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 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 839 "select-entity-view": "Select entity view",
840 840 "make-public": "Make entity view public",
  841 + "start-date": "Start date",
841 842 "start-ts": "Start time",
  843 + "end-date": "End date",
842 844 "end-ts": "End time",
843 845 "date-limits": "Date limits",
844 846 "client-attributes": "Client attributes",
845 847 "shared-attributes": "Shared attributes",
846 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 860 "event": {
851 861 "event-type": "Event type",
... ...
... ... @@ -839,7 +839,7 @@
839 839 "client-attributes": "Client attributes",
840 840 "shared-attributes": "Shared attributes",
841 841 "server-attributes": "Server attributes",
842   - "latest-timeseries": "Latest timeseries"
  842 + "timeseries": "Timeseries"
843 843 },
844 844 "event": {
845 845 "event-type": "Tipo de evento",
... ...