Browse Source

Merge pull request #1072 from thingsboard/feature/entity-view

Feature/entity view
pull/1109/head
VoBa 8 years ago
committed by GitHub
parent
commit
151cf2376f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 69
      application/src/main/data/upgrade/2.1.1/schema_update.cql
  2. 31
      application/src/main/data/upgrade/2.1.1/schema_update.sql
  3. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  4. 6
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  5. 26
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  6. 218
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  7. 5
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  8. 9
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  9. 31
      application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
  10. 69
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  11. 6
      application/src/main/java/org/thingsboard/server/service/telemetry/sub/Subscription.java
  12. 5
      application/src/main/resources/thingsboard.yml
  13. 456
      application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java
  14. 2
      application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java
  15. 26
      application/src/test/java/org/thingsboard/server/controller/nosql/EntityViewControllerNoSqlTest.java
  16. 31
      application/src/test/java/org/thingsboard/server/controller/sql/EntityViewControllerSqlTest.java
  17. 1
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  18. 2
      common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
  19. 78
      common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java
  20. 42
      common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java
  21. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  22. 44
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java
  23. 4
      common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java
  24. 47
      common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java
  25. 43
      common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java
  26. 5
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  27. 120
      dao/src/main/java/org/thingsboard/server/dao/entityview/CassandraEntityViewDao.java
  28. 73
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java
  29. 58
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
  30. 356
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
  31. 19
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  32. 157
      dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java
  33. 158
      dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java
  34. 56
      dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java
  35. 103
      dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java
  36. 5
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  37. 44
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  38. 78
      dao/src/main/resources/cassandra/schema-entities.cql
  39. 14
      dao/src/main/resources/sql/schema-entities.sql
  40. 3
      dao/src/test/resources/sql/drop-all-tables.sql
  41. 3
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  42. 137
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java
  43. 238
      ui/src/app/api/entity-view.service.js
  44. 17
      ui/src/app/api/entity.service.js
  45. 2
      ui/src/app/app.js
  46. 9
      ui/src/app/common/types.constant.js
  47. 45
      ui/src/app/entity-view/add-entity-view.tpl.html
  48. 123
      ui/src/app/entity-view/add-entity-views-to-customer.controller.js
  49. 77
      ui/src/app/entity-view/add-entity-views-to-customer.tpl.html
  50. 123
      ui/src/app/entity-view/assign-to-customer.controller.js
  51. 76
      ui/src/app/entity-view/assign-to-customer.tpl.html
  52. 22
      ui/src/app/entity-view/entity-view-card.tpl.html
  53. 119
      ui/src/app/entity-view/entity-view-fieldset.tpl.html
  54. 483
      ui/src/app/entity-view/entity-view.controller.js
  55. 121
      ui/src/app/entity-view/entity-view.directive.js
  56. 72
      ui/src/app/entity-view/entity-view.routes.js
  57. 75
      ui/src/app/entity-view/entity-views.tpl.html
  58. 41
      ui/src/app/entity-view/index.js
  59. 2
      ui/src/app/entity/attribute/attribute-table.directive.js
  60. 6
      ui/src/app/entity/entity-autocomplete.directive.js
  61. 2
      ui/src/app/layout/index.js
  62. 85
      ui/src/app/locale/locale.constant-en_US.json
  63. 52
      ui/src/app/services/menu.service.js

69
application/src/main/data/upgrade/2.1.1/schema_update.cql

