thingsboard-memoizeit

Details

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..9bd8f8d
--- /dev/null
+++ b/ui/src/app/api/entity-view.service.js
@@ -0,0 +1,237 @@
+/*
+ * 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..1d1b717 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"
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..fd3b5a8
--- /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) {
+                        assignEntiyViewsToCustomer($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..f980270
--- /dev/null
+++ b/ui/src/app/entity-view/entity-view.directive.js
@@ -0,0 +1,67 @@
+/*
+ * 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, toast, $translate, types, clipboardService, entityViewService, customerService) {
+    var linker = function (scope, element) {
+        var template = $templateCache.get(entityViewFieldsetTemplate);
+        element.html(template);
+
+        scope.types = types;
+        scope.isAssignedToCustomer = false;
+        scope.assignedCustomer = null;
+
+        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;
+                        }
+                    );
+                } else {
+                    scope.isAssignedToCustomer = false;
+                    scope.assignedCustomer = null;
+                }
+            }
+        });
+
+        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: '&',
+            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..14f5079
--- /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: {
+                entityViewsTypes: 'tenant',
+                searchEnabled: true,
+                searchByEntitySubtype: true,
+                searchEntityType: types.entityType.entityview,
+                pageTitle: 'entity-views.entity-views'
+            },
+            ncyBreadcrumb: {
+                label: '{"icon": "devices_other", "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: {
+                entityViewsTypes: 'customer',
+                searchEnabled: true,
+                searchByEntitySubtype: true,
+                searchEntityType: types.entityType.entityview,
+                pageTitle: 'customer.entity-views'
+            },
+            ncyBreadcrumb: {
+                label: '{"icon": "devices_other", "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..e0000c6
--- /dev/null
+++ b/ui/src/app/entity-view/entity-view-fieldset.tpl.html
@@ -0,0 +1,64 @@
+<!--
+
+    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="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-subtype-autocomplete
+                ng-disabled="$root.loading || !isEdit"
+                tb-required="true"
+                the-form="theForm"
+                ng-model="entityView.type"
+                entity-type="types.entityType.entityview">
+        </tb-entity-subtype-autocomplete>
+        <md-input-container class="md-block">
+            <label translate>entity-view.description</label>
+            <textarea ng-model="entityView.additionalInfo.description" rows="2"></textarea>
+        </md-input-container>
+	</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..5398449
--- /dev/null
+++ b/ui/src/app/entity-view/entity-views.tpl.html
@@ -0,0 +1,83 @@
+<!--
+
+    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-manage-credentials="vm.manageCredentials(event, vm.grid.detailsConfig.currentItem)"
+                       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.operatingItem().additionalInfo.gateway" md-on-select="vm.grid.triggerResize()" label="{{ 'extension.extensions' | translate }}">
+            <tb-extension-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-name="vm.grid.operatingItem().name"
+                                entity-type="{{vm.types.entityType.entityview}}">
+            </tb-extension-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..ebdd3ea
--- /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 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', EntityViewCardController)
+    .controller('EntityViewCardController', EntityViewCardController)
+    .controller('AssignEntityViewToCustomerController', AssignEntityViewToCustomerController)
+    .controller('AddEntityViewsToCustomerController', AddEntityViewsToCustomerController)
+    .directive('tbEntityView', EntityViewDirective)
+    .name;
diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json
index 6321d63..5e04a52 100644
--- a/ui/src/app/locale/locale.constant-en_US.json
+++ b/ui/src/app/locale/locale.constant-en_US.json
@@ -338,10 +338,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",
@@ -750,6 +752,77 @@
         "no-entities-prompt": "No entities found",
         "no-data": "No data 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"
+    },
     "event": {
         "event-type": "Event type",
         "type-error": "Error",