thingsboard-memoizeit

Details

diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
index 1c5e5f4..f8f27ed 100644
--- a/ui/src/app/api/widget.service.js
+++ b/ui/src/app/api/widget.service.js
@@ -20,6 +20,7 @@ import tinycolor from 'tinycolor2';
 import thingsboardLedLight from '../components/led-light.directive';
 import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
 import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
+import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget';
 
 import TbFlot from '../widget/lib/flot-widget';
 import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge';
@@ -34,7 +35,7 @@ import thingsboardTypes from '../common/types.constant';
 import thingsboardUtils from '../common/utils.service';
 
 export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
-    thingsboardAlarmsTableWidget, thingsboardTypes, thingsboardUtils])
+    thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardTypes, thingsboardUtils])
     .factory('widgetService', WidgetService)
     .name;
 
@@ -546,6 +547,14 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
 
          '    }\n\n' +
 
+         '    self.typeParameters = function() {\n\n' +
+                    {
+                        useCustomDatasources: false,
+                        maxDatasources: -1 //unlimited
+                        maxDataKeys: -1 //unlimited
+                    }
+         '    }\n\n' +
+
          '    self.onResize = function() {\n\n' +
 
          '    }\n\n' +
@@ -586,10 +595,21 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
             if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) {
                 result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema();
             }
+            if (angular.isFunction(widgetTypeInstance.typeParameters)) {
+                result.typeParameters = widgetTypeInstance.typeParameters();
+            } else {
+                result.typeParameters = {};
+            }
             if (angular.isFunction(widgetTypeInstance.useCustomDatasources)) {
-                result.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
+                result.typeParameters.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
             } else {
-                result.useCustomDatasources = false;
+                result.typeParameters.useCustomDatasources = false;
+            }
+            if (angular.isUndefined(result.typeParameters.maxDatasources)) {
+                result.typeParameters.maxDatasources = -1;
+            }
+            if (angular.isUndefined(result.typeParameters.maxDataKeys)) {
+                result.typeParameters.maxDataKeys = -1;
             }
             return result;
         } catch (e) {
@@ -629,7 +649,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
                     if (widgetType.dataKeySettingsSchema) {
                         widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema;
                     }
-                    widgetInfo.useCustomDatasources = widgetType.useCustomDatasources;
+                    widgetInfo.typeParameters = widgetType.typeParameters;
                     putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
                     putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem);
                     deferred.resolve(widgetInfo);
