thingsboard-developers
Changes
application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java 15(+15 -0)
ui/src/app/api/entity.service.js 249(+236 -13)
ui/src/app/api/entity-relation.service.js 14(+13 -1)
ui/src/app/common/types.constant.js 9(+9 -0)
ui/src/app/components/datasource-func.scss 38(+22 -16)
ui/src/app/components/datasource-func.tpl.html 111(+60 -51)
ui/src/app/entity/entity-filter.directive.js 18(+17 -1)
ui/src/app/entity/entity-filter.scss 17(+17 -0)
ui/src/app/entity/entity-filter.tpl.html 142(+142 -0)
ui/src/app/entity/entity-filter-view.directive.js 101(+100 -1)
ui/src/app/entity/entity-subtype-list.directive.js 146(+146 -0)
ui/src/app/entity/entity-subtype-list.scss 30(+30 -0)
ui/src/app/entity/entity-type-list.directive.js 111(+111 -0)
ui/src/app/entity/entity-type-list.scss 30(+30 -0)
ui/src/app/entity/entity-type-list.tpl.html 54(+54 -0)
ui/src/app/entity/index.js 14(+10 -4)
ui/src/app/locale/locale.constant.js 50(+48 -2)
ui/src/scss/main.scss 9(+9 -0)
Details
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
index 3ddc597..4aa1a0a 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
@@ -250,6 +250,21 @@ public class EntityRelationController extends BaseController {
}
}
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/relations/info", method = RequestMethod.POST)
+ @ResponseBody
+ public List<EntityRelationInfo> findInfoByQuery(@RequestBody EntityRelationsQuery query) throws ThingsboardException {
+ checkNotNull(query);
+ checkNotNull(query.getParameters());
+ checkNotNull(query.getFilters());
+ checkEntityId(query.getParameters().getEntityId());
+ try {
+ return checkNotNull(relationService.findInfoByQuery(query).get());
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
private RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) {
RelationTypeGroup result = defaultValue;
if (strRelationTypeGroup != null && strRelationTypeGroup.trim().length()>0) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
index 36ec567..296874e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
@@ -191,7 +191,7 @@ public class BaseRelationService implements RelationService {
@Override
public ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query) {
- log.trace("Executing findByQuery [{}][{}]", query);
+ log.trace("Executing findByQuery [{}]", query);
RelationsSearchParameters params = query.getParameters();
final List<EntityTypeFilter> filters = query.getFilters();
if (filters == null || filters.isEmpty()) {
@@ -224,6 +224,30 @@ public class BaseRelationService implements RelationService {
}
}
+ @Override
+ public ListenableFuture<List<EntityRelationInfo>> findInfoByQuery(EntityRelationsQuery query) {
+ log.trace("Executing findInfoByQuery [{}]", query);
+ ListenableFuture<List<EntityRelation>> relations = findByQuery(query);
+ EntitySearchDirection direction = query.getParameters().getDirection();
+ ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
+ (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
+ List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
+ relations1.stream().forEach(relation ->
+ futures.add(fetchRelationInfoAsync(relation,
+ relation2 -> direction == EntitySearchDirection.FROM ? relation2.getTo() : relation2.getFrom(),
+ (EntityRelationInfo relationInfo, String entityName) -> {
+ if (direction == EntitySearchDirection.FROM) {
+ relationInfo.setToName(entityName);
+ } else {
+ relationInfo.setFromName(entityName);
+ }
+ }))
+ );
+ return Futures.successfulAsList(futures);
+ });
+ return relationsInfo;
+ }
+
protected void validate(EntityRelation relation) {
if (relation == null) {
throw new DataValidationException("Relation type should be specified!");
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
index a810454..bd2e785 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
@@ -52,6 +52,8 @@ public interface RelationService {
ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query);
+ ListenableFuture<List<EntityRelationInfo>> findInfoByQuery(EntityRelationsQuery query);
+
// TODO: This method may be useful for some validations in the future
// ListenableFuture<Boolean> checkRecursiveRelation(EntityId from, EntityId to);
ui/src/app/api/entity.service.js 249(+236 -13)
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
index e647d9c..20157ce 100644
--- a/ui/src/app/api/entity.service.js
+++ b/ui/src/app/api/entity.service.js
@@ -32,6 +32,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
checkEntityAlias: checkEntityAlias,
filterAliasByEntityTypes: filterAliasByEntityTypes,
getAliasFilterTypesByEntityTypes: getAliasFilterTypesByEntityTypes,
+ prepareAllowedEntityTypesList: prepareAllowedEntityTypesList,
getEntityKeys: getEntityKeys,
createDatasourcesFromSubscriptionsInfo: createDatasourcesFromSubscriptionsInfo,
getRelatedEntities: getRelatedEntities,
@@ -176,6 +177,54 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
return deferred.promise;
}
+ function getSingleTenantByPageLinkPromise(pageLink) {
+ var user = userService.getCurrentUser();
+ var tenantId = user.tenantId;
+ var deferred = $q.defer();
+ tenantService.getTenant(tenantId).then(
+ function success(tenant) {
+ var tenantName = tenant.name;
+ var result = {
+ data: [],
+ nextPageLink: pageLink,
+ hasNext: false
+ };
+ if (tenantName.toLowerCase().startsWith(pageLink.textSearch)) {
+ result.data.push(tenant);
+ }
+ deferred.resolve(result);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function getSingleCustomerByPageLinkPromise(pageLink) {
+ var user = userService.getCurrentUser();
+ var customerId = user.customerId;
+ var deferred = $q.defer();
+ customerService.getCustomer(customerId).then(
+ function success(customer) {
+ var customerName = customer.name;
+ var result = {
+ data: [],
+ nextPageLink: pageLink,
+ hasNext: false
+ };
+ if (customerName.toLowerCase().startsWith(pageLink.textSearch)) {
+ result.data.push(customer);
+ }
+ deferred.resolve(result);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
function getEntitiesByPageLinkPromise(entityType, pageLink, config, subType) {
var promise;
var user = userService.getCurrentUser();
@@ -196,10 +245,18 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
}
break;
case types.entityType.tenant:
- promise = tenantService.getTenants(pageLink);
+ if (user.authority === 'TENANT_ADMIN') {
+ promise = getSingleTenantByPageLinkPromise(pageLink);
+ } else {
+ promise = tenantService.getTenants(pageLink);
+ }
break;
case types.entityType.customer:
- promise = customerService.getCustomers(pageLink);
+ if (user.authority === 'CUSTOMER_USER') {
+ promise = getSingleCustomerByPageLinkPromise(pageLink);
+ } else {
+ promise = customerService.getCustomers(pageLink);
+ }
break;
case types.entityType.rule:
promise = ruleService.getAllRules(pageLink);
@@ -283,6 +340,16 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
return { name: entity.name, entityType: entity.id.entityType, id: entity.id.id };
}
+ function entityRelationInfoToEntityInfo(entityRelationInfo, direction) {
+ var entityId = direction == types.entitySearchDirection.from ? entityRelationInfo.to : entityRelationInfo.from;
+ var name = direction == types.entitySearchDirection.from ? entityRelationInfo.toName : entityRelationInfo.fromName;
+ return {
+ name: name,
+ entityType: entityId.entityType,
+ id: entityId.id
+ };
+ }
+
function entitiesToEntitiesInfo(entities) {
var entitiesInfo = [];
for (var d = 0; d < entities.length; d++) {
@@ -291,19 +358,26 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
return entitiesInfo;
}
+ function entityRelationInfosToEntitiesInfo(entityRelations, direction) {
+ var entitiesInfo = [];
+ for (var d = 0; d < entityRelations.length; d++) {
+ entitiesInfo.push(entityRelationInfoToEntityInfo(entityRelations[d], direction));
+ }
+ return entitiesInfo;
+ }
+
+
function resolveAlias(entityAlias, stateParams) {
var deferred = $q.defer();
var filter = entityAlias.filter;
resolveAliasFilter(filter, stateParams, -1).then(
function (result) {
- var entities = result.entities;
var aliasInfo = {
alias: entityAlias.alias,
stateEntity: result.stateEntity,
resolveMultiple: filter.resolveMultiple
};
- var resolvedEntities = entitiesToEntitiesInfo(entities);
- aliasInfo.resolvedEntities = resolvedEntities;
+ aliasInfo.resolvedEntities = result.entities;
aliasInfo.currentEntity = null;
if (aliasInfo.resolvedEntities.length) {
aliasInfo.currentEntity = aliasInfo.resolvedEntities[0];
@@ -328,7 +402,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
getEntities(filter.entityType, filter.entityList).then(
function success(entities) {
if (entities && entities.length) {
- result.entities = entities;
+ result.entities = entitiesToEntitiesInfo(entities);
deferred.resolve(result);
} else {
deferred.reject();
@@ -343,7 +417,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
getEntitiesByNameFilter(filter.entityType, filter.entityNameFilter, maxItems).then(
function success(entities) {
if (entities && entities.length) {
- result.entities = entities;
+ result.entities = entitiesToEntitiesInfo(entities);
deferred.resolve(result);
} else {
deferred.reject();
@@ -359,7 +433,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
if (stateParams && stateParams.entityId) {
getEntity(stateParams.entityId.entityType, stateParams.entityId.id).then(
function success(entity) {
- result.entities = [entity];
+ result.entities = entitiesToEntitiesInfo([entity]);
deferred.resolve(result);
},
function fail() {
@@ -374,7 +448,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
getEntitiesByNameFilter(types.entityType.asset, filter.assetNameFilter, maxItems, null, filter.assetType).then(
function success(entities) {
if (entities && entities.length) {
- result.entities = entities;
+ result.entities = entitiesToEntitiesInfo(entities);
deferred.resolve(result);
} else {
deferred.reject();
@@ -389,7 +463,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
getEntitiesByNameFilter(types.entityType.device, filter.deviceNameFilter, maxItems, null, filter.deviceType).then(
function success(entities) {
if (entities && entities.length) {
- result.entities = entities;
+ result.entities = entitiesToEntitiesInfo(entities);
deferred.resolve(result);
} else {
deferred.reject();
@@ -400,8 +474,97 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
}
);
break;
-
- //TODO: Alias filter
+ case types.aliasFilterType.relationsQuery.value:
+ result.stateEntity = filter.rootStateEntity;
+ var rootEntityType;
+ var rootEntityId;
+ if (result.stateEntity && stateParams && stateParams.entityId) {
+ rootEntityType = stateParams.entityId.entityType;
+ rootEntityId = stateParams.entityId.id;
+ } else if (!result.stateEntity) {
+ rootEntityType = filter.rootEntity.entityType;
+ rootEntityId = filter.rootEntity.id;
+ }
+ if (rootEntityType && rootEntityId) {
+ var searchQuery = {
+ parameters: {
+ rootId: rootEntityId,
+ rootType: rootEntityType,
+ direction: filter.direction
+ },
+ filters: filter.filters
+ };
+ searchQuery.parameters.maxLevel = filter.maxLevel && filter.maxLevel > 0 ? filter.maxLevel : -1;
+ entityRelationService.findInfoByQuery(searchQuery).then(
+ function success(allRelations) {
+ if (allRelations && allRelations.length) {
+ if (angular.isDefined(maxItems) && maxItems > 0) {
+ var limit = Math.min(allRelations.length, maxItems);
+ allRelations.length = limit;
+ }
+ result.entities = entityRelationInfosToEntitiesInfo(allRelations, filter.direction);
+ deferred.resolve(result);
+ } else {
+ deferred.reject();
+ }
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ } else {
+ deferred.resolve(result);
+ }
+ break;
+ case types.aliasFilterType.assetSearchQuery.value:
+ case types.aliasFilterType.deviceSearchQuery.value:
+ result.stateEntity = filter.rootStateEntity;
+ if (result.stateEntity && stateParams && stateParams.entityId) {
+ rootEntityType = stateParams.entityId.entityType;
+ rootEntityId = stateParams.entityId.id;
+ } else if (!result.stateEntity) {
+ rootEntityType = filter.rootEntity.entityType;
+ rootEntityId = filter.rootEntity.id;
+ }
+ if (rootEntityType && rootEntityId) {
+ searchQuery = {
+ parameters: {
+ rootId: rootEntityId,
+ rootType: rootEntityType,
+ direction: filter.direction
+ },
+ relationType: filter.relationType
+ };
+ searchQuery.parameters.maxLevel = filter.maxLevel && filter.maxLevel > 0 ? filter.maxLevel : -1;
+ var findByQueryPromise;
+ if (filter.type == types.aliasFilterType.assetSearchQuery.value) {
+ searchQuery.assetTypes = filter.assetTypes;
+ findByQueryPromise = assetService.findByQuery(searchQuery, false);
+ } else if (filter.type == types.aliasFilterType.deviceSearchQuery.value) {
+ searchQuery.deviceTypes = filter.deviceTypes;
+ findByQueryPromise = deviceService.findByQuery(searchQuery, false);
+ }
+ findByQueryPromise.then(
+ function success(entities) {
+ if (entities && entities.length) {
+ if (angular.isDefined(maxItems) && maxItems > 0) {
+ var limit = Math.min(entities.length, maxItems);
+ entities.length = limit;
+ }
+ result.entities = entitiesToEntitiesInfo(entities);
+ deferred.resolve(result);
+ } else {
+ deferred.reject();
+ }
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ } else {
+ deferred.resolve(result);
+ }
+ break;
}
return deferred.promise;
}
@@ -420,9 +583,33 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false;
case types.aliasFilterType.deviceType.value:
return entityTypes.indexOf(types.entityType.device) > -1 ? true : false;
+ case types.aliasFilterType.relationsQuery.value:
+ if (filter.filters && filter.filters.length) {
+ var match = false;
+ for (var f=0;f<filter.filters.length;f++) {
+ var relationFilter = filter.filters[f];
+ if (relationFilter.entityTypes && relationFilter.entityTypes.length) {
+ for (var et=0;et<relationFilter.entityTypes.length;et++) {
+ if (entityTypes.indexOf(relationFilter.entityTypes[et]) > -1) {
+ match = true;
+ break;
+ }
+ }
+ } else {
+ match = true;
+ break;
+ }
+ }
+ return match;
+ } else {
+ return true;
+ }
+ case types.aliasFilterType.assetSearchQuery.value:
+ return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false;
+ case types.aliasFilterType.deviceSearchQuery.value:
+ return entityTypes.indexOf(types.entityType.device) > -1 ? true : false;
}
}
- //TODO: Alias filter
return false;
}
@@ -474,6 +661,42 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
return result;
}
+ function prepareAllowedEntityTypesList(allowedEntityTypes) {
+ var authority = userService.getAuthority();
+ var entityTypes = {};
+ switch(authority) {
+ case 'SYS_ADMIN':
+ entityTypes.tenant = types.entityType.tenant;
+ entityTypes.rule = types.entityType.rule;
+ entityTypes.plugin = types.entityType.plugin;
+ break;
+ case 'TENANT_ADMIN':
+ entityTypes.device = types.entityType.device;
+ entityTypes.asset = types.entityType.asset;
+ entityTypes.tenant = types.entityType.tenant;
+ entityTypes.customer = types.entityType.customer;
+ entityTypes.rule = types.entityType.rule;
+ entityTypes.plugin = types.entityType.plugin;
+ entityTypes.dashboard = types.entityType.dashboard;
+ break;
+ case 'CUSTOMER_USER':
+ entityTypes.device = types.entityType.device;
+ entityTypes.asset = types.entityType.asset;
+ entityTypes.customer = types.entityType.customer;
+ entityTypes.dashboard = types.entityType.dashboard;
+ break;
+ }
+
+ if (allowedEntityTypes) {
+ for (var entityType in entityTypes) {
+ if (allowedEntityTypes.indexOf(entityTypes[entityType]) === -1) {
+ delete entityTypes[entityType];
+ }
+ }
+ }
+ return entityTypes;
+ }
+
function checkEntityAlias(entityAlias) {
var deferred = $q.defer();
ui/src/app/api/entity-relation.service.js 14(+13 -1)
diff --git a/ui/src/app/api/entity-relation.service.js b/ui/src/app/api/entity-relation.service.js
index 875b2fa..351c252 100644
--- a/ui/src/app/api/entity-relation.service.js
+++ b/ui/src/app/api/entity-relation.service.js
@@ -30,7 +30,8 @@ function EntityRelationService($http, $q) {
findByTo: findByTo,
findInfoByTo: findInfoByTo,
findByToAndType: findByToAndType,
- findByQuery: findByQuery
+ findByQuery: findByQuery,
+ findInfoByQuery: findInfoByQuery
}
return service;
@@ -159,4 +160,15 @@ function EntityRelationService($http, $q) {
return deferred.promise;
}
+ function findInfoByQuery(query) {
+ var deferred = $q.defer();
+ var url = '/api/relations/info';
+ $http.post(url, query).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
}
ui/src/app/common/types.constant.js 9(+9 -0)
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index 522abff..fe3f73c 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -146,46 +146,55 @@ export default angular.module('thingsboard.types', [])
entityTypeTranslations: {
"DEVICE": {
type: 'entity.type-device',
+ typePlural: 'entity.type-devices',
list: 'entity.list-of-devices',
nameStartsWith: 'entity.device-name-starts-with'
},
"ASSET": {
type: 'entity.type-asset',
+ typePlural: 'entity.type-assets',
list: 'entity.list-of-assets',
nameStartsWith: 'entity.asset-name-starts-with'
},
"RULE": {
type: 'entity.type-rule',
+ typePlural: 'entity.type-rules',
list: 'entity.list-of-rules',
nameStartsWith: 'entity.rule-name-starts-with'
},
"PLUGIN": {
type: 'entity.type-plugin',
+ typePlural: 'entity.type-plugins',
list: 'entity.list-of-plugins',
nameStartsWith: 'entity.plugin-name-starts-with'
},
"TENANT": {
type: 'entity.type-tenant',
+ typePlural: 'entity.type-tenants',
list: 'entity.list-of-tenants',
nameStartsWith: 'entity.tenant-name-starts-with'
},
"CUSTOMER": {
type: 'entity.type-customer',
+ typePlural: 'entity.type-customers',
list: 'entity.list-of-customers',
nameStartsWith: 'entity.customer-name-starts-with'
},
"USER": {
type: 'entity.type-user',
+ typePlural: 'entity.type-users',
list: 'entity.list-of-users',
nameStartsWith: 'entity.user-name-starts-with'
},
"DASHBOARD": {
type: 'entity.type-dashboard',
+ typePlural: 'entity.type-dashboards',
list: 'entity.list-of-dashboards',
nameStartsWith: 'entity.dashboard-name-starts-with'
},
"ALARM": {
type: 'entity.type-alarm',
+ typePlural: 'entity.type-alarms',
list: 'entity.list-of-alarms',
nameStartsWith: 'entity.alarm-name-starts-with'
}
diff --git a/ui/src/app/components/datasource-func.directive.js b/ui/src/app/components/datasource-func.directive.js
index 0f66dca..fe54696 100644
--- a/ui/src/app/components/datasource-func.directive.js
+++ b/ui/src/app/components/datasource-func.directive.js
@@ -71,6 +71,13 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
}
}, true);
+ scope.$watch('datasourceName', function () {
+ if (ngModelCtrl.$viewValue) {
+ ngModelCtrl.$viewValue.name = scope.datasourceName;
+ scope.updateValidity();
+ }
+ });
+
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
var funcDataKeys = [];
@@ -78,6 +85,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
funcDataKeys = funcDataKeys.concat(ngModelCtrl.$viewValue.dataKeys);
}
scope.funcDataKeys = funcDataKeys;
+ scope.datasourceName = ngModelCtrl.$viewValue.name;
}
};
ui/src/app/components/datasource-func.scss 38(+22 -16)
diff --git a/ui/src/app/components/datasource-func.scss b/ui/src/app/components/datasource-func.scss
index 1a5dcc7..8739ed8 100644
--- a/ui/src/app/components/datasource-func.scss
+++ b/ui/src/app/components/datasource-func.scss
@@ -15,23 +15,29 @@
*/
@import '../../scss/constants';
-.tb-func-datakey-autocomplete {
- .tb-not-found {
- display: block;
- line-height: 1.5;
- height: 48px;
- .tb-no-entries {
- line-height: 48px;
- }
+.tb-datasource-func {
+ @media (min-width: $layout-breakpoint-gt-sm) {
+ padding-left: 8px;
}
- li {
- height: auto !important;
- white-space: normal !important;
+
+ md-input-container.tb-datasource-name {
+ .md-errors-spacer {
+ display: none;
+ }
}
-}
-tb-datasource-func {
- @media (min-width: $layout-breakpoint-gt-sm) {
- padding-left: 8px;
+ .tb-func-datakey-autocomplete {
+ .tb-not-found {
+ display: block;
+ line-height: 1.5;
+ height: 48px;
+ .tb-no-entries {
+ line-height: 48px;
+ }
+ }
+ li {
+ height: auto !important;
+ white-space: normal !important;
+ }
}
-}
\ No newline at end of file
+}
ui/src/app/components/datasource-func.tpl.html 111(+60 -51)
diff --git a/ui/src/app/components/datasource-func.tpl.html b/ui/src/app/components/datasource-func.tpl.html
index 1853b9d..e8e08f7 100644
--- a/ui/src/app/components/datasource-func.tpl.html
+++ b/ui/src/app/components/datasource-func.tpl.html
@@ -15,59 +15,68 @@
limitations under the License.
-->
-<section flex layout='column' style="padding-left: 4px;">
- <md-chips flex
- id="function_datakey_chips"
- ng-required="true"
- ng-model="funcDataKeys" md-autocomplete-snap
- md-transform-chip="transformDataKeyChip($chip)"
- md-require-match="false">
- <md-autocomplete
- md-no-cache="false"
- id="dataKey"
- md-selected-item="selectedDataKey"
- md-search-text="dataKeySearchText"
- md-items="item in dataKeysSearch(dataKeySearchText)"
- md-item-text="item.name"
- md-min-length="0"
- placeholder="{{ 'datakey.function-types' | translate }}"
- md-menu-class="tb-func-datakey-autocomplete">
- <span md-highlight-text="dataKeySearchText" md-highlight-flags="^i">{{item}}</span>
- <md-not-found>
- <div class="tb-not-found">
- <div class="tb-no-entries" ng-if="!textIsNotEmpty(dataKeySearchText)">
- <span translate>device.no-keys-found</span>
+<section class="tb-datasource-func" flex layout='column'
+ layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
+ <md-input-container class="tb-datasource-name" md-no-float style="min-width: 200px;">
+ <input name="datasourceName"
+ placeholder="{{ 'datasource.name' | translate }}"
+ ng-model="datasourceName"
+ aria-label="{{ 'datasource.name' | translate }}">
+ </md-input-container>
+ <section flex layout='column' style="padding-left: 4px;">
+ <md-chips flex
+ id="function_datakey_chips"
+ ng-required="true"
+ ng-model="funcDataKeys" md-autocomplete-snap
+ md-transform-chip="transformDataKeyChip($chip)"
+ md-require-match="false">
+ <md-autocomplete
+ md-no-cache="false"
+ id="dataKey"
+ md-selected-item="selectedDataKey"
+ md-search-text="dataKeySearchText"
+ md-items="item in dataKeysSearch(dataKeySearchText)"
+ md-item-text="item.name"
+ md-min-length="0"
+ placeholder="{{ 'datakey.function-types' | translate }}"
+ md-menu-class="tb-func-datakey-autocomplete">
+ <span md-highlight-text="dataKeySearchText" md-highlight-flags="^i">{{item}}</span>
+ <md-not-found>
+ <div class="tb-not-found">
+ <div class="tb-no-entries" ng-if="!textIsNotEmpty(dataKeySearchText)">
+ <span translate>device.no-keys-found</span>
+ </div>
+ <div ng-if="textIsNotEmpty(dataKeySearchText)">
+ <span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:'...'}}" }'>device.no-key-matching</span>
+ <span>
+ <a translate ng-click="createKey($event, '#function_datakey_chips')">device.create-new-key</a>
+ </span>
+ </div>
</div>
- <div ng-if="textIsNotEmpty(dataKeySearchText)">
- <span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:'...'}}" }'>device.no-key-matching</span>
- <span>
- <a translate ng-click="createKey($event, '#function_datakey_chips')">device.create-new-key</a>
- </span>
- </div>
- </div>
- </md-not-found>
- </md-autocomplete>
- <md-chip-template>
- <div layout="row" layout-align="start center" class="tb-attribute-chip">
- <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
- <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
- </div>
- <div layout="row" flex>
- <div class="tb-chip-label">
- {{$chip.label}}
+ </md-not-found>
+ </md-autocomplete>
+ <md-chip-template>
+ <div layout="row" layout-align="start center" class="tb-attribute-chip">
+ <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
+ <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
</div>
- <div class="tb-chip-separator">: </div>
- <div class="tb-chip-label">
- <strong>{{$chip.name}}</strong>
+ <div layout="row" flex>
+ <div class="tb-chip-label">
+ {{$chip.label}}
+ </div>
+ <div class="tb-chip-separator">: </div>
+ <div class="tb-chip-label">
+ <strong>{{$chip.name}}</strong>
+ </div>
</div>
+ <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
+ <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
+ </md-button>
</div>
- <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
- <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
- </md-button>
- </div>
- </md-chip-template>
- </md-chips>
- <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
- <div translate ng-message="funcTypes" class="tb-error-message">datakey.function-types-required</div>
- </div>
+ </md-chip-template>
+ </md-chips>
+ <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
+ <div translate ng-message="funcTypes" class="tb-error-message">datakey.function-types-required</div>
+ </div>
+ </section>
</section>
diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js
index a9a68c7..5372b62 100644
--- a/ui/src/app/dashboard/add-widget.controller.js
+++ b/ui/src/app/dashboard/add-widget.controller.js
@@ -15,7 +15,7 @@
*/
/* eslint-disable import/no-unresolved, import/default */
-import entityAliasDialogTemplate from '../entity/entity-alias-dialog.tpl.html';
+import entityAliasDialogTemplate from '../entity/alias/entity-alias-dialog.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 7c97a94..65eb833 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -15,7 +15,7 @@
*/
/* eslint-disable import/no-unresolved, import/default */
-import entityAliasesTemplate from '../entity/entity-aliases.tpl.html';
+import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
import dashboardSettingsTemplate from './dashboard-settings.tpl.html';
import manageDashboardLayoutsTemplate from './layouts/manage-dashboard-layouts.tpl.html';
import manageDashboardStatesTemplate from './states/manage-dashboard-states.tpl.html';
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
index 5b788cc..6532914 100644
--- a/ui/src/app/dashboard/edit-widget.directive.js
+++ b/ui/src/app/dashboard/edit-widget.directive.js
@@ -15,7 +15,7 @@
*/
/* eslint-disable import/no-unresolved, import/default */
-import entityAliasDialogTemplate from '../entity/entity-alias-dialog.tpl.html';
+import entityAliasDialogTemplate from '../entity/alias/entity-alias-dialog.tpl.html';
import editWidgetTemplate from './edit-widget.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
ui/src/app/entity/entity-filter.directive.js 18(+17 -1)
diff --git a/ui/src/app/entity/entity-filter.directive.js b/ui/src/app/entity/entity-filter.directive.js
index 9566716..b81f369 100644
--- a/ui/src/app/entity/entity-filter.directive.js
+++ b/ui/src/app/entity/entity-filter.directive.js
@@ -63,7 +63,23 @@ export default function EntityFilterDirective($compile, $templateCache, $q, $doc
filter.deviceType = null;
filter.deviceNameFilter = '';
break;
- //TODO: Alias filter
+ case types.aliasFilterType.relationsQuery.value:
+ case types.aliasFilterType.assetSearchQuery.value:
+ case types.aliasFilterType.deviceSearchQuery.value:
+ filter.rootStateEntity = false;
+ filter.rootEntity = null;
+ filter.direction = types.entitySearchDirection.from;
+ filter.maxLevel = 1;
+ if (filter.type === types.aliasFilterType.relationsQuery.value) {
+ filter.filters = [];
+ } else if (filter.type === types.aliasFilterType.assetSearchQuery.value) {
+ filter.relationType = null;
+ filter.assetTypes = [];
+ } else if (filter.type === types.aliasFilterType.deviceSearchQuery.value) {
+ filter.relationType = null;
+ filter.deviceTypes = [];
+ }
+ break;
}
scope.filter = filter;
}
ui/src/app/entity/entity-filter.scss 17(+17 -0)
diff --git a/ui/src/app/entity/entity-filter.scss b/ui/src/app/entity/entity-filter.scss
index ebbea3d..7e998ca 100644
--- a/ui/src/app/entity/entity-filter.scss
+++ b/ui/src/app/entity/entity-filter.scss
@@ -16,4 +16,21 @@
.tb-entity-filter {
+ #relationsQueryFilter {
+ padding-top: 20px;
+ tb-entity-select {
+ min-height: 92px;
+ }
+ }
+
+ .tb-root-state-entity-switch {
+ padding-left: 10px;
+ .root-state-entity-switch {
+ margin: 0;
+ }
+ .root-state-entity-label {
+ margin: 5px 0;
+ }
+ }
+
}
\ No newline at end of file
ui/src/app/entity/entity-filter.tpl.html 142(+142 -0)
diff --git a/ui/src/app/entity/entity-filter.tpl.html b/ui/src/app/entity/entity-filter.tpl.html
index 77b6d3c..4f5af00 100644
--- a/ui/src/app/entity/entity-filter.tpl.html
+++ b/ui/src/app/entity/entity-filter.tpl.html
@@ -88,4 +88,146 @@
aria-label="{{ 'device.name-starts-with' | translate }}">
</md-input-container>
</section>
+ <section layout="column" ng-if="filter.type == types.aliasFilterType.relationsQuery.value" id="relationsQueryFilter">
+ <label class="tb-small">{{ 'alias.root-entity' | translate }}</label>
+ <div flex layout="row">
+ <tb-entity-select flex
+ the-form="theForm"
+ tb-required="!filter.rootStateEntity"
+ ng-disabled="filter.rootStateEntity"
+ ng-model="filter.rootEntity">
+ </tb-entity-select>
+ <section class="tb-root-state-entity-switch" layout="column" layout-align="start center">
+ <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
+ <md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
+ aria-label="{{ 'alias.root-state-entity' | translate }}">
+ </md-switch>
+ </section>
+ </div>
+ <div flex layout="row">
+ <md-input-container class="md-block" style="min-width: 100px;">
+ <label translate>relation.direction</label>
+ <md-select required ng-model="filter.direction">
+ <md-option ng-repeat="direction in types.entitySearchDirection" ng-value="direction">
+ {{ ('relation.search-direction.' + direction) | translate}}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <md-input-container flex class="md-block">
+ <label translate>alias.max-relation-level</label>
+ <input name="maxRelationLevel"
+ type="number"
+ min="1"
+ step="1"
+ placeholder="{{ 'alias.unlimited-level' | translate }}"
+ ng-model="filter.maxLevel"
+ aria-label="{{ 'alias.max-relation-level' | translate }}">
+ </md-input-container>
+ </div>
+ <div class="md-caption" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>relation.relation-filters</div>
+ <tb-relation-filters
+ ng-model="filter.filters"
+ allowed-entity-types="allowedEntityTypes">
+ </tb-relation-filters>
+ </section>
+ <section layout="column" ng-if="filter.type == types.aliasFilterType.assetSearchQuery.value" id="assetSearchQueryFilter">
+ <label class="tb-small">{{ 'alias.root-entity' | translate }}</label>
+ <div flex layout="row">
+ <tb-entity-select flex
+ the-form="theForm"
+ tb-required="!filter.rootStateEntity"
+ ng-disabled="filter.rootStateEntity"
+ ng-model="filter.rootEntity">
+ </tb-entity-select>
+ <section class="tb-root-state-entity-switch" layout="column" layout-align="start center">
+ <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
+ <md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
+ aria-label="{{ 'alias.root-state-entity' | translate }}">
+ </md-switch>
+ </section>
+ </div>
+ <div flex layout="row">
+ <md-input-container class="md-block" style="min-width: 100px;">
+ <label translate>relation.direction</label>
+ <md-select required ng-model="filter.direction">
+ <md-option ng-repeat="direction in types.entitySearchDirection" ng-value="direction">
+ {{ ('relation.search-direction.' + direction) | translate}}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <md-input-container flex class="md-block">
+ <label translate>alias.max-relation-level</label>
+ <input name="maxRelationLevel"
+ type="number"
+ min="1"
+ step="1"
+ placeholder="{{ 'alias.unlimited-level' | translate }}"
+ ng-model="filter.maxLevel"
+ aria-label="{{ 'alias.max-relation-level' | translate }}">
+ </md-input-container>
+ </div>
+ <div class="md-caption" style="color: rgba(0,0,0,0.57);" translate>relation.relation-type</div>
+ <tb-relation-type-autocomplete flex
+ hide-label
+ the-form="theForm"
+ ng-model="filter.relationType"
+ tb-required="false">
+ </tb-relation-type-autocomplete>
+ <div class="md-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>asset.asset-types</div>
+ <tb-entity-subtype-list
+ tb-required="true"
+ entity-type="types.entityType.asset"
+ ng-model="filter.assetTypes">
+ </tb-entity-subtype-list>
+ </section>
+ <section layout="column" ng-if="filter.type == types.aliasFilterType.deviceSearchQuery.value" id="deviceSearchQueryFilter">
+ <label class="tb-small">{{ 'alias.root-entity' | translate }}</label>
+ <div flex layout="row">
+ <tb-entity-select flex
+ the-form="theForm"
+ tb-required="!filter.rootStateEntity"
+ ng-disabled="filter.rootStateEntity"
+ ng-model="filter.rootEntity">
+ </tb-entity-select>
+ <section class="tb-root-state-entity-switch" layout="column" layout-align="start center">
+ <label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
+ <md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
+ aria-label="{{ 'alias.root-state-entity' | translate }}">
+ </md-switch>
+ </section>
+ </div>
+ <div flex layout="row">
+ <md-input-container class="md-block" style="min-width: 100px;">
+ <label translate>relation.direction</label>
+ <md-select required ng-model="filter.direction">
+ <md-option ng-repeat="direction in types.entitySearchDirection" ng-value="direction">
+ {{ ('relation.search-direction.' + direction) | translate}}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <md-input-container flex class="md-block">
+ <label translate>alias.max-relation-level</label>
+ <input name="maxRelationLevel"
+ type="number"
+ min="1"
+ step="1"
+ placeholder="{{ 'alias.unlimited-level' | translate }}"
+ ng-model="filter.maxLevel"
+ aria-label="{{ 'alias.max-relation-level' | translate }}">
+ </md-input-container>
+ </div>
+ <div class="md-caption" style="color: rgba(0,0,0,0.57);" translate>relation.relation-type</div>
+ <tb-relation-type-autocomplete flex
+ hide-label
+ the-form="theForm"
+ ng-model="filter.relationType"
+ tb-required="false">
+ </tb-relation-type-autocomplete>
+ <div class="md-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>device.device-types</div>
+ <tb-entity-subtype-list
+ tb-required="true"
+ entity-type="types.entityType.device"
+ ng-model="filter.deviceTypes">
+ </tb-entity-subtype-list>
+ </section>
</div>
ui/src/app/entity/entity-filter-view.directive.js 101(+100 -1)
diff --git a/ui/src/app/entity/entity-filter-view.directive.js b/ui/src/app/entity/entity-filter-view.directive.js
index 6a358b2..66c1b66 100644
--- a/ui/src/app/entity/entity-filter-view.directive.js
+++ b/ui/src/app/entity/entity-filter-view.directive.js
@@ -74,7 +74,106 @@ export default function EntityFilterViewDirective($compile, $templateCache, $q,
scope.filterDisplayValue = $translate.instant('alias.filter-type-device-type-description', {deviceType: deviceType});
}
break;
- //TODO: Alias filter
+ case types.aliasFilterType.relationsQuery.value:
+ var rootEntityText;
+ var directionText;
+ var allEntitiesText = $translate.instant('alias.all-entities');
+ var anyRelationText = $translate.instant('alias.any-relation');
+ if (scope.filter.rootStateEntity) {
+ rootEntityText = $translate.instant('alias.state-entity');
+ } else {
+ rootEntityText = $translate.instant(types.entityTypeTranslations[scope.filter.rootEntity.entityType].type);
+ }
+ directionText = $translate.instant('relation.direction-type.' + scope.filter.direction);
+ var relationFilters = scope.filter.filters;
+ if (relationFilters && relationFilters.length) {
+ var relationFiltersDisplayValues = [];
+ relationFilters.forEach(function(relationFilter) {
+ var entitiesText;
+ if (relationFilter.entityTypes && relationFilter.entityTypes.length) {
+ var entitiesNamesList = [];
+ relationFilter.entityTypes.forEach(function(entityType) {
+ entitiesNamesList.push(
+ $translate.instant(types.entityTypeTranslations[entityType].typePlural)
+ );
+ });
+ entitiesText = entitiesNamesList.join(', ');
+ } else {
+ entitiesText = allEntitiesText;
+ }
+ var relationTypeText;
+ if (relationFilter.relationType && relationFilter.relationType.length) {
+ relationTypeText = "'" + relationFilter.relationType + "'";
+ } else {
+ relationTypeText = anyRelationText;
+ }
+ var relationFilterDisplayValue = $translate.instant('alias.filter-type-relations-query-description',
+ {
+ entities: entitiesText,
+ relationType: relationTypeText,
+ direction: directionText,
+ rootEntity: rootEntityText
+ }
+ );
+ relationFiltersDisplayValues.push(relationFilterDisplayValue);
+ });
+ scope.filterDisplayValue = relationFiltersDisplayValues.join(', ');
+ } else {
+ scope.filterDisplayValue = $translate.instant('alias.filter-type-relations-query-description',
+ {
+ entities: allEntitiesText,
+ relationType: anyRelationText,
+ direction: directionText,
+ rootEntity: rootEntityText
+ }
+ );
+ }
+ break;
+ case types.aliasFilterType.assetSearchQuery.value:
+ case types.aliasFilterType.deviceSearchQuery.value:
+ allEntitiesText = $translate.instant('alias.all-entities');
+ anyRelationText = $translate.instant('alias.any-relation');
+ if (scope.filter.rootStateEntity) {
+ rootEntityText = $translate.instant('alias.state-entity');
+ } else {
+ rootEntityText = $translate.instant(types.entityTypeTranslations[scope.filter.rootEntity.entityType].type);
+ }
+ directionText = $translate.instant('relation.direction-type.' + scope.filter.direction);
+ var relationTypeText;
+ if (scope.filter.relationType && scope.filter.relationType.length) {
+ relationTypeText = "'" + scope.filter.relationType + "'";
+ } else {
+ relationTypeText = anyRelationText;
+ }
+
+ var translationValues = {
+ relationType: relationTypeText,
+ direction: directionText,
+ rootEntity: rootEntityText
+ }
+
+ if (scope.filter.type == types.aliasFilterType.assetSearchQuery.value) {
+ var assetTypesQuoted = [];
+ scope.filter.assetTypes.forEach(function(assetType) {
+ assetTypesQuoted.push("'"+assetType+"'");
+ });
+ var assetTypesText = assetTypesQuoted.join(', ');
+ translationValues.assetTypes = assetTypesText;
+ scope.filterDisplayValue = $translate.instant('alias.filter-type-asset-search-query-description',
+ translationValues
+ );
+ } else {
+ var deviceTypesQuoted = [];
+ scope.filter.deviceTypes.forEach(function(deviceType) {
+ deviceTypesQuoted.push("'"+deviceType+"'");
+ });
+ var deviceTypesText = deviceTypesQuoted.join(', ');
+ translationValues.deviceTypes = deviceTypesText;
+ scope.filterDisplayValue = $translate.instant('alias.filter-type-device-search-query-description',
+ translationValues
+ );
+ }
+ break;
default:
scope.filterDisplayValue = scope.filter.type;
break;
diff --git a/ui/src/app/entity/entity-subtype-autocomplete.directive.js b/ui/src/app/entity/entity-subtype-autocomplete.directive.js
index 98110b0..76c15d3 100644
--- a/ui/src/app/entity/entity-subtype-autocomplete.directive.js
+++ b/ui/src/app/entity/entity-subtype-autocomplete.directive.js
@@ -114,6 +114,9 @@ export default function EntitySubtypeAutocomplete($compile, $templateCache, $q,
scope.selectEntitySubtypeText = 'asset.select-asset-type';
scope.entitySubtypeText = 'asset.asset-type';
scope.entitySubtypeRequiredText = 'asset.asset-type-required';
+ scope.$on('assetSaved', function() {
+ scope.entitySubtypes = null;
+ });
} else if (scope.entityType == types.entityType.device) {
scope.selectEntitySubtypeText = 'device.select-device-type';
scope.entitySubtypeText = 'device.device-type';
ui/src/app/entity/entity-subtype-list.directive.js 146(+146 -0)
diff --git a/ui/src/app/entity/entity-subtype-list.directive.js b/ui/src/app/entity/entity-subtype-list.directive.js
new file mode 100644
index 0000000..c7d6329
--- /dev/null
+++ b/ui/src/app/entity/entity-subtype-list.directive.js
@@ -0,0 +1,146 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entitySubtypeListTemplate from './entity-subtype-list.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import './entity-subtype-list.scss';
+
+/*@ngInject*/
+export default function EntitySubtypeListDirective($compile, $templateCache, $q, $mdUtil, $translate, $filter, types, assetService, deviceService) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+
+ var template = $templateCache.get(entitySubtypeListTemplate);
+ element.html(template);
+
+ scope.ngModelCtrl = ngModelCtrl;
+
+
+ scope.entitySubtypesList = [];
+ scope.entitySubtypes = null;
+
+ if (scope.entityType == types.entityType.asset) {
+ scope.placeholder = scope.tbRequired ? $translate.instant('asset.enter-asset-type')
+ : $translate.instant('asset.any-asset');
+ scope.secondaryPlaceholder = '+' + $translate.instant('asset.asset-type');
+ scope.noSubtypesMathingText = 'asset.no-asset-types-matching';
+ scope.subtypeListEmptyText = 'asset.asset-type-list-empty';
+ } else if (scope.entityType == types.entityType.device) {
+ scope.placeholder = scope.tbRequired ? $translate.instant('device.enter-device-type')
+ : $translate.instant('device.any-device');
+ scope.secondaryPlaceholder = '+' + $translate.instant('device.device-type');
+ scope.noSubtypesMathingText = 'device.no-device-types-matching';
+ scope.subtypeListEmptyText = 'device.device-type-list-empty';
+ }
+
+ scope.$watch('tbRequired', function () {
+ scope.updateValidity();
+ });
+
+ scope.fetchEntitySubtypes = function(searchText) {
+ var deferred = $q.defer();
+ loadSubTypes().then(
+ function success(subTypes) {
+ var result = $filter('filter')(subTypes, {'$': searchText});
+ if (result && result.length) {
+ deferred.resolve(result);
+ } else {
+ deferred.resolve([searchText]);
+ }
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ scope.updateValidity = function() {
+ var value = ngModelCtrl.$viewValue;
+ var valid = !scope.tbRequired || value && value.length > 0;
+ ngModelCtrl.$setValidity('entitySubtypesList', valid);
+ }
+
+ ngModelCtrl.$render = function () {
+ scope.entitySubtypesList = ngModelCtrl.$viewValue;
+ if (!scope.entitySubtypesList) {
+ scope.entitySubtypesList = [];
+ }
+ }
+
+ scope.$watch('entitySubtypesList', function () {
+ ngModelCtrl.$setViewValue(scope.entitySubtypesList);
+ scope.updateValidity();
+ }, true);
+
+ function loadSubTypes() {
+ var deferred = $q.defer();
+ if (!scope.entitySubtypes) {
+ var entitySubtypesPromise;
+ if (scope.entityType == types.entityType.asset) {
+ entitySubtypesPromise = assetService.getAssetTypes();
+ } else if (scope.entityType == types.entityType.device) {
+ entitySubtypesPromise = deviceService.getDeviceTypes();
+ }
+ if (entitySubtypesPromise) {
+ entitySubtypesPromise.then(
+ function success(types) {
+ scope.entitySubtypes = [];
+ types.forEach(function (type) {
+ scope.entitySubtypes.push(type.type);
+ });
+ deferred.resolve(scope.entitySubtypes);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ } else {
+ deferred.reject();
+ }
+ } else {
+ deferred.resolve(scope.entitySubtypes);
+ }
+ return deferred.promise;
+ }
+
+ $compile(element.contents())(scope);
+
+ $mdUtil.nextTick(function(){
+ var inputElement = angular.element('input', element);
+ inputElement.on('blur', function() {
+ scope.inputTouched = true;
+ } );
+ });
+
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ link: linker,
+ scope: {
+ disabled:'=ngDisabled',
+ tbRequired: '=?',
+ entityType: "="
+ }
+ };
+
+}
ui/src/app/entity/entity-subtype-list.scss 30(+30 -0)
diff --git a/ui/src/app/entity/entity-subtype-list.scss b/ui/src/app/entity/entity-subtype-list.scss
new file mode 100644
index 0000000..bbb2a1c
--- /dev/null
+++ b/ui/src/app/entity/entity-subtype-list.scss
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*.tb-entity-subtype-list {
+ #entity_subtype_list_chips {
+ .md-chips {
+ padding-bottom: 1px;
+ }
+ }
+ .tb-error-messages {
+ margin-top: -11px;
+ height: 35px;
+ .tb-error-message {
+ padding-left: 1px;
+ }
+ }
+}*/
diff --git a/ui/src/app/entity/entity-subtype-list.tpl.html b/ui/src/app/entity/entity-subtype-list.tpl.html
new file mode 100644
index 0000000..2a1519a
--- /dev/null
+++ b/ui/src/app/entity/entity-subtype-list.tpl.html
@@ -0,0 +1,54 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+
+<section flex layout='column' class="tb-entity-subtype-list">
+ <md-chips flex
+ readonly="disabled"
+ id="entity_subtype_list_chips"
+ ng-required="tbRequired"
+ ng-model="entitySubtypesList"
+ placeholder="{{placeholder}}"
+ secondary-placeholder="{{secondaryPlaceholder}}"
+ md-autocomplete-snap
+ md-require-match="false">
+ <md-autocomplete
+ md-no-cache="true"
+ id="entitySubtype"
+ md-selected-item="selectedEntitySubtype"
+ md-search-text="entitySubtypeSearchText"
+ md-items="item in fetchEntitySubtypes(entitySubtypeSearchText)"
+ md-item-text="item"
+ md-min-length="0"
+ placeholder="{{ (!entitySubtypesList || !entitySubtypesList.length) ? placeholder : secondaryPlaceholder }}">
+ <md-item-template>
+ <span md-highlight-text="entitySubtypeSearchText" md-highlight-flags="^i">{{item}}</span>
+ </md-item-template>
+ <md-not-found>
+ <span translate translate-values='{ entitySubtype: entitySubtypeSearchText }'>{{noSubtypesMathingText}}</span>
+ </md-not-found>
+ </md-autocomplete>
+ <md-chip-template>
+ <span>
+ <strong>{{$chip}}</strong>
+ </span>
+ </md-chip-template>
+ </md-chips>
+ <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" ng-if="inputTouched && tbRequired" role="alert">
+ <div translate ng-message="entitySubtypesList" class="tb-error-message">{{subtypeListEmptyText}}</div>
+ </div>
+</section>
ui/src/app/entity/entity-type-list.directive.js 111(+111 -0)
diff --git a/ui/src/app/entity/entity-type-list.directive.js b/ui/src/app/entity/entity-type-list.directive.js
new file mode 100644
index 0000000..cb07cfc
--- /dev/null
+++ b/ui/src/app/entity/entity-type-list.directive.js
@@ -0,0 +1,111 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entityTypeListTemplate from './entity-type-list.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import './entity-type-list.scss';
+
+/*@ngInject*/
+export default function EntityTypeListDirective($compile, $templateCache, $q, $mdUtil, $translate, $filter, types, entityService) {
+
+ var linker = function (scope, element, attrs, ngModelCtrl) {
+
+ var template = $templateCache.get(entityTypeListTemplate);
+ element.html(template);
+
+ scope.ngModelCtrl = ngModelCtrl;
+
+ scope.placeholder = scope.tbRequired ? $translate.instant('entity.enter-entity-type')
+ : $translate.instant('entity.any-entity');
+ scope.secondaryPlaceholder = '+' + $translate.instant('entity.entity-type');
+
+ var entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes);
+ scope.entityTypesList = [];
+ for (var type in entityTypes) {
+ var entityTypeInfo = {};
+ entityTypeInfo.value = entityTypes[type];
+ entityTypeInfo.name = $translate.instant(types.entityTypeTranslations[entityTypeInfo.value].type) + '';
+ scope.entityTypesList.push(entityTypeInfo);
+ }
+
+ scope.$watch('tbRequired', function () {
+ scope.updateValidity();
+ });
+
+ scope.fetchEntityTypes = function(searchText) {
+ var deferred = $q.defer();
+ var entityTypes = $filter('filter')(scope.entityTypesList, {name: searchText});
+ deferred.resolve(entityTypes);
+ return deferred.promise;
+ }
+
+ scope.updateValidity = function() {
+ var value = ngModelCtrl.$viewValue;
+ var valid = !scope.tbRequired || value && value.length > 0;
+ ngModelCtrl.$setValidity('entityTypeList', valid);
+ }
+
+ ngModelCtrl.$render = function () {
+ scope.entityTypeList = [];
+ var value = ngModelCtrl.$viewValue;
+ if (value && value.length) {
+ value.forEach(function(type) {
+ var entityTypeInfo = {};
+ entityTypeInfo.value = type;
+ entityTypeInfo.name = $translate.instant(types.entityTypeTranslations[entityTypeInfo.value].type) + '';
+ scope.entityTypeList.push(entityTypeInfo);
+ });
+ }
+ }
+
+ scope.$watch('entityTypeList', function () {
+ var values = [];
+ if (scope.entityTypeList && scope.entityTypeList.length) {
+ scope.entityTypeList.forEach(function(entityType) {
+ values.push(entityType.value);
+ });
+ }
+ ngModelCtrl.$setViewValue(values);
+ scope.updateValidity();
+ }, true);
+
+ $compile(element.contents())(scope);
+
+ $mdUtil.nextTick(function(){
+ var inputElement = angular.element('input', element);
+ inputElement.on('blur', function() {
+ scope.inputTouched = true;
+ } );
+ });
+
+ }
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ link: linker,
+ scope: {
+ disabled:'=ngDisabled',
+ tbRequired: '=?',
+ allowedEntityTypes: '=?'
+ }
+ };
+
+}
ui/src/app/entity/entity-type-list.scss 30(+30 -0)
diff --git a/ui/src/app/entity/entity-type-list.scss b/ui/src/app/entity/entity-type-list.scss
new file mode 100644
index 0000000..b94b992
--- /dev/null
+++ b/ui/src/app/entity/entity-type-list.scss
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*.tb-entity-type-list {
+ #entity_type_list_chips {
+ .md-chips {
+ padding-bottom: 1px;
+ }
+ }
+ .tb-error-messages {
+ margin-top: -11px;
+ height: 35px;
+ .tb-error-message {
+ padding-left: 1px;
+ }
+ }
+}*/
\ No newline at end of file
ui/src/app/entity/entity-type-list.tpl.html 54(+54 -0)
diff --git a/ui/src/app/entity/entity-type-list.tpl.html b/ui/src/app/entity/entity-type-list.tpl.html
new file mode 100644
index 0000000..ff10a30
--- /dev/null
+++ b/ui/src/app/entity/entity-type-list.tpl.html
@@ -0,0 +1,54 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+
+<section flex layout='column' class="tb-entity-type-list">
+ <md-chips flex
+ readonly="disabled"
+ id="entity_type_list_chips"
+ ng-required="tbRequired"
+ ng-model="entityTypeList"
+ placeholder="{{placeholder}}"
+ secondary-placeholder="{{secondaryPlaceholder}}"
+ md-autocomplete-snap
+ md-require-match="true">
+ <md-autocomplete
+ md-no-cache="true"
+ id="entityType"
+ md-selected-item="selectedEntityType"
+ md-search-text="entityTypeSearchText"
+ md-items="item in fetchEntityTypes(entityTypeSearchText)"
+ md-item-text="item.name"
+ md-min-length="0"
+ placeholder="{{ (!entityTypeList || !entityTypeList.length) ? placeholder : secondaryPlaceholder }}">
+ <md-item-template>
+ <span md-highlight-text="entityTypeSearchText" md-highlight-flags="^i">{{item.name}}</span>
+ </md-item-template>
+ <md-not-found>
+ <span translate translate-values='{ entityType: entityTypeSearchText }'>entity.no-entity-types-matching</span>
+ </md-not-found>
+ </md-autocomplete>
+ <md-chip-template>
+ <span>
+ <strong>{{$chip.name}}</strong>
+ </span>
+ </md-chip-template>
+ </md-chips>
+ <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" ng-if="inputTouched && tbRequired" role="alert">
+ <div translate ng-message="entityTypeList" class="tb-error-message">entity.entity-type-list-empty</div>
+ </div>
+</section>
diff --git a/ui/src/app/entity/entity-type-select.directive.js b/ui/src/app/entity/entity-type-select.directive.js
index bca2ee0..068c141 100644
--- a/ui/src/app/entity/entity-type-select.directive.js
+++ b/ui/src/app/entity/entity-type-select.directive.js
@@ -23,7 +23,7 @@ import entityTypeSelectTemplate from './entity-type-select.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
-export default function EntityTypeSelect($compile, $templateCache, utils, userService, types) {
+export default function EntityTypeSelect($compile, $templateCache, utils, entityService, userService, types) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(entityTypeSelectTemplate);
@@ -39,36 +39,7 @@ export default function EntityTypeSelect($compile, $templateCache, utils, userSe
scope.ngModelCtrl = ngModelCtrl;
- var authority = userService.getAuthority();
- scope.entityTypes = {};
- switch(authority) {
- case 'SYS_ADMIN':
- scope.entityTypes.tenant = types.entityType.tenant;
- scope.entityTypes.rule = types.entityType.rule;
- scope.entityTypes.plugin = types.entityType.plugin;
- break;
- case 'TENANT_ADMIN':
- scope.entityTypes.device = types.entityType.device;
- scope.entityTypes.asset = types.entityType.asset;
- scope.entityTypes.customer = types.entityType.customer;
- scope.entityTypes.rule = types.entityType.rule;
- scope.entityTypes.plugin = types.entityType.plugin;
- scope.entityTypes.dashboard = types.entityType.dashboard;
- break;
- case 'CUSTOMER_USER':
- scope.entityTypes.device = types.entityType.device;
- scope.entityTypes.asset = types.entityType.asset;
- scope.entityTypes.dashboard = types.entityType.dashboard;
- break;
- }
-
- if (scope.allowedEntityTypes) {
- for (var entityType in scope.entityTypes) {
- if (scope.allowedEntityTypes.indexOf(scope.entityTypes[entityType]) === -1) {
- delete scope.entityTypes[entityType];
- }
- }
- }
+ scope.entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes);
scope.typeName = function(type) {
return type ? types.entityTypeTranslations[type].type : '';
ui/src/app/entity/index.js 14(+10 -4)
diff --git a/ui/src/app/entity/index.js b/ui/src/app/entity/index.js
index 2fc1d63..2b8d434 100644
--- a/ui/src/app/entity/index.js
+++ b/ui/src/app/entity/index.js
@@ -14,9 +14,11 @@
* limitations under the License.
*/
-import EntityAliasesController from './entity-aliases.controller';
-import EntityAliasDialogController from './entity-alias-dialog.controller';
+import EntityAliasesController from './alias/entity-aliases.controller';
+import EntityAliasDialogController from './alias/entity-alias-dialog.controller';
import EntityTypeSelectDirective from './entity-type-select.directive';
+import EntityTypeListDirective from './entity-type-list.directive';
+import EntitySubtypeListDirective from './entity-subtype-list.directive';
import EntitySubtypeSelectDirective from './entity-subtype-select.directive';
import EntitySubtypeAutocompleteDirective from './entity-subtype-autocomplete.directive';
import EntityAutocompleteDirective from './entity-autocomplete.directive';
@@ -24,11 +26,12 @@ import EntityListDirective from './entity-list.directive';
import EntitySelectDirective from './entity-select.directive';
import EntityFilterDirective from './entity-filter.directive';
import EntityFilterViewDirective from './entity-filter-view.directive';
-import AliasesEntitySelectPanelController from './aliases-entity-select-panel.controller';
-import AliasesEntitySelectDirective from './aliases-entity-select.directive';
+import AliasesEntitySelectPanelController from './alias/aliases-entity-select-panel.controller';
+import AliasesEntitySelectDirective from './alias/aliases-entity-select.directive';
import AddAttributeDialogController from './attribute/add-attribute-dialog.controller';
import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller';
import AttributeTableDirective from './attribute/attribute-table.directive';
+import RelationFiltersDirective from './relation/relation-filters.directive';
import RelationTableDirective from './relation/relation-table.directive';
import RelationTypeAutocompleteDirective from './relation/relation-type-autocomplete.directive';
@@ -39,6 +42,8 @@ export default angular.module('thingsboard.entity', [])
.controller('AddAttributeDialogController', AddAttributeDialogController)
.controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController)
.directive('tbEntityTypeSelect', EntityTypeSelectDirective)
+ .directive('tbEntityTypeList', EntityTypeListDirective)
+ .directive('tbEntitySubtypeList', EntitySubtypeListDirective)
.directive('tbEntitySubtypeSelect', EntitySubtypeSelectDirective)
.directive('tbEntitySubtypeAutocomplete', EntitySubtypeAutocompleteDirective)
.directive('tbEntityAutocomplete', EntityAutocompleteDirective)
@@ -48,6 +53,7 @@ export default angular.module('thingsboard.entity', [])
.directive('tbEntityFilterView', EntityFilterViewDirective)
.directive('tbAliasesEntitySelect', AliasesEntitySelectDirective)
.directive('tbAttributeTable', AttributeTableDirective)
+ .directive('tbRelationFilters', RelationFiltersDirective)
.directive('tbRelationTable', RelationTableDirective)
.directive('tbRelationTypeAutocomplete', RelationTypeAutocompleteDirective)
.name;
diff --git a/ui/src/app/entity/relation/relation-filters.directive.js b/ui/src/app/entity/relation/relation-filters.directive.js
new file mode 100644
index 0000000..59821dd
--- /dev/null
+++ b/ui/src/app/entity/relation/relation-filters.directive.js
@@ -0,0 +1,85 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './relation-filters.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import relationFiltersTemplate from './relation-filters.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RelationFilters($compile, $templateCache) {
+
+ return {
+ restrict: "E",
+ require: "^ngModel",
+ scope: {
+ allowedEntityTypes: '=?'
+ },
+ link: linker
+ };
+
+ function linker( scope, element, attrs, ngModelCtrl ) {
+
+ var template = $templateCache.get(relationFiltersTemplate);
+ element.html(template);
+
+ scope.relationFilters = [];
+
+ scope.addFilter = addFilter;
+ scope.removeFilter = removeFilter;
+
+ ngModelCtrl.$render = function () {
+ if (ngModelCtrl.$viewValue) {
+ var value = ngModelCtrl.$viewValue;
+ value.forEach(function (filter) {
+ scope.relationFilters.push(filter);
+ });
+ }
+ scope.$watch('relationFilters', function (newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal)) {
+ updateValue();
+ }
+ }, true);
+ }
+
+ function addFilter() {
+ var filter = {
+ relationType: null,
+ entityTypes: []
+ };
+ scope.relationFilters.push(filter);
+ }
+
+ function removeFilter($event, filter) {
+ var index = scope.relationFilters.indexOf(filter);
+ if (index > -1) {
+ scope.relationFilters.splice(index, 1);
+ }
+ }
+
+ function updateValue() {
+ var value = [];
+ scope.relationFilters.forEach(function (filter) {
+ value.push(filter);
+ });
+ ngModelCtrl.$setViewValue(value);
+ }
+ $compile(element.contents())(scope);
+ }
+}
diff --git a/ui/src/app/entity/relation/relation-filters.scss b/ui/src/app/entity/relation/relation-filters.scss
new file mode 100644
index 0000000..50d49af
--- /dev/null
+++ b/ui/src/app/entity/relation/relation-filters.scss
@@ -0,0 +1,77 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.tb-relation-filters {
+ .header {
+ padding-left: 5px;
+ padding-right: 5px;
+ padding-bottom: 5px;
+ .cell {
+ padding-left: 5px;
+ padding-right: 5px;
+ color: rgba(0,0,0,.54);
+ font-size: 12px;
+ font-weight: 700;
+ white-space: nowrap;
+ }
+ }
+ .body {
+ padding-left: 5px;
+ padding-right: 5px;
+ max-height: 300px;
+ overflow: auto;
+ padding-bottom: 20px;
+ .row {
+ padding-top: 5px;
+ }
+ .cell {
+ padding-left: 5px;
+ padding-right: 5px;
+
+ md-select {
+ margin: 0 0 24px;
+ }
+
+ md-input-container {
+ margin: 0;
+ }
+
+ md-chips-wrap {
+ padding: 0px;
+ margin: 0 0 24px;
+ .md-chip-input-container {
+ margin: 0;
+ }
+ md-autocomplete {
+ height: 30px;
+ md-autocomplete-wrap {
+ height: 30px;
+ }
+ }
+ }
+ .md-chips .md-chip-input-container input {
+ padding: 2px 2px 2px;
+ height: 26px;
+ line-height: 26px;
+ }
+
+ }
+
+ .md-button {
+ margin: 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/entity/relation/relation-filters.tpl.html b/ui/src/app/entity/relation/relation-filters.tpl.html
new file mode 100644
index 0000000..679a488
--- /dev/null
+++ b/ui/src/app/entity/relation/relation-filters.tpl.html
@@ -0,0 +1,67 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<div class="tb-relation-filters">
+ <div class="header" ng-show="relationFilters.length">
+ <div layout="row" layout-align="start center">
+ <span class="cell" style="width: 200px; min-width: 200px;" translate>relation.type</span>
+ <span class="cell" flex translate>entity.entity-types</span>
+ <span class="cell" style="width: 40px; min-width: 40px;"> </span>
+ </div>
+ </div>
+ <div class="body" ng-show="relationFilters.length">
+ <div class="row" ng-form name="relationFilterForm" flex layout="row" layout-align="start center" ng-repeat="filter in relationFilters track by $index">
+ <div class="md-whiteframe-1dp" flex layout="row" layout-align="start center">
+ <tb-relation-type-autocomplete class="cell" style="width: 200px; min-width: 200px;"
+ hide-label
+ the-form="relationFilterForm"
+ ng-model="filter.relationType"
+ tb-required="false">
+ </tb-relation-type-autocomplete>
+ <tb-entity-type-list class="cell" flex
+ ng-model="filter.entityTypes"
+ allowed-entity-types="allowedEntityTypes"
+ tb-required="false">
+ </tb-entity-type-list>
+ <md-button ng-disabled="loading" class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"
+ ng-click="removeFilter($event, filter)" aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'relation.remove-relation-filter' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+ </div>
+ </div>
+ </div>
+ <div class="any-filter" ng-show="!relationFilters.length">
+ <span layout-align="center center"
+ class="tb-prompt" translate>relation.any-relation</span>
+ </div>
+ <div>
+ <md-button ng-disabled="loading" class="md-primary md-raised" ng-click="addFilter($event)" aria-label="{{ 'action.add' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'relation.add-relation-filter' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons">
+ add
+ </md-icon>
+ {{ 'action.add' | translate }}
+ </md-button>
+ </div>
+</div>
\ No newline at end of file
diff --git a/ui/src/app/entity/relation/relation-type-autocomplete.directive.js b/ui/src/app/entity/relation/relation-type-autocomplete.directive.js
index a7e5ea4..4b5480d 100644
--- a/ui/src/app/entity/relation/relation-type-autocomplete.directive.js
+++ b/ui/src/app/entity/relation/relation-type-autocomplete.directive.js
@@ -29,6 +29,8 @@ export default function RelationTypeAutocomplete($compile, $templateCache, $q, $
element.html(template);
scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+ scope.hideLabel = angular.isDefined(attrs.hideLabel) ? true : false;
+
scope.relationType = null;
scope.relationTypeSearchText = '';
scope.relationTypes = [];
diff --git a/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html b/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html
index f71c134..39f32d6 100644
--- a/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html
+++ b/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html
@@ -26,7 +26,7 @@
md-items="item in fetchRelationTypes(relationTypeSearchText)"
md-item-text="item"
md-min-length="0"
- md-floating-label="{{ 'relation.relation-type' | translate }}"
+ md-floating-label="{{ tbRequired ? ('relation.relation-type' | translate) : ( !relationType ? ('relation.any-relation-type' | translate) : ' ') }}"
md-select-on-match="true"
md-menu-class="tb-relation-type-autocomplete">
<md-item-template>
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 8d4a098..86d5240 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -16,7 +16,7 @@
/* eslint-disable import/no-unresolved, import/default */
import importDialogTemplate from './import-dialog.tpl.html';
-import entityAliasesTemplate from '../entity/entity-aliases.tpl.html';
+import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
ui/src/app/locale/locale.constant.js 50(+48 -2)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 88a6021..cd21747 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -129,14 +129,24 @@ export default angular.module('thingsboard.locale', [])
"filter-type-device-type-description": "Devices of type '{{deviceType}}'",
"filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'",
"filter-type-relations-query": "Relations query",
+ "filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"filter-type-asset-search-query": "Asset search query",
+ "filter-type-asset-search-query-description": "Assets with types {{assetTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"filter-type-device-search-query": "Device search query",
+ "filter-type-device-search-query-description": "Devices with types {{deviceTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"entity-filter": "Entity filter",
"resolve-multiple": "Resolve as multiple entities",
"filter-type": "Filter type",
"filter-type-required": "Filter type is required.",
"entity-filter-no-entity-matched": "No entities matching specified filter were found.",
- "no-entity-filter-specified": "No entity filter specified"
+ "no-entity-filter-specified": "No entity filter specified",
+ "root-state-entity": "Use dashboard state entity as root",
+ "root-entity": "Root entity",
+ "max-relation-level": "Max relation level",
+ "unlimited-level": "Unlimited level",
+ "state-entity": "Dashboard state entity",
+ "all-entities": "All entities",
+ "any-relation": "any"
},
"asset": {
"asset": "Asset",
@@ -159,6 +169,11 @@ export default angular.module('thingsboard.locale', [])
"asset-type": "Asset type",
"asset-type-required": "Asset type is required.",
"select-asset-type": "Select asset type",
+ "enter-asset-type": "Enter asset type",
+ "any-asset": "Any asset",
+ "no-asset-types-matching": "No asset types matching '{{entitySubtype}}' were found.",
+ "asset-type-list-empty": "No asset types selected.",
+ "asset-types": "Asset types",
"name": "Name",
"name-required": "Name is required.",
"description": "Description",
@@ -444,6 +459,7 @@ export default angular.module('thingsboard.locale', [])
},
"datasource": {
"type": "Datasource type",
+ "name": "Name",
"add-datasource-prompt": "Please add datasource"
},
"details": {
@@ -524,6 +540,11 @@ export default angular.module('thingsboard.locale', [])
"device-type": "Device type",
"device-type-required": "Device type is required.",
"select-device-type": "Select device type",
+ "enter-device-type": "Enter device type",
+ "any-device": "Any device",
+ "no-device-types-matching": "No device types matching '{{entitySubtype}}' were found.",
+ "device-type-list-empty": "No device types selected.",
+ "device-types": "Device types",
"name": "Name",
"name-required": "Name is required.",
"description": "Description",
@@ -564,10 +585,17 @@ export default angular.module('thingsboard.locale', [])
"remove-alias": "Remove entity alias",
"add-alias": "Add entity alias",
"entity-list": "Entity list",
+ "entity-type": "Entity type",
+ "entity-types": "Entity types",
+ "entity-type-list": "Entity type list",
+ "any-entity": "Any entity",
+ "enter-entity-type": "Enter entity type",
"no-entities-matching": "No entities matching '{{entity}}' were found.",
+ "no-entity-types-matching": "No entity types matching '{{entityType}}' were found.",
"name-starts-with": "Name starts with",
"use-entity-name-filter": "Use filter",
"entity-list-empty": "No entities selected.",
+ "entity-type-list-empty": "No entity types selected.",
"entity-name-filter-required": "Entity name filter is required.",
"entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.",
"all-subtypes": "All",
@@ -581,30 +609,39 @@ export default angular.module('thingsboard.locale', [])
"type": "Type",
"type-required": "Entity type is required.",
"type-device": "Device",
+ "type-devices": "Devices",
"list-of-devices": "{ count, select, 1 {One device} other {List of # devices} }",
"device-name-starts-with": "Devices whose names start with '{{prefix}}'",
"type-asset": "Asset",
+ "type-assets": "Assets",
"list-of-assets": "{ count, select, 1 {One asset} other {List of # assets} }",
"asset-name-starts-with": "Assets whose names start with '{{prefix}}'",
"type-rule": "Rule",
+ "type-rules": "Rules",
"list-of-rules": "{ count, select, 1 {One rule} other {List of # rules} }",
"rule-name-starts-with": "Rules whose names start with '{{prefix}}'",
"type-plugin": "Plugin",
+ "type-plugins": "Plugins",
"list-of-plugins": "{ count, select, 1 {One plugin} other {List of # plugins} }",
"plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'",
"type-tenant": "Tenant",
+ "type-tenants": "Tenants",
"list-of-tenants": "{ count, select, 1 {One tenant} other {List of # tenants} }",
"tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'",
"type-customer": "Customer",
+ "type-customers": "Customers",
"list-of-customers": "{ count, select, 1 {One customer} other {List of # customers} }",
"customer-name-starts-with": "Customers whose names start with '{{prefix}}'",
"type-user": "User",
+ "type-users": "Users",
"list-of-users": "{ count, select, 1 {One user} other {List of # users} }",
"user-name-starts-with": "Users whose names start with '{{prefix}}'",
"type-dashboard": "Dashboard",
+ "type-dashboards": "Dashboards",
"list-of-dashboards": "{ count, select, 1 {One dashboard} other {List of # dashboards} }",
"dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'",
"type-alarm": "Alarm",
+ "type-alarms": "Alarms",
"list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }",
"alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'"
},
@@ -770,6 +807,10 @@ export default angular.module('thingsboard.locale', [])
"FROM": "From",
"TO": "To"
},
+ "direction-type": {
+ "FROM": "from",
+ "TO": "to"
+ },
"from-relations": "Outbound relations",
"to-relations": "Inbound relations",
"selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected",
@@ -783,6 +824,7 @@ export default angular.module('thingsboard.locale', [])
"delete": "Delete relation",
"relation-type": "Relation type",
"relation-type-required": "Relation type is required.",
+ "any-relation-type": "Any type",
"add": "Add relation",
"delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?",
"delete-to-relation-text": "Be careful, after the confirmation the entity '{{entityName}}' will be unrelated from the current entity.",
@@ -791,7 +833,11 @@ export default angular.module('thingsboard.locale', [])
"delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?",
"delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.",
"delete-from-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?",
- "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities."
+ "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities.",
+ "remove-relation-filter": "Remove relation filter",
+ "add-relation-filter": "Add relation filter",
+ "any-relation": "Any relation",
+ "relation-filters": "Relation filters"
},
"rule": {
"rule": "Rule",
ui/src/scss/main.scss 9(+9 -0)
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index dbe3543..bf44493 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -236,6 +236,15 @@ div {
}
}
+.md-caption {
+ &.tb-required:after {
+ content: ' *';
+ font-size: 10px;
+ vertical-align: top;
+ color: rgba(0,0,0,0.54);
+ }
+}
+
pre.tb-highlight {
background-color: #f7f7f7;
display: block;