thingsboard-memoizeit
Changes
application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java 46(+33 -13)
common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationInfo.java 9(+9 -0)
ui/src/app/api/entity-relation.service.js 13(+13 -0)
ui/src/app/device/devices.tpl.html 6(+6 -0)
ui/src/app/entity/index.js 2(+2 -0)
ui/src/app/locale/locale.constant.js 33(+24 -9)
ui/src/app/plugin/plugins.tpl.html 6(+6 -0)
ui/src/app/rule/rules.tpl.html 6(+6 -0)
ui/src/app/tenant/tenants.tpl.html 6(+6 -0)
ui/src/scss/main.scss 1(+1 -0)
Details
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
index 1a08d56..3ddc597 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
@@ -34,7 +34,7 @@ import java.util.List;
@RequestMapping("/api")
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)
@ResponseStatus(value = HttpStatus.OK)
public void saveRelation(@RequestBody EntityRelation relation) throws ThingsboardException {
@@ -42,31 +42,33 @@ public class EntityRelationController extends BaseController {
checkNotNull(relation);
checkEntityId(relation.getFrom());
checkEntityId(relation.getTo());
+ if (relation.getTypeGroup() == null) {
+ relation.setTypeGroup(RelationTypeGroup.COMMON);
+ }
relationService.saveRelation(relation).get();
} catch (Exception 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"})
@ResponseStatus(value = HttpStatus.OK)
public void deleteRelation(@RequestParam("fromId") String strFromId,
@RequestParam("fromType") String strFromType,
@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 {
checkParameter("fromId", strFromId);
checkParameter("fromType", strFromType);
checkParameter("relationType", strRelationType);
- checkParameter("relationTypeGroup", strRelationTypeGroup);
checkParameter("toId", strToId);
checkParameter("toType", strToType);
EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
EntityId toId = EntityIdFactory.getByTypeAndId(strToType, strToId);
checkEntityId(fromId);
checkEntityId(toId);
- RelationTypeGroup relationTypeGroup = RelationTypeGroup.valueOf(strRelationTypeGroup);
+ RelationTypeGroup relationTypeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
try {
Boolean found = relationService.deleteRelation(fromId, toId, strRelationType, relationTypeGroup).get();
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"})
@ResponseStatus(value = HttpStatus.OK)
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"})
@ResponseStatus(value = HttpStatus.OK)
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"})
@ResponseBody
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"})
@ResponseBody
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"})
@ResponseBody
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"})
@ResponseBody
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"})
@ResponseBody
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)
@ResponseBody
public List<EntityRelation> findByQuery(@RequestBody EntityRelationsQuery query) throws ThingsboardException {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationInfo.java
index 709ad79..5012691 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationInfo.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationInfo.java
@@ -20,6 +20,7 @@ public class EntityRelationInfo extends EntityRelation {
private static final long serialVersionUID = 2807343097519543363L;
+ private String fromName;
private String toName;
public EntityRelationInfo() {
@@ -30,6 +31,14 @@ public class EntityRelationInfo extends EntityRelation {
super(entityRelation);
}
+ public String getFromName() {
+ return fromName;
+ }
+
+ public void setFromName(String fromName) {
+ this.fromName = fromName;
+ }
+
public String getToName() {
return toName;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
index 9559cd3..36ec567 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
@@ -23,29 +23,17 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
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.UUIDBased;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo;
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.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 java.util.*;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiConsumer;
/**
* Created by ashvayka on 28.04.17.
@@ -133,23 +121,16 @@ public class BaseRelationService implements RelationService {
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)));
- return Futures.successfulAsList(futures);
+ relations1.stream().forEach(relation ->
+ futures.add(fetchRelationInfoAsync(relation,
+ relation2 -> relation2.getTo(),
+ (EntityRelationInfo relationInfo, String entityName) -> relationInfo.setToName(entityName)))
+ );
+ return Futures.successfulAsList(futures);
});
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
public ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup) {
log.trace("Executing findByFromAndType [{}][{}][{}]", from, relationType, typeGroup);
@@ -168,6 +149,38 @@ public class BaseRelationService implements RelationService {
}
@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
public ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup) {
log.trace("Executing findByToAndType [{}][{}][{}]", to, relationType, typeGroup);
validate(to);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
index 868769f..a810454 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
@@ -46,6 +46,8 @@ public interface RelationService {
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>> findByQuery(EntityRelationsQuery query);
ui/src/app/api/entity-relation.service.js 13(+13 -0)
diff --git a/ui/src/app/api/entity-relation.service.js b/ui/src/app/api/entity-relation.service.js
index 7039645..875b2fa 100644
--- a/ui/src/app/api/entity-relation.service.js
+++ b/ui/src/app/api/entity-relation.service.js
@@ -28,6 +28,7 @@ function EntityRelationService($http, $q) {
findInfoByFrom: findInfoByFrom,
findByFromAndType: findByFromAndType,
findByTo: findByTo,
+ findInfoByTo: findInfoByTo,
findByToAndType: findByToAndType,
findByQuery: findByQuery
}
@@ -122,6 +123,18 @@ function EntityRelationService($http, $q) {
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) {
var deferred = $q.defer();
var url = '/api/relations?toId=' + toId;
diff --git a/ui/src/app/customer/customers.tpl.html b/ui/src/app/customer/customers.tpl.html
index 9221984..6c70a70 100644
--- a/ui/src/app/customer/customers.tpl.html
+++ b/ui/src/app/customer/customers.tpl.html
@@ -55,5 +55,11 @@
default-event-type="{{vm.types.eventType.alarm.value}}">
</tb-event-table>
</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>
</tb-grid>
ui/src/app/device/devices.tpl.html 6(+6 -0)
diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html
index 2f90db8..3ff6c1c 100644
--- a/ui/src/app/device/devices.tpl.html
+++ b/ui/src/app/device/devices.tpl.html
@@ -56,4 +56,10 @@
default-event-type="{{vm.types.eventType.alarm.value}}">
</tb-event-table>
</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>
diff --git a/ui/src/app/entity/entity-filter.tpl.html b/ui/src/app/entity/entity-filter.tpl.html
index cbb997e..7e49ba9 100644
--- a/ui/src/app/entity/entity-filter.tpl.html
+++ b/ui/src/app/entity/entity-filter.tpl.html
@@ -41,7 +41,7 @@
</md-autocomplete>
<md-chip-template>
<span>
- <strong>{{itemName($chip)}}</strong>
+ <strong>{{$chip.name}}</strong>
</span>
</md-chip-template>
</md-chips>
diff --git a/ui/src/app/entity/entity-select.tpl.html b/ui/src/app/entity/entity-select.tpl.html
index ce0a16b..13e17e7 100644
--- a/ui/src/app/entity/entity-select.tpl.html
+++ b/ui/src/app/entity/entity-select.tpl.html
@@ -17,6 +17,9 @@
-->
<div layout='row' class="tb-entity-select">
<tb-entity-type-select style="min-width: 100px;"
+ the-form="theForm"
+ ng-disabled="disabled"
+ tb-required="tbRequired"
ng-model="model.entityType">
</tb-entity-type-select>
<tb-entity-autocomplete flex ng-if="model.entityType"
diff --git a/ui/src/app/entity/entity-type-select.directive.js b/ui/src/app/entity/entity-type-select.directive.js
index 3a595a3..1dc6741 100644
--- a/ui/src/app/entity/entity-type-select.directive.js
+++ b/ui/src/app/entity/entity-type-select.directive.js
@@ -29,6 +29,8 @@ export default function EntityTypeSelect($compile, $templateCache, utils, userSe
var template = $templateCache.get(entityTypeSelectTemplate);
element.html(template);
+ scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+
if (angular.isDefined(attrs.hideLabel)) {
scope.showLabel = false;
} else {
@@ -103,6 +105,9 @@ export default function EntityTypeSelect($compile, $templateCache, utils, userSe
require: "^ngModel",
link: linker,
scope: {
+ theForm: '=?',
+ tbRequired: '=?',
+ disabled:'=ngDisabled',
allowedEntityTypes: "=?"
}
};
diff --git a/ui/src/app/entity/entity-type-select.tpl.html b/ui/src/app/entity/entity-type-select.tpl.html
index e31cbf5..86d0eeb 100644
--- a/ui/src/app/entity/entity-type-select.tpl.html
+++ b/ui/src/app/entity/entity-type-select.tpl.html
@@ -17,9 +17,13 @@
-->
<md-input-container>
<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">
{{typeName(type) | translate}}
</md-option>
</md-select>
-</md-input-container>
\ No newline at end of file
+ <div ng-messages="theForm.entityType.$error">
+ <div ng-message="required" translate>entity.type-required</div>
+ </div>
+</md-input-container>
ui/src/app/entity/index.js 2(+2 -0)
diff --git a/ui/src/app/entity/index.js b/ui/src/app/entity/index.js
index bed9562..e8cc437 100644
--- a/ui/src/app/entity/index.js
+++ b/ui/src/app/entity/index.js
@@ -27,6 +27,7 @@ import AddAttributeDialogController from './attribute/add-attribute-dialog.contr
import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller';
import AttributeTableDirective from './attribute/attribute-table.directive';
import RelationTableDirective from './relation/relation-table.directive';
+import RelationTypeAutocompleteDirective from './relation/relation-type-autocomplete.directive';
export default angular.module('thingsboard.entity', [])
.controller('EntityAliasesController', EntityAliasesController)
@@ -42,4 +43,5 @@ export default angular.module('thingsboard.entity', [])
.directive('tbAliasesEntitySelect', AliasesEntitySelectDirective)
.directive('tbAttributeTable', AttributeTableDirective)
.directive('tbRelationTable', RelationTableDirective)
+ .directive('tbRelationTypeAutocomplete', RelationTypeAutocompleteDirective)
.name;
diff --git a/ui/src/app/entity/relation/add-relation-dialog.controller.js b/ui/src/app/entity/relation/add-relation-dialog.controller.js
index c331946..e05be52 100644
--- a/ui/src/app/entity/relation/add-relation-dialog.controller.js
+++ b/ui/src/app/entity/relation/add-relation-dialog.controller.js
@@ -14,14 +14,20 @@
* limitations under the License.
*/
/*@ngInject*/
-export default function AddRelationDialogController($scope, $mdDialog, types, entityRelationService, from) {
+export default function AddRelationDialogController($scope, $mdDialog, types, entityRelationService, direction, entityId) {
var vm = this;
vm.types = types;
+ vm.direction = direction;
+ vm.targetEntityId = {};
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.add = add;
@@ -32,6 +38,11 @@ export default function AddRelationDialogController($scope, $mdDialog, types, en
}
function add() {
+ if (vm.direction == vm.types.entitySearchDirection.from) {
+ vm.relation.to = vm.targetEntityId;
+ } else {
+ vm.relation.from = vm.targetEntityId;
+ }
$scope.theForm.$setPristine();
entityRelationService.saveRelation(vm.relation).then(
function success() {
diff --git a/ui/src/app/entity/relation/add-relation-dialog.tpl.html b/ui/src/app/entity/relation/add-relation-dialog.tpl.html
index b32d23a..b45013b 100644
--- a/ui/src/app/entity/relation/add-relation-dialog.tpl.html
+++ b/ui/src/app/entity/relation/add-relation-dialog.tpl.html
@@ -32,19 +32,16 @@
<div class="md-dialog-content">
<md-content class="md-padding" layout="column">
<fieldset ng-disabled="loading">
- <md-input-container class="md-block">
- <label translate>relation.relation-type</label>
- <md-select required ng-model="vm.relation.type" ng-disabled="loading">
- <md-option ng-repeat="type in vm.types.entityRelationType" ng-value="type">
- <span>{{('relation.relation-types.' + type) | translate}}</span>
- </md-option>
- </md-select>
- </md-input-container>
- <span class="tb-small">{{'entity.entity' | translate }}</span>
+ <tb-relation-type-autocomplete ng-model="vm.relation.type"
+ tb-required="true"
+ ng-disabled="loading">
+ </tb-relation-type-autocomplete>
+ <small>{{(vm.direction == vm.types.entitySearchDirection.from ?
+ 'relation.to-entity' : 'relation.from-entity') | translate}}</small>
<tb-entity-select flex
the-form="theForm"
tb-required="true"
- ng-model="vm.relation.to">
+ ng-model="vm.targetEntityId">
</tb-entity-select>
</fieldset>
</md-content>
diff --git a/ui/src/app/entity/relation/relation-table.directive.js b/ui/src/app/entity/relation/relation-table.directive.js
index bf4aa52..729ff36 100644
--- a/ui/src/app/entity/relation/relation-table.directive.js
+++ b/ui/src/app/entity/relation/relation-table.directive.js
@@ -45,13 +45,17 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
let vm = this;
+ vm.types = types;
+
+ vm.direction = vm.types.entitySearchDirection.from;
+
vm.relations = [];
vm.relationsCount = 0;
vm.allRelations = [];
vm.selectedRelations = [];
vm.query = {
- order: 'typeName',
+ order: 'type',
limit: 5,
page: 1,
search: null
@@ -62,19 +66,23 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
vm.onReorder = onReorder;
vm.onPaginate = onPaginate;
vm.addRelation = addRelation;
- vm.editRelation = editRelation;
vm.deleteRelation = deleteRelation;
vm.deleteRelations = deleteRelations;
vm.reloadRelations = reloadRelations;
vm.updateRelations = updateRelations;
-
$scope.$watch("vm.entityId", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) {
reloadRelations();
}
});
+ $scope.$watch("vm.direction", function(newVal, prevVal) {
+ if (newVal && !angular.equals(newVal, prevVal)) {
+ reloadRelations();
+ }
+ });
+
$scope.$watch("vm.query.search", function(newVal, prevVal) {
if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
updateRelations();
@@ -102,7 +110,7 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
if ($event) {
$event.stopPropagation();
}
- var from = {
+ var entityId = {
id: vm.entityId,
entityType: vm.entityType
};
@@ -111,7 +119,7 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
controllerAs: 'vm',
templateUrl: addRelationTemplate,
parent: angular.element($document[0].body),
- locals: { from: from },
+ locals: { direction: vm.direction, entityId: entityId },
fullscreen: true,
targetEvent: $event
}).then(function () {
@@ -120,36 +128,100 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
});
}
- function editRelation($event, /*relation*/) {
+ function deleteRelation($event, relation) {
if ($event) {
$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*/) {
- if ($event) {
- $event.stopPropagation();
+ 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 () {
+ entityRelationService.deleteRelation(
+ relation.from.id,
+ relation.from.entityType,
+ relation.type,
+ relation.to.id,
+ relation.to.entityType).then(
+ function success() {
+ reloadRelations();
+ }
+ );
+ });
}
- //TODO:
}
function deleteRelations($event) {
if ($event) {
$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 () {
vm.allRelations.length = 0;
vm.relations.length = 0;
- vm.relationsPromise = entityRelationService.findInfoByFrom(vm.entityId, vm.entityType);
+ vm.relationsPromise;
+ if (vm.direction == vm.types.entitySearchDirection.from) {
+ vm.relationsPromise = entityRelationService.findInfoByFrom(vm.entityId, vm.entityType);
+ } else {
+ vm.relationsPromise = entityRelationService.findInfoByTo(vm.entityId, vm.entityType);
+ }
vm.relationsPromise.then(
function success(allRelations) {
allRelations.forEach(function(relation) {
- relation.typeName = $translate.instant('relation.relation-type.' + relation.type);
- relation.toEntityTypeName = $translate.instant(utils.entityTypeName(relation.to.entityType));
+ if (vm.direction == vm.types.entitySearchDirection.from) {
+ relation.toEntityTypeName = $translate.instant(utils.entityTypeName(relation.to.entityType));
+ } else {
+ relation.fromEntityTypeName = $translate.instant(utils.entityTypeName(relation.from.entityType));
+ }
});
vm.allRelations = allRelations;
vm.selectedRelations = [];
diff --git a/ui/src/app/entity/relation/relation-table.tpl.html b/ui/src/app/entity/relation/relation-table.tpl.html
index dc0df57..c93755b 100644
--- a/ui/src/app/entity/relation/relation-table.tpl.html
+++ b/ui/src/app/entity/relation/relation-table.tpl.html
@@ -16,11 +16,22 @@
-->
<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">
<md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedRelations.length
&& vm.query.search === null">
<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>
<md-button class="md-icon-button" ng-click="vm.addRelation($event)">
<md-icon>add</md-icon>
@@ -66,7 +77,7 @@
<md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedRelations.length">
<div class="md-toolbar-tools">
<span translate
- translate-values="{count: selectedRelations.length}"
+ translate-values="{count: vm.selectedRelations.length}"
translate-interpolation="messageformat">relation.selected-relations</span>
<span flex></span>
<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">
<thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
<tr md-row>
- <th md-column md-order-by="typeName"><span translate>relation.type</span></th>
- <th md-column md-order-by="toEntityTypeName"><span translate>relation.to-entity-type</span></th>
- <th md-column md-order-by="toName"><span translate>relation.to-entity-name</span></th>
+ <th md-column md-order-by="type"><span translate>relation.type</span></th>
+ <th md-column ng-if="vm.direction == vm.types.entitySearchDirection.from"
+ 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> </span></th>
</tr>
</thead>
<tbody md-body>
<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.toEntityTypeName }}</td>
- <td md-cell>{{ relation.toName }}</td>
+ <td md-cell>{{ relation.type }}</td>
+ <td md-cell ng-if="vm.direction == vm.types.entitySearchDirection.from">{{ relation.toEntityTypeName }}</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">
- <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-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">delete</md-icon>
<md-tooltip md-direction="top">
diff --git a/ui/src/app/entity/relation/relation-type-autocomplete.directive.js b/ui/src/app/entity/relation/relation-type-autocomplete.directive.js
new file mode 100644
index 0000000..a7e5ea4
--- /dev/null
+++ b/ui/src/app/entity/relation/relation-type-autocomplete.directive.js
@@ -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'
+ }
+ };
+}
diff --git a/ui/src/app/entity/relation/relation-type-autocomplete.scss b/ui/src/app/entity/relation/relation-type-autocomplete.scss
new file mode 100644
index 0000000..9fd0fe0
--- /dev/null
+++ b/ui/src/app/entity/relation/relation-type-autocomplete.scss
@@ -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;
+ }
+}
diff --git a/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html b/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html
new file mode 100644
index 0000000..f71c134
--- /dev/null
+++ b/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html
@@ -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>
ui/src/app/locale/locale.constant.js 33(+24 -9)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index d25db5d..fcde13c 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -544,6 +544,7 @@ export default angular.module('thingsboard.locale', [])
"entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.",
"all-subtypes": "All",
"type": "Type",
+ "type-required": "Entity type is required.",
"type-device": "Device",
"type-asset": "Asset",
"type-rule": "Rule",
@@ -718,19 +719,33 @@ export default angular.module('thingsboard.locale', [])
},
"relation": {
"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",
"type": "Type",
- "to-entity-type": "Entity type",
- "to-entity-name": "Entity name",
- "edit": "Edit relation",
+ "to-entity-type": "To entity type",
+ "to-entity-name": "To entity name",
+ "from-entity-type": "From entity type",
+ "from-entity-name": "From entity name",
+ "to-entity": "To entity",
+ "from-entity": "From entity",
"delete": "Delete relation",
"relation-type": "Relation type",
- "relation-types": {
- "Contains": "Contains",
- "Manages": "Manages"
- },
- "add": "Add relation"
+ "relation-type-required": "Relation type is required.",
+ "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.",
+ "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",
ui/src/app/plugin/plugins.tpl.html 6(+6 -0)
diff --git a/ui/src/app/plugin/plugins.tpl.html b/ui/src/app/plugin/plugins.tpl.html
index bd8cf74..d04ebcb 100644
--- a/ui/src/app/plugin/plugins.tpl.html
+++ b/ui/src/app/plugin/plugins.tpl.html
@@ -56,5 +56,11 @@
disabled-event-types="{{vm.types.eventType.alarm.value}}">
</tb-event-table>
</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>
</tb-grid>
ui/src/app/rule/rules.tpl.html 6(+6 -0)
diff --git a/ui/src/app/rule/rules.tpl.html b/ui/src/app/rule/rules.tpl.html
index bd7ea48..16097cc 100644
--- a/ui/src/app/rule/rules.tpl.html
+++ b/ui/src/app/rule/rules.tpl.html
@@ -56,5 +56,11 @@
disabled-event-types="{{vm.types.eventType.alarm.value}}">
</tb-event-table>
</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>
</tb-grid>
ui/src/app/tenant/tenants.tpl.html 6(+6 -0)
diff --git a/ui/src/app/tenant/tenants.tpl.html b/ui/src/app/tenant/tenants.tpl.html
index ada2a3f..00350a4 100644
--- a/ui/src/app/tenant/tenants.tpl.html
+++ b/ui/src/app/tenant/tenants.tpl.html
@@ -53,5 +53,11 @@
default-event-type="{{vm.types.eventType.alarm.value}}">
</tb-event-table>
</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>
</tb-grid>
ui/src/scss/main.scss 1(+1 -0)
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index c062d4e..dbe3543 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -436,6 +436,7 @@ md-tabs.tb-headless {
***********************/
section.tb-header-buttons {
+ pointer-events: none;
position: absolute;
right: 0px;
top: 86px;