Merge remote-tracking branch 'upstream/master' into dao-refactoring-vs

This commit is contained in:
volodymyr-babak 2017-05-30 19:45:58 +03:00
commit 2f79950178
24 changed files with 454 additions and 94 deletions

View File

@ -34,7 +34,7 @@ import java.util.List;
@RequestMapping("/api") @RequestMapping("/api")
public class EntityRelationController extends BaseController { public class EntityRelationController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relation", method = RequestMethod.POST) @RequestMapping(value = "/relation", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void saveRelation(@RequestBody EntityRelation relation) throws ThingsboardException { public void saveRelation(@RequestBody EntityRelation relation) throws ThingsboardException {
@ -42,31 +42,33 @@ public class EntityRelationController extends BaseController {
checkNotNull(relation); checkNotNull(relation);
checkEntityId(relation.getFrom()); checkEntityId(relation.getFrom());
checkEntityId(relation.getTo()); checkEntityId(relation.getTo());
if (relation.getTypeGroup() == null) {
relation.setTypeGroup(RelationTypeGroup.COMMON);
}
relationService.saveRelation(relation).get(); relationService.saveRelation(relation).get();
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relation", method = RequestMethod.DELETE, params = {"fromId", "fromType", "relationType", "toId", "toType"}) @RequestMapping(value = "/relation", method = RequestMethod.DELETE, params = {"fromId", "fromType", "relationType", "toId", "toType"})
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void deleteRelation(@RequestParam("fromId") String strFromId, public void deleteRelation(@RequestParam("fromId") String strFromId,
@RequestParam("fromType") String strFromType, @RequestParam("fromType") String strFromType,
@RequestParam("relationType") String strRelationType, @RequestParam("relationType") String strRelationType,
@RequestParam("relationTypeGroup") String strRelationTypeGroup, @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup,
@RequestParam("toId") String strToId, @RequestParam("toType") String strToType) throws ThingsboardException { @RequestParam("toId") String strToId, @RequestParam("toType") String strToType) throws ThingsboardException {
checkParameter("fromId", strFromId); checkParameter("fromId", strFromId);
checkParameter("fromType", strFromType); checkParameter("fromType", strFromType);
checkParameter("relationType", strRelationType); checkParameter("relationType", strRelationType);
checkParameter("relationTypeGroup", strRelationTypeGroup);
checkParameter("toId", strToId); checkParameter("toId", strToId);
checkParameter("toType", strToType); checkParameter("toType", strToType);
EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId); EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
EntityId toId = EntityIdFactory.getByTypeAndId(strToType, strToId); EntityId toId = EntityIdFactory.getByTypeAndId(strToType, strToId);
checkEntityId(fromId); checkEntityId(fromId);
checkEntityId(toId); checkEntityId(toId);
RelationTypeGroup relationTypeGroup = RelationTypeGroup.valueOf(strRelationTypeGroup); RelationTypeGroup relationTypeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
try { try {
Boolean found = relationService.deleteRelation(fromId, toId, strRelationType, relationTypeGroup).get(); Boolean found = relationService.deleteRelation(fromId, toId, strRelationType, relationTypeGroup).get();
if (!found) { if (!found) {
@ -77,7 +79,7 @@ public class EntityRelationController extends BaseController {
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"id", "type"}) @RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"id", "type"})
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void deleteRelations(@RequestParam("entityId") String strId, public void deleteRelations(@RequestParam("entityId") String strId,
@ -93,7 +95,7 @@ public class EntityRelationController extends BaseController {
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relation", method = RequestMethod.GET, params = {"fromId", "fromType", "relationType", "toId", "toType"}) @RequestMapping(value = "/relation", method = RequestMethod.GET, params = {"fromId", "fromType", "relationType", "toId", "toType"})
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void checkRelation(@RequestParam("fromId") String strFromId, public void checkRelation(@RequestParam("fromId") String strFromId,
@ -121,7 +123,7 @@ public class EntityRelationController extends BaseController {
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"fromId", "fromType"}) @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"fromId", "fromType"})
@ResponseBody @ResponseBody
public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId, public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId,
@ -139,7 +141,7 @@ public class EntityRelationController extends BaseController {
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {"fromId", "fromType"}) @RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {"fromId", "fromType"})
@ResponseBody @ResponseBody
public List<EntityRelationInfo> findInfoByFrom(@RequestParam("fromId") String strFromId, public List<EntityRelationInfo> findInfoByFrom(@RequestParam("fromId") String strFromId,
@ -157,7 +159,7 @@ public class EntityRelationController extends BaseController {
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"fromId", "fromType", "relationType"}) @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"fromId", "fromType", "relationType"})
@ResponseBody @ResponseBody
public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId, public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId,
@ -177,7 +179,7 @@ public class EntityRelationController extends BaseController {
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"toId", "toType"}) @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"toId", "toType"})
@ResponseBody @ResponseBody
public List<EntityRelation> findByTo(@RequestParam("toId") String strToId, public List<EntityRelation> findByTo(@RequestParam("toId") String strToId,
@ -195,7 +197,25 @@ public class EntityRelationController extends BaseController {
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {"toId", "toType"})
@ResponseBody
public List<EntityRelationInfo> findInfoByTo(@RequestParam("toId") String strToId,
@RequestParam("toType") String strToType,
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
checkParameter("toId", strToId);
checkParameter("toType", strToType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strToType, strToId);
checkEntityId(entityId);
RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
try {
return checkNotNull(relationService.findInfoByTo(entityId, typeGroup).get());
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"toId", "toType", "relationType"}) @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"toId", "toType", "relationType"})
@ResponseBody @ResponseBody
public List<EntityRelation> findByTo(@RequestParam("toId") String strToId, public List<EntityRelation> findByTo(@RequestParam("toId") String strToId,
@ -215,7 +235,7 @@ public class EntityRelationController extends BaseController {
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations", method = RequestMethod.POST) @RequestMapping(value = "/relations", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public List<EntityRelation> findByQuery(@RequestBody EntityRelationsQuery query) throws ThingsboardException { public List<EntityRelation> findByQuery(@RequestBody EntityRelationsQuery query) throws ThingsboardException {

View File

@ -20,6 +20,7 @@ public class EntityRelationInfo extends EntityRelation {
private static final long serialVersionUID = 2807343097519543363L; private static final long serialVersionUID = 2807343097519543363L;
private String fromName;
private String toName; private String toName;
public EntityRelationInfo() { public EntityRelationInfo() {
@ -30,6 +31,14 @@ public class EntityRelationInfo extends EntityRelation {
super(entityRelation); super(entityRelation);
} }
public String getFromName() {
return fromName;
}
public void setFromName(String fromName) {
this.fromName = fromName;
}
public String getToName() { public String getToName() {
return toName; return toName;
} }

View File

@ -23,29 +23,17 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo; import org.thingsboard.server.common.data.relation.EntityRelationInfo;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.plugin.PluginService;
import org.thingsboard.server.dao.rule.RuleService;
import org.thingsboard.server.dao.tenant.TenantService;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
/** /**
* Created by ashvayka on 28.04.17. * Created by ashvayka on 28.04.17.
@ -133,23 +121,16 @@ public class BaseRelationService implements RelationService {
ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations, ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
(AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> { (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>(); List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
relations1.stream().forEach(relation -> futures.add(fetchRelationInfoAsync(relation))); relations1.stream().forEach(relation ->
futures.add(fetchRelationInfoAsync(relation,
relation2 -> relation2.getTo(),
(EntityRelationInfo relationInfo, String entityName) -> relationInfo.setToName(entityName)))
);
return Futures.successfulAsList(futures); return Futures.successfulAsList(futures);
}); });
return relationsInfo; return relationsInfo;
} }
private ListenableFuture<EntityRelationInfo> fetchRelationInfoAsync(EntityRelation relation) {
ListenableFuture<String> entityName = entityService.fetchEntityNameAsync(relation.getTo());
ListenableFuture<EntityRelationInfo> entityRelationInfo =
Futures.transform(entityName, (Function<String, EntityRelationInfo>) entityName1 -> {
EntityRelationInfo entityRelationInfo1 = new EntityRelationInfo(relation);
entityRelationInfo1.setToName(entityName1);
return entityRelationInfo1;
});
return entityRelationInfo;
}
@Override @Override
public ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup) { public ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup) {
log.trace("Executing findByFromAndType [{}][{}][{}]", from, relationType, typeGroup); log.trace("Executing findByFromAndType [{}][{}][{}]", from, relationType, typeGroup);
@ -167,6 +148,38 @@ public class BaseRelationService implements RelationService {
return relationDao.findAllByTo(to, typeGroup); return relationDao.findAllByTo(to, typeGroup);
} }
@Override
public ListenableFuture<List<EntityRelationInfo>> findInfoByTo(EntityId to, RelationTypeGroup typeGroup) {
log.trace("Executing findInfoByTo [{}][{}]", to, typeGroup);
validate(to);
validateTypeGroup(typeGroup);
ListenableFuture<List<EntityRelation>> relations = relationDao.findAllByTo(to, typeGroup);
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 -> relation2.getFrom(),
(EntityRelationInfo relationInfo, String entityName) -> relationInfo.setFromName(entityName)))
);
return Futures.successfulAsList(futures);
});
return relationsInfo;
}
private ListenableFuture<EntityRelationInfo> fetchRelationInfoAsync(EntityRelation relation,
Function<EntityRelation, EntityId> entityIdGetter,
BiConsumer<EntityRelationInfo, String> entityNameSetter) {
ListenableFuture<String> entityName = entityService.fetchEntityNameAsync(entityIdGetter.apply(relation));
ListenableFuture<EntityRelationInfo> entityRelationInfo =
Futures.transform(entityName, (Function<String, EntityRelationInfo>) entityName1 -> {
EntityRelationInfo entityRelationInfo1 = new EntityRelationInfo(relation);
entityNameSetter.accept(entityRelationInfo1, entityName1);
return entityRelationInfo1;
});
return entityRelationInfo;
}
@Override @Override
public ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup) { public ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup) {
log.trace("Executing findByToAndType [{}][{}][{}]", to, relationType, typeGroup); log.trace("Executing findByToAndType [{}][{}][{}]", to, relationType, typeGroup);

View File

@ -46,6 +46,8 @@ public interface RelationService {
ListenableFuture<List<EntityRelation>> findByTo(EntityId to, RelationTypeGroup typeGroup); ListenableFuture<List<EntityRelation>> findByTo(EntityId to, RelationTypeGroup typeGroup);
ListenableFuture<List<EntityRelationInfo>> findInfoByTo(EntityId to, RelationTypeGroup typeGroup);
ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup); ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup);
ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query); ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query);

View File

@ -28,6 +28,7 @@ function EntityRelationService($http, $q) {
findInfoByFrom: findInfoByFrom, findInfoByFrom: findInfoByFrom,
findByFromAndType: findByFromAndType, findByFromAndType: findByFromAndType,
findByTo: findByTo, findByTo: findByTo,
findInfoByTo: findInfoByTo,
findByToAndType: findByToAndType, findByToAndType: findByToAndType,
findByQuery: findByQuery findByQuery: findByQuery
} }
@ -122,6 +123,18 @@ function EntityRelationService($http, $q) {
return deferred.promise; return deferred.promise;
} }
function findInfoByTo(toId, toType) {
var deferred = $q.defer();
var url = '/api/relations/info?toId=' + toId;
url += '&toType=' + toType;
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function findByToAndType(toId, toType, relationType) { function findByToAndType(toId, toType, relationType) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/relations?toId=' + toId; var url = '/api/relations?toId=' + toId;

View File

@ -55,5 +55,11 @@
default-event-type="{{vm.types.eventType.alarm.value}}"> default-event-type="{{vm.types.eventType.alarm.value}}">
</tb-event-table> </tb-event-table>
</md-tab> </md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
<tb-relation-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.customer}}">
</tb-relation-table>
</md-tab>
</md-tabs> </md-tabs>
</tb-grid> </tb-grid>

View File

@ -56,4 +56,10 @@
default-event-type="{{vm.types.eventType.alarm.value}}"> default-event-type="{{vm.types.eventType.alarm.value}}">
</tb-event-table> </tb-event-table>
</md-tab> </md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
<tb-relation-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.device}}">
</tb-relation-table>
</md-tab>
</tb-grid> </tb-grid>

View File

@ -41,7 +41,7 @@
</md-autocomplete> </md-autocomplete>
<md-chip-template> <md-chip-template>
<span> <span>
<strong>{{itemName($chip)}}</strong> <strong>{{$chip.name}}</strong>
</span> </span>
</md-chip-template> </md-chip-template>
</md-chips> </md-chips>

View File

@ -17,6 +17,9 @@
--> -->
<div layout='row' class="tb-entity-select"> <div layout='row' class="tb-entity-select">
<tb-entity-type-select style="min-width: 100px;" <tb-entity-type-select style="min-width: 100px;"
the-form="theForm"
ng-disabled="disabled"
tb-required="tbRequired"
ng-model="model.entityType"> ng-model="model.entityType">
</tb-entity-type-select> </tb-entity-type-select>
<tb-entity-autocomplete flex ng-if="model.entityType" <tb-entity-autocomplete flex ng-if="model.entityType"

View File

@ -29,6 +29,8 @@ export default function EntityTypeSelect($compile, $templateCache, utils, userSe
var template = $templateCache.get(entityTypeSelectTemplate); var template = $templateCache.get(entityTypeSelectTemplate);
element.html(template); element.html(template);
scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
if (angular.isDefined(attrs.hideLabel)) { if (angular.isDefined(attrs.hideLabel)) {
scope.showLabel = false; scope.showLabel = false;
} else { } else {
@ -103,6 +105,9 @@ export default function EntityTypeSelect($compile, $templateCache, utils, userSe
require: "^ngModel", require: "^ngModel",
link: linker, link: linker,
scope: { scope: {
theForm: '=?',
tbRequired: '=?',
disabled:'=ngDisabled',
allowedEntityTypes: "=?" allowedEntityTypes: "=?"
} }
}; };

View File

@ -17,9 +17,13 @@
--> -->
<md-input-container> <md-input-container>
<label ng-if="showLabel">{{ 'entity.type' | translate }}</label> <label ng-if="showLabel">{{ 'entity.type' | translate }}</label>
<md-select ng-model="entityType" class="tb-entity-type-select" aria-label="{{ 'entity.type' | translate }}"> <md-select ng-required="tbRequired" ng-disabled="disabled" name="entityType"
ng-model="entityType" class="tb-entity-type-select" aria-label="{{ 'entity.type' | translate }}">
<md-option ng-repeat="type in entityTypes" ng-value="type"> <md-option ng-repeat="type in entityTypes" ng-value="type">
{{typeName(type) | translate}} {{typeName(type) | translate}}
</md-option> </md-option>
</md-select> </md-select>
<div ng-messages="theForm.entityType.$error">
<div ng-message="required" translate>entity.type-required</div>
</div>
</md-input-container> </md-input-container>

View File

@ -27,6 +27,7 @@ import AddAttributeDialogController from './attribute/add-attribute-dialog.contr
import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller'; import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller';
import AttributeTableDirective from './attribute/attribute-table.directive'; import AttributeTableDirective from './attribute/attribute-table.directive';
import RelationTableDirective from './relation/relation-table.directive'; import RelationTableDirective from './relation/relation-table.directive';
import RelationTypeAutocompleteDirective from './relation/relation-type-autocomplete.directive';
export default angular.module('thingsboard.entity', []) export default angular.module('thingsboard.entity', [])
.controller('EntityAliasesController', EntityAliasesController) .controller('EntityAliasesController', EntityAliasesController)
@ -42,4 +43,5 @@ export default angular.module('thingsboard.entity', [])
.directive('tbAliasesEntitySelect', AliasesEntitySelectDirective) .directive('tbAliasesEntitySelect', AliasesEntitySelectDirective)
.directive('tbAttributeTable', AttributeTableDirective) .directive('tbAttributeTable', AttributeTableDirective)
.directive('tbRelationTable', RelationTableDirective) .directive('tbRelationTable', RelationTableDirective)
.directive('tbRelationTypeAutocomplete', RelationTypeAutocompleteDirective)
.name; .name;

View File

@ -14,14 +14,20 @@
* limitations under the License. * limitations under the License.
*/ */
/*@ngInject*/ /*@ngInject*/
export default function AddRelationDialogController($scope, $mdDialog, types, entityRelationService, from) { export default function AddRelationDialogController($scope, $mdDialog, types, entityRelationService, direction, entityId) {
var vm = this; var vm = this;
vm.types = types; vm.types = types;
vm.direction = direction;
vm.targetEntityId = {};
vm.relation = {}; vm.relation = {};
vm.relation.from = from; if (vm.direction == vm.types.entitySearchDirection.from) {
vm.relation.from = entityId;
} else {
vm.relation.to = entityId;
}
vm.relation.type = types.entityRelationType.contains; vm.relation.type = types.entityRelationType.contains;
vm.add = add; vm.add = add;
@ -32,6 +38,11 @@ export default function AddRelationDialogController($scope, $mdDialog, types, en
} }
function add() { function add() {
if (vm.direction == vm.types.entitySearchDirection.from) {
vm.relation.to = vm.targetEntityId;
} else {
vm.relation.from = vm.targetEntityId;
}
$scope.theForm.$setPristine(); $scope.theForm.$setPristine();
entityRelationService.saveRelation(vm.relation).then( entityRelationService.saveRelation(vm.relation).then(
function success() { function success() {

View File

@ -32,19 +32,16 @@
<div class="md-dialog-content"> <div class="md-dialog-content">
<md-content class="md-padding" layout="column"> <md-content class="md-padding" layout="column">
<fieldset ng-disabled="loading"> <fieldset ng-disabled="loading">
<md-input-container class="md-block"> <tb-relation-type-autocomplete ng-model="vm.relation.type"
<label translate>relation.relation-type</label> tb-required="true"
<md-select required ng-model="vm.relation.type" ng-disabled="loading"> ng-disabled="loading">
<md-option ng-repeat="type in vm.types.entityRelationType" ng-value="type"> </tb-relation-type-autocomplete>
<span>{{('relation.relation-types.' + type) | translate}}</span> <small>{{(vm.direction == vm.types.entitySearchDirection.from ?
</md-option> 'relation.to-entity' : 'relation.from-entity') | translate}}</small>
</md-select>
</md-input-container>
<span class="tb-small">{{'entity.entity' | translate }}</span>
<tb-entity-select flex <tb-entity-select flex
the-form="theForm" the-form="theForm"
tb-required="true" tb-required="true"
ng-model="vm.relation.to"> ng-model="vm.targetEntityId">
</tb-entity-select> </tb-entity-select>
</fieldset> </fieldset>
</md-content> </md-content>

View File

@ -45,13 +45,17 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
let vm = this; let vm = this;
vm.types = types;
vm.direction = vm.types.entitySearchDirection.from;
vm.relations = []; vm.relations = [];
vm.relationsCount = 0; vm.relationsCount = 0;
vm.allRelations = []; vm.allRelations = [];
vm.selectedRelations = []; vm.selectedRelations = [];
vm.query = { vm.query = {
order: 'typeName', order: 'type',
limit: 5, limit: 5,
page: 1, page: 1,
search: null search: null
@ -62,19 +66,23 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
vm.onReorder = onReorder; vm.onReorder = onReorder;
vm.onPaginate = onPaginate; vm.onPaginate = onPaginate;
vm.addRelation = addRelation; vm.addRelation = addRelation;
vm.editRelation = editRelation;
vm.deleteRelation = deleteRelation; vm.deleteRelation = deleteRelation;
vm.deleteRelations = deleteRelations; vm.deleteRelations = deleteRelations;
vm.reloadRelations = reloadRelations; vm.reloadRelations = reloadRelations;
vm.updateRelations = updateRelations; vm.updateRelations = updateRelations;
$scope.$watch("vm.entityId", function(newVal, prevVal) { $scope.$watch("vm.entityId", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) { if (newVal && !angular.equals(newVal, prevVal)) {
reloadRelations(); reloadRelations();
} }
}); });
$scope.$watch("vm.direction", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) {
reloadRelations();
}
});
$scope.$watch("vm.query.search", function(newVal, prevVal) { $scope.$watch("vm.query.search", function(newVal, prevVal) {
if (!angular.equals(newVal, prevVal) && vm.query.search != null) { if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
updateRelations(); updateRelations();
@ -102,7 +110,7 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
var from = { var entityId = {
id: vm.entityId, id: vm.entityId,
entityType: vm.entityType entityType: vm.entityType
}; };
@ -111,7 +119,7 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
controllerAs: 'vm', controllerAs: 'vm',
templateUrl: addRelationTemplate, templateUrl: addRelationTemplate,
parent: angular.element($document[0].body), parent: angular.element($document[0].body),
locals: { from: from }, locals: { direction: vm.direction, entityId: entityId },
fullscreen: true, fullscreen: true,
targetEvent: $event targetEvent: $event
}).then(function () { }).then(function () {
@ -120,36 +128,100 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
}); });
} }
function editRelation($event, /*relation*/) { function deleteRelation($event, relation) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
//TODO: if (relation) {
var title;
var content;
if (vm.direction == vm.types.entitySearchDirection.from) {
title = $translate.instant('relation.delete-to-relation-title', {entityName: relation.toName});
content = $translate.instant('relation.delete-to-relation-text', {entityName: relation.toName});
} else {
title = $translate.instant('relation.delete-from-relation-title', {entityName: relation.fromName});
content = $translate.instant('relation.delete-from-relation-text', {entityName: relation.fromName});
} }
function deleteRelation($event, /*relation*/) { var confirm = $mdDialog.confirm()
if ($event) { .targetEvent($event)
$event.stopPropagation(); .title(title)
.htmlContent(content)
.ariaLabel(title)
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () {
entityRelationService.deleteRelation(
relation.from.id,
relation.from.entityType,
relation.type,
relation.to.id,
relation.to.entityType).then(
function success() {
reloadRelations();
}
);
});
} }
//TODO:
} }
function deleteRelations($event) { function deleteRelations($event) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
//TODO: if (vm.selectedRelations && vm.selectedRelations.length > 0) {
var title;
var content;
if (vm.direction == vm.types.entitySearchDirection.from) {
title = $translate.instant('relation.delete-to-relations-title', {count: vm.selectedRelations.length}, 'messageformat');
content = $translate.instant('relation.delete-to-relations-text');
} else {
title = $translate.instant('relation.delete-from-relations-title', {count: vm.selectedRelations.length}, 'messageformat');
content = $translate.instant('relation.delete-from-relations-text');
}
var confirm = $mdDialog.confirm()
.targetEvent($event)
.title(title)
.htmlContent(content)
.ariaLabel(title)
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () {
var tasks = [];
for (var i=0;i<vm.selectedRelations.length;i++) {
var relation = vm.selectedRelations[i];
tasks.push( entityRelationService.deleteRelation(
relation.from.id,
relation.from.entityType,
relation.type,
relation.to.id,
relation.to.entityType));
}
$q.all(tasks).then(function () {
reloadRelations();
});
});
}
} }
function reloadRelations () { function reloadRelations () {
vm.allRelations.length = 0; vm.allRelations.length = 0;
vm.relations.length = 0; vm.relations.length = 0;
vm.relationsPromise;
if (vm.direction == vm.types.entitySearchDirection.from) {
vm.relationsPromise = entityRelationService.findInfoByFrom(vm.entityId, vm.entityType); vm.relationsPromise = entityRelationService.findInfoByFrom(vm.entityId, vm.entityType);
} else {
vm.relationsPromise = entityRelationService.findInfoByTo(vm.entityId, vm.entityType);
}
vm.relationsPromise.then( vm.relationsPromise.then(
function success(allRelations) { function success(allRelations) {
allRelations.forEach(function(relation) { allRelations.forEach(function(relation) {
relation.typeName = $translate.instant('relation.relation-type.' + relation.type); if (vm.direction == vm.types.entitySearchDirection.from) {
relation.toEntityTypeName = $translate.instant(utils.entityTypeName(relation.to.entityType)); relation.toEntityTypeName = $translate.instant(utils.entityTypeName(relation.to.entityType));
} else {
relation.fromEntityTypeName = $translate.instant(utils.entityTypeName(relation.from.entityType));
}
}); });
vm.allRelations = allRelations; vm.allRelations = allRelations;
vm.selectedRelations = []; vm.selectedRelations = [];

View File

@ -16,11 +16,22 @@
--> -->
<md-content flex class="md-padding tb-absolute-fill tb-relation-table tb-data-table" layout="column"> <md-content flex class="md-padding tb-absolute-fill tb-relation-table tb-data-table" layout="column">
<section layout="row">
<md-input-container class="md-block" style="width: 200px;">
<label translate>relation.direction</label>
<md-select ng-model="vm.direction" ng-disabled="loading">
<md-option ng-repeat="direction in vm.types.entitySearchDirection" ng-value="direction">
{{ ('relation.search-direction.' + direction) | translate}}
</md-option>
</md-select>
</md-input-container>
</section>
<div layout="column" class="md-whiteframe-z1"> <div layout="column" class="md-whiteframe-z1">
<md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedRelations.length <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedRelations.length
&& vm.query.search === null"> && vm.query.search === null">
<div class="md-toolbar-tools"> <div class="md-toolbar-tools">
<span translate>relation.entity-relations</span> <span>{{(vm.direction == vm.types.entitySearchDirection.from ?
'relation.from-relations' : 'relation.to-relations') | translate}}</span>
<span flex></span> <span flex></span>
<md-button class="md-icon-button" ng-click="vm.addRelation($event)"> <md-button class="md-icon-button" ng-click="vm.addRelation($event)">
<md-icon>add</md-icon> <md-icon>add</md-icon>
@ -66,7 +77,7 @@
<md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedRelations.length"> <md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedRelations.length">
<div class="md-toolbar-tools"> <div class="md-toolbar-tools">
<span translate <span translate
translate-values="{count: selectedRelations.length}" translate-values="{count: vm.selectedRelations.length}"
translate-interpolation="messageformat">relation.selected-relations</span> translate-interpolation="messageformat">relation.selected-relations</span>
<span flex></span> <span flex></span>
<md-button class="md-icon-button" ng-click="vm.deleteRelations($event)"> <md-button class="md-icon-button" ng-click="vm.deleteRelations($event)">
@ -81,25 +92,26 @@
<table md-table md-row-select multiple="" ng-model="vm.selectedRelations" md-progress="vm.relationsDeferred.promise"> <table md-table md-row-select multiple="" ng-model="vm.selectedRelations" md-progress="vm.relationsDeferred.promise">
<thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder"> <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
<tr md-row> <tr md-row>
<th md-column md-order-by="typeName"><span translate>relation.type</span></th> <th md-column md-order-by="type"><span translate>relation.type</span></th>
<th md-column md-order-by="toEntityTypeName"><span translate>relation.to-entity-type</span></th> <th md-column ng-if="vm.direction == vm.types.entitySearchDirection.from"
<th md-column md-order-by="toName"><span translate>relation.to-entity-name</span></th> md-order-by="toEntityTypeName"><span translate>relation.to-entity-type</span></th>
<th md-column ng-if="vm.direction == vm.types.entitySearchDirection.to"
md-order-by="fromEntityTypeName"><span translate>relation.from-entity-type</span></th>
<th md-column ng-if="vm.direction == vm.types.entitySearchDirection.from"
md-order-by="toName"><span translate>relation.to-entity-name</span></th>
<th md-column ng-if="vm.direction == vm.types.entitySearchDirection.to"
md-order-by="fromName"><span translate>relation.from-entity-name</span></th>
<th md-column><span>&nbsp</span></th> <th md-column><span>&nbsp</span></th>
</tr> </tr>
</thead> </thead>
<tbody md-body> <tbody md-body>
<tr md-row md-select="relation" md-select-id="relation" md-auto-select ng-repeat="relation in vm.relations"> <tr md-row md-select="relation" md-select-id="relation" md-auto-select ng-repeat="relation in vm.relations">
<td md-cell>{{ relation.typeName }}</td> <td md-cell>{{ relation.type }}</td>
<td md-cell>{{ relation.toEntityTypeName }}</td> <td md-cell ng-if="vm.direction == vm.types.entitySearchDirection.from">{{ relation.toEntityTypeName }}</td>
<td md-cell>{{ relation.toName }}</td> <td md-cell ng-if="vm.direction == vm.types.entitySearchDirection.to">{{ relation.fromEntityTypeName }}</td>
<td md-cell ng-if="vm.direction == vm.types.entitySearchDirection.from">{{ relation.toName }}</td>
<td md-cell ng-if="vm.direction == vm.types.entitySearchDirection.to">{{ relation.fromName }}</td>
<td md-cell class="tb-action-cell"> <td md-cell class="tb-action-cell">
<md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}"
ng-click="vm.editRelation($event, relation)">
<md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
<md-tooltip md-direction="top">
{{ 'relation.edit' | translate }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button" aria-label="{{ 'action.delete' | translate }}" ng-click="vm.deleteRelation($event, relation)"> <md-button class="md-icon-button" aria-label="{{ 'action.delete' | translate }}" ng-click="vm.deleteRelation($event, relation)">
<md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">delete</md-icon> <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">delete</md-icon>
<md-tooltip md-direction="top"> <md-tooltip md-direction="top">

View File

@ -0,0 +1,86 @@
/*
* 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-type-autocomplete.scss';
/* eslint-disable import/no-unresolved, import/default */
import relationTypeAutocompleteTemplate from './relation-type-autocomplete.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function RelationTypeAutocomplete($compile, $templateCache, $q, $filter, assetService, deviceService, types) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(relationTypeAutocompleteTemplate);
element.html(template);
scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
scope.relationType = null;
scope.relationTypeSearchText = '';
scope.relationTypes = [];
for (var type in types.entityRelationType) {
scope.relationTypes.push(types.entityRelationType[type]);
}
scope.fetchRelationTypes = function(searchText) {
var deferred = $q.defer();
var result = $filter('filter')(scope.relationTypes, {'$': searchText});
if (result && result.length) {
deferred.resolve(result);
} else {
deferred.resolve([searchText]);
}
return deferred.promise;
}
scope.relationTypeSearchTextChanged = function() {
}
scope.updateView = function () {
if (!scope.disabled) {
ngModelCtrl.$setViewValue(scope.relationType);
}
}
ngModelCtrl.$render = function () {
scope.relationType = ngModelCtrl.$viewValue;
}
scope.$watch('relationType', function (newValue, prevValue) {
if (!angular.equals(newValue, prevValue)) {
scope.updateView();
}
});
scope.$watch('disabled', function () {
scope.updateView();
});
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
link: linker,
scope: {
theForm: '=?',
tbRequired: '=?',
disabled:'=ngDisabled'
}
};
}

View File

@ -0,0 +1,25 @@
/**
* 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-type-autocomplete {
.tb-relation-type-item {
display: block;
height: 48px;
}
li {
height: auto !important;
white-space: normal !important;
}
}

View File

@ -0,0 +1,40 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-autocomplete ng-required="tbRequired"
ng-disabled="disabled"
md-no-cache="true"
md-input-name="relationType"
ng-model="relationType"
md-selected-item="relationType"
md-search-text="relationTypeSearchText"
md-search-text-change="relationTypeSearchTextChanged()"
md-items="item in fetchRelationTypes(relationTypeSearchText)"
md-item-text="item"
md-min-length="0"
md-floating-label="{{ 'relation.relation-type' | translate }}"
md-select-on-match="true"
md-menu-class="tb-relation-type-autocomplete">
<md-item-template>
<div class="tb-relation-type-item">
<span md-highlight-text="relationTypeSearchText" md-highlight-flags="^i">{{item}}</span>
</div>
</md-item-template>
<div ng-messages="theForm.relationType.$error">
<div translate ng-message="required">relation.relation-type-required</div>
</div>
</md-autocomplete>

View File

@ -544,6 +544,7 @@ export default angular.module('thingsboard.locale', [])
"entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.", "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.",
"all-subtypes": "All", "all-subtypes": "All",
"type": "Type", "type": "Type",
"type-required": "Entity type is required.",
"type-device": "Device", "type-device": "Device",
"type-asset": "Asset", "type-asset": "Asset",
"type-rule": "Rule", "type-rule": "Rule",
@ -718,19 +719,33 @@ export default angular.module('thingsboard.locale', [])
}, },
"relation": { "relation": {
"relations": "Relations", "relations": "Relations",
"entity-relations": "Entity relations", "direction": "Direction",
"search-direction": {
"FROM": "From",
"TO": "To"
},
"from-relations": "Outbound relations",
"to-relations": "Inbound relations",
"selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected", "selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected",
"type": "Type", "type": "Type",
"to-entity-type": "Entity type", "to-entity-type": "To entity type",
"to-entity-name": "Entity name", "to-entity-name": "To entity name",
"edit": "Edit relation", "from-entity-type": "From entity type",
"from-entity-name": "From entity name",
"to-entity": "To entity",
"from-entity": "From entity",
"delete": "Delete relation", "delete": "Delete relation",
"relation-type": "Relation type", "relation-type": "Relation type",
"relation-types": { "relation-type-required": "Relation type is required.",
"Contains": "Contains", "add": "Add relation",
"Manages": "Manages" "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.",
"add": "Add relation" "delete-to-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?",
"delete-to-relations-text": "Be careful, after the confirmation all selected relations will be removed and corresponding entities will be unrelated from the current entity.",
"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."
}, },
"rule": { "rule": {
"rule": "Rule", "rule": "Rule",

View File

@ -56,5 +56,11 @@
disabled-event-types="{{vm.types.eventType.alarm.value}}"> disabled-event-types="{{vm.types.eventType.alarm.value}}">
</tb-event-table> </tb-event-table>
</md-tab> </md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
<tb-relation-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.plugin}}">
</tb-relation-table>
</md-tab>
</md-tabs> </md-tabs>
</tb-grid> </tb-grid>

View File

@ -56,5 +56,11 @@
disabled-event-types="{{vm.types.eventType.alarm.value}}"> disabled-event-types="{{vm.types.eventType.alarm.value}}">
</tb-event-table> </tb-event-table>
</md-tab> </md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
<tb-relation-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.rule}}">
</tb-relation-table>
</md-tab>
</md-tabs> </md-tabs>
</tb-grid> </tb-grid>

View File

@ -53,5 +53,11 @@
default-event-type="{{vm.types.eventType.alarm.value}}"> default-event-type="{{vm.types.eventType.alarm.value}}">
</tb-event-table> </tb-event-table>
</md-tab> </md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
<tb-relation-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-type="{{vm.types.entityType.tenant}}">
</tb-relation-table>
</md-tab>
</md-tabs> </md-tabs>
</tb-grid> </tb-grid>

View File

@ -436,6 +436,7 @@ md-tabs.tb-headless {
***********************/ ***********************/
section.tb-header-buttons { section.tb-header-buttons {
pointer-events: none;
position: absolute; position: absolute;
right: 0px; right: 0px;
top: 86px; top: 86px;