diff --git a/ui/src/app/components/datasource.directive.js b/ui/src/app/components/datasource.directive.js
index b5327e2..42011b0 100644
--- a/ui/src/app/components/datasource.directive.js
+++ b/ui/src/app/components/datasource.directive.js
@@ -82,6 +82,7 @@ function Datasource($compile, $templateCache, utils, types) {
         require: "^ngModel",
         scope: {
             aliasController: '=',
+            maxDataKeys: '=',
             widgetType: '=',
             functionsOnly: '=',
             datakeySettingsSchema: '=',
diff --git a/ui/src/app/components/datasource.tpl.html b/ui/src/app/components/datasource.tpl.html
index 0633f5f..41b18db 100644
--- a/ui/src/app/components/datasource.tpl.html
+++ b/ui/src/app/components/datasource.tpl.html
@@ -27,6 +27,7 @@
         <tb-datasource-func flex
                             ng-switch-default
                             ng-model="model"
+                            max-data-keys="maxDataKeys"
                             datakey-settings-schema="datakeySettingsSchema"
                             ng-required="model.type === types.datasourceType.function"
                             widget-type="widgetType"
@@ -34,6 +35,7 @@
         </tb-datasource-func>
         <tb-datasource-entity flex
                               ng-model="model"
+                              max-data-keys="maxDataKeys"
                               datakey-settings-schema="datakeySettingsSchema"
                               ng-switch-when="entity"
                               ng-required="model.type === types.datasourceType.entity"
diff --git a/ui/src/app/components/datasource-entity.directive.js b/ui/src/app/components/datasource-entity.directive.js
index e6ec8f8..46c8b63 100644
--- a/ui/src/app/components/datasource-entity.directive.js
+++ b/ui/src/app/components/datasource-entity.directive.js
@@ -156,11 +156,19 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
         };
 
         scope.transformTimeseriesDataKeyChip = function (chip) {
-            return scope.generateDataKey({chip: chip, type: types.dataKeyType.timeseries});
+            if (scope.maxDataKeys > 0 && ngModelCtrl.$viewValue.dataKeys.length >= scope.maxDataKeys ) {
+                return null;
+            } else {
+                return scope.generateDataKey({chip: chip, type: types.dataKeyType.timeseries});
+            }
         };
 
         scope.transformAttributeDataKeyChip = function (chip) {
-            return scope.generateDataKey({chip: chip, type: types.dataKeyType.attribute});
+            if (scope.maxDataKeys > 0 && ngModelCtrl.$viewValue.dataKeys.length >= scope.maxDataKeys ) {
+                return null;
+            } else {
+                return scope.generateDataKey({chip: chip, type: types.dataKeyType.attribute});
+            }
         };
 
         scope.transformAlarmDataKeyChip = function (chip) {
@@ -272,6 +280,7 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
         require: "^ngModel",
         scope: {
             widgetType: '=',
+            maxDataKeys: '=',
             aliasController: '=',
             datakeySettingsSchema: '=',
             generateDataKey: '&',
diff --git a/ui/src/app/components/datasource-entity.tpl.html b/ui/src/app/components/datasource-entity.tpl.html
index 9876491..e788f30 100644
--- a/ui/src/app/components/datasource-entity.tpl.html
+++ b/ui/src/app/components/datasource-entity.tpl.html
@@ -186,5 +186,10 @@
 				<div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.latest.value" class="tb-error-message">datakey.timeseries-or-attributes-required</div>
 			    <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div>
 			</div>
+		   <div class="md-caption" style="color: rgba(0,0,0,0.57);" ng-if="maxDataKeys != -1"
+				translate="datakey.maximum-timeseries-or-attributes"
+				translate-values="{count: maxDataKeys}"
+				translate-interpolation="messageformat"
+		   ></div>
 	   </section>
 </section>
diff --git a/ui/src/app/components/datasource-func.directive.js b/ui/src/app/components/datasource-func.directive.js
index 2a2ae82..96e0387 100644
--- a/ui/src/app/components/datasource-func.directive.js
+++ b/ui/src/app/components/datasource-func.directive.js
@@ -117,7 +117,11 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
         };
 
         scope.transformFuncDataKeyChip = function (chip) {
-            return scope.generateDataKey({chip: chip, type: types.dataKeyType.function});
+            if (scope.maxDataKeys > 0 && ngModelCtrl.$viewValue.dataKeys.length >= scope.maxDataKeys ) {
+                return null;
+            } else {
+                return scope.generateDataKey({chip: chip, type: types.dataKeyType.function});
+            }
         };
 
         scope.transformAlarmDataKeyChip = function (chip) {
@@ -217,6 +221,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
         require: "^ngModel",
         scope: {
             widgetType: '=',
+            maxDataKeys: '=',
             generateDataKey: '&',
             datakeySettingsSchema: '='
         },
diff --git a/ui/src/app/components/datasource-func.tpl.html b/ui/src/app/components/datasource-func.tpl.html
index 7fa4aac..8a57b07 100644
--- a/ui/src/app/components/datasource-func.tpl.html
+++ b/ui/src/app/components/datasource-func.tpl.html
@@ -17,7 +17,7 @@
 -->
 <section class="tb-datasource-func" flex layout='column'
 		 layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
-	<md-input-container ng-if="widgetType != types.widgetType.alarm.value"
+	<md-input-container ng-show="widgetType != types.widgetType.alarm.value"
 						class="tb-datasource-name" md-no-float style="min-width: 200px;">
 		<input name="datasourceName"
 			   placeholder="{{ 'datasource.name' | translate }}"
@@ -132,5 +132,10 @@
 			<div translate ng-message="datasourceKeys" ng-if="widgetType !== types.widgetType.alarm.value" class="tb-error-message">datakey.function-types-required</div>
 			<div translate ng-message="datasourceKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div>
 		</div>
+		<div class="md-caption" style="color: rgba(0,0,0,0.57);" ng-if="maxDataKeys != -1"
+			 translate="datakey.maximum-function-types"
+			 translate-values="{count: maxDataKeys}"
+			 translate-interpolation="messageformat"
+		></div>
    </section>
 </section>
diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js
index baedd09..f2e231f 100644
--- a/ui/src/app/components/widget.controller.js
+++ b/ui/src/app/components/widget.controller.js
@@ -128,7 +128,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
 
     var widgetTypeInstance;
 
-    vm.useCustomDatasources = false;
+    vm.typeParameters = widgetInfo.typeParameters;
 
     try {
         widgetTypeInstance = new widgetType(widgetContext);
@@ -154,9 +154,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
     if (!widgetTypeInstance.onDestroy) {
         widgetTypeInstance.onDestroy = function() {};
     }
-    if (widgetTypeInstance.useCustomDatasources) {
-        vm.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
-    }
 
     //TODO: widgets visibility
 
@@ -502,7 +499,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
                 var subscription = widgetContext.subscriptions[id];
                 subscriptionChanged = subscriptionChanged || subscription.onAliasesChanged(aliasIds);
             }
-            if (subscriptionChanged && !vm.useCustomDatasources) {
+            if (subscriptionChanged && !vm.typeParameters.useCustomDatasources) {
                 reInit();
             }
         });
@@ -513,7 +510,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
 
         configureWidgetElement();
         var deferred = $q.defer();
-        if (!vm.useCustomDatasources) {
+        if (!vm.typeParameters.useCustomDatasources) {
             createDefaultSubscription().then(
                 function success() {
                     subscriptionInited = true;
@@ -535,7 +532,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
     function reInit() {
         onDestroy();
         configureWidgetElement();
-        if (!vm.useCustomDatasources) {
+        if (!vm.typeParameters.useCustomDatasources) {
             createDefaultSubscription().then(
                 function success() {
                     subscriptionInited = true;
@@ -575,7 +572,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
             } catch (e) {
                 handleWidgetException(e);
             }
-            if (!vm.useCustomDatasources && widgetContext.defaultSubscription) {
+            if (!vm.typeParameters.useCustomDatasources && widgetContext.defaultSubscription) {
                 widgetContext.defaultSubscription.subscribe();
             }
         }
diff --git a/ui/src/app/components/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js
index 3849cd3..4a083c1 100644
--- a/ui/src/app/components/widget-config.directive.js
+++ b/ui/src/app/components/widget-config.directive.js
@@ -442,6 +442,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
             forceExpandDatasources: '=?',
             isDataEnabled: '=?',
             widgetType: '=',
+            typeParameters: '=',
             widgetSettingsSchema: '=',
             datakeySettingsSchema: '=',
             aliasController: '=',
diff --git a/ui/src/app/components/widget-config.tpl.html b/ui/src/app/components/widget-config.tpl.html
index 4bfe55c..14cb27c 100644
--- a/ui/src/app/components/widget-config.tpl.html
+++ b/ui/src/app/components/widget-config.tpl.html
@@ -62,7 +62,14 @@
                          && isDataEnabled">
                 <v-pane id="datasources-pane" expanded="true">
                     <v-pane-header>
-                        {{ 'widget-config.datasources' | translate }}
+                        <div layout="column">
+                            <div>{{ 'widget-config.datasources' | translate }}</div>
+                            <div class="md-caption" style="color: rgba(0,0,0,0.57);" ng-if="typeParameters.maxDatasources != -1"
+                                 translate="widget-config.maximum-datasources"
+                                 translate-values="{count: typeParameters.maxDatasources}"
+                                 translate-interpolation="messageformat"
+                            ></div>
+                        </div>
                     </v-pane-header>
                     <v-pane-content>
                         <div ng-if="datasources.length === 0">
@@ -88,6 +95,7 @@
                                          style="padding: 0 0 0 10px; margin: 5px;">
                                         <tb-datasource flex ng-model="datasource.value"
                                                        widget-type="widgetType"
+                                                       max-data-keys="typeParameters.maxDataKeys"
                                                        alias-controller="aliasController"
                                                        functions-only="functionsOnly"
                                                        datakey-settings-schema="datakeySettingsSchema"
@@ -111,7 +119,7 @@
                             </div>
                         </div>
                         <div flex layout="row" layout-align="start center">
-                            <md-button ng-disabled="loading" class="md-primary md-raised"
+                            <md-button ng-show="typeParameters.maxDatasources == -1 || datasources.length < typeParameters.maxDatasources" ng-disabled="loading" class="md-primary md-raised"
                                        ng-click="addDatasource($event)" aria-label="{{ 'action.add' | translate }}">
                                 <md-tooltip md-direction="top">
                                     {{ 'widget-config.add-datasource' | translate }}
@@ -140,7 +148,7 @@
                     </v-pane-content>
                 </v-pane>
             </v-accordion>
-            <v-accordion id="alarn-source-accordion" control="alarmSourceAccordion" class="vAccordion--default"
+            <v-accordion id="alarm-source-accordion" control="alarmSourceAccordion" class="vAccordion--default"
                          ng-if="widgetType === types.widgetType.alarm.value && isDataEnabled">
                 <v-pane id="alarm-source-pane" expanded="true">
                     <v-pane-header>
diff --git a/ui/src/app/dashboard/add-widget.tpl.html b/ui/src/app/dashboard/add-widget.tpl.html
index 7617760..894c23d 100644
--- a/ui/src/app/dashboard/add-widget.tpl.html
+++ b/ui/src/app/dashboard/add-widget.tpl.html
@@ -33,6 +33,7 @@
             <div class="md-dialog-content" style="padding-top: 0px;">
                 <fieldset ng-disabled="loading" style="position: relative; height: 600px;">
                     <tb-widget-config widget-type="vm.widget.type"
+                                      type-parameters="vm.widgetInfo.typeParameters"
                                       force-expand-datasources="true"
                                       ng-model="vm.widgetConfig"
                                       widget-settings-schema="vm.settingsSchema"
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 735c881..f41a635 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -918,7 +918,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
                     }
                 }
 
-                if (widgetTypeInfo.useCustomDatasources) {
+                if (widgetTypeInfo.typeParameters.useCustomDatasources) {
                     addWidget(newWidget);
                 } else {
                     $mdDialog.show({
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index 1a3b9bf..72820bb 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -212,6 +212,7 @@
                          class="tb-absolute-fill" md-border-bottom>
                     <md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}">
                         <tb-dashboard
+                                alias-controller="vm.dashboardCtx.aliasController"
                                 widgets="vm.timeseriesWidgetTypes"
                                 is-edit="false"
                                 is-mobile="true"
@@ -222,6 +223,7 @@
                     </md-tab>
                     <md-tab ng-if="vm.latestWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.latest-values' | translate }}">
                         <tb-dashboard
+                                alias-controller="vm.dashboardCtx.aliasController"
                                 widgets="vm.latestWidgetTypes"
                                 is-edit="false"
                                 is-mobile="true"
@@ -232,6 +234,7 @@
                     </md-tab>
                     <md-tab ng-if="vm.rpcWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.rpc' | translate }}">
                         <tb-dashboard
+                                alias-controller="vm.dashboardCtx.aliasController"
                                 widgets="vm.rpcWidgetTypes"
                                 is-edit="false"
                                 is-mobile="true"
@@ -242,6 +245,7 @@
                     </md-tab>
                     <md-tab ng-if="vm.alarmWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.alarm' | translate }}">
                         <tb-dashboard
+                                alias-controller="vm.dashboardCtx.aliasController"
                                 widgets="vm.alarmWidgetTypes"
                                 is-edit="false"
                                 is-mobile="true"
@@ -252,6 +256,7 @@
                     </md-tab>
                     <md-tab ng-if="vm.staticWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.static' | translate }}">
                         <tb-dashboard
+                                alias-controller="vm.dashboardCtx.aliasController"
                                 widgets="vm.staticWidgetTypes"
                                 is-edit="false"
                                 is-mobile="true"
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
index d0c651a..dcf05e2 100644
--- a/ui/src/app/dashboard/edit-widget.directive.js
+++ b/ui/src/app/dashboard/edit-widget.directive.js
@@ -40,7 +40,8 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
                             };
                             var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
                             var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
-                            scope.isDataEnabled = !widgetInfo.useCustomDatasources;
+                            scope.typeParameters = widgetInfo.typeParameters;
+                            scope.isDataEnabled = !widgetInfo.typeParameters.useCustomDatasources;
                             if (!settingsSchema || settingsSchema === '') {
                                 scope.settingsSchema = {};
                             } else {
diff --git a/ui/src/app/dashboard/edit-widget.tpl.html b/ui/src/app/dashboard/edit-widget.tpl.html
index 4b4382e..74f5755 100644
--- a/ui/src/app/dashboard/edit-widget.tpl.html
+++ b/ui/src/app/dashboard/edit-widget.tpl.html
@@ -17,6 +17,7 @@
 -->
 <fieldset ng-disabled="loading">
 	<tb-widget-config widget-type="widget.type"
+					  type-parameters="typeParameters"
 					  ng-model="widgetConfig"
 					  is-data-enabled="isDataEnabled"
 					  widget-settings-schema="settingsSchema"
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index b213746..4710b92 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -499,9 +499,11 @@ export default angular.module('thingsboard.locale', [])
                     "alarm": "Alarm fields",
                     "timeseries-required": "Entity timeseries are required.",
                     "timeseries-or-attributes-required": "Entity timeseries/attributes are required.",
+                    "maximum-timeseries-or-attributes": "Maximum { count, select, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }",
                     "alarm-fields-required": "Alarm fields are required.",
                     "function-types": "Function types",
-                    "function-types-required": "Function types are required."
+                    "function-types-required": "Function types are required.",
+                    "maximum-function-types": "Maximum { count, select, 1 {1 function type is allowed.} other {# function types are allowed} }"
                 },
                 "datasource": {
                     "type": "Datasource type",
@@ -691,7 +693,13 @@ export default angular.module('thingsboard.locale', [])
                     "type-alarm": "Alarm",
                     "type-alarms": "Alarms",
                     "list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }",
-                    "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'"
+                    "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'",
+                    "search": "Search entities",
+                    "selected-entities": "{ count, select, 1 {1 entity} other {# entities} } selected",
+                    "entity-name": "Entity name",
+                    "details": "Entity details",
+                    "no-entities-prompt": "No entities found",
+                    "no-data": "No data to display"
                 },
                 "event": {
                     "event-type": "Event type",
@@ -1144,6 +1152,7 @@ export default angular.module('thingsboard.locale', [])
                     "use-dashboard-timewindow": "Use dashboard timewindow",
                     "display-legend": "Display legend",
                     "datasources": "Datasources",
+                    "maximum-datasources": "Maximum { count, select, 1 {1 datasource is allowed.} other {# datasources are allowed} }",
                     "datasource-type": "Type",
                     "datasource-parameters": "Parameters",
                     "remove-datasource": "Remove datasource",
diff --git a/ui/src/app/widget/lib/entities-table-widget.js b/ui/src/app/widget/lib/entities-table-widget.js
new file mode 100644
index 0000000..4b8aa6e
--- /dev/null
+++ b/ui/src/app/widget/lib/entities-table-widget.js
@@ -0,0 +1,474 @@
+/*
+ * Copyright © 2016-2017 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 './entities-table-widget.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entitiesTableWidgetTemplate from './entities-table-widget.tpl.html';
+//import entityDetailsDialogTemplate from './entitiy-details-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import tinycolor from 'tinycolor2';
+import cssjs from '../../../vendor/css.js/css';
+
+export default angular.module('thingsboard.widgets.entitiesTableWidget', [])
+    .directive('tbEntitiesTableWidget', EntitiesTableWidget)
+    .name;
+
+/*@ngInject*/
+function EntitiesTableWidget() {
+    return {
+        restrict: "E",
+        scope: true,
+        bindToController: {
+            tableId: '=',
+            ctx: '='
+        },
+        controller: EntitiesTableWidgetController,
+        controllerAs: 'vm',
+        templateUrl: entitiesTableWidgetTemplate
+    };
+}
+
+/*@ngInject*/
+function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $translate, utils, types) {
+    var vm = this;
+
+    vm.stylesInfo = {};
+    vm.contentsInfo = {};
+    vm.columnWidth = {};
+
+    vm.showData = true;
+    vm.hasData = false;
+
+    vm.entities = [];
+    vm.entitiesCount = 0;
+
+    vm.datasources = null;
+    vm.allEntities = null;
+
+    vm.currentEntity = null;
+
+    vm.displayEntityName = true;
+    vm.displayEntityType = true;
+    vm.displayActions = false; //TODO: Widget actions
+    vm.displayPagination = true;
+    vm.defaultPageSize = 10;
+    vm.defaultSortOrder = 'entityName';
+
+    vm.query = {
+        order: vm.defaultSortOrder,
+        limit: vm.defaultPageSize,
+        page: 1,
+        search: null
+    };
+
+    vm.searchAction = {
+        name: 'action.search',
+        show: true,
+        onAction: function() {
+            vm.enterFilterMode();
+        },
+        icon: 'search'
+    };
+
+    vm.enterFilterMode = enterFilterMode;
+    vm.exitFilterMode = exitFilterMode;
+    vm.onReorder = onReorder;
+    vm.onPaginate = onPaginate;
+    vm.onRowClick = onRowClick;
+    vm.isCurrent = isCurrent;
+
+    vm.cellStyle = cellStyle;
+    vm.cellContent = cellContent;
+
+    $scope.$watch('vm.ctx', function() {
+        if (vm.ctx) {
+            vm.settings = vm.ctx.settings;
+            vm.widgetConfig = vm.ctx.widgetConfig;
+            vm.subscription = vm.ctx.defaultSubscription;
+            vm.datasources = vm.subscription.datasources;
+            initializeConfig();
+            updateDatasources();
+        }
+    });
+
+    $scope.$watch("vm.query.search", function(newVal, prevVal) {
+        if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
+            updateEntities();
+        }
+    });
+
+    $scope.$on('entities-table-data-updated', function(event, tableId) {
+        if (vm.tableId == tableId) {
+            if (vm.subscription) {
+                updateEntitiesData(vm.subscription.data);
+                updateEntities();
+                $scope.$digest();
+            }
+        }
+    });
+
+    $scope.$watch(function() { return $mdMedia('gt-xs'); }, function(isGtXs) {
+        vm.isGtXs = isGtXs;
+    });
+
+    $scope.$watch(function() { return $mdMedia('gt-md'); }, function(isGtMd) {
+        vm.isGtMd = isGtMd;
+        if (vm.isGtMd) {
+            vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3];
+        } else {
+            vm.limitOptions = null;
+        }
+    });
+
+    function initializeConfig() {
+
+        vm.ctx.widgetActions = [ vm.searchAction ];
+
+        if (vm.settings.entitiesTitle && vm.settings.entitiesTitle.length) {
+            var translationId = types.translate.customTranslationsPrefix + vm.settings.entitiesTitle;
+            var translation = $translate.instant(translationId);
+            if (translation != translationId) {
+                vm.entitiesTitle = translation + '';
+            } else {
+                vm.entitiesTitle = vm.settings.entitiesTitle;
+            }
+        } else {
+            vm.entitiesTitle = $translate.instant('entity.entities');
+        }
+
+        vm.ctx.widgetTitle = vm.entitiesTitle;
+
+        vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true;
+        vm.displayEntityName = angular.isDefined(vm.settings.displayEntityName) ? vm.settings.displayEntityName : true;
+        vm.displayEntityType = angular.isDefined(vm.settings.displayEntityType) ? vm.settings.displayEntityType : true;
+        vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true;
+
+        var pageSize = vm.settings.defaultPageSize;
+        if (angular.isDefined(pageSize) && Number.isInteger(pageSize) && pageSize > 0) {
+            vm.defaultPageSize = pageSize;
+        }
+
+        if (vm.settings.defaultSortOrder && vm.settings.defaultSortOrder.length) {
+            vm.defaultSortOrder = vm.settings.defaultSortOrder;
+        }
+
+        vm.query.order = vm.defaultSortOrder;
+        vm.query.limit = vm.defaultPageSize;
+        if (vm.isGtMd) {
+            vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3];
+        } else {
+            vm.limitOptions = null;
+        }
+
+        var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)';
+        var defaultColor = tinycolor(origColor);
+        var mdDark = defaultColor.setAlpha(0.87).toRgbString();
+        var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();
+        var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();
+        //var mdDarkIcon = mdDarkSecondary;
+        var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
+
+        var cssString = 'table.md-table th.md-column {\n'+
+            'color: ' + mdDarkSecondary + ';\n'+
+            '}\n'+
+            'table.md-table th.md-column.md-checkbox-column md-checkbox:not(.md-checked) .md-icon {\n'+
+            'border-color: ' + mdDarkSecondary + ';\n'+
+            '}\n'+
+            'table.md-table th.md-column md-icon.md-sort-icon {\n'+
+            'color: ' + mdDarkDisabled + ';\n'+
+            '}\n'+
+            'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+
+            'color: ' + mdDark + ';\n'+
+            '}\n'+
+            'table.md-table td.md-cell {\n'+
+            'color: ' + mdDark + ';\n'+
+            'border-top: 1px '+mdDarkDivider+' solid;\n'+
+            '}\n'+
+            'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+
+            'border-color: ' + mdDarkSecondary + ';\n'+
+            '}\n'+
+            'table.md-table td.md-cell.md-placeholder {\n'+
+            'color: ' + mdDarkDisabled + ';\n'+
+            '}\n'+
+            'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+
+            'color: ' + mdDarkSecondary + ';\n'+
+            '}\n'+
+            '.md-table-pagination {\n'+
+            'color: ' + mdDarkSecondary + ';\n'+
+            'border-top: 1px '+mdDarkDivider+' solid;\n'+
+            '}\n'+
+            '.md-table-pagination .buttons md-icon {\n'+
+            'color: ' + mdDarkSecondary + ';\n'+
+            '}\n'+
+            '.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+
+            'color: ' + mdDarkSecondary + ';\n'+
+            '}';
+
+        var cssParser = new cssjs();
+        cssParser.testMode = false;
+        var namespace = 'entities-table-' + hashCode(cssString);
+        cssParser.cssPreviewNamespace = namespace;
+        cssParser.createStyleElement(namespace, cssString);
+        $element.addClass(namespace);
+
+        function hashCode(str) {
+            var hash = 0;
+            var i, char;
+            if (str.length === 0) return hash;
+            for (i = 0; i < str.length; i++) {
+                char = str.charCodeAt(i);
+                hash = ((hash << 5) - hash) + char;
+                hash = hash & hash;
+            }
+            return hash;
+        }
+    }
+
+    function enterFilterMode () {
+        vm.query.search = '';
+        vm.ctx.hideTitlePanel = true;
+    }
+
+    function exitFilterMode () {
+        vm.query.search = null;
+        updateEntities();
+        vm.ctx.hideTitlePanel = false;
+    }
+
+    function onReorder () {
+        updateEntities();
+    }
+
+    function onPaginate () {
+        updateEntities();
+    }
+
+    function onRowClick($event, entity) {
+        if (vm.currentEntity != entity) {
+            vm.currentEntity = entity;
+        }
+    }
+
+    function isCurrent(entity) {
+        return (vm.currentEntity && entity && vm.currentEntity.id && entity.id) &&
+            (vm.currentEntity.id.id === entity.id.id);
+    }
+
+    function updateEntities() {
+        var result = $filter('orderBy')(vm.allEntities, vm.query.order);
+        if (vm.query.search != null) {
+            result = $filter('filter')(result, {$: vm.query.search});
+        }
+        vm.entitiesCount = result.length;
+
+        if (vm.displayPagination) {
+            var startIndex = vm.query.limit * (vm.query.page - 1);
+            vm.entities = result.slice(startIndex, startIndex + vm.query.limit);
+        } else {
+            vm.entities = result;
+        }
+    }
+
+    function cellStyle(entity, key) {
+        var style = {};
+        if (entity && key) {
+            var styleInfo = vm.stylesInfo[key.label];
+            var value = getEntityValue(entity, key);
+            if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
+                try {
+                    style = styleInfo.cellStyleFunction(value);
+                } catch (e) {
+                    style = {};
+                }
+            } else {
+                style = defaultStyle(key, value);
+            }
+        }
+        if (!style.width) {
+            var columnWidth = vm.columnWidth[key.label];
+            style.width = columnWidth;
+        }
+        return style;
+    }
+
+    function cellContent(entity, key) {
+        var strContent = '';
+        if (entity && key) {
+            var contentInfo = vm.contentsInfo[key.label];
+            var value = getEntityValue(entity, key);
+            if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
+                if (angular.isDefined(value)) {
+                    strContent = '' + value;
+                }
+                var content = strContent;
+                try {
+                    content = contentInfo.cellContentFunction(value, entity, $filter);
+                } catch (e) {
+                    content = strContent;
+                }
+            } else {
+                content = defaultContent(key, value);
+            }
+            return content;
+        } else {
+            return strContent;
+        }
+    }
+
+    function defaultContent(key, value) {
+        if (angular.isDefined(value)) {
+            return value;
+        } else {
+            return '';
+        }
+    }
+
+    function defaultStyle(/*key, value*/) {
+        return {};
+    }
+
+    const getDescendantProp = (obj, path) => (
+        path.split('.').reduce((acc, part) => acc && acc[part], obj)
+    );
+
+    function getEntityValue(entity, key) {
+        return getDescendantProp(entity, key.name);
+    }
+
+    function updateEntitiesData(data) {
+        if (vm.allEntities) {
+            for (var i=0;i<vm.allEntities.length;i++) {
+                var entity = vm.allEntities[i];
+                for (var a=0;a<vm.dataKeys.length;a++) {
+                    var dataKey = vm.dataKeys[a];
+                    var index = i * vm.dataKeys.length + a;
+                    var keyData = data[index].data;
+                    if (keyData && keyData.length && keyData[0].length > 1) {
+                        var value = keyData[0][1];
+                        entity[dataKey.name] = value;
+                    } else {
+                        entity[dataKey.name] = '';
+                    }
+                }
+            }
+        }
+    }
+
+    function updateDatasources() {
+
+        vm.stylesInfo = {};
+        vm.contentsInfo = {};
+        vm.columnWidth = {};
+        vm.dataKeys = [];
+        vm.allEntities = [];
+
+        var datasource;
+        var dataKey;
+
+        datasource = vm.datasources[0];
+
+        vm.ctx.widgetTitle = utils.createLabelFromDatasource(datasource, vm.entitiesTitle);
+
+        for (var d = 0; d < datasource.dataKeys.length; d++ ) {
+            dataKey = angular.copy(datasource.dataKeys[d]);
+            if (dataKey.type == types.dataKeyType.function) {
+                dataKey.name = dataKey.label;
+            }
+            vm.dataKeys.push(dataKey);
+
+            var translationId = types.translate.customTranslationsPrefix + dataKey.label;
+            var translation = $translate.instant(translationId);
+            if (translation != translationId) {
+                dataKey.title = translation + '';
+            } else {
+                dataKey.title = dataKey.label;
+            }
+
+            var keySettings = dataKey.settings;
+
+            var cellStyleFunction = null;
+            var useCellStyleFunction = false;
+
+            if (keySettings.useCellStyleFunction === true) {
+                if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {
+                    try {
+                        cellStyleFunction = new Function('value', keySettings.cellStyleFunction);
+                        useCellStyleFunction = true;
+                    } catch (e) {
+                        cellStyleFunction = null;
+                        useCellStyleFunction = false;
+                    }
+                }
+            }
+
+            vm.stylesInfo[dataKey.label] = {
+                useCellStyleFunction: useCellStyleFunction,
+                cellStyleFunction: cellStyleFunction
+            };
+
+            var cellContentFunction = null;
+            var useCellContentFunction = false;
+
+            if (keySettings.useCellContentFunction === true) {
+                if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {
+                    try {
+                        cellContentFunction = new Function('value, entity, filter', keySettings.cellContentFunction);
+                        useCellContentFunction = true;
+                    } catch (e) {
+                        cellContentFunction = null;
+                        useCellContentFunction = false;
+                    }
+                }
+            }
+
+            vm.contentsInfo[dataKey.label] = {
+                useCellContentFunction: useCellContentFunction,
+                cellContentFunction: cellContentFunction
+            };
+
+            var columnWidth = angular.isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px';
+            vm.columnWidth[dataKey.label] = columnWidth;
+        }
+
+        for (var i=0;i<vm.datasources.length;i++) {
+            datasource = vm.datasources[i];
+            var entity = {
+                id: {}
+            };
+            entity.entityName = datasource.entityName;
+            if (datasource.entityId) {
+                entity.id.id = datasource.entityId;
+            }
+            if (datasource.entityType) {
+                entity.id.entityType = datasource.entityType;
+                entity.entityType = $translate.instant(types.entityTypeTranslations[datasource.entityType].type) + '';
+            } else {
+                entity.entityType = '';
+            }
+            for (d = 0; d < vm.dataKeys.length; d++) {
+                dataKey = vm.dataKeys[d];
+                entity[dataKey.name] = '';
+            }
+            vm.allEntities.push(entity);
+        }
+
+    }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/widget/lib/entities-table-widget.scss b/ui/src/app/widget/lib/entities-table-widget.scss
new file mode 100644
index 0000000..30c42cc
--- /dev/null
+++ b/ui/src/app/widget/lib/entities-table-widget.scss
@@ -0,0 +1,58 @@
+/**
+ * Copyright © 2016-2017 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-has-timewindow {
+  .tb-entities-table {
+    md-toolbar {
+      min-height: 60px;
+      max-height: 60px;
+      &.md-table-toolbar {
+        .md-toolbar-tools {
+          max-height: 60px;
+        }
+      }
+    }
+  }
+}
+
+.tb-entities-table {
+
+  md-toolbar {
+    min-height: 39px;
+    max-height: 39px;
+    &.md-table-toolbar {
+      .md-toolbar-tools {
+        max-height: 39px;
+      }
+    }
+  }
+
+  &.tb-data-table {
+    table.md-table, table.md-table.md-row-select {
+      tbody {
+        tr {
+          td {
+            &.tb-action-cell {
+              min-width: 36px;
+              max-width: 36px;
+              width: 36px;
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/ui/src/app/widget/lib/entities-table-widget.tpl.html b/ui/src/app/widget/lib/entities-table-widget.tpl.html
new file mode 100644
index 0000000..247d479
--- /dev/null
+++ b/ui/src/app/widget/lib/entities-table-widget.tpl.html
@@ -0,0 +1,86 @@
+<!--
+
+    Copyright © 2016-2017 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 class="tb-absolute-fill tb-entities-table tb-data-table" layout="column">
+    <div ng-show="vm.showData" flex class="tb-absolute-fill" layout="column">
+        <md-toolbar class="md-table-toolbar md-default" ng-show="vm.query.search != null">
+            <div class="md-toolbar-tools">
+                <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
+                    <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{'entity.search' | translate}}
+                    </md-tooltip>
+                </md-button>
+                <md-input-container flex>
+                    <label>&nbsp;</label>
+                    <input ng-model="vm.query.search" placeholder="{{'entity.search' | translate}}"/>
+                </md-input-container>
+                <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
+                    <md-icon aria-label="Close" class="material-icons">close</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.close' | translate }}
+                    </md-tooltip>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-table-container flex>
+            <table md-table>
+                <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
+                <tr md-row>
+                    <th md-column ng-if="vm.displayEntityName" md-order-by="entityName"><span translate>entity.entity-name</span></th>
+                    <th md-column ng-if="vm.displayEntityType" md-order-by="entityType"><span translate>entity.entity-type</span></th>
+                    <th md-column md-order-by="{{ key.name }}" ng-repeat="key in vm.dataKeys"><span>{{ key.title }}</span></th>
+                    <th md-column ng-if="vm.displayActions"><span>&nbsp</span></th>
+                </tr>
+                </thead>
+                <tbody md-body>
+                <tr ng-show="vm.entities.length" md-row md-select="entity"
+                    md-select-id="id.id" md-auto-select="false" ng-repeat="entity in vm.entities"
+                    ng-click="vm.onRowClick($event, entity)" ng-class="{'tb-current': vm.isCurrent(entity)}">
+                    <td md-cell flex ng-if="vm.displayEntityName">{{entity.entityName}}</td>
+                    <td md-cell flex ng-if="vm.displayEntityType">{{entity.entityType}}</td>
+                    <td md-cell flex ng-repeat="key in vm.dataKeys"
+                        ng-style="vm.cellStyle(entity, key)"
+                        ng-bind-html="vm.cellContent(entity, key)">
+                    </td>
+                    <td md-cell ng-if="vm.displayActions" class="tb-action-cell">
+                        <!--md-button class="md-icon-button" aria-label="{{ 'entity.details' | translate }}"
+                                   ng-click="vm.openEntityDetails($event, entity)">
+                            <md-icon aria-label="{{ 'entity.details' | translate }}" class="material-icons">more_horiz</md-icon>
+                            <md-tooltip md-direction="top">
+                                {{ 'entity.details' | translate }}
+                            </md-tooltip>
+                        </md-button-->
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+            <md-divider></md-divider>
+            <span ng-show="!vm.entities.length"
+                  layout-align="center center"
+                  class="no-data-found" translate>entity.no-entities-prompt</span>
+        </md-table-container>
+        <md-table-pagination ng-if="vm.displayPagination" md-boundary-links md-limit="vm.query.limit" md-limit-options="vm.limitOptions"
+                             md-page="vm.query.page" md-total="{{vm.entitiesCount}}"
+                             md-on-paginate="vm.onPaginate" md-page-select="vm.isGtMd">
+        </md-table-pagination>
+    </div>
+    <span ng-show="!vm.showData"
+          layout-align="center center"
+          style="text-transform: uppercase; display: flex;"
+          class="tb-absolute-fill" translate>entity.no-data</span>
+</div>
diff --git a/ui/src/app/widget/widget-library.controller.js b/ui/src/app/widget/widget-library.controller.js
index bda8835..798227f 100644
--- a/ui/src/app/widget/widget-library.controller.js
+++ b/ui/src/app/widget/widget-library.controller.js
@@ -13,6 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+import AliasController from '../api/alias-controller';
+
 /* eslint-disable import/no-unresolved, import/default */
 
 import selectWidgetTypeTemplate from './select-widget-type.tpl.html';
@@ -21,7 +24,8 @@ import selectWidgetTypeTemplate from './select-widget-type.tpl.html';
 
 /*@ngInject*/
 export default function WidgetLibraryController($scope, $rootScope, $q, widgetService, userService, importExport,
-                                                $state, $stateParams, $document, $mdDialog, $translate, $filter, types) {
+                                                $state, $stateParams, $document, $mdDialog, $translate, $filter,
+                                                utils, types, entityService) {
 
     var vm = this;
 
@@ -31,6 +35,14 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe
     vm.widgetTypes = [];
     vm.dashboardInitComplete = false;
 
+    var stateController = {
+        getStateParams: function() {
+            return {};
+        }
+    };
+    vm.aliasController = new AliasController($scope, $q, $filter, utils,
+        types, entityService, stateController, {});
+
     vm.noData = noData;
     vm.dashboardInited = dashboardInited;
     vm.dashboardInitFailed = dashboardInitFailed;
diff --git a/ui/src/app/widget/widget-library.tpl.html b/ui/src/app/widget/widget-library.tpl.html
index bd3e0c3..b2dd4a7 100644
--- a/ui/src/app/widget/widget-library.tpl.html
+++ b/ui/src/app/widget/widget-library.tpl.html
@@ -28,6 +28,7 @@
 		  class="md-headline tb-absolute-fill">widgets-bundle.empty</span>
 </section>
 <tb-dashboard
+	alias-controller="vm.aliasController"
 	widgets="vm.widgetTypes"
 	is-edit="false"
 	is-edit-action-enabled="true"