thingsboard-aplcache

Changes

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() {
+
+    }
+}
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) {
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;
+    }
+
+}
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,
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>&nbsp;</label>
+                        <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">
+                            search
+                        </md-icon>
+                        <input id="entity-view-search" autofocus ng-model="vm.searchText"
+                               ng-change="vm.searchEntityViewTextUpdated()"
+                               placeholder="{{ 'common.enter-search' | translate }}"/>
+                    </md-input-container>
+                    <div style='min-height: 150px;'>
+					<span translate layout-align="center center"
+                          style="text-transform: uppercase; display: flex; height: 150px;"
+                          class="md-subhead"
+                          ng-show="vm.noData()">entity-view.no-entity-views-text</span>
+                        <md-virtual-repeat-container ng-show="vm.hasData()"
+                                                     tb-scope-element="repeatContainer" md-top-index="vm.topIndex" flex
+                                                     style='min-height: 150px; width: 100%;'>
+                            <md-list>
+                                <md-list-item md-virtual-repeat="entityView in vm.theEntityViews" md-on-demand
+                                              class="repeated-item" flex>
+                                    <md-checkbox ng-click="vm.toggleEntityViewSelection($event, entityView)"
+                                                 aria-label="{{ 'item.selected' | translate }}"
+                                                 ng-checked="entityView.selected"></md-checkbox>
+                                    <span> {{ entityView.name }} </span>
+                                </md-list-item>
+                            </md-list>
+                        </md-virtual-repeat-container>
+                    </div>
+                </fieldset>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="$root.loading || vm.entityViews.selectedCount == 0" type="submit"
+                       class="md-raised md-primary">
+                {{ 'action.assign' | translate }}
+            </md-button>
+            <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+                translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
\ 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>&nbsp;</label>
+                        <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">
+                            search
+                        </md-icon>
+                        <input id="customer-search" autofocus ng-model="vm.searchText"
+                               ng-change="vm.searchCustomerTextUpdated()"
+                               placeholder="{{ 'common.enter-search' | translate }}"/>
+                    </md-input-container>
+                    <div style='min-height: 150px;'>
+					<span translate layout-align="center center"
+                          style="text-transform: uppercase; display: flex; height: 150px;"
+                          class="md-subhead"
+                          ng-show="vm.noData()">customer.no-customers-text</span>
+                        <md-virtual-repeat-container ng-show="vm.hasData()"
+                                                     tb-scope-element="repeatContainer" md-top-index="vm.topIndex" flex
+                                                     style='min-height: 150px; width: 100%;'>
+                            <md-list>
+                                <md-list-item md-virtual-repeat="customer in vm.theCustomers" md-on-demand
+                                              class="repeated-item" flex>
+                                    <md-checkbox ng-click="vm.toggleCustomerSelection($event, customer)"
+                                                 aria-label="{{ 'item.selected' | translate }}"
+                                                 ng-checked="vm.isCustomerSelected(customer)"></md-checkbox>
+                                    <span> {{ customer.title }} </span>
+                                </md-list-item>
+                            </md-list>
+                        </md-virtual-repeat-container>
+                    </div>
+                </fieldset>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="$root.loading || vm.customers.selection==null" type="submit" class="md-raised md-primary">
+                {{ 'action.assign' | translate }}
+            </md-button>
+            <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+                translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
\ No newline at end of file
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();
+            });
+        });
+    }
+}
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: '&'
+        }
+    };
+}
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>
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>
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>
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;
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,
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",
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'
+                                }
+                            ]
+                        }];
                 }
             }
         }