thingsboard-memoizeit

Changes

ui/src/app/components/datasource-device.directive.js 248(+0 -248)

ui/src/app/components/datasource-device.scss 44(+0 -44)

ui/src/app/components/datasource-device.tpl.html 137(+0 -137)

ui/src/app/components/device-alias-select.directive.js 144(+0 -144)

ui/src/app/components/device-alias-select.tpl.html 52(+0 -52)

ui/src/app/components/device-filter.directive.js 217(+0 -217)

ui/src/app/components/device-filter.scss 45(+0 -45)

ui/src/app/components/device-filter.tpl.html 67(+0 -67)

ui/src/app/dashboard/aliases-device-select.directive.js 153(+0 -153)

ui/src/app/dashboard/aliases-device-select.scss 46(+0 -46)

ui/src/app/dashboard/aliases-device-select-button.tpl.html 32(+0 -32)

ui/src/app/dashboard/aliases-device-select-panel.controller.js 31(+0 -31)

ui/src/app/dashboard/device-aliases.controller.js 227(+0 -227)

ui/src/app/dashboard/device-aliases.tpl.html 94(+0 -94)

ui/src/app/device/attribute/add-attribute-dialog.controller.js 50(+0 -50)

ui/src/app/device/attribute/add-attribute-dialog.tpl.html 95(+0 -95)

ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js 73(+0 -73)

ui/src/app/device/attribute/add-widget-to-dashboard-dialog.tpl.html 81(+0 -81)

ui/src/app/device/attribute/attribute-table.directive.js 421(+0 -421)

ui/src/app/device/attribute/attribute-table.scss 50(+0 -50)

ui/src/app/device/attribute/attribute-table.tpl.html 210(+0 -210)

ui/src/app/device/attribute/edit-attribute-value.controller.js 71(+0 -71)

ui/src/app/device/attribute/edit-attribute-value.tpl.html 72(+0 -72)

Details

diff --git a/ui/src/app/api/asset.service.js b/ui/src/app/api/asset.service.js
index f685b1e..2fbb11d 100644
--- a/ui/src/app/api/asset.service.js
+++ b/ui/src/app/api/asset.service.js
@@ -31,7 +31,8 @@ function AssetService($http, $q, customerService, userService) {
         getTenantAssets: getTenantAssets,
         getCustomerAssets: getCustomerAssets,
         findByQuery: findByQuery,
-        fetchAssetsByNameFilter: fetchAssetsByNameFilter
+        fetchAssetsByNameFilter: fetchAssetsByNameFilter,
+        getAssetTypes: getAssetTypes
     }
 
     return service;
@@ -152,7 +153,7 @@ function AssetService($http, $q, customerService, userService) {
         return deferred.promise;
     }
 
