Commit ecb5c50d61add5a92c33ee91afef5244109e8480

Authored by Igor Kulikov
1 parent 3c43d039

UI: Implement Entity relations management.

@@ -34,7 +34,7 @@ import java.util.List; @@ -34,7 +34,7 @@ import java.util.List;
34 @RequestMapping("/api") 34 @RequestMapping("/api")
35 public class EntityRelationController extends BaseController { 35 public class EntityRelationController extends BaseController {
36 36
37 - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 37 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
38 @RequestMapping(value = "/relation", method = RequestMethod.POST) 38 @RequestMapping(value = "/relation", method = RequestMethod.POST)
39 @ResponseStatus(value = HttpStatus.OK) 39 @ResponseStatus(value = HttpStatus.OK)
40 public void saveRelation(@RequestBody EntityRelation relation) throws ThingsboardException { 40 public void saveRelation(@RequestBody EntityRelation relation) throws ThingsboardException {
@@ -42,31 +42,33 @@ public class EntityRelationController extends BaseController { @@ -42,31 +42,33 @@ public class EntityRelationController extends BaseController {
42 checkNotNull(relation); 42 checkNotNull(relation);
43 checkEntityId(relation.getFrom()); 43 checkEntityId(relation.getFrom());
44 checkEntityId(relation.getTo()); 44 checkEntityId(relation.getTo());
  45 + if (relation.getTypeGroup() == null) {
  46 + relation.setTypeGroup(RelationTypeGroup.COMMON);
  47 + }
45 relationService.saveRelation(relation).get(); 48 relationService.saveRelation(relation).get();
46 } catch (Exception e) { 49 } catch (Exception e) {
47 throw handleException(e); 50 throw handleException(e);
48 } 51 }
49 } 52 }
50 53
51 - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 54 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
52 @RequestMapping(value = "/relation", method = RequestMethod.DELETE, params = {"fromId", "fromType", "relationType", "toId", "toType"}) 55 @RequestMapping(value = "/relation", method = RequestMethod.DELETE, params = {"fromId", "fromType", "relationType", "toId", "toType"})
53 @ResponseStatus(value = HttpStatus.OK) 56 @ResponseStatus(value = HttpStatus.OK)
54 public void deleteRelation(@RequestParam("fromId") String strFromId, 57 public void deleteRelation(@RequestParam("fromId") String strFromId,
55 @RequestParam("fromType") String strFromType, 58 @RequestParam("fromType") String strFromType,
56 @RequestParam("relationType") String strRelationType, 59 @RequestParam("relationType") String strRelationType,
57 - @RequestParam("relationTypeGroup") String strRelationTypeGroup, 60 + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup,
58 @RequestParam("toId") String strToId, @RequestParam("toType") String strToType) throws ThingsboardException { 61 @RequestParam("toId") String strToId, @RequestParam("toType") String strToType) throws ThingsboardException {
59 checkParameter("fromId", strFromId); 62 checkParameter("fromId", strFromId);
60 checkParameter("fromType", strFromType); 63 checkParameter("fromType", strFromType);
61 checkParameter("relationType", strRelationType); 64 checkParameter("relationType", strRelationType);
62 - checkParameter("relationTypeGroup", strRelationTypeGroup);  
63 checkParameter("toId", strToId); 65 checkParameter("toId", strToId);
64 checkParameter("toType", strToType); 66 checkParameter("toType", strToType);
65 EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId); 67 EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
66 EntityId toId = EntityIdFactory.getByTypeAndId(strToType, strToId); 68 EntityId toId = EntityIdFactory.getByTypeAndId(strToType, strToId);
67 checkEntityId(fromId); 69 checkEntityId(fromId);
68 checkEntityId(toId); 70 checkEntityId(toId);
69 - RelationTypeGroup relationTypeGroup = RelationTypeGroup.valueOf(strRelationTypeGroup); 71 + RelationTypeGroup relationTypeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
70 try { 72 try {
71 Boolean found = relationService.deleteRelation(fromId, toId, strRelationType, relationTypeGroup).get(); 73 Boolean found = relationService.deleteRelation(fromId, toId, strRelationType, relationTypeGroup).get();
72 if (!found) { 74 if (!found) {
@@ -77,7 +79,7 @@ public class EntityRelationController extends BaseController { @@ -77,7 +79,7 @@ public class EntityRelationController extends BaseController {
77 } 79 }
78 } 80 }
79 81
80 - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 82 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')")
81 @RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"id", "type"}) 83 @RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"id", "type"})
82 @ResponseStatus(value = HttpStatus.OK) 84 @ResponseStatus(value = HttpStatus.OK)
83 public void deleteRelations(@RequestParam("entityId") String strId, 85 public void deleteRelations(@RequestParam("entityId") String strId,
@@ -93,7 +95,7 @@ public class EntityRelationController extends BaseController { @@ -93,7 +95,7 @@ public class EntityRelationController extends BaseController {
93 } 95 }
94 } 96 }
95 97
96 - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 98 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
97 @RequestMapping(value = "/relation", method = RequestMethod.GET, params = {"fromId", "fromType", "relationType", "toId", "toType"}) 99 @RequestMapping(value = "/relation", method = RequestMethod.GET, params = {"fromId", "fromType", "relationType", "toId", "toType"})
98 @ResponseStatus(value = HttpStatus.OK) 100 @ResponseStatus(value = HttpStatus.OK)
99 public void checkRelation(@RequestParam("fromId") String strFromId, 101 public void checkRelation(@RequestParam("fromId") String strFromId,
@@ -121,7 +123,7 @@ public class EntityRelationController extends BaseController { @@ -121,7 +123,7 @@ public class EntityRelationController extends BaseController {
121 } 123 }
122 } 124 }
123 125
124 - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 126 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
125 @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"fromId", "fromType"}) 127 @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"fromId", "fromType"})
126 @ResponseBody 128 @ResponseBody
127 public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId, 129 public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId,
@@ -139,7 +141,7 @@ public class EntityRelationController extends BaseController { @@ -139,7 +141,7 @@ public class EntityRelationController extends BaseController {
139 } 141 }
140 } 142 }
141 143
142 - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 144 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
143 @RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {"fromId", "fromType"}) 145 @RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {"fromId", "fromType"})
144 @ResponseBody 146 @ResponseBody
145 public List<EntityRelationInfo> findInfoByFrom(@RequestParam("fromId") String strFromId, 147 public List<EntityRelationInfo> findInfoByFrom(@RequestParam("fromId") String strFromId,
@@ -157,7 +159,7 @@ public class EntityRelationController extends BaseController { @@ -157,7 +159,7 @@ public class EntityRelationController extends BaseController {
157 } 159 }
158 } 160 }
159 161
160 - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 162 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
161 @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"fromId", "fromType", "relationType"}) 163 @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"fromId", "fromType", "relationType"})
162 @ResponseBody 164 @ResponseBody
163 public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId, 165 public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId,
@@ -177,7 +179,7 @@ public class EntityRelationController extends BaseController { @@ -177,7 +179,7 @@ public class EntityRelationController extends BaseController {
177 } 179 }
178 } 180 }
179 181
180 - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 182 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
181 @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"toId", "toType"}) 183 @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"toId", "toType"})
182 @ResponseBody 184 @ResponseBody
183 public List<EntityRelation> findByTo(@RequestParam("toId") String strToId, 185 public List<EntityRelation> findByTo(@RequestParam("toId") String strToId,
@@ -195,7 +197,25 @@ public class EntityRelationController extends BaseController { @@ -195,7 +197,25 @@ public class EntityRelationController extends BaseController {
195 } 197 }
196 } 198 }
197 199
198 - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 200 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
  201 + @RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {"toId", "toType"})
  202 + @ResponseBody
  203 + public List<EntityRelationInfo> findInfoByTo(@RequestParam("toId") String strToId,
  204 + @RequestParam("toType") String strToType,
  205 + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
  206 + checkParameter("toId", strToId);
  207 + checkParameter("toType", strToType);
  208 + EntityId entityId = EntityIdFactory.getByTypeAndId(strToType, strToId);
  209 + checkEntityId(entityId);
  210 + RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
  211 + try {
  212 + return checkNotNull(relationService.findInfoByTo(entityId, typeGroup).get());
  213 + } catch (Exception e) {
  214 + throw handleException(e);
  215 + }
  216 + }
  217 +
  218 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