@ -0,0 +1,69 @@
--
-- Copyright © 2016-2018 The Thingsboard Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_name;
DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_search_text;
DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_customer;
DROP TABLE IF EXISTS thingsboard.entity_views;
CREATE TABLE IF NOT EXISTS thingsboard.entity_views (
id timeuuid,
entity_id timeuuid,
entity_type text,
tenant_id timeuuid,
customer_id timeuuid,
name text,
keys text,
start_ts bigint,
end_ts bigint,
search_text text,
additional_info text,
PRIMARY KEY (id, entity_id, tenant_id, customer_id)
);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_name AS
SELECT *
from thingsboard.entity_views
WHERE tenant_id IS NOT NULL
AND entity_id IS NOT NULL
AND customer_id IS NOT NULL
AND name IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY (tenant_id, name, id, customer_id, entity_id)
WITH CLUSTERING ORDER BY (name ASC, id DESC, customer_id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_search_text AS
SELECT *
from thingsboard.entity_views
WHERE tenant_id IS NOT NULL
AND entity_id IS NOT NULL
AND customer_id IS NOT NULL
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY (tenant_id, search_text, id, customer_id, entity_id)
WITH CLUSTERING ORDER BY (search_text ASC, id DESC, customer_id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_customer AS
SELECT *
from thingsboard.entity_views
WHERE tenant_id IS NOT NULL
AND customer_id IS NOT NULL
AND entity_id IS NOT NULL
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY (tenant_id, customer_id, search_text, id, entity_id)
WITH CLUSTERING ORDER BY (customer_id DESC, search_text ASC, id DESC);

31
application/src/main/data/upgrade/2.1.1/schema_update.sql

@ -0,0 +1,31 @@
--
-- Copyright © 2016-2018 The Thingsboard Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
DROP TABLE IF EXISTS entity_views;
CREATE TABLE IF NOT EXISTS entity_views (
id varchar(31) NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY,
entity_id varchar(31),
entity_type varchar(255),
tenant_id varchar(31),
customer_id varchar(31),
name varchar(255),
keys varchar(255),
start_ts bigint,
end_ts bigint,
search_text varchar(255),
additional_info varchar
);

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

@ -48,6 +48,7 @@ import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
@ -159,6 +160,10 @@ public class ActorSystemContext {
@Getter
private AuditLogService auditLogService;
@Autowired
@Getter
private EntityViewService entityViewService;
@Autowired
@Getter
private TelemetrySubscriptionService tsSubService;

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

@ -41,6 +41,7 @@ import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
@ -212,6 +213,11 @@ class DefaultTbContext implements TbContext {
return mainCtx.getRelationService();
}
@Override
public EntityViewService getEntityViewService() {
return mainCtx.getEntityViewService();
}
@Override
public MailService getMailService() {
if (mainCtx.isAllowSystemMailService()) {

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

@ -56,6 +56,7 @@ import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
@ -139,6 +140,9 @@ public abstract class BaseController {
@Autowired
protected DeviceStateService deviceStateService;
@Autowired
protected EntityViewService entityViewService;
@ExceptionHandler(ThingsboardException.class)
public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
errorResponseHandler.handle(ex, response);
@ -313,6 +317,9 @@ public abstract class BaseController {
case USER:
checkUserId(new UserId(entityId.getId()));
return;
case ENTITY_VIEW:
checkEntityViewId(new EntityViewId(entityId.getId()));
return;
default:
throw new IllegalArgumentException("Unsupported entity type: " + entityId.getEntityType());
}
@ -340,6 +347,25 @@ public abstract class BaseController {
}
}
protected EntityView checkEntityViewId(EntityViewId entityViewId) throws ThingsboardException {
try {
validateId(entityViewId, "Incorrect entityViewId " + entityViewId);
EntityView entityView = entityViewService.findEntityViewById(entityViewId);
checkEntityView(entityView);
return entityView;
} catch (Exception e) {
throw handleException(e, false);
}
}
protected void checkEntityView(EntityView entityView) throws ThingsboardException {
checkNotNull(entityView);
checkTenantId(entityView.getTenantId());
if (entityView.getCustomerId() != null && !entityView.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
checkCustomerId(entityView.getCustomerId());
}
}
Asset checkAssetId(AssetId assetId) throws ThingsboardException {
try {
validateId(assetId, "Incorrect assetId " + assetId);

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

@ -0,0 +1,218 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import java.util.List;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.CustomerController.CUSTOMER_ID;
/**
* Created by Victor Basanets on 8/28/2017.
*/
@RestController
@RequestMapping("/api")
public class EntityViewController extends BaseController {
public static final String ENTITY_VIEW_ID = "entityViewId";
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.GET)
@ResponseBody
public EntityView getEntityViewById(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
return checkEntityViewId(new EntityViewId(toUUID(strEntityViewId)));
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityView", method = RequestMethod.POST)
@ResponseBody
public EntityView saveEntityView(@RequestBody EntityView entityView) throws ThingsboardException {
try {
entityView.setTenantId(getCurrentUser().getTenantId());
EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView));
logEntityAction(savedEntityView.getId(), savedEntityView, null,
entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
return savedEntityView;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ENTITY_VIEW), entityView, null,
entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void deleteEntityView(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
EntityView entityView = checkEntityViewId(entityViewId);
entityViewService.deleteEntityView(entityViewId);
logEntityAction(entityViewId, entityView, entityView.getCustomerId(),
ActionType.DELETED,null, strEntityViewId);
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ENTITY_VIEW),
null,
null,
ActionType.DELETED, e, strEntityViewId);
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/{customerId}/entityView/{entityViewId}", method = RequestMethod.POST)
@ResponseBody
public EntityView assignEntityViewToCustomer(@PathVariable(CUSTOMER_ID) String strCustomerId,
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(CUSTOMER_ID, strCustomerId);
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
Customer customer = checkCustomerId(customerId);
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
checkEntityViewId(entityViewId);
EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToCustomer(entityViewId, customerId));
logEntityAction(entityViewId, savedEntityView,
savedEntityView.getCustomerId(),
ActionType.ASSIGNED_TO_CUSTOMER, null, strEntityViewId, strCustomerId, customer.getName());
return savedEntityView;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ENTITY_VIEW), null,
null,
ActionType.ASSIGNED_TO_CUSTOMER, e, strEntityViewId, strCustomerId);
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/entityView/{entityViewId}", method = RequestMethod.DELETE)
@ResponseBody
public EntityView unassignEntityViewFromCustomer(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
EntityView entityView = checkEntityViewId(entityViewId);
if (entityView.getCustomerId() == null || entityView.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
throw new IncorrectParameterException("Entity View isn't assigned to any customer!");
}
Customer customer = checkCustomerId(entityView.getCustomerId());
EntityView savedEntityView = checkNotNull(entityViewService.unassignEntityViewFromCustomer(entityViewId));
logEntityAction(entityViewId, entityView,
entityView.getCustomerId(),
ActionType.UNASSIGNED_FROM_CUSTOMER, null, strEntityViewId, customer.getId().toString(), customer.getName());
return savedEntityView;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ENTITY_VIEW), null,
null,
ActionType.UNASSIGNED_FROM_CUSTOMER, e, strEntityViewId);
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/entityViews", params = {"limit"}, method = RequestMethod.GET)
@ResponseBody
public TextPageData<EntityView> getCustomerEntityViews(
@PathVariable("customerId") String strCustomerId,
@RequestParam int limit,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String idOffset,
@RequestParam(required = false) String textOffset) throws ThingsboardException {
checkParameter("customerId", strCustomerId);
try {
TenantId tenantId = getCurrentUser().getTenantId();
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId);
TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/entityViews", params = {"limit"}, method = RequestMethod.GET)
@ResponseBody
public TextPageData<EntityView> getTenantEntityViews(
@RequestParam int limit,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String idOffset,
@RequestParam(required = false) String textOffset) throws ThingsboardException {
try {
TenantId tenantId = getCurrentUser().getTenantId();
TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
return checkNotNull(entityViewService.findEntityViewByTenantId(tenantId, pageLink));
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityViews", method = RequestMethod.POST)
@ResponseBody
public List<EntityView> findByQuery(@RequestBody EntityViewSearchQuery query) throws ThingsboardException {
checkNotNull(query);
checkNotNull(query.getParameters());
checkEntityId(query.getParameters().getEntityId());
try {
List<EntityView> entityViews = checkNotNull(entityViewService.findEntityViewsByQuery(query).get());
entityViews = entityViews.stream().filter(entityView -> {
try {
checkEntityView(entityView);
return true;
} catch (ThingsboardException e) {
return false;
}
}).collect(Collectors.toList());
return entityViews;
} catch (Exception e) {
throw handleException(e);
}
}
}

5
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -92,6 +92,11 @@ public class ThingsboardInstallService {
dataUpdateService.updateData("1.4.0");
case "2.0.0":
log.info("Upgrading ThingsBoard from version 2.0.0 to 2.1.1 ...");
databaseUpgradeService.upgradeDatabase("2.0.0");
log.info("Updating system data...");
systemDataLoaderService.deleteSystemWidgetBundle("charts");

9
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -107,6 +107,15 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
log.info("Schema updated.");
}
break;
case "2.0.0":
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
log.info("Updating schema ...");
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.1.1", SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
log.info("Schema updated.");
}
break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
}

31
application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java

@ -26,6 +26,7 @@ import org.springframework.stereotype.Component;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
@ -44,6 +46,7 @@ import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
@ -66,6 +69,7 @@ public class AccessValidator {
public static final String CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "Customer user is not allowed to perform this operation!";
public static final String SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "System administrator is not allowed to perform this operation!";
public static final String DEVICE_WITH_REQUESTED_ID_NOT_FOUND = "Device with requested id wasn't found!";
public static final String ENTITY_VIEW_WITH_REQUESTED_ID_NOT_FOUND = "Entity-view with requested id wasn't found!";
@Autowired
protected TenantService tenantService;
@ -88,6 +92,9 @@ public class AccessValidator {
@Autowired
protected RuleChainService ruleChainService;
@Autowired
protected EntityViewService entityViewService;
private ExecutorService executor;
@PostConstruct
@ -158,6 +165,9 @@ public class AccessValidator {
case TENANT:
validateTenant(currentUser, entityId, callback);
return;
case ENTITY_VIEW:
validateEntityView(currentUser, entityId, callback);
return;
default:
//TODO: add support of other entities
throw new IllegalStateException("Not Implemented!");
@ -293,6 +303,27 @@ public class AccessValidator {
}
}
private void validateEntityView(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
if (currentUser.isSystemAdmin()) {
callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
} else {
ListenableFuture<EntityView> entityViewFuture = entityViewService.findEntityViewByIdAsync(new EntityViewId(entityId.getId()));
Futures.addCallback(entityViewFuture, getCallback(callback, entityView -> {
if (entityView == null) {
return ValidationResult.entityNotFound(ENTITY_VIEW_WITH_REQUESTED_ID_NOT_FOUND);
} else {
if (!entityView.getTenantId().equals(currentUser.getTenantId())) {
return ValidationResult.accessDenied("Entity-view doesn't belong to the current Tenant!");
} else if (currentUser.isCustomerUser() && !entityView.getCustomerId().equals(currentUser.getCustomerId())) {
return ValidationResult.accessDenied("Entity-view doesn't belong to the current Customer!");
} else {
return ValidationResult.ok(entityView);
}
}
}), executor);
}
}
private <T, V> FutureCallback<T> getCallback(FutureCallback<ValidationResult> callback, Function<T, ValidationResult<V>> transformer) {
return new FutureCallback<T>() {
@Override

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

@ -29,9 +29,11 @@ import org.thingsboard.rule.engine.api.util.DonAsynchron;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
@ -48,6 +50,8 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
@ -64,6 +68,7 @@ import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@ -101,6 +106,9 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
@Autowired
private ClusterRpcService rpcService;
@Autowired
private EntityViewService entityViewService;
@Autowired
@Lazy
private DeviceStateService stateService;
@ -133,20 +141,64 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
@Override
public void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub) {
long startTime = 0L;
long endTime = 0L;
if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) {
EntityView entityView = entityViewService.findEntityViewById(new EntityViewId(entityId.getId()));
entityId = entityView.getEntityId();
startTime = entityView.getStartTimeMs();
endTime = entityView.getEndTimeMs();
sub = getUpdatedSubscriptionState(entityId, sub, entityView);
}
Optional<ServerAddress> server = routingService.resolveById(entityId);
Subscription subscription;
if (server.isPresent()) {
ServerAddress address = server.get();
log.trace("[{}] Forwarding subscription [{}] for device [{}] to [{}]", sessionId, sub.getSubscriptionId(), entityId, address);
subscription = new Subscription(sub, true, address);
log.trace("[{}] Forwarding subscription [{}] for [{}] entity [{}] to [{}]", sessionId, sub.getSubscriptionId(), entityId.getEntityType().name(), entityId, address);
subscription = new Subscription(sub, true, address, startTime, endTime);
tellNewSubscription(address, sessionId, subscription);
} else {
log.trace("[{}] Registering local subscription [{}] for device [{}]", sessionId, sub.getSubscriptionId(), entityId);
subscription = new Subscription(sub, true);
log.trace("[{}] Registering local subscription [{}] for [{}] entity [{}]", sessionId, sub.getSubscriptionId(), entityId.getEntityType().name(), entityId);
subscription = new Subscription(sub, true, null, startTime, endTime);
}
registerSubscription(sessionId, entityId, subscription);
}
private SubscriptionState getUpdatedSubscriptionState(EntityId entityId, SubscriptionState sub, EntityView entityView) {
boolean allKeys;
Map<String, Long> keyStates;
if (sub.getType().equals(TelemetryFeature.TIMESERIES) && !entityView.getKeys().getTimeseries().isEmpty()) {
allKeys = false;
keyStates = sub.getKeyStates().entrySet()
.stream().filter(entry -> entityView.getKeys().getTimeseries().contains(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
} else if (sub.getType().equals(TelemetryFeature.ATTRIBUTES)) {
if (sub.getScope().equals(DataConstants.CLIENT_SCOPE) && !entityView.getKeys().getAttributes().getCs().isEmpty()) {
allKeys = false;
keyStates = filterMap(sub, entityView.getKeys().getAttributes().getCs());
} else if (sub.getScope().equals(DataConstants.SERVER_SCOPE) && !entityView.getKeys().getAttributes().getSs().isEmpty()) {
allKeys = false;
keyStates = filterMap(sub, entityView.getKeys().getAttributes().getSs());
} else if (sub.getScope().equals(DataConstants.SERVER_SCOPE) && !entityView.getKeys().getAttributes().getSh().isEmpty()) {
allKeys = false;
keyStates = filterMap(sub, entityView.getKeys().getAttributes().getSh());
} else {
allKeys = sub.isAllKeys();
keyStates = sub.getKeyStates();
}
} else {
allKeys = sub.isAllKeys();
keyStates = sub.getKeyStates();
}
return new SubscriptionState(sub.getWsSessionId(), sub.getSubscriptionId(), entityId, sub.getType(), allKeys, keyStates, sub.getScope());
}
private Map<String, Long> filterMap(SubscriptionState sub, List<String> allowedKeys) {
return sub.getKeyStates().entrySet()
.stream().filter(entry -> allowedKeys.contains(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
@Override
public void cleanupLocalWsSessionSubscriptions(TelemetryWebSocketSessionRef sessionRef, String sessionId) {
cleanupLocalWsSessionSubscriptions(sessionId);
@ -415,7 +467,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
onLocalSubUpdate(entityId, s -> TelemetryFeature.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> {
List<TsKvEntry> subscriptionUpdate = null;
for (AttributeKvEntry kv : attributes) {
if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
if (isInTimeRange(s, kv.getLastUpdateTs()) && (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey()))) {
if (subscriptionUpdate == null) {
subscriptionUpdate = new ArrayList<>();
}
@ -430,7 +482,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
onLocalSubUpdate(entityId, s -> TelemetryFeature.TIMESERIES == s.getType(), s -> {
List<TsKvEntry> subscriptionUpdate = null;
for (TsKvEntry kv : ts) {
if (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey()))) {
if (isInTimeRange(s, kv.getTs()) && (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey())))) {
if (subscriptionUpdate == null) {
subscriptionUpdate = new ArrayList<>();
}
@ -441,6 +493,11 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
});
}
private boolean isInTimeRange(Subscription subscription, long kvTime) {
return (subscription.getStartTime() == 0 || subscription.getStartTime() <= kvTime)
&& (subscription.getEndTime() == 0 || subscription.getEndTime() >= kvTime);
}
private void onLocalSubUpdate(EntityId entityId, Predicate<Subscription> filter, Function<Subscription, List<TsKvEntry>> f) {
Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
if (deviceSubscriptions != null) {

6
application/src/main/java/org/thingsboard/server/service/telemetry/sub/Subscription.java

@ -30,9 +30,11 @@ public class Subscription {
private final SubscriptionState sub;
private final boolean local;
private ServerAddress server;
private long startTime;
private long endTime;
public Subscription(SubscriptionState sub, boolean local) {
this(sub, local, null);
public Subscription(SubscriptionState sub, boolean local, ServerAddress server) {
this(sub, local, server, 0L, 0L);
}
public String getWsSessionId() {

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

@ -302,6 +302,9 @@ caffeine:
assets:
timeToLiveInMinutes: 1440
maxSize: 100000
entityViews:
timeToLiveInMinutes: 1440
maxSize: 100000
redis:
# standalone or cluster
@ -363,7 +366,7 @@ spring:
password: "${SPRING_DATASOURCE_PASSWORD:}"
# PostgreSQL DAO Configuration
#spring:
# spring:
# data:
# sql:
# repositories:

456
application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java

@ -0,0 +1,456 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import com.datastax.driver.core.utils.UUIDs;
import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.commons.lang3.RandomStringUtils;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.objects.AttributesEntityView;
import org.thingsboard.server.common.data.objects.TelemetryEntityView;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.model.ModelConstants;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
public abstract class BaseEntityViewControllerTest extends AbstractControllerTest {
private IdComparator<EntityView> idComparator;
private Tenant savedTenant;
private User tenantAdmin;
private Device testDevice;
private TelemetryEntityView telemetry;
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
idComparator = new IdComparator<>();
savedTenant = doPost("/api/tenant", getNewTenant("My tenant"), Tenant.class);
Assert.assertNotNull(savedTenant);
tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
Device device = new Device();
device.setName("Test device");
device.setType("default");
testDevice = doPost("/api/device", device, Device.class);
telemetry = new TelemetryEntityView(
Arrays.asList("109", "209", "309"),
new AttributesEntityView(
Arrays.asList("caValue1", "caValue2", "caValue3", "caValue4"),
Arrays.asList("saValue1", "saValue2", "saValue3", "saValue4"),
Arrays.asList("shValue1", "shValue2", "shValue3", "shValue4")));
}
@After
public void afterTest() throws Exception {
loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
.andExpect(status().isOk());
}
@Test
public void testFindEntityViewById() throws Exception {
EntityView savedView = getNewSavedEntityView("Test entity view");
EntityView foundView = doGet("/api/entityView/" + savedView.getId().getId().toString(), EntityView.class);
Assert.assertNotNull(foundView);
assertEquals(savedView, foundView);
}
@Test
public void testSaveEntityView() throws Exception {
EntityView savedView = getNewSavedEntityView("Test entity view");
Assert.assertNotNull(savedView);
Assert.assertNotNull(savedView.getId());
Assert.assertTrue(savedView.getCreatedTime() > 0);
assertEquals(savedTenant.getId(), savedView.getTenantId());
Assert.assertNotNull(savedView.getCustomerId());
assertEquals(NULL_UUID, savedView.getCustomerId().getId());
assertEquals(savedView.getName(), savedView.getName());
savedView.setName("New test entity view");
doPost("/api/entityView", savedView, EntityView.class);
EntityView foundEntityView = doGet("/api/entityView/" + savedView.getId().getId().toString(), EntityView.class);
assertEquals(foundEntityView.getName(), savedView.getName());
assertEquals(foundEntityView.getKeys(), telemetry);
}
@Test
public void testDeleteEntityView() throws Exception {
EntityView view = getNewSavedEntityView("Test entity view");
Customer customer = doPost("/api/customer", getNewCustomer("My customer"), Customer.class);
view.setCustomerId(customer.getId());
EntityView savedView = doPost("/api/entityView", view, EntityView.class);
doDelete("/api/entityView/" + savedView.getId().getId().toString())
.andExpect(status().isOk());
doGet("/api/entityView/" + savedView.getId().getId().toString())
.andExpect(status().isNotFound());
}
@Test
public void testSaveEntityViewWithEmptyName() throws Exception {
doPost("/api/entityView", new EntityView())
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Entity view name should be specified!")));
}
@Test
public void testAssignAndUnAssignedEntityViewToCustomer() throws Exception {
EntityView view = getNewSavedEntityView("Test entity view");
Customer savedCustomer = doPost("/api/customer", getNewCustomer("My customer"), Customer.class);
view.setCustomerId(savedCustomer.getId());
EntityView savedView = doPost("/api/entityView", view, EntityView.class);
EntityView assignedView = doPost(
"/api/customer/" + savedCustomer.getId().getId().toString() + "/entityView/" + savedView.getId().getId().toString(),
EntityView.class);
assertEquals(savedCustomer.getId(), assignedView.getCustomerId());
EntityView foundView = doGet("/api/entityView/" + savedView.getId().getId().toString(), EntityView.class);
assertEquals(savedCustomer.getId(), foundView.getCustomerId());
EntityView unAssignedView = doDelete("/api/customer/entityView/" + savedView.getId().getId().toString(), EntityView.class);
assertEquals(ModelConstants.NULL_UUID, unAssignedView.getCustomerId().getId());
foundView = doGet("/api/entityView/" + savedView.getId().getId().toString(), EntityView.class);
assertEquals(ModelConstants.NULL_UUID, foundView.getCustomerId().getId());
}
@Test
public void testAssignEntityViewToNonExistentCustomer() throws Exception {
EntityView savedView = getNewSavedEntityView("Test entity view");
doPost("/api/customer/" + UUIDs.timeBased().toString() + "/device/" + savedView.getId().getId().toString())
.andExpect(status().isNotFound());
}
@Test
public void testAssignEntityViewToCustomerFromDifferentTenant() throws Exception {
loginSysAdmin();
Tenant tenant2 = getNewTenant("Different tenant");
Tenant savedTenant2 = doPost("/api/tenant", tenant2, Tenant.class);
Assert.assertNotNull(savedTenant2);
User tenantAdmin2 = new User();
tenantAdmin2.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin2.setTenantId(savedTenant2.getId());
tenantAdmin2.setEmail("tenant3@thingsboard.org");
tenantAdmin2.setFirstName("Joe");
tenantAdmin2.setLastName("Downs");
createUserAndLogin(tenantAdmin2, "testPassword1");
Customer customer = getNewCustomer("Different customer");
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
login(tenantAdmin.getEmail(), "testPassword1");
EntityView savedView = getNewSavedEntityView("Test entity view");
doPost("/api/customer/" + savedCustomer.getId().getId().toString() + "/entityView/" + savedView.getId().getId().toString())
.andExpect(status().isForbidden());
loginSysAdmin();
doDelete("/api/tenant/" + savedTenant2.getId().getId().toString())
.andExpect(status().isOk());
}
@Test
public void testGetCustomerEntityViews() throws Exception {
CustomerId customerId = doPost("/api/customer", getNewCustomer("Test customer"), Customer.class).getId();
String urlTemplate = "/api/customer/" + customerId.getId().toString() + "/entityViews?";
List<EntityView> views = new ArrayList<>();
for (int i = 0; i < 128; i++) {
views.add(doPost("/api/customer/" + customerId.getId().toString() + "/entityView/"
+ getNewSavedEntityView("Test entity view " + i).getId().getId().toString(), EntityView.class));
}
List<EntityView> loadedViews = loadListOf(new TextPageLink(23), urlTemplate);
Collections.sort(views, idComparator);
Collections.sort(loadedViews, idComparator);
assertEquals(views, loadedViews);
}
@Test
public void testGetCustomerEntityViewsByName() throws Exception {
CustomerId customerId = doPost("/api/customer", getNewCustomer("Test customer"), Customer.class).getId();
String urlTemplate = "/api/customer/" + customerId.getId().toString() + "/entityViews?";
String name1 = "Entity view name1";
List<EntityView> namesOfView1 = fillListOf(125, name1, "/api/customer/" + customerId.getId().toString()
+ "/entityView/");
List<EntityView> loadedNamesOfView1 = loadListOf(new TextPageLink(15, name1), urlTemplate);
Collections.sort(namesOfView1, idComparator);
Collections.sort(loadedNamesOfView1, idComparator);
assertEquals(namesOfView1, loadedNamesOfView1);
String name2 = "Entity view name2";
List<EntityView> NamesOfView2 = fillListOf(143, name2, "/api/customer/" + customerId.getId().toString()
+ "/entityView/");
List<EntityView> loadedNamesOfView2 = loadListOf(new TextPageLink(4, name2), urlTemplate);
Collections.sort(NamesOfView2, idComparator);
Collections.sort(loadedNamesOfView2, idComparator);
assertEquals(NamesOfView2, loadedNamesOfView2);
for (EntityView view : loadedNamesOfView1) {
doDelete("/api/customer/entityView/" + view.getId().getId().toString()).andExpect(status().isOk());
}
TextPageData<EntityView> pageData = doGetTypedWithPageLink(urlTemplate,
new TypeReference<TextPageData<EntityView>>() {
}, new TextPageLink(4, name1));
Assert.assertFalse(pageData.hasNext());
assertEquals(0, pageData.getData().size());
for (EntityView view : loadedNamesOfView2) {
doDelete("/api/customer/entityView/" + view.getId().getId().toString()).andExpect(status().isOk());
}
pageData = doGetTypedWithPageLink(urlTemplate, new TypeReference<TextPageData<EntityView>>() {
},
new TextPageLink(4, name2));
Assert.assertFalse(pageData.hasNext());
assertEquals(0, pageData.getData().size());
}
@Test
public void testGetTenantEntityViews() throws Exception {
List<EntityView> views = new ArrayList<>();
for (int i = 0; i < 178; i++) {
views.add(getNewSavedEntityView("Test entity view" + i));
}
List<EntityView> loadedViews = loadListOf(new TextPageLink(23), "/api/tenant/entityViews?");
Collections.sort(views, idComparator);
Collections.sort(loadedViews, idComparator);
assertEquals(views, loadedViews);
}
@Test
public void testGetTenantEntityViewsByName() throws Exception {
String name1 = "Entity view name1";
List<EntityView> namesOfView1 = fillListOf(143, name1);
List<EntityView> loadedNamesOfView1 = loadListOf(new TextPageLink(15, name1), "/api/tenant/entityViews?");
Collections.sort(namesOfView1, idComparator);
Collections.sort(loadedNamesOfView1, idComparator);
assertEquals(namesOfView1, loadedNamesOfView1);
String name2 = "Entity view name2";
List<EntityView> NamesOfView2 = fillListOf(75, name2);
List<EntityView> loadedNamesOfView2 = loadListOf(new TextPageLink(4, name2), "/api/tenant/entityViews?");
Collections.sort(NamesOfView2, idComparator);
Collections.sort(loadedNamesOfView2, idComparator);
assertEquals(NamesOfView2, loadedNamesOfView2);
for (EntityView view : loadedNamesOfView1) {
doDelete("/api/entityView/" + view.getId().getId().toString()).andExpect(status().isOk());
}
TextPageData<EntityView> pageData = doGetTypedWithPageLink("/api/tenant/entityViews?",
new TypeReference<TextPageData<EntityView>>() {
}, new TextPageLink(4, name1));
Assert.assertFalse(pageData.hasNext());
assertEquals(0, pageData.getData().size());
for (EntityView view : loadedNamesOfView2) {
doDelete("/api/entityView/" + view.getId().getId().toString()).andExpect(status().isOk());
}
pageData = doGetTypedWithPageLink("/api/tenant/entityViews?", new TypeReference<TextPageData<EntityView>>() {
},
new TextPageLink(4, name2));
Assert.assertFalse(pageData.hasNext());
assertEquals(0, pageData.getData().size());
}
@Test
public void testTheCopyOfAttrsIntoTSForTheView() throws Exception {
Set<String> actualAttributesSet =
getAttributesByKeys("{\"caValue1\":\"value1\", \"caValue2\":true, \"caValue3\":42.0, \"caValue4\":73}");
Set<String> expectedActualAttributesSet =
new HashSet<>(Arrays.asList("caValue1", "caValue2", "caValue3", "caValue4"));
assertTrue(actualAttributesSet.containsAll(expectedActualAttributesSet));
Thread.sleep(1000);
EntityView savedView = getNewSavedEntityView("Test entity view");
List<Map<String, Object>> values = doGetAsync("/api/plugins/telemetry/ENTITY_VIEW/" + savedView.getId().getId().toString() +
"/values/attributes?keys=" + String.join(",", actualAttributesSet), List.class);
assertEquals("value1", getValue(values, "caValue1"));
assertEquals(true, getValue(values, "caValue2"));
assertEquals(42.0, getValue(values, "caValue3"));
assertEquals(73, getValue(values, "caValue4"));
}
@Test
public void testTheCopyOfAttrsOutOfTSForTheView() throws Exception {
Set<String> actualAttributesSet =
getAttributesByKeys("{\"caValue1\":\"value1\", \"caValue2\":true, \"caValue3\":42.0, \"caValue4\":73}");
Set<String> expectedActualAttributesSet = new HashSet<>(Arrays.asList("caValue1", "caValue2", "caValue3", "caValue4"));
assertTrue(actualAttributesSet.containsAll(expectedActualAttributesSet));
Thread.sleep(1000);
List<Map<String, Object>> valueTelemetryOfDevices = doGetAsync("/api/plugins/telemetry/DEVICE/" + testDevice.getId().getId().toString() +
"/values/attributes?keys=" + String.join(",", actualAttributesSet), List.class);
EntityView view = new EntityView();
view.setEntityId(testDevice.getId());
view.setTenantId(savedTenant.getId());
view.setName("Test entity view");
view.setKeys(telemetry);
view.setStartTimeMs((long) getValue(valueTelemetryOfDevices, "lastActivityTime") * 10);
view.setEndTimeMs((long) getValue(valueTelemetryOfDevices, "lastActivityTime") / 10);
EntityView savedView = doPost("/api/entityView", view, EntityView.class);
List<Map<String, Object>> values = doGetAsync("/api/plugins/telemetry/ENTITY_VIEW/" + savedView.getId().getId().toString() +
"/values/attributes?keys=" + String.join(",", actualAttributesSet), List.class);
assertEquals(0, values.size());
}
private Set<String> getAttributesByKeys(String stringKV) throws Exception {
String viewDeviceId = testDevice.getId().getId().toString();
DeviceCredentials deviceCredentials =
doGet("/api/device/" + viewDeviceId + "/credentials", DeviceCredentials.class);
assertEquals(testDevice.getId(), deviceCredentials.getDeviceId());
String accessToken = deviceCredentials.getCredentialsId();
assertNotNull(accessToken);
String clientId = MqttAsyncClient.generateClientId();
MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId);
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(accessToken);
client.connect(options);
Thread.sleep(3000);
MqttMessage message = new MqttMessage();
message.setPayload((stringKV).getBytes());
client.publish("v1/devices/me/attributes", message);
Thread.sleep(1000);
return new HashSet<>(doGetAsync("/api/plugins/telemetry/DEVICE/" + viewDeviceId + "/keys/attributes", List.class));
}
private Object getValue(List<Map<String, Object>> values, String stringValue) {
return values.size() == 0 ? null :
values.stream()
.filter(value -> value.get("key").equals(stringValue))
.findFirst().get().get("value");
}
private EntityView getNewSavedEntityView(String name) throws Exception {
EntityView view = new EntityView();
view.setEntityId(testDevice.getId());
view.setTenantId(savedTenant.getId());
view.setName(name);
view.setKeys(telemetry);
return doPost("/api/entityView", view, EntityView.class);
}
private Customer getNewCustomer(String title) {
Customer customer = new Customer();
customer.setTitle(title);
return customer;
}
private Tenant getNewTenant(String title) {
Tenant tenant = new Tenant();
tenant.setTitle(title);
return tenant;
}
private List<EntityView> fillListOf(int limit, String partOfName, String urlTemplate) throws Exception {
List<EntityView> views = new ArrayList<>();
for (EntityView view : fillListOf(limit, partOfName)) {
views.add(doPost(urlTemplate + view.getId().getId().toString(), EntityView.class));
}
return views;
}
private List<EntityView> fillListOf(int limit, String partOfName) throws Exception {
List<EntityView> viewNames = new ArrayList<>();
for (int i = 0; i < limit; i++) {
String fullName = partOfName + ' ' + RandomStringUtils.randomAlphanumeric(15);
fullName = i % 2 == 0 ? fullName.toLowerCase() : fullName.toUpperCase();
EntityView view = getNewSavedEntityView(fullName);
Customer customer = getNewCustomer("Test customer " + String.valueOf(Math.random()));
view.setCustomerId(doPost("/api/customer", customer, Customer.class).getId());
viewNames.add(doPost("/api/entityView", view, EntityView.class));
}
return viewNames;
}
private List<EntityView> loadListOf(TextPageLink pageLink, String urlTemplate) throws Exception {
List<EntityView> loadedItems = new ArrayList<>();
TextPageData<EntityView> pageData;
do {
pageData = doGetTypedWithPageLink(urlTemplate, new TypeReference<TextPageData<EntityView>>() {
}, pageLink);
loadedItems.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageData.getNextPageLink();
}
} while (pageData.hasNext());
return loadedItems;
}
}

2
application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java

@ -24,7 +24,7 @@ import java.util.Arrays;
@RunWith(ClasspathSuite.class)
@ClasspathSuite.ClassnameFilters({
"org.thingsboard.server.controller.sql.*SqlTest",
"org.thingsboard.server.controller.sql.*Test",
})
public class ControllerSqlTestSuite {

26
application/src/test/java/org/thingsboard/server/controller/nosql/EntityViewControllerNoSqlTest.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller.nosql;
import org.thingsboard.server.controller.BaseEntityViewControllerTest;
import org.thingsboard.server.dao.service.DaoNoSqlTest;
/**
* Created by Victor Basanets on 8/27/2017.
*/
@DaoNoSqlTest
public class EntityViewControllerNoSqlTest extends BaseEntityViewControllerTest {
}

31
application/src/test/java/org/thingsboard/server/controller/sql/EntityViewControllerSqlTest.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller.sql;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.controller.BaseEntityViewControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Arrays;
/**
* Created by Victor Basanets on 8/27/2017.
*/
@DaoSqlTest
public class EntityViewControllerSqlTest extends BaseEntityViewControllerTest {
}

1
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -20,4 +20,5 @@ public class CacheConstants {
public static final String RELATIONS_CACHE = "relations";
public static final String DEVICE_CACHE = "devices";
public static final String ASSET_CACHE = "assets";
public static final String ENTITY_VIEW_CACHE = "entityViews";
}

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

@ -19,5 +19,5 @@ package org.thingsboard.server.common.data;
* @author Andrew Shvayka
*/
public enum EntityType {
TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE;
TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW
}

78
common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java

@ -0,0 +1,78 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.objects.TelemetryEntityView;
/**
* Created by Victor Basanets on 8/27/2017.
*/
@Data
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class EntityView extends SearchTextBasedWithAdditionalInfo<EntityViewId>
implements HasName, HasTenantId, HasCustomerId {
private static final long serialVersionUID = 5582010124562018986L;
private EntityId entityId;
private TenantId tenantId;
private CustomerId customerId;
private String name;
private TelemetryEntityView keys;
private long startTimeMs;
private long endTimeMs;
public EntityView() {
super();
}
public EntityView(EntityViewId id) {
super(id);
}
public EntityView(EntityView entityView) {
super(entityView);
}
@Override
public String getSearchText() {
return getName() /*What the ...*/;
}
@Override
public CustomerId getCustomerId() {
return customerId;
}
@Override
public String getName() {
return name;
}
@Override
public TenantId getTenantId() {
return tenantId;
}
}

42
common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.entityview;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.EntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import java.util.Collections;
import java.util.List;
@Data
public class EntityViewSearchQuery {
private RelationsSearchParameters parameters;
private String relationType;
public EntityRelationsQuery toEntitySearchQuery() {
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(parameters);
query.setFilters(
Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType,
Collections.singletonList(EntityType.ENTITY_VIEW))));
return query;
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java

@ -57,6 +57,8 @@ public class EntityIdFactory {
return new RuleChainId(uuid);
case RULE_NODE:
return new RuleNodeId(uuid);
case ENTITY_VIEW:
return new EntityViewId(uuid);
}
throw new IllegalArgumentException("EntityType " + type + " is not supported!");
}

44
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java

@ -0,0 +1,44 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
/**
* Created by Victor Basanets on 8/27/2017.
*/
public class EntityViewId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@JsonCreator
public EntityViewId(@JsonProperty("id") UUID id) {
super(id);
}
public static EntityViewId fromString(String entityViewID) {
return new EntityViewId(UUID.fromString(entityViewID));
}
@Override
public EntityType getEntityType() {
return EntityType.ENTITY_VIEW;
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java

@ -42,4 +42,8 @@ public class BaseReadTsKvQuery extends BaseTsKvQuery implements ReadTsKvQuery {
this(key, startTs, endTs, endTs - startTs, 1, Aggregation.AVG, "DESC");
}
public BaseReadTsKvQuery(String key, long startTs, long endTs, int limit, String orderBy) {
this(key, startTs, endTs, endTs - startTs, limit, Aggregation.NONE, orderBy);
}
}

47
common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java

@ -0,0 +1,47 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.objects;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Victor Basanets on 9/05/2017.
*/
@Data
@NoArgsConstructor
public class AttributesEntityView {
private List<String> cs = new ArrayList<>();
private List<String> ss = new ArrayList<>();
private List<String> sh = new ArrayList<>();
public AttributesEntityView(List<String> cs,
List<String> ss,
List<String> sh) {
this.cs = new ArrayList<>(cs);
this.ss = new ArrayList<>(ss);
this.sh = new ArrayList<>(sh);
}
public AttributesEntityView(AttributesEntityView obj) {
this(obj.getCs(), obj.getSs(), obj.getSh());
}
}

43
common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.objects;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Victor Basanets on 9/05/2017.
*/
@Data
@NoArgsConstructor
public class TelemetryEntityView {
private List<String> timeseries;
private AttributesEntityView attributes;
public TelemetryEntityView(List<String> timeseries, AttributesEntityView attributes) {
this.timeseries = new ArrayList<>(timeseries);
this.attributes = attributes;
}
public TelemetryEntityView(TelemetryEntityView obj) {
this(obj.getTimeseries(), obj.getAttributes());
}
}

5
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java

@ -32,6 +32,7 @@ import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.DataValidator;
@ -69,6 +70,9 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
@Autowired
private DeviceService deviceService;
@Autowired
private EntityViewService entityViewService;
@Autowired
private DashboardService dashboardService;
@ -113,6 +117,7 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
dashboardService.unassignCustomerDashboards(customerId);
assetService.unassignCustomerAssets(customer.getTenantId(), customerId);
deviceService.unassignCustomerDevices(customer.getTenantId(), customerId);
entityViewService.unassignCustomerEntityViews(customer.getTenantId(), customerId);
userService.deleteCustomerUsers(customer.getTenantId(), customerId);
deleteEntityRelations(customerId);
customerDao.removeById(customerId.getId());

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

@ -0,0 +1,120 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.entityview;
import com.datastax.driver.core.Statement;
import com.datastax.driver.core.querybuilder.Select;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.EntitySubtypeEntity;
import org.thingsboard.server.dao.model.nosql.EntityViewEntity;
import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao;
import org.thingsboard.server.dao.util.NoSqlDao;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_AND_SEARCH_TEXT;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_NAME_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TABLE_FAMILY_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TENANT_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ID_PROPERTY;
/**
* Created by Victor Basanets on 9/06/2017.
*/
@Component
@Slf4j
@NoSqlDao
public class CassandraEntityViewDao extends CassandraAbstractSearchTextDao<EntityViewEntity, EntityView> implements EntityViewDao {
@Override
protected Class<EntityViewEntity> getColumnFamilyClass() {
return EntityViewEntity.class;
}
@Override
protected String getColumnFamilyName() {
return ENTITY_VIEW_TABLE_FAMILY_NAME;
}
@Override
public EntityView save(EntityView domain) {
EntityView savedEntityView = super.save(domain);
EntitySubtype entitySubtype = new EntitySubtype(savedEntityView.getTenantId(), EntityType.ENTITY_VIEW,
savedEntityView.getId().getEntityType().toString());
EntitySubtypeEntity entitySubtypeEntity = new EntitySubtypeEntity(entitySubtype);
Statement saveStatement = cluster.getMapper(EntitySubtypeEntity.class).saveQuery(entitySubtypeEntity);
executeWrite(saveStatement);
return savedEntityView;
}
@Override
public List<EntityView> findEntityViewsByTenantId(UUID tenantId, TextPageLink pageLink) {
log.debug("Try to find entity views by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
List<EntityViewEntity> entityViewEntities =
findPageWithTextSearch(ENTITY_VIEW_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
Collections.singletonList(eq(TENANT_ID_PROPERTY, tenantId)), pageLink);
log.trace("Found entity views [{}] by tenantId [{}] and pageLink [{}]",
entityViewEntities, tenantId, pageLink);
return DaoUtil.convertDataList(entityViewEntities);
}
@Override
public Optional<EntityView> findEntityViewByTenantIdAndName(UUID tenantId, String name) {
Select.Where query = select().from(ENTITY_VIEW_BY_TENANT_AND_NAME).where();
query.and(eq(ENTITY_VIEW_TENANT_ID_PROPERTY, tenantId));
query.and(eq(ENTITY_VIEW_NAME_PROPERTY, name));
return Optional.ofNullable(DaoUtil.getData(findOneByStatement(query)));
}
@Override
public List<EntityView> findEntityViewsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) {
log.debug("Try to find entity views by tenantId [{}], customerId[{}] and pageLink [{}]",
tenantId, customerId, pageLink);
List<EntityViewEntity> entityViewEntities = findPageWithTextSearch(
ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_AND_SEARCH_TEXT,
Arrays.asList(eq(CUSTOMER_ID_PROPERTY, customerId), eq(TENANT_ID_PROPERTY, tenantId)),
pageLink);
log.trace("Found find entity views [{}] by tenantId [{}], customerId [{}] and pageLink [{}]",
entityViewEntities, tenantId, customerId, pageLink);
return DaoUtil.convertDataList(entityViewEntities);
}
@Override
public ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId) {
log.debug("Try to find entity views by tenantId [{}] and entityId [{}]", tenantId, entityId);
Select.Where query = select().from(getColumnFamilyName()).where();
query.and(eq(TENANT_ID_PROPERTY, tenantId));
query.and(eq(ENTITY_ID_COLUMN, entityId));
return findListByStatementAsync(query);
}
}

73
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java

@ -0,0 +1,73 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.entityview;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.Dao;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* Created by Victor Basanets on 8/28/2017.
*/
public interface EntityViewDao extends Dao<EntityView> {
/**
* Save or update device object
*
* @param entityView the entity-view object
* @return saved entity-view object
*/
EntityView save(EntityView entityView);
/**
* Find entity views by tenantId and page link.
*
* @param tenantId the tenantId
* @param pageLink the page link
* @return the list of entity view objects
*/
List<EntityView> findEntityViewsByTenantId(UUID tenantId, TextPageLink pageLink);
/**
* Find entity views by tenantId and entity view name.
*
* @param tenantId the tenantId
* @param name the entity view name
* @return the optional entity view object
*/
Optional<EntityView> findEntityViewByTenantIdAndName(UUID tenantId, String name);
/**
* Find entity views by tenantId, customerId and page link.
*
* @param tenantId the tenantId
* @param customerId the customerId
* @param pageLink the page link
* @return the list of entity view objects
*/
List<EntityView> findEntityViewsByTenantIdAndCustomerId(UUID tenantId,
UUID customerId,
TextPageLink pageLink);
ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId);
}

58
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java

@ -0,0 +1,58 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.entityview;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import java.util.List;
/**
* Created by Victor Basanets on 8/27/2017.
*/
public interface EntityViewService {
EntityView saveEntityView(EntityView entityView);
EntityView assignEntityViewToCustomer(EntityViewId entityViewId, CustomerId customerId);
EntityView unassignEntityViewFromCustomer(EntityViewId entityViewId);
void unassignCustomerEntityViews(TenantId tenantId, CustomerId customerId);
EntityView findEntityViewById(EntityViewId entityViewId);
TextPageData<EntityView> findEntityViewByTenantId(TenantId tenantId, TextPageLink pageLink);
TextPageData<EntityView> findEntityViewsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
ListenableFuture<List<EntityView>> findEntityViewsByQuery(EntityViewSearchQuery query);
ListenableFuture<EntityView> findEntityViewByIdAsync(EntityViewId entityViewId);
ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(TenantId tenantId, EntityId entityId);
void deleteEntityView(EntityViewId entityViewId);
void deleteEntityViewsByTenantId(TenantId tenantId);
}

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

@ -0,0 +1,356 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.entityview;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerDao;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.tenant.TenantDao;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.CacheConstants.ENTITY_VIEW_CACHE;
import static org.thingsboard.server.common.data.CacheConstants.RELATIONS_CACHE;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
/**
* Created by Victor Basanets on 8/28/2017.
*/
@Service
@Slf4j
public class EntityViewServiceImpl extends AbstractEntityService implements EntityViewService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_PAGE_LINK = "Incorrect page link ";
public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId ";
public static final String INCORRECT_ENTITY_VIEW_ID = "Incorrect entityViewId ";
@Autowired
private EntityViewDao entityViewDao;
@Autowired
private TenantDao tenantDao;
@Autowired
private CustomerDao customerDao;
@Autowired
private AttributesService attributesService;
@Autowired
private CacheManager cacheManager;
@Caching(evict = {
@CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityView.tenantId, #entityView.entityId}"),
@CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityView.id}")})
@Override
public EntityView saveEntityView(EntityView entityView) {
log.trace("Executing save entity view [{}]", entityView);
entityViewValidator.validate(entityView);
EntityView savedEntityView = entityViewDao.save(entityView);
List<ListenableFuture<List<Void>>> futures = new ArrayList<>();
if (savedEntityView.getKeys() != null) {
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.CLIENT_SCOPE, savedEntityView.getKeys().getAttributes().getCs()));
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SERVER_SCOPE, savedEntityView.getKeys().getAttributes().getSs()));
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SHARED_SCOPE, savedEntityView.getKeys().getAttributes().getSh()));
}
for (ListenableFuture<List<Void>> future : futures) {
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
log.error("Failed to copy attributes to entity view", e);
throw new RuntimeException("Failed to copy attributes to entity view", e);
}
}
return savedEntityView;
}
@Override
public EntityView assignEntityViewToCustomer(EntityViewId entityViewId, CustomerId customerId) {
EntityView entityView = findEntityViewById(entityViewId);
entityView.setCustomerId(customerId);
return saveEntityView(entityView);
}
@CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityViewId}")
@Override
public EntityView unassignEntityViewFromCustomer(EntityViewId entityViewId) {
EntityView entityView = findEntityViewById(entityViewId);
entityView.setCustomerId(null);
return saveEntityView(entityView);
}
@Override
public void unassignCustomerEntityViews(TenantId tenantId, CustomerId customerId) {
log.trace("Executing unassignCustomerEntityViews, tenantId [{}], customerId [{}]", tenantId, customerId);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateId(customerId, INCORRECT_CUSTOMER_ID + customerId);
new CustomerEntityViewsUnAssigner(tenantId).removeEntities(customerId);
}
@Cacheable(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityViewId}")
@Override
public EntityView findEntityViewById(EntityViewId entityViewId) {
log.trace("Executing findEntityViewById [{}]", entityViewId);
validateId(entityViewId, INCORRECT_ENTITY_VIEW_ID + entityViewId);
return entityViewDao.findById(entityViewId.getId());
}
@Override
public TextPageData<EntityView> findEntityViewByTenantId(TenantId tenantId, TextPageLink pageLink) {
log.trace("Executing findEntityViewsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink);
List<EntityView> entityViews = entityViewDao.findEntityViewsByTenantId(tenantId.getId(), pageLink);
return new TextPageData<>(entityViews, pageLink);
}
@Override
public TextPageData<EntityView> findEntityViewsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId,
TextPageLink pageLink) {
log.trace("Executing findEntityViewByTenantIdAndCustomerId, tenantId [{}], customerId [{}]," +
" pageLink [{}]", tenantId, customerId, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateId(customerId, INCORRECT_CUSTOMER_ID + customerId);
validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink);
List<EntityView> entityViews = entityViewDao.findEntityViewsByTenantIdAndCustomerId(tenantId.getId(),
customerId.getId(), pageLink);
return new TextPageData<>(entityViews, pageLink);
}
@Override
public ListenableFuture<List<EntityView>> findEntityViewsByQuery(EntityViewSearchQuery query) {
ListenableFuture<List<EntityRelation>> relations = relationService.findByQuery(query.toEntitySearchQuery());
ListenableFuture<List<EntityView>> entityViews = Futures.transformAsync(relations, r -> {
EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection();
List<ListenableFuture<EntityView>> futures = new ArrayList<>();
for (EntityRelation relation : r) {
EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom();
if (entityId.getEntityType() == EntityType.ENTITY_VIEW) {
futures.add(findEntityViewByIdAsync(new EntityViewId(entityId.getId())));
}
}
return Futures.successfulAsList(futures);
});
return entityViews;
}
@Override
public ListenableFuture<EntityView> findEntityViewByIdAsync(EntityViewId entityViewId) {
log.trace("Executing findEntityViewById [{}]", entityViewId);
validateId(entityViewId, INCORRECT_ENTITY_VIEW_ID + entityViewId);
return entityViewDao.findByIdAsync(entityViewId.getId());
}
@Override
public ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(TenantId tenantId, EntityId entityId) {
log.trace("Executing findEntityViewsByTenantIdAndEntityIdAsync, tenantId [{}], entityId [{}]", tenantId, entityId);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateId(entityId.getId(), "Incorrect entityId" + entityId);
List<Object> tenantIdAndEntityId = new ArrayList<>();
tenantIdAndEntityId.add(tenantId);
tenantIdAndEntityId.add(entityId);
Cache cache = cacheManager.getCache(ENTITY_VIEW_CACHE);
List<EntityView> fromCache = cache.get(tenantIdAndEntityId, List.class);
if (fromCache != null) {
return Futures.immediateFuture(fromCache);
} else {
ListenableFuture<List<EntityView>> entityViewsFuture = entityViewDao.findEntityViewsByTenantIdAndEntityIdAsync(tenantId.getId(), entityId.getId());
Futures.addCallback(entityViewsFuture,
new FutureCallback<List<EntityView>>() {
@Override
public void onSuccess(@Nullable List<EntityView> result) {
cache.putIfAbsent(tenantIdAndEntityId, result);
}
@Override
public void onFailure(Throwable t) {
log.error("Error while finding entity views by tenantId and entityId", t);
}
});
return entityViewsFuture;
}
}
@CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityViewId}")
@Override
public void deleteEntityView(EntityViewId entityViewId) {
log.trace("Executing deleteEntityView [{}]", entityViewId);
validateId(entityViewId, INCORRECT_ENTITY_VIEW_ID + entityViewId);
deleteEntityRelations(entityViewId);
EntityView entityView = entityViewDao.findById(entityViewId.getId());
cacheManager.getCache(ENTITY_VIEW_CACHE).evict(Arrays.asList(entityView.getTenantId(), entityView.getEntityId()));
entityViewDao.removeById(entityViewId.getId());
}
@Override
public void deleteEntityViewsByTenantId(TenantId tenantId) {
log.trace("Executing deleteEntityViewsByTenantId, tenantId [{}]", tenantId);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
tenantEntityViewRemover.removeEntities(tenantId);
}
private ListenableFuture<List<Void>> copyAttributesFromEntityToEntityView(EntityView entityView, String scope, Collection<String> keys) {
if (keys != null && !keys.isEmpty()) {
ListenableFuture<List<AttributeKvEntry>> getAttrFuture = attributesService.find(entityView.getEntityId(), scope, keys);
return Futures.transform(getAttrFuture, attributeKvEntries -> {
List<AttributeKvEntry> filteredAttributes = new ArrayList<>();
if (attributeKvEntries != null && !attributeKvEntries.isEmpty()) {
filteredAttributes =
attributeKvEntries.stream()
.filter(attributeKvEntry -> {
long startTime = entityView.getStartTimeMs();
long endTime = entityView.getEndTimeMs();
long lastUpdateTs = attributeKvEntry.getLastUpdateTs();
return startTime == 0 && endTime == 0 ||
(endTime == 0 && startTime < lastUpdateTs) ||
(startTime == 0 && endTime > lastUpdateTs)
? true : startTime < lastUpdateTs && endTime > lastUpdateTs;
}).collect(Collectors.toList());
}
try {
return attributesService.save(entityView.getId(), scope, filteredAttributes).get();
} catch (InterruptedException | ExecutionException e) {
log.error("Failed to copy attributes to entity view", e);
throw new RuntimeException("Failed to copy attributes to entity view", e);
}
});
} else {
return Futures.immediateFuture(null);
}
}
private DataValidator<EntityView> entityViewValidator =
new DataValidator<EntityView>() {
@Override
protected void validateCreate(EntityView entityView) {
entityViewDao.findEntityViewByTenantIdAndName(entityView.getTenantId().getId(), entityView.getName())
.ifPresent(e -> {
throw new DataValidationException("Entity view with such name already exists!");
});
}
@Override
protected void validateUpdate(EntityView entityView) {
entityViewDao.findEntityViewByTenantIdAndName(entityView.getTenantId().getId(), entityView.getName())
.ifPresent(e -> {
if (!e.getUuidId().equals(entityView.getUuidId())) {
throw new DataValidationException("Entity view with such name already exists!");
}
});
}
@Override
protected void validateDataImpl(EntityView entityView) {
if (StringUtils.isEmpty(entityView.getName())) {
throw new DataValidationException("Entity view name should be specified!");
}
if (entityView.getTenantId() == null) {
throw new DataValidationException("Entity view should be assigned to tenant!");
} else {
Tenant tenant = tenantDao.findById(entityView.getTenantId().getId());
if (tenant == null) {
throw new DataValidationException("Entity view is referencing to non-existent tenant!");
}
}
if (entityView.getCustomerId() == null) {
entityView.setCustomerId(new CustomerId(NULL_UUID));
} else if (!entityView.getCustomerId().getId().equals(NULL_UUID)) {
Customer customer = customerDao.findById(entityView.getCustomerId().getId());
if (customer == null) {
throw new DataValidationException("Can't assign entity view to non-existent customer!");
}
if (!customer.getTenantId().getId().equals(entityView.getTenantId().getId())) {
throw new DataValidationException("Can't assign entity view to customer from different tenant!");
}
}
}
};
private PaginatedRemover<TenantId, EntityView> tenantEntityViewRemover =
new PaginatedRemover<TenantId, EntityView>() {
@Override
protected List<EntityView> findEntities(TenantId id, TextPageLink pageLink) {
return entityViewDao.findEntityViewsByTenantId(id.getId(), pageLink);
}
@Override
protected void removeEntity(EntityView entity) {
deleteEntityView(new EntityViewId(entity.getUuidId()));
}
};
private class CustomerEntityViewsUnAssigner extends PaginatedRemover<CustomerId, EntityView> {
private TenantId tenantId;
CustomerEntityViewsUnAssigner(TenantId tenantId) {
this.tenantId = tenantId;
}
@Override
protected List<EntityView> findEntities(CustomerId id, TextPageLink pageLink) {
return entityViewDao.findEntityViewsByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink);
}
@Override
protected void removeEntity(EntityView entity) {
unassignEntityViewFromCustomer(new EntityViewId(entity.getUuidId()));
}
}
}

19
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -52,7 +52,6 @@ public class ModelConstants {
public static final String ATTRIBUTE_KEY_COLUMN = "attribute_key";
public static final String LAST_UPDATE_TS_COLUMN = "last_update_ts";
/**
* Cassandra user constants.
*/
@ -130,12 +129,12 @@ public class ModelConstants {
* Cassandra device constants.
*/
public static final String DEVICE_COLUMN_FAMILY_NAME = "device";
public static final String DEVICE_FAMILY_NAME = "device";
public static final String DEVICE_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
public static final String DEVICE_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY;
public static final String DEVICE_NAME_PROPERTY = "name";
public static final String DEVICE_TYPE_PROPERTY = "type";
public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text";
public static final String DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_by_type_and_search_text";
public static final String DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_and_search_text";
@ -143,6 +142,22 @@ public class ModelConstants {
public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name";
public static final String DEVICE_TYPES_BY_TENANT_VIEW_NAME = "device_types_by_tenant";
/**
* Cassandra entityView constants.
*/
public static final String ENTITY_VIEW_TABLE_FAMILY_NAME = "entity_views";
public static final String ENTITY_VIEW_ENTITY_ID_PROPERTY = ENTITY_ID_COLUMN;
public static final String ENTITY_VIEW_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
public static final String ENTITY_VIEW_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY;
public static final String ENTITY_VIEW_NAME_PROPERTY = DEVICE_NAME_PROPERTY;
public static final String ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_AND_SEARCH_TEXT = "entity_view_by_tenant_and_customer";
public static final String ENTITY_VIEW_KEYS_PROPERTY = "keys";
public static final String ENTITY_VIEW_START_TS_PROPERTY = "start_ts";
public static final String ENTITY_VIEW_END_TS_PROPERTY = "end_ts";
public static final String ENTITY_VIEW_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
public static final String ENTITY_VIEW_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "entity_view_by_tenant_and_search_text";
public static final String ENTITY_VIEW_BY_TENANT_AND_NAME = "entity_view_by_tenant_and_name";
/**
* Cassandra audit log constants.
*/

157
dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java

@ -0,0 +1,157 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.model.nosql;
import com.datastax.driver.core.utils.UUIDs;
import com.datastax.driver.mapping.annotations.Column;
import com.datastax.driver.mapping.annotations.PartitionKey;
import com.datastax.driver.mapping.annotations.Table;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.annotations.Type;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.objects.TelemetryEntityView;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.SearchTextEntity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import java.io.IOException;
import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TABLE_FAMILY_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
/**
* Created by Victor Basanets on 8/31/2017.
*/
@Data
@Table(name = ENTITY_VIEW_TABLE_FAMILY_NAME)
@EqualsAndHashCode
@ToString
@Slf4j
public class EntityViewEntity implements SearchTextEntity<EntityView> {
@PartitionKey(value = 0)
@Column(name = ID_PROPERTY)
private UUID id;
@Enumerated(EnumType.STRING)
@Column(name = ENTITY_TYPE_PROPERTY)
private EntityType entityType;
@PartitionKey(value = 1)
@Column(name = ModelConstants.ENTITY_VIEW_TENANT_ID_PROPERTY)
private UUID tenantId;
@PartitionKey(value = 2)
@Column(name = ModelConstants.ENTITY_VIEW_CUSTOMER_ID_PROPERTY)
private UUID customerId;
@Column(name = ModelConstants.ENTITY_VIEW_ENTITY_ID_PROPERTY)
private UUID entityId;
@Column(name = ModelConstants.ENTITY_VIEW_NAME_PROPERTY)
private String name;
@Column(name = ModelConstants.ENTITY_VIEW_KEYS_PROPERTY)
private String keys;
@Column(name = ModelConstants.ENTITY_VIEW_START_TS_PROPERTY)
private long startTs;
@Column(name = ModelConstants.ENTITY_VIEW_END_TS_PROPERTY)
private long endTs;
@Column(name = ModelConstants.SEARCH_TEXT_PROPERTY)
private String searchText;
@Type(type = "json")
@Column(name = ModelConstants.ENTITY_VIEW_ADDITIONAL_INFO_PROPERTY)
private JsonNode additionalInfo;
private static final ObjectMapper mapper = new ObjectMapper();
public EntityViewEntity() {
super();
}
public EntityViewEntity(EntityView entityView) {
if (entityView.getId() != null) {
this.id = entityView.getId().getId();
}
if (entityView.getEntityId() != null) {
this.entityId = entityView.getEntityId().getId();
this.entityType = entityView.getEntityId().getEntityType();
}
if (entityView.getTenantId() != null) {
this.tenantId = entityView.getTenantId().getId();
}
if (entityView.getCustomerId() != null) {
this.customerId = entityView.getCustomerId().getId();
}
this.name = entityView.getName();
try {
this.keys = mapper.writeValueAsString(entityView.getKeys());
} catch (IOException e) {
log.error("Unable to serialize entity view keys!", e);
}
this.startTs = entityView.getStartTimeMs();
this.endTs = entityView.getEndTimeMs();
this.searchText = entityView.getSearchText();
this.additionalInfo = entityView.getAdditionalInfo();
}
@Override
public String getSearchTextSource() {
return name;
}
@Override
public EntityView toData() {
EntityView entityView = new EntityView(new EntityViewId(id));
entityView.setCreatedTime(UUIDs.unixTimestamp(id));
if (entityId != null) {
entityView.setEntityId(EntityIdFactory.getByTypeAndId(entityType.name(), entityId.toString()));
}
if (tenantId != null) {
entityView.setTenantId(new TenantId(tenantId));
}
if (customerId != null) {
entityView.setCustomerId(new CustomerId(customerId));
}
entityView.setName(name);
try {
entityView.setKeys(mapper.readValue(keys, TelemetryEntityView.class));
} catch (IOException e) {
log.error("Unable to read entity view keys!", e);
}
entityView.setStartTimeMs(startTs);
entityView.setEndTimeMs(endTs);
entityView.setAdditionalInfo(additionalInfo);
return entityView;
}
}

158
dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java

@ -0,0 +1,158 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.model.sql;
import com.datastax.driver.core.utils.UUIDs;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.objects.TelemetryEntityView;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.SearchTextEntity;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Table;
import java.io.IOException;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_PROPERTY;
/**
* Created by Victor Basanets on 8/30/2017.
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@TypeDef(name = "json", typeClass = JsonStringType.class)
@Table(name = ModelConstants.ENTITY_VIEW_TABLE_FAMILY_NAME)
@Slf4j
public class EntityViewEntity extends BaseSqlEntity<EntityView> implements SearchTextEntity<EntityView> {
@Column(name = ModelConstants.ENTITY_VIEW_ENTITY_ID_PROPERTY)
private String entityId;
@Enumerated(EnumType.STRING)
@Column(name = ENTITY_TYPE_PROPERTY)
private EntityType entityType;
@Column(name = ModelConstants.ENTITY_VIEW_TENANT_ID_PROPERTY)
private String tenantId;
@Column(name = ModelConstants.ENTITY_VIEW_CUSTOMER_ID_PROPERTY)
private String customerId;
@Column(name = ModelConstants.ENTITY_VIEW_NAME_PROPERTY)
private String name;
@Column(name = ModelConstants.ENTITY_VIEW_KEYS_PROPERTY)
private String keys;
@Column(name = ModelConstants.ENTITY_VIEW_START_TS_PROPERTY)
private long startTs;
@Column(name = ModelConstants.ENTITY_VIEW_END_TS_PROPERTY)
private long endTs;
@Column(name = ModelConstants.SEARCH_TEXT_PROPERTY)
private String searchText;
@Type(type = "json")
@Column(name = ModelConstants.ENTITY_VIEW_ADDITIONAL_INFO_PROPERTY)
private JsonNode additionalInfo;
private static final ObjectMapper mapper = new ObjectMapper();
public EntityViewEntity() {
super();
}
public EntityViewEntity(EntityView entityView) {
if (entityView.getId() != null) {
this.setId(entityView.getId().getId());
}
if (entityView.getEntityId() != null) {
this.entityId = toString(entityView.getEntityId().getId());
this.entityType = entityView.getEntityId().getEntityType();
}
if (entityView.getTenantId() != null) {
this.tenantId = toString(entityView.getTenantId().getId());
}
if (entityView.getCustomerId() != null) {
this.customerId = toString(entityView.getCustomerId().getId());
}
this.name = entityView.getName();
try {
this.keys = mapper.writeValueAsString(entityView.getKeys());
} catch (IOException e) {
log.error("Unable to serialize entity view keys!", e);
}
this.startTs = entityView.getStartTimeMs();
this.endTs = entityView.getEndTimeMs();
this.searchText = entityView.getSearchText();
this.additionalInfo = entityView.getAdditionalInfo();
}
@Override
public String getSearchTextSource() {
return name;
}
@Override
public void setSearchText(String searchText) {
this.searchText = searchText;
}
@Override
public EntityView toData() {
EntityView entityView = new EntityView(new EntityViewId(getId()));
entityView.setCreatedTime(UUIDs.unixTimestamp(getId()));
if (entityId != null) {
entityView.setEntityId(EntityIdFactory.getByTypeAndId(entityType.name(), toUUID(entityId).toString()));
}
if (tenantId != null) {
entityView.setTenantId(new TenantId(toUUID(tenantId)));
}
if (customerId != null) {
entityView.setCustomerId(new CustomerId(toUUID(customerId)));
}
entityView.setName(name);
try {
entityView.setKeys(mapper.readValue(keys, TelemetryEntityView.class));
} catch (IOException e) {
log.error("Unable to read entity view keys!", e);
}
entityView.setStartTimeMs(startTs);
entityView.setEndTimeMs(endTs);
entityView.setAdditionalInfo(additionalInfo);
return entityView;
}
}

56
dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java

@ -0,0 +1,56 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.entityview;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.dao.model.sql.EntityViewEntity;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
/**
* Created by Victor Basanets on 8/31/2017.
*/
@SqlDao
public interface EntityViewRepository extends CrudRepository<EntityViewEntity, String> {
@Query("SELECT e FROM EntityViewEntity e WHERE e.tenantId = :tenantId " +
"AND LOWER(e.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " +
"AND e.id > :idOffset ORDER BY e.id")
List<EntityViewEntity> findByTenantId(@Param("tenantId") String tenantId,
@Param("textSearch") String textSearch,
@Param("idOffset") String idOffset,
Pageable pageable);
@Query("SELECT e FROM EntityViewEntity e WHERE e.tenantId = :tenantId " +
"AND e.customerId = :customerId " +
"AND LOWER(e.searchText) LIKE LOWER(CONCAT(:searchText, '%')) " +
"AND e.id > :idOffset ORDER BY e.id")
List<EntityViewEntity> findByTenantIdAndCustomerId(@Param("tenantId") String tenantId,
@Param("customerId") String customerId,
@Param("searchText") String searchText,
@Param("idOffset") String idOffset,
Pageable pageable);
EntityViewEntity findByTenantIdAndName(String tenantId, String name);
List<EntityViewEntity> findAllByTenantIdAndEntityId(String tenantId, String entityId);
}

103
dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java

@ -0,0 +1,103 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.entityview;
import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.UUIDConverter;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.entityview.EntityViewDao;
import org.thingsboard.server.dao.model.sql.EntityViewEntity;
import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUIDs;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR;
/**
* Created by Victor Basanets on 8/31/2017.
*/
@Component
@SqlDao
public class JpaEntityViewDao extends JpaAbstractSearchTextDao<EntityViewEntity, EntityView>
implements EntityViewDao {
@Autowired
private EntityViewRepository entityViewRepository;
@Override
protected Class<EntityViewEntity> getEntityClass() {
return EntityViewEntity.class;
}
@Override
protected CrudRepository<EntityViewEntity, String> getCrudRepository() {
return entityViewRepository;
}
@Override
public List<EntityView> findEntityViewsByTenantId(UUID tenantId, TextPageLink pageLink) {
return DaoUtil.convertDataList(
entityViewRepository.findByTenantId(
fromTimeUUID(tenantId),
Objects.toString(pageLink.getTextSearch(), ""),
pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()),
new PageRequest(0, pageLink.getLimit())));
}
@Override
public Optional<EntityView> findEntityViewByTenantIdAndName(UUID tenantId, String name) {
return Optional.ofNullable(
DaoUtil.getData(entityViewRepository.findByTenantIdAndName(fromTimeUUID(tenantId), name)));
}
@Override
public List<EntityView> findEntityViewsByTenantIdAndCustomerId(UUID tenantId,
UUID customerId,
TextPageLink pageLink) {
return DaoUtil.convertDataList(
entityViewRepository.findByTenantIdAndCustomerId(
fromTimeUUID(tenantId),
fromTimeUUID(customerId),
Objects.toString(pageLink.getTextSearch(), ""),
pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()),
new PageRequest(0, pageLink.getLimit())
));
}
@Override
public ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId) {
return service.submit(() -> DaoUtil.convertDataList(
entityViewRepository.findAllByTenantIdAndEntityId(UUIDConverter.fromTimeUUID(tenantId), UUIDConverter.fromTimeUUID(entityId))));
}
}

5
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java

@ -29,6 +29,7 @@ import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DataValidator;
@ -63,6 +64,9 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe
@Autowired
private DeviceService deviceService;
@Autowired
private EntityViewService entityViewService;
@Autowired
private WidgetsBundleService widgetsBundleService;
@ -103,6 +107,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe
dashboardService.deleteDashboardsByTenantId(tenantId);
assetService.deleteAssetsByTenantId(tenantId);
deviceService.deleteDevicesByTenantId(tenantId);
entityViewService.deleteEntityViewsByTenantId(tenantId);
userService.deleteTenantAdmins(tenantId);
ruleChainService.deleteRuleChainsByTenantId(tenantId);
tenantDao.removeById(tenantId.getId());

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

@ -21,15 +21,22 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.Validator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank;
@ -46,10 +53,21 @@ public class BaseTimeseriesService implements TimeseriesService {
@Autowired
private TimeseriesDao timeseriesDao;
@Autowired
private EntityViewService entityViewService;
@Override
public ListenableFuture<List<TsKvEntry>> findAll(EntityId entityId, List<ReadTsKvQuery> queries) {
validate(entityId);
queries.forEach(BaseTimeseriesService::validate);
if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) {
EntityView entityView = entityViewService.findEntityViewById((EntityViewId) entityId);
List<ReadTsKvQuery> filteredQueries =
queries.stream()
.filter(query -> entityView.getKeys().getTimeseries().isEmpty() || entityView.getKeys().getTimeseries().contains(query.getKey()))
.collect(Collectors.toList());
return timeseriesDao.findAllAsync(entityView.getEntityId(), updateQueriesForEntityView(entityView, filteredQueries));
}
return timeseriesDao.findAllAsync(entityId, queries);
}
@ -58,6 +76,19 @@ public class BaseTimeseriesService implements TimeseriesService {
validate(entityId);
List<ListenableFuture<TsKvEntry>> futures = Lists.newArrayListWithExpectedSize(keys.size());
keys.forEach(key -> Validator.validateString(key, "Incorrect key " + key));
if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) {
EntityView entityView = entityViewService.findEntityViewById((EntityViewId) entityId);
List<String> filteredKeys = new ArrayList<>(keys);
if (!entityView.getKeys().getTimeseries().isEmpty()) {
filteredKeys.retainAll(entityView.getKeys().getTimeseries());
}
List<ReadTsKvQuery> queries =
filteredKeys.stream()
.map(key -> new BaseReadTsKvQuery(key, entityView.getStartTimeMs(), entityView.getEndTimeMs(), 1, "ASC"))
.collect(Collectors.toList());
return timeseriesDao.findAllAsync(entityView.getEntityId(), updateQueriesForEntityView(entityView, queries));
}
keys.forEach(key -> futures.add(timeseriesDao.findLatest(entityId, key)));
return Futures.allAsList(futures);
}
@ -92,11 +123,24 @@ public class BaseTimeseriesService implements TimeseriesService {
}
private void saveAndRegisterFutures(List<ListenableFuture<Void>> futures, EntityId entityId, TsKvEntry tsKvEntry, long ttl) {
if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) {
throw new IncorrectParameterException("Telemetry data can't be stored for entity view. Only read only");
}
futures.add(timeseriesDao.savePartition(entityId, tsKvEntry.getTs(), tsKvEntry.getKey(), ttl));
futures.add(timeseriesDao.saveLatest(entityId, tsKvEntry));
futures.add(timeseriesDao.save(entityId, tsKvEntry, ttl));
}
private List<ReadTsKvQuery> updateQueriesForEntityView(EntityView entityView, List<ReadTsKvQuery> queries) {
return queries.stream().map(query -> {
long startTs = entityView.getStartTimeMs() == 0 ? query.getStartTs() : entityView.getStartTimeMs();
long endTs = entityView.getEndTimeMs() == 0 ? query.getEndTs() : entityView.getEndTimeMs();
return startTs <= query.getStartTs() && endTs >= query.getEndTs() ? query :
new BaseReadTsKvQuery(query.getKey(), startTs, endTs, query.getInterval(), query.getLimit(), query.getAggregation());
}).collect(Collectors.toList());
}
@Override
public ListenableFuture<List<Void>> remove(EntityId entityId, List<DeleteTsKvQuery> deleteTsKvQueries) {
validate(entityId);

78
dao/src/main/resources/cassandra/schema-entities.cql

@ -165,35 +165,55 @@ CREATE TABLE IF NOT EXISTS thingsboard.device (
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_and_name AS
SELECT *
from thingsboard.device
WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL
WHERE tenant_id IS NOT NULL
AND customer_id IS NOT NULL
AND type IS NOT NULL
AND name IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY ( tenant_id, name, id, customer_id, type)
WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_and_search_text AS
SELECT *
from thingsboard.device
WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
WHERE tenant_id IS NOT NULL
AND customer_id IS NOT NULL
AND type IS NOT NULL
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY ( tenant_id, search_text, id, customer_id, type)
WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_by_type_and_search_text AS
SELECT *
from thingsboard.device
WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
WHERE tenant_id IS NOT NULL
AND customer_id IS NOT NULL
AND type IS NOT NULL
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY ( tenant_id, type, search_text, id, customer_id)
WITH CLUSTERING ORDER BY ( type ASC, search_text ASC, id DESC, customer_id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_customer_and_search_text AS
SELECT *
from thingsboard.device
WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
WHERE tenant_id IS NOT NULL
AND customer_id IS NOT NULL
AND type IS NOT NULL
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY ( customer_id, tenant_id, search_text, id, type )
WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC );
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_customer_by_type_and_search_text AS
SELECT *
from thingsboard.device
WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
WHERE tenant_id IS NOT NULL
AND customer_id IS NOT NULL
AND type IS NOT NULL
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY ( customer_id, tenant_id, type, search_text, id )
WITH CLUSTERING ORDER BY ( tenant_id DESC, type ASC, search_text ASC, id DESC );
@ -603,3 +623,51 @@ CREATE TABLE IF NOT EXISTS thingsboard.rule_node (
additional_info text,
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS thingsboard.entity_views (
id timeuuid,
entity_id timeuuid,
entity_type text,
tenant_id timeuuid,
customer_id timeuuid,
name text,
keys text,
start_ts bigint,
end_ts bigint,
search_text text,
additional_info text,
PRIMARY KEY (id, entity_id, tenant_id, customer_id)
);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_name AS
SELECT *
from thingsboard.entity_views
WHERE tenant_id IS NOT NULL
AND entity_id IS NOT NULL
AND customer_id IS NOT NULL
AND name IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY (tenant_id, name, id, customer_id, entity_id)
WITH CLUSTERING ORDER BY (name ASC, id DESC, customer_id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_search_text AS
SELECT *
from thingsboard.entity_views
WHERE tenant_id IS NOT NULL
AND entity_id IS NOT NULL
AND customer_id IS NOT NULL
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY (tenant_id, search_text, id, customer_id, entity_id)
WITH CLUSTERING ORDER BY (search_text ASC, id DESC, customer_id DESC);
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_customer AS
SELECT *
from thingsboard.entity_views
WHERE tenant_id IS NOT NULL
AND customer_id IS NOT NULL
AND entity_id IS NOT NULL
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY (tenant_id, customer_id, search_text, id, entity_id)
WITH CLUSTERING ORDER BY (customer_id DESC, search_text ASC, id DESC);

14
dao/src/main/resources/sql/schema-entities.sql

@ -227,3 +227,17 @@ CREATE TABLE IF NOT EXISTS rule_node (
debug_mode boolean,
search_text varchar(255)
);
CREATE TABLE IF NOT EXISTS entity_views (
id varchar(31) NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY,
entity_id varchar(31),
entity_type varchar(255),
tenant_id varchar(31),
customer_id varchar(31),
name varchar(255),
keys varchar(255),
start_ts bigint,
end_ts bigint,
search_text varchar(255),
additional_info varchar
);

3
dao/src/test/resources/sql/drop-all-tables.sql

@ -18,4 +18,5 @@ DROP TABLE IF EXISTS user_credentials;
DROP TABLE IF EXISTS widget_type;
DROP TABLE IF EXISTS widgets_bundle;
DROP TABLE IF EXISTS rule_node;
DROP TABLE IF EXISTS rule_chain;
DROP TABLE IF EXISTS rule_chain;
DROP TABLE IF EXISTS entity_views;

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

@ -26,6 +26,7 @@ import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
@ -83,6 +84,8 @@ public interface TbContext {
RelationService getRelationService();
EntityViewService getEntityViewService();
ListeningExecutor getJsExecutor();
ListeningExecutor getMailExecutor();

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

@ -0,0 +1,137 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.TbRelationTypes;
import org.thingsboard.rule.engine.api.util.DonAsynchron;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.session.SessionMsgType;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "copy attributes",
configClazz = EmptyNodeConfiguration.class,
nodeDescription = "Copy attributes from asset/device to entity view and changes message originator to related entity view",
nodeDetails = "Copy attributes from asset/device to related entity view according to entity view configuration. \n " +
"Copy will be done only for attributes that are between start and end dates and according to attribute keys configuration. \n" +
"Changes message originator to related entity view and produces new messages according to count of updated entity views",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbNodeEmptyConfig",
icon = "content_copy"
)
public class TbCopyAttributesToEntityViewNode implements TbNode {
EmptyNodeConfiguration config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class);
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
if (!msg.getMetaData().getData().isEmpty()) {
long now = System.currentTimeMillis();
String scope = msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()) ?
DataConstants.CLIENT_SCOPE : msg.getMetaData().getValue("scope");
ListenableFuture<List<EntityView>> entityViewsFuture =
ctx.getEntityViewService().findEntityViewsByTenantIdAndEntityIdAsync(ctx.getTenantId(), msg.getOriginator());
DonAsynchron.withCallback(entityViewsFuture,
entityViews -> {
for (EntityView entityView : entityViews) {
long startTime = entityView.getStartTimeMs();
long endTime = entityView.getEndTimeMs();
if ((endTime != 0 && endTime > now && startTime < now) || (endTime == 0 && startTime < now)) {
Set<AttributeKvEntry> attributes =
JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())).getAttributes();
List<AttributeKvEntry> filteredAttributes =
attributes.stream()
.filter(attr -> {
switch (scope) {
case DataConstants.CLIENT_SCOPE:
if (entityView.getKeys().getAttributes().getCs().isEmpty()) {
return true;
}
return entityView.getKeys().getAttributes().getCs().contains(attr.getKey());
case DataConstants.SERVER_SCOPE:
if (entityView.getKeys().getAttributes().getSs().isEmpty()) {
return true;
}
return entityView.getKeys().getAttributes().getSs().contains(attr.getKey());
case DataConstants.SHARED_SCOPE:
if (entityView.getKeys().getAttributes().getSh().isEmpty()) {
return true;
}
return entityView.getKeys().getAttributes().getSh().contains(attr.getKey());
}
return false;
}).collect(Collectors.toList());
ctx.getTelemetryService().saveAndNotify(entityView.getId(), scope, filteredAttributes,
new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
TbMsg updMsg = ctx.transformMsg(msg, msg.getType(), entityView.getId(), msg.getMetaData(), msg.getData());
ctx.tellNext(updMsg, SUCCESS);
}
@Override
public void onFailure(Throwable t) {
ctx.tellFailure(msg, t);
}
});
}
}
},
t -> ctx.tellFailure(msg, t));
} else {
ctx.tellNext(msg, FAILURE);
}
}
@Override
public void destroy() {
}
}

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