-    function getTenantAssets(pageLink, applyCustomersInfo, config) {
+    function getTenantAssets(pageLink, applyCustomersInfo, config, type) {
         var deferred = $q.defer();
         var url = '/api/tenant/assets?limit=' + pageLink.limit;
         if (angular.isDefined(pageLink.textSearch)) {
@@ -164,6 +165,9 @@ function AssetService($http, $q, customerService, userService) {
         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(
@@ -184,7 +188,7 @@ function AssetService($http, $q, customerService, userService) {
         return deferred.promise;
     }
 
-    function getCustomerAssets(customerId, pageLink, applyCustomersInfo, config) {
+    function getCustomerAssets(customerId, pageLink, applyCustomersInfo, config, type) {
         var deferred = $q.defer();
         var url = '/api/customer/' + customerId + '/assets?limit=' + pageLink.limit;
         if (angular.isDefined(pageLink.textSearch)) {
@@ -196,6 +200,9 @@ function AssetService($http, $q, customerService, userService) {
         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(
@@ -258,4 +265,15 @@ function AssetService($http, $q, customerService, userService) {
         return deferred.promise;
     }
 
+    function getAssetTypes() {
+        var deferred = $q.defer();
+        var url = '/api/asset/types';
+        $http.get(url).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
 }
diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js
index d12d025..b1d1bb6 100644
--- a/ui/src/app/api/device.service.js
+++ b/ui/src/app/api/device.service.js
@@ -41,12 +41,13 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         deleteDeviceAttributes: deleteDeviceAttributes,
         sendOneWayRpcCommand: sendOneWayRpcCommand,
         sendTwoWayRpcCommand: sendTwoWayRpcCommand,
-        findByQuery: findByQuery
+        findByQuery: findByQuery,
+        getDeviceTypes: getDeviceTypes
     }
 
     return service;
 
-    function getTenantDevices(pageLink, applyCustomersInfo, config) {
+    function getTenantDevices(pageLink, applyCustomersInfo, config, type) {
         var deferred = $q.defer();
         var url = '/api/tenant/devices?limit=' + pageLink.limit;
         if (angular.isDefined(pageLink.textSearch)) {
@@ -58,6 +59,9 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         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(
@@ -78,7 +82,7 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         return deferred.promise;
     }
 
-    function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config) {
+    function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config, type) {
         var deferred = $q.defer();
         var url = '/api/customer/' + customerId + '/devices?limit=' + pageLink.limit;
         if (angular.isDefined(pageLink.textSearch)) {
@@ -90,6 +94,9 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         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(
@@ -286,4 +293,15 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         return deferred.promise;
     }
 
+    function getDeviceTypes() {
+        var deferred = $q.defer();
+        var url = '/api/device/types';
+        $http.get(url).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
 }
diff --git a/ui/src/app/app.config.js b/ui/src/app/app.config.js
index fef3273..074437d 100644
--- a/ui/src/app/app.config.js
+++ b/ui/src/app/app.config.js
@@ -160,10 +160,6 @@ export default function AppConfig($provide,
             indigoTheme();
         }
 
-        $mdThemingProvider.theme('tb-search-input', 'default')
-            .primaryPalette('tb-primary')
-            .backgroundPalette('tb-primary');
-
         $mdThemingProvider.setDefaultTheme('default');
         //$mdThemingProvider.alwaysWatchTheme(true);
     }
diff --git a/ui/src/app/asset/asset.controller.js b/ui/src/app/asset/asset.controller.js
index d0944ae..1253891 100644
--- a/ui/src/app/asset/asset.controller.js
+++ b/ui/src/app/asset/asset.controller.js
@@ -47,7 +47,8 @@ export function AssetCardController(types) {
 
 
 /*@ngInject*/
-export function AssetController(userService, assetService, customerService, $state, $stateParams, $document, $mdDialog, $q, $translate, types) {
+export function AssetController($rootScope, userService, assetService, customerService, $state, $stateParams,
+                                $document, $mdDialog, $q, $translate, types) {
 
     var customerId = $stateParams.customerId;
 
@@ -129,8 +130,8 @@ export function AssetController(userService, assetService, customerService, $sta
         }
 
         if (vm.assetsScope === 'tenant') {
-            fetchAssetsFunction = function (pageLink) {
-                return assetService.getTenantAssets(pageLink, true);
+            fetchAssetsFunction = function (pageLink, assetType) {
+                return assetService.getTenantAssets(pageLink, true, null, assetType);
             };
             deleteAssetFunction = function (assetId) {
                 return assetService.deleteAsset(assetId);
@@ -229,8 +230,8 @@ export function AssetController(userService, assetService, customerService, $sta
 
 
         } else if (vm.assetsScope === 'customer' || vm.assetsScope === 'customer_user') {
-            fetchAssetsFunction = function (pageLink) {
-                return assetService.getCustomerAssets(customerId, pageLink, true);
+            fetchAssetsFunction = function (pageLink, assetType) {
+                return assetService.getCustomerAssets(customerId, pageLink, true, null, assetType);
             };
             deleteAssetFunction = function (assetId) {
                 return assetService.unassignAssetFromCustomer(assetId);
@@ -333,6 +334,7 @@ export function AssetController(userService, assetService, customerService, $sta
         var deferred = $q.defer();
         assetService.saveAsset(asset).then(
             function success(savedAsset) {
+                $rootScope.$broadcast('assetSaved');
                 var assets = [ savedAsset ];
                 customerService.applyAssignedCustomersInfo(assets).then(
                     function success(items) {
diff --git a/ui/src/app/asset/asset.directive.js b/ui/src/app/asset/asset.directive.js
index 8c13082..7110e6a 100644
--- a/ui/src/app/asset/asset.directive.js
+++ b/ui/src/app/asset/asset.directive.js
@@ -25,6 +25,7 @@ export default function AssetDirective($compile, $templateCache, toast, $transla
         var template = $templateCache.get(assetFieldsetTemplate);
         element.html(template);
 
+        scope.types = types;
         scope.isAssignedToCustomer = false;
         scope.isPublic = false;
         scope.assignedCustomer = null;
diff --git a/ui/src/app/asset/asset.routes.js b/ui/src/app/asset/asset.routes.js
index c9a312d..732f74c 100644
--- a/ui/src/app/asset/asset.routes.js
+++ b/ui/src/app/asset/asset.routes.js
@@ -20,7 +20,7 @@ import assetsTemplate from './assets.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function AssetRoutes($stateProvider) {
+export default function AssetRoutes($stateProvider, types) {
     $stateProvider
         .state('home.assets', {
             url: '/assets',
@@ -37,6 +37,8 @@ export default function AssetRoutes($stateProvider) {
             data: {
                 assetsType: 'tenant',
                 searchEnabled: true,
+                searchByEntitySubtype: true,
+                searchEntityType: types.entityType.asset,
                 pageTitle: 'asset.assets'
             },
             ncyBreadcrumb: {
@@ -58,6 +60,8 @@ export default function AssetRoutes($stateProvider) {
             data: {
                 assetsType: 'customer',
                 searchEnabled: true,
+                searchByEntitySubtype: true,
+                searchEntityType: types.entityType.asset,
                 pageTitle: 'customer.assets'
             },
             ncyBreadcrumb: {
diff --git a/ui/src/app/asset/asset-card.tpl.html b/ui/src/app/asset/asset-card.tpl.html
index 3c06558..30d0483 100644
--- a/ui/src/app/asset/asset-card.tpl.html
+++ b/ui/src/app/asset/asset-card.tpl.html
@@ -15,5 +15,8 @@
     limitations under the License.
 
 -->
-<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
-<div class="tb-small" ng-show="vm.isPublic()">{{'asset.public' | translate}}</div>
+<div flex layout="column" style="margin-top: -10px;">
+    <div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
+    <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
+    <div class="tb-small" ng-show="vm.isPublic()">{{'asset.public' | translate}}</div>
+</div>
diff --git a/ui/src/app/asset/asset-fieldset.tpl.html b/ui/src/app/asset/asset-fieldset.tpl.html
index 8cf0c96..d921b2e 100644
--- a/ui/src/app/asset/asset-fieldset.tpl.html
+++ b/ui/src/app/asset/asset-fieldset.tpl.html
@@ -56,13 +56,13 @@
                 <div translate ng-message="required">asset.name-required</div>
             </div>
         </md-input-container>
-        <md-input-container class="md-block">
-            <label translate>asset.type</label>
-            <input required name="type" ng-model="asset.type">
-            <div ng-messages="theForm.name.$error">
-                <div translate ng-message="required">asset.type-required</div>
-            </div>
-        </md-input-container>
+        <tb-entity-subtype-autocomplete
+                ng-disabled="loading || !isEdit"
+                tb-required="true"
+                the-form="theForm"
+                ng-model="asset.type"
+                entity-type="types.entityType.asset">
+        </tb-entity-subtype-autocomplete>
         <md-input-container class="md-block">
             <label translate>asset.description</label>
             <textarea ng-model="asset.additionalInfo.description" rows="2"></textarea>
diff --git a/ui/src/app/components/grid.directive.js b/ui/src/app/components/grid.directive.js
index 5664400..296456a 100644
--- a/ui/src/app/components/grid.directive.js
+++ b/ui/src/app/components/grid.directive.js
@@ -197,7 +197,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
         },
 
         getLength: function () {
-            if (vm.items.hasNext) {
+            if (vm.items.hasNext && !vm.items.pending) {
                 return vm.items.rowData.length + pageSize;
             } else {
                 return vm.items.rowData.length;
@@ -206,7 +206,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
 
         fetchMoreItems_: function () {
             if (vm.items.hasNext && !vm.items.pending) {
-                var promise = vm.fetchItemsFunc(vm.items.nextPageLink);
+                var promise = vm.fetchItemsFunc(vm.items.nextPageLink, $scope.searchConfig.searchEntitySubtype);
                 if (promise) {
                     vm.items.pending = true;
                     promise.then(
@@ -433,6 +433,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
         reload();
     });
 
+    $scope.$on('searchEntitySubtypeUpdated', function () {
+        reload();
+    });
+
     vm.onGridInited(vm);
 
     vm.itemRows.getItemAtIndex(pageSize);
@@ -441,18 +445,16 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
         if (vm.items && vm.items.pending) {
             vm.items.reloadPending = true;
         } else {
-            vm.items = {
-                data: [],
-                rowData: [],
-                nextPageLink: {
-                    limit: pageSize,
-                    textSearch: $scope.searchConfig.searchText
-                },
-                selections: {},
-                selectedCount: 0,
-                hasNext: true,
-                pending: false
+            vm.items.data.length = 0;
+            vm.items.rowData.length = 0;
+            vm.items.nextPageLink = {
+                limit: pageSize,
+                textSearch: $scope.searchConfig.searchText
             };
+            vm.items.selections = {};
+            vm.items.selectedCount = 0;
+            vm.items.hasNext = true;
+            vm.items.pending = false;
             vm.detailsConfig.isDetailsOpen = false;
             vm.items.reloadPending = false;
             vm.itemRows.getItemAtIndex(pageSize);
diff --git a/ui/src/app/components/grid.tpl.html b/ui/src/app/components/grid.tpl.html
index c29c2e5..24285d8 100644
--- a/ui/src/app/components/grid.tpl.html
+++ b/ui/src/app/components/grid.tpl.html
@@ -24,9 +24,8 @@
         <md-virtual-repeat-container ng-show="vm.hasData()" tb-scope-element="repeatContainer" id="tb-vertical-container" md-top-index="vm.topIndex" flex>
             <div class="md-padding" layout="column">
                 <section layout="row"  md-virtual-repeat="rowItem in vm.itemRows" md-on-demand md-item-size="vm.itemHeight">
-                    <div flex ng-repeat="n in [] | range:vm.columns" ng-style="{'height':vm.itemHeight+'px'}">
-                        <md-card ng-if="rowItem[n]"
-                                 ng-class="{'tb-current-item': vm.isCurrentItem(rowItem[n])}"
+                    <div flex ng-repeat="n in [] | range:vm.columns" ng-style="{'height':vm.itemHeight+'px'}" ng-if="rowItem[n]">
+                        <md-card ng-class="{'tb-current-item': vm.isCurrentItem(rowItem[n])}"
                                  class="repeated-item tb-card-item" ng-style="{'height':(vm.itemHeight-16)+'px','cursor':'pointer'}"
                                  ng-click="vm.clickItemFunc($event, rowItem[n])">
                             <section layout="row" layout-wrap>
@@ -43,7 +42,7 @@
                                 </md-card-title>
                             </section>
                             <md-card-content flex>
-                                <tb-grid-card-content grid-ctl="vm" parent-ctl="vm.parentCtl" item-controller="vm.itemCardController" item-template="vm.itemCardTemplate" item="rowItem[n]"></tb-grid-card-content>
+                                <tb-grid-card-content flex grid-ctl="vm" parent-ctl="vm.parentCtl" item-controller="vm.itemCardController" item-template="vm.itemCardTemplate" item="rowItem[n]"></tb-grid-card-content>
                             </md-card-content>
                             <md-card-actions layout="row" layout-align="end end">
                                 <md-button ng-if="action.isEnabled(rowItem[n])" ng-disabled="loading" class="md-icon-button md-primary" ng-repeat="action in vm.actionsList"
@@ -56,6 +55,8 @@
                             </md-card-actions>
                         </md-card>
                     </div>
+                    <div flex ng-repeat="n in [] | range:vm.columns" ng-style="{'height':vm.itemHeight+'px'}" ng-if="!rowItem[n]">
+                    </div>
                 </section>
             </div>
         </md-virtual-repeat-container>
diff --git a/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html b/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html
index 151c05a..9f86b9b 100644
--- a/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html
+++ b/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html
@@ -58,7 +58,7 @@
                                         {{ 'dashboard.search-states' | translate }}
                                     </md-tooltip>
                                 </md-button>
-                                <md-input-container md-theme="tb-search-input" flex>
+                                <md-input-container flex>
                                     <label>&nbsp;</label>
                                     <input ng-model="vm.query.search" placeholder="{{ 'dashboard.search-states' | translate }}"/>
                                 </md-input-container>
diff --git a/ui/src/app/device/device.controller.js b/ui/src/app/device/device.controller.js
index 55b7083..3e8c9ac 100644
--- a/ui/src/app/device/device.controller.js
+++ b/ui/src/app/device/device.controller.js
@@ -48,7 +48,8 @@ export function DeviceCardController(types) {
 
 
 /*@ngInject*/
-export function DeviceController(userService, deviceService, customerService, $state, $stateParams, $document, $mdDialog, $q, $translate, types) {
+export function DeviceController($rootScope, userService, deviceService, customerService, $state, $stateParams,
+                                 $document, $mdDialog, $q, $translate, types) {
 
     var customerId = $stateParams.customerId;
 
@@ -131,8 +132,8 @@ export function DeviceController(userService, deviceService, customerService, $s
         }
 
         if (vm.devicesScope === 'tenant') {
-            fetchDevicesFunction = function (pageLink) {
-                return deviceService.getTenantDevices(pageLink, true);
+            fetchDevicesFunction = function (pageLink, deviceType) {
+                return deviceService.getTenantDevices(pageLink, true, null, deviceType);
             };
             deleteDeviceFunction = function (deviceId) {
                 return deviceService.deleteDevice(deviceId);
@@ -242,8 +243,8 @@ export function DeviceController(userService, deviceService, customerService, $s
 
 
         } else if (vm.devicesScope === 'customer' || vm.devicesScope === 'customer_user') {
-            fetchDevicesFunction = function (pageLink) {
-                return deviceService.getCustomerDevices(customerId, pageLink, true);
+            fetchDevicesFunction = function (pageLink, deviceType) {
+                return deviceService.getCustomerDevices(customerId, pageLink, true, null, deviceType);
             };
             deleteDeviceFunction = function (deviceId) {
                 return deviceService.unassignDeviceFromCustomer(deviceId);
@@ -368,6 +369,7 @@ export function DeviceController(userService, deviceService, customerService, $s
         var deferred = $q.defer();
         deviceService.saveDevice(device).then(
             function success(savedDevice) {
+                $rootScope.$broadcast('deviceSaved');
                 var devices = [ savedDevice ];
                 customerService.applyAssignedCustomersInfo(devices).then(
                     function success(items) {
diff --git a/ui/src/app/device/device.directive.js b/ui/src/app/device/device.directive.js
index eb50a15..eda4fb2 100644
--- a/ui/src/app/device/device.directive.js
+++ b/ui/src/app/device/device.directive.js
@@ -25,6 +25,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
         var template = $templateCache.get(deviceFieldsetTemplate);
         element.html(template);
 
+        scope.types = types;
         scope.isAssignedToCustomer = false;
         scope.isPublic = false;
         scope.assignedCustomer = null;
diff --git a/ui/src/app/device/device.routes.js b/ui/src/app/device/device.routes.js
index c1f5b27..b1ca7ed 100644
--- a/ui/src/app/device/device.routes.js
+++ b/ui/src/app/device/device.routes.js
@@ -20,7 +20,7 @@ import devicesTemplate from './devices.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function DeviceRoutes($stateProvider) {
+export default function DeviceRoutes($stateProvider, types) {
     $stateProvider
         .state('home.devices', {
             url: '/devices',
@@ -37,6 +37,8 @@ export default function DeviceRoutes($stateProvider) {
             data: {
                 devicesType: 'tenant',
                 searchEnabled: true,
+                searchByEntitySubtype: true,
+                searchEntityType: types.entityType.device,
                 pageTitle: 'device.devices'
             },
             ncyBreadcrumb: {
@@ -58,6 +60,8 @@ export default function DeviceRoutes($stateProvider) {
             data: {
                 devicesType: 'customer',
                 searchEnabled: true,
+                searchByEntitySubtype: true,
+                searchEntityType: types.entityType.device,
                 pageTitle: 'customer.devices'
             },
             ncyBreadcrumb: {
diff --git a/ui/src/app/device/device-card.tpl.html b/ui/src/app/device/device-card.tpl.html
index bb84af7..58f4c85 100644
--- a/ui/src/app/device/device-card.tpl.html
+++ b/ui/src/app/device/device-card.tpl.html
@@ -15,5 +15,8 @@
     limitations under the License.
 
 -->
-<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
-<div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
+<div flex layout="column" style="margin-top: -10px;">
+    <div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
+    <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
+    <div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
+</div>
diff --git a/ui/src/app/device/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html
index fd6c9d1..767e47a 100644
--- a/ui/src/app/device/device-fieldset.tpl.html
+++ b/ui/src/app/device/device-fieldset.tpl.html
@@ -66,6 +66,13 @@
 	      		<div translate ng-message="required">device.name-required</div>
 	    	</div>				
 		</md-input-container>
+        <tb-entity-subtype-autocomplete
+                ng-disabled="loading || !isEdit"
+                tb-required="true"
+                the-form="theForm"
+                ng-model="device.type"
+                entity-type="types.entityType.device">
+        </tb-entity-subtype-autocomplete>
         <md-input-container class="md-block">
             <md-checkbox ng-disabled="loading || !isEdit" flex aria-label="{{ 'device.is-gateway' | translate }}"
                          ng-model="device.additionalInfo.gateway">{{ 'device.is-gateway' | translate }}
diff --git a/ui/src/app/entity/attribute/attribute-table.tpl.html b/ui/src/app/entity/attribute/attribute-table.tpl.html
index bf3b8cd..3b10902 100644
--- a/ui/src/app/entity/attribute/attribute-table.tpl.html
+++ b/ui/src/app/entity/attribute/attribute-table.tpl.html
@@ -63,7 +63,7 @@
                         {{ 'action.search' | translate }}
                     </md-tooltip>
                 </md-button>
-                <md-input-container md-theme="tb-search-input" flex>
+                <md-input-container flex>
                     <label>&nbsp;</label>
                     <input ng-model="query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
                 </md-input-container>
diff --git a/ui/src/app/entity/entity-subtype-autocomplete.directive.js b/ui/src/app/entity/entity-subtype-autocomplete.directive.js
new file mode 100644
index 0000000..98110b0
--- /dev/null
+++ b/ui/src/app/entity/entity-subtype-autocomplete.directive.js
@@ -0,0 +1,141 @@
+/*
+ * 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 './entity-subtype-autocomplete.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entitySubtypeAutocompleteTemplate from './entity-subtype-autocomplete.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function EntitySubtypeAutocomplete($compile, $templateCache, $q, $filter, assetService, deviceService, types) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(entitySubtypeAutocompleteTemplate);
+        element.html(template);
+
+        scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+        scope.subType = null;
+        scope.subTypeSearchText = '';
+        scope.entitySubtypes = null;
+
+        scope.fetchSubTypes = function(searchText) {
+            var deferred = $q.defer();
+            loadSubTypes().then(
+                function success(subTypes) {
+                    var result = $filter('filter')(subTypes, {'$': searchText});
+                    if (result && result.length) {
+                        deferred.resolve(result);
+                    } else {
+                        deferred.resolve([searchText]);
+                    }
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+            return deferred.promise;
+        }
+
+        scope.subTypeSearchTextChanged = function() {
+        }
+
+        scope.updateView = function () {
+            if (!scope.disabled) {
+                ngModelCtrl.$setViewValue(scope.subType);
+            }
+        }
+
+        ngModelCtrl.$render = function () {
+            scope.subType = ngModelCtrl.$viewValue;
+        }
+
+        scope.$watch('entityType', function () {
+            load();
+        });
+
+        scope.$watch('subType', function (newValue, prevValue) {
+            if (!angular.equals(newValue, prevValue)) {
+                scope.updateView();
+            }
+        });
+
+        scope.$watch('disabled', function () {
+            scope.updateView();
+        });
+
+        function loadSubTypes() {
+            var deferred = $q.defer();
+            if (!scope.entitySubtypes) {
+                var entitySubtypesPromise;
+                if (scope.entityType == types.entityType.asset) {
+                    entitySubtypesPromise = assetService.getAssetTypes();
+                } else if (scope.entityType == types.entityType.device) {
+                    entitySubtypesPromise = deviceService.getDeviceTypes();
+                }
+                if (entitySubtypesPromise) {
+                    entitySubtypesPromise.then(
+                        function success(types) {
+                            scope.entitySubtypes = [];
+                            types.forEach(function (type) {
+                                scope.entitySubtypes.push(type.type);
+                            });
+                            deferred.resolve(scope.entitySubtypes);
+                        },
+                        function fail() {
+                            deferred.reject();
+                        }
+                    );
+                } else {
+                    deferred.reject();
+                }
+            } else {
+                deferred.resolve(scope.entitySubtypes);
+            }
+            return deferred.promise;
+        }
+
+        function load() {
+            if (scope.entityType == types.entityType.asset) {
+                scope.selectEntitySubtypeText = 'asset.select-asset-type';
+                scope.entitySubtypeText = 'asset.asset-type';
+                scope.entitySubtypeRequiredText = 'asset.asset-type-required';
+            } else if (scope.entityType == types.entityType.device) {
+                scope.selectEntitySubtypeText = 'device.select-device-type';
+                scope.entitySubtypeText = 'device.device-type';
+                scope.entitySubtypeRequiredText = 'device.device-type-required';
+                scope.$on('deviceSaved', function() {
+                    scope.entitySubtypes = null;
+                });
+            }
+        }
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            theForm: '=?',
+            tbRequired: '=?',
+            disabled:'=ngDisabled',
+            entityType: "="
+        }
+    };
+}
diff --git a/ui/src/app/entity/entity-subtype-autocomplete.tpl.html b/ui/src/app/entity/entity-subtype-autocomplete.tpl.html
new file mode 100644
index 0000000..ce220ee
--- /dev/null
+++ b/ui/src/app/entity/entity-subtype-autocomplete.tpl.html
@@ -0,0 +1,41 @@
+<!--
+
+    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.
+
+-->
+<md-autocomplete ng-required="tbRequired"
+                 ng-disabled="disabled"
+                 md-no-cache="true"
+                 md-input-name="subType"
+                 ng-model="subType"
+                 md-selected-item="subType"
+                 md-search-text="subTypeSearchText"
+                 md-search-text-change="subTypeSearchTextChanged()"
+                 md-items="item in fetchSubTypes(subTypeSearchText)"
+                 md-item-text="item"
+                 md-min-length="0"
+                 placeholder="{{ selectEntitySubtypeText | translate }}"
+                 md-floating-label="{{ entitySubtypeText | translate }}"
+                 md-select-on-match="true"
+                 md-menu-class="tb-entity-subtype-autocomplete">
+    <md-item-template>
+        <div class="tb-entity-subtype-item">
+            <span md-highlight-text="subTypeSearchText" md-highlight-flags="^i">{{item}}</span>
+        </div>
+    </md-item-template>
+    <div ng-messages="theForm.subType.$error">
+        <div translate ng-message="required">{{ entitySubtypeRequiredText }}</div>
+    </div>
+</md-autocomplete>
diff --git a/ui/src/app/entity/entity-subtype-select.directive.js b/ui/src/app/entity/entity-subtype-select.directive.js
new file mode 100644
index 0000000..4a2d33e
--- /dev/null
+++ b/ui/src/app/entity/entity-subtype-select.directive.js
@@ -0,0 +1,138 @@
+/*
+ * 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 './entity-subtype-select.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entitySubtypeSelectTemplate from './entity-subtype-select.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function EntitySubtypeSelect($compile, $templateCache, $translate, assetService, deviceService, types) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(entitySubtypeSelectTemplate);
+        element.html(template);
+
+        if (angular.isDefined(attrs.hideLabel)) {
+            scope.showLabel = false;
+        } else {
+            scope.showLabel = true;
+        }
+
+        scope.ngModelCtrl = ngModelCtrl;
+
+        scope.entitySubtypes = [];
+
+        scope.subTypeName = function(subType) {
+            if (subType && subType.length) {
+                if (scope.typeTranslatePrefix) {
+                    return $translate.instant(scope.typeTranslatePrefix + '.' + subType);
+                } else {
+                    return subType;
+                }
+            } else {
+                return $translate.instant('entity.all-subtypes');
+            }
+        }
+
+        scope.$watch('entityType', function () {
+            load();
+        });
+
+        scope.$watch('entitySubtype', function (newValue, prevValue) {
+            if (!angular.equals(newValue, prevValue)) {
+                scope.updateView();
+            }
+        });
+
+        scope.updateView = function () {
+            ngModelCtrl.$setViewValue(scope.entitySubtype);
+        };
+
+        ngModelCtrl.$render = function () {
+            scope.entitySubtype = ngModelCtrl.$viewValue;
+        };
+
+        function loadSubTypes() {
+            scope.entitySubtypes.length = 0;
+            var entitySubtypesPromise;
+            if (scope.entityType == types.entityType.asset) {
+                entitySubtypesPromise = assetService.getAssetTypes();
+            } else if (scope.entityType == types.entityType.device) {
+                entitySubtypesPromise = deviceService.getDeviceTypes();
+            }
+            if (entitySubtypesPromise) {
+                entitySubtypesPromise.then(
+                    function success(types) {
+                        scope.entitySubtypes.push('');
+                        types.forEach(function(type) {
+                            scope.entitySubtypes.push(type.type);
+                        });
+                        if (scope.entitySubtypes.indexOf(scope.entitySubtype) == -1) {
+                            scope.entitySubtype = '';
+                        }
+                    },
+                    function fail() {}
+                );
+            }
+
+        }
+
+        function load() {
+            if (scope.entityType == types.entityType.asset) {
+                scope.entitySubtypeTitle = 'asset.asset-type';
+                scope.entitySubtypeRequiredText = 'asset.asset-type-required';
+            } else if (scope.entityType == types.entityType.device) {
+                scope.entitySubtypeTitle = 'device.device-type';
+                scope.entitySubtypeRequiredText = 'device.device-type-required';
+            }
+            scope.entitySubtypes.length = 0;
+            if (scope.entitySubtypesList && scope.entitySubtypesList.length) {
+                scope.entitySubtypesList.forEach(function(subType) {
+                    scope.entitySubtypes.push(subType);
+                });
+            } else {
+                loadSubTypes();
+                if (scope.entityType == types.entityType.asset) {
+                    scope.$on('assetSaved', function() {
+                        loadSubTypes();
+                    });
+                } else if (scope.entityType == types.entityType.device) {
+                    scope.$on('deviceSaved', function() {
+                        loadSubTypes();
+                    });
+                }
+            }
+        }
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            theForm: '=?',
+            entityType: "=",
+            entitySubtypesList: "=?",
+            typeTranslatePrefix: "@?"
+        }
+    };
+}
diff --git a/ui/src/app/entity/index.js b/ui/src/app/entity/index.js
index 07c0862..8423f11 100644
--- a/ui/src/app/entity/index.js
+++ b/ui/src/app/entity/index.js
@@ -16,6 +16,8 @@
 
 import EntityAliasesController from './entity-aliases.controller';
 import EntityTypeSelectDirective from './entity-type-select.directive';
+import EntitySubtypeSelectDirective from './entity-subtype-select.directive';
+import EntitySubtypeAutocompleteDirective from './entity-subtype-autocomplete.directive';
 import EntityFilterDirective from './entity-filter.directive';
 import AliasesEntitySelectPanelController from './aliases-entity-select-panel.controller';
 import AliasesEntitySelectDirective from './aliases-entity-select.directive';
@@ -29,6 +31,8 @@ export default angular.module('thingsboard.entity', [])
     .controller('AddAttributeDialogController', AddAttributeDialogController)
     .controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController)
     .directive('tbEntityTypeSelect', EntityTypeSelectDirective)
+    .directive('tbEntitySubtypeSelect', EntitySubtypeSelectDirective)
+    .directive('tbEntitySubtypeAutocomplete', EntitySubtypeAutocompleteDirective)
     .directive('tbEntityFilter', EntityFilterDirective)
     .directive('tbAliasesEntitySelect', AliasesEntitySelectDirective)
     .directive('tbAttributeTable', AttributeTableDirective)
diff --git a/ui/src/app/layout/home.controller.js b/ui/src/app/layout/home.controller.js
index 4979501..5091da8 100644
--- a/ui/src/app/layout/home.controller.js
+++ b/ui/src/app/layout/home.controller.js
@@ -25,7 +25,7 @@ import logoSvg from '../../svg/logo_title_white.svg';
 /* eslint-disable angular/angularelement */
 
 /*@ngInject*/
-export default function HomeController(loginService, userService, deviceService, Fullscreen, $scope, $element, $rootScope, $document, $state,
+export default function HomeController(types, loginService, userService, deviceService, Fullscreen, $scope, $element, $rootScope, $document, $state,
                                        $window, $log, $mdMedia, $animate, $timeout) {
 
     var siteSideNav = $('.tb-site-sidenav', $element);
@@ -38,8 +38,11 @@ export default function HomeController(loginService, userService, deviceService,
     if (angular.isUndefined($rootScope.searchConfig)) {
         $rootScope.searchConfig = {
             searchEnabled: false,
+            searchByEntitySubtype: false,
+            searchEntityType: null,
             showSearch: false,
-            searchText: ""
+            searchText: "",
+            searchEntitySubtype: ""
         };
     }
 
@@ -47,6 +50,7 @@ export default function HomeController(loginService, userService, deviceService,
     vm.isLockSidenav = false;
 
     vm.displaySearchMode = displaySearchMode;
+    vm.displayEntitySubtypeSearch = displayEntitySubtypeSearch;
     vm.openSidenav = openSidenav;
     vm.goBack = goBack;
     vm.searchTextUpdated = searchTextUpdated;
@@ -54,25 +58,35 @@ export default function HomeController(loginService, userService, deviceService,
     vm.toggleFullscreen = toggleFullscreen;
 
     $scope.$on('$stateChangeSuccess', function (evt, to, toParams, from) {
+        watchEntitySubtype(false);
         if (angular.isDefined(to.data.searchEnabled)) {
             $scope.searchConfig.searchEnabled = to.data.searchEnabled;
+            $scope.searchConfig.searchByEntitySubtype = to.data.searchByEntitySubtype;
+            $scope.searchConfig.searchEntityType = to.data.searchEntityType;
             if ($scope.searchConfig.searchEnabled === false || to.name !== from.name) {
                 $scope.searchConfig.showSearch = false;
                 $scope.searchConfig.searchText = "";
+                $scope.searchConfig.searchEntitySubtype = "";
             }
         } else {
             $scope.searchConfig.searchEnabled = false;
+            $scope.searchConfig.searchByEntitySubtype = false;
+            $scope.searchConfig.searchEntityType = null;
             $scope.searchConfig.showSearch = false;
             $scope.searchConfig.searchText = "";
+            $scope.searchConfig.searchEntitySubtype = "";
         }
+        watchEntitySubtype($scope.searchConfig.searchByEntitySubtype);
     });
 
-    if ($mdMedia('gt-sm')) {
+    vm.isGtSm = $mdMedia('gt-sm');
+    if (vm.isGtSm) {
         vm.isLockSidenav = true;
         $animate.enabled(siteSideNav, false);
     }
 
     $scope.$watch(function() { return $mdMedia('gt-sm'); }, function(isGtSm) {
+        vm.isGtSm = isGtSm;
         vm.isLockSidenav = isGtSm;
         vm.isShowSidenav = isGtSm;
         if (!isGtSm) {
@@ -84,11 +98,28 @@ export default function HomeController(loginService, userService, deviceService,
         }
     });
 
+    function watchEntitySubtype(enableWatch) {
+        if ($scope.entitySubtypeWatch) {
+            $scope.entitySubtypeWatch();
+        }
+        if (enableWatch) {
+            $scope.entitySubtypeWatch = $scope.$watch('searchConfig.searchEntitySubtype', function (newVal, prevVal) {
+                if (!angular.equals(newVal, prevVal)) {
+                    $scope.$broadcast('searchEntitySubtypeUpdated');
+                }
+            });
+        }
+    }
+
     function displaySearchMode() {
         return $scope.searchConfig.searchEnabled &&
             $scope.searchConfig.showSearch;
     }
 
+    function displayEntitySubtypeSearch() {
+        return $scope.searchConfig.searchByEntitySubtype && vm.isGtSm;
+    }
+
     function toggleFullscreen() {
         if (Fullscreen.isEnabled()) {
             Fullscreen.cancel();
diff --git a/ui/src/app/layout/home.scss b/ui/src/app/layout/home.scss
index edcdbc8..a809cf6 100644
--- a/ui/src/app/layout/home.scss
+++ b/ui/src/app/layout/home.scss
@@ -70,3 +70,11 @@ md-icon.tb-logo-title {
   z-index: 2;
   white-space: nowrap;
 }
+
+.tb-entity-subtype-search {
+  margin-top: 15px;
+}
+
+.tb-entity-search {
+  margin-top: 34px;
+}
diff --git a/ui/src/app/layout/home.tpl.html b/ui/src/app/layout/home.tpl.html
index bfb37eb..8e642eb 100644
--- a/ui/src/app/layout/home.tpl.html
+++ b/ui/src/app/layout/home.tpl.html
@@ -39,7 +39,7 @@
   </md-sidenav>
 
   <div flex layout="column" tabIndex="-1" role="main">
-    <md-toolbar class="md-whiteframe-z1 tb-primary-toolbar" ng-class="{'md-hue-1': vm.displaySearchMode()}">
+    <md-toolbar class="md-whiteframe-z1 tb-primary-toolbar">
     	<div layout="row" flex class="md-toolbar-tools">
 		      <md-button id="main" hide-gt-sm ng-show="!forceFullscreen"
 		      		class="md-icon-button" ng-click="vm.openSidenav()" aria-label="{{ 'home.menu' | translate }}" ng-class="{'tb-invisible': vm.displaySearchMode()}">
@@ -55,10 +55,18 @@
 			  <div flex layout="row" ng-show="!vm.displaySearchMode()" tb-no-animate class="md-toolbar-tools">
 				  <span ng-cloak ncy-breadcrumb></span>
 			  </div>
-			  <md-input-container ng-show="vm.displaySearchMode()" md-theme="tb-search-input" flex>
-	              <label>&nbsp;</label>
-	              <input ng-model="searchConfig.searchText" ng-change="vm.searchTextUpdated()" placeholder="{{ 'common.enter-search' | translate }}"/>
-	          </md-input-container>		      
+			  <div layout="row" ng-show="vm.displaySearchMode()" md-theme="tb-dark" flex>
+				  <div class="tb-entity-subtype-search" layout="row" layout-align="start center" ng-if="vm.displayEntitySubtypeSearch()">
+					  <tb-entity-subtype-select
+							  entity-type="searchConfig.searchEntityType"
+							  ng-model="searchConfig.searchEntitySubtype">
+					  </tb-entity-subtype-select>
+				  </div>
+				  <md-input-container ng-class="{'tb-entity-search': vm.displayEntitySubtypeSearch()}" flex>
+					  <label>&nbsp;</label>
+					  <input ng-model="searchConfig.searchText" ng-change="vm.searchTextUpdated()" placeholder="{{ 'common.enter-search' | translate }}"/>
+				  </md-input-container>
+			  </div>
 		      <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}" ng-show="searchConfig.searchEnabled" ng-click="searchConfig.showSearch = !searchConfig.showSearch">
 		      	  <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
           	  </md-button>
diff --git a/ui/src/app/layout/user-menu.directive.js b/ui/src/app/layout/user-menu.directive.js
index 6d09e67..53e8c45 100644
--- a/ui/src/app/layout/user-menu.directive.js
+++ b/ui/src/app/layout/user-menu.directive.js
@@ -48,16 +48,10 @@ function UserMenuController($scope, userService, $translate, $state) {
     var dashboardUser = userService.getCurrentUser();
 
     vm.authorityName = authorityName;
-    vm.displaySearchMode = displaySearchMode;
     vm.logout = logout;
     vm.openProfile = openProfile;
     vm.userDisplayName = userDisplayName;
 
-    function displaySearchMode() {
-        return $scope.searchConfig.searchEnabled &&
-            $scope.searchConfig.showSearch;
-    }
-
     function authorityName() {
         var name = "user.anonymous";
         if (dashboardUser) {
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 622754c..50f6d8f 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -124,6 +124,9 @@ export default angular.module('thingsboard.locale', [])
                     "unassign-from-customer": "Unassign from customer",
                     "delete": "Delete asset",
                     "asset-public": "Asset is public",
+                    "asset-type": "Asset type",
+                    "asset-type-required": "Asset type is required.",
+                    "select-asset-type": "Select asset type",
                     "name": "Name",
                     "name-required": "Name is required.",
                     "description": "Description",
@@ -477,6 +480,9 @@ export default angular.module('thingsboard.locale', [])
                     "rsa-key-required": "RSA public key is required.",
                     "secret": "Secret",
                     "secret-required": "Secret is required.",
+                    "device-type": "Device type",
+                    "device-type-required": "Device type is required.",
+                    "select-device-type": "Select device type",
                     "name": "Name",
                     "name-required": "Name is required.",
                     "description": "Description",
@@ -521,6 +527,7 @@ export default angular.module('thingsboard.locale', [])
                     "entity-list-empty": "No entities selected.",
                     "entity-name-filter-required": "Entity name filter is required.",
                     "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.",
+                    "all-subtypes": "All",
                     "type": "Type",
                     "type-device": "Device",
                     "type-asset": "Asset",