Browse Source

TB-61: Implemented new alias filters.

pull/164/head
Igor Kulikov 9 years ago
parent
commit
9cdfe4ef54
  1. 15
      application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
  2. 26
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
  3. 2
      dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
  4. 14
      ui/src/app/api/entity-relation.service.js
  5. 249
      ui/src/app/api/entity.service.js
  6. 9
      ui/src/app/common/types.constant.js
  7. 8
      ui/src/app/components/datasource-func.directive.js
  8. 38
      ui/src/app/components/datasource-func.scss
  9. 111
      ui/src/app/components/datasource-func.tpl.html
  10. 2
      ui/src/app/dashboard/add-widget.controller.js
  11. 2
      ui/src/app/dashboard/dashboard.controller.js
  12. 2
      ui/src/app/dashboard/edit-widget.directive.js
  13. 0
      ui/src/app/entity/alias/aliases-entity-select-button.tpl.html
  14. 0
      ui/src/app/entity/alias/aliases-entity-select-panel.controller.js
  15. 0
      ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html
  16. 0
      ui/src/app/entity/alias/aliases-entity-select.directive.js
  17. 0
      ui/src/app/entity/alias/aliases-entity-select.scss
  18. 0
      ui/src/app/entity/alias/entity-alias-dialog.controller.js
  19. 0
      ui/src/app/entity/alias/entity-alias-dialog.scss
  20. 0
      ui/src/app/entity/alias/entity-alias-dialog.tpl.html
  21. 0
      ui/src/app/entity/alias/entity-aliases.controller.js
  22. 0
      ui/src/app/entity/alias/entity-aliases.scss
  23. 0
      ui/src/app/entity/alias/entity-aliases.tpl.html
  24. 101
      ui/src/app/entity/entity-filter-view.directive.js
  25. 18
      ui/src/app/entity/entity-filter.directive.js
  26. 17
      ui/src/app/entity/entity-filter.scss
  27. 142
      ui/src/app/entity/entity-filter.tpl.html
  28. 3
      ui/src/app/entity/entity-subtype-autocomplete.directive.js
  29. 146
      ui/src/app/entity/entity-subtype-list.directive.js
  30. 30
      ui/src/app/entity/entity-subtype-list.scss
  31. 54
      ui/src/app/entity/entity-subtype-list.tpl.html
  32. 111
      ui/src/app/entity/entity-type-list.directive.js
  33. 30
      ui/src/app/entity/entity-type-list.scss
  34. 54
      ui/src/app/entity/entity-type-list.tpl.html
  35. 33
      ui/src/app/entity/entity-type-select.directive.js
  36. 14
      ui/src/app/entity/index.js
  37. 85
      ui/src/app/entity/relation/relation-filters.directive.js
  38. 77
      ui/src/app/entity/relation/relation-filters.scss
  39. 67
      ui/src/app/entity/relation/relation-filters.tpl.html
  40. 2
      ui/src/app/entity/relation/relation-type-autocomplete.directive.js
  41. 2
      ui/src/app/entity/relation/relation-type-autocomplete.tpl.html
  42. 2
      ui/src/app/import-export/import-export.service.js
  43. 50
      ui/src/app/locale/locale.constant.js
  44. 9
      ui/src/scss/main.scss

15
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) {

26
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!");

2
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);

14
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;
}
}

249
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();

9
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'
}

8
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;
}
};

38
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;
}
}
}
}

111
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:&apos;...&apos;}}" }'>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:&apos;...&apos;}}" }'>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>

2
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 */

2
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';

2
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 */

0
ui/src/app/entity/aliases-entity-select-button.tpl.html → ui/src/app/entity/alias/aliases-entity-select-button.tpl.html

0
ui/src/app/entity/aliases-entity-select-panel.controller.js → ui/src/app/entity/alias/aliases-entity-select-panel.controller.js

0
ui/src/app/entity/aliases-entity-select-panel.tpl.html → ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html

0
ui/src/app/entity/aliases-entity-select.directive.js → ui/src/app/entity/alias/aliases-entity-select.directive.js

0
ui/src/app/entity/aliases-entity-select.scss → ui/src/app/entity/alias/aliases-entity-select.scss

0
ui/src/app/entity/entity-alias-dialog.controller.js → ui/src/app/entity/alias/entity-alias-dialog.controller.js

0
ui/src/app/entity/entity-alias-dialog.scss → ui/src/app/entity/alias/entity-alias-dialog.scss

0
ui/src/app/entity/entity-alias-dialog.tpl.html → ui/src/app/entity/alias/entity-alias-dialog.tpl.html

0
ui/src/app/entity/entity-aliases.controller.js → ui/src/app/entity/alias/entity-aliases.controller.js

0
ui/src/app/entity/entity-aliases.scss → ui/src/app/entity/alias/entity-aliases.scss

0
ui/src/app/entity/entity-aliases.tpl.html → ui/src/app/entity/alias/entity-aliases.tpl.html

101
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;

18
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;
}

17
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;
}
}
}

142
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>

3
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';

146
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: "="
}
};
}

30
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;
}
}
}*/

54
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>

111
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: '=?'
}
};
}

30
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;
}
}
}*/

54
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>

33
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 : '';

14
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;

85
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);
}
}

77
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;
}
}
}

67
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;">&nbsp</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>

2
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 = [];

2
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>

2
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 */

50
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",

9
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;

Loading…
Cancel
Save