@ -0,0 +1,238 @@
/*
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import thingsboardTypes from '../common/types.constant';
export default angular.module('thingsboard.api.entityView', [thingsboardTypes])
.factory('entityViewService', EntityViewService)
.name;
/*@ngInject*/
function EntityViewService($http, $q, $window, userService, attributeService, customerService, types) {
var service = {
assignEntityViewToCustomer: assignEntityViewToCustomer,
deleteEntityView: deleteEntityView,
getCustomerEntityViews: getCustomerEntityViews,
getEntityView: getEntityView,
getEntityViews: getEntityViews,
getTenantEntityViews: getTenantEntityViews,
saveEntityView: saveEntityView,
unassignEntityViewFromCustomer: unassignEntityViewFromCustomer,
getEntityViewAttributes: getEntityViewAttributes,
subscribeForEntityViewAttributes: subscribeForEntityViewAttributes,
unsubscribeForEntityViewAttributes: unsubscribeForEntityViewAttributes,
findByQuery: findByQuery,
getEntityViewTypes: getEntityViewTypes
}
return service;
function getTenantEntityViews(pageLink, applyCustomersInfo, config, type) {
var deferred = $q.defer();
var url = '/api/tenant/entityViews?limit=' + pageLink.limit;
if (angular.isDefined(pageLink.textSearch)) {
url += '&textSearch=' + pageLink.textSearch;
}
if (angular.isDefined(pageLink.idOffset)) {
url += '&idOffset=' + pageLink.idOffset;
}
if (angular.isDefined(pageLink.textOffset)) {
url += '&textOffset=' + pageLink.textOffset;
}
if (angular.isDefined(type) && type.length) {
url += '&type=' + type;
}
$http.get(url, config).then(function success(response) {
if (applyCustomersInfo) {
customerService.applyAssignedCustomersInfo(response.data.data).then(
function success(data) {
response.data.data = data;
deferred.resolve(response.data);
},
function fail() {
deferred.reject();
}
);
} else {
deferred.resolve(response.data);
}
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function getCustomerEntityViews(customerId, pageLink, applyCustomersInfo, config, type) {
var deferred = $q.defer();
var url = '/api/customer/' + customerId + '/entityViews?limit=' + pageLink.limit;
if (angular.isDefined(pageLink.textSearch)) {
url += '&textSearch=' + pageLink.textSearch;
}
if (angular.isDefined(pageLink.idOffset)) {
url += '&idOffset=' + pageLink.idOffset;
}
if (angular.isDefined(pageLink.textOffset)) {
url += '&textOffset=' + pageLink.textOffset;
}
if (angular.isDefined(type) && type.length) {
url += '&type=' + type;
}
$http.get(url, config).then(function success(response) {
if (applyCustomersInfo) {
customerService.applyAssignedCustomerInfo(response.data.data, customerId).then(
function success(data) {
response.data.data = data;
deferred.resolve(response.data);
},
function fail() {
deferred.reject();
}
);
} else {
deferred.resolve(response.data);
}
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function getEntityView(entityViewId, ignoreErrors, config) {
var deferred = $q.defer();
var url = '/api/entityView/' + entityViewId;
if (!config) {
config = {};
}
config = Object.assign(config, { ignoreErrors: ignoreErrors });
$http.get(url, config).then(function success(response) {
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
function getEntityViews(entityViewIds, config) {
var deferred = $q.defer();
var ids = '';
for (var i=0;i<entityViewIds.length;i++) {
if (i>0) {
ids += ',';
}
ids += entityViewIds[i];
}
var url = '/api/entityViews?entityViewIds=' + ids;
$http.get(url, config).then(function success(response) {
var entityViews = response.data;
entityViews.sort(function (entityView1, entityView2) {
var id1 = entityView1.id.id;
var id2 = entityView2.id.id;
var index1 = entityViewIds.indexOf(id1);
var index2 = entityViewIds.indexOf(id2);
return index1 - index2;
});
deferred.resolve(entityViews);
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
function saveEntityView(entityView) {
var deferred = $q.defer();
var url = '/api/entityView';
$http.post(url, entityView).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function deleteEntityView(entityViewId) {
var deferred = $q.defer();
var url = '/api/entityView/' + entityViewId;
$http.delete(url).then(function success() {
deferred.resolve();
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function assignEntityViewToCustomer(customerId, entityViewId) {
var deferred = $q.defer();
var url = '/api/customer/' + customerId + '/entityView/' + entityViewId;
$http.post(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function unassignEntityViewFromCustomer(entityViewId) {
var deferred = $q.defer();
var url = '/api/customer/entityView/' + entityViewId;
$http.delete(url).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function getEntityViewAttributes(entityViewId, attributeScope, query, successCallback, config) {
return attributeService.getEntityAttributes(types.entityType.entityView, entityViewId, attributeScope, query, successCallback, config);
}
function subscribeForEntityViewAttributes(entityViewId, attributeScope) {
return attributeService.subscribeForEntityAttributes(types.entityType.entityView, entityViewId, attributeScope);
}
function unsubscribeForEntityViewAttributes(subscriptionId) {
attributeService.unsubscribeForEntityAttributes(subscriptionId);
}
function findByQuery(query, ignoreErrors, config) {
var deferred = $q.defer();
var url = '/api/entityViews';
if (!config) {
config = {};
}
config = Object.assign(config, { ignoreErrors: ignoreErrors });
$http.post(url, query, config).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function getEntityViewTypes(config) {
var deferred = $q.defer();
var url = '/api/entityView/types';
$http.get(url, config).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
}

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

@ -20,8 +20,9 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes])
.name;
/*@ngInject*/
function EntityService($http, $q, $filter, $translate, $log, userService, deviceService,
assetService, tenantService, customerService, ruleChainService, dashboardService, entityRelationService, attributeService, types, utils) {
function EntityService($http, $q, $filter, $translate, $log, userService, deviceService, assetService, tenantService,
customerService, ruleChainService, dashboardService, entityRelationService, attributeService,
entityViewService, types, utils) {
var service = {
getEntity: getEntity,
getEntities: getEntities,
@ -54,6 +55,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
case types.entityType.asset:
promise = assetService.getAsset(entityId, true, config);
break;
case types.entityType.entityView:
promise = entityViewService.getEntityView(entityId, true, config);
break;
case types.entityType.tenant:
promise = tenantService.getTenant(entityId, config);
break;
@ -239,6 +243,13 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
promise = assetService.getTenantAssets(pageLink, false, config, subType);
}
break;
case types.entityType.entityView:
if (user.authority === 'CUSTOMER_USER') {
promise = entityViewService.getCustomerEntityViews(customerId, pageLink, false, config, subType);
} else {
promise = entityViewService.getTenantEntityViews(pageLink, false, config, subType);
}
break;
case types.entityType.tenant:
if (user.authority === 'TENANT_ADMIN') {
promise = getSingleTenantByPageLinkPromise(pageLink, config);
@ -725,6 +736,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
case 'TENANT_ADMIN':
entityTypes.device = types.entityType.device;
entityTypes.asset = types.entityType.asset;
entityTypes.entityView = types.entityType.entityView;
entityTypes.tenant = types.entityType.tenant;
entityTypes.customer = types.entityType.customer;
entityTypes.dashboard = types.entityType.dashboard;
@ -735,6 +747,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
case 'CUSTOMER_USER':
entityTypes.device = types.entityType.device;
entityTypes.asset = types.entityType.asset;
entityTypes.entityView = types.entityType.entityView;
entityTypes.customer = types.entityType.customer;
entityTypes.dashboard = types.entityType.dashboard;
if (useAliasEntityTypes) {

2
ui/src/app/app.js

@ -67,6 +67,7 @@ import thingsboardClipboard from './services/clipboard.service';
import thingsboardHome from './layout';
import thingsboardApiLogin from './api/login.service';
import thingsboardApiDevice from './api/device.service';
import thingsboardApiEntityView from './api/entity-view.service';
import thingsboardApiUser from './api/user.service';
import thingsboardApiEntityRelation from './api/entity-relation.service';
import thingsboardApiAsset from './api/asset.service';
@ -133,6 +134,7 @@ angular.module('thingsboard', [
thingsboardHome,
thingsboardApiLogin,
thingsboardApiDevice,
thingsboardApiEntityView,
thingsboardApiUser,
thingsboardApiEntityRelation,
thingsboardApiAsset,

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

@ -327,7 +327,8 @@ export default angular.module('thingsboard.types', [])
dashboard: "DASHBOARD",
alarm: "ALARM",
rulechain: "RULE_CHAIN",
rulenode: "RULE_NODE"
rulenode: "RULE_NODE",
entityView: "ENTITY_VIEW"
},
aliasEntityType: {
current_customer: "CURRENT_CUSTOMER"
@ -345,6 +346,12 @@ export default angular.module('thingsboard.types', [])
list: 'entity.list-of-assets',
nameStartsWith: 'entity.asset-name-starts-with'
},
"ENTITY_VIEW": {
type: 'entity.type-entity-view',
typePlural: 'entity.type-entity-views',
list: 'entity.list-of-entity-views',
nameStartsWith: 'entity.entity-view-name-starts-with'
},
"TENANT": {
type: 'entity.type-tenant',
typePlural: 'entity.type-tenants',

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

@ -0,0 +1,45 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-dialog aria-label="{{ 'entity-view.add' | translate }}" tb-help="'entityViews'" help-container-id="help-container">
<form name="theForm" ng-submit="vm.add()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>entity-view.add</h2>
<span flex></span>
<div id="help-container"></div>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
<md-dialog-content>
<div class="md-dialog-content">
<tb-entity-view entity-view="vm.item" is-edit="true" the-form="theForm"></tb-entity-view>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
{{ 'action.add' | translate }}
</md-button>
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
</md-dialog-actions>
</form>
</md-dialog>

123
ui/src/app/entity-view/add-entity-views-to-customer.controller.js

@ -0,0 +1,123 @@
/*
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*@ngInject*/
export default function AddEntityViewsToCustomerController(entityViewService, $mdDialog, $q, customerId, entityViews) {
var vm = this;
vm.entityViews = entityViews;
vm.searchText = '';
vm.assign = assign;
vm.cancel = cancel;
vm.hasData = hasData;
vm.noData = noData;
vm.searchEntityViewTextUpdated = searchEntityViewTextUpdated;
vm.toggleEntityViewSelection = toggleEntityViewSelection;
vm.theEntityViews = {
getItemAtIndex: function (index) {
if (index > vm.entityViews.data.length) {
vm.theEntityViews.fetchMoreItems_(index);
return null;
}
var item = vm.entityViews.data[index];
if (item) {
item.indexNumber = index + 1;
}
return item;
},
getLength: function () {
if (vm.entityViews.hasNext) {
return vm.entityViews.data.length + vm.entityViews.nextPageLink.limit;
} else {
return vm.entityViews.data.length;
}
},
fetchMoreItems_: function () {
if (vm.entityViews.hasNext && !vm.entityViews.pending) {
vm.entityViews.pending = true;
entityViewService.getTenantEntityViews(vm.entityViews.nextPageLink, false).then(
function success(entityViews) {
vm.entityViews.data = vm.entityViews.data.concat(entityViews.data);
vm.entityViews.nextPageLink = entityViews.nextPageLink;
vm.entityViews.hasNext = entityViews.hasNext;
if (vm.entityViews.hasNext) {
vm.entityViews.nextPageLink.limit = vm.entityViews.pageSize;
}
vm.entityViews.pending = false;
},
function fail() {
vm.entityViews.hasNext = false;
vm.entityViews.pending = false;
});
}
}
};
function cancel () {
$mdDialog.cancel();
}
function assign() {
var tasks = [];
for (var entityViewId in vm.entityViews.selections) {
tasks.push(entityViewService.assignEntityViewToCustomer(customerId, entityViewId));
}
$q.all(tasks).then(function () {
$mdDialog.hide();
});
}
function noData() {
return vm.entityViews.data.length == 0 && !vm.entityViews.hasNext;
}
function hasData() {
return vm.entityViews.data.length > 0;
}
function toggleEntityViewSelection($event, entityView) {
$event.stopPropagation();
var selected = angular.isDefined(entityView.selected) && entityView.selected;
entityView.selected = !selected;
if (entityView.selected) {
vm.entityViews.selections[entityView.id.id] = true;
vm.entityViews.selectedCount++;
} else {
delete vm.entityViews.selections[entityView.id.id];
vm.entityViews.selectedCount--;
}
}
function searchEntityViewTextUpdated() {
vm.entityViews = {
pageSize: vm.entityViews.pageSize,
data: [],
nextPageLink: {
limit: vm.entityViews.pageSize,
textSearch: vm.searchText
},
selections: {},
selectedCount: 0,
hasNext: true,
pending: false
};
}
}

77
ui/src/app/entity-view/add-entity-views-to-customer.tpl.html

@ -0,0 +1,77 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-dialog aria-label="{{ 'entity-view.assign-to-customer' | translate }}">
<form name="theForm" ng-submit="vm.assign()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>entity-view.assign-entity-view-to-customer</h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
<md-dialog-content>
<div class="md-dialog-content">
<fieldset>
<span translate>entity-view.assign-entity-view-to-customer-text</span>
<md-input-container class="md-block" style='margin-bottom: 0px;'>
<label>&nbsp;</label>
<md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">
search
</md-icon>
<input id="entity-view-search" autofocus ng-model="vm.searchText"
ng-change="vm.searchEntityViewTextUpdated()"
placeholder="{{ 'common.enter-search' | translate }}"/>
</md-input-container>
<div style='min-height: 150px;'>
<span translate layout-align="center center"
style="text-transform: uppercase; display: flex; height: 150px;"
class="md-subhead"
ng-show="vm.noData()">entity-view.no-entity-views-text</span>
<md-virtual-repeat-container ng-show="vm.hasData()"
tb-scope-element="repeatContainer" md-top-index="vm.topIndex" flex
style='min-height: 150px; width: 100%;'>
<md-list>
<md-list-item md-virtual-repeat="entityView in vm.theEntityViews" md-on-demand
class="repeated-item" flex>
<md-checkbox ng-click="vm.toggleEntityViewSelection($event, entityView)"
aria-label="{{ 'item.selected' | translate }}"
ng-checked="entityView.selected"></md-checkbox>
<span> {{ entityView.name }} </span>
</md-list-item>
</md-list>
</md-virtual-repeat-container>
</div>
</fieldset>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-disabled="$root.loading || vm.entityViews.selectedCount == 0" type="submit"
class="md-raised md-primary">
{{ 'action.assign' | translate }}
</md-button>
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
translate }}
</md-button>
</md-dialog-actions>
</form>
</md-dialog>

123
ui/src/app/entity-view/assign-to-customer.controller.js

@ -0,0 +1,123 @@
/*
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*@ngInject*/
export default function AssignEntityViewToCustomerController(customerService, entityViewService, $mdDialog, $q, entityViewIds, customers) {
var vm = this;
vm.customers = customers;
vm.searchText = '';
vm.assign = assign;
vm.cancel = cancel;
vm.isCustomerSelected = isCustomerSelected;
vm.hasData = hasData;
vm.noData = noData;
vm.searchCustomerTextUpdated = searchCustomerTextUpdated;
vm.toggleCustomerSelection = toggleCustomerSelection;
vm.theCustomers = {
getItemAtIndex: function (index) {
if (index > vm.customers.data.length) {
vm.theCustomers.fetchMoreItems_(index);
return null;
}
var item = vm.customers.data[index];
if (item) {
item.indexNumber = index + 1;
}
return item;
},
getLength: function () {
if (vm.customers.hasNext) {
return vm.customers.data.length + vm.customers.nextPageLink.limit;
} else {
return vm.customers.data.length;
}
},
fetchMoreItems_: function () {
if (vm.customers.hasNext && !vm.customers.pending) {
vm.customers.pending = true;
customerService.getCustomers(vm.customers.nextPageLink).then(
function success(customers) {
vm.customers.data = vm.customers.data.concat(customers.data);
vm.customers.nextPageLink = customers.nextPageLink;
vm.customers.hasNext = customers.hasNext;
if (vm.customers.hasNext) {
vm.customers.nextPageLink.limit = vm.customers.pageSize;
}
vm.customers.pending = false;
},
function fail() {
vm.customers.hasNext = false;
vm.customers.pending = false;
});
}
}
};
function cancel() {
$mdDialog.cancel();
}
function assign() {
var tasks = [];
for (var i=0; i < entityViewIds.length;i++) {
tasks.push(entityViewService.assignEntityViewToCustomer(vm.customers.selection.id.id, entityViewIds[i]));
}
$q.all(tasks).then(function () {
$mdDialog.hide();
});
}
function noData() {
return vm.customers.data.length == 0 && !vm.customers.hasNext;
}
function hasData() {
return vm.customers.data.length > 0;
}
function toggleCustomerSelection($event, customer) {
$event.stopPropagation();
if (vm.isCustomerSelected(customer)) {
vm.customers.selection = null;
} else {
vm.customers.selection = customer;
}
}
function isCustomerSelected(customer) {
return vm.customers.selection != null && customer &&
customer.id.id === vm.customers.selection.id.id;
}
function searchCustomerTextUpdated() {
vm.customers = {
pageSize: vm.customers.pageSize,
data: [],
nextPageLink: {
limit: vm.customers.pageSize,
textSearch: vm.searchText
},
selection: null,
hasNext: true,
pending: false
};
}
}

76
ui/src/app/entity-view/assign-to-customer.tpl.html

@ -0,0 +1,76 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-dialog aria-label="{{ 'entity-view.assign-entity-view-to-customer' | translate }}">
<form name="theForm" ng-submit="vm.assign()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>entity-view.assign-entity-view-to-customer</h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
<md-dialog-content>
<div class="md-dialog-content">
<fieldset>
<span translate>entity-view.assign-to-customer-text</span>
<md-input-container class="md-block" style='margin-bottom: 0px;'>
<label>&nbsp;</label>
<md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">
search
</md-icon>
<input id="customer-search" autofocus ng-model="vm.searchText"
ng-change="vm.searchCustomerTextUpdated()"
placeholder="{{ 'common.enter-search' | translate }}"/>
</md-input-container>
<div style='min-height: 150px;'>
<span translate layout-align="center center"
style="text-transform: uppercase; display: flex; height: 150px;"
class="md-subhead"
ng-show="vm.noData()">customer.no-customers-text</span>
<md-virtual-repeat-container ng-show="vm.hasData()"
tb-scope-element="repeatContainer" md-top-index="vm.topIndex" flex
style='min-height: 150px; width: 100%;'>
<md-list>
<md-list-item md-virtual-repeat="customer in vm.theCustomers" md-on-demand
class="repeated-item" flex>
<md-checkbox ng-click="vm.toggleCustomerSelection($event, customer)"
aria-label="{{ 'item.selected' | translate }}"
ng-checked="vm.isCustomerSelected(customer)"></md-checkbox>
<span> {{ customer.title }} </span>
</md-list-item>
</md-list>
</md-virtual-repeat-container>
</div>
</fieldset>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-disabled="$root.loading || vm.customers.selection==null" type="submit" class="md-raised md-primary">
{{ 'action.assign' | translate }}
</md-button>
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
translate }}
</md-button>
</md-dialog-actions>
</form>
</md-dialog>

22
ui/src/app/entity-view/entity-view-card.tpl.html

@ -0,0 +1,22 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div flex layout="column" style="margin-top: -10px;">
<div style="text-transform: uppercase; padding-bottom: 5px;">{{vm.item.type}}</div>
<div class="tb-card-description">{{vm.item.additionalInfo.description}}</div>
<div style="padding-top: 5px;" class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'entity-view.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
</div>

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

@ -0,0 +1,119 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-button ng-click="onMakePublic({event: $event})"
ng-show="!isEdit && entityViewScope === 'tenant' && !isAssignedToCustomer && !isPublic"
class="md-raised md-primary">{{ 'entity-view.make-public' | translate }}</md-button>
<md-button ng-click="onAssignToCustomer({event: $event})"
ng-show="!isEdit && entityViewScope === 'tenant' && !isAssignedToCustomer"
class="md-raised md-primary">{{ 'entity-view.assign-to-customer' | translate }}</md-button>
<md-button ng-click="onUnassignFromCustomer({event: $event})"
ng-show="!isEdit && (entityViewScope === 'customer' || entityViewScope === 'tenant') && isAssignedToCustomer"
class="md-raised md-primary">{{'entity-view.unassign-from-customer' | translate }}</md-button>
<md-button ng-click="onDeleteEntityView({event: $event})"
ng-show="!isEdit && entityViewScope === 'tenant'"
class="md-raised md-primary">{{ 'entity-view.delete' | translate }}</md-button>
<div layout="row">
<md-button ngclipboard data-clipboard-action="copy"
ngclipboard-success="onEntityViewIdCopied(e)"
data-clipboard-text="{{entityView.id.id}}" ng-show="!isEdit"
class="md-raised">
<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
<span translate>entity-view.copyId</span>
</md-button>
</div>
<md-content class="md-padding" layout="column">
<md-input-container class="md-block"
ng-show="!isEdit && isAssignedToCustomer && entityViewScope === 'tenant'">
<label translate>entity-view.assignedToCustomer</label>
<input ng-model="assignedCustomer.title" disabled>
</md-input-container>
<fieldset ng-disabled="$root.loading || !isEdit">
<md-input-container class="md-block">
<label translate>entity-view.name</label>
<input required name="name" ng-model="entityView.name">
<div ng-messages="theForm.name.$error">
<div translate ng-message="required">entity-view.name-required</div>
</div>
</md-input-container>
<tb-entity-select flex ng-disabled="!isEdit"
the-form="theForm"
tb-required="true"
allowed-entity-types="allowedEntityTypes"
ng-model="entityView.entityId">
</tb-entity-select>
<md-input-container class="md-block">
<label translate>entity-view.description</label>
<textarea ng-model="entityView.additionalInfo.description" rows="2"></textarea>
</md-input-container>
<section layout="column">
<label translate class="tb-title no-padding">entity-view.client-attributes</label>
<md-chips style="padding-bottom: 15px;"
ng-required="false"
readonly="!isEdit"
ng-model="entityView.keys.attributes.cs"
placeholder="{{'entity-view.client-attributes' | translate}}"
md-separator-keys="separatorKeys">
</md-chips>
<label translate class="tb-title no-padding">entity-view.shared-attributes</label>
<md-chips style="padding-bottom: 15px;"
ng-required="false"
readonly="!isEdit"
ng-model="entityView.keys.attributes.sh"
placeholder="{{'entity-view.shared-attributes' | translate}}"
md-separator-keys="separatorKeys">
</md-chips>
<label translate class="tb-title no-padding">entity-view.server-attributes</label>
<md-chips style="padding-bottom: 15px;"
ng-required="false"
readonly="!isEdit"
ng-model="entityView.keys.attributes.ss"
placeholder="{{'entity-view.server-attributes' | translate}}"
md-separator-keys="separatorKeys">
</md-chips>
<label translate class="tb-title no-padding">entity-view.latest-timeseries</label>
<md-chips ng-required="false"
readonly="!isEdit"
ng-model="entityView.keys.timeseries"
placeholder="{{'entity-view.latest-timeseries' | translate}}"
md-separator-keys="separatorKeys">
</md-chips>
</section>
<section layout="column">
<section layout="row" layout-align="start start">
<mdp-date-picker ng-model="startTimeMs"
mdp-max-date="maxStartTimeMs"
mdp-placeholder="{{ 'entity-view.start-ts' | translate }}"></mdp-date-picker>
<mdp-time-picker ng-model="startTimeMs"
mdp-max-date="maxStartTimeMs"
mdp-placeholder="{{ 'entity-view.start-ts' | translate }}"
mdp-auto-switch="true"></mdp-time-picker>
</section>
<section layout="row" layout-align="start start">
<mdp-date-picker ng-model="endTimeMs"
mdp-min-date="minEndTimeMs"
mdp-placeholder="{{ 'entity-view.end-ts' | translate }}"></mdp-date-picker>
<mdp-time-picker ng-model="endTimeMs"
mdp-min-date="minEndTimeMs"
mdp-placeholder="{{ 'entity-view.end-ts' | translate }}"
mdp-auto-switch="true"></mdp-time-picker>
</section>
</section>
</fieldset>
</md-content>

483
ui/src/app/entity-view/entity-view.controller.js

@ -0,0 +1,483 @@
/*
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable import/no-unresolved, import/default */
import addEntityViewTemplate from './add-entity-view.tpl.html';
import entityViewCard from './entity-view-card.tpl.html';
import assignToCustomerTemplate from './assign-to-customer.tpl.html';
import addEntityViewsToCustomerTemplate from './add-entity-views-to-customer.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export function EntityViewCardController(types) {
var vm = this;
vm.types = types;
vm.isAssignedToCustomer = function() {
if (vm.item && vm.item.customerId && vm.parentCtl.entityViewsScope === 'tenant' &&
vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) {
return true;
}
return false;
}
vm.isPublic = function() {
if (vm.item && vm.item.assignedCustomer && vm.parentCtl.entityViewsScope === 'tenant' && vm.item.assignedCustomer.isPublic) {
return true;
}
return false;
}
}
/*@ngInject*/
export function EntityViewController($rootScope, userService, entityViewService, customerService, $state, $stateParams,
$document, $mdDialog, $q, $translate, types) {
var customerId = $stateParams.customerId;
var entityViewActionsList = [];
var entityViewGroupActionsList = [];
var vm = this;
vm.types = types;
vm.entityViewGridConfig = {
deleteItemTitleFunc: deleteEntityViewTitle,
deleteItemContentFunc: deleteEntityViewText,
deleteItemsTitleFunc: deleteEntityViewsTitle,
deleteItemsActionTitleFunc: deleteEntityViewsActionTitle,
deleteItemsContentFunc: deleteEntityViewsText,
saveItemFunc: saveEntityView,
getItemTitleFunc: getEntityViewTitle,
itemCardController: 'EntityViewCardController',
itemCardTemplateUrl: entityViewCard,
parentCtl: vm,
actionsList: entityViewActionsList,
groupActionsList: entityViewGroupActionsList,
onGridInited: gridInited,
addItemTemplateUrl: addEntityViewTemplate,
addItemText: function() { return $translate.instant('entity-view.add-entity-view-text') },
noItemsText: function() { return $translate.instant('entity-view.no-entity-views-text') },
itemDetailsText: function() { return $translate.instant('entity-view.entity-view-details') },
isDetailsReadOnly: isCustomerUser,
isSelectionEnabled: function () {
return !isCustomerUser();
}
};
if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
vm.entityViewGridConfig.items = $stateParams.items;
}
if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
vm.entityViewGridConfig.topIndex = $stateParams.topIndex;
}
vm.entityViewsScope = $state.$current.data.entityViewsType;
vm.assignToCustomer = assignToCustomer;
vm.makePublic = makePublic;
vm.unassignFromCustomer = unassignFromCustomer;
initController();
function initController() {
var fetchEntityViewsFunction = null;
var deleteEntityViewFunction = null;
var refreshEntityViewsParamsFunction = null;
var user = userService.getCurrentUser();
if (user.authority === 'CUSTOMER_USER') {
vm.entityViewsScope = 'customer_user';
customerId = user.customerId;
}
if (customerId) {
vm.customerEntityViewsTitle = $translate.instant('customer.entity-views');
customerService.getShortCustomerInfo(customerId).then(
function success(info) {
if (info.isPublic) {
vm.customerEntityViewsTitle = $translate.instant('customer.public-entity-views');
}
}
);
}
if (vm.entityViewsScope === 'tenant') {
fetchEntityViewsFunction = function (pageLink, entityViewType) {
return entityViewService.getTenantEntityViews(pageLink, true, null, entityViewType);
};
deleteEntityViewFunction = function (entityViewId) {
return entityViewService.deleteEntityView(entityViewId);
};
refreshEntityViewsParamsFunction = function() {
return {"topIndex": vm.topIndex};
};
entityViewActionsList.push(
{
onAction: function ($event, item) {
assignToCustomer($event, [ item.id.id ]);
},
name: function() { return $translate.instant('action.assign') },
details: function() { return $translate.instant('entity-view.assign-to-customer') },
icon: "assignment_ind",
isEnabled: function(entityView) {
return entityView && (!entityView.customerId || entityView.customerId.id === types.id.nullUid);
}
}
);
entityViewActionsList.push(
{
onAction: function ($event, item) {
unassignFromCustomer($event, item, false);
},
name: function() { return $translate.instant('action.unassign') },
details: function() { return $translate.instant('entity-view.unassign-from-customer') },
icon: "assignment_return",
isEnabled: function(entityView) {
return entityView && entityView.customerId && entityView.customerId.id !== types.id.nullUid && !entityView.assignedCustomer.isPublic;
}
}
);
entityViewActionsList.push({
onAction: function ($event, item) {
unassignFromCustomer($event, item, true);
},
name: function() { return $translate.instant('action.make-private') },
details: function() { return $translate.instant('entity-view.make-private') },
icon: "reply",
isEnabled: function(entityView) {
return entityView && entityView.customerId && entityView.customerId.id !== types.id.nullUid && entityView.assignedCustomer.isPublic;
}
});
entityViewActionsList.push(
{
onAction: function ($event, item) {
vm.grid.deleteItem($event, item);
},
name: function() { return $translate.instant('action.delete') },
details: function() { return $translate.instant('entity-view.delete') },
icon: "delete"
}
);
entityViewGroupActionsList.push(
{
onAction: function ($event, items) {
assignEntityViewsToCustomer($event, items);
},
name: function() { return $translate.instant('entity-view.assign-entity-views') },
details: function(selectedCount) {
return $translate.instant('entity-view.assign-entity-views-text', {count: selectedCount}, "messageformat");
},
icon: "assignment_ind"
}
);
entityViewGroupActionsList.push(
{
onAction: function ($event) {
vm.grid.deleteItems($event);
},
name: function() { return $translate.instant('entity-view.delete-entity-views') },
details: deleteEntityViewsActionTitle,
icon: "delete"
}
);
} else if (vm.entityViewsScope === 'customer' || vm.entityViewsScope === 'customer_user') {
fetchEntityViewsFunction = function (pageLink, entityViewType) {
return entityViewService.getCustomerEntityViews(customerId, pageLink, true, null, entityViewType);
};
deleteEntityViewFunction = function (entityViewId) {
return entityViewService.unassignEntityViewFromCustomer(entityViewId);
};
refreshEntityViewsParamsFunction = function () {
return {"customerId": customerId, "topIndex": vm.topIndex};
};
if (vm.entityViewsScope === 'customer') {
entityViewActionsList.push(
{
onAction: function ($event, item) {
unassignFromCustomer($event, item, false);
},
name: function() { return $translate.instant('action.unassign') },
details: function() { return $translate.instant('entity-view.unassign-from-customer') },
icon: "assignment_return",
isEnabled: function(entityView) {
return entityView && !entityView.assignedCustomer.isPublic;
}
}
);
entityViewGroupActionsList.push(
{
onAction: function ($event, items) {
unassignEntityViewsFromCustomer($event, items);
},
name: function() { return $translate.instant('entity-view.unassign-entity-views') },
details: function(selectedCount) {
return $translate.instant('entity-view.unassign-entity-views-action-title', {count: selectedCount}, "messageformat");
},
icon: "assignment_return"
}
);
vm.entityViewGridConfig.addItemAction = {
onAction: function ($event) {
addEntityViewsToCustomer($event);
},
name: function() { return $translate.instant('entity-view.assign-entity-views') },
details: function() { return $translate.instant('entity-view.assign-new-entity-view') },
icon: "add"
};
} else if (vm.entityViewsScope === 'customer_user') {
vm.entityViewGridConfig.addItemAction = {};
}
}
vm.entityViewGridConfig.refreshParamsFunc = refreshEntityViewsParamsFunction;
vm.entityViewGridConfig.fetchItemsFunc = fetchEntityViewsFunction;
vm.entityViewGridConfig.deleteItemFunc = deleteEntityViewFunction;
}
function deleteEntityViewTitle(entityView) {
return $translate.instant('entity-view.delete-entity-view-title', {entityViewName: entityView.name});
}
function deleteEntityViewText() {
return $translate.instant('entity-view.delete-entity-view-text');
}
function deleteEntityViewsTitle(selectedCount) {
return $translate.instant('entity-view.delete-entity-views-title', {count: selectedCount}, 'messageformat');
}
function deleteEntityViewsActionTitle(selectedCount) {
return $translate.instant('entity-view.delete-entity-views-action-title', {count: selectedCount}, 'messageformat');
}
function deleteEntityViewsText () {
return $translate.instant('entity-view.delete-entity-views-text');
}
function gridInited(grid) {
vm.grid = grid;
}
function getEntityViewTitle(entityView) {
return entityView ? entityView.name : '';
}
function saveEntityView(entityView) {
var deferred = $q.defer();
entityViewService.saveEntityView(entityView).then(
function success(savedEntityView) {
$rootScope.$broadcast('entityViewSaved');
var entityViews = [ savedEntityView ];
customerService.applyAssignedCustomersInfo(entityViews).then(
function success(items) {
if (items && items.length == 1) {
deferred.resolve(items[0]);
} else {
deferred.reject();
}
},
function fail() {
deferred.reject();
}
);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function isCustomerUser() {
return vm.entityViewsScope === 'customer_user';
}
function assignToCustomer($event, entityViewIds) {
if ($event) {
$event.stopPropagation();
}
var pageSize = 10;
customerService.getCustomers({limit: pageSize, textSearch: ''}).then(
function success(_customers) {
var customers = {
pageSize: pageSize,
data: _customers.data,
nextPageLink: _customers.nextPageLink,
selection: null,
hasNext: _customers.hasNext,
pending: false
};
if (customers.hasNext) {
customers.nextPageLink.limit = pageSize;
}
$mdDialog.show({
controller: 'AssignEntityViewToCustomerController',
controllerAs: 'vm',
templateUrl: assignToCustomerTemplate,
locals: {entityViewIds: entityViewIds, customers: customers},
parent: angular.element($document[0].body),
fullscreen: true,
targetEvent: $event
}).then(function () {
vm.grid.refreshList();
}, function () {
});
},
function fail() {
});
}
function addEntityViewsToCustomer($event) {
if ($event) {
$event.stopPropagation();
}
var pageSize = 10;
entityViewService.getTenantEntityViews({limit: pageSize, textSearch: ''}, false).then(
function success(_entityViews) {
var entityViews = {
pageSize: pageSize,
data: _entityViews.data,
nextPageLink: _entityViews.nextPageLink,
selections: {},
selectedCount: 0,
hasNext: _entityViews.hasNext,
pending: false
};
if (entityViews.hasNext) {
entityViews.nextPageLink.limit = pageSize;
}
$mdDialog.show({
controller: 'AddEntityViewsToCustomerController',
controllerAs: 'vm',
templateUrl: addEntityViewsToCustomerTemplate,
locals: {customerId: customerId, entityViews: entityViews},
parent: angular.element($document[0].body),
fullscreen: true,
targetEvent: $event
}).then(function () {
vm.grid.refreshList();
}, function () {
});
},
function fail() {
});
}
function assignEntityViewsToCustomer($event, items) {
var entityViewIds = [];
for (var id in items.selections) {
entityViewIds.push(id);
}
assignToCustomer($event, entityViewIds);
}
function unassignFromCustomer($event, entityView, isPublic) {
if ($event) {
$event.stopPropagation();
}
var title;
var content;
var label;
if (isPublic) {
title = $translate.instant('entity-view.make-private-entity-view-title', {entityViewName: entityView.name});
content = $translate.instant('entity-view.make-private-entity-view-text');
label = $translate.instant('entity-view.make-private');
} else {
title = $translate.instant('entity-view.unassign-entity-view-title', {entityViewName: entityView.name});
content = $translate.instant('entity-view.unassign-entity-view-text');
label = $translate.instant('entity-view.unassign-entity-view');
}
var confirm = $mdDialog.confirm()
.targetEvent($event)
.title(title)
.htmlContent(content)
.ariaLabel(label)
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () {
entityViewService.unassignEntityViewFromCustomer(entityView.id.id).then(function success() {
vm.grid.refreshList();
});
});
}
function unassignEntityViewsFromCustomer($event, items) {
var confirm = $mdDialog.confirm()
.targetEvent($event)
.title($translate.instant('entity-view.unassign-entity-views-title', {count: items.selectedCount}, 'messageformat'))
.htmlContent($translate.instant('entity-view.unassign-entity-views-text'))
.ariaLabel($translate.instant('entity-view.unassign-entity-view'))
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () {
var tasks = [];
for (var id in items.selections) {
tasks.push(entityViewService.unassignEntityViewFromCustomer(id));
}
$q.all(tasks).then(function () {
vm.grid.refreshList();
});
});
}
function makePublic($event, entityView) {
if ($event) {
$event.stopPropagation();
}
var confirm = $mdDialog.confirm()
.targetEvent($event)
.title($translate.instant('entity-view.make-public-entity-view-title', {entityViewName: entityView.name}))
.htmlContent($translate.instant('entity-view.make-public-entity-view-text'))
.ariaLabel($translate.instant('entity-view.make-public'))
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () {
entityViewService.makeEntityViewPublic(entityView.id.id).then(function success() {
vm.grid.refreshList();
});
});
}
}

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

@ -0,0 +1,121 @@
/*
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable import/no-unresolved, import/default */
import entityViewFieldsetTemplate from './entity-view-fieldset.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function EntityViewDirective($compile, $templateCache, $filter, toast, $translate, $mdConstant,
types, clipboardService, entityViewService, customerService) {
var linker = function (scope, element) {
var template = $templateCache.get(entityViewFieldsetTemplate);
element.html(template);
scope.types = types;
scope.isAssignedToCustomer = false;
scope.isPublic = false;
scope.assignedCustomer = null;
scope.allowedEntityTypes = [types.entityType.device, types.entityType.asset];
var semicolon = 186;
scope.separatorKeys = [$mdConstant.KEY_CODE.ENTER, $mdConstant.KEY_CODE.COMMA, semicolon];
scope.$watch('entityView', function(newVal) {
if (newVal) {
if (scope.entityView.customerId && scope.entityView.customerId.id !== types.id.nullUid) {
scope.isAssignedToCustomer = true;
customerService.getShortCustomerInfo(scope.entityView.customerId.id).then(
function success(customer) {
scope.assignedCustomer = customer;
scope.isPublic = customer.isPublic;
}
);
} else {
scope.isAssignedToCustomer = false;
scope.isPublic = false;
scope.assignedCustomer = null;
}
if (scope.entityView.startTimeMs > 0) {
scope.startTimeMs = new Date(scope.entityView.startTimeMs);
}
if (scope.entityView.endTimeMs > 0) {
scope.endTimeMs = new Date(scope.entityView.endTimeMs);
}
if (!scope.entityView.keys) {
scope.entityView.keys = {};
scope.entityView.keys.timeseries = [];
scope.entityView.keys.attributes = {};
scope.entityView.keys.attributes.ss = [];
scope.entityView.keys.attributes.cs = [];
scope.entityView.keys.attributes.sh = [];
}
}
});
scope.$watch('startTimeMs', function (newDate) {
if (newDate) {
if (newDate.getTime() > scope.maxStartTimeMs) {
scope.startTimeMs = angular.copy(scope.maxStartTimeMs);
}
updateMinMaxDates();
}
});
scope.$watch('endTimeMs', function (newDate) {
if (newDate) {
if (newDate.getTime() < scope.minEndTimeMs) {
scope.endTimeMs = angular.copy(scope.minEndTimeMs);
}
updateMinMaxDates();
}
});
function updateMinMaxDates() {
if (scope.endTimeMs) {
scope.maxStartTimeMs = angular.copy(new Date(scope.endTimeMs.getTime()));
scope.entityView.endTimeMs = scope.endTimeMs.getTime();
}
if (scope.startTimeMs) {
scope.minEndTimeMs = angular.copy(new Date(scope.startTimeMs.getTime()));
scope.entityView.startTimeMs = scope.startTimeMs.getTime();
}
}
scope.onEntityViewIdCopied = function() {
toast.showSuccess($translate.instant('entity-view.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
};
$compile(element.contents())(scope);
}
return {
restrict: "E",
link: linker,
scope: {
entityView: '=',
isEdit: '=',
entityViewScope: '=',
theForm: '=',
onAssignToCustomer: '&',
onMakePublic: '&',
onUnassignFromCustomer: '&',
onDeleteEntityView: '&'
}
};
}

72
ui/src/app/entity-view/entity-view.routes.js

@ -0,0 +1,72 @@
/*
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable import/no-unresolved, import/default */
import entityViewsTemplate from './entity-views.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function EntityViewRoutes($stateProvider, types) {
$stateProvider
.state('home.entityViews', {
url: '/entityViews',
params: {'topIndex': 0},
module: 'private',
auth: ['TENANT_ADMIN', 'CUSTOMER_USER'],
views: {
"content@home": {
templateUrl: entityViewsTemplate,
controller: 'EntityViewController',
controllerAs: 'vm'
}
},
data: {
entityViewsType: 'tenant',
searchEnabled: true,
searchByEntitySubtype: true,
searchEntityType: types.entityType.entityView,
pageTitle: 'entity-view.entity-views'
},
ncyBreadcrumb: {
label: '{"icon": "view_stream", "label": "entity-view.entity-views"}'
}
})
.state('home.customers.entityViews', {
url: '/:customerId/entityViews',
params: {'topIndex': 0},
module: 'private',
auth: ['TENANT_ADMIN'],
views: {
"content@home": {
templateUrl: entityViewsTemplate,
controllerAs: 'vm',
controller: 'EntityViewController'
}
},
data: {
entityViewsType: 'customer',
searchEnabled: true,
searchByEntitySubtype: true,
searchEntityType: types.entityType.entityView,
pageTitle: 'customer.entity-views'
},
ncyBreadcrumb: {
label: '{"icon": "view_stream", "label": "{{ vm.customerEntityViewsTitle }}", "translate": "false"}'
}
});
}

75
ui/src/app/entity-view/entity-views.tpl.html

@ -0,0 +1,75 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<tb-grid grid-configuration="vm.entityViewGridConfig">
<details-buttons tb-help="'entityViews'" help-container-id="help-container">
<div id="help-container"></div>
</details-buttons>
<md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
id="tabs" md-border-bottom flex class="tb-absolute-fill">
<md-tab label="{{ 'entity-view.details' | translate }}">
<tb-entity-view entity-view="vm.grid.operatingItem()"
is-edit="vm.grid.detailsConfig.isDetailsEditMode"
entity-view-scope="vm.entityViewsScope"
the-form="vm.grid.detailsForm"
on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
on-delete-entity-view="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-entity-view>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.entityView}}"
entity-name="vm.grid.operatingItem().name"
default-attribute-scope="{{vm.types.attributesScope.client.value}}">
</tb-attribute-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
<tb-attribute-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.entityView}}"
entity-name="vm.grid.operatingItem().name"
default-attribute-scope="{{vm.types.latestTelemetry.value}}"
disable-attribute-scope-selection="true">
</tb-attribute-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
<tb-alarm-table flex entity-type="vm.types.entityType.entityView"
entity-id="vm.grid.operatingItem().id.id">
</tb-alarm-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'entity-view.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.entityView"
entity-id="vm.grid.operatingItem().id.id"
tenant-id="vm.grid.operatingItem().tenantId.id"
default-event-type="{{vm.types.eventType.error.value}}">
</tb-event-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
<tb-relation-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.entityView}}">
</tb-relation-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table flex entity-type="vm.types.entityType.entityView"
entity-id="vm.grid.operatingItem().id.id"
audit-log-mode="{{vm.types.auditLogMode.entity}}">
</tb-audit-log-table>
</md-tab>
</tb-grid>

41
ui/src/app/entity-view/index.js

@ -0,0 +1,41 @@
/*
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import uiRouter from 'angular-ui-router';
import thingsboardGrid from '../components/grid.directive';
import thingsboardApiUser from '../api/user.service';
import thingsboardApiEntityView from '../api/entity-view.service';
import thingsboardApiCustomer from '../api/customer.service';
import EntityViewRoutes from './entity-view.routes';
import {EntityViewController, EntityViewCardController} from './entity-view.controller';
import AssignEntityViewToCustomerController from './assign-to-customer.controller';
import AddEntityViewsToCustomerController from './add-entity-views-to-customer.controller';
import EntityViewDirective from './entity-view.directive';
export default angular.module('thingsboard.entityView', [
uiRouter,
thingsboardGrid,
thingsboardApiUser,
thingsboardApiEntityView,
thingsboardApiCustomer
])
.config(EntityViewRoutes)
.controller('EntityViewController', EntityViewController)
.controller('EntityViewCardController', EntityViewCardController)
.controller('AssignEntityViewToCustomerController', AssignEntityViewToCustomerController)
.controller('AddEntityViewsToCustomerController', AddEntityViewsToCustomerController)
.directive('tbEntityView', EntityViewDirective)
.name;

2
ui/src/app/entity/attribute/attribute-table.directive.js

@ -54,7 +54,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
scope.entityType = attrs.entityType;
if (scope.entityType === types.entityType.device) {
if (scope.entityType === types.entityType.device || scope.entityType === types.entityType.entityView) {
scope.attributeScopes = types.attributesScope;
scope.attributeScopeSelectionReadonly = false;
} else {

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

@ -131,6 +131,12 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter
scope.noEntitiesMatchingText = 'device.no-devices-matching';
scope.entityRequiredText = 'device.device-required';
break;
case types.entityType.entityView:
scope.selectEntityText = 'entity-view.select-entity-view';
scope.entityText = 'entity-view.entity-view';
scope.noEntitiesMatchingText = 'entity-view.no-entity-views-matching';
scope.entityRequiredText = 'entity-view.entity-view-required';
break;
case types.entityType.rulechain:
scope.selectEntityText = 'rulechain.select-rulechain';
scope.entityText = 'rulechain.rulechain';

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

@ -48,6 +48,7 @@ import thingsboardAdmin from '../admin';
import thingsboardProfile from '../profile';
import thingsboardAsset from '../asset';
import thingsboardDevice from '../device';
import thingsboardEntityView from '../entity-view';
import thingsboardWidgetLibrary from '../widget';
import thingsboardDashboard from '../dashboard';
import thingsboardRuleChain from '../rulechain';
@ -79,6 +80,7 @@ export default angular.module('thingsboard.home', [
thingsboardProfile,
thingsboardAsset,
thingsboardDevice,
thingsboardEntityView,
thingsboardWidgetLibrary,
thingsboardDashboard,
thingsboardRuleChain,

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

@ -343,10 +343,12 @@
"dashboard": "Customer Dashboard",
"dashboards": "Customer Dashboards",
"devices": "Customer Devices",
"entity-views": "Customer Entity Views",
"assets": "Customer Assets",
"public-dashboards": "Public Dashboards",
"public-devices": "Public Devices",
"public-assets": "Public Assets",
"public-entity-views": "Public Entity Views",
"add": "Add Customer",
"delete": "Delete customer",
"manage-customer-users": "Manage customer users",
@ -711,6 +713,10 @@
"type-assets": "Assets",
"list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }",
"asset-name-starts-with": "Assets whose names start with '{{prefix}}'",
"type-entity-view": "Entity View",
"type-entity-views": "Entity Views",
"list-of-entity-views": "{ count, plural, 1 {One entity view} other {List of # entity views} }",
"entity-view-name-starts-with": "Entity Views whose names start with '{{prefix}}'",
"type-rule": "Rule",
"type-rules": "Rules",
"list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }",
@ -756,6 +762,85 @@
"no-data": "No data to display",
"columns-to-display": "Columns to Display"
},
"entity-view": {
"entity-view": "Entity View",
"entity-views": "Entity Views",
"management": "Entity View management",
"view-entity-views": "View Entity Views",
"entity-view-alias": "Entity View alias",
"aliases": "Entity View aliases",
"no-alias-matching": "'{{alias}}' not found.",
"no-aliases-found": "No aliases found.",
"no-key-matching": "'{{key}}' not found.",
"no-keys-found": "No keys found.",
"create-new-alias": "Create a new one!",
"create-new-key": "Create a new one!",
"duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Entity View aliases must be unique whithin the dashboard.",
"configure-alias": "Configure '{{alias}}' alias",
"no-entity-views-matching": "No entity views matching '{{entity}}' were found.",
"alias": "Alias",
"alias-required": "Entity View alias is required.",
"remove-alias": "Remove entity view alias",
"add-alias": "Add entity view alias",
"name-starts-with": "Entity View name starts with",
"entity-view-list": "Entity View list",
"use-entity-view-name-filter": "Use filter",
"entity-view-list-empty": "No entity views selected.",
"entity-view-name-filter-required": "Entity view name filter is required.",
"entity-view-name-filter-no-entity-view-matched": "No entity views starting with '{{entityView}}' were found.",
"add": "Add Entity View",
"assign-to-customer": "Assign to customer",
"assign-entity-view-to-customer": "Assign Entity View(s) To Customer",
"assign-entity-view-to-customer-text": "Please select the entity views to assign to the customer",
"no-entity-views-text": "No entity views found",
"assign-to-customer-text": "Please select the customer to assign the entity view(s)",
"entity-view-details": "Entity view details",
"add-entity-view-text": "Add new entity view",
"delete": "Delete entity view",
"assign-entity-views": "Assign entity views",
"assign-entity-views-text": "Assign { count, plural, 1 {1 entityView} other {# entityViews} } to customer",
"delete-entity-views": "Delete entity views",
"unassign-from-customer": "Unassign from customer",
"unassign-entity-views": "Unassign entity views",
"unassign-entity-views-action-title": "Unassign { count, plural, 1 {1 entityView} other {# entityViews} } from customer",
"assign-new-entity-view": "Assign new entity view",
"delete-entity-view-title": "Are you sure you want to delete the entity view '{{entityViewName}}'?",
"delete-entity-view-text": "Be careful, after the confirmation the entity view and all related data will become unrecoverable.",
"delete-entity-views-title": "Are you sure you want to entity view { count, plural, 1 {1 entityView} other {# entityViews} }?",
"delete-entity-views-action-title": "Delete { count, plural, 1 {1 entityView} other {# entityViews} }",
"delete-entity-views-text": "Be careful, after the confirmation all selected entity views will be removed and all related data will become unrecoverable.",
"unassign-entity-view-title": "Are you sure you want to unassign the entity view '{{entityViewName}}'?",
"unassign-entity-view-text": "After the confirmation the entity view will be unassigned and won't be accessible by the customer.",
"unassign-entity-view": "Unassign entity view",
"unassign-entity-views-title": "Are you sure you want to unassign { count, plural, 1 {1 entityView} other {# entityViews} }?",
"unassign-entity-views-text": "After the confirmation all selected entity views will be unassigned and won't be accessible by the customer.",
"entity-view-type": "Entity View type",
"entity-view-type-required": "Entity View type is required.",
"select-entity-view-type": "Select entity view type",
"enter-entity-view-type": "Enter entity view type",
"any-entity-view": "Any entity view",
"no-entity-view-types-matching": "No entity view types matching '{{entitySubtype}}' were found.",
"entity-view-type-list-empty": "No entity view types selected.",
"entity-view-types": "Entity View types",
"name": "Name",
"name-required": "Name is required.",
"description": "Description",
"events": "Events",
"details": "Details",
"copyId": "Copy entity view Id",
"assignedToCustomer": "Assigned to customer",
"unable-entity-view-device-alias-title": "Unable to delete entity view alias",
"unable-entity-view-device-alias-text": "Device alias '{{entityViewAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
"select-entity-view": "Select entity view",
"make-public": "Make entity view public",
"start-ts": "Start time",
"end-ts": "End time",
"date-limits": "Date limits",
"client-attributes": "Client attributes",
"shared-attributes": "Shared attributes",
"server-attributes": "Server attributes",
"latest-timeseries": "Latest timeseries"
},
"event": {
"event-type": "Event type",
"type-error": "Error",

52
ui/src/app/services/menu.service.js

@ -167,6 +167,12 @@ function Menu(userService, $state, $rootScope) {
state: 'home.devices',
icon: 'devices_other'
},
{
name: 'entity-view.entity-views',
type: 'link',
state: 'home.entityViews',
icon: 'view_stream'
},
{
name: 'widget.widget-library',
type: 'link',
@ -227,6 +233,16 @@ function Menu(userService, $state, $rootScope) {
}
]
},
{
name: 'entity-view.management',
places: [
{
name: 'entity-view.entity-views',
icon: 'view_stream',
state: 'home.entityViews'
}
]
},
{
name: 'dashboard.management',
places: [
@ -273,6 +289,12 @@ function Menu(userService, $state, $rootScope) {
state: 'home.devices',
icon: 'devices_other'
},
{
name: 'entity-view.entity-views',
type: 'link',
state: 'home.entityViews',
icon: 'view_stream'
},
{
name: 'dashboard.dashboards',
type: 'link',
@ -301,16 +323,26 @@ function Menu(userService, $state, $rootScope) {
}
]
},
{
name: 'dashboard.view-dashboards',
places: [
{
name: 'dashboard.dashboards',
icon: 'dashboard',
state: 'home.dashboards'
}
]
}];
{
name: 'entity-view.management',
places: [
{
name: 'entity-view.entity-views',
icon: 'view_stream',
state: 'home.entityViews'
}
]
},
{
name: 'dashboard.view-dashboards',
places: [
{
name: 'dashboard.dashboards',
icon: 'dashboard',
state: 'home.dashboards'
}
]
}];
}
}
}

Loading…
Cancel
Save