thingsboard-aplcache
Changes
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java 9(+9 -0)
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java 69(+63 -6)
application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java 456(+456 -0)
application/src/test/java/org/thingsboard/server/controller/nosql/EntityViewControllerNoSqlTest.java 26(+26 -0)
application/src/test/java/org/thingsboard/server/controller/sql/EntityViewControllerSqlTest.java 31(+31 -0)
common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java 42(+42 -0)
common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java 47(+47 -0)
common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java 43(+43 -0)
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java 137(+137 -0)
ui/src/app/api/entity.service.js 17(+15 -2)
ui/src/app/api/entity-view.service.js 238(+238 -0)
ui/src/app/app.js 2(+2 -0)
ui/src/app/common/types.constant.js 9(+8 -1)
ui/src/app/entity-view/entity-view.controller.js 483(+483 -0)
ui/src/app/entity-view/entity-view.directive.js 121(+121 -0)
ui/src/app/entity-view/entity-view.routes.js 72(+72 -0)
ui/src/app/entity-view/entity-view-fieldset.tpl.html 119(+119 -0)
ui/src/app/entity-view/entity-views.tpl.html 75(+75 -0)
ui/src/app/entity-view/index.js 41(+41 -0)
ui/src/app/layout/index.js 2(+2 -0)
ui/src/app/locale/locale.constant-en_US.json 85(+85 -0)
ui/src/app/services/menu.service.js 52(+42 -10)
Details
diff --git a/application/src/main/data/upgrade/2.1.1/schema_update.cql b/application/src/main/data/upgrade/2.1.1/schema_update.cql
new file mode 100644
index 0000000..a633634
--- /dev/null
+++ b/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);
diff --git a/application/src/main/data/upgrade/2.1.1/schema_update.sql b/application/src/main/data/upgrade/2.1.1/schema_update.sql
new file mode 100644
index 0000000..bd2c341
--- /dev/null
+++ b/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
+);
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index d33734e..e3682de 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/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;
@@ -161,6 +162,10 @@ public class ActorSystemContext {
@Autowired
@Getter
+ private EntityViewService entityViewService;
+
+ @Autowired
+ @Getter
private TelemetrySubscriptionService tsSubService;
@Autowired
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
index d5260da..4e779d4 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
+++ b/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;
@@ -213,6 +214,11 @@ class DefaultTbContext implements TbContext {
}
@Override
+ public EntityViewService getEntityViewService() {
+ return mainCtx.getEntityViewService();
+ }
+
+ @Override
public MailService getMailService() {
if (mainCtx.isAllowSystemMailService()) {
return mainCtx.getMailService();
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index de73fe0..889630b 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/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);
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
new file mode 100644
index 0000000..0ba35e8
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
index 2a55abf..da154e9 100644
--- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
+++ b/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");
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
index 3a4a837..7d701b7 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
+++ b/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);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
index 600820e..eaf1018 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
+++ b/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
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
index 548c417..fb9a160 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
+++ b/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;
@@ -102,6 +107,9 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
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) {
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/Subscription.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/Subscription.java
index 811c055..32d6243 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/Subscription.java
+++ b/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() {
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 0cc075c..4d89eaf 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/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:
diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java
new file mode 100644
index 0000000..ee97b97
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java
index 3b3d9b0..cdfa001 100644
--- a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java
+++ b/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 {
diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/EntityViewControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/EntityViewControllerNoSqlTest.java
new file mode 100644
index 0000000..404e4e2
--- /dev/null
+++ b/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 {
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/EntityViewControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/EntityViewControllerSqlTest.java
new file mode 100644
index 0000000..76d9925
--- /dev/null
+++ b/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 {
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
index 21de402..698a69e 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
+++ b/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";
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
index fe9c018..ef4994a 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
+++ b/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
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java
new file mode 100644
index 0000000..25b066d
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java
new file mode 100644
index 0000000..752eafc
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
index ed4cf2f..4e35c0b 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
+++ b/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!");
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java
new file mode 100644
index 0000000..459dd99
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java
index 3c48adf..3e4e8ef 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java
+++ b/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);
+ }
+
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java
new file mode 100644
index 0000000..1c32579
--- /dev/null
+++ b/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());
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java
new file mode 100644
index 0000000..c899c65
--- /dev/null
+++ b/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());
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
index 36b250d..9d96f3a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
+++ b/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;
@@ -70,6 +71,9 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
private DeviceService deviceService;
@Autowired
+ private EntityViewService entityViewService;
+
+ @Autowired
private DashboardService dashboardService;
@Override
@@ -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());
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/CassandraEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/CassandraEntityViewDao.java
new file mode 100644
index 0000000..395f902
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java
new file mode 100644
index 0000000..ba43385
--- /dev/null
+++ b/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);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
new file mode 100644
index 0000000..19f326c
--- /dev/null
+++ b/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);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
new file mode 100644
index 0000000..2c5ec75
--- /dev/null
+++ b/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()));
+ }
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
index 3a934eb..9890ff6 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
+++ b/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";
@@ -144,6 +143,22 @@ public class ModelConstants {
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.
*/
public static final String AUDIT_LOG_COLUMN_FAMILY_NAME = "audit_log";
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java
new file mode 100644
index 0000000..eb7f4fe
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java
new file mode 100644
index 0000000..60f6951
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java
new file mode 100644
index 0000000..0f5fdf5
--- /dev/null
+++ b/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);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java
new file mode 100644
index 0000000..912c9d5
--- /dev/null
+++ b/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))));
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
index d92c941..a94e715 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
+++ b/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;
@@ -64,6 +65,9 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe
private DeviceService deviceService;
@Autowired
+ private EntityViewService entityViewService;
+
+ @Autowired
private WidgetsBundleService widgetsBundleService;
@Autowired
@@ -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());
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
index 98e859f..264cdc8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
+++ b/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);
diff --git a/dao/src/main/resources/cassandra/schema-entities.cql b/dao/src/main/resources/cassandra/schema-entities.cql
index e1a21eb..bd978f7 100644
--- a/dao/src/main/resources/cassandra/schema-entities.cql
+++ b/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);
diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql
index 20efec8..abd20ab 100644
--- a/dao/src/main/resources/sql/schema-entities.sql
+++ b/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
+);
diff --git a/dao/src/test/resources/sql/drop-all-tables.sql b/dao/src/test/resources/sql/drop-all-tables.sql
index 23b6a56..b1fb72c 100644
--- a/dao/src/test/resources/sql/drop-all-tables.sql
+++ b/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;
\ No newline at end of file
+DROP TABLE IF EXISTS rule_chain;
+DROP TABLE IF EXISTS entity_views;
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
index e7ef0dd..f9d3c64 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
+++ b/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();
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java
new file mode 100644
index 0000000..40e00ec
--- /dev/null
+++ b/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() {
+
+ }
+}
ui/src/app/api/entity.service.js 17(+15 -2)
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
index 2e29238..ce4dc42 100644
--- a/ui/src/app/api/entity.service.js
+++ b/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) {
ui/src/app/api/entity-view.service.js 238(+238 -0)
diff --git a/ui/src/app/api/entity-view.service.js b/ui/src/app/api/entity-view.service.js
new file mode 100644
index 0000000..e34d3a4
--- /dev/null
+++ b/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;
+ }
+
+}
ui/src/app/app.js 2(+2 -0)
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index c8cdeb0..a3a179e 100644
--- a/ui/src/app/app.js
+++ b/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,
ui/src/app/common/types.constant.js 9(+8 -1)
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index 1e34577..15da584 100644
--- a/ui/src/app/common/types.constant.js
+++ b/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',
diff --git a/ui/src/app/entity/attribute/attribute-table.directive.js b/ui/src/app/entity/attribute/attribute-table.directive.js
index 0061854..3a7c164 100644
--- a/ui/src/app/entity/attribute/attribute-table.directive.js
+++ b/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 {
diff --git a/ui/src/app/entity/entity-autocomplete.directive.js b/ui/src/app/entity/entity-autocomplete.directive.js
index e46c614..8c1fed7 100644
--- a/ui/src/app/entity/entity-autocomplete.directive.js
+++ b/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';
diff --git a/ui/src/app/entity-view/add-entity-view.tpl.html b/ui/src/app/entity-view/add-entity-view.tpl.html
new file mode 100644
index 0000000..48a1788
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/ui/src/app/entity-view/add-entity-views-to-customer.controller.js b/ui/src/app/entity-view/add-entity-views-to-customer.controller.js
new file mode 100644
index 0000000..8e39546
--- /dev/null
+++ b/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
+ };
+ }
+
+}
diff --git a/ui/src/app/entity-view/add-entity-views-to-customer.tpl.html b/ui/src/app/entity-view/add-entity-views-to-customer.tpl.html
new file mode 100644
index 0000000..1149a1d
--- /dev/null
+++ b/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> </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>
\ No newline at end of file
diff --git a/ui/src/app/entity-view/assign-to-customer.controller.js b/ui/src/app/entity-view/assign-to-customer.controller.js
new file mode 100644
index 0000000..3e09ae6
--- /dev/null
+++ b/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
+ };
+ }
+}
diff --git a/ui/src/app/entity-view/assign-to-customer.tpl.html b/ui/src/app/entity-view/assign-to-customer.tpl.html
new file mode 100644
index 0000000..7c1fa25
--- /dev/null
+++ b/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> </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>
\ No newline at end of file
ui/src/app/entity-view/entity-view.controller.js 483(+483 -0)
diff --git a/ui/src/app/entity-view/entity-view.controller.js b/ui/src/app/entity-view/entity-view.controller.js
new file mode 100644
index 0000000..2c8c8ea
--- /dev/null
+++ b/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();
+ });
+ });
+ }
+}
ui/src/app/entity-view/entity-view.directive.js 121(+121 -0)
diff --git a/ui/src/app/entity-view/entity-view.directive.js b/ui/src/app/entity-view/entity-view.directive.js
new file mode 100644
index 0000000..e1ae82f
--- /dev/null
+++ b/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: '&'
+ }
+ };
+}
ui/src/app/entity-view/entity-view.routes.js 72(+72 -0)
diff --git a/ui/src/app/entity-view/entity-view.routes.js b/ui/src/app/entity-view/entity-view.routes.js
new file mode 100644
index 0000000..c14d430
--- /dev/null
+++ b/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"}'
+ }
+ });
+
+}
diff --git a/ui/src/app/entity-view/entity-view-card.tpl.html b/ui/src/app/entity-view/entity-view-card.tpl.html
new file mode 100644
index 0000000..1e90928
--- /dev/null
+++ b/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>
ui/src/app/entity-view/entity-view-fieldset.tpl.html 119(+119 -0)
diff --git a/ui/src/app/entity-view/entity-view-fieldset.tpl.html b/ui/src/app/entity-view/entity-view-fieldset.tpl.html
new file mode 100644
index 0000000..3894eb9
--- /dev/null
+++ b/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>
ui/src/app/entity-view/entity-views.tpl.html 75(+75 -0)
diff --git a/ui/src/app/entity-view/entity-views.tpl.html b/ui/src/app/entity-view/entity-views.tpl.html
new file mode 100644
index 0000000..50a0adb
--- /dev/null
+++ b/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>
ui/src/app/entity-view/index.js 41(+41 -0)
diff --git a/ui/src/app/entity-view/index.js b/ui/src/app/entity-view/index.js
new file mode 100644
index 0000000..69f669e
--- /dev/null
+++ b/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;
ui/src/app/layout/index.js 2(+2 -0)
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index 8f2958d..192400a 100644
--- a/ui/src/app/layout/index.js
+++ b/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,
ui/src/app/locale/locale.constant-en_US.json 85(+85 -0)
diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json
index 2caf013..481d677 100644
--- a/ui/src/app/locale/locale.constant-en_US.json
+++ b/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",
ui/src/app/services/menu.service.js 52(+42 -10)
diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js
index 1c33d1f..869a25d 100644
--- a/ui/src/app/services/menu.service.js
+++ b/ui/src/app/services/menu.service.js
@@ -168,6 +168,12 @@ function Menu(userService, $state, $rootScope) {
icon: 'devices_other'
},
{
+ name: 'entity-view.entity-views',
+ type: 'link',
+ state: 'home.entityViews',
+ icon: 'view_stream'
+ },
+ {
name: 'widget.widget-library',
type: 'link',
state: 'home.widgets-bundles',
@@ -228,6 +234,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: [
{
@@ -274,6 +290,12 @@ function Menu(userService, $state, $rootScope) {
icon: 'devices_other'
},
{
+ name: 'entity-view.entity-views',
+ type: 'link',
+ state: 'home.entityViews',
+ icon: 'view_stream'
+ },
+ {
name: 'dashboard.dashboards',
type: 'link',
state: 'home.dashboards',
@@ -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'
+ }
+ ]
+ }];
}
}
}