From 9cdfe4ef54e38e4a0b962665abae1bef431a548d Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 12 Jun 2017 12:59:51 +0300 Subject: [PATCH] TB-61: Implemented new alias filters. --- .../controller/EntityRelationController.java | 15 ++ .../dao/relation/BaseRelationService.java | 26 +- .../server/dao/relation/RelationService.java | 2 + ui/src/app/api/entity-relation.service.js | 14 +- ui/src/app/api/entity.service.js | 249 +++++++++++++++++- ui/src/app/common/types.constant.js | 9 + .../components/datasource-func.directive.js | 8 + ui/src/app/components/datasource-func.scss | 38 +-- .../app/components/datasource-func.tpl.html | 111 ++++---- ui/src/app/dashboard/add-widget.controller.js | 2 +- ui/src/app/dashboard/dashboard.controller.js | 2 +- ui/src/app/dashboard/edit-widget.directive.js | 2 +- .../aliases-entity-select-button.tpl.html | 0 .../aliases-entity-select-panel.controller.js | 0 .../aliases-entity-select-panel.tpl.html | 0 .../aliases-entity-select.directive.js | 0 .../{ => alias}/aliases-entity-select.scss | 0 .../entity-alias-dialog.controller.js | 0 .../{ => alias}/entity-alias-dialog.scss | 0 .../{ => alias}/entity-alias-dialog.tpl.html | 0 .../{ => alias}/entity-aliases.controller.js | 0 .../entity/{ => alias}/entity-aliases.scss | 0 .../{ => alias}/entity-aliases.tpl.html | 0 .../entity/entity-filter-view.directive.js | 101 ++++++- ui/src/app/entity/entity-filter.directive.js | 18 +- ui/src/app/entity/entity-filter.scss | 17 ++ ui/src/app/entity/entity-filter.tpl.html | 142 ++++++++++ .../entity-subtype-autocomplete.directive.js | 3 + .../entity/entity-subtype-list.directive.js | 146 ++++++++++ ui/src/app/entity/entity-subtype-list.scss | 30 +++ .../app/entity/entity-subtype-list.tpl.html | 54 ++++ .../app/entity/entity-type-list.directive.js | 111 ++++++++ ui/src/app/entity/entity-type-list.scss | 30 +++ ui/src/app/entity/entity-type-list.tpl.html | 54 ++++ .../entity/entity-type-select.directive.js | 33 +-- ui/src/app/entity/index.js | 14 +- .../relation/relation-filters.directive.js | 85 ++++++ .../app/entity/relation/relation-filters.scss | 77 ++++++ .../entity/relation/relation-filters.tpl.html | 67 +++++ .../relation-type-autocomplete.directive.js | 2 + .../relation-type-autocomplete.tpl.html | 2 +- .../import-export/import-export.service.js | 2 +- ui/src/app/locale/locale.constant.js | 50 +++- ui/src/scss/main.scss | 9 + 44 files changed, 1399 insertions(+), 126 deletions(-) rename ui/src/app/entity/{ => alias}/aliases-entity-select-button.tpl.html (100%) rename ui/src/app/entity/{ => alias}/aliases-entity-select-panel.controller.js (100%) rename ui/src/app/entity/{ => alias}/aliases-entity-select-panel.tpl.html (100%) rename ui/src/app/entity/{ => alias}/aliases-entity-select.directive.js (100%) rename ui/src/app/entity/{ => alias}/aliases-entity-select.scss (100%) rename ui/src/app/entity/{ => alias}/entity-alias-dialog.controller.js (100%) rename ui/src/app/entity/{ => alias}/entity-alias-dialog.scss (100%) rename ui/src/app/entity/{ => alias}/entity-alias-dialog.tpl.html (100%) rename ui/src/app/entity/{ => alias}/entity-aliases.controller.js (100%) rename ui/src/app/entity/{ => alias}/entity-aliases.scss (100%) rename ui/src/app/entity/{ => alias}/entity-aliases.tpl.html (100%) create mode 100644 ui/src/app/entity/entity-subtype-list.directive.js create mode 100644 ui/src/app/entity/entity-subtype-list.scss create mode 100644 ui/src/app/entity/entity-subtype-list.tpl.html create mode 100644 ui/src/app/entity/entity-type-list.directive.js create mode 100644 ui/src/app/entity/entity-type-list.scss create mode 100644 ui/src/app/entity/entity-type-list.tpl.html create mode 100644 ui/src/app/entity/relation/relation-filters.directive.js create mode 100644 ui/src/app/entity/relation/relation-filters.scss create mode 100644 ui/src/app/entity/relation/relation-filters.tpl.html diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java index 3ddc5975a7..4aa1a0a5cb 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java @@ -250,6 +250,21 @@ public class EntityRelationController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/relations/info", method = RequestMethod.POST) + @ResponseBody + public List findInfoByQuery(@RequestBody EntityRelationsQuery query) throws ThingsboardException { + checkNotNull(query); + checkNotNull(query.getParameters()); + checkNotNull(query.getFilters()); + checkEntityId(query.getParameters().getEntityId()); + try { + return checkNotNull(relationService.findInfoByQuery(query).get()); + } catch (Exception e) { + throw handleException(e); + } + } + private RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) { RelationTypeGroup result = defaultValue; if (strRelationTypeGroup != null && strRelationTypeGroup.trim().length()>0) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index 36ec56735b..296874e20f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -191,7 +191,7 @@ public class BaseRelationService implements RelationService { @Override public ListenableFuture> findByQuery(EntityRelationsQuery query) { - log.trace("Executing findByQuery [{}][{}]", query); + log.trace("Executing findByQuery [{}]", query); RelationsSearchParameters params = query.getParameters(); final List filters = query.getFilters(); if (filters == null || filters.isEmpty()) { @@ -224,6 +224,30 @@ public class BaseRelationService implements RelationService { } } + @Override + public ListenableFuture> findInfoByQuery(EntityRelationsQuery query) { + log.trace("Executing findInfoByQuery [{}]", query); + ListenableFuture> relations = findByQuery(query); + EntitySearchDirection direction = query.getParameters().getDirection(); + ListenableFuture> relationsInfo = Futures.transform(relations, + (AsyncFunction, List>) relations1 -> { + List> futures = new ArrayList<>(); + relations1.stream().forEach(relation -> + futures.add(fetchRelationInfoAsync(relation, + relation2 -> direction == EntitySearchDirection.FROM ? relation2.getTo() : relation2.getFrom(), + (EntityRelationInfo relationInfo, String entityName) -> { + if (direction == EntitySearchDirection.FROM) { + relationInfo.setToName(entityName); + } else { + relationInfo.setFromName(entityName); + } + })) + ); + return Futures.successfulAsList(futures); + }); + return relationsInfo; + } + protected void validate(EntityRelation relation) { if (relation == null) { throw new DataValidationException("Relation type should be specified!"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java index a810454f2a..bd2e785a5b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java @@ -52,6 +52,8 @@ public interface RelationService { ListenableFuture> findByQuery(EntityRelationsQuery query); + ListenableFuture> findInfoByQuery(EntityRelationsQuery query); + // TODO: This method may be useful for some validations in the future // ListenableFuture checkRecursiveRelation(EntityId from, EntityId to); diff --git a/ui/src/app/api/entity-relation.service.js b/ui/src/app/api/entity-relation.service.js index 875b2fac38..351c252c56 100644 --- a/ui/src/app/api/entity-relation.service.js +++ b/ui/src/app/api/entity-relation.service.js @@ -30,7 +30,8 @@ function EntityRelationService($http, $q) { findByTo: findByTo, findInfoByTo: findInfoByTo, findByToAndType: findByToAndType, - findByQuery: findByQuery + findByQuery: findByQuery, + findInfoByQuery: findInfoByQuery } return service; @@ -159,4 +160,15 @@ function EntityRelationService($http, $q) { return deferred.promise; } + function findInfoByQuery(query) { + var deferred = $q.defer(); + var url = '/api/relations/info'; + $http.post(url, query).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + } diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js index e647d9cfb2..20157ce918 100644 --- a/ui/src/app/api/entity.service.js +++ b/ui/src/app/api/entity.service.js @@ -32,6 +32,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device checkEntityAlias: checkEntityAlias, filterAliasByEntityTypes: filterAliasByEntityTypes, getAliasFilterTypesByEntityTypes: getAliasFilterTypesByEntityTypes, + prepareAllowedEntityTypesList: prepareAllowedEntityTypesList, getEntityKeys: getEntityKeys, createDatasourcesFromSubscriptionsInfo: createDatasourcesFromSubscriptionsInfo, getRelatedEntities: getRelatedEntities, @@ -176,6 +177,54 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device return deferred.promise; } + function getSingleTenantByPageLinkPromise(pageLink) { + var user = userService.getCurrentUser(); + var tenantId = user.tenantId; + var deferred = $q.defer(); + tenantService.getTenant(tenantId).then( + function success(tenant) { + var tenantName = tenant.name; + var result = { + data: [], + nextPageLink: pageLink, + hasNext: false + }; + if (tenantName.toLowerCase().startsWith(pageLink.textSearch)) { + result.data.push(tenant); + } + deferred.resolve(result); + }, + function fail() { + deferred.reject(); + } + ); + return deferred.promise; + } + + function getSingleCustomerByPageLinkPromise(pageLink) { + var user = userService.getCurrentUser(); + var customerId = user.customerId; + var deferred = $q.defer(); + customerService.getCustomer(customerId).then( + function success(customer) { + var customerName = customer.name; + var result = { + data: [], + nextPageLink: pageLink, + hasNext: false + }; + if (customerName.toLowerCase().startsWith(pageLink.textSearch)) { + result.data.push(customer); + } + deferred.resolve(result); + }, + function fail() { + deferred.reject(); + } + ); + return deferred.promise; + } + function getEntitiesByPageLinkPromise(entityType, pageLink, config, subType) { var promise; var user = userService.getCurrentUser(); @@ -196,10 +245,18 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device } break; case types.entityType.tenant: - promise = tenantService.getTenants(pageLink); + if (user.authority === 'TENANT_ADMIN') { + promise = getSingleTenantByPageLinkPromise(pageLink); + } else { + promise = tenantService.getTenants(pageLink); + } break; case types.entityType.customer: - promise = customerService.getCustomers(pageLink); + if (user.authority === 'CUSTOMER_USER') { + promise = getSingleCustomerByPageLinkPromise(pageLink); + } else { + promise = customerService.getCustomers(pageLink); + } break; case types.entityType.rule: promise = ruleService.getAllRules(pageLink); @@ -283,6 +340,16 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device return { name: entity.name, entityType: entity.id.entityType, id: entity.id.id }; } + function entityRelationInfoToEntityInfo(entityRelationInfo, direction) { + var entityId = direction == types.entitySearchDirection.from ? entityRelationInfo.to : entityRelationInfo.from; + var name = direction == types.entitySearchDirection.from ? entityRelationInfo.toName : entityRelationInfo.fromName; + return { + name: name, + entityType: entityId.entityType, + id: entityId.id + }; + } + function entitiesToEntitiesInfo(entities) { var entitiesInfo = []; for (var d = 0; d < entities.length; d++) { @@ -291,19 +358,26 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device return entitiesInfo; } + function entityRelationInfosToEntitiesInfo(entityRelations, direction) { + var entitiesInfo = []; + for (var d = 0; d < entityRelations.length; d++) { + entitiesInfo.push(entityRelationInfoToEntityInfo(entityRelations[d], direction)); + } + return entitiesInfo; + } + + function resolveAlias(entityAlias, stateParams) { var deferred = $q.defer(); var filter = entityAlias.filter; resolveAliasFilter(filter, stateParams, -1).then( function (result) { - var entities = result.entities; var aliasInfo = { alias: entityAlias.alias, stateEntity: result.stateEntity, resolveMultiple: filter.resolveMultiple }; - var resolvedEntities = entitiesToEntitiesInfo(entities); - aliasInfo.resolvedEntities = resolvedEntities; + aliasInfo.resolvedEntities = result.entities; aliasInfo.currentEntity = null; if (aliasInfo.resolvedEntities.length) { aliasInfo.currentEntity = aliasInfo.resolvedEntities[0]; @@ -328,7 +402,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device getEntities(filter.entityType, filter.entityList).then( function success(entities) { if (entities && entities.length) { - result.entities = entities; + result.entities = entitiesToEntitiesInfo(entities); deferred.resolve(result); } else { deferred.reject(); @@ -343,7 +417,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device getEntitiesByNameFilter(filter.entityType, filter.entityNameFilter, maxItems).then( function success(entities) { if (entities && entities.length) { - result.entities = entities; + result.entities = entitiesToEntitiesInfo(entities); deferred.resolve(result); } else { deferred.reject(); @@ -359,7 +433,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device if (stateParams && stateParams.entityId) { getEntity(stateParams.entityId.entityType, stateParams.entityId.id).then( function success(entity) { - result.entities = [entity]; + result.entities = entitiesToEntitiesInfo([entity]); deferred.resolve(result); }, function fail() { @@ -374,7 +448,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device getEntitiesByNameFilter(types.entityType.asset, filter.assetNameFilter, maxItems, null, filter.assetType).then( function success(entities) { if (entities && entities.length) { - result.entities = entities; + result.entities = entitiesToEntitiesInfo(entities); deferred.resolve(result); } else { deferred.reject(); @@ -389,7 +463,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device getEntitiesByNameFilter(types.entityType.device, filter.deviceNameFilter, maxItems, null, filter.deviceType).then( function success(entities) { if (entities && entities.length) { - result.entities = entities; + result.entities = entitiesToEntitiesInfo(entities); deferred.resolve(result); } else { deferred.reject(); @@ -400,8 +474,97 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device } ); break; - - //TODO: Alias filter + case types.aliasFilterType.relationsQuery.value: + result.stateEntity = filter.rootStateEntity; + var rootEntityType; + var rootEntityId; + if (result.stateEntity && stateParams && stateParams.entityId) { + rootEntityType = stateParams.entityId.entityType; + rootEntityId = stateParams.entityId.id; + } else if (!result.stateEntity) { + rootEntityType = filter.rootEntity.entityType; + rootEntityId = filter.rootEntity.id; + } + if (rootEntityType && rootEntityId) { + var searchQuery = { + parameters: { + rootId: rootEntityId, + rootType: rootEntityType, + direction: filter.direction + }, + filters: filter.filters + }; + searchQuery.parameters.maxLevel = filter.maxLevel && filter.maxLevel > 0 ? filter.maxLevel : -1; + entityRelationService.findInfoByQuery(searchQuery).then( + function success(allRelations) { + if (allRelations && allRelations.length) { + if (angular.isDefined(maxItems) && maxItems > 0) { + var limit = Math.min(allRelations.length, maxItems); + allRelations.length = limit; + } + result.entities = entityRelationInfosToEntitiesInfo(allRelations, filter.direction); + deferred.resolve(result); + } else { + deferred.reject(); + } + }, + function fail() { + deferred.reject(); + } + ); + } else { + deferred.resolve(result); + } + break; + case types.aliasFilterType.assetSearchQuery.value: + case types.aliasFilterType.deviceSearchQuery.value: + result.stateEntity = filter.rootStateEntity; + if (result.stateEntity && stateParams && stateParams.entityId) { + rootEntityType = stateParams.entityId.entityType; + rootEntityId = stateParams.entityId.id; + } else if (!result.stateEntity) { + rootEntityType = filter.rootEntity.entityType; + rootEntityId = filter.rootEntity.id; + } + if (rootEntityType && rootEntityId) { + searchQuery = { + parameters: { + rootId: rootEntityId, + rootType: rootEntityType, + direction: filter.direction + }, + relationType: filter.relationType + }; + searchQuery.parameters.maxLevel = filter.maxLevel && filter.maxLevel > 0 ? filter.maxLevel : -1; + var findByQueryPromise; + if (filter.type == types.aliasFilterType.assetSearchQuery.value) { + searchQuery.assetTypes = filter.assetTypes; + findByQueryPromise = assetService.findByQuery(searchQuery, false); + } else if (filter.type == types.aliasFilterType.deviceSearchQuery.value) { + searchQuery.deviceTypes = filter.deviceTypes; + findByQueryPromise = deviceService.findByQuery(searchQuery, false); + } + findByQueryPromise.then( + function success(entities) { + if (entities && entities.length) { + if (angular.isDefined(maxItems) && maxItems > 0) { + var limit = Math.min(entities.length, maxItems); + entities.length = limit; + } + result.entities = entitiesToEntitiesInfo(entities); + deferred.resolve(result); + } else { + deferred.reject(); + } + }, + function fail() { + deferred.reject(); + } + ); + } else { + deferred.resolve(result); + } + break; } return deferred.promise; } @@ -420,9 +583,33 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false; case types.aliasFilterType.deviceType.value: return entityTypes.indexOf(types.entityType.device) > -1 ? true : false; + case types.aliasFilterType.relationsQuery.value: + if (filter.filters && filter.filters.length) { + var match = false; + for (var f=0;f -1) { + match = true; + break; + } + } + } else { + match = true; + break; + } + } + return match; + } else { + return true; + } + case types.aliasFilterType.assetSearchQuery.value: + return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false; + case types.aliasFilterType.deviceSearchQuery.value: + return entityTypes.indexOf(types.entityType.device) > -1 ? true : false; } } - //TODO: Alias filter return false; } @@ -474,6 +661,42 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device return result; } + function prepareAllowedEntityTypesList(allowedEntityTypes) { + var authority = userService.getAuthority(); + var entityTypes = {}; + switch(authority) { + case 'SYS_ADMIN': + entityTypes.tenant = types.entityType.tenant; + entityTypes.rule = types.entityType.rule; + entityTypes.plugin = types.entityType.plugin; + break; + case 'TENANT_ADMIN': + entityTypes.device = types.entityType.device; + entityTypes.asset = types.entityType.asset; + entityTypes.tenant = types.entityType.tenant; + entityTypes.customer = types.entityType.customer; + entityTypes.rule = types.entityType.rule; + entityTypes.plugin = types.entityType.plugin; + entityTypes.dashboard = types.entityType.dashboard; + break; + case 'CUSTOMER_USER': + entityTypes.device = types.entityType.device; + entityTypes.asset = types.entityType.asset; + entityTypes.customer = types.entityType.customer; + entityTypes.dashboard = types.entityType.dashboard; + break; + } + + if (allowedEntityTypes) { + for (var entityType in entityTypes) { + if (allowedEntityTypes.indexOf(entityTypes[entityType]) === -1) { + delete entityTypes[entityType]; + } + } + } + return entityTypes; + } + function checkEntityAlias(entityAlias) { var deferred = $q.defer(); diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 522abff190..fe3f73ca88 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -146,46 +146,55 @@ export default angular.module('thingsboard.types', []) entityTypeTranslations: { "DEVICE": { type: 'entity.type-device', + typePlural: 'entity.type-devices', list: 'entity.list-of-devices', nameStartsWith: 'entity.device-name-starts-with' }, "ASSET": { type: 'entity.type-asset', + typePlural: 'entity.type-assets', list: 'entity.list-of-assets', nameStartsWith: 'entity.asset-name-starts-with' }, "RULE": { type: 'entity.type-rule', + typePlural: 'entity.type-rules', list: 'entity.list-of-rules', nameStartsWith: 'entity.rule-name-starts-with' }, "PLUGIN": { type: 'entity.type-plugin', + typePlural: 'entity.type-plugins', list: 'entity.list-of-plugins', nameStartsWith: 'entity.plugin-name-starts-with' }, "TENANT": { type: 'entity.type-tenant', + typePlural: 'entity.type-tenants', list: 'entity.list-of-tenants', nameStartsWith: 'entity.tenant-name-starts-with' }, "CUSTOMER": { type: 'entity.type-customer', + typePlural: 'entity.type-customers', list: 'entity.list-of-customers', nameStartsWith: 'entity.customer-name-starts-with' }, "USER": { type: 'entity.type-user', + typePlural: 'entity.type-users', list: 'entity.list-of-users', nameStartsWith: 'entity.user-name-starts-with' }, "DASHBOARD": { type: 'entity.type-dashboard', + typePlural: 'entity.type-dashboards', list: 'entity.list-of-dashboards', nameStartsWith: 'entity.dashboard-name-starts-with' }, "ALARM": { type: 'entity.type-alarm', + typePlural: 'entity.type-alarms', list: 'entity.list-of-alarms', nameStartsWith: 'entity.alarm-name-starts-with' } diff --git a/ui/src/app/components/datasource-func.directive.js b/ui/src/app/components/datasource-func.directive.js index 0f66dcaa29..fe546962c7 100644 --- a/ui/src/app/components/datasource-func.directive.js +++ b/ui/src/app/components/datasource-func.directive.js @@ -71,6 +71,13 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, } }, true); + scope.$watch('datasourceName', function () { + if (ngModelCtrl.$viewValue) { + ngModelCtrl.$viewValue.name = scope.datasourceName; + scope.updateValidity(); + } + }); + ngModelCtrl.$render = function () { if (ngModelCtrl.$viewValue) { var funcDataKeys = []; @@ -78,6 +85,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, funcDataKeys = funcDataKeys.concat(ngModelCtrl.$viewValue.dataKeys); } scope.funcDataKeys = funcDataKeys; + scope.datasourceName = ngModelCtrl.$viewValue.name; } }; diff --git a/ui/src/app/components/datasource-func.scss b/ui/src/app/components/datasource-func.scss index 1a5dcc72ea..8739ed8fee 100644 --- a/ui/src/app/components/datasource-func.scss +++ b/ui/src/app/components/datasource-func.scss @@ -15,23 +15,29 @@ */ @import '../../scss/constants'; -.tb-func-datakey-autocomplete { - .tb-not-found { - display: block; - line-height: 1.5; - height: 48px; - .tb-no-entries { - line-height: 48px; - } +.tb-datasource-func { + @media (min-width: $layout-breakpoint-gt-sm) { + padding-left: 8px; } - li { - height: auto !important; - white-space: normal !important; + + md-input-container.tb-datasource-name { + .md-errors-spacer { + display: none; + } } -} -tb-datasource-func { - @media (min-width: $layout-breakpoint-gt-sm) { - padding-left: 8px; + .tb-func-datakey-autocomplete { + .tb-not-found { + display: block; + line-height: 1.5; + height: 48px; + .tb-no-entries { + line-height: 48px; + } + } + li { + height: auto !important; + white-space: normal !important; + } } -} \ No newline at end of file +} diff --git a/ui/src/app/components/datasource-func.tpl.html b/ui/src/app/components/datasource-func.tpl.html index 1853b9d503..e8e08f7f0c 100644 --- a/ui/src/app/components/datasource-func.tpl.html +++ b/ui/src/app/components/datasource-func.tpl.html @@ -15,59 +15,68 @@ limitations under the License. --> -
- - - {{item}} - -
-
- device.no-keys-found +
+ + + +
+ + + {{item}} + +
+
+ device.no-keys-found +
+
+ device.no-key-matching + + device.create-new-key + +
-
- device.no-key-matching - - device.create-new-key - -
-
- - - -
-
-
-
-
-
- {{$chip.label}} + + + +
+
+
-
:
-
- {{$chip.name}} +
+
+ {{$chip.label}} +
+
:
+
+ {{$chip.name}} +
+ + edit +
- - edit - -
-
- - + + + +
diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js index a9a68c74af..5372b6217e 100644 --- a/ui/src/app/dashboard/add-widget.controller.js +++ b/ui/src/app/dashboard/add-widget.controller.js @@ -15,7 +15,7 @@ */ /* eslint-disable import/no-unresolved, import/default */ -import entityAliasDialogTemplate from '../entity/entity-alias-dialog.tpl.html'; +import entityAliasDialogTemplate from '../entity/alias/entity-alias-dialog.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js index 7c97a9462a..65eb833dd4 100644 --- a/ui/src/app/dashboard/dashboard.controller.js +++ b/ui/src/app/dashboard/dashboard.controller.js @@ -15,7 +15,7 @@ */ /* eslint-disable import/no-unresolved, import/default */ -import entityAliasesTemplate from '../entity/entity-aliases.tpl.html'; +import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html'; import dashboardSettingsTemplate from './dashboard-settings.tpl.html'; import manageDashboardLayoutsTemplate from './layouts/manage-dashboard-layouts.tpl.html'; import manageDashboardStatesTemplate from './states/manage-dashboard-states.tpl.html'; diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js index 5b788cc82c..65329149e5 100644 --- a/ui/src/app/dashboard/edit-widget.directive.js +++ b/ui/src/app/dashboard/edit-widget.directive.js @@ -15,7 +15,7 @@ */ /* eslint-disable import/no-unresolved, import/default */ -import entityAliasDialogTemplate from '../entity/entity-alias-dialog.tpl.html'; +import entityAliasDialogTemplate from '../entity/alias/entity-alias-dialog.tpl.html'; import editWidgetTemplate from './edit-widget.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ diff --git a/ui/src/app/entity/aliases-entity-select-button.tpl.html b/ui/src/app/entity/alias/aliases-entity-select-button.tpl.html similarity index 100% rename from ui/src/app/entity/aliases-entity-select-button.tpl.html rename to ui/src/app/entity/alias/aliases-entity-select-button.tpl.html diff --git a/ui/src/app/entity/aliases-entity-select-panel.controller.js b/ui/src/app/entity/alias/aliases-entity-select-panel.controller.js similarity index 100% rename from ui/src/app/entity/aliases-entity-select-panel.controller.js rename to ui/src/app/entity/alias/aliases-entity-select-panel.controller.js diff --git a/ui/src/app/entity/aliases-entity-select-panel.tpl.html b/ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html similarity index 100% rename from ui/src/app/entity/aliases-entity-select-panel.tpl.html rename to ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html diff --git a/ui/src/app/entity/aliases-entity-select.directive.js b/ui/src/app/entity/alias/aliases-entity-select.directive.js similarity index 100% rename from ui/src/app/entity/aliases-entity-select.directive.js rename to ui/src/app/entity/alias/aliases-entity-select.directive.js diff --git a/ui/src/app/entity/aliases-entity-select.scss b/ui/src/app/entity/alias/aliases-entity-select.scss similarity index 100% rename from ui/src/app/entity/aliases-entity-select.scss rename to ui/src/app/entity/alias/aliases-entity-select.scss diff --git a/ui/src/app/entity/entity-alias-dialog.controller.js b/ui/src/app/entity/alias/entity-alias-dialog.controller.js similarity index 100% rename from ui/src/app/entity/entity-alias-dialog.controller.js rename to ui/src/app/entity/alias/entity-alias-dialog.controller.js diff --git a/ui/src/app/entity/entity-alias-dialog.scss b/ui/src/app/entity/alias/entity-alias-dialog.scss similarity index 100% rename from ui/src/app/entity/entity-alias-dialog.scss rename to ui/src/app/entity/alias/entity-alias-dialog.scss diff --git a/ui/src/app/entity/entity-alias-dialog.tpl.html b/ui/src/app/entity/alias/entity-alias-dialog.tpl.html similarity index 100% rename from ui/src/app/entity/entity-alias-dialog.tpl.html rename to ui/src/app/entity/alias/entity-alias-dialog.tpl.html diff --git a/ui/src/app/entity/entity-aliases.controller.js b/ui/src/app/entity/alias/entity-aliases.controller.js similarity index 100% rename from ui/src/app/entity/entity-aliases.controller.js rename to ui/src/app/entity/alias/entity-aliases.controller.js diff --git a/ui/src/app/entity/entity-aliases.scss b/ui/src/app/entity/alias/entity-aliases.scss similarity index 100% rename from ui/src/app/entity/entity-aliases.scss rename to ui/src/app/entity/alias/entity-aliases.scss diff --git a/ui/src/app/entity/entity-aliases.tpl.html b/ui/src/app/entity/alias/entity-aliases.tpl.html similarity index 100% rename from ui/src/app/entity/entity-aliases.tpl.html rename to ui/src/app/entity/alias/entity-aliases.tpl.html diff --git a/ui/src/app/entity/entity-filter-view.directive.js b/ui/src/app/entity/entity-filter-view.directive.js index 6a358b2e16..66c1b667fe 100644 --- a/ui/src/app/entity/entity-filter-view.directive.js +++ b/ui/src/app/entity/entity-filter-view.directive.js @@ -74,7 +74,106 @@ export default function EntityFilterViewDirective($compile, $templateCache, $q, scope.filterDisplayValue = $translate.instant('alias.filter-type-device-type-description', {deviceType: deviceType}); } break; - //TODO: Alias filter + case types.aliasFilterType.relationsQuery.value: + var rootEntityText; + var directionText; + var allEntitiesText = $translate.instant('alias.all-entities'); + var anyRelationText = $translate.instant('alias.any-relation'); + if (scope.filter.rootStateEntity) { + rootEntityText = $translate.instant('alias.state-entity'); + } else { + rootEntityText = $translate.instant(types.entityTypeTranslations[scope.filter.rootEntity.entityType].type); + } + directionText = $translate.instant('relation.direction-type.' + scope.filter.direction); + var relationFilters = scope.filter.filters; + if (relationFilters && relationFilters.length) { + var relationFiltersDisplayValues = []; + relationFilters.forEach(function(relationFilter) { + var entitiesText; + if (relationFilter.entityTypes && relationFilter.entityTypes.length) { + var entitiesNamesList = []; + relationFilter.entityTypes.forEach(function(entityType) { + entitiesNamesList.push( + $translate.instant(types.entityTypeTranslations[entityType].typePlural) + ); + }); + entitiesText = entitiesNamesList.join(', '); + } else { + entitiesText = allEntitiesText; + } + var relationTypeText; + if (relationFilter.relationType && relationFilter.relationType.length) { + relationTypeText = "'" + relationFilter.relationType + "'"; + } else { + relationTypeText = anyRelationText; + } + var relationFilterDisplayValue = $translate.instant('alias.filter-type-relations-query-description', + { + entities: entitiesText, + relationType: relationTypeText, + direction: directionText, + rootEntity: rootEntityText + } + ); + relationFiltersDisplayValues.push(relationFilterDisplayValue); + }); + scope.filterDisplayValue = relationFiltersDisplayValues.join(', '); + } else { + scope.filterDisplayValue = $translate.instant('alias.filter-type-relations-query-description', + { + entities: allEntitiesText, + relationType: anyRelationText, + direction: directionText, + rootEntity: rootEntityText + } + ); + } + break; + case types.aliasFilterType.assetSearchQuery.value: + case types.aliasFilterType.deviceSearchQuery.value: + allEntitiesText = $translate.instant('alias.all-entities'); + anyRelationText = $translate.instant('alias.any-relation'); + if (scope.filter.rootStateEntity) { + rootEntityText = $translate.instant('alias.state-entity'); + } else { + rootEntityText = $translate.instant(types.entityTypeTranslations[scope.filter.rootEntity.entityType].type); + } + directionText = $translate.instant('relation.direction-type.' + scope.filter.direction); + var relationTypeText; + if (scope.filter.relationType && scope.filter.relationType.length) { + relationTypeText = "'" + scope.filter.relationType + "'"; + } else { + relationTypeText = anyRelationText; + } + + var translationValues = { + relationType: relationTypeText, + direction: directionText, + rootEntity: rootEntityText + } + + if (scope.filter.type == types.aliasFilterType.assetSearchQuery.value) { + var assetTypesQuoted = []; + scope.filter.assetTypes.forEach(function(assetType) { + assetTypesQuoted.push("'"+assetType+"'"); + }); + var assetTypesText = assetTypesQuoted.join(', '); + translationValues.assetTypes = assetTypesText; + scope.filterDisplayValue = $translate.instant('alias.filter-type-asset-search-query-description', + translationValues + ); + } else { + var deviceTypesQuoted = []; + scope.filter.deviceTypes.forEach(function(deviceType) { + deviceTypesQuoted.push("'"+deviceType+"'"); + }); + var deviceTypesText = deviceTypesQuoted.join(', '); + translationValues.deviceTypes = deviceTypesText; + scope.filterDisplayValue = $translate.instant('alias.filter-type-device-search-query-description', + translationValues + ); + } + break; default: scope.filterDisplayValue = scope.filter.type; break; diff --git a/ui/src/app/entity/entity-filter.directive.js b/ui/src/app/entity/entity-filter.directive.js index 9566716d3e..b81f36941d 100644 --- a/ui/src/app/entity/entity-filter.directive.js +++ b/ui/src/app/entity/entity-filter.directive.js @@ -63,7 +63,23 @@ export default function EntityFilterDirective($compile, $templateCache, $q, $doc filter.deviceType = null; filter.deviceNameFilter = ''; break; - //TODO: Alias filter + case types.aliasFilterType.relationsQuery.value: + case types.aliasFilterType.assetSearchQuery.value: + case types.aliasFilterType.deviceSearchQuery.value: + filter.rootStateEntity = false; + filter.rootEntity = null; + filter.direction = types.entitySearchDirection.from; + filter.maxLevel = 1; + if (filter.type === types.aliasFilterType.relationsQuery.value) { + filter.filters = []; + } else if (filter.type === types.aliasFilterType.assetSearchQuery.value) { + filter.relationType = null; + filter.assetTypes = []; + } else if (filter.type === types.aliasFilterType.deviceSearchQuery.value) { + filter.relationType = null; + filter.deviceTypes = []; + } + break; } scope.filter = filter; } diff --git a/ui/src/app/entity/entity-filter.scss b/ui/src/app/entity/entity-filter.scss index ebbea3dea8..7e998ca674 100644 --- a/ui/src/app/entity/entity-filter.scss +++ b/ui/src/app/entity/entity-filter.scss @@ -16,4 +16,21 @@ .tb-entity-filter { + #relationsQueryFilter { + padding-top: 20px; + tb-entity-select { + min-height: 92px; + } + } + + .tb-root-state-entity-switch { + padding-left: 10px; + .root-state-entity-switch { + margin: 0; + } + .root-state-entity-label { + margin: 5px 0; + } + } + } \ No newline at end of file diff --git a/ui/src/app/entity/entity-filter.tpl.html b/ui/src/app/entity/entity-filter.tpl.html index 77b6d3cef5..4f5af00bf2 100644 --- a/ui/src/app/entity/entity-filter.tpl.html +++ b/ui/src/app/entity/entity-filter.tpl.html @@ -88,4 +88,146 @@ aria-label="{{ 'device.name-starts-with' | translate }}"> +
+ +
+ + +
+ + + +
+
+
+ + + + + {{ ('relation.search-direction.' + direction) | translate}} + + + + + + + +
+
relation.relation-filters
+ + +
+
+ +
+ + +
+ + + +
+
+
+ + + + + {{ ('relation.search-direction.' + direction) | translate}} + + + + + + + +
+
relation.relation-type
+ + +
asset.asset-types
+ + +
+
+ +
+ + +
+ + + +
+
+
+ + + + + {{ ('relation.search-direction.' + direction) | translate}} + + + + + + + +
+
relation.relation-type
+ + +
device.device-types
+ + +
diff --git a/ui/src/app/entity/entity-subtype-autocomplete.directive.js b/ui/src/app/entity/entity-subtype-autocomplete.directive.js index 98110b05a5..76c15d3250 100644 --- a/ui/src/app/entity/entity-subtype-autocomplete.directive.js +++ b/ui/src/app/entity/entity-subtype-autocomplete.directive.js @@ -114,6 +114,9 @@ export default function EntitySubtypeAutocomplete($compile, $templateCache, $q, scope.selectEntitySubtypeText = 'asset.select-asset-type'; scope.entitySubtypeText = 'asset.asset-type'; scope.entitySubtypeRequiredText = 'asset.asset-type-required'; + scope.$on('assetSaved', function() { + scope.entitySubtypes = null; + }); } else if (scope.entityType == types.entityType.device) { scope.selectEntitySubtypeText = 'device.select-device-type'; scope.entitySubtypeText = 'device.device-type'; diff --git a/ui/src/app/entity/entity-subtype-list.directive.js b/ui/src/app/entity/entity-subtype-list.directive.js new file mode 100644 index 0000000000..c7d63297e2 --- /dev/null +++ b/ui/src/app/entity/entity-subtype-list.directive.js @@ -0,0 +1,146 @@ +/* + * 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. + */ + +/* eslint-disable import/no-unresolved, import/default */ + +import entitySubtypeListTemplate from './entity-subtype-list.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +import './entity-subtype-list.scss'; + +/*@ngInject*/ +export default function EntitySubtypeListDirective($compile, $templateCache, $q, $mdUtil, $translate, $filter, types, assetService, deviceService) { + + var linker = function (scope, element, attrs, ngModelCtrl) { + + var template = $templateCache.get(entitySubtypeListTemplate); + element.html(template); + + scope.ngModelCtrl = ngModelCtrl; + + + scope.entitySubtypesList = []; + scope.entitySubtypes = null; + + if (scope.entityType == types.entityType.asset) { + scope.placeholder = scope.tbRequired ? $translate.instant('asset.enter-asset-type') + : $translate.instant('asset.any-asset'); + scope.secondaryPlaceholder = '+' + $translate.instant('asset.asset-type'); + scope.noSubtypesMathingText = 'asset.no-asset-types-matching'; + scope.subtypeListEmptyText = 'asset.asset-type-list-empty'; + } else if (scope.entityType == types.entityType.device) { + scope.placeholder = scope.tbRequired ? $translate.instant('device.enter-device-type') + : $translate.instant('device.any-device'); + scope.secondaryPlaceholder = '+' + $translate.instant('device.device-type'); + scope.noSubtypesMathingText = 'device.no-device-types-matching'; + scope.subtypeListEmptyText = 'device.device-type-list-empty'; + } + + scope.$watch('tbRequired', function () { + scope.updateValidity(); + }); + + scope.fetchEntitySubtypes = 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.updateValidity = function() { + var value = ngModelCtrl.$viewValue; + var valid = !scope.tbRequired || value && value.length > 0; + ngModelCtrl.$setValidity('entitySubtypesList', valid); + } + + ngModelCtrl.$render = function () { + scope.entitySubtypesList = ngModelCtrl.$viewValue; + if (!scope.entitySubtypesList) { + scope.entitySubtypesList = []; + } + } + + scope.$watch('entitySubtypesList', function () { + ngModelCtrl.$setViewValue(scope.entitySubtypesList); + scope.updateValidity(); + }, true); + + 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; + } + + $compile(element.contents())(scope); + + $mdUtil.nextTick(function(){ + var inputElement = angular.element('input', element); + inputElement.on('blur', function() { + scope.inputTouched = true; + } ); + }); + + } + + return { + restrict: "E", + require: "^ngModel", + link: linker, + scope: { + disabled:'=ngDisabled', + tbRequired: '=?', + entityType: "=" + } + }; + +} diff --git a/ui/src/app/entity/entity-subtype-list.scss b/ui/src/app/entity/entity-subtype-list.scss new file mode 100644 index 0000000000..bbb2a1cc94 --- /dev/null +++ b/ui/src/app/entity/entity-subtype-list.scss @@ -0,0 +1,30 @@ +/** + * 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-entity-subtype-list { + #entity_subtype_list_chips { + .md-chips { + padding-bottom: 1px; + } + } + .tb-error-messages { + margin-top: -11px; + height: 35px; + .tb-error-message { + padding-left: 1px; + } + } +}*/ diff --git a/ui/src/app/entity/entity-subtype-list.tpl.html b/ui/src/app/entity/entity-subtype-list.tpl.html new file mode 100644 index 0000000000..2a1519aa9a --- /dev/null +++ b/ui/src/app/entity/entity-subtype-list.tpl.html @@ -0,0 +1,54 @@ + + +
+ + + + {{item}} + + + {{noSubtypesMathingText}} + + + + + {{$chip}} + + + + +
diff --git a/ui/src/app/entity/entity-type-list.directive.js b/ui/src/app/entity/entity-type-list.directive.js new file mode 100644 index 0000000000..cb07cfc877 --- /dev/null +++ b/ui/src/app/entity/entity-type-list.directive.js @@ -0,0 +1,111 @@ +/* + * 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. + */ + +/* eslint-disable import/no-unresolved, import/default */ + +import entityTypeListTemplate from './entity-type-list.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +import './entity-type-list.scss'; + +/*@ngInject*/ +export default function EntityTypeListDirective($compile, $templateCache, $q, $mdUtil, $translate, $filter, types, entityService) { + + var linker = function (scope, element, attrs, ngModelCtrl) { + + var template = $templateCache.get(entityTypeListTemplate); + element.html(template); + + scope.ngModelCtrl = ngModelCtrl; + + scope.placeholder = scope.tbRequired ? $translate.instant('entity.enter-entity-type') + : $translate.instant('entity.any-entity'); + scope.secondaryPlaceholder = '+' + $translate.instant('entity.entity-type'); + + var entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes); + scope.entityTypesList = []; + for (var type in entityTypes) { + var entityTypeInfo = {}; + entityTypeInfo.value = entityTypes[type]; + entityTypeInfo.name = $translate.instant(types.entityTypeTranslations[entityTypeInfo.value].type) + ''; + scope.entityTypesList.push(entityTypeInfo); + } + + scope.$watch('tbRequired', function () { + scope.updateValidity(); + }); + + scope.fetchEntityTypes = function(searchText) { + var deferred = $q.defer(); + var entityTypes = $filter('filter')(scope.entityTypesList, {name: searchText}); + deferred.resolve(entityTypes); + return deferred.promise; + } + + scope.updateValidity = function() { + var value = ngModelCtrl.$viewValue; + var valid = !scope.tbRequired || value && value.length > 0; + ngModelCtrl.$setValidity('entityTypeList', valid); + } + + ngModelCtrl.$render = function () { + scope.entityTypeList = []; + var value = ngModelCtrl.$viewValue; + if (value && value.length) { + value.forEach(function(type) { + var entityTypeInfo = {}; + entityTypeInfo.value = type; + entityTypeInfo.name = $translate.instant(types.entityTypeTranslations[entityTypeInfo.value].type) + ''; + scope.entityTypeList.push(entityTypeInfo); + }); + } + } + + scope.$watch('entityTypeList', function () { + var values = []; + if (scope.entityTypeList && scope.entityTypeList.length) { + scope.entityTypeList.forEach(function(entityType) { + values.push(entityType.value); + }); + } + ngModelCtrl.$setViewValue(values); + scope.updateValidity(); + }, true); + + $compile(element.contents())(scope); + + $mdUtil.nextTick(function(){ + var inputElement = angular.element('input', element); + inputElement.on('blur', function() { + scope.inputTouched = true; + } ); + }); + + } + + return { + restrict: "E", + require: "^ngModel", + link: linker, + scope: { + disabled:'=ngDisabled', + tbRequired: '=?', + allowedEntityTypes: '=?' + } + }; + +} diff --git a/ui/src/app/entity/entity-type-list.scss b/ui/src/app/entity/entity-type-list.scss new file mode 100644 index 0000000000..b94b992f77 --- /dev/null +++ b/ui/src/app/entity/entity-type-list.scss @@ -0,0 +1,30 @@ +/** + * 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-entity-type-list { + #entity_type_list_chips { + .md-chips { + padding-bottom: 1px; + } + } + .tb-error-messages { + margin-top: -11px; + height: 35px; + .tb-error-message { + padding-left: 1px; + } + } +}*/ \ No newline at end of file diff --git a/ui/src/app/entity/entity-type-list.tpl.html b/ui/src/app/entity/entity-type-list.tpl.html new file mode 100644 index 0000000000..ff10a30d9b --- /dev/null +++ b/ui/src/app/entity/entity-type-list.tpl.html @@ -0,0 +1,54 @@ + + +
+ + + + {{item.name}} + + + entity.no-entity-types-matching + + + + + {{$chip.name}} + + + + +
diff --git a/ui/src/app/entity/entity-type-select.directive.js b/ui/src/app/entity/entity-type-select.directive.js index bca2ee0cfe..068c1412bf 100644 --- a/ui/src/app/entity/entity-type-select.directive.js +++ b/ui/src/app/entity/entity-type-select.directive.js @@ -23,7 +23,7 @@ import entityTypeSelectTemplate from './entity-type-select.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function EntityTypeSelect($compile, $templateCache, utils, userService, types) { +export default function EntityTypeSelect($compile, $templateCache, utils, entityService, userService, types) { var linker = function (scope, element, attrs, ngModelCtrl) { var template = $templateCache.get(entityTypeSelectTemplate); @@ -39,36 +39,7 @@ export default function EntityTypeSelect($compile, $templateCache, utils, userSe scope.ngModelCtrl = ngModelCtrl; - var authority = userService.getAuthority(); - scope.entityTypes = {}; - switch(authority) { - case 'SYS_ADMIN': - scope.entityTypes.tenant = types.entityType.tenant; - scope.entityTypes.rule = types.entityType.rule; - scope.entityTypes.plugin = types.entityType.plugin; - break; - case 'TENANT_ADMIN': - scope.entityTypes.device = types.entityType.device; - scope.entityTypes.asset = types.entityType.asset; - scope.entityTypes.customer = types.entityType.customer; - scope.entityTypes.rule = types.entityType.rule; - scope.entityTypes.plugin = types.entityType.plugin; - scope.entityTypes.dashboard = types.entityType.dashboard; - break; - case 'CUSTOMER_USER': - scope.entityTypes.device = types.entityType.device; - scope.entityTypes.asset = types.entityType.asset; - scope.entityTypes.dashboard = types.entityType.dashboard; - break; - } - - if (scope.allowedEntityTypes) { - for (var entityType in scope.entityTypes) { - if (scope.allowedEntityTypes.indexOf(scope.entityTypes[entityType]) === -1) { - delete scope.entityTypes[entityType]; - } - } - } + scope.entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes); scope.typeName = function(type) { return type ? types.entityTypeTranslations[type].type : ''; diff --git a/ui/src/app/entity/index.js b/ui/src/app/entity/index.js index 2fc1d6300b..2b8d434a9e 100644 --- a/ui/src/app/entity/index.js +++ b/ui/src/app/entity/index.js @@ -14,9 +14,11 @@ * limitations under the License. */ -import EntityAliasesController from './entity-aliases.controller'; -import EntityAliasDialogController from './entity-alias-dialog.controller'; +import EntityAliasesController from './alias/entity-aliases.controller'; +import EntityAliasDialogController from './alias/entity-alias-dialog.controller'; import EntityTypeSelectDirective from './entity-type-select.directive'; +import EntityTypeListDirective from './entity-type-list.directive'; +import EntitySubtypeListDirective from './entity-subtype-list.directive'; import EntitySubtypeSelectDirective from './entity-subtype-select.directive'; import EntitySubtypeAutocompleteDirective from './entity-subtype-autocomplete.directive'; import EntityAutocompleteDirective from './entity-autocomplete.directive'; @@ -24,11 +26,12 @@ import EntityListDirective from './entity-list.directive'; import EntitySelectDirective from './entity-select.directive'; import EntityFilterDirective from './entity-filter.directive'; import EntityFilterViewDirective from './entity-filter-view.directive'; -import AliasesEntitySelectPanelController from './aliases-entity-select-panel.controller'; -import AliasesEntitySelectDirective from './aliases-entity-select.directive'; +import AliasesEntitySelectPanelController from './alias/aliases-entity-select-panel.controller'; +import AliasesEntitySelectDirective from './alias/aliases-entity-select.directive'; import AddAttributeDialogController from './attribute/add-attribute-dialog.controller'; import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller'; import AttributeTableDirective from './attribute/attribute-table.directive'; +import RelationFiltersDirective from './relation/relation-filters.directive'; import RelationTableDirective from './relation/relation-table.directive'; import RelationTypeAutocompleteDirective from './relation/relation-type-autocomplete.directive'; @@ -39,6 +42,8 @@ export default angular.module('thingsboard.entity', []) .controller('AddAttributeDialogController', AddAttributeDialogController) .controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController) .directive('tbEntityTypeSelect', EntityTypeSelectDirective) + .directive('tbEntityTypeList', EntityTypeListDirective) + .directive('tbEntitySubtypeList', EntitySubtypeListDirective) .directive('tbEntitySubtypeSelect', EntitySubtypeSelectDirective) .directive('tbEntitySubtypeAutocomplete', EntitySubtypeAutocompleteDirective) .directive('tbEntityAutocomplete', EntityAutocompleteDirective) @@ -48,6 +53,7 @@ export default angular.module('thingsboard.entity', []) .directive('tbEntityFilterView', EntityFilterViewDirective) .directive('tbAliasesEntitySelect', AliasesEntitySelectDirective) .directive('tbAttributeTable', AttributeTableDirective) + .directive('tbRelationFilters', RelationFiltersDirective) .directive('tbRelationTable', RelationTableDirective) .directive('tbRelationTypeAutocomplete', RelationTypeAutocompleteDirective) .name; diff --git a/ui/src/app/entity/relation/relation-filters.directive.js b/ui/src/app/entity/relation/relation-filters.directive.js new file mode 100644 index 0000000000..59821dd2ba --- /dev/null +++ b/ui/src/app/entity/relation/relation-filters.directive.js @@ -0,0 +1,85 @@ +/* + * 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 './relation-filters.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import relationFiltersTemplate from './relation-filters.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function RelationFilters($compile, $templateCache) { + + return { + restrict: "E", + require: "^ngModel", + scope: { + allowedEntityTypes: '=?' + }, + link: linker + }; + + function linker( scope, element, attrs, ngModelCtrl ) { + + var template = $templateCache.get(relationFiltersTemplate); + element.html(template); + + scope.relationFilters = []; + + scope.addFilter = addFilter; + scope.removeFilter = removeFilter; + + ngModelCtrl.$render = function () { + if (ngModelCtrl.$viewValue) { + var value = ngModelCtrl.$viewValue; + value.forEach(function (filter) { + scope.relationFilters.push(filter); + }); + } + scope.$watch('relationFilters', function (newVal, prevVal) { + if (!angular.equals(newVal, prevVal)) { + updateValue(); + } + }, true); + } + + function addFilter() { + var filter = { + relationType: null, + entityTypes: [] + }; + scope.relationFilters.push(filter); + } + + function removeFilter($event, filter) { + var index = scope.relationFilters.indexOf(filter); + if (index > -1) { + scope.relationFilters.splice(index, 1); + } + } + + function updateValue() { + var value = []; + scope.relationFilters.forEach(function (filter) { + value.push(filter); + }); + ngModelCtrl.$setViewValue(value); + } + $compile(element.contents())(scope); + } +} diff --git a/ui/src/app/entity/relation/relation-filters.scss b/ui/src/app/entity/relation/relation-filters.scss new file mode 100644 index 0000000000..50d49af3a8 --- /dev/null +++ b/ui/src/app/entity/relation/relation-filters.scss @@ -0,0 +1,77 @@ +/** + * 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-relation-filters { + .header { + padding-left: 5px; + padding-right: 5px; + padding-bottom: 5px; + .cell { + padding-left: 5px; + padding-right: 5px; + color: rgba(0,0,0,.54); + font-size: 12px; + font-weight: 700; + white-space: nowrap; + } + } + .body { + padding-left: 5px; + padding-right: 5px; + max-height: 300px; + overflow: auto; + padding-bottom: 20px; + .row { + padding-top: 5px; + } + .cell { + padding-left: 5px; + padding-right: 5px; + + md-select { + margin: 0 0 24px; + } + + md-input-container { + margin: 0; + } + + md-chips-wrap { + padding: 0px; + margin: 0 0 24px; + .md-chip-input-container { + margin: 0; + } + md-autocomplete { + height: 30px; + md-autocomplete-wrap { + height: 30px; + } + } + } + .md-chips .md-chip-input-container input { + padding: 2px 2px 2px; + height: 26px; + line-height: 26px; + } + + } + + .md-button { + margin: 0; + } + } +} \ No newline at end of file diff --git a/ui/src/app/entity/relation/relation-filters.tpl.html b/ui/src/app/entity/relation/relation-filters.tpl.html new file mode 100644 index 0000000000..679a4884a3 --- /dev/null +++ b/ui/src/app/entity/relation/relation-filters.tpl.html @@ -0,0 +1,67 @@ + +
+
+
+ relation.type + entity.entity-types +   +
+
+
+
+
+ + + + + + + {{ 'relation.remove-relation-filter' | translate }} + + + close + + +
+
+
+
+ relation.any-relation +
+
+ + + {{ 'relation.add-relation-filter' | translate }} + + + add + + {{ 'action.add' | translate }} + +
+
\ No newline at end of file diff --git a/ui/src/app/entity/relation/relation-type-autocomplete.directive.js b/ui/src/app/entity/relation/relation-type-autocomplete.directive.js index a7e5ea45bb..4b5480d61b 100644 --- a/ui/src/app/entity/relation/relation-type-autocomplete.directive.js +++ b/ui/src/app/entity/relation/relation-type-autocomplete.directive.js @@ -29,6 +29,8 @@ export default function RelationTypeAutocomplete($compile, $templateCache, $q, $ element.html(template); scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; + scope.hideLabel = angular.isDefined(attrs.hideLabel) ? true : false; + scope.relationType = null; scope.relationTypeSearchText = ''; scope.relationTypes = []; diff --git a/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html b/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html index f71c13418a..39f32d6706 100644 --- a/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html +++ b/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html @@ -26,7 +26,7 @@ md-items="item in fetchRelationTypes(relationTypeSearchText)" md-item-text="item" md-min-length="0" - md-floating-label="{{ 'relation.relation-type' | translate }}" + md-floating-label="{{ tbRequired ? ('relation.relation-type' | translate) : ( !relationType ? ('relation.any-relation-type' | translate) : ' ') }}" md-select-on-match="true" md-menu-class="tb-relation-type-autocomplete"> diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js index 8d4a09828b..86d5240953 100644 --- a/ui/src/app/import-export/import-export.service.js +++ b/ui/src/app/import-export/import-export.service.js @@ -16,7 +16,7 @@ /* eslint-disable import/no-unresolved, import/default */ import importDialogTemplate from './import-dialog.tpl.html'; -import entityAliasesTemplate from '../entity/entity-aliases.tpl.html'; +import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index 88a6021568..cd21747fe0 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -129,14 +129,24 @@ export default angular.module('thingsboard.locale', []) "filter-type-device-type-description": "Devices of type '{{deviceType}}'", "filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'", "filter-type-relations-query": "Relations query", + "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-asset-search-query": "Asset search query", + "filter-type-asset-search-query-description": "Assets with types {{assetTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", "filter-type-device-search-query": "Device search query", + "filter-type-device-search-query-description": "Devices with types {{deviceTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", "entity-filter": "Entity filter", "resolve-multiple": "Resolve as multiple entities", "filter-type": "Filter type", "filter-type-required": "Filter type is required.", "entity-filter-no-entity-matched": "No entities matching specified filter were found.", - "no-entity-filter-specified": "No entity filter specified" + "no-entity-filter-specified": "No entity filter specified", + "root-state-entity": "Use dashboard state entity as root", + "root-entity": "Root entity", + "max-relation-level": "Max relation level", + "unlimited-level": "Unlimited level", + "state-entity": "Dashboard state entity", + "all-entities": "All entities", + "any-relation": "any" }, "asset": { "asset": "Asset", @@ -159,6 +169,11 @@ export default angular.module('thingsboard.locale', []) "asset-type": "Asset type", "asset-type-required": "Asset type is required.", "select-asset-type": "Select asset type", + "enter-asset-type": "Enter asset type", + "any-asset": "Any asset", + "no-asset-types-matching": "No asset types matching '{{entitySubtype}}' were found.", + "asset-type-list-empty": "No asset types selected.", + "asset-types": "Asset types", "name": "Name", "name-required": "Name is required.", "description": "Description", @@ -444,6 +459,7 @@ export default angular.module('thingsboard.locale', []) }, "datasource": { "type": "Datasource type", + "name": "Name", "add-datasource-prompt": "Please add datasource" }, "details": { @@ -524,6 +540,11 @@ export default angular.module('thingsboard.locale', []) "device-type": "Device type", "device-type-required": "Device type is required.", "select-device-type": "Select device type", + "enter-device-type": "Enter device type", + "any-device": "Any device", + "no-device-types-matching": "No device types matching '{{entitySubtype}}' were found.", + "device-type-list-empty": "No device types selected.", + "device-types": "Device types", "name": "Name", "name-required": "Name is required.", "description": "Description", @@ -564,10 +585,17 @@ export default angular.module('thingsboard.locale', []) "remove-alias": "Remove entity alias", "add-alias": "Add entity alias", "entity-list": "Entity list", + "entity-type": "Entity type", + "entity-types": "Entity types", + "entity-type-list": "Entity type list", + "any-entity": "Any entity", + "enter-entity-type": "Enter entity type", "no-entities-matching": "No entities matching '{{entity}}' were found.", + "no-entity-types-matching": "No entity types matching '{{entityType}}' were found.", "name-starts-with": "Name starts with", "use-entity-name-filter": "Use filter", "entity-list-empty": "No entities selected.", + "entity-type-list-empty": "No entity types 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", @@ -581,30 +609,39 @@ export default angular.module('thingsboard.locale', []) "type": "Type", "type-required": "Entity type is required.", "type-device": "Device", + "type-devices": "Devices", "list-of-devices": "{ count, select, 1 {One device} other {List of # devices} }", "device-name-starts-with": "Devices whose names start with '{{prefix}}'", "type-asset": "Asset", + "type-assets": "Assets", "list-of-assets": "{ count, select, 1 {One asset} other {List of # assets} }", "asset-name-starts-with": "Assets whose names start with '{{prefix}}'", "type-rule": "Rule", + "type-rules": "Rules", "list-of-rules": "{ count, select, 1 {One rule} other {List of # rules} }", "rule-name-starts-with": "Rules whose names start with '{{prefix}}'", "type-plugin": "Plugin", + "type-plugins": "Plugins", "list-of-plugins": "{ count, select, 1 {One plugin} other {List of # plugins} }", "plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'", "type-tenant": "Tenant", + "type-tenants": "Tenants", "list-of-tenants": "{ count, select, 1 {One tenant} other {List of # tenants} }", "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'", "type-customer": "Customer", + "type-customers": "Customers", "list-of-customers": "{ count, select, 1 {One customer} other {List of # customers} }", "customer-name-starts-with": "Customers whose names start with '{{prefix}}'", "type-user": "User", + "type-users": "Users", "list-of-users": "{ count, select, 1 {One user} other {List of # users} }", "user-name-starts-with": "Users whose names start with '{{prefix}}'", "type-dashboard": "Dashboard", + "type-dashboards": "Dashboards", "list-of-dashboards": "{ count, select, 1 {One dashboard} other {List of # dashboards} }", "dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'", "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}}'" }, @@ -770,6 +807,10 @@ export default angular.module('thingsboard.locale', []) "FROM": "From", "TO": "To" }, + "direction-type": { + "FROM": "from", + "TO": "to" + }, "from-relations": "Outbound relations", "to-relations": "Inbound relations", "selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected", @@ -783,6 +824,7 @@ export default angular.module('thingsboard.locale', []) "delete": "Delete relation", "relation-type": "Relation type", "relation-type-required": "Relation type is required.", + "any-relation-type": "Any type", "add": "Add relation", "delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?", "delete-to-relation-text": "Be careful, after the confirmation the entity '{{entityName}}' will be unrelated from the current entity.", @@ -791,7 +833,11 @@ export default angular.module('thingsboard.locale', []) "delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?", "delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.", "delete-from-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?", - "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities." + "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities.", + "remove-relation-filter": "Remove relation filter", + "add-relation-filter": "Add relation filter", + "any-relation": "Any relation", + "relation-filters": "Relation filters" }, "rule": { "rule": "Rule", diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss index dbe3543efa..bf444937a4 100644 --- a/ui/src/scss/main.scss +++ b/ui/src/scss/main.scss @@ -236,6 +236,15 @@ div { } } +.md-caption { + &.tb-required:after { + content: ' *'; + font-size: 10px; + vertical-align: top; + color: rgba(0,0,0,0.54); + } +} + pre.tb-highlight { background-color: #f7f7f7; display: block;