Commit 9cdfe4ef54e38e4a0b962665abae1bef431a548d

Authored by Igor Kulikov
1 parent a90f16f4

TB-61: Implemented new alias filters.

Showing 44 changed files with 1399 additions and 126 deletions
@@ -250,6 +250,21 @@ public class EntityRelationController extends BaseController { @@ -250,6 +250,21 @@ public class EntityRelationController extends BaseController {
250 } 250 }
251 } 251 }
252 252
  253 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
  254 + @RequestMapping(value = "/relations/info", method = RequestMethod.POST)
  255 + @ResponseBody
  256 + public List<EntityRelationInfo> findInfoByQuery(@RequestBody EntityRelationsQuery query) throws ThingsboardException {
  257 + checkNotNull(query);
  258 + checkNotNull(query.getParameters());
  259 + checkNotNull(query.getFilters());
  260 + checkEntityId(query.getParameters().getEntityId());
  261 + try {
  262 + return checkNotNull(relationService.findInfoByQuery(query).get());
  263 + } catch (Exception e) {
  264 + throw handleException(e);
  265 + }
  266 + }
  267 +
253 private RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) { 268 private RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) {
254 RelationTypeGroup result = defaultValue; 269 RelationTypeGroup result = defaultValue;
255 if (strRelationTypeGroup != null && strRelationTypeGroup.trim().length()>0) { 270 if (strRelationTypeGroup != null && strRelationTypeGroup.trim().length()>0) {
@@ -191,7 +191,7 @@ public class BaseRelationService implements RelationService { @@ -191,7 +191,7 @@ public class BaseRelationService implements RelationService {
191 191
192 @Override 192 @Override
193 public ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query) { 193 public ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query) {
194 - log.trace("Executing findByQuery [{}][{}]", query); 194 + log.trace("Executing findByQuery [{}]", query);
195 RelationsSearchParameters params = query.getParameters(); 195 RelationsSearchParameters params = query.getParameters();
196 final List<EntityTypeFilter> filters = query.getFilters(); 196 final List<EntityTypeFilter> filters = query.getFilters();
197 if (filters == null || filters.isEmpty()) { 197 if (filters == null || filters.isEmpty()) {
@@ -224,6 +224,30 @@ public class BaseRelationService implements RelationService { @@ -224,6 +224,30 @@ public class BaseRelationService implements RelationService {
224 } 224 }
225 } 225 }
226 226
  227 + @Override
  228 + public ListenableFuture<List<EntityRelationInfo>> findInfoByQuery(EntityRelationsQuery query) {
  229 + log.trace("Executing findInfoByQuery [{}]", query);
  230 + ListenableFuture<List<EntityRelation>> relations = findByQuery(query);
  231 + EntitySearchDirection direction = query.getParameters().getDirection();
  232 + ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
  233 + (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
  234 + List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
  235 + relations1.stream().forEach(relation ->
  236 + futures.add(fetchRelationInfoAsync(relation,
  237 + relation2 -> direction == EntitySearchDirection.FROM ? relation2.getTo() : relation2.getFrom(),
  238 + (EntityRelationInfo relationInfo, String entityName) -> {
  239 + if (direction == EntitySearchDirection.FROM) {
  240 + relationInfo.setToName(entityName);
  241 + } else {
  242 + relationInfo.setFromName(entityName);
  243 + }
  244 + }))
  245 + );
  246 + return Futures.successfulAsList(futures);
  247 + });
  248 + return relationsInfo;
  249 + }
  250 +
227 protected void validate(EntityRelation relation) { 251 protected void validate(EntityRelation relation) {
228 if (relation == null) { 252 if (relation == null) {
229 throw new DataValidationException("Relation type should be specified!"); 253 throw new DataValidationException("Relation type should be specified!");
@@ -52,6 +52,8 @@ public interface RelationService { @@ -52,6 +52,8 @@ public interface RelationService {
52 52
53 ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query); 53 ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query);
54 54
  55 + ListenableFuture<List<EntityRelationInfo>> findInfoByQuery(EntityRelationsQuery query);
  56 +
55 // TODO: This method may be useful for some validations in the future 57 // TODO: This method may be useful for some validations in the future
56 // ListenableFuture<Boolean> checkRecursiveRelation(EntityId from, EntityId to); 58 // ListenableFuture<Boolean> checkRecursiveRelation(EntityId from, EntityId to);
57 59
@@ -30,7 +30,8 @@ function EntityRelationService($http, $q) { @@ -30,7 +30,8 @@ function EntityRelationService($http, $q) {
30 findByTo: findByTo, 30 findByTo: findByTo,
31 findInfoByTo: findInfoByTo, 31 findInfoByTo: findInfoByTo,
32 findByToAndType: findByToAndType, 32 findByToAndType: findByToAndType,
33 - findByQuery: findByQuery 33 + findByQuery: findByQuery,
  34 + findInfoByQuery: findInfoByQuery
34 } 35 }
35 36
36 return service; 37 return service;
@@ -159,4 +160,15 @@ function EntityRelationService($http, $q) { @@ -159,4 +160,15 @@ function EntityRelationService($http, $q) {
159 return deferred.promise; 160 return deferred.promise;
160 } 161 }
161 162
  163 + function findInfoByQuery(query) {
  164 + var deferred = $q.defer();
  165 + var url = '/api/relations/info';
  166 + $http.post(url, query).then(function success(response) {
  167 + deferred.resolve(response.data);
  168 + }, function fail() {
  169 + deferred.reject();
  170 + });
  171 + return deferred.promise;
  172 + }
  173 +
162 } 174 }
@@ -32,6 +32,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -32,6 +32,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
32 checkEntityAlias: checkEntityAlias, 32 checkEntityAlias: checkEntityAlias,
33 filterAliasByEntityTypes: filterAliasByEntityTypes, 33 filterAliasByEntityTypes: filterAliasByEntityTypes,
34 getAliasFilterTypesByEntityTypes: getAliasFilterTypesByEntityTypes, 34 getAliasFilterTypesByEntityTypes: getAliasFilterTypesByEntityTypes,
  35 + prepareAllowedEntityTypesList: prepareAllowedEntityTypesList,
35 getEntityKeys: getEntityKeys, 36 getEntityKeys: getEntityKeys,
36 createDatasourcesFromSubscriptionsInfo: createDatasourcesFromSubscriptionsInfo, 37 createDatasourcesFromSubscriptionsInfo: createDatasourcesFromSubscriptionsInfo,
37 getRelatedEntities: getRelatedEntities, 38 getRelatedEntities: getRelatedEntities,
@@ -176,6 +177,54 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -176,6 +177,54 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
176 return deferred.promise; 177 return deferred.promise;
177 } 178 }
178 179
  180 + function getSingleTenantByPageLinkPromise(pageLink) {
  181 + var user = userService.getCurrentUser();
  182 + var tenantId = user.tenantId;
  183 + var deferred = $q.defer();
  184 + tenantService.getTenant(tenantId).then(
  185 + function success(tenant) {
  186 + var tenantName = tenant.name;
  187 + var result = {
  188 + data: [],
  189 + nextPageLink: pageLink,
  190 + hasNext: false
  191 + };
  192 + if (tenantName.toLowerCase().startsWith(pageLink.textSearch)) {
  193 + result.data.push(tenant);
  194 + }
  195 + deferred.resolve(result);
  196 + },
  197 + function fail() {
  198 + deferred.reject();
  199 + }
  200 + );
  201 + return deferred.promise;
  202 + }
  203 +
  204 + function getSingleCustomerByPageLinkPromise(pageLink) {
  205 + var user = userService.getCurrentUser();
  206 + var customerId = user.customerId;
  207 + var deferred = $q.defer();
  208 + customerService.getCustomer(customerId).then(
  209 + function success(customer) {
  210 + var customerName = customer.name;
  211 + var result = {
  212 + data: [],
  213 + nextPageLink: pageLink,
  214 + hasNext: false
  215 + };
  216 + if (customerName.toLowerCase().startsWith(pageLink.textSearch)) {
  217 + result.data.push(customer);
  218 + }
  219 + deferred.resolve(result);
  220 + },
  221 + function fail() {
  222 + deferred.reject();
  223 + }
  224 + );
  225 + return deferred.promise;
  226 + }
  227 +
179 function getEntitiesByPageLinkPromise(entityType, pageLink, config, subType) { 228 function getEntitiesByPageLinkPromise(entityType, pageLink, config, subType) {
180 var promise; 229 var promise;
181 var user = userService.getCurrentUser(); 230 var user = userService.getCurrentUser();
@@ -196,10 +245,18 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -196,10 +245,18 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
196 } 245 }
197 break; 246 break;
198 case types.entityType.tenant: 247 case types.entityType.tenant:
199 - promise = tenantService.getTenants(pageLink); 248 + if (user.authority === 'TENANT_ADMIN') {
  249 + promise = getSingleTenantByPageLinkPromise(pageLink);
  250 + } else {
  251 + promise = tenantService.getTenants(pageLink);
  252 + }
200 break; 253 break;
201 case types.entityType.customer: 254 case types.entityType.customer:
202 - promise = customerService.getCustomers(pageLink); 255 + if (user.authority === 'CUSTOMER_USER') {
  256 + promise = getSingleCustomerByPageLinkPromise(pageLink);
  257 + } else {
  258 + promise = customerService.getCustomers(pageLink);
  259 + }
203 break; 260 break;
204 case types.entityType.rule: 261 case types.entityType.rule:
205 promise = ruleService.getAllRules(pageLink); 262 promise = ruleService.getAllRules(pageLink);
@@ -283,6 +340,16 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -283,6 +340,16 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
283 return { name: entity.name, entityType: entity.id.entityType, id: entity.id.id }; 340 return { name: entity.name, entityType: entity.id.entityType, id: entity.id.id };
284 } 341 }
285 342
  343 + function entityRelationInfoToEntityInfo(entityRelationInfo, direction) {
  344 + var entityId = direction == types.entitySearchDirection.from ? entityRelationInfo.to : entityRelationInfo.from;
  345 + var name = direction == types.entitySearchDirection.from ? entityRelationInfo.toName : entityRelationInfo.fromName;
  346 + return {
  347 + name: name,
  348 + entityType: entityId.entityType,
  349 + id: entityId.id
  350 + };
  351 + }
  352 +
286 function entitiesToEntitiesInfo(entities) { 353 function entitiesToEntitiesInfo(entities) {
287 var entitiesInfo = []; 354 var entitiesInfo = [];
288 for (var d = 0; d < entities.length; d++) { 355 for (var d = 0; d < entities.length; d++) {
@@ -291,19 +358,26 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -291,19 +358,26 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
291 return entitiesInfo; 358 return entitiesInfo;
292 } 359 }
293 360
  361 + function entityRelationInfosToEntitiesInfo(entityRelations, direction) {
  362 + var entitiesInfo = [];
  363 + for (var d = 0; d < entityRelations.length; d++) {
  364 + entitiesInfo.push(entityRelationInfoToEntityInfo(entityRelations[d], direction));
  365 + }
  366 + return entitiesInfo;
  367 + }
  368 +
  369 +
294 function resolveAlias(entityAlias, stateParams) { 370 function resolveAlias(entityAlias, stateParams) {
295 var deferred = $q.defer(); 371 var deferred = $q.defer();
296 var filter = entityAlias.filter; 372 var filter = entityAlias.filter;
297 resolveAliasFilter(filter, stateParams, -1).then( 373 resolveAliasFilter(filter, stateParams, -1).then(
298 function (result) { 374 function (result) {
299 - var entities = result.entities;  
300 var aliasInfo = { 375 var aliasInfo = {
301 alias: entityAlias.alias, 376 alias: entityAlias.alias,
302 stateEntity: result.stateEntity, 377 stateEntity: result.stateEntity,
303 resolveMultiple: filter.resolveMultiple 378 resolveMultiple: filter.resolveMultiple
304 }; 379 };
305 - var resolvedEntities = entitiesToEntitiesInfo(entities);  
306 - aliasInfo.resolvedEntities = resolvedEntities; 380 + aliasInfo.resolvedEntities = result.entities;
307 aliasInfo.currentEntity = null; 381 aliasInfo.currentEntity = null;
308 if (aliasInfo.resolvedEntities.length) { 382 if (aliasInfo.resolvedEntities.length) {
309 aliasInfo.currentEntity = aliasInfo.resolvedEntities[0]; 383 aliasInfo.currentEntity = aliasInfo.resolvedEntities[0];
@@ -328,7 +402,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -328,7 +402,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
328 getEntities(filter.entityType, filter.entityList).then( 402 getEntities(filter.entityType, filter.entityList).then(
329 function success(entities) { 403 function success(entities) {
330 if (entities && entities.length) { 404 if (entities && entities.length) {
331 - result.entities = entities; 405 + result.entities = entitiesToEntitiesInfo(entities);
332 deferred.resolve(result); 406 deferred.resolve(result);
333 } else { 407 } else {
334 deferred.reject(); 408 deferred.reject();
@@ -343,7 +417,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -343,7 +417,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
343 getEntitiesByNameFilter(filter.entityType, filter.entityNameFilter, maxItems).then( 417 getEntitiesByNameFilter(filter.entityType, filter.entityNameFilter, maxItems).then(
344 function success(entities) { 418 function success(entities) {
345 if (entities && entities.length) { 419 if (entities && entities.length) {
346 - result.entities = entities; 420 + result.entities = entitiesToEntitiesInfo(entities);
347 deferred.resolve(result); 421 deferred.resolve(result);
348 } else { 422 } else {
349 deferred.reject(); 423 deferred.reject();
@@ -359,7 +433,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -359,7 +433,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
359 if (stateParams && stateParams.entityId) { 433 if (stateParams && stateParams.entityId) {
360 getEntity(stateParams.entityId.entityType, stateParams.entityId.id).then( 434 getEntity(stateParams.entityId.entityType, stateParams.entityId.id).then(
361 function success(entity) { 435 function success(entity) {
362 - result.entities = [entity]; 436 + result.entities = entitiesToEntitiesInfo([entity]);
363 deferred.resolve(result); 437 deferred.resolve(result);
364 }, 438 },
365 function fail() { 439 function fail() {
@@ -374,7 +448,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -374,7 +448,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
374 getEntitiesByNameFilter(types.entityType.asset, filter.assetNameFilter, maxItems, null, filter.assetType).then( 448 getEntitiesByNameFilter(types.entityType.asset, filter.assetNameFilter, maxItems, null, filter.assetType).then(
375 function success(entities) { 449 function success(entities) {
376 if (entities && entities.length) { 450 if (entities && entities.length) {
377 - result.entities = entities; 451 + result.entities = entitiesToEntitiesInfo(entities);
378 deferred.resolve(result); 452 deferred.resolve(result);
379 } else { 453 } else {
380 deferred.reject(); 454 deferred.reject();
@@ -389,7 +463,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -389,7 +463,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
389 getEntitiesByNameFilter(types.entityType.device, filter.deviceNameFilter, maxItems, null, filter.deviceType).then( 463 getEntitiesByNameFilter(types.entityType.device, filter.deviceNameFilter, maxItems, null, filter.deviceType).then(
390 function success(entities) { 464 function success(entities) {
391 if (entities && entities.length) { 465 if (entities && entities.length) {
392 - result.entities = entities; 466 + result.entities = entitiesToEntitiesInfo(entities);
393 deferred.resolve(result); 467 deferred.resolve(result);
394 } else { 468 } else {
395 deferred.reject(); 469 deferred.reject();
@@ -400,8 +474,97 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -400,8 +474,97 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
400 } 474 }
401 ); 475 );
402 break; 476 break;
403 -  
404 - //TODO: Alias filter 477 + case types.aliasFilterType.relationsQuery.value:
  478 + result.stateEntity = filter.rootStateEntity;
  479 + var rootEntityType;
  480 + var rootEntityId;
  481 + if (result.stateEntity && stateParams && stateParams.entityId) {
  482 + rootEntityType = stateParams.entityId.entityType;
  483 + rootEntityId = stateParams.entityId.id;
  484 + } else if (!result.stateEntity) {
  485 + rootEntityType = filter.rootEntity.entityType;
  486 + rootEntityId = filter.rootEntity.id;
  487 + }
  488 + if (rootEntityType && rootEntityId) {
  489 + var searchQuery = {
  490 + parameters: {
  491 + rootId: rootEntityId,
  492 + rootType: rootEntityType,
  493 + direction: filter.direction
  494 + },
  495 + filters: filter.filters
  496 + };
  497 + searchQuery.parameters.maxLevel = filter.maxLevel && filter.maxLevel > 0 ? filter.maxLevel : -1;
  498 + entityRelationService.findInfoByQuery(searchQuery).then(
  499 + function success(allRelations) {
  500 + if (allRelations && allRelations.length) {
  501 + if (angular.isDefined(maxItems) && maxItems > 0) {
  502 + var limit = Math.min(allRelations.length, maxItems);
  503 + allRelations.length = limit;
  504 + }
  505 + result.entities = entityRelationInfosToEntitiesInfo(allRelations, filter.direction);
  506 + deferred.resolve(result);
  507 + } else {
  508 + deferred.reject();
  509 + }
  510 + },
  511 + function fail() {
  512 + deferred.reject();
  513 + }
  514 + );
  515 + } else {
  516 + deferred.resolve(result);
  517 + }
  518 + break;
  519 + case types.aliasFilterType.assetSearchQuery.value:
  520 + case types.aliasFilterType.deviceSearchQuery.value:
  521 + result.stateEntity = filter.rootStateEntity;
  522 + if (result.stateEntity && stateParams && stateParams.entityId) {
  523 + rootEntityType = stateParams.entityId.entityType;
  524 + rootEntityId = stateParams.entityId.id;
  525 + } else if (!result.stateEntity) {
  526 + rootEntityType = filter.rootEntity.entityType;
  527 + rootEntityId = filter.rootEntity.id;
  528 + }
  529 + if (rootEntityType && rootEntityId) {
  530 + searchQuery = {
  531 + parameters: {
  532 + rootId: rootEntityId,
  533 + rootType: rootEntityType,
  534 + direction: filter.direction
  535 + },
  536 + relationType: filter.relationType
  537 + };
  538 + searchQuery.parameters.maxLevel = filter.maxLevel && filter.maxLevel > 0 ? filter.maxLevel : -1;
  539 + var findByQueryPromise;
  540 + if (filter.type == types.aliasFilterType.assetSearchQuery.value) {
  541 + searchQuery.assetTypes = filter.assetTypes;
  542 + findByQueryPromise = assetService.findByQuery(searchQuery, false);
  543 + } else if (filter.type == types.aliasFilterType.deviceSearchQuery.value) {
  544 + searchQuery.deviceTypes = filter.deviceTypes;
  545 + findByQueryPromise = deviceService.findByQuery(searchQuery, false);
  546 + }
  547 + findByQueryPromise.then(
  548 + function success(entities) {
  549 + if (entities && entities.length) {
  550 + if (angular.isDefined(maxItems) && maxItems > 0) {
  551 + var limit = Math.min(entities.length, maxItems);
  552 + entities.length = limit;
  553 + }
  554 + result.entities = entitiesToEntitiesInfo(entities);
  555 + deferred.resolve(result);
  556 + } else {
  557 + deferred.reject();
  558 + }
  559 + },
  560 + function fail() {
  561 + deferred.reject();
  562 + }
  563 + );
  564 + } else {
  565 + deferred.resolve(result);
  566 + }
  567 + break;
405 } 568 }
406 return deferred.promise; 569 return deferred.promise;
407 } 570 }
@@ -420,9 +583,33 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -420,9 +583,33 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
420 return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false; 583 return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false;
421 case types.aliasFilterType.deviceType.value: 584 case types.aliasFilterType.deviceType.value:
422 return entityTypes.indexOf(types.entityType.device) > -1 ? true : false; 585 return entityTypes.indexOf(types.entityType.device) > -1 ? true : false;
  586 + case types.aliasFilterType.relationsQuery.value:
  587 + if (filter.filters && filter.filters.length) {
  588 + var match = false;
  589 + for (var f=0;f<filter.filters.length;f++) {
  590 + var relationFilter = filter.filters[f];
  591 + if (relationFilter.entityTypes && relationFilter.entityTypes.length) {
  592 + for (var et=0;et<relationFilter.entityTypes.length;et++) {
  593 + if (entityTypes.indexOf(relationFilter.entityTypes[et]) > -1) {
  594 + match = true;
  595 + break;
  596 + }
  597 + }
  598 + } else {
  599 + match = true;
  600 + break;
  601 + }
  602 + }
  603 + return match;
  604 + } else {
  605 + return true;
  606 + }
  607 + case types.aliasFilterType.assetSearchQuery.value:
  608 + return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false;
  609 + case types.aliasFilterType.deviceSearchQuery.value:
  610 + return entityTypes.indexOf(types.entityType.device) > -1 ? true : false;
423 } 611 }
424 } 612 }
425 - //TODO: Alias filter  
426 return false; 613 return false;
427 } 614 }
428 615
@@ -474,6 +661,42 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -474,6 +661,42 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
474 return result; 661 return result;
475 } 662 }
476 663
  664 + function prepareAllowedEntityTypesList(allowedEntityTypes) {
  665 + var authority = userService.getAuthority();
  666 + var entityTypes = {};
  667 + switch(authority) {
  668 + case 'SYS_ADMIN':
  669 + entityTypes.tenant = types.entityType.tenant;
  670 + entityTypes.rule = types.entityType.rule;
  671 + entityTypes.plugin = types.entityType.plugin;
  672 + break;
  673 + case 'TENANT_ADMIN':
  674 + entityTypes.device = types.entityType.device;
  675 + entityTypes.asset = types.entityType.asset;
  676 + entityTypes.tenant = types.entityType.tenant;
  677 + entityTypes.customer = types.entityType.customer;
  678 + entityTypes.rule = types.entityType.rule;
  679 + entityTypes.plugin = types.entityType.plugin;
  680 + entityTypes.dashboard = types.entityType.dashboard;
  681 + break;
  682 + case 'CUSTOMER_USER':
  683 + entityTypes.device = types.entityType.device;
  684 + entityTypes.asset = types.entityType.asset;
  685 + entityTypes.customer = types.entityType.customer;
  686 + entityTypes.dashboard = types.entityType.dashboard;
  687 + break;
  688 + }
  689 +
  690 + if (allowedEntityTypes) {
  691 + for (var entityType in entityTypes) {
  692 + if (allowedEntityTypes.indexOf(entityTypes[entityType]) === -1) {
  693 + delete entityTypes[entityType];
  694 + }
  695 + }
  696 + }
  697 + return entityTypes;
  698 + }
  699 +
477 700
478 function checkEntityAlias(entityAlias) { 701 function checkEntityAlias(entityAlias) {
479 var deferred = $q.defer(); 702 var deferred = $q.defer();
@@ -146,46 +146,55 @@ export default angular.module('thingsboard.types', []) @@ -146,46 +146,55 @@ export default angular.module('thingsboard.types', [])
146 entityTypeTranslations: { 146 entityTypeTranslations: {
147 "DEVICE": { 147 "DEVICE": {
148 type: 'entity.type-device', 148 type: 'entity.type-device',
  149 + typePlural: 'entity.type-devices',
149 list: 'entity.list-of-devices', 150 list: 'entity.list-of-devices',
150 nameStartsWith: 'entity.device-name-starts-with' 151 nameStartsWith: 'entity.device-name-starts-with'
151 }, 152 },
152 "ASSET": { 153 "ASSET": {
153 type: 'entity.type-asset', 154 type: 'entity.type-asset',
  155 + typePlural: 'entity.type-assets',
154 list: 'entity.list-of-assets', 156 list: 'entity.list-of-assets',
155 nameStartsWith: 'entity.asset-name-starts-with' 157 nameStartsWith: 'entity.asset-name-starts-with'
156 }, 158 },
157 "RULE": { 159 "RULE": {
158 type: 'entity.type-rule', 160 type: 'entity.type-rule',
  161 + typePlural: 'entity.type-rules',
159 list: 'entity.list-of-rules', 162 list: 'entity.list-of-rules',
160 nameStartsWith: 'entity.rule-name-starts-with' 163 nameStartsWith: 'entity.rule-name-starts-with'
161 }, 164 },
162 "PLUGIN": { 165 "PLUGIN": {
163 type: 'entity.type-plugin', 166 type: 'entity.type-plugin',
  167 + typePlural: 'entity.type-plugins',
164 list: 'entity.list-of-plugins', 168 list: 'entity.list-of-plugins',
165 nameStartsWith: 'entity.plugin-name-starts-with' 169 nameStartsWith: 'entity.plugin-name-starts-with'
166 }, 170 },
167 "TENANT": { 171 "TENANT": {
168 type: 'entity.type-tenant', 172 type: 'entity.type-tenant',
  173 + typePlural: 'entity.type-tenants',
169 list: 'entity.list-of-tenants', 174 list: 'entity.list-of-tenants',
170 nameStartsWith: 'entity.tenant-name-starts-with' 175 nameStartsWith: 'entity.tenant-name-starts-with'
171 }, 176 },
172 "CUSTOMER": { 177 "CUSTOMER": {
173 type: 'entity.type-customer', 178 type: 'entity.type-customer',
  179 + typePlural: 'entity.type-customers',
174 list: 'entity.list-of-customers', 180 list: 'entity.list-of-customers',
175 nameStartsWith: 'entity.customer-name-starts-with' 181 nameStartsWith: 'entity.customer-name-starts-with'
176 }, 182 },
177 "USER": { 183 "USER": {
178 type: 'entity.type-user', 184 type: 'entity.type-user',
  185 + typePlural: 'entity.type-users',
179 list: 'entity.list-of-users', 186 list: 'entity.list-of-users',
180 nameStartsWith: 'entity.user-name-starts-with' 187 nameStartsWith: 'entity.user-name-starts-with'
181 }, 188 },
182 "DASHBOARD": { 189 "DASHBOARD": {
183 type: 'entity.type-dashboard', 190 type: 'entity.type-dashboard',
  191 + typePlural: 'entity.type-dashboards',
184 list: 'entity.list-of-dashboards', 192 list: 'entity.list-of-dashboards',
185 nameStartsWith: 'entity.dashboard-name-starts-with' 193 nameStartsWith: 'entity.dashboard-name-starts-with'
186 }, 194 },
187 "ALARM": { 195 "ALARM": {
188 type: 'entity.type-alarm', 196 type: 'entity.type-alarm',
  197 + typePlural: 'entity.type-alarms',
189 list: 'entity.list-of-alarms', 198 list: 'entity.list-of-alarms',
190 nameStartsWith: 'entity.alarm-name-starts-with' 199 nameStartsWith: 'entity.alarm-name-starts-with'
191 } 200 }
@@ -71,6 +71,13 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, @@ -71,6 +71,13 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
71 } 71 }
72 }, true); 72 }, true);
73 73
  74 + scope.$watch('datasourceName', function () {
  75 + if (ngModelCtrl.$viewValue) {
  76 + ngModelCtrl.$viewValue.name = scope.datasourceName;
  77 + scope.updateValidity();
  78 + }
  79 + });
  80 +
74 ngModelCtrl.$render = function () { 81 ngModelCtrl.$render = function () {
75 if (ngModelCtrl.$viewValue) { 82 if (ngModelCtrl.$viewValue) {
76 var funcDataKeys = []; 83 var funcDataKeys = [];
@@ -78,6 +85,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, @@ -78,6 +85,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
78 funcDataKeys = funcDataKeys.concat(ngModelCtrl.$viewValue.dataKeys); 85 funcDataKeys = funcDataKeys.concat(ngModelCtrl.$viewValue.dataKeys);
79 } 86 }
80 scope.funcDataKeys = funcDataKeys; 87 scope.funcDataKeys = funcDataKeys;
  88 + scope.datasourceName = ngModelCtrl.$viewValue.name;
81 } 89 }
82 }; 90 };
83 91
@@ -15,23 +15,29 @@ @@ -15,23 +15,29 @@
15 */ 15 */
16 @import '../../scss/constants'; 16 @import '../../scss/constants';
17 17
18 -.tb-func-datakey-autocomplete {  
19 - .tb-not-found {  
20 - display: block;  
21 - line-height: 1.5;  
22 - height: 48px;  
23 - .tb-no-entries {  
24 - line-height: 48px;  
25 - } 18 +.tb-datasource-func {
  19 + @media (min-width: $layout-breakpoint-gt-sm) {
  20 + padding-left: 8px;
26 } 21 }
27 - li {  
28 - height: auto !important;  
29 - white-space: normal !important; 22 +
  23 + md-input-container.tb-datasource-name {
  24 + .md-errors-spacer {
  25 + display: none;
  26 + }
30 } 27 }
31 -}  
32 28
33 -tb-datasource-func {  
34 - @media (min-width: $layout-breakpoint-gt-sm) {  
35 - padding-left: 8px; 29 + .tb-func-datakey-autocomplete {
  30 + .tb-not-found {
  31 + display: block;
  32 + line-height: 1.5;
  33 + height: 48px;
  34 + .tb-no-entries {
  35 + line-height: 48px;
  36 + }
  37 + }
  38 + li {
  39 + height: auto !important;
  40 + white-space: normal !important;
  41 + }
36 } 42 }
37 -}  
  43 +}
@@ -15,59 +15,68 @@ @@ -15,59 +15,68 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<section flex layout='column' style="padding-left: 4px;">  
19 - <md-chips flex  
20 - id="function_datakey_chips"  
21 - ng-required="true"  
22 - ng-model="funcDataKeys" md-autocomplete-snap  
23 - md-transform-chip="transformDataKeyChip($chip)"  
24 - md-require-match="false">  
25 - <md-autocomplete  
26 - md-no-cache="false"  
27 - id="dataKey"  
28 - md-selected-item="selectedDataKey"  
29 - md-search-text="dataKeySearchText"  
30 - md-items="item in dataKeysSearch(dataKeySearchText)"  
31 - md-item-text="item.name"  
32 - md-min-length="0"  
33 - placeholder="{{ 'datakey.function-types' | translate }}"  
34 - md-menu-class="tb-func-datakey-autocomplete">  
35 - <span md-highlight-text="dataKeySearchText" md-highlight-flags="^i">{{item}}</span>  
36 - <md-not-found>  
37 - <div class="tb-not-found">  
38 - <div class="tb-no-entries" ng-if="!textIsNotEmpty(dataKeySearchText)">  
39 - <span translate>device.no-keys-found</span> 18 +<section class="tb-datasource-func" flex layout='column'
  19 + layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
  20 + <md-input-container class="tb-datasource-name" md-no-float style="min-width: 200px;">
  21 + <input name="datasourceName"
  22 + placeholder="{{ 'datasource.name' | translate }}"
  23 + ng-model="datasourceName"
  24 + aria-label="{{ 'datasource.name' | translate }}">
  25 + </md-input-container>
  26 + <section flex layout='column' style="padding-left: 4px;">
  27 + <md-chips flex
  28 + id="function_datakey_chips"
  29 + ng-required="true"
  30 + ng-model="funcDataKeys" md-autocomplete-snap
  31 + md-transform-chip="transformDataKeyChip($chip)"
  32 + md-require-match="false">
  33 + <md-autocomplete
  34 + md-no-cache="false"
  35 + id="dataKey"
  36 + md-selected-item="selectedDataKey"
  37 + md-search-text="dataKeySearchText"
  38 + md-items="item in dataKeysSearch(dataKeySearchText)"
  39 + md-item-text="item.name"
  40 + md-min-length="0"
  41 + placeholder="{{ 'datakey.function-types' | translate }}"
  42 + md-menu-class="tb-func-datakey-autocomplete">
  43 + <span md-highlight-text="dataKeySearchText" md-highlight-flags="^i">{{item}}</span>
  44 + <md-not-found>
  45 + <div class="tb-not-found">
  46 + <div class="tb-no-entries" ng-if="!textIsNotEmpty(dataKeySearchText)">
  47 + <span translate>device.no-keys-found</span>
  48 + </div>
  49 + <div ng-if="textIsNotEmpty(dataKeySearchText)">
  50 + <span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:&apos;...&apos;}}" }'>device.no-key-matching</span>
  51 + <span>
  52 + <a translate ng-click="createKey($event, '#function_datakey_chips')">device.create-new-key</a>
  53 + </span>
  54 + </div>
40 </div> 55 </div>
41 - <div ng-if="textIsNotEmpty(dataKeySearchText)">  
42 - <span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:&apos;...&apos;}}" }'>device.no-key-matching</span>  
43 - <span>  
44 - <a translate ng-click="createKey($event, '#function_datakey_chips')">device.create-new-key</a>  
45 - </span>  
46 - </div>  
47 - </div>  
48 - </md-not-found>  
49 - </md-autocomplete>  
50 - <md-chip-template>  
51 - <div layout="row" layout-align="start center" class="tb-attribute-chip">  
52 - <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">  
53 - <div class="tb-color-result" ng-style="{background: $chip.color}"></div>  
54 - </div>  
55 - <div layout="row" flex>  
56 - <div class="tb-chip-label">  
57 - {{$chip.label}} 56 + </md-not-found>
  57 + </md-autocomplete>
  58 + <md-chip-template>
  59 + <div layout="row" layout-align="start center" class="tb-attribute-chip">
  60 + <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
  61 + <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
58 </div> 62 </div>
59 - <div class="tb-chip-separator">: </div>  
60 - <div class="tb-chip-label">  
61 - <strong>{{$chip.name}}</strong> 63 + <div layout="row" flex>
  64 + <div class="tb-chip-label">
  65 + {{$chip.label}}
  66 + </div>
  67 + <div class="tb-chip-separator">: </div>
  68 + <div class="tb-chip-label">
  69 + <strong>{{$chip.name}}</strong>
  70 + </div>
62 </div> 71 </div>
  72 + <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
  73 + <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
  74 + </md-button>
63 </div> 75 </div>
64 - <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">  
65 - <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>  
66 - </md-button>  
67 - </div>  
68 - </md-chip-template>  
69 - </md-chips>  
70 - <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">  
71 - <div translate ng-message="funcTypes" class="tb-error-message">datakey.function-types-required</div>  
72 - </div> 76 + </md-chip-template>
  77 + </md-chips>
  78 + <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
  79 + <div translate ng-message="funcTypes" class="tb-error-message">datakey.function-types-required</div>
  80 + </div>
  81 + </section>
73 </section> 82 </section>
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 */ 15 */
16 /* eslint-disable import/no-unresolved, import/default */ 16 /* eslint-disable import/no-unresolved, import/default */
17 17
18 -import entityAliasDialogTemplate from '../entity/entity-alias-dialog.tpl.html'; 18 +import entityAliasDialogTemplate from '../entity/alias/entity-alias-dialog.tpl.html';
19 19
20 /* eslint-enable import/no-unresolved, import/default */ 20 /* eslint-enable import/no-unresolved, import/default */
21 21
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 */ 15 */
16 /* eslint-disable import/no-unresolved, import/default */ 16 /* eslint-disable import/no-unresolved, import/default */
17 17
18 -import entityAliasesTemplate from '../entity/entity-aliases.tpl.html'; 18 +import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
19 import dashboardSettingsTemplate from './dashboard-settings.tpl.html'; 19 import dashboardSettingsTemplate from './dashboard-settings.tpl.html';
20 import manageDashboardLayoutsTemplate from './layouts/manage-dashboard-layouts.tpl.html'; 20 import manageDashboardLayoutsTemplate from './layouts/manage-dashboard-layouts.tpl.html';
21 import manageDashboardStatesTemplate from './states/manage-dashboard-states.tpl.html'; 21 import manageDashboardStatesTemplate from './states/manage-dashboard-states.tpl.html';
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 */ 15 */
16 /* eslint-disable import/no-unresolved, import/default */ 16 /* eslint-disable import/no-unresolved, import/default */
17 17
18 -import entityAliasDialogTemplate from '../entity/entity-alias-dialog.tpl.html'; 18 +import entityAliasDialogTemplate from '../entity/alias/entity-alias-dialog.tpl.html';
19 import editWidgetTemplate from './edit-widget.tpl.html'; 19 import editWidgetTemplate from './edit-widget.tpl.html';
20 20
21 /* eslint-enable import/no-unresolved, import/default */ 21 /* eslint-enable import/no-unresolved, import/default */
ui/src/app/entity/alias/aliases-entity-select-button.tpl.html renamed from ui/src/app/entity/aliases-entity-select-button.tpl.html
ui/src/app/entity/alias/aliases-entity-select-panel.controller.js renamed from ui/src/app/entity/aliases-entity-select-panel.controller.js
ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html renamed from ui/src/app/entity/aliases-entity-select-panel.tpl.html
ui/src/app/entity/alias/aliases-entity-select.directive.js renamed from ui/src/app/entity/aliases-entity-select.directive.js
ui/src/app/entity/alias/aliases-entity-select.scss renamed from ui/src/app/entity/aliases-entity-select.scss
ui/src/app/entity/alias/entity-alias-dialog.controller.js renamed from ui/src/app/entity/entity-alias-dialog.controller.js
ui/src/app/entity/alias/entity-alias-dialog.scss renamed from ui/src/app/entity/entity-alias-dialog.scss
ui/src/app/entity/alias/entity-alias-dialog.tpl.html renamed from ui/src/app/entity/entity-alias-dialog.tpl.html
ui/src/app/entity/alias/entity-aliases.controller.js renamed from ui/src/app/entity/entity-aliases.controller.js
ui/src/app/entity/alias/entity-aliases.scss renamed from ui/src/app/entity/entity-aliases.scss
ui/src/app/entity/alias/entity-aliases.tpl.html renamed from ui/src/app/entity/entity-aliases.tpl.html
@@ -74,7 +74,106 @@ export default function EntityFilterViewDirective($compile, $templateCache, $q, @@ -74,7 +74,106 @@ export default function EntityFilterViewDirective($compile, $templateCache, $q,
74 scope.filterDisplayValue = $translate.instant('alias.filter-type-device-type-description', {deviceType: deviceType}); 74 scope.filterDisplayValue = $translate.instant('alias.filter-type-device-type-description', {deviceType: deviceType});
75 } 75 }
76 break; 76 break;
77 - //TODO: Alias filter 77 + case types.aliasFilterType.relationsQuery.value:
  78 + var rootEntityText;
  79 + var directionText;
  80 + var allEntitiesText = $translate.instant('alias.all-entities');
  81 + var anyRelationText = $translate.instant('alias.any-relation');
  82 + if (scope.filter.rootStateEntity) {
  83 + rootEntityText = $translate.instant('alias.state-entity');
  84 + } else {
  85 + rootEntityText = $translate.instant(types.entityTypeTranslations[scope.filter.rootEntity.entityType].type);
  86 + }
  87 + directionText = $translate.instant('relation.direction-type.' + scope.filter.direction);
  88 + var relationFilters = scope.filter.filters;
  89 + if (relationFilters && relationFilters.length) {
  90 + var relationFiltersDisplayValues = [];
  91 + relationFilters.forEach(function(relationFilter) {
  92 + var entitiesText;
  93 + if (relationFilter.entityTypes && relationFilter.entityTypes.length) {
  94 + var entitiesNamesList = [];
  95 + relationFilter.entityTypes.forEach(function(entityType) {
  96 + entitiesNamesList.push(
  97 + $translate.instant(types.entityTypeTranslations[entityType].typePlural)
  98 + );
  99 + });
  100 + entitiesText = entitiesNamesList.join(', ');
  101 + } else {
  102 + entitiesText = allEntitiesText;
  103 + }
  104 + var relationTypeText;
  105 + if (relationFilter.relationType && relationFilter.relationType.length) {
  106 + relationTypeText = "'" + relationFilter.relationType + "'";
  107 + } else {
  108 + relationTypeText = anyRelationText;
  109 + }
  110 + var relationFilterDisplayValue = $translate.instant('alias.filter-type-relations-query-description',
  111 + {
  112 + entities: entitiesText,
  113 + relationType: relationTypeText,
  114 + direction: directionText,
  115 + rootEntity: rootEntityText
  116 + }
  117 + );
  118 + relationFiltersDisplayValues.push(relationFilterDisplayValue);
  119 + });
  120 + scope.filterDisplayValue = relationFiltersDisplayValues.join(', ');
  121 + } else {
  122 + scope.filterDisplayValue = $translate.instant('alias.filter-type-relations-query-description',
  123 + {
  124 + entities: allEntitiesText,
  125 + relationType: anyRelationText,
  126 + direction: directionText,
  127 + rootEntity: rootEntityText
  128 + }
  129 + );
  130 + }
  131 + break;
  132 + case types.aliasFilterType.assetSearchQuery.value:
  133 + case types.aliasFilterType.deviceSearchQuery.value:
  134 + allEntitiesText = $translate.instant('alias.all-entities');
  135 + anyRelationText = $translate.instant('alias.any-relation');
  136 + if (scope.filter.rootStateEntity) {
  137 + rootEntityText = $translate.instant('alias.state-entity');
  138 + } else {
  139 + rootEntityText = $translate.instant(types.entityTypeTranslations[scope.filter.rootEntity.entityType].type);
  140 + }
  141 + directionText = $translate.instant('relation.direction-type.' + scope.filter.direction);
  142 + var relationTypeText;
  143 + if (scope.filter.relationType && scope.filter.relationType.length) {
  144 + relationTypeText = "'" + scope.filter.relationType + "'";
  145 + } else {
  146 + relationTypeText = anyRelationText;
  147 + }
  148 +
  149 + var translationValues = {
  150 + relationType: relationTypeText,
  151 + direction: directionText,
  152 + rootEntity: rootEntityText
  153 + }
  154 +
  155 + if (scope.filter.type == types.aliasFilterType.assetSearchQuery.value) {
  156 + var assetTypesQuoted = [];
  157 + scope.filter.assetTypes.forEach(function(assetType) {
  158 + assetTypesQuoted.push("'"+assetType+"'");
  159 + });
  160 + var assetTypesText = assetTypesQuoted.join(', ');
  161 + translationValues.assetTypes = assetTypesText;
  162 + scope.filterDisplayValue = $translate.instant('alias.filter-type-asset-search-query-description',
  163 + translationValues
  164 + );
  165 + } else {
  166 + var deviceTypesQuoted = [];
  167 + scope.filter.deviceTypes.forEach(function(deviceType) {
  168 + deviceTypesQuoted.push("'"+deviceType+"'");
  169 + });
  170 + var deviceTypesText = deviceTypesQuoted.join(', ');
  171 + translationValues.deviceTypes = deviceTypesText;
  172 + scope.filterDisplayValue = $translate.instant('alias.filter-type-device-search-query-description',
  173 + translationValues
  174 + );
  175 + }
  176 + break;
78 default: 177 default:
79 scope.filterDisplayValue = scope.filter.type; 178 scope.filterDisplayValue = scope.filter.type;
80 break; 179 break;
@@ -63,7 +63,23 @@ export default function EntityFilterDirective($compile, $templateCache, $q, $doc @@ -63,7 +63,23 @@ export default function EntityFilterDirective($compile, $templateCache, $q, $doc
63 filter.deviceType = null; 63 filter.deviceType = null;
64 filter.deviceNameFilter = ''; 64 filter.deviceNameFilter = '';
65 break; 65 break;
66 - //TODO: Alias filter 66 + case types.aliasFilterType.relationsQuery.value:
  67 + case types.aliasFilterType.assetSearchQuery.value:
  68 + case types.aliasFilterType.deviceSearchQuery.value:
  69 + filter.rootStateEntity = false;
  70 + filter.rootEntity = null;
  71 + filter.direction = types.entitySearchDirection.from;
  72 + filter.maxLevel = 1;
  73 + if (filter.type === types.aliasFilterType.relationsQuery.value) {
  74 + filter.filters = [];
  75 + } else if (filter.type === types.aliasFilterType.assetSearchQuery.value) {
  76 + filter.relationType = null;
  77 + filter.assetTypes = [];
  78 + } else if (filter.type === types.aliasFilterType.deviceSearchQuery.value) {
  79 + filter.relationType = null;
  80 + filter.deviceTypes = [];
  81 + }
  82 + break;
67 } 83 }
68 scope.filter = filter; 84 scope.filter = filter;
69 } 85 }
@@ -16,4 +16,21 @@ @@ -16,4 +16,21 @@
16 16
17 .tb-entity-filter { 17 .tb-entity-filter {
18 18
  19 + #relationsQueryFilter {
  20 + padding-top: 20px;
  21 + tb-entity-select {
  22 + min-height: 92px;
  23 + }
  24 + }
  25 +
  26 + .tb-root-state-entity-switch {
  27 + padding-left: 10px;
  28 + .root-state-entity-switch {
  29 + margin: 0;
  30 + }
  31 + .root-state-entity-label {
  32 + margin: 5px 0;
  33 + }
  34 + }
  35 +
19 } 36 }
@@ -88,4 +88,146 @@ @@ -88,4 +88,146 @@
88 aria-label="{{ 'device.name-starts-with' | translate }}"> 88 aria-label="{{ 'device.name-starts-with' | translate }}">
89 </md-input-container> 89 </md-input-container>
90 </section> 90 </section>
  91 + <section layout="column" ng-if="filter.type == types.aliasFilterType.relationsQuery.value" id="relationsQueryFilter">
  92 + <label class="tb-small">{{ 'alias.root-entity' | translate }}</label>
  93 + <div flex layout="row">
  94 + <tb-entity-select flex
  95 + the-form="theForm"
  96 + tb-required="!filter.rootStateEntity"
  97 + ng-disabled="filter.rootStateEntity"
  98 + ng-model="filter.rootEntity">
  99 + </tb-entity-select>
  100 + <section class="tb-root-state-entity-switch" layout="column" layout-align="start center">
  101 + <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
  102 + <md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
  103 + aria-label="{{ 'alias.root-state-entity' | translate }}">
  104 + </md-switch>
  105 + </section>
  106 + </div>
  107 + <div flex layout="row">
  108 + <md-input-container class="md-block" style="min-width: 100px;">
  109 + <label translate>relation.direction</label>
  110 + <md-select required ng-model="filter.direction">
  111 + <md-option ng-repeat="direction in types.entitySearchDirection" ng-value="direction">
  112 + {{ ('relation.search-direction.' + direction) | translate}}
  113 + </md-option>
  114 + </md-select>
  115 + </md-input-container>
  116 + <md-input-container flex class="md-block">
  117 + <label translate>alias.max-relation-level</label>
  118 + <input name="maxRelationLevel"
  119 + type="number"
  120 + min="1"
  121 + step="1"
  122 + placeholder="{{ 'alias.unlimited-level' | translate }}"
  123 + ng-model="filter.maxLevel"
  124 + aria-label="{{ 'alias.max-relation-level' | translate }}">
  125 + </md-input-container>
  126 + </div>
  127 + <div class="md-caption" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>relation.relation-filters</div>
  128 + <tb-relation-filters
  129 + ng-model="filter.filters"
  130 + allowed-entity-types="allowedEntityTypes">
  131 + </tb-relation-filters>
  132 + </section>
  133 + <section layout="column" ng-if="filter.type == types.aliasFilterType.assetSearchQuery.value" id="assetSearchQueryFilter">
  134 + <label class="tb-small">{{ 'alias.root-entity' | translate }}</label>
  135 + <div flex layout="row">
  136 + <tb-entity-select flex
  137 + the-form="theForm"
  138 + tb-required="!filter.rootStateEntity"
  139 + ng-disabled="filter.rootStateEntity"
  140 + ng-model="filter.rootEntity">
  141 + </tb-entity-select>
  142 + <section class="tb-root-state-entity-switch" layout="column" layout-align="start center">
  143 + <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
  144 + <md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
  145 + aria-label="{{ 'alias.root-state-entity' | translate }}">
  146 + </md-switch>
  147 + </section>
  148 + </div>
  149 + <div flex layout="row">
  150 + <md-input-container class="md-block" style="min-width: 100px;">
  151 + <label translate>relation.direction</label>
  152 + <md-select required ng-model="filter.direction">
  153 + <md-option ng-repeat="direction in types.entitySearchDirection" ng-value="direction">
  154 + {{ ('relation.search-direction.' + direction) | translate}}
  155 + </md-option>
  156 + </md-select>
  157 + </md-input-container>
  158 + <md-input-container flex class="md-block">
  159 + <label translate>alias.max-relation-level</label>
  160 + <input name="maxRelationLevel"
  161 + type="number"
  162 + min="1"
  163 + step="1"
  164 + placeholder="{{ 'alias.unlimited-level' | translate }}"
  165 + ng-model="filter.maxLevel"
  166 + aria-label="{{ 'alias.max-relation-level' | translate }}">
  167 + </md-input-container>
  168 + </div>
  169 + <div class="md-caption" style="color: rgba(0,0,0,0.57);" translate>relation.relation-type</div>
  170 + <tb-relation-type-autocomplete flex
  171 + hide-label
  172 + the-form="theForm"
  173 + ng-model="filter.relationType"
  174 + tb-required="false">
  175 + </tb-relation-type-autocomplete>
  176 + <div class="md-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>asset.asset-types</div>
  177 + <tb-entity-subtype-list
  178 + tb-required="true"
  179 + entity-type="types.entityType.asset"
  180 + ng-model="filter.assetTypes">
  181 + </tb-entity-subtype-list>
  182 + </section>
  183 + <section layout="column" ng-if="filter.type == types.aliasFilterType.deviceSearchQuery.value" id="deviceSearchQueryFilter">
  184 + <label class="tb-small">{{ 'alias.root-entity' | translate }}</label>
  185 + <div flex layout="row">
  186 + <tb-entity-select flex
  187 + the-form="theForm"
  188 + tb-required="!filter.rootStateEntity"
  189 + ng-disabled="filter.rootStateEntity"
  190 + ng-model="filter.rootEntity">
  191 + </tb-entity-select>
  192 + <section class="tb-root-state-entity-switch" layout="column" layout-align="start center">
  193 + <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
  194 + <md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
  195 + aria-label="{{ 'alias.root-state-entity' | translate }}">
  196 + </md-switch>
  197 + </section>
  198 + </div>
  199 + <div flex layout="row">
  200 + <md-input-container class="md-block" style="min-width: 100px;">
  201 + <label translate>relation.direction</label>
  202 + <md-select required ng-model="filter.direction">
  203 + <md-option ng-repeat="direction in types.entitySearchDirection" ng-value="direction">
  204 + {{ ('relation.search-direction.' + direction) | translate}}
  205 + </md-option>
  206 + </md-select>
  207 + </md-input-container>
  208 + <md-input-container flex class="md-block">
  209 + <label translate>alias.max-relation-level</label>
  210 + <input name="maxRelationLevel"
  211 + type="number"
  212 + min="1"
  213 + step="1"
  214 + placeholder="{{ 'alias.unlimited-level' | translate }}"
  215 + ng-model="filter.maxLevel"
  216 + aria-label="{{ 'alias.max-relation-level' | translate }}">
  217 + </md-input-container>
  218 + </div>
  219 + <div class="md-caption" style="color: rgba(0,0,0,0.57);" translate>relation.relation-type</div>
  220 + <tb-relation-type-autocomplete flex
  221 + hide-label
  222 + the-form="theForm"
  223 + ng-model="filter.relationType"
  224 + tb-required="false">
  225 + </tb-relation-type-autocomplete>
  226 + <div class="md-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>device.device-types</div>
  227 + <tb-entity-subtype-list
  228 + tb-required="true"
  229 + entity-type="types.entityType.device"
  230 + ng-model="filter.deviceTypes">
  231 + </tb-entity-subtype-list>
  232 + </section>
91 </div> 233 </div>
@@ -114,6 +114,9 @@ export default function EntitySubtypeAutocomplete($compile, $templateCache, $q, @@ -114,6 +114,9 @@ export default function EntitySubtypeAutocomplete($compile, $templateCache, $q,
114 scope.selectEntitySubtypeText = 'asset.select-asset-type'; 114 scope.selectEntitySubtypeText = 'asset.select-asset-type';
115 scope.entitySubtypeText = 'asset.asset-type'; 115 scope.entitySubtypeText = 'asset.asset-type';
116 scope.entitySubtypeRequiredText = 'asset.asset-type-required'; 116 scope.entitySubtypeRequiredText = 'asset.asset-type-required';
  117 + scope.$on('assetSaved', function() {
  118 + scope.entitySubtypes = null;
  119 + });
117 } else if (scope.entityType == types.entityType.device) { 120 } else if (scope.entityType == types.entityType.device) {
118 scope.selectEntitySubtypeText = 'device.select-device-type'; 121 scope.selectEntitySubtypeText = 'device.select-device-type';
119 scope.entitySubtypeText = 'device.device-type'; 122 scope.entitySubtypeText = 'device.device-type';
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +/* eslint-disable import/no-unresolved, import/default */
  18 +
  19 +import entitySubtypeListTemplate from './entity-subtype-list.tpl.html';
  20 +
  21 +/* eslint-enable import/no-unresolved, import/default */
  22 +
  23 +import './entity-subtype-list.scss';
  24 +
  25 +/*@ngInject*/
  26 +export default function EntitySubtypeListDirective($compile, $templateCache, $q, $mdUtil, $translate, $filter, types, assetService, deviceService) {
  27 +
  28 + var linker = function (scope, element, attrs, ngModelCtrl) {
  29 +
  30 + var template = $templateCache.get(entitySubtypeListTemplate);
  31 + element.html(template);
  32 +
  33 + scope.ngModelCtrl = ngModelCtrl;
  34 +
  35 +
  36 + scope.entitySubtypesList = [];
  37 + scope.entitySubtypes = null;
  38 +
  39 + if (scope.entityType == types.entityType.asset) {
  40 + scope.placeholder = scope.tbRequired ? $translate.instant('asset.enter-asset-type')
  41 + : $translate.instant('asset.any-asset');
  42 + scope.secondaryPlaceholder = '+' + $translate.instant('asset.asset-type');
  43 + scope.noSubtypesMathingText = 'asset.no-asset-types-matching';
  44 + scope.subtypeListEmptyText = 'asset.asset-type-list-empty';
  45 + } else if (scope.entityType == types.entityType.device) {
  46 + scope.placeholder = scope.tbRequired ? $translate.instant('device.enter-device-type')
  47 + : $translate.instant('device.any-device');
  48 + scope.secondaryPlaceholder = '+' + $translate.instant('device.device-type');
  49 + scope.noSubtypesMathingText = 'device.no-device-types-matching';
  50 + scope.subtypeListEmptyText = 'device.device-type-list-empty';
  51 + }
  52 +
  53 + scope.$watch('tbRequired', function () {
  54 + scope.updateValidity();
  55 + });
  56 +
  57 + scope.fetchEntitySubtypes = function(searchText) {
  58 + var deferred = $q.defer();
  59 + loadSubTypes().then(
  60 + function success(subTypes) {
  61 + var result = $filter('filter')(subTypes, {'$': searchText});
  62 + if (result && result.length) {
  63 + deferred.resolve(result);
  64 + } else {
  65 + deferred.resolve([searchText]);
  66 + }
  67 + },
  68 + function fail() {
  69 + deferred.reject();
  70 + }
  71 + );
  72 + return deferred.promise;
  73 + }
  74 +
  75 + scope.updateValidity = function() {
  76 + var value = ngModelCtrl.$viewValue;
  77 + var valid = !scope.tbRequired || value && value.length > 0;
  78 + ngModelCtrl.$setValidity('entitySubtypesList', valid);
  79 + }
  80 +
  81 + ngModelCtrl.$render = function () {
  82 + scope.entitySubtypesList = ngModelCtrl.$viewValue;
  83 + if (!scope.entitySubtypesList) {
  84 + scope.entitySubtypesList = [];
  85 + }
  86 + }
  87 +
  88 + scope.$watch('entitySubtypesList', function () {
  89 + ngModelCtrl.$setViewValue(scope.entitySubtypesList);
  90 + scope.updateValidity();
  91 + }, true);
  92 +
  93 + function loadSubTypes() {
  94 + var deferred = $q.defer();
  95 + if (!scope.entitySubtypes) {
  96 + var entitySubtypesPromise;
  97 + if (scope.entityType == types.entityType.asset) {
  98 + entitySubtypesPromise = assetService.getAssetTypes();
  99 + } else if (scope.entityType == types.entityType.device) {
  100 + entitySubtypesPromise = deviceService.getDeviceTypes();
  101 + }
  102 + if (entitySubtypesPromise) {
  103 + entitySubtypesPromise.then(
  104 + function success(types) {
  105 + scope.entitySubtypes = [];
  106 + types.forEach(function (type) {
  107 + scope.entitySubtypes.push(type.type);
  108 + });
  109 + deferred.resolve(scope.entitySubtypes);
  110 + },
  111 + function fail() {
  112 + deferred.reject();
  113 + }
  114 + );
  115 + } else {
  116 + deferred.reject();
  117 + }
  118 + } else {
  119 + deferred.resolve(scope.entitySubtypes);
  120 + }
  121 + return deferred.promise;
  122 + }
  123 +
  124 + $compile(element.contents())(scope);
  125 +
  126 + $mdUtil.nextTick(function(){
  127 + var inputElement = angular.element('input', element);
  128 + inputElement.on('blur', function() {
  129 + scope.inputTouched = true;
  130 + } );
  131 + });
  132 +
  133 + }
  134 +
  135 + return {
  136 + restrict: "E",
  137 + require: "^ngModel",
  138 + link: linker,
  139 + scope: {
  140 + disabled:'=ngDisabled',
  141 + tbRequired: '=?',
  142 + entityType: "="
  143 + }
  144 + };
  145 +
  146 +}
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +/*.tb-entity-subtype-list {
  18 + #entity_subtype_list_chips {
  19 + .md-chips {
  20 + padding-bottom: 1px;
  21 + }
  22 + }
  23 + .tb-error-messages {
  24 + margin-top: -11px;
  25 + height: 35px;
  26 + .tb-error-message {
  27 + padding-left: 1px;
  28 + }
  29 + }
  30 +}*/
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +
  19 +<section flex layout='column' class="tb-entity-subtype-list">
  20 + <md-chips flex
  21 + readonly="disabled"
  22 + id="entity_subtype_list_chips"
  23 + ng-required="tbRequired"
  24 + ng-model="entitySubtypesList"
  25 + placeholder="{{placeholder}}"
  26 + secondary-placeholder="{{secondaryPlaceholder}}"
  27 + md-autocomplete-snap
  28 + md-require-match="false">
  29 + <md-autocomplete
  30 + md-no-cache="true"
  31 + id="entitySubtype"
  32 + md-selected-item="selectedEntitySubtype"
  33 + md-search-text="entitySubtypeSearchText"
  34 + md-items="item in fetchEntitySubtypes(entitySubtypeSearchText)"
  35 + md-item-text="item"
  36 + md-min-length="0"
  37 + placeholder="{{ (!entitySubtypesList || !entitySubtypesList.length) ? placeholder : secondaryPlaceholder }}">
  38 + <md-item-template>
  39 + <span md-highlight-text="entitySubtypeSearchText" md-highlight-flags="^i">{{item}}</span>
  40 + </md-item-template>
  41 + <md-not-found>
  42 + <span translate translate-values='{ entitySubtype: entitySubtypeSearchText }'>{{noSubtypesMathingText}}</span>
  43 + </md-not-found>
  44 + </md-autocomplete>
  45 + <md-chip-template>
  46 + <span>
  47 + <strong>{{$chip}}</strong>
  48 + </span>
  49 + </md-chip-template>
  50 + </md-chips>
  51 + <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" ng-if="inputTouched && tbRequired" role="alert">
  52 + <div translate ng-message="entitySubtypesList" class="tb-error-message">{{subtypeListEmptyText}}</div>
  53 + </div>
  54 +</section>
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +/* eslint-disable import/no-unresolved, import/default */
  18 +
  19 +import entityTypeListTemplate from './entity-type-list.tpl.html';
  20 +
  21 +/* eslint-enable import/no-unresolved, import/default */
  22 +
  23 +import './entity-type-list.scss';
  24 +
  25 +/*@ngInject*/
  26 +export default function EntityTypeListDirective($compile, $templateCache, $q, $mdUtil, $translate, $filter, types, entityService) {
  27 +
  28 + var linker = function (scope, element, attrs, ngModelCtrl) {
  29 +
  30 + var template = $templateCache.get(entityTypeListTemplate);
  31 + element.html(template);
  32 +
  33 + scope.ngModelCtrl = ngModelCtrl;
  34 +
  35 + scope.placeholder = scope.tbRequired ? $translate.instant('entity.enter-entity-type')
  36 + : $translate.instant('entity.any-entity');
  37 + scope.secondaryPlaceholder = '+' + $translate.instant('entity.entity-type');
  38 +
  39 + var entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes);
  40 + scope.entityTypesList = [];
  41 + for (var type in entityTypes) {
  42 + var entityTypeInfo = {};
  43 + entityTypeInfo.value = entityTypes[type];
  44 + entityTypeInfo.name = $translate.instant(types.entityTypeTranslations[entityTypeInfo.value].type) + '';
  45 + scope.entityTypesList.push(entityTypeInfo);
  46 + }
  47 +
  48 + scope.$watch('tbRequired', function () {
  49 + scope.updateValidity();
  50 + });
  51 +
  52 + scope.fetchEntityTypes = function(searchText) {
  53 + var deferred = $q.defer();
  54 + var entityTypes = $filter('filter')(scope.entityTypesList, {name: searchText});
  55 + deferred.resolve(entityTypes);
  56 + return deferred.promise;
  57 + }
  58 +
  59 + scope.updateValidity = function() {
  60 + var value = ngModelCtrl.$viewValue;
  61 + var valid = !scope.tbRequired || value && value.length > 0;
  62 + ngModelCtrl.$setValidity('entityTypeList', valid);
  63 + }
  64 +
  65 + ngModelCtrl.$render = function () {
  66 + scope.entityTypeList = [];
  67 + var value = ngModelCtrl.$viewValue;
  68 + if (value && value.length) {
  69 + value.forEach(function(type) {
  70 + var entityTypeInfo = {};
  71 + entityTypeInfo.value = type;
  72 + entityTypeInfo.name = $translate.instant(types.entityTypeTranslations[entityTypeInfo.value].type) + '';
  73 + scope.entityTypeList.push(entityTypeInfo);
  74 + });
  75 + }
  76 + }
  77 +
  78 + scope.$watch('entityTypeList', function () {
  79 + var values = [];
  80 + if (scope.entityTypeList && scope.entityTypeList.length) {
  81 + scope.entityTypeList.forEach(function(entityType) {
  82 + values.push(entityType.value);
  83 + });
  84 + }
  85 + ngModelCtrl.$setViewValue(values);
  86 + scope.updateValidity();
  87 + }, true);
  88 +
  89 + $compile(element.contents())(scope);
  90 +
  91 + $mdUtil.nextTick(function(){
  92 + var inputElement = angular.element('input', element);
  93 + inputElement.on('blur', function() {
  94 + scope.inputTouched = true;
  95 + } );
  96 + });
  97 +
  98 + }
  99 +
  100 + return {
  101 + restrict: "E",
  102 + require: "^ngModel",
  103 + link: linker,
  104 + scope: {
  105 + disabled:'=ngDisabled',
  106 + tbRequired: '=?',
  107 + allowedEntityTypes: '=?'
  108 + }
  109 + };
  110 +
  111 +}
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +/*.tb-entity-type-list {
  18 + #entity_type_list_chips {
  19 + .md-chips {
  20 + padding-bottom: 1px;
  21 + }
  22 + }
  23 + .tb-error-messages {
  24 + margin-top: -11px;
  25 + height: 35px;
  26 + .tb-error-message {
  27 + padding-left: 1px;
  28 + }
  29 + }
  30 +}*/
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +
  19 +<section flex layout='column' class="tb-entity-type-list">
  20 + <md-chips flex
  21 + readonly="disabled"
  22 + id="entity_type_list_chips"
  23 + ng-required="tbRequired"
  24 + ng-model="entityTypeList"
  25 + placeholder="{{placeholder}}"
  26 + secondary-placeholder="{{secondaryPlaceholder}}"
  27 + md-autocomplete-snap
  28 + md-require-match="true">
  29 + <md-autocomplete
  30 + md-no-cache="true"
  31 + id="entityType"
  32 + md-selected-item="selectedEntityType"
  33 + md-search-text="entityTypeSearchText"
  34 + md-items="item in fetchEntityTypes(entityTypeSearchText)"
  35 + md-item-text="item.name"
  36 + md-min-length="0"
  37 + placeholder="{{ (!entityTypeList || !entityTypeList.length) ? placeholder : secondaryPlaceholder }}">
  38 + <md-item-template>
  39 + <span md-highlight-text="entityTypeSearchText" md-highlight-flags="^i">{{item.name}}</span>
  40 + </md-item-template>
  41 + <md-not-found>
  42 + <span translate translate-values='{ entityType: entityTypeSearchText }'>entity.no-entity-types-matching</span>
  43 + </md-not-found>
  44 + </md-autocomplete>
  45 + <md-chip-template>
  46 + <span>
  47 + <strong>{{$chip.name}}</strong>
  48 + </span>
  49 + </md-chip-template>
  50 + </md-chips>
  51 + <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" ng-if="inputTouched && tbRequired" role="alert">
  52 + <div translate ng-message="entityTypeList" class="tb-error-message">entity.entity-type-list-empty</div>
  53 + </div>
  54 +</section>
@@ -23,7 +23,7 @@ import entityTypeSelectTemplate from './entity-type-select.tpl.html'; @@ -23,7 +23,7 @@ import entityTypeSelectTemplate from './entity-type-select.tpl.html';
23 /* eslint-enable import/no-unresolved, import/default */ 23 /* eslint-enable import/no-unresolved, import/default */
24 24
25 /*@ngInject*/ 25 /*@ngInject*/
26 -export default function EntityTypeSelect($compile, $templateCache, utils, userService, types) { 26 +export default function EntityTypeSelect($compile, $templateCache, utils, entityService, userService, types) {
27 27
28 var linker = function (scope, element, attrs, ngModelCtrl) { 28 var linker = function (scope, element, attrs, ngModelCtrl) {
29 var template = $templateCache.get(entityTypeSelectTemplate); 29 var template = $templateCache.get(entityTypeSelectTemplate);
@@ -39,36 +39,7 @@ export default function EntityTypeSelect($compile, $templateCache, utils, userSe @@ -39,36 +39,7 @@ export default function EntityTypeSelect($compile, $templateCache, utils, userSe
39 39
40 scope.ngModelCtrl = ngModelCtrl; 40 scope.ngModelCtrl = ngModelCtrl;
41 41
42 - var authority = userService.getAuthority();  
43 - scope.entityTypes = {};  
44 - switch(authority) {  
45 - case 'SYS_ADMIN':  
46 - scope.entityTypes.tenant = types.entityType.tenant;  
47 - scope.entityTypes.rule = types.entityType.rule;  
48 - scope.entityTypes.plugin = types.entityType.plugin;  
49 - break;  
50 - case 'TENANT_ADMIN':  
51 - scope.entityTypes.device = types.entityType.device;  
52 - scope.entityTypes.asset = types.entityType.asset;  
53 - scope.entityTypes.customer = types.entityType.customer;  
54 - scope.entityTypes.rule = types.entityType.rule;  
55 - scope.entityTypes.plugin = types.entityType.plugin;  
56 - scope.entityTypes.dashboard = types.entityType.dashboard;  
57 - break;  
58 - case 'CUSTOMER_USER':  
59 - scope.entityTypes.device = types.entityType.device;  
60 - scope.entityTypes.asset = types.entityType.asset;  
61 - scope.entityTypes.dashboard = types.entityType.dashboard;  
62 - break;  
63 - }  
64 -  
65 - if (scope.allowedEntityTypes) {  
66 - for (var entityType in scope.entityTypes) {  
67 - if (scope.allowedEntityTypes.indexOf(scope.entityTypes[entityType]) === -1) {  
68 - delete scope.entityTypes[entityType];  
69 - }  
70 - }  
71 - } 42 + scope.entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes);
72 43
73 scope.typeName = function(type) { 44 scope.typeName = function(type) {
74 return type ? types.entityTypeTranslations[type].type : ''; 45 return type ? types.entityTypeTranslations[type].type : '';
@@ -14,9 +14,11 @@ @@ -14,9 +14,11 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 16
17 -import EntityAliasesController from './entity-aliases.controller';  
18 -import EntityAliasDialogController from './entity-alias-dialog.controller'; 17 +import EntityAliasesController from './alias/entity-aliases.controller';
  18 +import EntityAliasDialogController from './alias/entity-alias-dialog.controller';
19 import EntityTypeSelectDirective from './entity-type-select.directive'; 19 import EntityTypeSelectDirective from './entity-type-select.directive';
  20 +import EntityTypeListDirective from './entity-type-list.directive';
  21 +import EntitySubtypeListDirective from './entity-subtype-list.directive';
20 import EntitySubtypeSelectDirective from './entity-subtype-select.directive'; 22 import EntitySubtypeSelectDirective from './entity-subtype-select.directive';
21 import EntitySubtypeAutocompleteDirective from './entity-subtype-autocomplete.directive'; 23 import EntitySubtypeAutocompleteDirective from './entity-subtype-autocomplete.directive';
22 import EntityAutocompleteDirective from './entity-autocomplete.directive'; 24 import EntityAutocompleteDirective from './entity-autocomplete.directive';
@@ -24,11 +26,12 @@ import EntityListDirective from './entity-list.directive'; @@ -24,11 +26,12 @@ import EntityListDirective from './entity-list.directive';
24 import EntitySelectDirective from './entity-select.directive'; 26 import EntitySelectDirective from './entity-select.directive';
25 import EntityFilterDirective from './entity-filter.directive'; 27 import EntityFilterDirective from './entity-filter.directive';
26 import EntityFilterViewDirective from './entity-filter-view.directive'; 28 import EntityFilterViewDirective from './entity-filter-view.directive';
27 -import AliasesEntitySelectPanelController from './aliases-entity-select-panel.controller';  
28 -import AliasesEntitySelectDirective from './aliases-entity-select.directive'; 29 +import AliasesEntitySelectPanelController from './alias/aliases-entity-select-panel.controller';
  30 +import AliasesEntitySelectDirective from './alias/aliases-entity-select.directive';
29 import AddAttributeDialogController from './attribute/add-attribute-dialog.controller'; 31 import AddAttributeDialogController from './attribute/add-attribute-dialog.controller';
30 import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller'; 32 import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller';
31 import AttributeTableDirective from './attribute/attribute-table.directive'; 33 import AttributeTableDirective from './attribute/attribute-table.directive';
  34 +import RelationFiltersDirective from './relation/relation-filters.directive';
32 import RelationTableDirective from './relation/relation-table.directive'; 35 import RelationTableDirective from './relation/relation-table.directive';
33 import RelationTypeAutocompleteDirective from './relation/relation-type-autocomplete.directive'; 36 import RelationTypeAutocompleteDirective from './relation/relation-type-autocomplete.directive';
34 37
@@ -39,6 +42,8 @@ export default angular.module('thingsboard.entity', []) @@ -39,6 +42,8 @@ export default angular.module('thingsboard.entity', [])
39 .controller('AddAttributeDialogController', AddAttributeDialogController) 42 .controller('AddAttributeDialogController', AddAttributeDialogController)
40 .controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController) 43 .controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController)
41 .directive('tbEntityTypeSelect', EntityTypeSelectDirective) 44 .directive('tbEntityTypeSelect', EntityTypeSelectDirective)
  45 + .directive('tbEntityTypeList', EntityTypeListDirective)
  46 + .directive('tbEntitySubtypeList', EntitySubtypeListDirective)
42 .directive('tbEntitySubtypeSelect', EntitySubtypeSelectDirective) 47 .directive('tbEntitySubtypeSelect', EntitySubtypeSelectDirective)
43 .directive('tbEntitySubtypeAutocomplete', EntitySubtypeAutocompleteDirective) 48 .directive('tbEntitySubtypeAutocomplete', EntitySubtypeAutocompleteDirective)
44 .directive('tbEntityAutocomplete', EntityAutocompleteDirective) 49 .directive('tbEntityAutocomplete', EntityAutocompleteDirective)
@@ -48,6 +53,7 @@ export default angular.module('thingsboard.entity', []) @@ -48,6 +53,7 @@ export default angular.module('thingsboard.entity', [])
48 .directive('tbEntityFilterView', EntityFilterViewDirective) 53 .directive('tbEntityFilterView', EntityFilterViewDirective)
49 .directive('tbAliasesEntitySelect', AliasesEntitySelectDirective) 54 .directive('tbAliasesEntitySelect', AliasesEntitySelectDirective)
50 .directive('tbAttributeTable', AttributeTableDirective) 55 .directive('tbAttributeTable', AttributeTableDirective)
  56 + .directive('tbRelationFilters', RelationFiltersDirective)
51 .directive('tbRelationTable', RelationTableDirective) 57 .directive('tbRelationTable', RelationTableDirective)
52 .directive('tbRelationTypeAutocomplete', RelationTypeAutocompleteDirective) 58 .directive('tbRelationTypeAutocomplete', RelationTypeAutocompleteDirective)
53 .name; 59 .name;
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import './relation-filters.scss';
  18 +
  19 +/* eslint-disable import/no-unresolved, import/default */
  20 +
  21 +import relationFiltersTemplate from './relation-filters.tpl.html';
  22 +
  23 +/* eslint-enable import/no-unresolved, import/default */
  24 +
  25 +/*@ngInject*/
  26 +export default function RelationFilters($compile, $templateCache) {
  27 +
  28 + return {
  29 + restrict: "E",
  30 + require: "^ngModel",
  31 + scope: {
  32 + allowedEntityTypes: '=?'
  33 + },
  34 + link: linker
  35 + };
  36 +
  37 + function linker( scope, element, attrs, ngModelCtrl ) {
  38 +
  39 + var template = $templateCache.get(relationFiltersTemplate);
  40 + element.html(template);
  41 +
  42 + scope.relationFilters = [];
  43 +
  44 + scope.addFilter = addFilter;
  45 + scope.removeFilter = removeFilter;
  46 +
  47 + ngModelCtrl.$render = function () {
  48 + if (ngModelCtrl.$viewValue) {
  49 + var value = ngModelCtrl.$viewValue;
  50 + value.forEach(function (filter) {
  51 + scope.relationFilters.push(filter);
  52 + });
  53 + }
  54 + scope.$watch('relationFilters', function (newVal, prevVal) {
  55 + if (!angular.equals(newVal, prevVal)) {
  56 + updateValue();
  57 + }
  58 + }, true);
  59 + }
  60 +
  61 + function addFilter() {
  62 + var filter = {
  63 + relationType: null,
  64 + entityTypes: []
  65 + };
  66 + scope.relationFilters.push(filter);
  67 + }
  68 +
  69 + function removeFilter($event, filter) {
  70 + var index = scope.relationFilters.indexOf(filter);
  71 + if (index > -1) {
  72 + scope.relationFilters.splice(index, 1);
  73 + }
  74 + }
  75 +
  76 + function updateValue() {
  77 + var value = [];
  78 + scope.relationFilters.forEach(function (filter) {
  79 + value.push(filter);
  80 + });
  81 + ngModelCtrl.$setViewValue(value);
  82 + }
  83 + $compile(element.contents())(scope);
  84 + }
  85 +}
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +.tb-relation-filters {
  18 + .header {
  19 + padding-left: 5px;
  20 + padding-right: 5px;
  21 + padding-bottom: 5px;
  22 + .cell {
  23 + padding-left: 5px;
  24 + padding-right: 5px;
  25 + color: rgba(0,0,0,.54);
  26 + font-size: 12px;
  27 + font-weight: 700;
  28 + white-space: nowrap;
  29 + }
  30 + }
  31 + .body {
  32 + padding-left: 5px;
  33 + padding-right: 5px;
  34 + max-height: 300px;
  35 + overflow: auto;
  36 + padding-bottom: 20px;
  37 + .row {
  38 + padding-top: 5px;
  39 + }
  40 + .cell {
  41 + padding-left: 5px;
  42 + padding-right: 5px;
  43 +
  44 + md-select {
  45 + margin: 0 0 24px;
  46 + }
  47 +
  48 + md-input-container {
  49 + margin: 0;
  50 + }
  51 +
  52 + md-chips-wrap {
  53 + padding: 0px;
  54 + margin: 0 0 24px;
  55 + .md-chip-input-container {
  56 + margin: 0;
  57 + }
  58 + md-autocomplete {
  59 + height: 30px;
  60 + md-autocomplete-wrap {
  61 + height: 30px;
  62 + }
  63 + }
  64 + }
  65 + .md-chips .md-chip-input-container input {
  66 + padding: 2px 2px 2px;
  67 + height: 26px;
  68 + line-height: 26px;
  69 + }
  70 +
  71 + }
  72 +
  73 + .md-button {
  74 + margin: 0;
  75 + }
  76 + }
  77 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-relation-filters">
  19 + <div class="header" ng-show="relationFilters.length">
  20 + <div layout="row" layout-align="start center">
  21 + <span class="cell" style="width: 200px; min-width: 200px;" translate>relation.type</span>
  22 + <span class="cell" flex translate>entity.entity-types</span>
  23 + <span class="cell" style="width: 40px; min-width: 40px;">&nbsp</span>
  24 + </div>
  25 + </div>
  26 + <div class="body" ng-show="relationFilters.length">
  27 + <div class="row" ng-form name="relationFilterForm" flex layout="row" layout-align="start center" ng-repeat="filter in relationFilters track by $index">
  28 + <div class="md-whiteframe-1dp" flex layout="row" layout-align="start center">
  29 + <tb-relation-type-autocomplete class="cell" style="width: 200px; min-width: 200px;"
  30 + hide-label
  31 + the-form="relationFilterForm"
  32 + ng-model="filter.relationType"
  33 + tb-required="false">
  34 + </tb-relation-type-autocomplete>
  35 + <tb-entity-type-list class="cell" flex
  36 + ng-model="filter.entityTypes"
  37 + allowed-entity-types="allowedEntityTypes"
  38 + tb-required="false">
  39 + </tb-entity-type-list>
  40 + <md-button ng-disabled="loading" class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"
  41 + ng-click="removeFilter($event, filter)" aria-label="{{ 'action.remove' | translate }}">
  42 + <md-tooltip md-direction="top">
  43 + {{ 'relation.remove-relation-filter' | translate }}
  44 + </md-tooltip>
  45 + <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">
  46 + close
  47 + </md-icon>
  48 + </md-button>
  49 + </div>
  50 + </div>
  51 + </div>
  52 + <div class="any-filter" ng-show="!relationFilters.length">
  53 + <span layout-align="center center"
  54 + class="tb-prompt" translate>relation.any-relation</span>
  55 + </div>
  56 + <div>
  57 + <md-button ng-disabled="loading" class="md-primary md-raised" ng-click="addFilter($event)" aria-label="{{ 'action.add' | translate }}">
  58 + <md-tooltip md-direction="top">
  59 + {{ 'relation.add-relation-filter' | translate }}
  60 + </md-tooltip>
  61 + <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons">
  62 + add
  63 + </md-icon>
  64 + {{ 'action.add' | translate }}
  65 + </md-button>
  66 + </div>
  67 +</div>
@@ -29,6 +29,8 @@ export default function RelationTypeAutocomplete($compile, $templateCache, $q, $ @@ -29,6 +29,8 @@ export default function RelationTypeAutocomplete($compile, $templateCache, $q, $
29 element.html(template); 29 element.html(template);
30 30
31 scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; 31 scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
  32 + scope.hideLabel = angular.isDefined(attrs.hideLabel) ? true : false;
  33 +
32 scope.relationType = null; 34 scope.relationType = null;
33 scope.relationTypeSearchText = ''; 35 scope.relationTypeSearchText = '';
34 scope.relationTypes = []; 36 scope.relationTypes = [];
@@ -26,7 +26,7 @@ @@ -26,7 +26,7 @@
26 md-items="item in fetchRelationTypes(relationTypeSearchText)" 26 md-items="item in fetchRelationTypes(relationTypeSearchText)"
27 md-item-text="item" 27 md-item-text="item"
28 md-min-length="0" 28 md-min-length="0"
29 - md-floating-label="{{ 'relation.relation-type' | translate }}" 29 + md-floating-label="{{ tbRequired ? ('relation.relation-type' | translate) : ( !relationType ? ('relation.any-relation-type' | translate) : ' ') }}"
30 md-select-on-match="true" 30 md-select-on-match="true"
31 md-menu-class="tb-relation-type-autocomplete"> 31 md-menu-class="tb-relation-type-autocomplete">
32 <md-item-template> 32 <md-item-template>
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 /* eslint-disable import/no-unresolved, import/default */ 16 /* eslint-disable import/no-unresolved, import/default */
17 17
18 import importDialogTemplate from './import-dialog.tpl.html'; 18 import importDialogTemplate from './import-dialog.tpl.html';
19 -import entityAliasesTemplate from '../entity/entity-aliases.tpl.html'; 19 +import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
20 20
21 /* eslint-enable import/no-unresolved, import/default */ 21 /* eslint-enable import/no-unresolved, import/default */
22 22
@@ -129,14 +129,24 @@ export default angular.module('thingsboard.locale', []) @@ -129,14 +129,24 @@ export default angular.module('thingsboard.locale', [])
129 "filter-type-device-type-description": "Devices of type '{{deviceType}}'", 129 "filter-type-device-type-description": "Devices of type '{{deviceType}}'",
130 "filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'", 130 "filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'",
131 "filter-type-relations-query": "Relations query", 131 "filter-type-relations-query": "Relations query",
  132 + "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
132 "filter-type-asset-search-query": "Asset search query", 133 "filter-type-asset-search-query": "Asset search query",
  134 + "filter-type-asset-search-query-description": "Assets with types {{assetTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
133 "filter-type-device-search-query": "Device search query", 135 "filter-type-device-search-query": "Device search query",
  136 + "filter-type-device-search-query-description": "Devices with types {{deviceTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
134 "entity-filter": "Entity filter", 137 "entity-filter": "Entity filter",
135 "resolve-multiple": "Resolve as multiple entities", 138 "resolve-multiple": "Resolve as multiple entities",
136 "filter-type": "Filter type", 139 "filter-type": "Filter type",
137 "filter-type-required": "Filter type is required.", 140 "filter-type-required": "Filter type is required.",
138 "entity-filter-no-entity-matched": "No entities matching specified filter were found.", 141 "entity-filter-no-entity-matched": "No entities matching specified filter were found.",
139 - "no-entity-filter-specified": "No entity filter specified" 142 + "no-entity-filter-specified": "No entity filter specified",
  143 + "root-state-entity": "Use dashboard state entity as root",
  144 + "root-entity": "Root entity",
  145 + "max-relation-level": "Max relation level",
  146 + "unlimited-level": "Unlimited level",
  147 + "state-entity": "Dashboard state entity",
  148 + "all-entities": "All entities",
  149 + "any-relation": "any"
140 }, 150 },
141 "asset": { 151 "asset": {
142 "asset": "Asset", 152 "asset": "Asset",
@@ -159,6 +169,11 @@ export default angular.module('thingsboard.locale', []) @@ -159,6 +169,11 @@ export default angular.module('thingsboard.locale', [])
159 "asset-type": "Asset type", 169 "asset-type": "Asset type",
160 "asset-type-required": "Asset type is required.", 170 "asset-type-required": "Asset type is required.",
161 "select-asset-type": "Select asset type", 171 "select-asset-type": "Select asset type",
  172 + "enter-asset-type": "Enter asset type",
  173 + "any-asset": "Any asset",
  174 + "no-asset-types-matching": "No asset types matching '{{entitySubtype}}' were found.",
  175 + "asset-type-list-empty": "No asset types selected.",
  176 + "asset-types": "Asset types",
162 "name": "Name", 177 "name": "Name",
163 "name-required": "Name is required.", 178 "name-required": "Name is required.",
164 "description": "Description", 179 "description": "Description",
@@ -444,6 +459,7 @@ export default angular.module('thingsboard.locale', []) @@ -444,6 +459,7 @@ export default angular.module('thingsboard.locale', [])
444 }, 459 },
445 "datasource": { 460 "datasource": {
446 "type": "Datasource type", 461 "type": "Datasource type",
  462 + "name": "Name",
447 "add-datasource-prompt": "Please add datasource" 463 "add-datasource-prompt": "Please add datasource"
448 }, 464 },
449 "details": { 465 "details": {
@@ -524,6 +540,11 @@ export default angular.module('thingsboard.locale', []) @@ -524,6 +540,11 @@ export default angular.module('thingsboard.locale', [])
524 "device-type": "Device type", 540 "device-type": "Device type",
525 "device-type-required": "Device type is required.", 541 "device-type-required": "Device type is required.",
526 "select-device-type": "Select device type", 542 "select-device-type": "Select device type",
  543 + "enter-device-type": "Enter device type",
  544 + "any-device": "Any device",
  545 + "no-device-types-matching": "No device types matching '{{entitySubtype}}' were found.",
  546 + "device-type-list-empty": "No device types selected.",
  547 + "device-types": "Device types",
527 "name": "Name", 548 "name": "Name",
528 "name-required": "Name is required.", 549 "name-required": "Name is required.",
529 "description": "Description", 550 "description": "Description",
@@ -564,10 +585,17 @@ export default angular.module('thingsboard.locale', []) @@ -564,10 +585,17 @@ export default angular.module('thingsboard.locale', [])
564 "remove-alias": "Remove entity alias", 585 "remove-alias": "Remove entity alias",
565 "add-alias": "Add entity alias", 586 "add-alias": "Add entity alias",
566 "entity-list": "Entity list", 587 "entity-list": "Entity list",
  588 + "entity-type": "Entity type",
  589 + "entity-types": "Entity types",
  590 + "entity-type-list": "Entity type list",
  591 + "any-entity": "Any entity",
  592 + "enter-entity-type": "Enter entity type",
567 "no-entities-matching": "No entities matching '{{entity}}' were found.", 593 "no-entities-matching": "No entities matching '{{entity}}' were found.",
  594 + "no-entity-types-matching": "No entity types matching '{{entityType}}' were found.",
568 "name-starts-with": "Name starts with", 595 "name-starts-with": "Name starts with",
569 "use-entity-name-filter": "Use filter", 596 "use-entity-name-filter": "Use filter",
570 "entity-list-empty": "No entities selected.", 597 "entity-list-empty": "No entities selected.",
  598 + "entity-type-list-empty": "No entity types selected.",
571 "entity-name-filter-required": "Entity name filter is required.", 599 "entity-name-filter-required": "Entity name filter is required.",
572 "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.", 600 "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.",
573 "all-subtypes": "All", 601 "all-subtypes": "All",
@@ -581,30 +609,39 @@ export default angular.module('thingsboard.locale', []) @@ -581,30 +609,39 @@ export default angular.module('thingsboard.locale', [])
581 "type": "Type", 609 "type": "Type",
582 "type-required": "Entity type is required.", 610 "type-required": "Entity type is required.",
583 "type-device": "Device", 611 "type-device": "Device",
  612 + "type-devices": "Devices",
584 "list-of-devices": "{ count, select, 1 {One device} other {List of # devices} }", 613 "list-of-devices": "{ count, select, 1 {One device} other {List of # devices} }",
585 "device-name-starts-with": "Devices whose names start with '{{prefix}}'", 614 "device-name-starts-with": "Devices whose names start with '{{prefix}}'",
586 "type-asset": "Asset", 615 "type-asset": "Asset",
  616 + "type-assets": "Assets",
587 "list-of-assets": "{ count, select, 1 {One asset} other {List of # assets} }", 617 "list-of-assets": "{ count, select, 1 {One asset} other {List of # assets} }",
588 "asset-name-starts-with": "Assets whose names start with '{{prefix}}'", 618 "asset-name-starts-with": "Assets whose names start with '{{prefix}}'",
589 "type-rule": "Rule", 619 "type-rule": "Rule",
  620 + "type-rules": "Rules",
590 "list-of-rules": "{ count, select, 1 {One rule} other {List of # rules} }", 621 "list-of-rules": "{ count, select, 1 {One rule} other {List of # rules} }",
591 "rule-name-starts-with": "Rules whose names start with '{{prefix}}'", 622 "rule-name-starts-with": "Rules whose names start with '{{prefix}}'",
592 "type-plugin": "Plugin", 623 "type-plugin": "Plugin",
  624 + "type-plugins": "Plugins",
593 "list-of-plugins": "{ count, select, 1 {One plugin} other {List of # plugins} }", 625 "list-of-plugins": "{ count, select, 1 {One plugin} other {List of # plugins} }",
594 "plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'", 626 "plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'",
595 "type-tenant": "Tenant", 627 "type-tenant": "Tenant",
  628 + "type-tenants": "Tenants",
596 "list-of-tenants": "{ count, select, 1 {One tenant} other {List of # tenants} }", 629 "list-of-tenants": "{ count, select, 1 {One tenant} other {List of # tenants} }",
597 "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'", 630 "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'",
598 "type-customer": "Customer", 631 "type-customer": "Customer",
  632 + "type-customers": "Customers",
599 "list-of-customers": "{ count, select, 1 {One customer} other {List of # customers} }", 633 "list-of-customers": "{ count, select, 1 {One customer} other {List of # customers} }",
600 "customer-name-starts-with": "Customers whose names start with '{{prefix}}'", 634 "customer-name-starts-with": "Customers whose names start with '{{prefix}}'",
601 "type-user": "User", 635 "type-user": "User",
  636 + "type-users": "Users",
602 "list-of-users": "{ count, select, 1 {One user} other {List of # users} }", 637 "list-of-users": "{ count, select, 1 {One user} other {List of # users} }",
603 "user-name-starts-with": "Users whose names start with '{{prefix}}'", 638 "user-name-starts-with": "Users whose names start with '{{prefix}}'",
604 "type-dashboard": "Dashboard", 639 "type-dashboard": "Dashboard",
  640 + "type-dashboards": "Dashboards",
605 "list-of-dashboards": "{ count, select, 1 {One dashboard} other {List of # dashboards} }", 641 "list-of-dashboards": "{ count, select, 1 {One dashboard} other {List of # dashboards} }",
606 "dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'", 642 "dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'",
607 "type-alarm": "Alarm", 643 "type-alarm": "Alarm",
  644 + "type-alarms": "Alarms",
608 "list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }", 645 "list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }",
609 "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'" 646 "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'"
610 }, 647 },
@@ -770,6 +807,10 @@ export default angular.module('thingsboard.locale', []) @@ -770,6 +807,10 @@ export default angular.module('thingsboard.locale', [])
770 "FROM": "From", 807 "FROM": "From",
771 "TO": "To" 808 "TO": "To"
772 }, 809 },
  810 + "direction-type": {
  811 + "FROM": "from",
  812 + "TO": "to"
  813 + },
773 "from-relations": "Outbound relations", 814 "from-relations": "Outbound relations",
774 "to-relations": "Inbound relations", 815 "to-relations": "Inbound relations",
775 "selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected", 816 "selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected",
@@ -783,6 +824,7 @@ export default angular.module('thingsboard.locale', []) @@ -783,6 +824,7 @@ export default angular.module('thingsboard.locale', [])
783 "delete": "Delete relation", 824 "delete": "Delete relation",
784 "relation-type": "Relation type", 825 "relation-type": "Relation type",
785 "relation-type-required": "Relation type is required.", 826 "relation-type-required": "Relation type is required.",
  827 + "any-relation-type": "Any type",
786 "add": "Add relation", 828 "add": "Add relation",
787 "delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?", 829 "delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?",
788 "delete-to-relation-text": "Be careful, after the confirmation the entity '{{entityName}}' will be unrelated from the current entity.", 830 "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', []) @@ -791,7 +833,11 @@ export default angular.module('thingsboard.locale', [])
791 "delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?", 833 "delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?",
792 "delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.", 834 "delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.",
793 "delete-from-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?", 835 "delete-from-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?",
794 - "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." 836 + "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.",
  837 + "remove-relation-filter": "Remove relation filter",
  838 + "add-relation-filter": "Add relation filter",
  839 + "any-relation": "Any relation",
  840 + "relation-filters": "Relation filters"
795 }, 841 },
796 "rule": { 842 "rule": {
797 "rule": "Rule", 843 "rule": "Rule",
@@ -236,6 +236,15 @@ div { @@ -236,6 +236,15 @@ div {
236 } 236 }
237 } 237 }
238 238
  239 +.md-caption {
  240 + &.tb-required:after {
  241 + content: ' *';
  242 + font-size: 10px;
  243 + vertical-align: top;
  244 + color: rgba(0,0,0,0.54);
  245 + }
  246 +}
  247 +
239 pre.tb-highlight { 248 pre.tb-highlight {
240 background-color: #f7f7f7; 249 background-color: #f7f7f7;
241 display: block; 250 display: block;