199 @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"toId", "toType", "relationType"}) 219 @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"toId", "toType", "relationType"})
200 @ResponseBody 220 @ResponseBody
201 public List<EntityRelation> findByTo(@RequestParam("toId") String strToId, 221 public List<EntityRelation> findByTo(@RequestParam("toId") String strToId,
@@ -215,7 +235,7 @@ public class EntityRelationController extends BaseController { @@ -215,7 +235,7 @@ public class EntityRelationController extends BaseController {
215 } 235 }
216 } 236 }
217 237
218 - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 238 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
219 @RequestMapping(value = "/relations", method = RequestMethod.POST) 239 @RequestMapping(value = "/relations", method = RequestMethod.POST)
220 @ResponseBody 240 @ResponseBody
221 public List<EntityRelation> findByQuery(@RequestBody EntityRelationsQuery query) throws ThingsboardException { 241 public List<EntityRelation> findByQuery(@RequestBody EntityRelationsQuery query) throws ThingsboardException {
@@ -20,6 +20,7 @@ public class EntityRelationInfo extends EntityRelation { @@ -20,6 +20,7 @@ public class EntityRelationInfo extends EntityRelation {
20 20
21 private static final long serialVersionUID = 2807343097519543363L; 21 private static final long serialVersionUID = 2807343097519543363L;
22 22
  23 + private String fromName;
23 private String toName; 24 private String toName;
24 25
25 public EntityRelationInfo() { 26 public EntityRelationInfo() {
@@ -30,6 +31,14 @@ public class EntityRelationInfo extends EntityRelation { @@ -30,6 +31,14 @@ public class EntityRelationInfo extends EntityRelation {
30 super(entityRelation); 31 super(entityRelation);
31 } 32 }
32 33
  34 + public String getFromName() {
  35 + return fromName;
  36 + }
  37 +
  38 + public void setFromName(String fromName) {
  39 + this.fromName = fromName;
  40 + }
  41 +
33 public String getToName() { 42 public String getToName() {
34 return toName; 43 return toName;
35 } 44 }
@@ -23,29 +23,17 @@ import lombok.extern.slf4j.Slf4j; @@ -23,29 +23,17 @@ import lombok.extern.slf4j.Slf4j;
23 import org.springframework.beans.factory.annotation.Autowired; 23 import org.springframework.beans.factory.annotation.Autowired;
24 import org.springframework.stereotype.Service; 24 import org.springframework.stereotype.Service;
25 import org.springframework.util.StringUtils; 25 import org.springframework.util.StringUtils;
26 -import org.thingsboard.server.common.data.BaseData;  
27 -import org.thingsboard.server.common.data.Device;  
28 -import org.thingsboard.server.common.data.EntityType;  
29 -import org.thingsboard.server.common.data.asset.Asset;  
30 -import org.thingsboard.server.common.data.id.AssetId;  
31 -import org.thingsboard.server.common.data.id.DeviceId;  
32 import org.thingsboard.server.common.data.id.EntityId; 26 import org.thingsboard.server.common.data.id.EntityId;
33 -import org.thingsboard.server.common.data.id.UUIDBased;  
34 import org.thingsboard.server.common.data.relation.EntityRelation; 27 import org.thingsboard.server.common.data.relation.EntityRelation;
35 import org.thingsboard.server.common.data.relation.EntityRelationInfo; 28 import org.thingsboard.server.common.data.relation.EntityRelationInfo;
36 import org.thingsboard.server.common.data.relation.RelationTypeGroup; 29 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
37 -import org.thingsboard.server.dao.asset.AssetService;  
38 -import org.thingsboard.server.dao.customer.CustomerService;  
39 -import org.thingsboard.server.dao.device.DeviceService;  
40 import org.thingsboard.server.dao.entity.EntityService; 30 import org.thingsboard.server.dao.entity.EntityService;
41 import org.thingsboard.server.dao.exception.DataValidationException; 31 import org.thingsboard.server.dao.exception.DataValidationException;
42 -import org.thingsboard.server.dao.plugin.PluginService;  
43 -import org.thingsboard.server.dao.rule.RuleService;  
44 -import org.thingsboard.server.dao.tenant.TenantService;  
45 32
46 import javax.annotation.Nullable; 33 import javax.annotation.Nullable;
47 import java.util.*; 34 import java.util.*;
48 import java.util.concurrent.ConcurrentHashMap; 35 import java.util.concurrent.ConcurrentHashMap;
  36 +import java.util.function.BiConsumer;
49 37
50 /** 38 /**
51 * Created by ashvayka on 28.04.17. 39 * Created by ashvayka on 28.04.17.
@@ -133,23 +121,16 @@ public class BaseRelationService implements RelationService { @@ -133,23 +121,16 @@ public class BaseRelationService implements RelationService {
133 ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations, 121 ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
134 (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> { 122 (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
135 List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>(); 123 List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
136 - relations1.stream().forEach(relation -> futures.add(fetchRelationInfoAsync(relation)));  
137 - return Futures.successfulAsList(futures); 124 + relations1.stream().forEach(relation ->
  125 + futures.add(fetchRelationInfoAsync(relation,
  126 + relation2 -> relation2.getTo(),
  127 + (EntityRelationInfo relationInfo, String entityName) -> relationInfo.setToName(entityName)))
  128 + );
  129 + return Futures.successfulAsList(futures);
138 }); 130 });
139 return relationsInfo; 131 return relationsInfo;
140 } 132 }
141 133
142 - private ListenableFuture<EntityRelationInfo> fetchRelationInfoAsync(EntityRelation relation) {  
143 - ListenableFuture<String> entityName = entityService.fetchEntityNameAsync(relation.getTo());  
144 - ListenableFuture<EntityRelationInfo> entityRelationInfo =  
145 - Futures.transform(entityName, (Function<String, EntityRelationInfo>) entityName1 -> {  
146 - EntityRelationInfo entityRelationInfo1 = new EntityRelationInfo(relation);  
147 - entityRelationInfo1.setToName(entityName1);  
148 - return entityRelationInfo1;  
149 - });  
150 - return entityRelationInfo;  
151 - }  
152 -  
153 @Override 134 @Override
154 public ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup) { 135 public ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup) {
155 log.trace("Executing findByFromAndType [{}][{}][{}]", from, relationType, typeGroup); 136 log.trace("Executing findByFromAndType [{}][{}][{}]", from, relationType, typeGroup);
@@ -168,6 +149,38 @@ public class BaseRelationService implements RelationService { @@ -168,6 +149,38 @@ public class BaseRelationService implements RelationService {
168 } 149 }
169 150
170 @Override 151 @Override
  152 + public ListenableFuture<List<EntityRelationInfo>> findInfoByTo(EntityId to, RelationTypeGroup typeGroup) {
  153 + log.trace("Executing findInfoByTo [{}][{}]", to, typeGroup);
  154 + validate(to);
  155 + validateTypeGroup(typeGroup);
  156 + ListenableFuture<List<EntityRelation>> relations = relationDao.findAllByTo(to, typeGroup);
  157 + ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
  158 + (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
  159 + List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
  160 + relations1.stream().forEach(relation ->
  161 + futures.add(fetchRelationInfoAsync(relation,
  162 + relation2 -> relation2.getFrom(),
  163 + (EntityRelationInfo relationInfo, String entityName) -> relationInfo.setFromName(entityName)))
  164 + );
  165 + return Futures.successfulAsList(futures);
  166 + });
  167 + return relationsInfo;
  168 + }
  169 +
  170 + private ListenableFuture<EntityRelationInfo> fetchRelationInfoAsync(EntityRelation relation,
  171 + Function<EntityRelation, EntityId> entityIdGetter,
  172 + BiConsumer<EntityRelationInfo, String> entityNameSetter) {
  173 + ListenableFuture<String> entityName = entityService.fetchEntityNameAsync(entityIdGetter.apply(relation));
  174 + ListenableFuture<EntityRelationInfo> entityRelationInfo =
  175 + Futures.transform(entityName, (Function<String, EntityRelationInfo>) entityName1 -> {
  176 + EntityRelationInfo entityRelationInfo1 = new EntityRelationInfo(relation);
  177 + entityNameSetter.accept(entityRelationInfo1, entityName1);
  178 + return entityRelationInfo1;
  179 + });
  180 + return entityRelationInfo;
  181 + }
  182 +
  183 + @Override
171 public ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup) { 184 public ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup) {
172 log.trace("Executing findByToAndType [{}][{}][{}]", to, relationType, typeGroup); 185 log.trace("Executing findByToAndType [{}][{}][{}]", to, relationType, typeGroup);
173 validate(to); 186 validate(to);
@@ -46,6 +46,8 @@ public interface RelationService { @@ -46,6 +46,8 @@ public interface RelationService {
46 46
47 ListenableFuture<List<EntityRelation>> findByTo(EntityId to, RelationTypeGroup typeGroup); 47 ListenableFuture<List<EntityRelation>> findByTo(EntityId to, RelationTypeGroup typeGroup);
48 48
  49 + ListenableFuture<List<EntityRelationInfo>> findInfoByTo(EntityId to, RelationTypeGroup typeGroup);
  50 +
49 ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup); 51 ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup);
50 52
51 ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query); 53 ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query);
@@ -28,6 +28,7 @@ function EntityRelationService($http, $q) { @@ -28,6 +28,7 @@ function EntityRelationService($http, $q) {
28 findInfoByFrom: findInfoByFrom, 28 findInfoByFrom: findInfoByFrom,
29 findByFromAndType: findByFromAndType, 29 findByFromAndType: findByFromAndType,
30 findByTo: findByTo, 30 findByTo: findByTo,
  31 + findInfoByTo: findInfoByTo,
31 findByToAndType: findByToAndType, 32 findByToAndType: findByToAndType,
32 findByQuery: findByQuery 33 findByQuery: findByQuery
33 } 34 }
@@ -122,6 +123,18 @@ function EntityRelationService($http, $q) { @@ -122,6 +123,18 @@ function EntityRelationService($http, $q) {
122 return deferred.promise; 123 return deferred.promise;
123 } 124 }
124 125
  126 + function findInfoByTo(toId, toType) {
  127 + var deferred = $q.defer();
  128 + var url = '/api/relations/info?toId=' + toId;
  129 + url += '&toType=' + toType;
  130 + $http.get(url, null).then(function success(response) {
  131 + deferred.resolve(response.data);
  132 + }, function fail() {
  133 + deferred.reject();
  134 + });
  135 + return deferred.promise;
  136 + }
  137 +
125 function findByToAndType(toId, toType, relationType) { 138 function findByToAndType(toId, toType, relationType) {
126 var deferred = $q.defer(); 139 var deferred = $q.defer();
127 var url = '/api/relations?toId=' + toId; 140 var url = '/api/relations?toId=' + toId;
@@ -55,5 +55,11 @@ @@ -55,5 +55,11 @@
55 default-event-type="{{vm.types.eventType.alarm.value}}"> 55 default-event-type="{{vm.types.eventType.alarm.value}}">
56 </tb-event-table> 56 </tb-event-table>
57 </md-tab> 57 </md-tab>
  58 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
  59 + <tb-relation-table flex
  60 + entity-id="vm.grid.operatingItem().id.id"
  61 + entity-type="{{vm.types.entityType.customer}}">
  62 + </tb-relation-table>
  63 + </md-tab>
58 </md-tabs> 64 </md-tabs>
59 </tb-grid> 65 </tb-grid>
@@ -56,4 +56,10 @@ @@ -56,4 +56,10 @@
56 default-event-type="{{vm.types.eventType.alarm.value}}"> 56 default-event-type="{{vm.types.eventType.alarm.value}}">
57 </tb-event-table> 57 </tb-event-table>
58 </md-tab> 58 </md-tab>
  59 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
  60 + <tb-relation-table flex
  61 + entity-id="vm.grid.operatingItem().id.id"
  62 + entity-type="{{vm.types.entityType.device}}">
  63 + </tb-relation-table>
  64 + </md-tab>
59 </tb-grid> 65 </tb-grid>
@@ -41,7 +41,7 @@ @@ -41,7 +41,7 @@
41 </md-autocomplete> 41 </md-autocomplete>
42 <md-chip-template> 42 <md-chip-template>
43 <span> 43 <span>
44 - <strong>{{itemName($chip)}}</strong> 44 + <strong>{{$chip.name}}</strong>
45 </span> 45 </span>
46 </md-chip-template> 46 </md-chip-template>
47 </md-chips> 47 </md-chips>
@@ -17,6 +17,9 @@ @@ -17,6 +17,9 @@
17 --> 17 -->
18 <div layout='row' class="tb-entity-select"> 18 <div layout='row' class="tb-entity-select">
19 <tb-entity-type-select style="min-width: 100px;" 19 <tb-entity-type-select style="min-width: 100px;"
  20 + the-form="theForm"
  21 + ng-disabled="disabled"
  22 + tb-required="tbRequired"
20 ng-model="model.entityType"> 23 ng-model="model.entityType">
21 </tb-entity-type-select> 24 </tb-entity-type-select>
22 <tb-entity-autocomplete flex ng-if="model.entityType" 25 <tb-entity-autocomplete flex ng-if="model.entityType"
@@ -29,6 +29,8 @@ export default function EntityTypeSelect($compile, $templateCache, utils, userSe @@ -29,6 +29,8 @@ export default function EntityTypeSelect($compile, $templateCache, utils, userSe
29 var template = $templateCache.get(entityTypeSelectTemplate); 29 var template = $templateCache.get(entityTypeSelectTemplate);
30 element.html(template); 30 element.html(template);
31 31
  32 + scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
  33 +
32 if (angular.isDefined(attrs.hideLabel)) { 34 if (angular.isDefined(attrs.hideLabel)) {
33 scope.showLabel = false; 35 scope.showLabel = false;
34 } else { 36 } else {
@@ -103,6 +105,9 @@ export default function EntityTypeSelect($compile, $templateCache, utils, userSe @@ -103,6 +105,9 @@ export default function EntityTypeSelect($compile, $templateCache, utils, userSe
103 require: "^ngModel", 105 require: "^ngModel",
104 link: linker, 106 link: linker,
105 scope: { 107 scope: {
  108 + theForm: '=?',
  109 + tbRequired: '=?',
  110 + disabled:'=ngDisabled',
106 allowedEntityTypes: "=?" 111 allowedEntityTypes: "=?"
107 } 112 }
108 }; 113 };
@@ -17,9 +17,13 @@ @@ -17,9 +17,13 @@
17 --> 17 -->
18 <md-input-container> 18 <md-input-container>
19 <label ng-if="showLabel">{{ 'entity.type' | translate }}</label> 19 <label ng-if="showLabel">{{ 'entity.type' | translate }}</label>
20 - <md-select ng-model="entityType" class="tb-entity-type-select" aria-label="{{ 'entity.type' | translate }}"> 20 + <md-select ng-required="tbRequired" ng-disabled="disabled" name="entityType"
  21 + ng-model="entityType" class="tb-entity-type-select" aria-label="{{ 'entity.type' | translate }}">
21 <md-option ng-repeat="type in entityTypes" ng-value="type"> 22 <md-option ng-repeat="type in entityTypes" ng-value="type">
22 {{typeName(type) | translate}} 23 {{typeName(type) | translate}}
23 </md-option> 24 </md-option>
24 </md-select> 25 </md-select>
25 -</md-input-container>  
  26 + <div ng-messages="theForm.entityType.$error">
  27 + <div ng-message="required" translate>entity.type-required</div>
  28 + </div>
  29 +</md-input-container>
@@ -27,6 +27,7 @@ import AddAttributeDialogController from './attribute/add-attribute-dialog.contr @@ -27,6 +27,7 @@ import AddAttributeDialogController from './attribute/add-attribute-dialog.contr
27 import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller'; 27 import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller';
28 import AttributeTableDirective from './attribute/attribute-table.directive'; 28 import AttributeTableDirective from './attribute/attribute-table.directive';
29 import RelationTableDirective from './relation/relation-table.directive'; 29 import RelationTableDirective from './relation/relation-table.directive';
  30 +import RelationTypeAutocompleteDirective from './relation/relation-type-autocomplete.directive';
30 31
31 export default angular.module('thingsboard.entity', []) 32 export default angular.module('thingsboard.entity', [])
32 .controller('EntityAliasesController', EntityAliasesController) 33 .controller('EntityAliasesController', EntityAliasesController)
@@ -42,4 +43,5 @@ export default angular.module('thingsboard.entity', []) @@ -42,4 +43,5 @@ export default angular.module('thingsboard.entity', [])
42 .directive('tbAliasesEntitySelect', AliasesEntitySelectDirective) 43 .directive('tbAliasesEntitySelect', AliasesEntitySelectDirective)
43 .directive('tbAttributeTable', AttributeTableDirective) 44 .directive('tbAttributeTable', AttributeTableDirective)
44 .directive('tbRelationTable', RelationTableDirective) 45 .directive('tbRelationTable', RelationTableDirective)
  46 + .directive('tbRelationTypeAutocomplete', RelationTypeAutocompleteDirective)
45 .name; 47 .name;
@@ -14,14 +14,20 @@ @@ -14,14 +14,20 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 /*@ngInject*/ 16 /*@ngInject*/
17 -export default function AddRelationDialogController($scope, $mdDialog, types, entityRelationService, from) { 17 +export default function AddRelationDialogController($scope, $mdDialog, types, entityRelationService, direction, entityId) {
18 18
19 var vm = this; 19 var vm = this;
20 20
21 vm.types = types; 21 vm.types = types;
  22 + vm.direction = direction;
  23 + vm.targetEntityId = {};
22 24
23 vm.relation = {}; 25 vm.relation = {};
24 - vm.relation.from = from; 26 + if (vm.direction == vm.types.entitySearchDirection.from) {
  27 + vm.relation.from = entityId;
  28 + } else {
  29 + vm.relation.to = entityId;
  30 + }
25 vm.relation.type = types.entityRelationType.contains; 31 vm.relation.type = types.entityRelationType.contains;
26 32
27 vm.add = add; 33 vm.add = add;
@@ -32,6 +38,11 @@ export default function AddRelationDialogController($scope, $mdDialog, types, en @@ -32,6 +38,11 @@ export default function AddRelationDialogController($scope, $mdDialog, types, en
32 } 38 }
33 39
34 function add() { 40 function add() {
  41 + if (vm.direction == vm.types.entitySearchDirection.from) {
  42 + vm.relation.to = vm.targetEntityId;
  43 + } else {
  44 + vm.relation.from = vm.targetEntityId;
  45 + }
35 $scope.theForm.$setPristine(); 46 $scope.theForm.$setPristine();
36 entityRelationService.saveRelation(vm.relation).then( 47 entityRelationService.saveRelation(vm.relation).then(
37 function success() { 48 function success() {
@@ -32,19 +32,16 @@ @@ -32,19 +32,16 @@
32 <div class="md-dialog-content"> 32 <div class="md-dialog-content">
33 <md-content class="md-padding" layout="column"> 33 <md-content class="md-padding" layout="column">
34 <fieldset ng-disabled="loading"> 34 <fieldset ng-disabled="loading">
35 - <md-input-container class="md-block">  
36 - <label translate>relation.relation-type</label>  
37 - <md-select required ng-model="vm.relation.type" ng-disabled="loading">  
38 - <md-option ng-repeat="type in vm.types.entityRelationType" ng-value="type">  
39 - <span>{{('relation.relation-types.' + type) | translate}}</span>  
40 - </md-option>  
41 - </md-select>  
42 - </md-input-container>  
43 - <span class="tb-small">{{'entity.entity' | translate }}</span> 35 + <tb-relation-type-autocomplete ng-model="vm.relation.type"
  36 + tb-required="true"
  37 + ng-disabled="loading">
  38 + </tb-relation-type-autocomplete>
  39 + <small>{{(vm.direction == vm.types.entitySearchDirection.from ?
  40 + 'relation.to-entity' : 'relation.from-entity') | translate}}</small>
44 <tb-entity-select flex 41 <tb-entity-select flex
45 the-form="theForm" 42 the-form="theForm"
46 tb-required="true" 43 tb-required="true"
47 - ng-model="vm.relation.to"> 44 + ng-model="vm.targetEntityId">
48 </tb-entity-select> 45 </tb-entity-select>
49 </fieldset> 46 </fieldset>
50 </md-content> 47 </md-content>
@@ -45,13 +45,17 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $ @@ -45,13 +45,17 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
45 45
46 let vm = this; 46 let vm = this;
47 47
  48 + vm.types = types;
  49 +
  50 + vm.direction = vm.types.entitySearchDirection.from;
  51 +
48 vm.relations = []; 52 vm.relations = [];
49 vm.relationsCount = 0; 53 vm.relationsCount = 0;
50 vm.allRelations = []; 54 vm.allRelations = [];
51 vm.selectedRelations = []; 55 vm.selectedRelations = [];
52 56
53 vm.query = { 57 vm.query = {
54 - order: 'typeName', 58 + order: 'type',
55 limit: 5, 59 limit: 5,
56 page: 1, 60 page: 1,
57 search: null 61 search: null
@@ -62,19 +66,23 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $ @@ -62,19 +66,23 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
62 vm.onReorder = onReorder; 66 vm.onReorder = onReorder;
63 vm.onPaginate = onPaginate; 67 vm.onPaginate = onPaginate;
64 vm.addRelation = addRelation; 68 vm.addRelation = addRelation;
65 - vm.editRelation = editRelation;  
66 vm.deleteRelation = deleteRelation; 69 vm.deleteRelation = deleteRelation;
67 vm.deleteRelations = deleteRelations; 70 vm.deleteRelations = deleteRelations;
68 vm.reloadRelations = reloadRelations; 71 vm.reloadRelations = reloadRelations;
69 vm.updateRelations = updateRelations; 72 vm.updateRelations = updateRelations;
70 73
71 -  
72 $scope.$watch("vm.entityId", function(newVal, prevVal) { 74 $scope.$watch("vm.entityId", function(newVal, prevVal) {
73 if (newVal && !angular.equals(newVal, prevVal)) { 75 if (newVal && !angular.equals(newVal, prevVal)) {
74 reloadRelations(); 76 reloadRelations();
75 } 77 }
76 }); 78 });
77 79
  80 + $scope.$watch("vm.direction", function(newVal, prevVal) {
  81 + if (newVal && !angular.equals(newVal, prevVal)) {
  82 + reloadRelations();
  83 + }
  84 + });
  85 +
78 $scope.$watch("vm.query.search", function(newVal, prevVal) { 86 $scope.$watch("vm.query.search", function(newVal, prevVal) {
79 if (!angular.equals(newVal, prevVal) && vm.query.search != null) { 87 if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
80 updateRelations(); 88 updateRelations();
@@ -102,7 +110,7 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $ @@ -102,7 +110,7 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
102 if ($event) { 110 if ($event) {
103 $event.stopPropagation(); 111 $event.stopPropagation();
104 } 112 }
105 - var from = { 113 + var entityId = {
106 id: vm.entityId, 114 id: vm.entityId,
107 entityType: vm.entityType 115 entityType: vm.entityType
108 }; 116 };
@@ -111,7 +119,7 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $ @@ -111,7 +119,7 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
111 controllerAs: 'vm', 119 controllerAs: 'vm',
112 templateUrl: addRelationTemplate, 120 templateUrl: addRelationTemplate,
113 parent: angular.element($document[0].body), 121 parent: angular.element($document[0].body),
114 - locals: { from: from }, 122 + locals: { direction: vm.direction, entityId: entityId },
115 fullscreen: true, 123 fullscreen: true,
116 targetEvent: $event 124 targetEvent: $event
117 }).then(function () { 125 }).then(function () {
@@ -120,36 +128,100 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $ @@ -120,36 +128,100 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
120 }); 128 });
121 } 129 }
122 130
123 - function editRelation($event, /*relation*/) { 131 + function deleteRelation($event, relation) {
124 if ($event) { 132 if ($event) {
125 $event.stopPropagation(); 133 $event.stopPropagation();
126 } 134 }
127 - //TODO:  
128 - } 135 + if (relation) {
  136 + var title;
  137 + var content;
  138 + if (vm.direction == vm.types.entitySearchDirection.from) {
  139 + title = $translate.instant('relation.delete-to-relation-title', {entityName: relation.toName});
  140 + content = $translate.instant('relation.delete-to-relation-text', {entityName: relation.toName});
  141 + } else {
  142 + title = $translate.instant('relation.delete-from-relation-title', {entityName: relation.fromName});
  143 + content = $translate.instant('relation.delete-from-relation-text', {entityName: relation.fromName});
  144 + }
129 145
130 - function deleteRelation($event, /*relation*/) {  
131 - if ($event) {  
132 - $event.stopPropagation(); 146 + var confirm = $mdDialog.confirm()
  147 + .targetEvent($event)
  148 + .title(title)
  149 + .htmlContent(content)
  150 + .ariaLabel(title)
  151 + .cancel($translate.instant('action.no'))
  152 + .ok($translate.instant('action.yes'));
  153 + $mdDialog.show(confirm).then(function () {
  154 + entityRelationService.deleteRelation(
  155 + relation.from.id,
  156 + relation.from.entityType,
  157 + relation.type,
  158 + relation.to.id,
  159 + relation.to.entityType).then(
  160 + function success() {
  161 + reloadRelations();
  162 + }
  163 + );
  164 + });
133 } 165 }
134 - //TODO:  
135 } 166 }
136 167
137 function deleteRelations($event) { 168 function deleteRelations($event) {
138 if ($event) { 169 if ($event) {
139 $event.stopPropagation(); 170 $event.stopPropagation();
140 } 171 }
141 - //TODO: 172 + if (vm.selectedRelations && vm.selectedRelations.length > 0) {
  173 + var title;
  174 + var content;
  175 + if (vm.direction == vm.types.entitySearchDirection.from) {
  176 + title = $translate.instant('relation.delete-to-relations-title', {count: vm.selectedRelations.length}, 'messageformat');
  177 + content = $translate.instant('relation.delete-to-relations-text');
  178 + } else {
  179 + title = $translate.instant('relation.delete-from-relations-title', {count: vm.selectedRelations.length}, 'messageformat');
  180 + content = $translate.instant('relation.delete-from-relations-text');
  181 + }
  182 + var confirm = $mdDialog.confirm()
  183 + .targetEvent($event)
  184 + .title(title)
  185 + .htmlContent(content)
  186 + .ariaLabel(title)
  187 + .cancel($translate.instant('action.no'))
  188 + .ok($translate.instant('action.yes'));
  189 + $mdDialog.show(confirm).then(function () {
  190 + var tasks = [];
  191 + for (var i=0;i<vm.selectedRelations.length;i++) {
  192 + var relation = vm.selectedRelations[i];
  193 + tasks.push( entityRelationService.deleteRelation(
  194 + relation.from.id,
  195 + relation.from.entityType,
  196 + relation.type,
  197 + relation.to.id,
  198 + relation.to.entityType));
  199 + }
  200 + $q.all(tasks).then(function () {
  201 + reloadRelations();
  202 + });
  203 +
  204 + });
  205 + }
142 } 206 }
143 207
144 function reloadRelations () { 208 function reloadRelations () {
145 vm.allRelations.length = 0; 209 vm.allRelations.length = 0;
146 vm.relations.length = 0; 210 vm.relations.length = 0;
147 - vm.relationsPromise = entityRelationService.findInfoByFrom(vm.entityId, vm.entityType); 211 + vm.relationsPromise;
  212 + if (vm.direction == vm.types.entitySearchDirection.from) {
  213 + vm.relationsPromise = entityRelationService.findInfoByFrom(vm.entityId, vm.entityType);
  214 + } else {
  215 + vm.relationsPromise = entityRelationService.findInfoByTo(vm.entityId, vm.entityType);
  216 + }
148 vm.relationsPromise.then( 217 vm.relationsPromise.then(
149 function success(allRelations) { 218 function success(allRelations) {
150 allRelations.forEach(function(relation) { 219 allRelations.forEach(function(relation) {
151 - relation.typeName = $translate.instant('relation.relation-type.' + relation.type);  
152 - relation.toEntityTypeName = $translate.instant(utils.entityTypeName(relation.to.entityType)); 220 + if (vm.direction == vm.types.entitySearchDirection.from) {
  221 + relation.toEntityTypeName = $translate.instant(utils.entityTypeName(relation.to.entityType));
  222 + } else {
  223 + relation.fromEntityTypeName = $translate.instant(utils.entityTypeName(relation.from.entityType));
  224 + }
153 }); 225 });
154 vm.allRelations = allRelations; 226 vm.allRelations = allRelations;
155 vm.selectedRelations = []; 227 vm.selectedRelations = [];
@@ -16,11 +16,22 @@ @@ -16,11 +16,22 @@
16 16
17 --> 17 -->
18 <md-content flex class="md-padding tb-absolute-fill tb-relation-table tb-data-table" layout="column"> 18 <md-content flex class="md-padding tb-absolute-fill tb-relation-table tb-data-table" layout="column">
  19 + <section layout="row">
  20 + <md-input-container class="md-block" style="width: 200px;">
  21 + <label translate>relation.direction</label>
  22 + <md-select ng-model="vm.direction" ng-disabled="loading">
  23 + <md-option ng-repeat="direction in vm.types.entitySearchDirection" ng-value="direction">
  24 + {{ ('relation.search-direction.' + direction) | translate}}
  25 + </md-option>
  26 + </md-select>
  27 + </md-input-container>
  28 + </section>
19 <div layout="column" class="md-whiteframe-z1"> 29 <div layout="column" class="md-whiteframe-z1">
20 <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedRelations.length 30 <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedRelations.length
21 && vm.query.search === null"> 31 && vm.query.search === null">
22 <div class="md-toolbar-tools"> 32 <div class="md-toolbar-tools">
23 - <span translate>relation.entity-relations</span> 33 + <span>{{(vm.direction == vm.types.entitySearchDirection.from ?
  34 + 'relation.from-relations' : 'relation.to-relations') | translate}}</span>
24 <span flex></span> 35 <span flex></span>
25 <md-button class="md-icon-button" ng-click="vm.addRelation($event)"> 36 <md-button class="md-icon-button" ng-click="vm.addRelation($event)">
26 <md-icon>add</md-icon> 37 <md-icon>add</md-icon>
@@ -66,7 +77,7 @@ @@ -66,7 +77,7 @@
66 <md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedRelations.length"> 77 <md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedRelations.length">
67 <div class="md-toolbar-tools"> 78 <div class="md-toolbar-tools">
68 <span translate 79 <span translate
69 - translate-values="{count: selectedRelations.length}" 80 + translate-values="{count: vm.selectedRelations.length}"
70 translate-interpolation="messageformat">relation.selected-relations</span> 81 translate-interpolation="messageformat">relation.selected-relations</span>
71 <span flex></span> 82 <span flex></span>
72 <md-button class="md-icon-button" ng-click="vm.deleteRelations($event)"> 83 <md-button class="md-icon-button" ng-click="vm.deleteRelations($event)">
@@ -81,25 +92,26 @@ @@ -81,25 +92,26 @@
81 <table md-table md-row-select multiple="" ng-model="vm.selectedRelations" md-progress="vm.relationsDeferred.promise"> 92 <table md-table md-row-select multiple="" ng-model="vm.selectedRelations" md-progress="vm.relationsDeferred.promise">
82 <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder"> 93 <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
83 <tr md-row> 94 <tr md-row>
84 - <th md-column md-order-by="typeName"><span translate>relation.type</span></th>  
85 - <th md-column md-order-by="toEntityTypeName"><span translate>relation.to-entity-type</span></th>  
86 - <th md-column md-order-by="toName"><span translate>relation.to-entity-name</span></th> 95 + <th md-column md-order-by="type"><span translate>relation.type</span></th>
  96 + <th md-column ng-if="vm.direction == vm.types.entitySearchDirection.from"
  97 + md-order-by="toEntityTypeName"><span translate>relation.to-entity-type</span></th>
  98 + <th md-column ng-if="vm.direction == vm.types.entitySearchDirection.to"
  99 + md-order-by="fromEntityTypeName"><span translate>relation.from-entity-type</span></th>
  100 + <th md-column ng-if="vm.direction == vm.types.entitySearchDirection.from"
  101 + md-order-by="toName"><span translate>relation.to-entity-name</span></th>
  102 + <th md-column ng-if="vm.direction == vm.types.entitySearchDirection.to"
  103 + md-order-by="fromName"><span translate>relation.from-entity-name</span></th>
87 <th md-column><span>&nbsp</span></th> 104 <th md-column><span>&nbsp</span></th>
88 </tr> 105 </tr>
89 </thead> 106 </thead>
90 <tbody md-body> 107 <tbody md-body>
91 <tr md-row md-select="relation" md-select-id="relation" md-auto-select ng-repeat="relation in vm.relations"> 108 <tr md-row md-select="relation" md-select-id="relation" md-auto-select ng-repeat="relation in vm.relations">
92 - <td md-cell>{{ relation.typeName }}</td>  
93 - <td md-cell>{{ relation.toEntityTypeName }}</td>  
94 - <td md-cell>{{ relation.toName }}</td> 109 + <td md-cell>{{ relation.type }}</td>
  110 + <td md-cell ng-if="vm.direction == vm.types.entitySearchDirection.from">{{ relation.toEntityTypeName }}</td>
  111 + <td md-cell ng-if="vm.direction == vm.types.entitySearchDirection.to">{{ relation.fromEntityTypeName }}</td>
  112 + <td md-cell ng-if="vm.direction == vm.types.entitySearchDirection.from">{{ relation.toName }}</td>
  113 + <td md-cell ng-if="vm.direction == vm.types.entitySearchDirection.to">{{ relation.fromName }}</td>
95 <td md-cell class="tb-action-cell"> 114 <td md-cell class="tb-action-cell">
96 - <md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}"  
97 - ng-click="vm.editRelation($event, relation)">  
98 - <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>  
99 - <md-tooltip md-direction="top">  
100 - {{ 'relation.edit' | translate }}  
101 - </md-tooltip>  
102 - </md-button>  
103 <md-button class="md-icon-button" aria-label="{{ 'action.delete' | translate }}" ng-click="vm.deleteRelation($event, relation)"> 115 <md-button class="md-icon-button" aria-label="{{ 'action.delete' | translate }}" ng-click="vm.deleteRelation($event, relation)">
104 <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">delete</md-icon> 116 <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">delete</md-icon>
105 <md-tooltip md-direction="top"> 117 <md-tooltip md-direction="top">
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +import './relation-type-autocomplete.scss';
  17 +
  18 +/* eslint-disable import/no-unresolved, import/default */
  19 +
  20 +import relationTypeAutocompleteTemplate from './relation-type-autocomplete.tpl.html';
  21 +
  22 +/* eslint-enable import/no-unresolved, import/default */
  23 +
  24 +/*@ngInject*/
  25 +export default function RelationTypeAutocomplete($compile, $templateCache, $q, $filter, assetService, deviceService, types) {
  26 +
  27 + var linker = function (scope, element, attrs, ngModelCtrl) {
  28 + var template = $templateCache.get(relationTypeAutocompleteTemplate);
  29 + element.html(template);
  30 +
  31 + scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
  32 + scope.relationType = null;
  33 + scope.relationTypeSearchText = '';
  34 + scope.relationTypes = [];
  35 + for (var type in types.entityRelationType) {
  36 + scope.relationTypes.push(types.entityRelationType[type]);
  37 + }
  38 +
  39 + scope.fetchRelationTypes = function(searchText) {
  40 + var deferred = $q.defer();
  41 + var result = $filter('filter')(scope.relationTypes, {'$': searchText});
  42 + if (result && result.length) {
  43 + deferred.resolve(result);
  44 + } else {
  45 + deferred.resolve([searchText]);
  46 + }
  47 + return deferred.promise;
  48 + }
  49 +
  50 + scope.relationTypeSearchTextChanged = function() {
  51 + }
  52 +
  53 + scope.updateView = function () {
  54 + if (!scope.disabled) {
  55 + ngModelCtrl.$setViewValue(scope.relationType);
  56 + }
  57 + }
  58 +
  59 + ngModelCtrl.$render = function () {
  60 + scope.relationType = ngModelCtrl.$viewValue;
  61 + }
  62 +
  63 + scope.$watch('relationType', function (newValue, prevValue) {
  64 + if (!angular.equals(newValue, prevValue)) {
  65 + scope.updateView();
  66 + }
  67 + });
  68 +
  69 + scope.$watch('disabled', function () {
  70 + scope.updateView();
  71 + });
  72 +
  73 + $compile(element.contents())(scope);
  74 + }
  75 +
  76 + return {
  77 + restrict: "E",
  78 + require: "^ngModel",
  79 + link: linker,
  80 + scope: {
  81 + theForm: '=?',
  82 + tbRequired: '=?',
  83 + disabled:'=ngDisabled'
  84 + }
  85 + };
  86 +}
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +.tb-relation-type-autocomplete {
  17 + .tb-relation-type-item {
  18 + display: block;
  19 + height: 48px;
  20 + }
  21 + li {
  22 + height: auto !important;
  23 + white-space: normal !important;
  24 + }
  25 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-autocomplete ng-required="tbRequired"
  19 + ng-disabled="disabled"
  20 + md-no-cache="true"
  21 + md-input-name="relationType"
  22 + ng-model="relationType"
  23 + md-selected-item="relationType"
  24 + md-search-text="relationTypeSearchText"
  25 + md-search-text-change="relationTypeSearchTextChanged()"
  26 + md-items="item in fetchRelationTypes(relationTypeSearchText)"
  27 + md-item-text="item"
  28 + md-min-length="0"
  29 + md-floating-label="{{ 'relation.relation-type' | translate }}"
  30 + md-select-on-match="true"
  31 + md-menu-class="tb-relation-type-autocomplete">
  32 + <md-item-template>
  33 + <div class="tb-relation-type-item">
  34 + <span md-highlight-text="relationTypeSearchText" md-highlight-flags="^i">{{item}}</span>
  35 + </div>
  36 + </md-item-template>
  37 + <div ng-messages="theForm.relationType.$error">
  38 + <div translate ng-message="required">relation.relation-type-required</div>
  39 + </div>
  40 +</md-autocomplete>
@@ -544,6 +544,7 @@ export default angular.module('thingsboard.locale', []) @@ -544,6 +544,7 @@ export default angular.module('thingsboard.locale', [])
544 "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.", 544 "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.",
545 "all-subtypes": "All", 545 "all-subtypes": "All",
546 "type": "Type", 546 "type": "Type",
  547 + "type-required": "Entity type is required.",
547 "type-device": "Device", 548 "type-device": "Device",
548 "type-asset": "Asset", 549 "type-asset": "Asset",
549 "type-rule": "Rule", 550 "type-rule": "Rule",
@@ -718,19 +719,33 @@ export default angular.module('thingsboard.locale', []) @@ -718,19 +719,33 @@ export default angular.module('thingsboard.locale', [])
718 }, 719 },
719 "relation": { 720 "relation": {
720 "relations": "Relations", 721 "relations": "Relations",
721 - "entity-relations": "Entity relations", 722 + "direction": "Direction",
  723 + "search-direction": {
  724 + "FROM": "From",
  725 + "TO": "To"
  726 + },
  727 + "from-relations": "Outbound relations",
  728 + "to-relations": "Inbound relations",
722 "selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected", 729 "selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected",
723 "type": "Type", 730 "type": "Type",
724 - "to-entity-type": "Entity type",  
725 - "to-entity-name": "Entity name",  
726 - "edit": "Edit relation", 731 + "to-entity-type": "To entity type",
  732 + "to-entity-name": "To entity name",
  733 + "from-entity-type": "From entity type",
  734 + "from-entity-name": "From entity name",
  735 + "to-entity": "To entity",
  736 + "from-entity": "From entity",
727 "delete": "Delete relation", 737 "delete": "Delete relation",
728 "relation-type": "Relation type", 738 "relation-type": "Relation type",
729 - "relation-types": {  
730 - "Contains": "Contains",  
731 - "Manages": "Manages"  
732 - },  
733 - "add": "Add relation" 739 + "relation-type-required": "Relation type is required.",
  740 + "add": "Add relation",
  741 + "delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?",
  742 + "delete-to-relation-text": "Be careful, after the confirmation the entity '{{entityName}}' will be unrelated from the current entity.",
  743 + "delete-to-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?",
  744 + "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.",
  745 + "delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?",
  746 + "delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.",
  747 + "delete-from-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?",
  748 + "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."
734 }, 749 },
735 "rule": { 750 "rule": {
736 "rule": "Rule", 751 "rule": "Rule",
@@ -56,5 +56,11 @@ @@ -56,5 +56,11 @@
56 disabled-event-types="{{vm.types.eventType.alarm.value}}"> 56 disabled-event-types="{{vm.types.eventType.alarm.value}}">
57 </tb-event-table> 57 </tb-event-table>
58 </md-tab> 58 </md-tab>
  59 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
  60 + <tb-relation-table flex
  61 + entity-id="vm.grid.operatingItem().id.id"
  62 + entity-type="{{vm.types.entityType.plugin}}">
  63 + </tb-relation-table>
  64 + </md-tab>
59 </md-tabs> 65 </md-tabs>
60 </tb-grid> 66 </tb-grid>
@@ -56,5 +56,11 @@ @@ -56,5 +56,11 @@
56 disabled-event-types="{{vm.types.eventType.alarm.value}}"> 56 disabled-event-types="{{vm.types.eventType.alarm.value}}">
57 </tb-event-table> 57 </tb-event-table>
58 </md-tab> 58 </md-tab>
  59 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
  60 + <tb-relation-table flex
  61 + entity-id="vm.grid.operatingItem().id.id"
  62 + entity-type="{{vm.types.entityType.rule}}">
  63 + </tb-relation-table>
  64 + </md-tab>
59 </md-tabs> 65 </md-tabs>
60 </tb-grid> 66 </tb-grid>
@@ -53,5 +53,11 @@ @@ -53,5 +53,11 @@
53 default-event-type="{{vm.types.eventType.alarm.value}}"> 53 default-event-type="{{vm.types.eventType.alarm.value}}">
54 </tb-event-table> 54 </tb-event-table>
55 </md-tab> 55 </md-tab>
  56 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
  57 + <tb-relation-table flex
  58 + entity-id="vm.grid.operatingItem().id.id"
  59 + entity-type="{{vm.types.entityType.tenant}}">
  60 + </tb-relation-table>
  61 + </md-tab>
56 </md-tabs> 62 </md-tabs>
57 </tb-grid> 63 </tb-grid>
@@ -436,6 +436,7 @@ md-tabs.tb-headless { @@ -436,6 +436,7 @@ md-tabs.tb-headless {
436 ***********************/ 436 ***********************/
437 437
438 section.tb-header-buttons { 438 section.tb-header-buttons {
  439 + pointer-events: none;
439 position: absolute; 440 position: absolute;
440 right: 0px; 441 right: 0px;
441 top: 86px; 442 top: 86px;