Browse Source

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

pull/152/head
Volodymyr Babak 9 years ago
parent
commit
44c28b5b0f
  1. 31
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  2. 12
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  3. 15
      application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
  4. 16
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
  5. 58
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java
  6. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java
  7. 18
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java
  8. 9
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java
  9. 3
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
  10. 5
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  11. 96
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  12. 25
      dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java
  13. 11
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
  14. 26
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
  15. 2
      dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
  16. 7
      dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
  17. 134
      ui/src/app/alarm/alarm-details-dialog.controller.js
  18. 27
      ui/src/app/alarm/alarm-details-dialog.scss
  19. 107
      ui/src/app/alarm/alarm-details-dialog.tpl.html
  20. 39
      ui/src/app/alarm/alarm-header.directive.js
  21. 23
      ui/src/app/alarm/alarm-header.tpl.html
  22. 67
      ui/src/app/alarm/alarm-row.directive.js
  23. 17
      ui/src/app/alarm/alarm-row.tpl.html
  24. 211
      ui/src/app/alarm/alarm-table.directive.js
  25. 49
      ui/src/app/alarm/alarm-table.tpl.html
  26. 78
      ui/src/app/alarm/alarm.scss
  27. 27
      ui/src/app/alarm/index.js
  28. 29
      ui/src/app/api/alarm.service.js
  29. 279
      ui/src/app/api/alias-controller.js
  30. 14
      ui/src/app/api/entity-relation.service.js
  31. 563
      ui/src/app/api/entity.service.js
  32. 207
      ui/src/app/api/subscription.js
  33. 7
      ui/src/app/asset/assets.tpl.html
  34. 2
      ui/src/app/asset/index.js
  35. 148
      ui/src/app/common/dashboard-utils.service.js
  36. 123
      ui/src/app/common/types.constant.js
  37. 37
      ui/src/app/common/utils.service.js
  38. 37
      ui/src/app/components/dashboard.directive.js
  39. 2
      ui/src/app/components/dashboard.tpl.html
  40. 32
      ui/src/app/components/datakey-config-dialog.controller.js
  41. 11
      ui/src/app/components/datasource-entity.directive.js
  42. 2
      ui/src/app/components/datasource-entity.tpl.html
  43. 8
      ui/src/app/components/datasource-func.directive.js
  44. 38
      ui/src/app/components/datasource-func.scss
  45. 111
      ui/src/app/components/datasource-func.tpl.html
  46. 2
      ui/src/app/components/datasource.directive.js
  47. 2
      ui/src/app/components/datasource.tpl.html
  48. 17
      ui/src/app/components/entity-alias-select.directive.js
  49. 12
      ui/src/app/components/widget-config.directive.js
  50. 4
      ui/src/app/components/widget-config.tpl.html
  51. 320
      ui/src/app/components/widget.controller.js
  52. 85
      ui/src/app/components/widget.directive.js
  53. 7
      ui/src/app/customer/customers.tpl.html
  54. 69
      ui/src/app/dashboard/add-widget.controller.js
  55. 2
      ui/src/app/dashboard/add-widget.tpl.html
  56. 41
      ui/src/app/dashboard/dashboard.controller.js
  57. 5
      ui/src/app/dashboard/dashboard.tpl.html
  58. 62
      ui/src/app/dashboard/edit-widget.directive.js
  59. 2
      ui/src/app/dashboard/edit-widget.tpl.html
  60. 2
      ui/src/app/dashboard/layouts/dashboard-layout.tpl.html
  61. 7
      ui/src/app/device/devices.tpl.html
  62. 2
      ui/src/app/device/index.js
  63. 0
      ui/src/app/entity/alias/aliases-entity-select-button.tpl.html
  64. 49
      ui/src/app/entity/alias/aliases-entity-select-panel.controller.js
  65. 10
      ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html
  66. 58
      ui/src/app/entity/alias/aliases-entity-select.directive.js
  67. 0
      ui/src/app/entity/alias/aliases-entity-select.scss
  68. 109
      ui/src/app/entity/alias/entity-alias-dialog.controller.js
  69. 19
      ui/src/app/entity/alias/entity-alias-dialog.scss
  70. 73
      ui/src/app/entity/alias/entity-alias-dialog.tpl.html
  71. 152
      ui/src/app/entity/alias/entity-aliases.controller.js
  72. 49
      ui/src/app/entity/alias/entity-aliases.scss
  73. 105
      ui/src/app/entity/alias/entity-aliases.tpl.html
  74. 12
      ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
  75. 43
      ui/src/app/entity/attribute/attribute-table.directive.js
  76. 2
      ui/src/app/entity/attribute/attribute-table.tpl.html
  77. 106
      ui/src/app/entity/entity-aliases.tpl.html
  78. 205
      ui/src/app/entity/entity-filter-view.directive.js
  79. 33
      ui/src/app/entity/entity-filter-view.scss
  80. 10
      ui/src/app/entity/entity-filter-view.tpl.html
  81. 232
      ui/src/app/entity/entity-filter.directive.js
  82. 31
      ui/src/app/entity/entity-filter.scss
  83. 262
      ui/src/app/entity/entity-filter.tpl.html
  84. 130
      ui/src/app/entity/entity-list.directive.js
  85. 30
      ui/src/app/entity/entity-list.scss
  86. 52
      ui/src/app/entity/entity-list.tpl.html
  87. 3
      ui/src/app/entity/entity-subtype-autocomplete.directive.js
  88. 146
      ui/src/app/entity/entity-subtype-list.directive.js
  89. 30
      ui/src/app/entity/entity-subtype-list.scss
  90. 54
      ui/src/app/entity/entity-subtype-list.tpl.html
  91. 111
      ui/src/app/entity/entity-type-list.directive.js
  92. 30
      ui/src/app/entity/entity-type-list.scss
  93. 54
      ui/src/app/entity/entity-type-list.tpl.html
  94. 35
      ui/src/app/entity/entity-type-select.directive.js
  95. 18
      ui/src/app/entity/index.js
  96. 85
      ui/src/app/entity/relation/relation-filters.directive.js
  97. 77
      ui/src/app/entity/relation/relation-filters.scss
  98. 67
      ui/src/app/entity/relation/relation-filters.tpl.html
  99. 4
      ui/src/app/entity/relation/relation-table.directive.js
  100. 2
      ui/src/app/entity/relation/relation-type-autocomplete.directive.js

31
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -22,10 +22,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmId;
import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.alarm.*;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.common.data.page.TextPageData;
@ -60,6 +57,19 @@ public class AlarmController extends BaseController {
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/info/{alarmId}", method = RequestMethod.GET)
@ResponseBody
public AlarmInfo getAlarmInfoById(@PathVariable("alarmId") String strAlarmId) throws ThingsboardException {
checkParameter("alarmId", strAlarmId);
try {
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
return checkAlarmInfoId(alarmId);
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm", method = RequestMethod.POST)
@ResponseBody
@ -103,24 +113,31 @@ public class AlarmController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{entityType}/{entityId}", method = RequestMethod.GET)
@ResponseBody
public TimePageData<Alarm> getAlarms(
public TimePageData<AlarmInfo> getAlarms(
@PathVariable("entityType") String strEntityType,
@PathVariable("entityId") String strEntityId,
@RequestParam(required = false) String searchStatus,
@RequestParam(required = false) String status,
@RequestParam int limit,
@RequestParam(required = false) Long startTime,
@RequestParam(required = false) Long endTime,
@RequestParam(required = false, defaultValue = "false") boolean ascOrder,
@RequestParam(required = false) String offset
@RequestParam(required = false) String offset,
@RequestParam(required = false) Boolean fetchOriginator
) throws ThingsboardException {
checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId);
AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus);
AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status);
if (alarmSearchStatus != null && alarmStatus != null) {
throw new ThingsboardException("Invalid alarms search query: Both parameters 'searchStatus' " +
"and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
checkEntityId(entityId);
try {
TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
return checkNotNull(alarmService.findAlarms(new AlarmQuery(entityId, pageLink, alarmStatus)).get());
return checkNotNull(alarmService.findAlarms(new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get());
} catch (Exception e) {
throw handleException(e);
}

12
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -25,6 +25,7 @@ import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.common.data.*;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmId;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.common.data.page.TextPageLink;
@ -349,6 +350,17 @@ public abstract class BaseController {
}
}
AlarmInfo checkAlarmInfoId(AlarmId alarmId) throws ThingsboardException {
try {
validateId(alarmId, "Incorrect alarmId " + alarmId);
AlarmInfo alarmInfo = alarmService.findAlarmInfoByIdAsync(alarmId).get();
checkAlarm(alarmInfo);
return alarmInfo;
} catch (Exception e) {
throw handleException(e, false);
}
}
protected void checkAlarm(Alarm alarm) throws ThingsboardException {
checkNotNull(alarm);
checkTenantId(alarm.getTenantId());

15
application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java

@ -250,6 +250,21 @@ public class EntityRelationController extends BaseController {
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations/info", method = RequestMethod.POST)
@ResponseBody
public List<EntityRelationInfo> findInfoByQuery(@RequestBody EntityRelationsQuery query) throws ThingsboardException {
checkNotNull(query);
checkNotNull(query.getParameters());
checkNotNull(query.getFilters());
checkEntityId(query.getParameters().getEntityId());
try {
return checkNotNull(relationService.findInfoByQuery(query).get());
} catch (Exception e) {
throw handleException(e);
}
}
private RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) {
RelationTypeGroup result = defaultValue;
if (strRelationTypeGroup != null && strRelationTypeGroup.trim().length()>0) {

16
common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java

@ -53,6 +53,22 @@ public class Alarm extends BaseData<AlarmId> implements HasName {
super(id);
}
public Alarm(Alarm alarm) {
super(alarm.getId());
this.createdTime = alarm.getCreatedTime();
this.tenantId = alarm.getTenantId();
this.type = alarm.getType();
this.originator = alarm.getOriginator();
this.severity = alarm.getSeverity();
this.status = alarm.getStatus();
this.startTs = alarm.getStartTs();
this.endTs = alarm.getEndTs();
this.ackTs = alarm.getAckTs();
this.clearTs = alarm.getClearTs();
this.details = alarm.getDetails();
this.propagate = alarm.isPropagate();
}
@Override
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getName() {

58
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java

@ -0,0 +1,58 @@
/**
* 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.
*/
package org.thingsboard.server.common.data.alarm;
public class AlarmInfo extends Alarm {
private static final long serialVersionUID = 2807343093519543363L;
private String originatorName;
public AlarmInfo() {
super();
}
public AlarmInfo(Alarm alarm) {
super(alarm);
}
public String getOriginatorName() {
return originatorName;
}
public void setOriginatorName(String originatorName) {
this.originatorName = originatorName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
AlarmInfo alarmInfo = (AlarmInfo) o;
return originatorName != null ? originatorName.equals(alarmInfo.originatorName) : alarmInfo.originatorName == null;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + (originatorName != null ? originatorName.hashCode() : 0);
return result;
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java

@ -32,6 +32,8 @@ public class AlarmQuery {
private EntityId affectedEntityId;
private TimePageLink pageLink;
private AlarmSearchStatus searchStatus;
private AlarmStatus status;
private Boolean fetchOriginator;
}

18
ui/src/app/entity/aliases-entity-select-panel.controller.js → common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java

@ -1,4 +1,4 @@
/*
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -14,18 +14,10 @@
* limitations under the License.
*/
/*@ngInject*/
export default function AliasesEntitySelectPanelController(mdPanelRef, $scope, types, entityAliases, entityAliasesInfo, onEntityAliasesUpdate) {
package org.thingsboard.server.common.data.alarm;
var vm = this;
vm._mdPanelRef = mdPanelRef;
vm.entityAliases = entityAliases;
vm.entityAliasesInfo = entityAliasesInfo;
vm.onEntityAliasesUpdate = onEntityAliasesUpdate;
public enum AlarmSearchStatus {
ANY, ACTIVE, CLEARED, ACK, UNACK
$scope.$watch('vm.entityAliases', function () {
if (onEntityAliasesUpdate) {
onEntityAliasesUpdate(vm.entityAliases);
}
}, true);
}

9
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java

@ -30,4 +30,13 @@ public enum AlarmStatus {
return this == CLEARED_ACK || this == CLEARED_UNACK;
}
public AlarmSearchStatus getClearSearchStatus() {
return this.isCleared() ? AlarmSearchStatus.CLEARED : AlarmSearchStatus.ACTIVE;
}
public AlarmSearchStatus getAckSearchStatus() {
return this.isAck() ? AlarmSearchStatus.ACK : AlarmSearchStatus.UNACK;
}
}

3
dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.alarm;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@ -36,5 +37,5 @@ public interface AlarmDao extends Dao<Alarm> {
Alarm save(Alarm alarm);
ListenableFuture<List<Alarm>> findAlarms(AlarmQuery query);
ListenableFuture<List<AlarmInfo>> findAlarms(AlarmQuery query);
}

5
dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.alarm;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmId;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.page.TimePageData;
@ -34,6 +35,8 @@ public interface AlarmService {
ListenableFuture<Alarm> findAlarmByIdAsync(AlarmId alarmId);
ListenableFuture<TimePageData<Alarm>> findAlarms(AlarmQuery query);
ListenableFuture<AlarmInfo> findAlarmInfoByIdAsync(AlarmId alarmId);
ListenableFuture<TimePageData<AlarmInfo>> findAlarms(AlarmQuery query);
}

96
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.alarm;
import com.google.common.base.Function;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
@ -24,15 +25,13 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmId;
import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.alarm.*;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.relation.EntityRelationsQuery;
import org.thingsboard.server.dao.relation.EntitySearchDirection;
@ -44,6 +43,7 @@ import org.thingsboard.server.dao.tenant.TenantDao;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@ -57,7 +57,6 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
public class BaseAlarmService extends AbstractEntityService implements AlarmService {
public static final String ALARM_RELATION_PREFIX = "ALARM_";
public static final String ALARM_RELATION = "ALARM_ANY";
@Autowired
private AlarmDao alarmDao;
@ -68,6 +67,9 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
@Autowired
private RelationService relationService;
@Autowired
private EntityService entityService;
protected ExecutorService readResultsProcessingExecutor;
@PostConstruct
@ -114,11 +116,9 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
query.setParameters(new RelationsSearchParameters(saved.getOriginator(), EntitySearchDirection.TO, Integer.MAX_VALUE));
List<EntityId> parentEntities = relationService.findByQuery(query).get().stream().map(r -> r.getFrom()).collect(Collectors.toList());
for (EntityId parentId : parentEntities) {
createRelation(new EntityRelation(parentId, saved.getId(), ALARM_RELATION, RelationTypeGroup.ALARM));
createRelation(new EntityRelation(parentId, saved.getId(), ALARM_RELATION_PREFIX + saved.getStatus().name(), RelationTypeGroup.ALARM));
createAlarmRelation(parentId, saved.getId(), saved.getStatus(), true);
}
createRelation(new EntityRelation(alarm.getOriginator(), saved.getId(), ALARM_RELATION, RelationTypeGroup.ALARM));
createRelation(new EntityRelation(alarm.getOriginator(), saved.getId(), ALARM_RELATION_PREFIX + saved.getStatus().name(), RelationTypeGroup.ALARM));
createAlarmRelation(alarm.getOriginator(), saved.getId(), saved.getStatus(), true);
return saved;
}
@ -197,12 +197,44 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
}
@Override
public ListenableFuture<TimePageData<Alarm>> findAlarms(AlarmQuery query) {
ListenableFuture<List<Alarm>> alarms = alarmDao.findAlarms(query);
return Futures.transform(alarms, new Function<List<Alarm>, TimePageData<Alarm>>() {
public ListenableFuture<AlarmInfo> findAlarmInfoByIdAsync(AlarmId alarmId) {
log.trace("Executing findAlarmInfoByIdAsync [{}]", alarmId);
validateId(alarmId, "Incorrect alarmId " + alarmId);
return Futures.transform(alarmDao.findAlarmByIdAsync(alarmId.getId()),
(AsyncFunction<Alarm, AlarmInfo>) alarm1 -> {
AlarmInfo alarmInfo = new AlarmInfo(alarm1);
return Futures.transform(
entityService.fetchEntityNameAsync(alarmInfo.getOriginator()), (Function<String, AlarmInfo>)
originatorName -> {
alarmInfo.setOriginatorName(originatorName);
return alarmInfo;
}
);
});
}
@Override
public ListenableFuture<TimePageData<AlarmInfo>> findAlarms(AlarmQuery query) {
ListenableFuture<List<AlarmInfo>> alarms = alarmDao.findAlarms(query);
if (query.getFetchOriginator() != null && query.getFetchOriginator().booleanValue()) {
alarms = Futures.transform(alarms, (AsyncFunction<List<AlarmInfo>, List<AlarmInfo>>) input -> {
List<ListenableFuture<AlarmInfo>> alarmFutures = new ArrayList<>(input.size());
for (AlarmInfo alarmInfo : input) {
alarmFutures.add(Futures.transform(
entityService.fetchEntityNameAsync(alarmInfo.getOriginator()), (Function<String, AlarmInfo>)
originatorName -> {
alarmInfo.setOriginatorName(originatorName);
return alarmInfo;
}
));
}
return Futures.successfulAsList(alarmFutures);
});
}
return Futures.transform(alarms, new Function<List<AlarmInfo>, TimePageData<AlarmInfo>>() {
@Nullable
@Override
public TimePageData<Alarm> apply(@Nullable List<Alarm> alarms) {
public TimePageData<AlarmInfo> apply(@Nullable List<AlarmInfo> alarms) {
return new TimePageData<>(alarms, query.getPageLink());
}
});
@ -243,17 +275,45 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
query.setParameters(new RelationsSearchParameters(alarm.getOriginator(), EntitySearchDirection.TO, Integer.MAX_VALUE));
List<EntityId> parentEntities = relationService.findByQuery(query).get().stream().map(r -> r.getFrom()).collect(Collectors.toList());
for (EntityId parentId : parentEntities) {
deleteRelation(new EntityRelation(parentId, alarm.getId(), ALARM_RELATION_PREFIX + oldStatus.name(), RelationTypeGroup.ALARM));
createRelation(new EntityRelation(parentId, alarm.getId(), ALARM_RELATION_PREFIX + newStatus.name(), RelationTypeGroup.ALARM));
}
deleteRelation(new EntityRelation(alarm.getOriginator(), alarm.getId(), ALARM_RELATION_PREFIX + oldStatus.name(), RelationTypeGroup.ALARM));
createRelation(new EntityRelation(alarm.getOriginator(), alarm.getId(), ALARM_RELATION_PREFIX + newStatus.name(), RelationTypeGroup.ALARM));
updateAlarmRelation(parentId, alarm.getId(), oldStatus, newStatus);
}
updateAlarmRelation(alarm.getOriginator(), alarm.getId(), oldStatus, newStatus);
} catch (ExecutionException | InterruptedException e) {
log.warn("[{}] Failed to update relations. Old status: [{}], New status: [{}]", alarm.getId(), oldStatus, newStatus);
throw new RuntimeException(e);
}
}
private void createAlarmRelation(EntityId entityId, EntityId alarmId, AlarmStatus status, boolean createAnyRelation) {
try {
if (createAnyRelation) {
createRelation(new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + AlarmSearchStatus.ANY.name(), RelationTypeGroup.ALARM));
}
createRelation(new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM));
createRelation(new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM));
createRelation(new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM));
} catch (ExecutionException | InterruptedException e) {
log.warn("[{}] Failed to create relation. Status: [{}]", alarmId, status);
throw new RuntimeException(e);
}
}
private void deleteAlarmRelation(EntityId entityId, EntityId alarmId, AlarmStatus status) {
try {
deleteRelation(new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM));
deleteRelation(new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM));
deleteRelation(new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM));
} catch (ExecutionException | InterruptedException e) {
log.warn("[{}] Failed to delete relation. Status: [{}]", alarmId, status);
throw new RuntimeException(e);
}
}
private void updateAlarmRelation(EntityId entityId, EntityId alarmId, AlarmStatus oldStatus, AlarmStatus newStatus) {
deleteAlarmRelation(entityId, alarmId, oldStatus);
createAlarmRelation(entityId, alarmId, newStatus, false);
}
private <T> ListenableFuture<T> getAndUpdate(AlarmId alarmId, Function<Alarm, T> function) {
validateId(alarmId, "Alarm id should be specified!");
ListenableFuture<Alarm> entity = alarmDao.findAlarmByIdAsync(alarmId.getId());

25
dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.alarm;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select;
import com.google.common.base.Function;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@ -26,7 +27,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation;
@ -86,15 +89,25 @@ public class CassandraAlarmDao extends CassandraAbstractModelDao<AlarmEntity, Al
}
@Override
public ListenableFuture<List<Alarm>> findAlarms(AlarmQuery query) {
log.trace("Try to find alarms by entity [{}], status [{}] and pageLink [{}]", query.getAffectedEntityId(), query.getStatus(), query.getPageLink());
public ListenableFuture<List<AlarmInfo>> findAlarms(AlarmQuery query) {
log.trace("Try to find alarms by entity [{}], searchStatus [{}], status [{}] and pageLink [{}]", query.getAffectedEntityId(), query.getSearchStatus(), query.getStatus(), query.getPageLink());
EntityId affectedEntity = query.getAffectedEntityId();
String relationType = query.getStatus() == null ? BaseAlarmService.ALARM_RELATION : BaseAlarmService.ALARM_RELATION_PREFIX + query.getStatus().name();
String searchStatusName;
if (query.getSearchStatus() == null && query.getStatus() == null) {
searchStatusName = AlarmSearchStatus.ANY.name();
} else if (query.getSearchStatus() != null) {
searchStatusName = query.getSearchStatus().name();
} else {
searchStatusName = query.getStatus().name();
}
String relationType = BaseAlarmService.ALARM_RELATION_PREFIX + searchStatusName;
ListenableFuture<List<EntityRelation>> relations = relationDao.findRelations(affectedEntity, relationType, RelationTypeGroup.ALARM, EntityType.ALARM, query.getPageLink());
return Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<Alarm>>) input -> {
List<ListenableFuture<Alarm>> alarmFutures = new ArrayList<>(input.size());
return Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<AlarmInfo>>) input -> {
List<ListenableFuture<AlarmInfo>> alarmFutures = new ArrayList<>(input.size());
for (EntityRelation relation : input) {
alarmFutures.add(findAlarmByIdAsync(relation.getTo().getId()));
alarmFutures.add(Futures.transform(
findAlarmByIdAsync(relation.getTo().getId()), (Function<Alarm, AlarmInfo>)
alarm1 -> new AlarmInfo(alarm1)));
}
return Futures.successfulAsList(alarmFutures);
});

11
dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java

@ -179,9 +179,14 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
eq(ModelConstants.RELATION_TYPE_GROUP_PROPERTY, typeGroup.name()),
eq(ModelConstants.RELATION_TYPE_PROPERTY, relationType),
eq(ModelConstants.RELATION_TO_TYPE_PROPERTY, childType.name())),
Arrays.asList(QueryBuilder.asc(ModelConstants.RELATION_TYPE_GROUP_PROPERTY),
QueryBuilder.asc(ModelConstants.RELATION_TYPE_PROPERTY),
QueryBuilder.asc(ModelConstants.RELATION_TO_TYPE_PROPERTY)),
Arrays.asList(
pageLink.isAscOrder() ? QueryBuilder.desc(ModelConstants.RELATION_TYPE_GROUP_PROPERTY) :
QueryBuilder.asc(ModelConstants.RELATION_TYPE_GROUP_PROPERTY),
pageLink.isAscOrder() ? QueryBuilder.desc(ModelConstants.RELATION_TYPE_PROPERTY) :
QueryBuilder.asc(ModelConstants.RELATION_TYPE_PROPERTY),
pageLink.isAscOrder() ? QueryBuilder.desc(ModelConstants.RELATION_TO_TYPE_PROPERTY) :
QueryBuilder.asc(ModelConstants.RELATION_TO_TYPE_PROPERTY)
),
pageLink, ModelConstants.RELATION_TO_ID_PROPERTY);
return getFuture(executeAsyncRead(query), rs -> getEntityRelations(rs));
}

26
dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java

@ -191,7 +191,7 @@ public class BaseRelationService implements RelationService {
@Override
public ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query) {
log.trace("Executing findByQuery [{}][{}]", query);
log.trace("Executing findByQuery [{}]", query);
RelationsSearchParameters params = query.getParameters();
final List<EntityTypeFilter> filters = query.getFilters();
if (filters == null || filters.isEmpty()) {
@ -224,6 +224,30 @@ public class BaseRelationService implements RelationService {
}
}
@Override
public ListenableFuture<List<EntityRelationInfo>> findInfoByQuery(EntityRelationsQuery query) {
log.trace("Executing findInfoByQuery [{}]", query);
ListenableFuture<List<EntityRelation>> relations = findByQuery(query);
EntitySearchDirection direction = query.getParameters().getDirection();
ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
(AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
relations1.stream().forEach(relation ->
futures.add(fetchRelationInfoAsync(relation,
relation2 -> direction == EntitySearchDirection.FROM ? relation2.getTo() : relation2.getFrom(),
(EntityRelationInfo relationInfo, String entityName) -> {
if (direction == EntitySearchDirection.FROM) {
relationInfo.setToName(entityName);
} else {
relationInfo.setFromName(entityName);
}
}))
);
return Futures.successfulAsList(futures);
});
return relationsInfo;
}
protected void validate(EntityRelation relation) {
if (relation == null) {
throw new DataValidationException("Relation type should be specified!");

2
dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java

@ -52,6 +52,8 @@ public interface RelationService {
ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query);
ListenableFuture<List<EntityRelationInfo>> findInfoByQuery(EntityRelationsQuery query);
// TODO: This method may be useful for some validations in the future
// ListenableFuture<Boolean> checkRecursiveRelation(EntityId from, EntityId to);

7
dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java

@ -22,10 +22,7 @@ import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.alarm.*;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
@ -117,7 +114,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
Alarm created = alarmService.createOrUpdateAlarm(alarm);
// Check child relation
TimePageData<Alarm> alarms = alarmService.findAlarms(AlarmQuery.builder()
TimePageData<AlarmInfo> alarms = alarmService.findAlarms(AlarmQuery.builder()
.affectedEntityId(childId)
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0L, System.currentTimeMillis(), false)

134
ui/src/app/alarm/alarm-details-dialog.controller.js

@ -0,0 +1,134 @@
/*
* 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 'brace/ext/language_tools';
import 'brace/mode/json';
import 'brace/theme/github';
import beautify from 'js-beautify';
import './alarm-details-dialog.scss';
const js_beautify = beautify.js;
/*@ngInject*/
export default function AlarmDetailsDialogController($mdDialog, $filter, $translate, types, alarmService, alarmId, showingCallback) {
var vm = this;
vm.alarmId = alarmId;
vm.types = types;
vm.alarm = null;
vm.alarmUpdated = false;
showingCallback.onShowing = function(scope, element) {
updateEditorSize(element);
}
vm.alarmDetailsOptions = {
useWrapMode: false,
mode: 'json',
showGutter: false,
showPrintMargin: false,
theme: 'github',
advanced: {
enableSnippets: false,
enableBasicAutocompletion: false,
enableLiveAutocompletion: false
},
onLoad: function (_ace) {
vm.editor = _ace;
}
};
vm.close = close;
vm.acknowledge = acknowledge;
vm.clear = clear;
loadAlarm();
function updateEditorSize(element) {
var newWidth = 600;
var newHeight = 200;
angular.element('#tb-alarm-details', element).height(newHeight.toString() + "px")
.width(newWidth.toString() + "px");
vm.editor.resize();
}
function loadAlarm() {
alarmService.getAlarmInfo(vm.alarmId).then(
function success(alarm) {
vm.alarm = alarm;
loadAlarmFields();
},
function fail() {
vm.alarm = null;
}
);
}
function loadAlarmFields() {
vm.createdTime = $filter('date')(vm.alarm.createdTime, 'yyyy-MM-dd HH:mm:ss');
vm.startTime = null;
if (vm.alarm.startTs) {
vm.startTime = $filter('date')(vm.alarm.startTs, 'yyyy-MM-dd HH:mm:ss');
}
vm.endTime = null;
if (vm.alarm.endTs) {
vm.endTime = $filter('date')(vm.alarm.endTs, 'yyyy-MM-dd HH:mm:ss');
}
vm.ackTime = null;
if (vm.alarm.ackTs) {
vm.ackTime = $filter('date')(vm.alarm.ackTs, 'yyyy-MM-dd HH:mm:ss')
}
vm.clearTime = null;
if (vm.alarm.clearTs) {
vm.clearTime = $filter('date')(vm.alarm.clearTs, 'yyyy-MM-dd HH:mm:ss');
}
vm.alarmSeverity = $translate.instant(types.alarmSeverity[vm.alarm.severity].name);
vm.alarmStatus = $translate.instant('alarm.display-status.' + vm.alarm.status);
vm.alarmDetails = null;
if (vm.alarm.details) {
vm.alarmDetails = angular.toJson(vm.alarm.details);
vm.alarmDetails = js_beautify(vm.alarmDetails, {indent_size: 4});
}
}
function acknowledge () {
alarmService.ackAlarm(vm.alarmId).then(
function success() {
vm.alarmUpdated = true;
loadAlarm();
}
);
}
function clear () {
alarmService.clearAlarm(vm.alarmId).then(
function success() {
vm.alarmUpdated = true;
loadAlarm();
}
);
}
function close () {
$mdDialog.hide(vm.alarmUpdated ? vm.alarm : null);
}
}

27
ui/src/app/alarm/alarm-details-dialog.scss

@ -0,0 +1,27 @@
/**
* 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-alarm-details-panel {
margin-left: 15px;
border: 1px solid #C0C0C0;
height: 100%;
#tb-alarm-details {
min-width: 600px;
min-height: 200px;
width: 100%;
height: 100%;
}
}

107
ui/src/app/alarm/alarm-details-dialog.tpl.html

@ -0,0 +1,107 @@
<!--
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-dialog aria-label="{{ 'alarm.alarm-details' | translate }}">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>alarm.alarm-details</h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.close()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-dialog-content>
<div class="md-dialog-content" layout="column">
<div layout="row">
<md-input-container class="md-block">
<label translate>alarm.created-time</label>
<input ng-model="vm.createdTime" readonly>
</md-input-container>
<md-input-container flex class="md-block">
<label translate>alarm.originator</label>
<input ng-model="vm.alarm.originatorName" readonly>
</md-input-container>
</div>
<div layout="row" ng-if="vm.startTime || vm.endTime">
<md-input-container ng-if="vm.startTime" flex class="md-block">
<label translate>alarm.start-time</label>
<input ng-model="vm.startTime" readonly>
</md-input-container>
<md-input-container ng-if="vm.endTime" flex class="md-block">
<label translate>alarm.end-time</label>
<input ng-model="vm.endTime" readonly>
</md-input-container>
<span flex ng-if="!vm.startTime || !vm.endTime"></span>
</div>
<div layout="row" ng-if="vm.ackTime || vm.clearTime">
<md-input-container ng-if="vm.ackTime" flex class="md-block">
<label translate>alarm.ack-time</label>
<input ng-model="vm.ackTime" readonly>
</md-input-container>
<md-input-container ng-if="vm.clearTime" flex class="md-block">
<label translate>alarm.clear-time</label>
<input ng-model="vm.clearTime" readonly>
</md-input-container>
<span flex ng-if="!vm.ackTime || !vm.clearTime"></span>
</div>
<div layout="row">
<md-input-container flex class="md-block">
<label translate>alarm.type</label>
<input ng-model="vm.alarm.type" readonly>
</md-input-container>
<md-input-container flex class="md-block">
<label translate>alarm.severity</label>
<input class="tb-severity" ng-class="vm.types.alarmSeverity[vm.alarm.severity].class"
ng-model="vm.alarmSeverity" readonly>
</md-input-container>
<md-input-container flex class="md-block">
<label translate>alarm.status</label>
<input ng-model="vm.alarmStatus" readonly>
</md-input-container>
</div>
<div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>alarm.details</div>
<div flex class="tb-alarm-details-panel" layout="column">
<div flex id="tb-alarm-details" readonly
ui-ace="vm.alarmDetailsOptions"
ng-model="vm.alarmDetails">
</div>
</div>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<md-button ng-if="vm.alarm.status==vm.types.alarmStatus.activeUnack ||
vm.alarm.status==vm.types.alarmStatus.clearedUnack"
class="md-raised md-primary"
ng-disabled="loading"
ng-click="vm.acknowledge()"
style="margin-right:20px;">{{ 'alarm.acknowledge' |
translate }}
</md-button>
<md-button ng-if="vm.alarm.status==vm.types.alarmStatus.activeAck ||
vm.alarm.status==vm.types.alarmStatus.activeUnack"
class="md-raised md-primary"
ng-disabled="loading"
ng-click="vm.clear()">{{ 'alarm.clear' |
translate }}
</md-button>
<span flex></span>
<md-button ng-disabled="loading" ng-click="vm.close()" style="margin-right:20px;">{{ 'action.close' |
translate }}
</md-button>
</md-dialog-actions>
</md-dialog>

39
ui/src/app/alarm/alarm-header.directive.js

@ -0,0 +1,39 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable import/no-unresolved, import/default */
import alarmHeaderTemplate from './alarm-header.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function AlarmHeaderDirective($compile, $templateCache) {
var linker = function (scope, element) {
var template = $templateCache.get(alarmHeaderTemplate);
element.html(template);
$compile(element.contents())(scope);
}
return {
restrict: "A",
replace: false,
link: linker,
scope: false
};
}

23
ui/src/app/alarm/alarm-header.tpl.html

@ -0,0 +1,23 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div translate class="tb-cell" flex="30">alarm.created-time</div>
<div translate class="tb-cell" flex="15">alarm.originator</div>
<div translate class="tb-cell" flex="20">alarm.type</div>
<div translate class="tb-cell" flex="15">alarm.severity</div>
<div translate class="tb-cell" flex="20">alarm.status</div>
<div translate class="tb-cell" flex="15">alarm.details</div>

67
ui/src/app/alarm/alarm-row.directive.js

@ -0,0 +1,67 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable import/no-unresolved, import/default */
import alarmDetailsDialogTemplate from './alarm-details-dialog.tpl.html';
import alarmRowTemplate from './alarm-row.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function AlarmRowDirective($compile, $templateCache, types, $mdDialog, $document) {
var linker = function (scope, element, attrs) {
var template = $templateCache.get(alarmRowTemplate);
element.html(template);
scope.alarm = attrs.alarm;
scope.types = types;
scope.showAlarmDetails = function($event) {
var onShowingCallback = {
onShowing: function(){}
}
$mdDialog.show({
controller: 'AlarmDetailsDialogController',
controllerAs: 'vm',
templateUrl: alarmDetailsDialogTemplate,
locals: {alarmId: scope.alarm.id.id, showingCallback: onShowingCallback},
parent: angular.element($document[0].body),
targetEvent: $event,
fullscreen: true,
skipHide: true,
onShowing: function(scope, element) {
onShowingCallback.onShowing(scope, element);
}
}).then(function (alarm) {
if (alarm) {
scope.alarm = alarm;
}
});
}
$compile(element.contents())(scope);
}
return {
restrict: "A",
replace: false,
link: linker,
scope: false
};
}

17
ui/src/app/event/event-row-alarm.tpl.html → ui/src/app/alarm/alarm-row.tpl.html

@ -15,14 +15,19 @@
limitations under the License.
-->
<div class="tb-cell" flex="30">{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div>
<div class="tb-cell" flex="20">{{event.body.server}}</div>
<div class="tb-cell" flex="20">
<md-button ng-if="event.body.body" class="md-icon-button md-primary"
ng-click="showContent($event, event.body.body, 'event.alarm')"
<div class="tb-cell" flex="30">{{alarm.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}</div>
<div class="tb-cell" flex="15">{{alarm.originatorName}}</div>
<div class="tb-cell" flex="20">{{alarm.type}}</div>
<div class="tb-cell tb-severity" flex="15" ng-class="types.alarmSeverity[alarm.severity].class">
{{ alarm ? (types.alarmSeverity[alarm.severity].name | translate) : '' }}
</div>
<div class="tb-cell" flex="20">{{ alarm ? (('alarm.display-status.' + alarm.status) | translate) : '' }}</div>
<div class="tb-cell" flex="15">
<md-button class="md-icon-button md-primary"
ng-click="showAlarmDetails($event)"
aria-label="{{ 'action.view' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.view' | translate }}
{{ 'alarm.details' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.view' | translate }}"
class="material-icons">

211
ui/src/app/alarm/alarm-table.directive.js

@ -0,0 +1,211 @@
/*
* 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 './alarm.scss';
/* eslint-disable import/no-unresolved, import/default */
import alarmTableTemplate from './alarm-table.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function AlarmTableDirective($compile, $templateCache, $rootScope, types, alarmService) {
var linker = function (scope, element) {
var template = $templateCache.get(alarmTableTemplate);
element.html(template);
scope.types = types;
scope.alarmSearchStatus = types.alarmSearchStatus.any;
var pageSize = 20;
var startTime = 0;
var endTime = 0;
scope.timewindow = {
history: {
timewindowMs: 24 * 60 * 60 * 1000 // 1 day
}
}
scope.topIndex = 0;
scope.theAlarms = {
getItemAtIndex: function (index) {
if (index > scope.alarms.data.length) {
scope.theAlarms.fetchMoreItems_(index);
return null;
}
var item = scope.alarms.data[index];
if (item) {
item.indexNumber = index + 1;
}
return item;
},
getLength: function () {
if (scope.alarms.hasNext) {
return scope.alarms.data.length + scope.alarms.nextPageLink.limit;
} else {
return scope.alarms.data.length;
}
},
fetchMoreItems_: function () {
if (scope.alarms.hasNext && !scope.alarms.pending) {
if (scope.entityType && scope.entityId && scope.alarmSearchStatus) {
var promise = alarmService.getAlarms(scope.entityType, scope.entityId,
scope.alarms.nextPageLink, scope.alarmSearchStatus, null, true, false);
if (promise) {
scope.alarms.pending = true;
promise.then(
function success(alarms) {
scope.alarms.data = scope.alarms.data.concat(alarms.data);
scope.alarms.nextPageLink = alarms.nextPageLink;
scope.alarms.hasNext = alarms.hasNext;
if (scope.alarms.hasNext) {
scope.alarms.nextPageLink.limit = pageSize;
}
scope.alarms.pending = false;
},
function fail() {
scope.alarms.hasNext = false;
scope.alarms.pending = false;
});
} else {
scope.alarms.hasNext = false;
}
} else {
scope.alarms.hasNext = false;
}
}
}
};
scope.$watch("entityId", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) {
resetFilter();
reload();
}
});
function destroyWatchers() {
if (scope.alarmSearchStatusWatchHandle) {
scope.alarmSearchStatusWatchHandle();
scope.alarmSearchStatusWatchHandle = null;
}
if (scope.timewindowWatchHandle) {
scope.timewindowWatchHandle();
scope.timewindowWatchHandle = null;
}
}
function initWatchers() {
scope.alarmSearchStatusWatchHandle = scope.$watch("alarmSearchStatus", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) {
reload();
}
});
scope.timewindowWatchHandle = scope.$watch("timewindow", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) {
reload();
}
}, true);
}
function resetFilter() {
destroyWatchers();
scope.timewindow = {
history: {
timewindowMs: 24 * 60 * 60 * 1000 // 1 day
}
};
scope.alarmSearchStatus = types.alarmSearchStatus.any;
initWatchers();
}
function updateTimeWindowRange () {
if (scope.timewindow.history.timewindowMs) {
var currentTime = (new Date).getTime();
startTime = currentTime - scope.timewindow.history.timewindowMs;
endTime = currentTime;
} else {
startTime = scope.timewindow.history.fixedTimewindow.startTimeMs;
endTime = scope.timewindow.history.fixedTimewindow.endTimeMs;
}
}
function reload () {
scope.topIndex = 0;
scope.selected = [];
updateTimeWindowRange();
scope.alarms = {
data: [],
nextPageLink: {
limit: pageSize,
startTime: startTime,
endTime: endTime
},
hasNext: true,
pending: false
};
scope.theAlarms.getItemAtIndex(pageSize);
}
scope.noData = function() {
return scope.alarms.data.length == 0 && !scope.alarms.hasNext;
}
scope.hasData = function() {
return scope.alarms.data.length > 0;
}
scope.loading = function() {
return $rootScope.loading;
}
scope.hasScroll = function() {
var repeatContainer = scope.repeatContainer[0];
if (repeatContainer) {
var scrollElement = repeatContainer.children[0];
if (scrollElement) {
return scrollElement.scrollHeight > scrollElement.clientHeight;
}
}
return false;
}
reload();
initWatchers();
$compile(element.contents())(scope);
}
return {
restrict: "E",
link: linker,
scope: {
entityType: '=',
entityId: '='
}
};
}

49
ui/src/app/alarm/alarm-table.tpl.html

@ -0,0 +1,49 @@
<!--
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-content flex class="md-padding tb-absolute-fill" layout="column">
<section layout="row">
<md-input-container class="md-block" style="width: 200px;">
<label translate>alarm.alarm-status</label>
<md-select ng-model="alarmSearchStatus" ng-disabled="loading()">
<md-option ng-repeat="searchStatus in types.alarmSearchStatus" ng-value="searchStatus">
{{ ('alarm.search-status.' + searchStatus) | translate }}
</md-option>
</md-select>
</md-input-container>
<tb-timewindow flex ng-model="timewindow" history-only as-button="true"></tb-timewindow>
</section>
<div flex layout="column" class="tb-alarm-container md-whiteframe-z1">
<md-list flex layout="column" class="tb-alarm-table">
<md-list class="tb-row tb-header" layout="row" tb-alarm-header>
</md-list>
<md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!loading()"
ng-show="loading()"></md-progress-linear>
<md-divider></md-divider>
<span translate layout-align="center center"
style="margin-top: 25px;"
class="tb-prompt" ng-show="noData()">alarm.no-alarms-prompt</span>
<md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer">
<md-list-item md-virtual-repeat="alarm in theAlarms" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}">
<md-list class="tb-row" flex layout="row" tb-alarm-row alarm="{{alarm}}">
</md-list>
<md-divider flex></md-divider>
</md-list-item>
</md-virtual-repeat-container>
</md-list>
</div>
</md-content>

78
ui/src/app/alarm/alarm.scss

@ -0,0 +1,78 @@
/**
* 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-alarm-container {
overflow-x: auto;
}
md-list.tb-alarm-table {
padding: 0px;
min-width: 700px;
md-list-item {
padding: 0px;
}
.tb-row {
height: 48px;
padding: 0px;
overflow: hidden;
}
.tb-row:hover {
background-color: #EEEEEE;
}
.tb-header:hover {
background: none;
}
.tb-header {
.tb-cell {
color: rgba(0,0,0,.54);
font-size: 12px;
font-weight: 700;
white-space: nowrap;
background: none;
}
}
.tb-cell {
padding: 0 24px;
margin: auto 0;
color: rgba(0,0,0,.87);
font-size: 13px;
vertical-align: middle;
text-align: left;
overflow: hidden;
.md-button {
padding: 0;
margin: 0;
}
}
.tb-cell.tb-number {
text-align: right;
}
}
#tb-alarm-content {
min-width: 400px;
min-height: 50px;
width: 100%;
height: 100%;
}

27
ui/src/app/alarm/index.js

@ -0,0 +1,27 @@
/*
* 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 AlarmDetailsDialogController from './alarm-details-dialog.controller';
import AlarmHeaderDirective from './alarm-header.directive';
import AlarmRowDirective from './alarm-row.directive';
import AlarmTableDirective from './alarm-table.directive';
export default angular.module('thingsboard.alarm', [])
.controller('AlarmDetailsDialogController', AlarmDetailsDialogController)
.directive('tbAlarmHeader', AlarmHeaderDirective)
.directive('tbAlarmRow', AlarmRowDirective)
.directive('tbAlarmTable', AlarmTableDirective)
.name;

29
ui/src/app/api/alarm.service.js

@ -21,6 +21,7 @@ export default angular.module('thingsboard.api.alarm', [])
function AlarmService($http, $q, $interval, $filter) {
var service = {
getAlarm: getAlarm,
getAlarmInfo: getAlarmInfo,
saveAlarm: saveAlarm,
ackAlarm: ackAlarm,
clearAlarm: clearAlarm,
@ -46,6 +47,21 @@ function AlarmService($http, $q, $interval, $filter) {
return deferred.promise;
}
function getAlarmInfo(alarmId, ignoreErrors, config) {
var deferred = $q.defer();
var url = '/api/alarm/info/' + alarmId;
if (!config) {
config = {};
}
config = Object.assign(config, { ignoreErrors: ignoreErrors });
$http.get(url, config).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function saveAlarm(alarm, ignoreErrors, config) {
var deferred = $q.defer();
var url = '/api/alarm';
@ -91,7 +107,7 @@ function AlarmService($http, $q, $interval, $filter) {
return deferred.promise;
}
function getAlarms(entityType, entityId, pageLink, alarmStatus, ascOrder, config) {
function getAlarms(entityType, entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator, ascOrder, config) {
var deferred = $q.defer();
var url = '/api/alarm/' + entityType + '/' + entityId + '?limit=' + pageLink.limit;
@ -104,9 +120,15 @@ function AlarmService($http, $q, $interval, $filter) {
if (angular.isDefined(pageLink.idOffset)) {
url += '&offset=' + pageLink.idOffset;
}
if (alarmSearchStatus) {
url += '&searchStatus=' + alarmSearchStatus;
}
if (alarmStatus) {
url += '&status=' + alarmStatus;
}
if (fetchOriginator) {
url += '&fetchOriginator=' + ((fetchOriginator===true) ? 'true' : 'false');
}
if (angular.isDefined(ascOrder) && ascOrder != null) {
url += '&ascOrder=' + (ascOrder ? 'true' : 'false');
}
@ -121,7 +143,8 @@ function AlarmService($http, $q, $interval, $filter) {
function fetchAlarms(alarmsQuery, pageLink, deferred, alarmsList) {
getAlarms(alarmsQuery.entityType, alarmsQuery.entityId,
pageLink, alarmsQuery.alarmStatus, false, {ignoreLoading: true}).then(
pageLink, alarmsQuery.alarmSearchStatus, alarmsQuery.alarmStatus,
alarmsQuery.fetchOriginator, false, {ignoreLoading: true}).then(
function success(alarms) {
if (!alarmsList) {
alarmsList = [];
@ -171,7 +194,9 @@ function AlarmService($http, $q, $interval, $filter) {
var alarmsQuery = {
entityType: entityType,
entityId: entityId,
alarmSearchStatus: null,
alarmStatus: alarmStatus,
fetchOriginator: false,
interval: interval,
limit: limit,
onAlarms: onAlarms

279
ui/src/app/api/alias-controller.js

@ -0,0 +1,279 @@
/*
* 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.
*/
const varsRegex = /\$\{([^\}]*)\}/g;
export default class AliasController {
constructor($scope, $q, $filter, utils, types, entityService, stateController, entityAliases) {
this.$scope = $scope;
this.$q = $q;
this.$filter = $filter;
this.utils = utils;
this.types = types;
this.entityService = entityService;
this.stateController = stateController;
this.entityAliases = angular.copy(entityAliases);
this.resolvedAliases = {};
this.resolvedAliasesPromise = {};
this.resolvedAliasesToStateEntities = {};
}
updateEntityAliases(newEntityAliases) {
var changedAliasIds = [];
for (var aliasId in newEntityAliases) {
var newEntityAlias = newEntityAliases[aliasId];
var prevEntityAlias = this.entityAliases[aliasId];
if (!angular.equals(newEntityAlias, prevEntityAlias)) {
changedAliasIds.push(aliasId);
this.setAliasUnresolved(aliasId);
}
}
for (aliasId in this.entityAliases) {
if (!newEntityAliases[aliasId]) {
changedAliasIds.push(aliasId);
this.setAliasUnresolved(aliasId);
}
}
this.entityAliases = angular.copy(newEntityAliases);
if (changedAliasIds.length) {
this.$scope.$broadcast('entityAliasesChanged', changedAliasIds);
}
}
dashboardStateChanged() {
var newEntityId = this.stateController.getStateParams().entityId;
var changedAliasIds = [];
for (var aliasId in this.resolvedAliasesToStateEntities) {
var prevEntityId = this.resolvedAliasesToStateEntities[aliasId];
if (!angular.equals(newEntityId, prevEntityId)) {
changedAliasIds.push(aliasId);
this.setAliasUnresolved(aliasId);
}
}
if (changedAliasIds.length) {
this.$scope.$broadcast('entityAliasesChanged', changedAliasIds);
}
}
setAliasUnresolved(aliasId) {
delete this.resolvedAliases[aliasId];
delete this.resolvedAliasesPromise[aliasId];
delete this.resolvedAliasesToStateEntities[aliasId];
}
getEntityAliases() {
return this.entityAliases;
}
getAliasInfo(aliasId) {
var deferred = this.$q.defer();
var aliasInfo = this.resolvedAliases[aliasId];
if (aliasInfo) {
deferred.resolve(aliasInfo);
return deferred.promise;
} else if (this.resolvedAliasesPromise[aliasId]) {
return this.resolvedAliasesPromise[aliasId];
} else {
this.resolvedAliasesPromise[aliasId] = deferred.promise;
var aliasCtrl = this;
var entityAlias = this.entityAliases[aliasId];
if (entityAlias) {
this.entityService.resolveAlias(entityAlias, this.stateController.getStateParams()).then(
function success(aliasInfo) {
aliasCtrl.resolvedAliases[aliasId] = aliasInfo;
if (aliasInfo.stateEntity) {
aliasCtrl.resolvedAliasesToStateEntities[aliasId] =
aliasCtrl.stateController.getStateParams().entityId;
}
aliasCtrl.$scope.$broadcast('entityAliasResolved', aliasId);
deferred.resolve(aliasInfo);
},
function fail() {
deferred.reject();
}
);
} else {
deferred.reject();
}
return this.resolvedAliasesPromise[aliasId];
}
}
resolveDatasource(datasource) {
var deferred = this.$q.defer();
if (datasource.type === this.types.datasourceType.entity) {
if (datasource.entityAliasId) {
this.getAliasInfo(datasource.entityAliasId).then(
function success(aliasInfo) {
datasource.aliasName = aliasInfo.alias;
if (aliasInfo.resolveMultiple) {
var newDatasource;
var resolvedEntities = aliasInfo.resolvedEntities;
if (resolvedEntities && resolvedEntities.length) {
var datasources = [];
for (var i=0;i<resolvedEntities.length;i++) {
var resolvedEntity = resolvedEntities[i];
newDatasource = angular.copy(datasource);
newDatasource.entityId = resolvedEntity.id;
newDatasource.entityType = resolvedEntity.entityType;
newDatasource.entityName = resolvedEntity.name;
newDatasource.name = resolvedEntity.name;
newDatasource.generated = i > 0 ? true : false;
datasources.push(newDatasource);
}
deferred.resolve(datasources);
} else {
if (aliasInfo.stateEntity) {
newDatasource = angular.copy(datasource);
newDatasource.unresolvedStateEntity = true;
deferred.resolve([newDatasource]);
} else {
deferred.reject();
}
}
} else {
var entity = aliasInfo.currentEntity;
if (entity) {
datasource.entityId = entity.id;
datasource.entityType = entity.entityType;
datasource.entityName = entity.name;
datasource.name = entity.name;
deferred.resolve([datasource]);
} else {
if (aliasInfo.stateEntity) {
datasource.unresolvedStateEntity = true;
deferred.resolve([datasource]);
} else {
deferred.reject();
}
}
}
},
function fail() {
deferred.reject();
}
);
} else { // entityId
datasource.aliasName = datasource.entityName;
datasource.name = datasource.entityName;
deferred.resolve([datasource]);
}
} else { // function
deferred.resolve([datasource]);
}
return deferred.promise;
}
resolveDatasources(datasources) {
function updateDataKeyLabel(dataKey, datasource) {
if (!dataKey.pattern) {
dataKey.pattern = angular.copy(dataKey.label);
}
var pattern = dataKey.pattern;
var label = dataKey.pattern;
var match = varsRegex.exec(pattern);
while (match !== null) {
var variable = match[0];
var variableName = match[1];
if (variableName === 'dsName') {
label = label.split(variable).join(datasource.name);
} else if (variableName === 'entityName') {
label = label.split(variable).join(datasource.entityName);
} else if (variableName === 'deviceName') {
label = label.split(variable).join(datasource.entityName);
} else if (variableName === 'aliasName') {
label = label.split(variable).join(datasource.aliasName);
}
match = varsRegex.exec(pattern);
}
dataKey.label = label;
}
function updateDatasourceKeyLabels(datasource) {
for (var dk = 0; dk < datasource.dataKeys.length; dk++) {
updateDataKeyLabel(datasource.dataKeys[dk], datasource);
}
}
var deferred = this.$q.defer();
var newDatasources = angular.copy(datasources);
var datasorceResolveTasks = [];
var aliasCtrl = this;
newDatasources.forEach(function (datasource) {
var resolveDatasourceTask = aliasCtrl.resolveDatasource(datasource);
datasorceResolveTasks.push(resolveDatasourceTask);
});
this.$q.all(datasorceResolveTasks).then(
function success(datasourcesArrays) {
var datasources = [].concat.apply([], datasourcesArrays);
datasources = aliasCtrl.$filter('orderBy')(datasources, '+generated');
var index = 0;
var functionIndex = 0;
datasources.forEach(function(datasource) {
if (datasource.type === aliasCtrl.types.datasourceType.function) {
var name;
if (datasource.name && datasource.name.length) {
name = datasource.name;
} else {
functionIndex++;
name = aliasCtrl.types.datasourceType.function;
if (functionIndex > 1) {
name += ' ' + functionIndex;
}
}
datasource.name = name;
datasource.aliasName = name;
datasource.entityName = name;
} else if (datasource.unresolvedStateEntity) {
datasource.name = "Unresolved";
datasource.entityName = "Unresolved";
}
datasource.dataKeys.forEach(function(dataKey) {
if (datasource.generated) {
dataKey._hash = Math.random();
dataKey.color = aliasCtrl.utils.getMaterialColor(index);
}
index++;
});
updateDatasourceKeyLabels(datasource);
});
deferred.resolve(datasources);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
getInstantAliasInfo(aliasId) {
return this.resolvedAliases[aliasId];
}
updateCurrentAliasEntity(aliasId, currentEntity) {
var aliasInfo = this.resolvedAliases[aliasId];
if (aliasInfo) {
var prevCurrentEntity = aliasInfo.currentEntity;
if (!angular.equals(currentEntity, prevCurrentEntity)) {
aliasInfo.currentEntity = currentEntity;
this.$scope.$broadcast('entityAliasesChanged', [aliasId]);
}
}
}
}

14
ui/src/app/api/entity-relation.service.js

@ -30,7 +30,8 @@ function EntityRelationService($http, $q) {
findByTo: findByTo,
findInfoByTo: findInfoByTo,
findByToAndType: findByToAndType,
findByQuery: findByQuery
findByQuery: findByQuery,
findInfoByQuery: findInfoByQuery
}
return service;
@ -159,4 +160,15 @@ function EntityRelationService($http, $q) {
return deferred.promise;
}
function findInfoByQuery(query) {
var deferred = $q.defer();
var url = '/api/relations/info';
$http.post(url, query).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
}

563
ui/src/app/api/entity.service.js

@ -27,10 +27,14 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
getEntity: getEntity,
getEntities: getEntities,
getEntitiesByNameFilter: getEntitiesByNameFilter,
processEntityAliases: processEntityAliases,
getEntityKeys: getEntityKeys,
resolveAlias: resolveAlias,
resolveAliasFilter: resolveAliasFilter,
checkEntityAlias: checkEntityAlias,
createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo,
filterAliasByEntityTypes: filterAliasByEntityTypes,
getAliasFilterTypesByEntityTypes: getAliasFilterTypesByEntityTypes,
prepareAllowedEntityTypesList: prepareAllowedEntityTypesList,
getEntityKeys: getEntityKeys,
createDatasourcesFromSubscriptionsInfo: createDatasourcesFromSubscriptionsInfo,
getRelatedEntities: getRelatedEntities,
saveRelatedEntity: saveRelatedEntity,
getRelatedEntity: getRelatedEntity,
@ -173,6 +177,54 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
return deferred.promise;
}
function getSingleTenantByPageLinkPromise(pageLink) {
var user = userService.getCurrentUser();
var tenantId = user.tenantId;
var deferred = $q.defer();
tenantService.getTenant(tenantId).then(
function success(tenant) {
var tenantName = tenant.name;
var result = {
data: [],
nextPageLink: pageLink,
hasNext: false
};
if (tenantName.toLowerCase().startsWith(pageLink.textSearch)) {
result.data.push(tenant);
}
deferred.resolve(result);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function getSingleCustomerByPageLinkPromise(pageLink) {
var user = userService.getCurrentUser();
var customerId = user.customerId;
var deferred = $q.defer();
customerService.getCustomer(customerId).then(
function success(customer) {
var customerName = customer.name;
var result = {
data: [],
nextPageLink: pageLink,
hasNext: false
};
if (customerName.toLowerCase().startsWith(pageLink.textSearch)) {
result.data.push(customer);
}
deferred.resolve(result);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function getEntitiesByPageLinkPromise(entityType, pageLink, config, subType) {
var promise;
var user = userService.getCurrentUser();
@ -193,10 +245,18 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
}
break;
case types.entityType.tenant:
promise = tenantService.getTenants(pageLink);
if (user.authority === 'TENANT_ADMIN') {
promise = getSingleTenantByPageLinkPromise(pageLink);
} else {
promise = tenantService.getTenants(pageLink);
}
break;
case types.entityType.customer:
promise = customerService.getCustomers(pageLink);
if (user.authority === 'CUSTOMER_USER') {
promise = getSingleCustomerByPageLinkPromise(pageLink);
} else {
promise = customerService.getCustomers(pageLink);
}
break;
case types.entityType.rule:
promise = ruleService.getAllRules(pageLink);
@ -221,17 +281,21 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
return promise;
}
function getEntitiesByNameFilter(entityType, entityNameFilter, limit, config, subType) {
var deferred = $q.defer();
var pageLink = {limit: limit, textSearch: entityNameFilter};
function getEntitiesByPageLink(entityType, pageLink, config, subType, data, deferred) {
var promise = getEntitiesByPageLinkPromise(entityType, pageLink, config, subType);
if (promise) {
promise.then(
function success(result) {
if (result.data && result.data.length > 0) {
deferred.resolve(result.data);
data = data.concat(result.data);
if (result.hasNext) {
pageLink = result.nextPageLink;
getEntitiesByPageLink(entityType, pageLink, config, subType, data, deferred);
} else {
deferred.resolve(null);
if (data && data.length > 0) {
deferred.resolve(data);
} else {
deferred.resolve(null);
}
}
},
function fail() {
@ -241,92 +305,418 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
} else {
deferred.resolve(null);
}
}
function getEntitiesByNameFilter(entityType, entityNameFilter, limit, config, subType) {
var deferred = $q.defer();
var pageLink = {limit: limit, textSearch: entityNameFilter};
if (limit == -1) { // all
var data = [];
pageLink.limit = 100;
getEntitiesByPageLink(entityType, pageLink, config, subType, data, deferred);
} else {
var promise = getEntitiesByPageLinkPromise(entityType, pageLink, config, subType);
if (promise) {
promise.then(
function success(result) {
if (result.data && result.data.length > 0) {
deferred.resolve(result.data);
} else {
deferred.resolve(null);
}
},
function fail() {
deferred.resolve(null);
}
);
} else {
deferred.resolve(null);
}
}
return deferred.promise;
}
function entityToEntityInfo(entityType, entity) {
return { name: entity.name, entityType: entityType, id: entity.id.id };
function entityToEntityInfo(entity) {
return { name: entity.name, entityType: entity.id.entityType, id: entity.id.id };
}
function entitiesToEntitiesInfo(entityType, entities) {
function entityRelationInfoToEntityInfo(entityRelationInfo, direction) {
var entityId = direction == types.entitySearchDirection.from ? entityRelationInfo.to : entityRelationInfo.from;
var name = direction == types.entitySearchDirection.from ? entityRelationInfo.toName : entityRelationInfo.fromName;
return {
name: name,
entityType: entityId.entityType,
id: entityId.id
};
}
function entitiesToEntitiesInfo(entities) {
var entitiesInfo = [];
for (var d = 0; d < entities.length; d++) {
entitiesInfo.push(entityToEntityInfo(entityType, entities[d]));
entitiesInfo.push(entityToEntityInfo(entities[d]));
}
return entitiesInfo;
}
function processEntityAlias(index, aliasIds, entityAliases, resolution, deferred) {
if (index < aliasIds.length) {
var aliasId = aliasIds[index];
var entityAlias = entityAliases[aliasId];
var alias = entityAlias.alias;
var entityFilter = entityAlias.entityFilter;
if (entityFilter.useFilter) {
var entityNameFilter = entityFilter.entityNameFilter;
getEntitiesByNameFilter(entityAlias.entityType, entityNameFilter, 100).then(
function(entities) {
if (entities && entities != null) {
var resolvedAlias = {alias: alias, entityType: entityAlias.entityType, entityId: entities[0].id.id};
resolution.aliasesInfo.entityAliases[aliasId] = resolvedAlias;
resolution.aliasesInfo.entityAliasesInfo[aliasId] = entitiesToEntitiesInfo(entityAlias.entityType, entities);
index++;
processEntityAlias(index, aliasIds, entityAliases, resolution, deferred);
function entityRelationInfosToEntitiesInfo(entityRelations, direction) {
var entitiesInfo = [];
for (var d = 0; d < entityRelations.length; d++) {
entitiesInfo.push(entityRelationInfoToEntityInfo(entityRelations[d], direction));
}
return entitiesInfo;
}
function resolveAlias(entityAlias, stateParams) {
var deferred = $q.defer();
var filter = entityAlias.filter;
resolveAliasFilter(filter, stateParams, -1).then(
function (result) {
var aliasInfo = {
alias: entityAlias.alias,
stateEntity: result.stateEntity,
resolveMultiple: filter.resolveMultiple
};
aliasInfo.resolvedEntities = result.entities;
aliasInfo.currentEntity = null;
if (aliasInfo.resolvedEntities.length) {
aliasInfo.currentEntity = aliasInfo.resolvedEntities[0];
}
deferred.resolve(aliasInfo);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function resolveAliasFilter(filter, stateParams, maxItems) {
var deferred = $q.defer();
var result = {
entities: [],
stateEntity: false
};
switch (filter.type) {
case types.aliasFilterType.entityList.value:
getEntities(filter.entityType, filter.entityList).then(
function success(entities) {
if (entities && entities.length) {
result.entities = entitiesToEntitiesInfo(entities);
deferred.resolve(result);
} else {
if (!resolution.error) {
resolution.error = 'dashboard.invalid-aliases-config';
}
index++;
processEntityAlias(index, aliasIds, entityAliases, resolution, deferred);
deferred.reject();
}
});
} else {
var entityList = entityFilter.entityList;
getEntities(entityAlias.entityType, entityList).then(
},
function fail() {
deferred.reject();
}
);
break;
case types.aliasFilterType.entityName.value:
getEntitiesByNameFilter(filter.entityType, filter.entityNameFilter, maxItems).then(
function success(entities) {
if (entities && entities.length > 0) {
var resolvedAlias = {alias: alias, entityType: entityAlias.entityType, entityId: entities[0].id.id};
resolution.aliasesInfo.entityAliases[aliasId] = resolvedAlias;
resolution.aliasesInfo.entityAliasesInfo[aliasId] = entitiesToEntitiesInfo(entityAlias.entityType, entities);
index++;
processEntityAlias(index, aliasIds, entityAliases, resolution, deferred);
if (entities && entities.length) {
result.entities = entitiesToEntitiesInfo(entities);
deferred.resolve(result);
} else {
if (!resolution.error) {
resolution.error = 'dashboard.invalid-aliases-config';
}
index++;
processEntityAlias(index, aliasIds, entityAliases, resolution, deferred);
deferred.reject();
}
},
function fail() {
deferred.reject();
}
);
break;
case types.aliasFilterType.stateEntity.value:
result.stateEntity = true;
if (stateParams && stateParams.entityId) {
getEntity(stateParams.entityId.entityType, stateParams.entityId.id).then(
function success(entity) {
result.entities = entitiesToEntitiesInfo([entity]);
deferred.resolve(result);
},
function fail() {
deferred.reject();
}
);
} else {
deferred.resolve(result);
}
break;
case types.aliasFilterType.assetType.value:
getEntitiesByNameFilter(types.entityType.asset, filter.assetNameFilter, maxItems, null, filter.assetType).then(
function success(entities) {
if (entities && entities.length) {
result.entities = entitiesToEntitiesInfo(entities);
deferred.resolve(result);
} else {
deferred.reject();
}
},
function fail() {
if (!resolution.error) {
resolution.error = 'dashboard.invalid-aliases-config';
deferred.reject();
}
);
break;
case types.aliasFilterType.deviceType.value:
getEntitiesByNameFilter(types.entityType.device, filter.deviceNameFilter, maxItems, null, filter.deviceType).then(
function success(entities) {
if (entities && entities.length) {
result.entities = entitiesToEntitiesInfo(entities);
deferred.resolve(result);
} else {
deferred.reject();
}
index++;
processEntityAlias(index, aliasIds, entityAliases, resolution, deferred);
},
function fail() {
deferred.reject();
}
);
break;
case types.aliasFilterType.relationsQuery.value:
result.stateEntity = filter.rootStateEntity;
var rootEntityType;
var rootEntityId;
if (result.stateEntity && stateParams && stateParams.entityId) {
rootEntityType = stateParams.entityId.entityType;
rootEntityId = stateParams.entityId.id;
} else if (!result.stateEntity) {
rootEntityType = filter.rootEntity.entityType;
rootEntityId = filter.rootEntity.id;
}
if (rootEntityType && rootEntityId) {
var searchQuery = {
parameters: {
rootId: rootEntityId,
rootType: rootEntityType,
direction: filter.direction
},
filters: filter.filters
};
searchQuery.parameters.maxLevel = filter.maxLevel && filter.maxLevel > 0 ? filter.maxLevel : -1;
entityRelationService.findInfoByQuery(searchQuery).then(
function success(allRelations) {
if (allRelations && allRelations.length) {
if (angular.isDefined(maxItems) && maxItems > 0) {
var limit = Math.min(allRelations.length, maxItems);
allRelations.length = limit;
}
result.entities = entityRelationInfosToEntitiesInfo(allRelations, filter.direction);
deferred.resolve(result);
} else {
deferred.reject();
}
},
function fail() {
deferred.reject();
}
);
} else {
deferred.resolve(result);
}
break;
case types.aliasFilterType.assetSearchQuery.value:
case types.aliasFilterType.deviceSearchQuery.value:
result.stateEntity = filter.rootStateEntity;
if (result.stateEntity && stateParams && stateParams.entityId) {
rootEntityType = stateParams.entityId.entityType;
rootEntityId = stateParams.entityId.id;
} else if (!result.stateEntity) {
rootEntityType = filter.rootEntity.entityType;
rootEntityId = filter.rootEntity.id;
}
if (rootEntityType && rootEntityId) {
searchQuery = {
parameters: {
rootId: rootEntityId,
rootType: rootEntityType,
direction: filter.direction
},
relationType: filter.relationType
};
searchQuery.parameters.maxLevel = filter.maxLevel && filter.maxLevel > 0 ? filter.maxLevel : -1;
var findByQueryPromise;
if (filter.type == types.aliasFilterType.assetSearchQuery.value) {
searchQuery.assetTypes = filter.assetTypes;
findByQueryPromise = assetService.findByQuery(searchQuery, false);
} else if (filter.type == types.aliasFilterType.deviceSearchQuery.value) {
searchQuery.deviceTypes = filter.deviceTypes;
findByQueryPromise = deviceService.findByQuery(searchQuery, false);
}
findByQueryPromise.then(
function success(entities) {
if (entities && entities.length) {
if (angular.isDefined(maxItems) && maxItems > 0) {
var limit = Math.min(entities.length, maxItems);
entities.length = limit;
}
result.entities = entitiesToEntitiesInfo(entities);
deferred.resolve(result);
} else {
deferred.reject();
}
},
function fail() {
deferred.reject();
}
);
} else {
deferred.resolve(result);
}
break;
}
return deferred.promise;
}
function filterAliasByEntityTypes(entityAlias, entityTypes) {
var filter = entityAlias.filter;
if (filterAliasFilterTypeByEntityTypes(filter.type, entityTypes)) {
switch (filter.type) {
case types.aliasFilterType.entityList.value:
return entityTypes.indexOf(filter.entityType) > -1 ? true : false;
case types.aliasFilterType.entityName.value:
return entityTypes.indexOf(filter.entityType) > -1 ? true : false;
case types.aliasFilterType.stateEntity.value:
return true;
case types.aliasFilterType.assetType.value:
return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false;
case types.aliasFilterType.deviceType.value:
return entityTypes.indexOf(types.entityType.device) > -1 ? true : false;
case types.aliasFilterType.relationsQuery.value:
if (filter.filters && filter.filters.length) {
var match = false;
for (var f=0;f<filter.filters.length;f++) {
var relationFilter = filter.filters[f];
if (relationFilter.entityTypes && relationFilter.entityTypes.length) {
for (var et=0;et<relationFilter.entityTypes.length;et++) {
if (entityTypes.indexOf(relationFilter.entityTypes[et]) > -1) {
match = true;
break;
}
}
} else {
match = true;
break;
}
}
return match;
} else {
return true;
}
case types.aliasFilterType.assetSearchQuery.value:
return entityTypes.indexOf(types.entityType.asset) > -1 ? true : false;
case types.aliasFilterType.deviceSearchQuery.value:
return entityTypes.indexOf(types.entityType.device) > -1 ? true : false;
}
} else {
deferred.resolve(resolution);
}
return false;
}
function processEntityAliases(entityAliases) {
var deferred = $q.defer();
var resolution = {
aliasesInfo: {
entityAliases: {},
entityAliasesInfo: {}
function filterAliasFilterTypeByEntityType(aliasFilterType, entityType) {
switch (aliasFilterType) {
case types.aliasFilterType.entityList.value:
return true;
case types.aliasFilterType.entityName.value:
return true;
case types.aliasFilterType.stateEntity.value:
return true;
case types.aliasFilterType.assetType.value:
return entityType === types.entityType.asset;
case types.aliasFilterType.deviceType.value:
return entityType === types.entityType.device;
case types.aliasFilterType.relationsQuery.value:
return true;
case types.aliasFilterType.assetSearchQuery.value:
return entityType === types.entityType.asset;
case types.aliasFilterType.deviceSearchQuery.value:
return entityType === types.entityType.device;
}
return false;
}
function filterAliasFilterTypeByEntityTypes(aliasFilterType, entityTypes) {
if (!entityTypes || !entityTypes.length) {
return true;
}
var valid = false;
entityTypes.forEach(function(entityType) {
valid = valid || filterAliasFilterTypeByEntityType(aliasFilterType, entityType);
});
return valid;
}
function getAliasFilterTypesByEntityTypes(entityTypes) {
var allAliasFilterTypes = types.aliasFilterType;
if (!entityTypes || !entityTypes.length) {
return allAliasFilterTypes;
}
var result = {};
for (var type in allAliasFilterTypes) {
var aliasFilterType = allAliasFilterTypes[type];
if (filterAliasFilterTypeByEntityTypes(aliasFilterType.value, entityTypes)) {
result[type] = aliasFilterType;
}
};
var aliasIds = [];
if (entityAliases) {
for (var aliasId in entityAliases) {
aliasIds.push(aliasId);
}
return result;
}
function prepareAllowedEntityTypesList(allowedEntityTypes) {
var authority = userService.getAuthority();
var entityTypes = {};
switch(authority) {
case 'SYS_ADMIN':
entityTypes.tenant = types.entityType.tenant;
entityTypes.rule = types.entityType.rule;
entityTypes.plugin = types.entityType.plugin;
break;
case 'TENANT_ADMIN':
entityTypes.device = types.entityType.device;
entityTypes.asset = types.entityType.asset;
entityTypes.tenant = types.entityType.tenant;
entityTypes.customer = types.entityType.customer;
entityTypes.rule = types.entityType.rule;
entityTypes.plugin = types.entityType.plugin;
entityTypes.dashboard = types.entityType.dashboard;
break;
case 'CUSTOMER_USER':
entityTypes.device = types.entityType.device;
entityTypes.asset = types.entityType.asset;
entityTypes.customer = types.entityType.customer;
entityTypes.dashboard = types.entityType.dashboard;
break;
}
if (allowedEntityTypes) {
for (var entityType in entityTypes) {
if (allowedEntityTypes.indexOf(entityTypes[entityType]) === -1) {
delete entityTypes[entityType];
}
}
}
processEntityAlias(0, aliasIds, entityAliases, resolution, deferred);
return entityTypes;
}
function checkEntityAlias(entityAlias) {
var deferred = $q.defer();
resolveAliasFilter(entityAlias.filter, null, 1).then(
function success(result) {
if (result.stateEntity) {
deferred.resolve(true);
} else {
var entities = result.entities;
if (entities && entities.length) {
deferred.resolve(true);
} else {
deferred.resolve(false);
}
}
},
function fail() {
deferred.resolve(false);
}
);
return deferred.promise;
}
@ -354,40 +744,13 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
}
}
deferred.resolve(result);
}, function fail(response) {
deferred.reject(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function checkEntityAlias(entityAlias) {
var deferred = $q.defer();
var entityType = entityAlias.entityType;
var entityFilter = entityAlias.entityFilter;
var promise;
if (entityFilter.useFilter) {
var entityNameFilter = entityFilter.entityNameFilter;
promise = getEntitiesByNameFilter(entityType, entityNameFilter, 1);
} else {
var entityList = entityFilter.entityList;
promise = getEntities(entityType, entityList);
}
promise.then(
function success(entities) {
if (entities && entities.length > 0) {
deferred.resolve(true);
} else {
deferred.resolve(false);
}
},
function fail() {
deferred.resolve(false);
}
);
return deferred.promise;
}
function createDatasoucesFromSubscriptionsInfo(subscriptionsInfo) {
function createDatasourcesFromSubscriptionsInfo(subscriptionsInfo) {
var deferred = $q.defer();
var datasources = [];
processSubscriptionsInfo(0, subscriptionsInfo, datasources, deferred);
@ -822,4 +1185,4 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
}
}
}
}

207
ui/src/app/api/subscription.js

@ -39,6 +39,9 @@ export default class Subscription {
this.cafs = {};
this.registrations = [];
var subscription = this;
var deferred = this.ctx.$q.defer();
if (this.type === this.ctx.types.widgetType.rpc.value) {
this.callbacks.rpcStateChanged = this.callbacks.rpcStateChanged || function(){};
this.callbacks.onRpcSuccess = this.callbacks.onRpcSuccess || function(){};
@ -56,7 +59,11 @@ export default class Subscription {
this.rpcEnabled = false;
this.executingRpcRequest = false;
this.executingPromises = [];
this.initRpc();
this.initRpc().then(
function() {
deferred.resolve(subscription);
}
);
} else {
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){};
this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){};
@ -66,6 +73,15 @@ export default class Subscription {
this.datasources = this.ctx.utils.validateDatasources(options.datasources);
this.datasourceListeners = [];
/*
* data = array of datasourceData
* datasourceData = {
* tbDatasource,
* dataKey, { name, config }
* data = array of [time, value]
* }
*/
this.data = [];
this.hiddenData = [];
this.originalTimewindow = null;
@ -103,11 +119,41 @@ export default class Subscription {
this.legendConfig.showMax === true ||
this.legendConfig.showAvg === true ||
this.legendConfig.showTotal === true);
this.initDataSubscription();
this.initDataSubscription().then(
function success() {
deferred.resolve(subscription);
},
function fail() {
deferred.reject();
}
);
}
return deferred.promise;
}
initDataSubscription() {
var deferred = this.ctx.$q.defer();
if (!this.ctx.aliasController) {
this.configureData();
deferred.resolve();
} else {
var subscription = this;
this.ctx.aliasController.resolveDatasources(this.datasources).then(
function success(datasources) {
subscription.datasources = datasources;
subscription.configureData();
deferred.resolve();
},
function fail() {
deferred.reject();
}
);
}
return deferred.promise;
}
configureData() {
var dataIndex = 0;
for (var i = 0; i < this.datasources.length; i++) {
var datasource = this.datasources[i];
@ -199,21 +245,46 @@ export default class Subscription {
}
initRpc() {
var deferred = this.ctx.$q.defer();
if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) {
this.targetDeviceAliasId = this.targetDeviceAliasIds[0];
if (this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId]) {
this.targetDeviceId = this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId].entityId;
var subscription = this;
this.ctx.aliasController.getAliasInfo(this.targetDeviceAliasId).then(
function success(aliasInfo) {
if (aliasInfo.currentEntity && aliasInfo.currentEntity.entityType == subscription.ctx.types.entityType.device) {
subscription.targetDeviceId = aliasInfo.currentEntity.id;
if (subscription.targetDeviceId) {
subscription.rpcEnabled = true;
} else {
subscription.rpcEnabled = subscription.ctx.$scope.widgetEditMode ? true : false;
}
subscription.callbacks.rpcStateChanged(subscription);
deferred.resolve();
} else {
subscription.rpcEnabled = false;
subscription.callbacks.rpcStateChanged(subscription);
deferred.resolve();
}
},
function fail () {
subscription.rpcEnabled = false;
subscription.callbacks.rpcStateChanged(subscription);
deferred.resolve();
}
);
} else {
if (this.targetDeviceIds && this.targetDeviceIds.length > 0) {
this.targetDeviceId = this.targetDeviceIds[0];
}
} else if (this.targetDeviceIds && this.targetDeviceIds.length > 0) {
this.targetDeviceId = this.targetDeviceIds[0];
}
if (this.targetDeviceId) {
this.rpcEnabled = true;
} else {
this.rpcEnabled = this.ctx.$scope.widgetEditMode ? true : false;
if (this.targetDeviceId) {
this.rpcEnabled = true;
} else {
this.rpcEnabled = this.ctx.$scope.widgetEditMode ? true : false;
}
this.callbacks.rpcStateChanged(this);
deferred.resolve();
}
this.callbacks.rpcStateChanged(this);
return deferred.promise;
}
clearRpcError() {
@ -319,11 +390,11 @@ export default class Subscription {
this.onDataUpdated();
}
onAliasesChanged() {
onAliasesChanged(aliasIds) {
if (this.type === this.ctx.types.widgetType.rpc.value) {
this.checkRpcTarget();
return this.checkRpcTarget(aliasIds);
} else {
this.checkSubscriptions();
return this.checkSubscriptions(aliasIds);
}
}
@ -481,39 +552,6 @@ export default class Subscription {
var datasource = this.datasources[i];
if (angular.isFunction(datasource))
continue;
var entityId = null;
var entityType = null;
if (datasource.type === this.ctx.types.datasourceType.entity) {
var aliasName = null;
var entityName = null;
if (datasource.entityId) {
entityId = datasource.entityId;
entityType = datasource.entityType;
datasource.name = datasource.entityName;
aliasName = datasource.entityName;
entityName = datasource.entityName;
} else if (datasource.entityAliasId) {
if (this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId]) {
entityId = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].entityId;
entityType = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].entityType;
datasource.name = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].alias;
aliasName = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].alias;
entityName = '';
var entitiesInfo = this.ctx.aliasesInfo.entityAliasesInfo[datasource.entityAliasId];
for (var d = 0; d < entitiesInfo.length; d++) {
if (entitiesInfo[d].id === entityId) {
entityName = entitiesInfo[d].name;
break;
}
}
}
}
} else {
datasource.name = datasource.name || this.ctx.types.datasourceType.function;
}
for (var dk = 0; dk < datasource.dataKeys.length; dk++) {
updateDataKeyLabel(datasource.dataKeys[dk], datasource.name, entityName, aliasName);
}
var subscription = this;
@ -521,8 +559,8 @@ export default class Subscription {
subscriptionType: this.type,
subscriptionTimewindow: this.subscriptionTimewindow,
datasource: datasource,
entityType: entityType,
entityId: entityId,
entityType: datasource.entityType,
entityId: datasource.entityId,
dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
},
@ -544,6 +582,10 @@ export default class Subscription {
this.datasourceListeners.push(listener);
this.ctx.datasourceService.subscribeToDatasource(listener);
if (datasource.unresolvedStateEntity) {
this.notifyDataLoaded();
this.onDataUpdated();
}
}
}
@ -557,48 +599,26 @@ export default class Subscription {
}
}
checkRpcTarget() {
var deviceId = null;
if (this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId]) {
deviceId = this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId].entityId;
}
if (!angular.equals(deviceId, this.targetDeviceId)) {
this.targetDeviceId = deviceId;
if (this.targetDeviceId) {
this.rpcEnabled = true;
} else {
this.rpcEnabled = this.ctx.$scope.widgetEditMode ? true : false;
}
this.callbacks.rpcStateChanged(this);
checkRpcTarget(aliasIds) {
if (aliasIds.indexOf(this.targetDeviceAliasId) > -1) {
return true;
} else {
return false;
}
}
checkSubscriptions() {
checkSubscriptions(aliasIds) {
var subscriptionsChanged = false;
for (var i = 0; i < this.datasourceListeners.length; i++) {
var listener = this.datasourceListeners[i];
var entityId = null;
var entityType = null;
var aliasName = null;
if (listener.datasource.type === this.ctx.types.datasourceType.entity) {
if (listener.datasource.entityAliasId &&
this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId]) {
entityId = this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId].entityId;
entityType = this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId].entityType;
aliasName = this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId].alias;
}
if (!angular.equals(entityId, listener.entityId) ||
!angular.equals(entityType, listener.entityType) ||
!angular.equals(aliasName, listener.datasource.name)) {
if (listener.datasource.entityAliasId) {
if (aliasIds.indexOf(listener.datasource.entityAliasId) > -1) {
subscriptionsChanged = true;
break;
}
}
}
if (subscriptionsChanged) {
this.unsubscribe();
this.subscribe();
}
return subscriptionsChanged;
}
destroy() {
@ -617,29 +637,6 @@ export default class Subscription {
}
const varsRegex = /\$\{([^\}]*)\}/g;
function updateDataKeyLabel(dataKey, dsName, entityName, aliasName) {
var pattern = dataKey.pattern;
var label = dataKey.pattern;
var match = varsRegex.exec(pattern);
while (match !== null) {
var variable = match[0];
var variableName = match[1];
if (variableName === 'dsName') {
label = label.split(variable).join(dsName);
} else if (variableName === 'entityName') {
label = label.split(variable).join(entityName);
} else if (variableName === 'deviceName') {
label = label.split(variable).join(entityName);
} else if (variableName === 'aliasName') {
label = label.split(variable).join(aliasName);
}
match = varsRegex.exec(pattern);
}
dataKey.label = label;
}
function calculateMin(data) {
if (data.length > 0) {
var result = Number(data[0][1]);

7
ui/src/app/asset/assets.tpl.html

@ -48,11 +48,16 @@
disable-attribute-scope-selection="true">
</tb-attribute-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'alarm.alarms' | translate }}">
<tb-alarm-table flex entity-type="vm.types.entityType.asset"
entity-id="vm.grid.operatingItem().id.id">
</tb-alarm-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'asset.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.asset"
entity-id="vm.grid.operatingItem().id.id"
tenant-id="vm.grid.operatingItem().tenantId.id"
default-event-type="{{vm.types.eventType.alarm.value}}">
default-event-type="{{vm.types.eventType.error.value}}">
</tb-event-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">

2
ui/src/app/asset/index.js

@ -15,7 +15,6 @@
*/
import uiRouter from 'angular-ui-router';
import thingsboardGrid from '../components/grid.directive';
import thingsboardEvent from '../event';
import thingsboardApiUser from '../api/user.service';
import thingsboardApiAsset from '../api/asset.service';
import thingsboardApiCustomer from '../api/customer.service';
@ -29,7 +28,6 @@ import AssetDirective from './asset.directive';
export default angular.module('thingsboard.asset', [
uiRouter,
thingsboardGrid,
thingsboardEvent,
thingsboardApiUser,
thingsboardApiAsset,
thingsboardApiCustomer

148
ui/src/app/common/dashboard-utils.service.js

@ -23,8 +23,10 @@ function DashboardUtils(types, utils, timeService) {
var service = {
validateAndUpdateDashboard: validateAndUpdateDashboard,
validateAndUpdateWidget: validateAndUpdateWidget,
getRootStateId: getRootStateId,
createSingleWidgetDashboard: createSingleWidgetDashboard,
createSingleEntityFilter: createSingleEntityFilter,
getStateLayoutsData: getStateLayoutsData,
createDefaultState: createDefaultState,
createDefaultLayoutData: createDefaultLayoutData,
@ -39,40 +41,104 @@ function DashboardUtils(types, utils, timeService) {
return service;
function validateAndUpdateEntityAliases(configuration) {
function validateAndUpdateEntityAliases(configuration, datasourcesByAliasId, targetDevicesByAliasId) {
var aliasId, entityAlias;
if (angular.isUndefined(configuration.entityAliases)) {
configuration.entityAliases = {};
if (configuration.deviceAliases) {
var deviceAliases = configuration.deviceAliases;
for (var aliasId in deviceAliases) {
for (aliasId in deviceAliases) {
var deviceAlias = deviceAliases[aliasId];
var alias = deviceAlias.alias;
var entityFilter = {
useFilter: false,
entityNameFilter: '',
entityList: []
}
if (deviceAlias.deviceFilter) {
entityFilter.useFilter = deviceAlias.deviceFilter.useFilter;
entityFilter.entityNameFilter = deviceAlias.deviceFilter.deviceNameFilter;
entityFilter.entityList = deviceAlias.deviceFilter.deviceList;
} else if (deviceAlias.deviceId) {
entityFilter.entityList = [deviceAlias.deviceId];
}
var entityAlias = {
id: aliasId,
alias: alias,
entityType: types.entityType.device,
entityFilter: entityFilter
};
configuration.entityAliases[aliasId] = entityAlias;
entityAlias = validateAndUpdateDeviceAlias(aliasId, deviceAlias, datasourcesByAliasId, targetDevicesByAliasId);
configuration.entityAliases[entityAlias.id] = entityAlias;
}
delete configuration.deviceAliases;
}
} else {
var entityAliases = configuration.entityAliases;
for (aliasId in entityAliases) {
entityAlias = entityAliases[aliasId];
entityAlias = validateAndUpdateEntityAlias(aliasId, entityAlias, datasourcesByAliasId, targetDevicesByAliasId);
if (aliasId != entityAlias.id) {
delete entityAliases[aliasId];
}
entityAliases[entityAlias.id] = entityAlias;
}
}
return configuration;
}
function validateAliasId(aliasId, datasourcesByAliasId, targetDevicesByAliasId) {
if (!aliasId || !angular.isString(aliasId) || aliasId.length != 36) {
var newAliasId = utils.guid();
var aliasDatasources = datasourcesByAliasId[aliasId];
if (aliasDatasources) {
aliasDatasources.forEach(
function(datasource) {
datasource.entityAliasId = newAliasId;
}
);
}
var targetDeviceAliasIdsList = targetDevicesByAliasId[aliasId];
if (targetDeviceAliasIdsList) {
targetDeviceAliasIdsList.forEach(
function(targetDeviceAliasIds) {
targetDeviceAliasIds[0] = newAliasId;
}
);
}
return newAliasId;
} else {
return aliasId;
}
}
function validateAndUpdateDeviceAlias(aliasId, deviceAlias, datasourcesByAliasId, targetDevicesByAliasId) {
aliasId = validateAliasId(aliasId, datasourcesByAliasId, targetDevicesByAliasId);
var alias = deviceAlias.alias;
var entityAlias = {
id: aliasId,
alias: alias,
filter: {
type: null,
entityType: types.entityType.device,
resolveMultiple: false
},
}
if (deviceAlias.deviceFilter) {
entityAlias.filter.type =
deviceAlias.deviceFilter.useFilter ? types.aliasFilterType.entityName.value : types.aliasFilterType.entityList.value;
if (entityAlias.filter.type == types.aliasFilterType.entityList.value) {
entityAlias.filter.entityList = deviceAlias.deviceFilter.deviceList;
} else {
entityAlias.filter.entityNameFilter = deviceAlias.deviceFilter.deviceNameFilter;
}
} else {
entityAlias.filter.type = types.aliasFilterType.entityList.value;
entityAlias.filter.entityList = [deviceAlias.deviceId];
}
return entityAlias;
}
function validateAndUpdateEntityAlias(aliasId, entityAlias, datasourcesByAliasId, targetDevicesByAliasId) {
entityAlias.id = validateAliasId(aliasId, datasourcesByAliasId, targetDevicesByAliasId);
if (!entityAlias.filter) {
entityAlias.filter = {
type: entityAlias.entityFilter.useFilter ? types.aliasFilterType.entityName.value : types.aliasFilterType.entityList.value,
entityType: entityAlias.entityType,
resolveMultiple: false
}
if (entityAlias.filter.type == types.aliasFilterType.entityList.value) {
entityAlias.filter.entityList = entityAlias.entityFilter.entityList;
} else {
entityAlias.filter.entityNameFilter = entityAlias.entityFilter.entityNameFilter;
}
delete entityAlias.entityType;
delete entityAlias.entityFilter;
}
return entityAlias;
}
function validateAndUpdateWidget(widget) {
if (!widget.config) {
widget.config = {};
@ -166,7 +232,34 @@ function DashboardUtils(types, utils, timeService) {
states[firstStateId].root = true;
}
}
dashboard.configuration = validateAndUpdateEntityAliases(dashboard.configuration);
var datasourcesByAliasId = {};
var targetDevicesByAliasId = {};
for (var widgetId in dashboard.configuration.widgets) {
widget = dashboard.configuration.widgets[widgetId];
widget.config.datasources.forEach(function (datasource) {
if (datasource.entityAliasId) {
var aliasId = datasource.entityAliasId;
var aliasDatasources = datasourcesByAliasId[aliasId];
if (!aliasDatasources) {
aliasDatasources = [];
datasourcesByAliasId[aliasId] = aliasDatasources;
}
aliasDatasources.push(datasource);
}
});
if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length) {
var aliasId = widget.config.targetDeviceAliasIds[0];
var targetDeviceAliasIdsList = targetDevicesByAliasId[aliasId];
if (!targetDeviceAliasIdsList) {
targetDeviceAliasIdsList = [];
targetDevicesByAliasId[aliasId] = targetDeviceAliasIdsList;
}
targetDeviceAliasIdsList.push(widget.config.targetDeviceAliasIds);
}
}
dashboard.configuration = validateAndUpdateEntityAliases(dashboard.configuration, datasourcesByAliasId, targetDevicesByAliasId);
if (angular.isUndefined(dashboard.configuration.timewindow)) {
dashboard.configuration.timewindow = timeService.defaultTimewindow();
@ -243,6 +336,15 @@ function DashboardUtils(types, utils, timeService) {
return dashboard;
}
function createSingleEntityFilter(entityType, entityId) {
return {
type: types.aliasFilterType.entityList.value,
entityList: [entityId],
entityType: entityType,
resolveMultiple: false
};
}
function getStateLayoutsData(dashboard, targetState) {
var dashboardConfiguration = dashboard.configuration;
var states = dashboardConfiguration.states;

123
ui/src/app/common/types.constant.js

@ -65,6 +65,69 @@ export default angular.module('thingsboard.types', [])
clearedUnack: "CLEARED_UNACK",
clearedAck: "CLEARED_ACK"
},
alarmSearchStatus: {
any: "ANY",
active: "ACTIVE",
cleared: "CLEARED",
ack: "ACK",
unack: "UNACK"
},
alarmSeverity: {
"CRITICAL": {
name: "alarm.severity-critical",
class: "tb-critical"
},
"MAJOR": {
name: "alarm.severity-major",
class: "tb-major"
},
"MINOR": {
name: "alarm.severity-minor",
class: "tb-minor"
},
"WARNING": {
name: "alarm.severity-warning",
class: "tb-warning"
},
"INDETERMINATE": {
name: "alarm.severity-indeterminate",
class: "tb-indeterminate"
}
},
aliasFilterType: {
entityList: {
value: 'entityList',
name: 'alias.filter-type-entity-list'
},
entityName: {
value: 'entityName',
name: 'alias.filter-type-entity-name'
},
stateEntity: {
value: 'stateEntity',
name: 'alias.filter-type-state-entity'
},
assetType: {
value: 'assetType',
name: 'alias.filter-type-asset-type'
},
deviceType: {
value: 'deviceType',
name: 'alias.filter-type-device-type'
},
relationsQuery: {
value: 'relationsQuery',
name: 'alias.filter-type-relations-query'
},
assetSearchQuery: {
value: 'assetSearchQuery',
name: 'alias.filter-type-asset-search-query'
},
deviceSearchQuery: {
value: 'deviceSearchQuery',
name: 'alias.filter-type-device-search-query'
}
},
position: {
top: {
value: "top",
@ -109,6 +172,62 @@ export default angular.module('thingsboard.types', [])
dashboard: "DASHBOARD",
alarm: "ALARM"
},
entityTypeTranslations: {
"DEVICE": {
type: 'entity.type-device',
typePlural: 'entity.type-devices',
list: 'entity.list-of-devices',
nameStartsWith: 'entity.device-name-starts-with'
},
"ASSET": {
type: 'entity.type-asset',
typePlural: 'entity.type-assets',
list: 'entity.list-of-assets',
nameStartsWith: 'entity.asset-name-starts-with'
},
"RULE": {
type: 'entity.type-rule',
typePlural: 'entity.type-rules',
list: 'entity.list-of-rules',
nameStartsWith: 'entity.rule-name-starts-with'
},
"PLUGIN": {
type: 'entity.type-plugin',
typePlural: 'entity.type-plugins',
list: 'entity.list-of-plugins',
nameStartsWith: 'entity.plugin-name-starts-with'
},
"TENANT": {
type: 'entity.type-tenant',
typePlural: 'entity.type-tenants',
list: 'entity.list-of-tenants',
nameStartsWith: 'entity.tenant-name-starts-with'
},
"CUSTOMER": {
type: 'entity.type-customer',
typePlural: 'entity.type-customers',
list: 'entity.list-of-customers',
nameStartsWith: 'entity.customer-name-starts-with'
},
"USER": {
type: 'entity.type-user',
typePlural: 'entity.type-users',
list: 'entity.list-of-users',
nameStartsWith: 'entity.user-name-starts-with'
},
"DASHBOARD": {
type: 'entity.type-dashboard',
typePlural: 'entity.type-dashboards',
list: 'entity.list-of-dashboards',
nameStartsWith: 'entity.dashboard-name-starts-with'
},
"ALARM": {
type: 'entity.type-alarm',
typePlural: 'entity.type-alarms',
list: 'entity.list-of-alarms',
nameStartsWith: 'entity.alarm-name-starts-with'
}
},
entitySearchDirection: {
from: "FROM",
to: "TO"
@ -118,10 +237,6 @@ export default angular.module('thingsboard.types', [])
manages: "Manages"
},
eventType: {
alarm: {
value: "ALARM",
name: "event.type-alarm"
},
error: {
value: "ERROR",
name: "event.type-error"

37
ui/src/app/common/utils.service.js

@ -106,10 +106,10 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty,
filterSearchTextEntities: filterSearchTextEntities,
guid: guid,
cleanCopy: cleanCopy,
isLocalUrl: isLocalUrl,
validateDatasources: validateDatasources,
createKey: createKey,
entityTypeName: entityTypeName
createKey: createKey
}
return service;
@ -291,6 +291,16 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
s4() + '-' + s4() + s4() + s4();
}
function cleanCopy(object) {
var copy = angular.copy(object);
for (var prop in copy) {
if (prop && prop.startsWith('$$')) {
delete copy[prop];
}
}
return copy;
}
function genNextColor(datasources) {
var index = 0;
if (datasources) {
@ -347,27 +357,4 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
return dataKey;
}
function entityTypeName (type) {
switch (type) {
case types.entityType.device:
return 'entity.type-device';
case types.entityType.asset:
return 'entity.type-asset';
case types.entityType.rule:
return 'entity.type-rule';
case types.entityType.plugin:
return 'entity.type-plugin';
case types.entityType.tenant:
return 'entity.type-tenant';
case types.entityType.customer:
return 'entity.type-customer';
case types.entityType.user:
return 'entity.type-user';
case types.entityType.dashboard:
return 'entity.type-dashboard';
case types.entityType.alarm:
return 'entity.type-alarm';
}
}
}

37
ui/src/app/components/dashboard.directive.js

@ -52,7 +52,7 @@ function Dashboard() {
bindToController: {
widgets: '=',
widgetLayouts: '=?',
aliasesInfo: '=',
aliasController: '=',
stateController: '=',
dashboardTimewindow: '=?',
columns: '=',
@ -85,7 +85,7 @@ function Dashboard() {
}
/*@ngInject*/
function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, timeService, types, utils) {
function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $mdUtil, timeService, types, utils) {
var highlightedMode = false;
var highlightedWidget = null;
@ -329,10 +329,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
$scope.$broadcast('toggleDashboardEditMode', vm.isEdit);
});
$scope.$watch('vm.aliasesInfo.entityAliases', function () {
$scope.$broadcast('entityAliasListChanged', vm.aliasesInfo);
}, true);
$scope.$on('gridster-resized', function (event, sizes, theGridster) {
if (checkIsLocalGridsterElement(theGridster)) {
vm.gridster = theGridster;
@ -796,7 +792,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
}
function dashboardLoaded() {
$timeout(function () {
$mdUtil.nextTick(function () {
if (vm.dashboardTimewindowWatch) {
vm.dashboardTimewindowWatch();
vm.dashboardTimewindowWatch = null;
@ -806,14 +802,27 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
}, true);
adoptMaxRows();
vm.dashboardLoading = false;
$timeout(function () {
var gridsterScope = gridsterElement.scope();
vm.gridster = gridsterScope.gridster;
if (vm.onInit) {
vm.onInit({dashboard: vm});
if ($scope.gridsterScopeWatcher) {
$scope.gridsterScopeWatcher();
}
$scope.gridsterScopeWatcher = $scope.$watch(
function() {
var hasScope = gridsterElement.scope() ? true : false;
return hasScope;
},
function(hasScope) {
if (hasScope) {
$scope.gridsterScopeWatcher();
$scope.gridsterScopeWatcher = null;
var gridsterScope = gridsterElement.scope();
vm.gridster = gridsterScope.gridster;
if (vm.onInit) {
vm.onInit({dashboard: vm});
}
}
}
}, 0, false);
}, 0, false);
);
});
}
function loading() {

2
ui/src/app/components/dashboard.tpl.html

@ -89,7 +89,7 @@
<div flex tb-widget
locals="{ visibleRect: vm.visibleRect,
widget: widget,
aliasesInfo: vm.aliasesInfo,
aliasController: vm.aliasController,
stateController: vm.stateController,
isEdit: vm.isEdit,
stDiff: vm.stDiff,

32
ui/src/app/components/datakey-config-dialog.controller.js

@ -20,14 +20,14 @@ export default angular.module('thingsboard.dialogs.datakeyConfigDialog', [things
.name;
/*@ngInject*/
function DatakeyConfigDialogController($scope, $mdDialog, entityService, dataKey, dataKeySettingsSchema, entityAlias, entityAliases) {
function DatakeyConfigDialogController($scope, $mdDialog, $q, entityService, dataKey, dataKeySettingsSchema, entityAlias, aliasController) {
var vm = this;
vm.dataKey = dataKey;
vm.dataKeySettingsSchema = dataKeySettingsSchema;
vm.entityAlias = entityAlias;
vm.entityAliases = entityAliases;
vm.aliasController = aliasController;
vm.hide = function () {
$mdDialog.hide();
@ -38,12 +38,28 @@ function DatakeyConfigDialogController($scope, $mdDialog, entityService, dataKey
};
vm.fetchEntityKeys = function (entityAliasId, query, type) {
var alias = vm.entityAliases[entityAliasId];
if (alias) {
return entityService.getEntityKeys(alias.entityType, alias.entityId, query, type);
} else {
return [];
}
var deferred = $q.defer();
vm.aliasController.getAliasInfo(entityAliasId).then(
function success(aliasInfo) {
var entity = aliasInfo.currentEntity;
if (entity) {
entityService.getEntityKeys(entity.entityType, entity.id, query, type).then(
function success(keys) {
deferred.resolve(keys);
},
function fail() {
deferred.resolve([]);
}
);
} else {
deferred.resolve([]);
}
},
function fail() {
deferred.resolve([]);
}
);
return deferred.promise;
};
vm.save = function () {

11
ui/src/app/components/datasource-entity.directive.js

@ -103,10 +103,9 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
var entityAliasId = ngModelCtrl.$viewValue.entityAliasId;
if (scope.entityAliases[entityAliasId]) {
scope.entityAlias = {id: entityAliasId, alias: scope.entityAliases[entityAliasId].alias,
entityType: scope.entityAliases[entityAliasId].entityType,
entityId: scope.entityAliases[entityAliasId].entityId};
var entityAliases = scope.aliasController.getEntityAliases();
if (entityAliases[entityAliasId]) {
scope.entityAlias = entityAliases[entityAliasId];
} else {
scope.entityAlias = null;
}
@ -182,7 +181,7 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
dataKey: angular.copy(dataKey),
dataKeySettingsSchema: scope.datakeySettingsSchema,
entityAlias: scope.entityAlias,
entityAliases: scope.entityAliases
aliasController: scope.aliasController
},
parent: angular.element($document[0].body),
fullscreen: true,
@ -236,7 +235,7 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
require: "^ngModel",
scope: {
widgetType: '=',
entityAliases: '=',
aliasController: '=',
datakeySettingsSchema: '=',
generateDataKey: '&',
fetchEntityKeys: '&',

2
ui/src/app/components/datasource-entity.tpl.html

@ -18,7 +18,7 @@
<section flex layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
<tb-entity-alias-select
tb-required="true"
entity-aliases="entityAliases"
alias-controller="aliasController"
ng-model="entityAlias"
on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias})">
</tb-entity-alias-select>

8
ui/src/app/components/datasource-func.directive.js

@ -71,6 +71,13 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
}
}, true);
scope.$watch('datasourceName', function () {
if (ngModelCtrl.$viewValue) {
ngModelCtrl.$viewValue.name = scope.datasourceName;
scope.updateValidity();
}
});
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
var funcDataKeys = [];
@ -78,6 +85,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
funcDataKeys = funcDataKeys.concat(ngModelCtrl.$viewValue.dataKeys);
}
scope.funcDataKeys = funcDataKeys;
scope.datasourceName = ngModelCtrl.$viewValue.name;
}
};

38
ui/src/app/components/datasource-func.scss

@ -15,23 +15,29 @@
*/
@import '../../scss/constants';
.tb-func-datakey-autocomplete {
.tb-not-found {
display: block;
line-height: 1.5;
height: 48px;
.tb-no-entries {
line-height: 48px;
}
.tb-datasource-func {
@media (min-width: $layout-breakpoint-gt-sm) {
padding-left: 8px;
}
li {
height: auto !important;
white-space: normal !important;
md-input-container.tb-datasource-name {
.md-errors-spacer {
display: none;
}
}
}
tb-datasource-func {
@media (min-width: $layout-breakpoint-gt-sm) {
padding-left: 8px;
.tb-func-datakey-autocomplete {
.tb-not-found {
display: block;
line-height: 1.5;
height: 48px;
.tb-no-entries {
line-height: 48px;
}
}
li {
height: auto !important;
white-space: normal !important;
}
}
}
}

111
ui/src/app/components/datasource-func.tpl.html

@ -15,59 +15,68 @@
limitations under the License.
-->
<section flex layout='column' style="padding-left: 4px;">
<md-chips flex
id="function_datakey_chips"
ng-required="true"
ng-model="funcDataKeys" md-autocomplete-snap
md-transform-chip="transformDataKeyChip($chip)"
md-require-match="false">
<md-autocomplete
md-no-cache="false"
id="dataKey"
md-selected-item="selectedDataKey"
md-search-text="dataKeySearchText"
md-items="item in dataKeysSearch(dataKeySearchText)"
md-item-text="item.name"
md-min-length="0"
placeholder="{{ 'datakey.function-types' | translate }}"
md-menu-class="tb-func-datakey-autocomplete">
<span md-highlight-text="dataKeySearchText" md-highlight-flags="^i">{{item}}</span>
<md-not-found>
<div class="tb-not-found">
<div class="tb-no-entries" ng-if="!textIsNotEmpty(dataKeySearchText)">
<span translate>device.no-keys-found</span>
<section class="tb-datasource-func" flex layout='column'
layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
<md-input-container class="tb-datasource-name" md-no-float style="min-width: 200px;">
<input name="datasourceName"
placeholder="{{ 'datasource.name' | translate }}"
ng-model="datasourceName"
aria-label="{{ 'datasource.name' | translate }}">
</md-input-container>
<section flex layout='column' style="padding-left: 4px;">
<md-chips flex
id="function_datakey_chips"
ng-required="true"
ng-model="funcDataKeys" md-autocomplete-snap
md-transform-chip="transformDataKeyChip($chip)"
md-require-match="false">
<md-autocomplete
md-no-cache="false"
id="dataKey"
md-selected-item="selectedDataKey"
md-search-text="dataKeySearchText"
md-items="item in dataKeysSearch(dataKeySearchText)"
md-item-text="item.name"
md-min-length="0"
placeholder="{{ 'datakey.function-types' | translate }}"
md-menu-class="tb-func-datakey-autocomplete">
<span md-highlight-text="dataKeySearchText" md-highlight-flags="^i">{{item}}</span>
<md-not-found>
<div class="tb-not-found">
<div class="tb-no-entries" ng-if="!textIsNotEmpty(dataKeySearchText)">
<span translate>device.no-keys-found</span>
</div>
<div ng-if="textIsNotEmpty(dataKeySearchText)">
<span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:&apos;...&apos;}}" }'>device.no-key-matching</span>
<span>
<a translate ng-click="createKey($event, '#function_datakey_chips')">device.create-new-key</a>
</span>
</div>
</div>
<div ng-if="textIsNotEmpty(dataKeySearchText)">
<span translate translate-values='{ key: "{{dataKeySearchText | truncate:true:6:&apos;...&apos;}}" }'>device.no-key-matching</span>
<span>
<a translate ng-click="createKey($event, '#function_datakey_chips')">device.create-new-key</a>
</span>
</div>
</div>
</md-not-found>
</md-autocomplete>
<md-chip-template>
<div layout="row" layout-align="start center" class="tb-attribute-chip">
<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
</div>
<div layout="row" flex>
<div class="tb-chip-label">
{{$chip.label}}
</md-not-found>
</md-autocomplete>
<md-chip-template>
<div layout="row" layout-align="start center" class="tb-attribute-chip">
<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
</div>
<div class="tb-chip-separator">: </div>
<div class="tb-chip-label">
<strong>{{$chip.name}}</strong>
<div layout="row" flex>
<div class="tb-chip-label">
{{$chip.label}}
</div>
<div class="tb-chip-separator">: </div>
<div class="tb-chip-label">
<strong>{{$chip.name}}</strong>
</div>
</div>
<md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
<md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
</md-button>
</div>
<md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
<md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
</md-button>
</div>
</md-chip-template>
</md-chips>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
<div translate ng-message="funcTypes" class="tb-error-message">datakey.function-types-required</div>
</div>
</md-chip-template>
</md-chips>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
<div translate ng-message="funcTypes" class="tb-error-message">datakey.function-types-required</div>
</div>
</section>
</section>

2
ui/src/app/components/datasource.directive.js

@ -76,7 +76,7 @@ function Datasource($compile, $templateCache, types) {
restrict: "E",
require: "^ngModel",
scope: {
entityAliases: '=',
aliasController: '=',
widgetType: '=',
functionsOnly: '=',
datakeySettingsSchema: '=',

2
ui/src/app/components/datasource.tpl.html

@ -37,7 +37,7 @@
ng-switch-when="entity"
ng-required="model.type === types.datasourceType.entity"
widget-type="widgetType"
entity-aliases="entityAliases"
alias-controller="aliasController"
generate-data-key="generateDataKey({chip: chip, type: type})"
fetch-entity-keys="fetchEntityKeys({entityAliasId: entityAliasId, query: query, type: type})"
on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias})">

17
ui/src/app/components/entity-alias-select.directive.js

@ -31,7 +31,7 @@ export default angular.module('thingsboard.directives.entityAliasSelect', [])
.name;
/*@ngInject*/
function EntityAliasSelect($compile, $templateCache, $mdConstant) {
function EntityAliasSelect($compile, $templateCache, $mdConstant, entityService) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(entityAliasSelectTemplate);
@ -49,19 +49,18 @@ function EntityAliasSelect($compile, $templateCache, $mdConstant) {
ngModelCtrl.$setValidity('entityAlias', valid);
};
scope.$watch('entityAliases', function () {
scope.$watch('aliasController', function () {
scope.entityAliasList = [];
for (var aliasId in scope.entityAliases) {
var entityAliases = scope.aliasController.getEntityAliases();
for (var aliasId in entityAliases) {
if (scope.allowedEntityTypes) {
if (scope.allowedEntityTypes.indexOf(scope.entityAliases[aliasId].entityType) === -1) {
if (!entityService.filterAliasByEntityTypes(entityAliases[aliasId], scope.allowedEntityTypes)) {
continue;
}
}
var entityAlias = {id: aliasId, alias: scope.entityAliases[aliasId].alias,
entityType: scope.entityAliases[aliasId].entityType, entityId: scope.entityAliases[aliasId].entityId};
scope.entityAliasList.push(entityAlias);
scope.entityAliasList.push(entityAliases[aliasId]);
}
}, true);
});
scope.$watch('entityAlias', function () {
scope.updateView();
@ -141,7 +140,7 @@ function EntityAliasSelect($compile, $templateCache, $mdConstant) {
link: linker,
scope: {
tbRequired: '=?',
entityAliases: '=',
aliasController: '=',
allowedEntityTypes: '=?',
onCreateEntityAlias: '&'
}

12
ui/src/app/components/widget-config.directive.js

@ -128,13 +128,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
} else if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) {
var aliasId = config.targetDeviceAliasIds[0];
if (scope.entityAliases[aliasId]) {
scope.targetDeviceAlias.value = {
id: aliasId,
alias: scope.entityAliases[aliasId].alias,
entityType: scope.entityAliases[aliasId].entityType,
entityId: scope.entityAliases[aliasId].entityId
};
var entityAliases = scope.aliasController.getEntityAliases();
if (entityAliases[aliasId]) {
scope.targetDeviceAlias.value = entityAliases[aliasId];
} else {
scope.targetDeviceAlias.value = null;
}
@ -395,7 +391,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
widgetType: '=',
widgetSettingsSchema: '=',
datakeySettingsSchema: '=',
entityAliases: '=',
aliasController: '=',
functionsOnly: '=',
fetchEntityKeys: '&',
onCreateEntityAlias: '&',

4
ui/src/app/components/widget-config.tpl.html

@ -60,7 +60,7 @@
style="padding: 0 0 0 10px; margin: 5px;">
<tb-datasource flex ng-model="datasource.value"
widget-type="widgetType"
entity-aliases="entityAliases"
alias-controller="aliasController"
functions-only="functionsOnly"
datakey-settings-schema="datakeySettingsSchema"
generate-data-key="generateDataKey(chip,type)"
@ -104,7 +104,7 @@
<v-pane-content style="padding: 0 5px;">
<tb-entity-alias-select flex
tb-required="widgetType === types.widgetType.rpc.value && !widgetEditMode"
entity-aliases="entityAliases"
alias-controller="aliasController"
allowed-entity-types="[types.entityType.device]"
ng-model="targetDeviceAlias.value"
on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias, allowedEntityTypes: allowedEntityTypes})">

320
ui/src/app/components/widget.controller.js

@ -20,9 +20,9 @@ import Subscription from '../api/subscription';
/* eslint-disable angular/angularelement */
/*@ngInject*/
export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, tbRaf, types, utils, timeService,
export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService,
datasourceService, entityService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
dashboardTimewindowApi, widget, aliasesInfo, stateController, widgetType) {
dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) {
var vm = this;
@ -37,23 +37,16 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
$scope.executingRpcRequest = false;
var gridsterItemInited = false;
var subscriptionInited = false;
var widgetSizeDetected = false;
var cafs = {};
/*
* data = array of datasourceData
* datasourceData = {
* tbDatasource,
* dataKey, { name, config }
* data = array of [time, value]
* }
*/
var widgetContext = {
inited: false,
$scope: $scope,
$container: $('#container', $element),
$containerParent: $($element),
$container: null,
$containerParent: null,
width: 0,
height: 0,
isEdit: isEdit,
@ -80,30 +73,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
createSubscription: function(options, subscribe) {
return createSubscription(options, subscribe);
},
// type: "timeseries" or "latest" or "rpc"
/* subscriptionInfo = [
{
entityType: ""
entityId: ""
entityName: ""
timeseries: [{ name: "", label: "" }, ..]
attributes: [{ name: "", label: "" }, ..]
}
..
]*/
// options = {
// timeWindowConfig,
// useDashboardTimewindow,
// legendConfig,
// decimals,
// units,
// callbacks [ onDataUpdated(subscription, apply) ]
// }
//
createSubscriptionFromInfo: function (type, subscriptionsInfo, options, useDefaultComponents, subscribe) {
return createSubscriptionFromInfo(type, subscriptionsInfo, options, useDefaultComponents, subscribe);
},
@ -149,7 +118,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
dashboardTimewindowApi: dashboardTimewindowApi,
types: types,
stDiff: stDiff,
aliasesInfo: aliasesInfo
aliasController: aliasController
};
var widgetTypeInstance;
@ -203,23 +172,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
vm.gridsterItemInitialized = gridsterItemInitialized;
initialize();
/*
options = {
type,
targetDeviceAliasIds, // RPC
targetDeviceIds, // RPC
datasources,
timeWindowConfig,
useDashboardTimewindow,
legendConfig,
decimals,
units,
callbacks
}
*/
initialize().then(
function(){
onInit();
}
);
function createSubscriptionFromInfo(type, subscriptionsInfo, options, useDefaultComponents, subscribe) {
var deferred = $q.defer();
@ -233,28 +190,42 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
}
}
entityService.createDatasoucesFromSubscriptionsInfo(subscriptionsInfo).then(
entityService.createDatasourcesFromSubscriptionsInfo(subscriptionsInfo).then(
function (datasources) {
options.datasources = datasources;
var subscription = createSubscription(options, subscribe);
if (useDefaultComponents) {
defaultSubscriptionOptions(subscription, options);
}
deferred.resolve(subscription);
createSubscription(options, subscribe).then(
function success(subscription) {
if (useDefaultComponents) {
defaultSubscriptionOptions(subscription, options);
}
deferred.resolve(subscription);
},
function fail() {
deferred.reject();
}
);
}
);
return deferred.promise;
}
function createSubscription(options, subscribe) {
var deferred = $q.defer();
options.dashboardTimewindow = dashboardTimewindow;
var subscription =
new Subscription(subscriptionContext, options);
widgetContext.subscriptions[subscription.id] = subscription;
if (subscribe) {
subscription.subscribe();
}
return subscription;
new Subscription(subscriptionContext, options).then(
function success(subscription) {
widgetContext.subscriptions[subscription.id] = subscription;
if (subscribe) {
subscription.subscribe();
}
deferred.resolve(subscription);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function defaultComponentsOptions(options) {
@ -310,8 +281,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
}
function createDefaultSubscription() {
var subscription;
var options;
var deferred = $q.defer();
if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
options = {
type: widget.type,
@ -319,16 +290,23 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
};
defaultComponentsOptions(options);
subscription = createSubscription(options);
defaultSubscriptionOptions(subscription, options);
createSubscription(options).then(
function success(subscription) {
defaultSubscriptionOptions(subscription, options);
// backward compatibility
// backward compatibility
widgetContext.datasources = subscription.datasources;
widgetContext.data = subscription.data;
widgetContext.hiddenData = subscription.hiddenData;
widgetContext.timeWindow = subscription.timeWindow;
widgetContext.datasources = subscription.datasources;
widgetContext.data = subscription.data;
widgetContext.hiddenData = subscription.hiddenData;
widgetContext.timeWindow = subscription.timeWindow;
widgetContext.defaultSubscription = subscription;
deferred.resolve();
},
function fail() {
deferred.reject();
}
);
} else if (widget.type === types.widgetType.rpc.value) {
$scope.loadingData = false;
@ -356,30 +334,126 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
$scope.rpcRejection = null;
}
}
subscription = createSubscription(options);
createSubscription(options).then(
function success(subscription) {
widgetContext.defaultSubscription = subscription;
deferred.resolve();
},
function fail() {
deferred.reject();
}
);
} else if (widget.type === types.widgetType.static.value) {
$scope.loadingData = false;
deferred.resolve();
} else {
deferred.resolve();
}
if (subscription) {
widgetContext.defaultSubscription = subscription;
}
return deferred.promise;
}
function configureWidgetElement() {
function initialize() {
$scope.displayLegend = angular.isDefined(widget.config.showLegend) ?
widget.config.showLegend : widget.type === types.widgetType.timeseries.value;
if (!vm.useCustomDatasources) {
createDefaultSubscription();
if ($scope.displayLegend) {
$scope.legendConfig = widget.config.legendConfig ||
{
position: types.position.bottom.value,
showMin: false,
showMax: false,
showAvg: widget.type === types.widgetType.timeseries.value,
showTotal: false
};
$scope.legendData = {
keys: [],
data: []
};
}
var html = '<div class="tb-absolute-fill tb-widget-error" ng-if="widgetErrorData">' +
'<span>Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}</span>' +
'</div>' +
'<div class="tb-absolute-fill tb-widget-loading" ng-show="loadingData" layout="column" layout-align="center center">' +
'<md-progress-circular md-mode="indeterminate" ng-disabled="!loadingData" class="md-accent" md-diameter="40"></md-progress-circular>' +
'</div>';
var containerHtml = '<div id="container">' + widgetInfo.templateHtml + '</div>';
if ($scope.displayLegend) {
var layoutType;
if ($scope.legendConfig.position === types.position.top.value ||
$scope.legendConfig.position === types.position.bottom.value) {
layoutType = 'column';
} else {
layoutType = 'row';
}
var legendStyle;
switch($scope.legendConfig.position) {
case types.position.top.value:
legendStyle = 'padding-bottom: 8px;';
break;
case types.position.bottom.value:
legendStyle = 'padding-top: 8px;';
break;
case types.position.left.value:
legendStyle = 'padding-right: 0px;';
break;
case types.position.right.value:
legendStyle = 'padding-left: 0px;';
break;
}
var legendHtml = '<tb-legend style="'+legendStyle+'" legend-config="legendConfig" legend-data="legendData"></tb-legend>';
containerHtml = '<div flex id="widget-container">' + containerHtml + '</div>';
html += '<div class="tb-absolute-fill" layout="'+layoutType+'">';
if ($scope.legendConfig.position === types.position.top.value ||
$scope.legendConfig.position === types.position.left.value) {
html += legendHtml;
html += containerHtml;
} else {
html += containerHtml;
html += legendHtml;
}
html += '</div>';
} else {
$scope.loadingData = false;
html += containerHtml;
}
//TODO:
/*if (progressElement) {
progressScope.$destroy();
progressScope = null;
progressElement.remove();
progressElement = null;
}*/
$element.html(html);
var containerElement = $scope.displayLegend ? angular.element($element[0].querySelector('#widget-container')) : $element;
widgetContext.$container = $('#container', containerElement);
widgetContext.$containerParent = $(containerElement);
$compile($element.contents())($scope);
addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
}
function destroyWidgetElement() {
removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
$element.html('');
widgetContext.$container = null;
widgetContext.$containerParent = null;
}
function initialize() {
$scope.$on('toggleDashboardEditMode', function (event, isEdit) {
onEditModeChanged(isEdit);
});
addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
$scope.$watch(function () {
return widget.row + ',' + widget.col + ',' + widget.config.mobileOrder;
}, function () {
@ -398,18 +472,60 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
onMobileModeChanged(newIsMobile);
});
$scope.$on('entityAliasListChanged', function (event, aliasesInfo) {
subscriptionContext.aliasesInfo = aliasesInfo;
$scope.$on('entityAliasesChanged', function (event, aliasIds) {
var subscriptionChanged = false;
for (var id in widgetContext.subscriptions) {
var subscription = widgetContext.subscriptions[id];
subscription.onAliasesChanged();
subscriptionChanged = subscriptionChanged || subscription.onAliasesChanged(aliasIds);
}
if (subscriptionChanged && !vm.useCustomDatasources) {
reInit();
}
});
$scope.$on("$destroy", function () {
removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
onDestroy();
});
configureWidgetElement();
var deferred = $q.defer();
if (!vm.useCustomDatasources) {
createDefaultSubscription().then(
function success() {
subscriptionInited = true;
deferred.resolve();
},
function fail() {
subscriptionInited = true;
deferred.reject();
}
);
} else {
$scope.loadingData = false;
subscriptionInited = true;
deferred.resolve();
}
return deferred.promise;
}
function reInit() {
onDestroy();
configureWidgetElement();
if (!vm.useCustomDatasources) {
createDefaultSubscription().then(
function success() {
subscriptionInited = true;
onInit();
},
function fail() {
subscriptionInited = true;
onInit();
}
);
} else {
subscriptionInited = true;
onInit();
}
}
function handleWidgetException(e) {
@ -417,8 +533,15 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
$scope.widgetErrorData = utils.processWidgetException(e);
}
function onInit() {
if (!widgetContext.inited) {
function isReady() {
return subscriptionInited && gridsterItemInited && widgetSizeDetected;
}
function onInit(skipSizeCheck) {
if (!skipSizeCheck) {
checkSize();
}
if (!widgetContext.inited && isReady()) {
widgetContext.inited = true;
try {
widgetTypeInstance.onInit();
@ -443,6 +566,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
widgetContext.width = width;
widgetContext.height = height;
sizeChanged = true;
widgetSizeDetected = true;
}
}
return sizeChanged;
@ -462,8 +586,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
handleWidgetException(e);
}
});
} else if (gridsterItemInited) {
onInit();
} else {
onInit(true);
}
}
}
@ -472,9 +596,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
if (item && item.gridster) {
widgetContext.isMobile = item.gridster.isMobile;
gridsterItemInited = true;
if (checkSize()) {
onInit();
}
onInit();
// gridsterItemElement = $(item.$element);
//updateVisibility();
}
@ -544,6 +666,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
var subscription = widgetContext.subscriptions[id];
subscription.destroy();
}
subscriptionInited = false;
widgetContext.subscriptions = [];
if (widgetContext.inited) {
widgetContext.inited = false;
@ -559,6 +682,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
handleWidgetException(e);
}
}
destroyWidgetElement();
}
//TODO: widgets visibility

85
ui/src/app/components/widget.directive.js

@ -28,7 +28,7 @@ export default angular.module('thingsboard.directives.widget', [thingsboardLegen
.name;
/*@ngInject*/
function Widget($controller, $compile, types, widgetService) {
function Widget($controller, widgetService) {
return {
scope: true,
link: function (scope, elem, attrs) {
@ -81,90 +81,9 @@ function Widget($controller, $compile, types, widgetService) {
elem.addClass(widgetNamespace);
var html = '<div class="tb-absolute-fill tb-widget-error" ng-if="widgetErrorData">' +
'<span>Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}</span>' +
'</div>' +
'<div class="tb-absolute-fill tb-widget-loading" ng-show="loadingData" layout="column" layout-align="center center">' +
'<md-progress-circular md-mode="indeterminate" ng-disabled="!loadingData" class="md-accent" md-diameter="40"></md-progress-circular>' +
'</div>';
scope.displayLegend = angular.isDefined(widget.config.showLegend) ?
widget.config.showLegend : widget.type === types.widgetType.timeseries.value;
var containerHtml = '<div id="container">' + widgetInfo.templateHtml + '</div>';
if (scope.displayLegend) {
scope.legendConfig = widget.config.legendConfig ||
{
position: types.position.bottom.value,
showMin: false,
showMax: false,
showAvg: widget.type === types.widgetType.timeseries.value,
showTotal: false
};
scope.legendData = {
keys: [],
data: []
};
var layoutType;
if (scope.legendConfig.position === types.position.top.value ||
scope.legendConfig.position === types.position.bottom.value) {
layoutType = 'column';
} else {
layoutType = 'row';
}
var legendStyle;
switch(scope.legendConfig.position) {
case types.position.top.value:
legendStyle = 'padding-bottom: 8px;';
break;
case types.position.bottom.value:
legendStyle = 'padding-top: 8px;';
break;
case types.position.left.value:
legendStyle = 'padding-right: 0px;';
break;
case types.position.right.value:
legendStyle = 'padding-left: 0px;';
break;
}
var legendHtml = '<tb-legend style="'+legendStyle+'" legend-config="legendConfig" legend-data="legendData"></tb-legend>';
containerHtml = '<div flex id="widget-container">' + containerHtml + '</div>';
html += '<div class="tb-absolute-fill" layout="'+layoutType+'">';
if (scope.legendConfig.position === types.position.top.value ||
scope.legendConfig.position === types.position.left.value) {
html += legendHtml;
html += containerHtml;
} else {
html += containerHtml;
html += legendHtml;
}
html += '</div>';
} else {
html += containerHtml;
}
//TODO:
/*if (progressElement) {
progressScope.$destroy();
progressScope = null;
progressElement.remove();
progressElement = null;
}*/
elem.html(html);
var containerElement = scope.displayLegend ? angular.element(elem[0].querySelector('#widget-container')) : elem;
$compile(elem.contents())(scope);
var widgetType = widgetService.getWidgetTypeFunction(widget.bundleAlias, widget.typeAlias, widget.isSystemType);
angular.extend(locals, {$scope: scope, $element: containerElement, widgetType: widgetType});
angular.extend(locals, {$scope: scope, $element: elem, widgetInfo: widgetInfo, widgetType: widgetType});
widgetController = $controller('WidgetController', locals);

7
ui/src/app/customer/customers.tpl.html

@ -48,11 +48,16 @@
disable-attribute-scope-selection="true">
</tb-attribute-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'alarm.alarms' | translate }}">
<tb-alarm-table flex entity-type="vm.types.entityType.customer"
entity-id="vm.grid.operatingItem().id.id">
</tb-alarm-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'customer.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.customer"
entity-id="vm.grid.operatingItem().id.id"
tenant-id="vm.grid.operatingItem().tenantId.id"
default-event-type="{{vm.types.eventType.alarm.value}}">
default-event-type="{{vm.types.eventType.error.value}}">
</tb-event-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">

69
ui/src/app/dashboard/add-widget.controller.js

@ -15,17 +15,18 @@
*/
/* eslint-disable import/no-unresolved, import/default */
import entityAliasesTemplate from '../entity/entity-aliases.tpl.html';
import entityAliasDialogTemplate from '../entity/alias/entity-alias-dialog.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function AddWidgetController($scope, widgetService, entityService, $mdDialog, $q, $document, types, dashboard, aliasesInfo, widget, widgetInfo) {
export default function AddWidgetController($scope, widgetService, entityService, $mdDialog, $q, $document, types, dashboard,
aliasController, widget, widgetInfo) {
var vm = this;
vm.dashboard = dashboard;
vm.aliasesInfo = aliasesInfo;
vm.aliasController = aliasController;
vm.widget = widget;
vm.widgetInfo = widgetInfo;
@ -85,7 +86,7 @@ export default function AddWidgetController($scope, widgetService, entityService
}
function cancel () {
$mdDialog.cancel({aliasesInfo: vm.aliasesInfo});
$mdDialog.cancel();
}
function add () {
@ -94,52 +95,58 @@ export default function AddWidgetController($scope, widgetService, entityService
vm.widget.config = vm.widgetConfig.config;
vm.widget.config.mobileOrder = vm.widgetConfig.layout.mobileOrder;
vm.widget.config.mobileHeight = vm.widgetConfig.layout.mobileHeight;
$mdDialog.hide({widget: vm.widget, aliasesInfo: vm.aliasesInfo});
$mdDialog.hide({widget: vm.widget});
}
}
function fetchEntityKeys (entityAliasId, query, type) {
var entityAlias = vm.aliasesInfo.entityAliases[entityAliasId];
if (entityAlias && entityAlias.entityId) {
return entityService.getEntityKeys(entityAlias.entityType, entityAlias.entityId, query, type);
} else {
return $q.when([]);
}
var deferred = $q.defer();
vm.aliasController.getAliasInfo(entityAliasId).then(
function success(aliasInfo) {
var entity = aliasInfo.currentEntity;
if (entity) {
entityService.getEntityKeys(entity.entityType, entity.id, query, type).then(
function success(keys) {
deferred.resolve(keys);
},
function fail() {
deferred.resolve([]);
}
);
} else {
deferred.resolve([]);
}
},
function fail() {
deferred.resolve([]);
}
);
return deferred.promise;
}
function createEntityAlias (event, alias, allowedEntityTypes) {
var deferred = $q.defer();
var singleEntityAlias = {id: null, alias: alias, entityType: types.entityType.device, entityFilter: null};
var singleEntityAlias = {id: null, alias: alias, filter: {}};
$mdDialog.show({
controller: 'EntityAliasesController',
controller: 'EntityAliasDialogController',
controllerAs: 'vm',
templateUrl: entityAliasesTemplate,
templateUrl: entityAliasDialogTemplate,
locals: {
config: {
entityAliases: angular.copy(vm.dashboard.configuration.entityAliases),
widgets: null,
isSingleEntityAlias: true,
singleEntityAlias: singleEntityAlias,
allowedEntityTypes: allowedEntityTypes
}
isAdd: true,
allowedEntityTypes: allowedEntityTypes,
entityAliases: vm.dashboard.configuration.entityAliases,
alias: singleEntityAlias
},
parent: angular.element($document[0].body),
fullscreen: true,
skipHide: true,
targetEvent: event
}).then(function (singleEntityAlias) {
vm.dashboard.configuration.entityAliases[singleEntityAlias.id] =
{ alias: singleEntityAlias.alias, entityType: singleEntityAlias.entityType, entityFilter: singleEntityAlias.entityFilter };
entityService.processEntityAliases(vm.dashboard.configuration.entityAliases).then(
function(resolution) {
if (!resolution.error) {
vm.aliasesInfo = resolution.aliasesInfo;
}
deferred.resolve(singleEntityAlias);
}
);
vm.dashboard.configuration.entityAliases[singleEntityAlias.id] = singleEntityAlias;
vm.aliasController.updateEntityAliases(vm.dashboard.configuration.entityAliases);
deferred.resolve(singleEntityAlias);
}, function () {
deferred.reject();
});

2
ui/src/app/dashboard/add-widget.tpl.html

@ -37,7 +37,7 @@
ng-model="vm.widgetConfig"
widget-settings-schema="vm.settingsSchema"
datakey-settings-schema="vm.dataKeySettingsSchema"
entity-aliases="vm.aliasesInfo.entityAliases"
alias-controller="vm.aliasController"
functions-only="vm.functionsOnly"
fetch-entity-keys="vm.fetchEntityKeys(entityAliasId, query, type)"
on-create-entity-alias="vm.createEntityAlias(event, alias, allowedEntityTypes)"

41
ui/src/app/dashboard/dashboard.controller.js

@ -15,7 +15,7 @@
*/
/* eslint-disable import/no-unresolved, import/default */
import entityAliasesTemplate from '../entity/entity-aliases.tpl.html';
import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
import dashboardSettingsTemplate from './dashboard-settings.tpl.html';
import manageDashboardLayoutsTemplate from './layouts/manage-dashboard-layouts.tpl.html';
import manageDashboardStatesTemplate from './states/manage-dashboard-states.tpl.html';
@ -24,8 +24,10 @@ import selectTargetLayoutTemplate from './layouts/select-target-layout.tpl.html'
/* eslint-enable import/no-unresolved, import/default */
import AliasController from '../api/alias-controller';
/*@ngInject*/
export default function DashboardController(types, dashboardUtils, widgetService, userService,
export default function DashboardController(types, utils, dashboardUtils, widgetService, userService,
dashboardService, timeService, entityService, itembuffer, importExport, hotkeys, $window, $rootScope,
$scope, $element, $state, $stateParams, $mdDialog, $mdMedia, $timeout, $document, $q, $translate, $filter) {
@ -342,6 +344,8 @@ export default function DashboardController(types, dashboardUtils, widgetService
vm.dashboardConfiguration = vm.dashboard.configuration;
vm.dashboardCtx.dashboard = vm.dashboard;
vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow;
vm.dashboardCtx.aliasController = new AliasController($scope, $q, $filter, utils,
types, entityService, vm.dashboardCtx.stateController, vm.dashboardConfiguration.entityAliases);
var parentScope = $window.parent.angular.element($window.frameElement).scope();
parentScope.$root.$broadcast('widgetEditModeInited');
parentScope.$root.$apply();
@ -349,7 +353,13 @@ export default function DashboardController(types, dashboardUtils, widgetService
dashboardService.getDashboard($stateParams.dashboardId)
.then(function success(dashboard) {
vm.dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard);
entityService.processEntityAliases(vm.dashboard.configuration.entityAliases)
vm.dashboardConfiguration = vm.dashboard.configuration;
vm.dashboardCtx.dashboard = vm.dashboard;
vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow;
vm.dashboardCtx.aliasController = new AliasController($scope, $q, $filter, utils,
types, entityService, vm.dashboardCtx.stateController, vm.dashboardConfiguration.entityAliases);
/* entityService.processEntityAliases(vm.dashboard.configuration.entityAliases)
.then(
function(resolution) {
if (resolution.error && !isTenantAdmin()) {
@ -362,7 +372,7 @@ export default function DashboardController(types, dashboardUtils, widgetService
vm.dashboardCtx.dashboardTimewindow = vm.dashboardConfiguration.timewindow;
}
}
);
);*/
}, function fail() {
vm.configurationError = true;
});
@ -373,6 +383,7 @@ export default function DashboardController(types, dashboardUtils, widgetService
var layoutsData = dashboardUtils.getStateLayoutsData(vm.dashboard, state);
if (layoutsData) {
vm.dashboardCtx.state = state;
vm.dashboardCtx.aliasController.dashboardStateChanged();
var layoutVisibilityChanged = false;
for (var l in vm.layouts) {
var layout = vm.layouts[l];
@ -916,7 +927,7 @@ export default function DashboardController(types, dashboardUtils, widgetService
templateUrl: addWidgetTemplate,
locals: {
dashboard: vm.dashboard,
aliasesInfo: vm.dashboardCtx.aliasesInfo,
aliasController: vm.dashboardCtx.aliasController,
widget: newWidget,
widgetInfo: widgetTypeInfo
},
@ -930,10 +941,8 @@ export default function DashboardController(types, dashboardUtils, widgetService
}
}).then(function (result) {
var widget = result.widget;
vm.dashboardCtx.aliasesInfo = result.aliasesInfo;
addWidget(widget);
}, function (rejection) {
vm.dashboardCtx.aliasesInfo = rejection.aliasesInfo;
}, function () {
});
}
}
@ -1025,7 +1034,7 @@ export default function DashboardController(types, dashboardUtils, widgetService
notifyDashboardUpdated();
}
function showAliasesResolutionError(error) {
/* function showAliasesResolutionError(error) {
var alert = $mdDialog.alert()
.parent(angular.element($document[0].body))
.clickOutsideToClose(true)
@ -1037,20 +1046,10 @@ export default function DashboardController(types, dashboardUtils, widgetService
alert._options.fullscreen = true;
$mdDialog.show(alert);
}
}*/
function entityAliasesUpdated() {
var deferred = $q.defer();
entityService.processEntityAliases(vm.dashboard.configuration.entityAliases)
.then(
function(resolution) {
if (resolution.aliasesInfo) {
vm.dashboardCtx.aliasesInfo = resolution.aliasesInfo;
}
deferred.resolve();
}
);
return deferred.promise;
vm.dashboardCtx.aliasController.updateEntityAliases(vm.dashboard.configuration.entityAliases);
}
function notifyDashboardUpdated() {

5
ui/src/app/dashboard/dashboard.tpl.html

@ -57,8 +57,7 @@
</tb-timewindow>
<tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()"
tooltip-direction="bottom"
ng-model="vm.dashboardCtx.aliasesInfo.entityAliases"
entity-aliases-info="vm.dashboardCtx.aliasesInfo.entityAliasesInfo">
alias-controller="vm.dashboardCtx.aliasController">
</tb-aliases-entity-select>
<md-button ng-show="vm.isEdit" aria-label="{{ 'entity.aliases' | translate }}" class="md-icon-button"
ng-click="vm.openEntityAliases($event)">
@ -179,7 +178,7 @@
<form name="vm.widgetForm" ng-if="vm.isEditingWidget">
<tb-edit-widget
dashboard="vm.dashboard"
aliases-info="vm.dashboardCtx.aliasesInfo"
alias-controller="vm.dashboardCtx.aliasController"
widget="vm.editingWidget"
widget-layout="vm.editingWidgetLayout"
the-form="vm.widgetForm">

62
ui/src/app/dashboard/edit-widget.directive.js

@ -15,7 +15,7 @@
*/
/* eslint-disable import/no-unresolved, import/default */
import entityAliasesTemplate from '../entity/entity-aliases.tpl.html';
import entityAliasDialogTemplate from '../entity/alias/entity-alias-dialog.tpl.html';
import editWidgetTemplate from './edit-widget.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
@ -68,47 +68,53 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
});
scope.fetchEntityKeys = function (entityAliasId, query, type) {
var entityAlias = scope.aliasesInfo.entityAliases[entityAliasId];
if (entityAlias && entityAlias.entityId) {
return entityService.getEntityKeys(entityAlias.entityType, entityAlias.entityId, query, type);
} else {
return $q.when([]);
}
var deferred = $q.defer();
scope.aliasController.getAliasInfo(entityAliasId).then(
function success(aliasInfo) {
var entity = aliasInfo.currentEntity;
if (entity) {
entityService.getEntityKeys(entity.entityType, entity.id, query, type).then(
function success(keys) {
deferred.resolve(keys);
},
function fail() {
deferred.resolve([]);
}
);
} else {
deferred.resolve([]);
}
},
function fail() {
deferred.resolve([]);
}
);
return deferred.promise;
};
scope.createEntityAlias = function (event, alias, allowedEntityTypes) {
var deferred = $q.defer();
var singleEntityAlias = {id: null, alias: alias, entityType: types.entityType.device, entityFilter: null};
var singleEntityAlias = {id: null, alias: alias, filter: {}};
$mdDialog.show({
controller: 'EntityAliasesController',
controller: 'EntityAliasDialogController',
controllerAs: 'vm',
templateUrl: entityAliasesTemplate,
templateUrl: entityAliasDialogTemplate,
locals: {
config: {
entityAliases: angular.copy(scope.dashboard.configuration.entityAliases),
widgets: null,
isSingleEntityAlias: true,
singleEntityAlias: singleEntityAlias,
allowedEntityTypes: allowedEntityTypes
}
isAdd: true,
allowedEntityTypes: allowedEntityTypes,
entityAliases: scope.dashboard.configuration.entityAliases,
alias: singleEntityAlias
},
parent: angular.element($document[0].body),
fullscreen: true,
skipHide: true,
targetEvent: event
}).then(function (singleEntityAlias) {
scope.dashboard.configuration.entityAliases[singleEntityAlias.id] =
{ alias: singleEntityAlias.alias, entityType: singleEntityAlias.entityType, entityFilter: singleEntityAlias.entityFilter };
entityService.processEntityAliases(scope.dashboard.configuration.entityAliases).then(
function(resolution) {
if (!resolution.error) {
scope.aliasesInfo = resolution.aliasesInfo;
}
deferred.resolve(singleEntityAlias);
}
);
scope.dashboard.configuration.entityAliases[singleEntityAlias.id] = singleEntityAlias;
scope.aliasController.updateEntityAliases(scope.dashboard.configuration.entityAliases);
deferred.resolve(singleEntityAlias);
}, function () {
deferred.reject();
});
@ -124,7 +130,7 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
link: linker,
scope: {
dashboard: '=',
aliasesInfo: '=',
aliasController: '=',
widget: '=',
widgetLayout: '=',
theForm: '='

2
ui/src/app/dashboard/edit-widget.tpl.html

@ -21,7 +21,7 @@
is-data-enabled="isDataEnabled"
widget-settings-schema="settingsSchema"
datakey-settings-schema="dataKeySettingsSchema"
entity-aliases="aliasesInfo.entityAliases"
alias-controller="aliasController"
functions-only="functionsOnly"
fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)"
on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)"

2
ui/src/app/dashboard/layouts/dashboard-layout.tpl.html

@ -45,7 +45,7 @@
widget-layouts="vm.layoutCtx.widgetLayouts"
columns="vm.layoutCtx.gridSettings.columns"
margins="vm.layoutCtx.gridSettings.margins"
aliases-info="vm.dashboardCtx.aliasesInfo"
alias-controller="vm.dashboardCtx.aliasController"
state-controller="vm.dashboardCtx.stateController"
dashboard-timewindow="vm.dashboardCtx.dashboardTimewindow"
is-edit="vm.isEdit"

7
ui/src/app/device/devices.tpl.html

@ -49,11 +49,16 @@
disable-attribute-scope-selection="true">
</tb-attribute-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'alarm.alarms' | translate }}">
<tb-alarm-table flex entity-type="vm.types.entityType.device"
entity-id="vm.grid.operatingItem().id.id">
</tb-alarm-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'device.events' | translate }}">
<tb-event-table flex entity-type="vm.types.entityType.device"
entity-id="vm.grid.operatingItem().id.id"
tenant-id="vm.grid.operatingItem().tenantId.id"
default-event-type="{{vm.types.eventType.alarm.value}}">
default-event-type="{{vm.types.eventType.error.value}}">
</tb-event-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">

2
ui/src/app/device/index.js

@ -15,7 +15,6 @@
*/
import uiRouter from 'angular-ui-router';
import thingsboardGrid from '../components/grid.directive';
import thingsboardEvent from '../event';
import thingsboardApiUser from '../api/user.service';
import thingsboardApiDevice from '../api/device.service';
import thingsboardApiCustomer from '../api/customer.service';
@ -30,7 +29,6 @@ import DeviceDirective from './device.directive';
export default angular.module('thingsboard.device', [
uiRouter,
thingsboardGrid,
thingsboardEvent,
thingsboardApiUser,
thingsboardApiDevice,
thingsboardApiCustomer

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

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

@ -0,0 +1,49 @@
/*
* 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.
*/
/*@ngInject*/
export default function AliasesEntitySelectPanelController(mdPanelRef, $scope, $filter, types, aliasController, onEntityAliasesUpdate) {
var vm = this;
vm._mdPanelRef = mdPanelRef;
vm.aliasController = aliasController;
vm.onEntityAliasesUpdate = onEntityAliasesUpdate;
vm.entityAliases = {};
vm.entityAliasesInfo = {};
vm.currentAliasEntityChanged = currentAliasEntityChanged;
var allEntityAliases = vm.aliasController.getEntityAliases();
for (var aliasId in allEntityAliases) {
var aliasInfo = vm.aliasController.getInstantAliasInfo(aliasId);
if (aliasInfo && !aliasInfo.resolveMultiple && aliasInfo.currentEntity) {
vm.entityAliasesInfo[aliasId] = angular.copy(aliasInfo);
vm.entityAliasesInfo[aliasId].selectedId = aliasInfo.currentEntity.id;
}
}
function currentAliasEntityChanged(aliasId, selectedId) {
var resolvedEntities = vm.entityAliasesInfo[aliasId].resolvedEntities;
var selected = $filter('filter')(resolvedEntities, {id: selectedId});
if (selected && selected.length) {
vm.aliasController.updateCurrentAliasEntity(aliasId, selected[0]);
if (onEntityAliasesUpdate) {
onEntityAliasesUpdate();
}
}
}
}

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

@ -18,12 +18,12 @@
<md-content flex layout="column">
<section flex layout="column">
<md-content flex class="md-padding" layout="column">
<div flex layout="row" ng-repeat="(aliasId, entityAlias) in vm.entityAliases">
<div flex layout="row" ng-repeat="(aliasId, entityAliasInfo) in vm.entityAliasesInfo">
<md-input-container flex>
<label>{{entityAlias.alias}}</label>
<md-select ng-model="vm.entityAliases[aliasId].entityId">
<md-option ng-repeat="entityInfo in vm.entityAliasesInfo[aliasId]" ng-value="entityInfo.id">
{{entityInfo.name}}
<label>{{entityAliasInfo.alias}}</label>
<md-select ng-model="entityAliasInfo.selectedId" ng-change="vm.currentAliasEntityChanged(aliasId, entityAliasInfo.selectedId)">
<md-option ng-repeat="resolvedEntity in entityAliasInfo.resolvedEntities" ng-value="resolvedEntity.id">
{{resolvedEntity.name}}
</md-option>
</md-select>
</md-input-container>

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

@ -29,7 +29,7 @@ import aliasesEntitySelectPanelTemplate from './aliases-entity-select-panel.tpl.
/*@ngInject*/
export default function AliasesEntitySelectDirective($compile, $templateCache, $mdMedia, types, $mdPanel, $document, $translate) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var linker = function (scope, element, attrs) {
/* tbAliasesEntitySelect (ng-model)
* {
@ -81,10 +81,8 @@ export default function AliasesEntitySelectDirective($compile, $templateCache, $
position: position,
fullscreen: false,
locals: {
'entityAliases': angular.copy(scope.model),
'entityAliasesInfo': scope.entityAliasesInfo,
'onEntityAliasesUpdate': function (entityAliases) {
scope.model = entityAliases;
'aliasController': scope.aliasController,
'onEntityAliasesUpdate': function () {
scope.updateView();
}
},
@ -96,41 +94,40 @@ export default function AliasesEntitySelectDirective($compile, $templateCache, $
$mdPanel.open(config);
}
scope.$on('entityAliasesChanged', function() {
scope.updateView();
});
scope.$on('entityAliasResolved', function() {
scope.updateView();
});
scope.updateView = function () {
var value = angular.copy(scope.model);
ngModelCtrl.$setViewValue(value);
updateDisplayValue();
}
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
scope.model = angular.copy(value);
updateDisplayValue();
}
}
function updateDisplayValue() {
var displayValue;
var singleValue = true;
var currentAliasId;
for (var aliasId in scope.model) {
if (!currentAliasId) {
currentAliasId = aliasId;
} else {
singleValue = false;
break;
var entityAliases = scope.aliasController.getEntityAliases();
for (var aliasId in entityAliases) {
var entityAlias = entityAliases[aliasId];
if (!entityAlias.filter.resolveMultiple) {
var resolvedAlias = scope.aliasController.getInstantAliasInfo(aliasId);
if (resolvedAlias && resolvedAlias.currentEntity) {
if (!currentAliasId) {
currentAliasId = aliasId;
} else {
singleValue = false;
break;
}
}
}
}
if (singleValue && currentAliasId) {
var entityId = scope.model[currentAliasId].entityId;
var entitiesInfo = scope.entityAliasesInfo[currentAliasId];
for (var i=0;i<entitiesInfo.length;i++) {
if (entitiesInfo[i].id === entityId) {
displayValue = entitiesInfo[i].name;
break;
}
}
var aliasInfo = scope.aliasController.getInstantAliasInfo(currentAliasId);
displayValue = aliasInfo.currentEntity.name;
} else {
displayValue = $translate.instant('entity.entities');
}
@ -142,9 +139,8 @@ export default function AliasesEntitySelectDirective($compile, $templateCache, $
return {
restrict: "E",
require: "^ngModel",
scope: {
entityAliasesInfo:'='
aliasController:'='
},
link: linker
};

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

109
ui/src/app/entity/alias/entity-alias-dialog.controller.js

@ -0,0 +1,109 @@
/*
* 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 './entity-alias-dialog.scss';
/*@ngInject*/
export default function EntityAliasDialogController($scope, $mdDialog, $q, $filter, utils, entityService, types, isAdd, allowedEntityTypes, entityAliases, alias) {
var vm = this;
vm.types = types;
vm.isAdd = isAdd;
vm.allowedEntityTypes = allowedEntityTypes;
if (angular.isArray(entityAliases)) {
vm.entityAliases = entityAliases;
} else {
vm.entityAliases = [];
for (var aliasId in entityAliases) {
vm.entityAliases.push(entityAliases[aliasId]);
}
}
if (vm.isAdd && !alias) {
vm.alias = {
alias: '',
filter: {
resolveMultiple: false
}
};
} else {
vm.alias = alias;
}
vm.cancel = cancel;
vm.save = save;
$scope.$watch('vm.alias.alias', function (newAlias) {
if (newAlias) {
var valid = true;
var result = $filter('filter')(vm.entityAliases, {alias: newAlias}, true);
if (result && result.length) {
if (vm.isAdd || vm.alias.id != result[0].id) {
valid = false;
}
}
$scope.theForm.aliasName.$setValidity('duplicateAliasName', valid);
}
});
$scope.$watch('theForm.$pristine', function() {
if ($scope.theForm && !$scope.theForm.$pristine) {
$scope.theForm.$setValidity('entityFilter', true);
}
});
function validate() {
var deferred = $q.defer();
var validationResult = {
entity: null,
stateEntity: false
}
entityService.resolveAliasFilter(vm.alias.filter, null, 1).then(
function success(result) {
validationResult.stateEntity = result.stateEntity;
var entities = result.entities;
if (entities.length) {
validationResult.entity = entities[0];
}
deferred.resolve(validationResult);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function cancel() {
$mdDialog.cancel();
}
function save() {
$scope.theForm.$setPristine();
validate().then(
function success() {
if (vm.isAdd) {
vm.alias.id = utils.guid();
}
$mdDialog.hide(vm.alias);
},
function fail() {
$scope.theForm.$setValidity('entityFilter', false);
}
)
}
}

19
ui/src/app/entity/entity-aliases.scss → ui/src/app/entity/alias/entity-alias-dialog.scss

@ -14,15 +14,14 @@
* limitations under the License.
*/
.tb-aliases-dialog {
.md-dialog-content {
padding-bottom: 0px;
}
.tb-alias {
padding: 10px 0 0 10px;
margin: 5px;
md-select.tb-entity-type-select {
padding-bottom: 24px;
.tb-entity-alias-dialog {
.tb-resolve-multiple-switch {
padding-left: 10px;
.resolve-multiple-switch {
margin: 0;
}
.resolve-multiple-label {
margin: 5px 0;
}
}
}
}

73
ui/src/app/entity/alias/entity-alias-dialog.tpl.html

@ -0,0 +1,73 @@
<!--
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-dialog class="tb-entity-alias-dialog" style="width: 600px;" aria-label="{{ (vm.isAdd ? 'alias.add' : 'alias.edit') | translate }}">
<form name="theForm" ng-submit="vm.save()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2>{{ (vm.isAdd ? 'alias.add' : 'alias.edit') | translate }}</h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!loading"></span>
<md-dialog-content>
<div class="md-dialog-content">
<fieldset ng-disabled="loading">
<div flex layout="column">
<div layout="row">
<md-input-container flex class="md-block">
<label translate>alias.name</label>
<input required name="aliasName"
ng-model="vm.alias.alias"
aria-label="{{ 'alias.name' | translate }}">
<div ng-messages="theForm.aliasName.$error">
<div ng-message="required" translate>alias.name-required</div>
<div ng-message="duplicateAliasName" translate>alias.duplicate-alias</div>
</div>
</md-input-container>
<section class="tb-resolve-multiple-switch" layout="column" layout-align="start center">
<label class="tb-small resolve-multiple-label" translate>alias.resolve-multiple</label>
<md-switch class="resolve-multiple-switch" ng-model="vm.alias.filter.resolveMultiple"
aria-label="{{ 'alias.resolve-multiple' | translate }}">
</md-switch>
</section>
</div>
<tb-entity-filter
ng-model="vm.alias.filter"
allowed-entity-types="vm.allowedEntityTypes"
the-form="theForm">
</tb-entity-filter>
<div class="tb-error-messages" ng-messages="theForm.$error" role="alert">
<div translate ng-message="entityFilter" class="tb-error-message">alias.entity-filter-no-entity-matched</div>
</div>
</div>
</fieldset>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
{{ (vm.isAdd ? 'action.add' : 'action.save') | translate }}
</md-button>
<md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
</md-dialog-actions>
</form>
</md-dialog>

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

@ -15,22 +15,27 @@
*/
import './entity-aliases.scss';
/* eslint-disable import/no-unresolved, import/default */
import entityAliasDialogTemplate from './entity-alias-dialog.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function EntityAliasesController(utils, entityService, toast, $scope, $mdDialog, $document, $q, $translate,
types, config) {
var vm = this;
vm.isSingleEntityAlias = config.isSingleEntityAlias;
vm.singleEntityAlias = config.singleEntityAlias;
vm.types = types;
vm.entityAliases = [];
vm.title = config.customTitle ? config.customTitle : 'entity.aliases';
vm.disableAdd = config.disableAdd;
vm.aliasToWidgetsMap = {};
vm.allowedEntityTypes = config.allowedEntityTypes;
vm.onFilterEntityChanged = onFilterEntityChanged;
vm.addAlias = addAlias;
vm.editAlias = editAlias;
vm.removeAlias = removeAlias;
vm.cancel = cancel;
@ -79,51 +84,61 @@ export default function EntityAliasesController(utils, entityService, toast, $sc
}
}
if (vm.isSingleEntityAlias) {
checkEntityAlias(vm.singleEntityAlias);
}
for (aliasId in config.entityAliases) {
var entityAlias = config.entityAliases[aliasId];
var result = {id: aliasId, alias: entityAlias.alias, entityType: entityAlias.entityType, entityFilter: entityAlias.entityFilter, changed: true};
checkEntityAlias(result);
var filter = entityAlias.filter;
if (!filter) {
filter = {
resolveMultiple: false
};
}
if (!filter.resolveMultiple) {
filter.resolveMultiple = false;
}
var result = {id: aliasId, alias: entityAlias.alias, filter: filter};
vm.entityAliases.push(result);
}
}
function checkEntityAlias(entityAlias) {
if (!entityAlias.entityType) {
entityAlias.entityType = types.entityType.device;
}
if (!entityAlias.entityFilter || entityAlias.entityFilter == null) {
entityAlias.entityFilter = {
useFilter: false,
entityNameFilter: '',
entityList: [],
};
}
function addAlias($event) {
openAliasDialog($event);
}
function onFilterEntityChanged(entity, entityAlias) {
if (entityAlias) {
if (!entityAlias.alias || entityAlias.alias.length == 0) {
entityAlias.changed = false;
}
if (!entityAlias.changed && entity && entityAlias.entityType) {
entityAlias.alias = entity.name;
}
}
function editAlias($event, entityAlias) {
openAliasDialog($event, entityAlias);
}
function addAlias() {
var aliasId = 0;
for (var a in vm.entityAliases) {
aliasId = Math.max(vm.entityAliases[a].id, aliasId);
function openAliasDialog($event, entityAlias) {
var isAdd = entityAlias ? false : true;
var aliasIndex;
if (!isAdd) {
aliasIndex = vm.entityAliases.indexOf(entityAlias);
}
aliasId++;
var entityAlias = {id: aliasId, alias: '', entityType: types.entityType.device,
entityFilter: {useFilter: false, entityNameFilter: '', entityList: []}, changed: false};
vm.entityAliases.push(entityAlias);
$mdDialog.show({
controller: 'EntityAliasDialogController',
controllerAs: 'vm',
templateUrl: entityAliasDialogTemplate,
locals: {
isAdd: isAdd,
allowedEntityTypes: vm.allowedEntityTypes,
entityAliases: vm.entityAliases,
alias: isAdd ? null : angular.copy(entityAlias)
},
parent: angular.element($document[0].body),
fullscreen: true,
skipHide: true,
targetEvent: $event
}).then(function (alias) {
if (isAdd) {
vm.entityAliases.push(alias);
} else {
vm.entityAliases[aliasIndex] = alias;
}
if ($scope.theForm) {
$scope.theForm.$setDirty();
}
}, function () {
});
}
function removeAlias($event, entityAlias) {
@ -160,61 +175,36 @@ export default function EntityAliasesController(utils, entityService, toast, $sc
$mdDialog.cancel();
}
function cleanupEntityFilter(entityFilter) {
if (entityFilter.useFilter) {
entityFilter.entityList = [];
} else {
entityFilter.entityNameFilter = '';
}
return entityFilter;
}
function save() {
var entityAliases = {};
var uniqueAliasList = {};
var valid = true;
var aliasId, maxAliasId;
var alias;
var i;
if (vm.isSingleEntityAlias) {
maxAliasId = 0;
vm.singleEntityAlias.entityFilter = cleanupEntityFilter(vm.singleEntityAlias.entityFilter);
for (i = 0; i < vm.entityAliases.length; i ++) {
aliasId = vm.entityAliases[i].id;
alias = vm.entityAliases[i].alias;
if (alias === vm.singleEntityAlias.alias) {
valid = false;
break;
}
maxAliasId = Math.max(aliasId, maxAliasId);
}
maxAliasId++;
vm.singleEntityAlias.id = maxAliasId;
} else {
for (i = 0; i < vm.entityAliases.length; i++) {
aliasId = vm.entityAliases[i].id;
alias = vm.entityAliases[i].alias;
if (!uniqueAliasList[alias]) {
uniqueAliasList[alias] = alias;
entityAliases[aliasId] = {alias: alias, entityType: vm.entityAliases[i].entityType, entityFilter: cleanupEntityFilter(vm.entityAliases[i].entityFilter)};
} else {
valid = false;
break;
}
var message, aliasId, alias, filter;
for (var i = 0; i < vm.entityAliases.length; i++) {
aliasId = vm.entityAliases[i].id;
alias = vm.entityAliases[i].alias;
filter = vm.entityAliases[i].filter;
if (uniqueAliasList[alias]) {
valid = false;
message = $translate.instant('entity.duplicate-alias-error', {alias: alias});
break;
} else if (!filter || !filter.type) {
valid = false;
message = $translate.instant('entity.missing-entity-filter-error', {alias: alias});
break;
} else {
uniqueAliasList[alias] = alias;
entityAliases[aliasId] = {id: aliasId, alias: alias, filter: filter};
}
}
if (valid) {
$scope.theForm.$setPristine();
if (vm.isSingleEntityAlias) {
$mdDialog.hide(vm.singleEntityAlias);
} else {
$mdDialog.hide(entityAliases);
}
$mdDialog.hide(entityAliases);
} else {
toast.showError($translate.instant('entity.duplicate-alias-error', {alias: alias}));
toast.showError(message);
}
}

49
ui/src/app/entity/alias/entity-aliases.scss

@ -0,0 +1,49 @@
/**
* 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-aliases-dialog {
.md-dialog-content {
padding-bottom: 0px;
padding-top: 0px;
}
.tb-aliases-header {
min-height: 40px;
padding: 0 34px 0 34px;
margin: 5px;
.tb-header-label {
font-size: 14px;
color: rgba(0, 0, 0, 0.570588);
}
}
.tb-alias {
padding: 0 0 0 10px;
margin: 5px;
md-input-container {
margin: 0px;
}
.tb-resolve-multiple-switch {
padding-left: 10px;
.resolve-multiple-switch {
margin: 0;
}
}
.md-button {
&.md-icon-button {
margin: 0px;
}
}
}
}

105
ui/src/app/entity/alias/entity-aliases.tpl.html

@ -0,0 +1,105 @@
<!--
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-dialog class="tb-aliases-dialog" style="width: 700px;" aria-label="{{ vm.title | translate }}">
<form name="theForm" ng-submit="vm.save()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2>{{ vm.title | translate }}</h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!loading"></span>
<div class="tb-aliases-header" flex layout="row" layout-align="start center">
<span flex="5"></span>
<div flex layout="row" layout-align="start center">
<span class="tb-header-label" translate flex="20" style="min-width: 150px;">alias.name</span>
<span class="tb-header-label" translate flex="70" style="padding-left: 10px;">alias.entity-filter</span>
<span class="tb-header-label" translate flex="10" style="padding-left: 10px; min-width: 120px;">alias.resolve-multiple</span>
<span style="min-width: 80px;"></span>
</div>
</div>
<md-divider></md-divider>
<md-dialog-content>
<div class="md-dialog-content">
<fieldset ng-disabled="loading">
<div ng-form name="aliasForm" flex layout="row" layout-align="start center" ng-repeat="entityAlias in vm.entityAliases track by $index">
<span flex="5">{{$index + 1}}.</span>
<di class="md-whiteframe-4dp tb-alias" flex layout="row" layout-align="start center">
<md-input-container flex="20" style="min-width: 150px;" md-no-float class="md-block">
<input required name="alias" placeholder="{{ 'entity.alias' | translate }}" ng-model="entityAlias.alias">
<div ng-messages="aliasForm.alias.$error">
<div translate ng-message="required">entity.alias-required</div>
</div>
</md-input-container>
<tb-entity-filter-view
flex="70" style="padding-left: 10px;"
ng-model="entityAlias.filter">
</tb-entity-filter-view>
<section flex="10" style="padding-left: 10px; min-width: 120px;"
class="tb-resolve-multiple-switch"
layout="column"
layout-align="center center">
<md-switch class="resolve-multiple-switch"
ng-model="entityAlias.filter.resolveMultiple"
aria-label="resolve-multiple-switcher">
</md-switch>
</section>
<md-button ng-disabled="loading" class="md-icon-button md-primary" style="min-width: 40px;"
ng-click="vm.editAlias($event, entityAlias)" aria-label="{{ 'action.edit' | translate }}">
<md-tooltip md-direction="top">
{{ 'alias.edit' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'alias.edit' | translate }}" class="material-icons">
edit
</md-icon>
</md-button>
<md-button ng-disabled="loading" class="md-icon-button md-primary" style="min-width: 40px;"
ng-click="vm.removeAlias($event, entityAlias)" aria-label="{{ 'action.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'entity.remove-alias' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">
close
</md-icon>
</md-button>
</di>
</div>
</fieldset>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<md-button ng-show="!vm.disableAdd" ng-disabled="loading" class="md-primary md-raised"
ng-click="vm.addAlias($event)"
aria-label="{{ 'alias.add' | translate }}">
<md-tooltip md-direction="top">
{{ 'alias.add' | translate }}
</md-tooltip>
<span translate>alias.add</span>
</md-button>
<span flex></span>
<md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
{{ 'action.save' | translate }}
</md-button>
<md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
</md-dialog-actions>
</form>
</md-dialog>

12
ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js

@ -22,7 +22,8 @@ import selectTargetLayoutTemplate from '../../dashboard/layouts/select-target-la
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, $q, $document, itembuffer, dashboardService, entityId, entityType, entityName, widget) {
export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, $q, $document, dashboardUtils,
types, itembuffer, dashboardService, entityId, entityType, entityName, widget) {
var vm = this;
@ -125,13 +126,8 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog,
targetDeviceAliases: {}
};
aliasesInfo.datasourceAliases[0] = {
aliasName: entityName,
entityType: entityType,
entityFilter: {
useFilter: false,
entityNameFilter: '',
entityList: [entityId]
}
alias: entityName,
filter: dashboardUtils.createSingleEntityFilter(entityType, entityId)
};
itembuffer.addWidgetToDashboard(theDashboard, targetState, targetLayout, vm.widget, aliasesInfo, null, 48, null, -1, -1).then(
function(theDashboard) {

43
ui/src/app/entity/attribute/attribute-table.directive.js

@ -26,10 +26,12 @@ import editAttributeValueTemplate from './edit-attribute-value.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import EditAttributeValueController from './edit-attribute-value.controller';
import AliasController from '../../api/alias-controller';
/*@ngInject*/
export default function AttributeTableDirective($compile, $templateCache, $rootScope, $q, $mdEditDialog, $mdDialog,
$document, $translate, $filter, utils, types, dashboardService, attributeService, widgetService) {
$mdUtil, $document, $translate, $filter, utils, types, dashboardUtils,
dashboardService, entityService, attributeService, widgetService) {
var linker = function (scope, element, attrs) {
@ -246,15 +248,19 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
}
scope.nextWidget = function() {
if (scope.widgetsCarousel.index < scope.widgetsList.length-1) {
scope.widgetsCarousel.index++;
}
$mdUtil.nextTick(function () {
if (scope.widgetsCarousel.index < scope.widgetsList.length - 1) {
scope.widgetsCarousel.index++;
}
});
}
scope.prevWidget = function() {
if (scope.widgetsCarousel.index > 0) {
scope.widgetsCarousel.index--;
}
$mdUtil.nextTick(function () {
if (scope.widgetsCarousel.index > 0) {
scope.widgetsCarousel.index--;
}
});
}
scope.enterWidgetMode = function() {
@ -281,23 +287,28 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
scope.firstBundle = true;
scope.selectedWidgetsBundleAlias = types.systemBundleAlias.cards;
scope.aliasesInfo = {
entityAliases: {
'1': {alias: scope.entityName, entityType: scope.entityType, entityId: scope.entityId}
},
entityAliasesInfo: {
'1': [
{name: scope.entityName, entityType: scope.entityType, id: scope.entityId}
]
var entityAlias = {
id: utils.guid(),
alias: scope.entityName,
filter: dashboardUtils.createSingleEntityFilter(scope.entityType, scope.entityId)
};
var entitiAliases = {};
entitiAliases[entityAlias.id] = entityAlias;
var stateController = {
getStateParams: function() {
return {};
}
};
scope.aliasController = new AliasController(scope, $q, $filter, utils,
types, entityService, stateController, entitiAliases);
var dataKeyType = scope.attributeScope === types.latestTelemetry ?
types.dataKeyType.timeseries : types.dataKeyType.attribute;
var datasource = {
type: types.datasourceType.entity,
entityAliasId: '1',
entityAliasId: entityAlias.id,
dataKeys: []
}
var i = 0;

2
ui/src/app/entity/attribute/attribute-table.tpl.html

@ -156,7 +156,7 @@
rn-swipe-disabled="true">
<li ng-repeat="widgets in widgetsList">
<tb-dashboard
aliases-info="aliasesInfo"
alias-controller="aliasController"
widgets="widgets"
get-st-diff="getServerTimeDiff()"
columns="20"

106
ui/src/app/entity/entity-aliases.tpl.html

@ -1,106 +0,0 @@
<!--
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-dialog class="tb-aliases-dialog" style="width: 700px;" aria-label="{{ vm.title | translate }}">
<form name="theForm" ng-submit="vm.save()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2>{{ vm.isSingleEntityAlias ? ('entity.configure-alias' | translate:vm.singleEntityAlias ) : (vm.title | translate) }}</h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!loading"></span>
<md-dialog-content>
<div class="md-dialog-content">
<fieldset ng-disabled="loading">
<div ng-show="vm.isSingleEntityAlias" layout="row">
<tb-entity-type-select style="min-width: 100px;"
ng-model="vm.singleEntityAlias.entityType"
allowed-entity-types="vm.allowedEntityTypes">
</tb-entity-type-select>
<tb-entity-filter flex entity-type="vm.singleEntityAlias.entityType" ng-model="vm.singleEntityAlias.entityFilter">
</tb-entity-filter>
</div>
<div ng-show="!vm.isSingleEntityAlias" flex layout="row" layout-align="start center">
<span flex="5"></span>
<div flex layout="row" layout-align="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<span translate flex="20" style="min-width: 100px;">entity.alias</span>
<span translate flex="20" style="min-width: 100px;">entity.type</span>
<span translate flex="60" style="min-width: 190px; padding-left: 10px;">entity.entities</span>
<span style="min-width: 40px;"></span>
</div>
</div>
<div ng-show="!vm.isSingleEntityAlias" style="max-height: 500px; overflow: auto; padding-bottom: 20px;">
<div ng-form name="aliasForm" flex layout="row" layout-align="start center" ng-repeat="entityAlias in vm.entityAliases track by $index">
<span flex="5">{{$index + 1}}.</span>
<div class="md-whiteframe-4dp tb-alias" flex layout="row" layout-align="start center">
<md-input-container flex="20" style="min-width: 100px;" md-no-float class="md-block">
<input required ng-change="entityAlias.changed=true" name="alias" placeholder="{{ 'entity.alias' | translate }}" ng-model="entityAlias.alias">
<div ng-messages="aliasForm.alias.$error">
<div translate ng-message="required">entity.alias-required</div>
</div>
</md-input-container>
<section flex="20" layout="column" style="min-width: 100px;" >
<tb-entity-type-select hide-label style="padding-left: 10px;"
ng-model="entityAlias.entityType"
allowed-entity-types="vm.allowedEntityTypes">
</tb-entity-type-select>
</section>
<section flex="60" layout="column">
<tb-entity-filter style="padding-left: 10px;"
entity-type="entityAlias.entityType"
ng-model="entityAlias.entityFilter"
on-matching-entity-change="vm.onFilterEntityChanged(entity, entityAlias)">
</tb-entity-filter>
</section>
<md-button ng-disabled="loading" class="md-icon-button md-primary" style="min-width: 40px;"
ng-click="vm.removeAlias($event, entityAlias)" aria-label="{{ 'action.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'entity.remove-alias' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">
close
</md-icon>
</md-button>
</div>
</div>
</div>
<div ng-show="!vm.isSingleEntityAlias && !vm.disableAdd" style="padding-bottom: 10px;">
<md-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}">
<md-tooltip md-direction="top">
{{ 'entity.add-alias' | translate }}
</md-tooltip>
<span translate>action.add</span>
</md-button>
</div>
</fieldset>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
{{ 'action.save' | translate }}
</md-button>
<md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
</md-dialog-actions>
</form>
</md-dialog>

205
ui/src/app/entity/entity-filter-view.directive.js

@ -0,0 +1,205 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable import/no-unresolved, import/default */
import entityFilterViewTemplate from './entity-filter-view.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import './entity-filter-view.scss';
/*@ngInject*/
export default function EntityFilterViewDirective($compile, $templateCache, $q, $document, $mdDialog, $translate, types/*, entityService*/) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(entityFilterViewTemplate);
element.html(template);
scope.ngModelCtrl = ngModelCtrl;
scope.types = types;
scope.filterDisplayValue = '';
scope.$watch('filter', function () {
scope.updateDisplayValue();
});
scope.updateDisplayValue = function() {
if (scope.filter && scope.filter.type) {
var entityType;
var prefix;
switch (scope.filter.type) {
case types.aliasFilterType.entityList.value:
entityType = scope.filter.entityType;
var count = scope.filter.entityList.length;
scope.filterDisplayValue = $translate.instant(types.entityTypeTranslations[entityType].list, {count: count}, 'messageformat');
break;
case types.aliasFilterType.entityName.value:
entityType = scope.filter.entityType;
prefix = scope.filter.entityNameFilter;
scope.filterDisplayValue = $translate.instant(types.entityTypeTranslations[entityType].nameStartsWith, {prefix: prefix});
break;
case types.aliasFilterType.stateEntity.value:
scope.filterDisplayValue = $translate.instant('alias.filter-type-state-entity-description');
break;
case types.aliasFilterType.assetType.value:
var assetType = scope.filter.assetType;
prefix = scope.filter.assetNameFilter;
if (prefix && prefix.length) {
scope.filterDisplayValue = $translate.instant('alias.filter-type-asset-type-and-name-description', {assetType: assetType, prefix: prefix});
} else {
scope.filterDisplayValue = $translate.instant('alias.filter-type-asset-type-description', {assetType: assetType});
}
break;
case types.aliasFilterType.deviceType.value:
var deviceType = scope.filter.deviceType;
prefix = scope.filter.deviceNameFilter;
if (prefix && prefix.length) {
scope.filterDisplayValue = $translate.instant('alias.filter-type-device-type-and-name-description', {deviceType: deviceType, prefix: prefix});
} else {
scope.filterDisplayValue = $translate.instant('alias.filter-type-device-type-description', {deviceType: deviceType});
}
break;
case types.aliasFilterType.relationsQuery.value:
var rootEntityText;
var directionText;
var allEntitiesText = $translate.instant('alias.all-entities');
var anyRelationText = $translate.instant('alias.any-relation');
if (scope.filter.rootStateEntity) {
rootEntityText = $translate.instant('alias.state-entity');
} else {
rootEntityText = $translate.instant(types.entityTypeTranslations[scope.filter.rootEntity.entityType].type);
}
directionText = $translate.instant('relation.direction-type.' + scope.filter.direction);
var relationFilters = scope.filter.filters;
if (relationFilters && relationFilters.length) {
var relationFiltersDisplayValues = [];
relationFilters.forEach(function(relationFilter) {
var entitiesText;
if (relationFilter.entityTypes && relationFilter.entityTypes.length) {
var entitiesNamesList = [];
relationFilter.entityTypes.forEach(function(entityType) {
entitiesNamesList.push(
$translate.instant(types.entityTypeTranslations[entityType].typePlural)
);
});
entitiesText = entitiesNamesList.join(', ');
} else {
entitiesText = allEntitiesText;
}
var relationTypeText;
if (relationFilter.relationType && relationFilter.relationType.length) {
relationTypeText = "'" + relationFilter.relationType + "'";
} else {
relationTypeText = anyRelationText;
}
var relationFilterDisplayValue = $translate.instant('alias.filter-type-relations-query-description',
{
entities: entitiesText,
relationType: relationTypeText,
direction: directionText,
rootEntity: rootEntityText
}
);
relationFiltersDisplayValues.push(relationFilterDisplayValue);
});
scope.filterDisplayValue = relationFiltersDisplayValues.join(', ');
} else {
scope.filterDisplayValue = $translate.instant('alias.filter-type-relations-query-description',
{
entities: allEntitiesText,
relationType: anyRelationText,
direction: directionText,
rootEntity: rootEntityText
}
);
}
break;
case types.aliasFilterType.assetSearchQuery.value:
case types.aliasFilterType.deviceSearchQuery.value:
allEntitiesText = $translate.instant('alias.all-entities');
anyRelationText = $translate.instant('alias.any-relation');
if (scope.filter.rootStateEntity) {
rootEntityText = $translate.instant('alias.state-entity');
} else {
rootEntityText = $translate.instant(types.entityTypeTranslations[scope.filter.rootEntity.entityType].type);
}
directionText = $translate.instant('relation.direction-type.' + scope.filter.direction);
var relationTypeText;
if (scope.filter.relationType && scope.filter.relationType.length) {
relationTypeText = "'" + scope.filter.relationType + "'";
} else {
relationTypeText = anyRelationText;
}
var translationValues = {
relationType: relationTypeText,
direction: directionText,
rootEntity: rootEntityText
}
if (scope.filter.type == types.aliasFilterType.assetSearchQuery.value) {
var assetTypesQuoted = [];
scope.filter.assetTypes.forEach(function(assetType) {
assetTypesQuoted.push("'"+assetType+"'");
});
var assetTypesText = assetTypesQuoted.join(', ');
translationValues.assetTypes = assetTypesText;
scope.filterDisplayValue = $translate.instant('alias.filter-type-asset-search-query-description',
translationValues
);
} else {
var deviceTypesQuoted = [];
scope.filter.deviceTypes.forEach(function(deviceType) {
deviceTypesQuoted.push("'"+deviceType+"'");
});
var deviceTypesText = deviceTypesQuoted.join(', ');
translationValues.deviceTypes = deviceTypesText;
scope.filterDisplayValue = $translate.instant('alias.filter-type-device-search-query-description',
translationValues
);
}
break;
default:
scope.filterDisplayValue = scope.filter.type;
break;
}
} else {
scope.filterDisplayValue = '';
}
}
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
scope.filter = ngModelCtrl.$viewValue;
} else {
scope.filter = null;
}
}
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
link: linker,
scope: true
};
}

33
ui/src/app/entity/entity-filter-view.scss

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-entity-filter-view {
.entity-filter-empty {
color: rgba(221, 44, 0, 0.87);
font-size: 14px;
line-height: 16px;
}
.entity-filter-type {
font-size: 14px;
line-height: 16px;
color: rgba(0, 0, 0, 0.570588);
}
.entity-filter-value {
font-size: 14px;
line-height: 16px;
color: rgba(0, 0, 0, 0.570588);
}
}

10
ui/src/app/event/event-header-alarm.tpl.html → ui/src/app/entity/entity-filter-view.tpl.html

@ -15,6 +15,10 @@
limitations under the License.
-->
<div translate class="tb-cell" flex="30">event.event-time</div>
<div translate class="tb-cell" flex="20">event.server</div>
<div translate class="tb-cell" flex="20">event.alarm</div>
<div layout='column' class="tb-entity-filter-view">
<div ng-if="!filter || !filter.type" class="entity-filter-empty" translate>alias.no-entity-filter-specified</div>
<div ng-if="filter && filter.type" layout="column">
<div class="entity-filter-value">{{ filterDisplayValue }}</div>
</div>
</div>

232
ui/src/app/entity/entity-filter.directive.js

@ -23,7 +23,7 @@ import entityFilterTemplate from './entity-filter.tpl.html';
import './entity-filter.scss';
/*@ngInject*/
export default function EntityFilterDirective($compile, $templateCache, $q, entityService) {
export default function EntityFilterDirective($compile, $templateCache, $q, $document, $mdDialog, types, entityService) {
var linker = function (scope, element, attrs, ngModelCtrl) {
@ -31,181 +31,76 @@ export default function EntityFilterDirective($compile, $templateCache, $q, enti
element.html(template);
scope.ngModelCtrl = ngModelCtrl;
scope.types = types;
scope.aliasFilterTypes = entityService.getAliasFilterTypesByEntityTypes(scope.allowedEntityTypes);
scope.fetchEntities = function(searchText, limit) {
var deferred = $q.defer();
entityService.getEntitiesByNameFilter(scope.entityType, searchText, limit).then(function success(result) {
if (result) {
deferred.resolve(result);
} else {
deferred.resolve([]);
}
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
scope.updateValidity = function() {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
var valid;
if (value.useFilter) {
ngModelCtrl.$setValidity('entityList', true);
if (angular.isDefined(value.entityNameFilter) && value.entityNameFilter.length > 0) {
ngModelCtrl.$setValidity('entityNameFilter', true);
valid = angular.isDefined(scope.model.matchingFilterEntity) && scope.model.matchingFilterEntity != null;
ngModelCtrl.$setValidity('entityNameFilterEntityMatch', valid);
} else {
ngModelCtrl.$setValidity('entityNameFilter', false);
}
} else {
ngModelCtrl.$setValidity('entityNameFilter', true);
ngModelCtrl.$setValidity('entityNameFilterDeviceMatch', true);
valid = angular.isDefined(value.entityList) && value.entityList.length > 0;
ngModelCtrl.$setValidity('entityList', valid);
}
}
}
ngModelCtrl.$render = function () {
destroyWatchers();
scope.model = {
useFilter: false,
entityList: [],
entityNameFilter: ''
scope.$watch('filter.type', function (newType, prevType) {
if (newType && newType != prevType) {
updateFilter();
}
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
var model = scope.model;
model.useFilter = value.useFilter === true ? true: false;
model.entityList = [];
model.entityNameFilter = value.entityNameFilter || '';
processEntityNameFilter(model.entityNameFilter).then(
function(entity) {
scope.model.matchingFilterEntity = entity;
if (value.entityList && value.entityList.length > 0) {
entityService.getEntities(scope.entityType, value.entityList).then(function (entities) {
model.entityList = entities;
updateMatchingEntity();
initWatchers();
});
} else {
updateMatchingEntity();
initWatchers();
}
});
function updateFilter() {
var filter = {};
filter.type = scope.filter.type;
filter.resolveMultiple = scope.filter.resolveMultiple;
switch (filter.type) {
case types.aliasFilterType.entityList.value:
filter.entityType = null;
filter.entityList = [];
break;
case types.aliasFilterType.entityName.value:
filter.entityType = null;
filter.entityNameFilter = '';
break;
case types.aliasFilterType.stateEntity.value:
break;
case types.aliasFilterType.assetType.value:
filter.assetType = null;
filter.assetNameFilter = '';
break;
case types.aliasFilterType.deviceType.value:
filter.deviceType = null;
filter.deviceNameFilter = '';
break;
case types.aliasFilterType.relationsQuery.value:
case types.aliasFilterType.assetSearchQuery.value:
case types.aliasFilterType.deviceSearchQuery.value:
filter.rootStateEntity = false;
filter.rootEntity = null;
filter.direction = types.entitySearchDirection.from;
filter.maxLevel = 1;
if (filter.type === types.aliasFilterType.relationsQuery.value) {
filter.filters = [];
} else if (filter.type === types.aliasFilterType.assetSearchQuery.value) {
filter.relationType = null;
filter.assetTypes = [];
} else if (filter.type === types.aliasFilterType.deviceSearchQuery.value) {
filter.relationType = null;
filter.deviceTypes = [];
}
)
break;
}
scope.filter = filter;
}
function updateMatchingEntity() {
if (scope.model.useFilter) {
scope.model.matchingEntity = scope.model.matchingFilterEntity;
} else {
if (scope.model.entityList && scope.model.entityList.length > 0) {
scope.model.matchingEntity = scope.model.entityList[0];
} else {
scope.model.matchingEntity = null;
}
}
}
function processEntityNameFilter(entityNameFilter) {
var deferred = $q.defer();
if (angular.isDefined(entityNameFilter) && entityNameFilter.length > 0) {
scope.fetchEntities(entityNameFilter, 1).then(function (entities) {
if (entities && entities.length > 0) {
deferred.resolve(entities[0]);
} else {
deferred.resolve(null);
}
});
} else {
deferred.resolve(null);
}
return deferred.promise;
}
scope.$watch('filter', function () {
scope.updateView();
});
function destroyWatchers() {
if (scope.entityTypeDeregistration) {
scope.entityTypeDeregistration();
scope.entityTypeDeregistration = null;
}
if (scope.entityListDeregistration) {
scope.entityListDeregistration();
scope.entityListDeregistration = null;
}
if (scope.useFilterDeregistration) {
scope.useFilterDeregistration();
scope.useFilterDeregistration = null;
}
if (scope.entityNameFilterDeregistration) {
scope.entityNameFilterDeregistration();
scope.entityNameFilterDeregistration = null;
}
if (scope.matchingEntityDeregistration) {
scope.matchingEntityDeregistration();
scope.matchingEntityDeregistration = null;
}
scope.updateView = function() {
ngModelCtrl.$setViewValue(scope.filter);
}
function initWatchers() {
scope.entityTypeDeregistration = scope.$watch('entityType', function (newEntityType, prevEntityType) {
if (!angular.equals(newEntityType, prevEntityType)) {
scope.model.entityList = [];
scope.model.entityNameFilter = '';
}
});
scope.entityListDeregistration = scope.$watch('model.entityList', function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
value.entityList = [];
if (scope.model.entityList && scope.model.entityList.length > 0) {
for (var i=0;i<scope.model.entityList.length;i++) {
value.entityList.push(scope.model.entityList[i].id.id);
}
}
updateMatchingEntity();
ngModelCtrl.$setViewValue(value);
scope.updateValidity();
}
}, true);
scope.useFilterDeregistration = scope.$watch('model.useFilter', function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
value.useFilter = scope.model.useFilter;
updateMatchingEntity();
ngModelCtrl.$setViewValue(value);
scope.updateValidity();
}
});
scope.entityNameFilterDeregistration = scope.$watch('model.entityNameFilter', function (newNameFilter, prevNameFilter) {
if (ngModelCtrl.$viewValue) {
if (!angular.equals(newNameFilter, prevNameFilter)) {
var value = ngModelCtrl.$viewValue;
value.entityNameFilter = scope.model.entityNameFilter;
processEntityNameFilter(value.entityNameFilter).then(
function(entity) {
scope.model.matchingFilterEntity = entity;
updateMatchingEntity();
ngModelCtrl.$setViewValue(value);
scope.updateValidity();
}
);
}
}
});
scope.matchingEntityDeregistration = scope.$watch('model.matchingEntity', function (newMatchingEntity, prevMatchingEntity) {
if (!angular.equals(newMatchingEntity, prevMatchingEntity)) {
if (scope.onMatchingEntityChange) {
scope.onMatchingEntityChange({entity: newMatchingEntity});
}
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
scope.filter = ngModelCtrl.$viewValue;
} else {
scope.filter = {
type: null,
resolveMultiple: false
}
});
}
}
$compile(element.contents())(scope);
@ -217,9 +112,8 @@ export default function EntityFilterDirective($compile, $templateCache, $q, enti
require: "^ngModel",
link: linker,
scope: {
entityType: '=',
isEdit: '=',
onMatchingEntityChange: '&'
theForm: '=',
allowedEntityTypes: '=?'
}
};

31
ui/src/app/entity/entity-filter.scss

@ -13,33 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-entity-filter {
#entity_list_chips {
.md-chips {
padding-bottom: 1px;
#relationsQueryFilter {
padding-top: 20px;
tb-entity-select {
min-height: 92px;
}
}
.entity-name-filter-input {
margin-top: 10px;
margin-bottom: 0px;
.md-errors-spacer {
min-height: 0px;
}
}
.tb-filter-switch {
.tb-root-state-entity-switch {
padding-left: 10px;
.filter-switch {
.root-state-entity-switch {
margin: 0;
}
.filter-label {
.root-state-entity-label {
margin: 5px 0;
}
}
.tb-error-messages {
margin-top: -11px;
height: 35px;
.tb-error-message {
padding-left: 1px;
}
}
}

262
ui/src/app/entity/entity-filter.tpl.html

@ -15,53 +15,219 @@
limitations under the License.
-->
<section layout='column' class="tb-entity-filter">
<section layout='row'>
<section layout="column" flex ng-show="!model.useFilter">
<md-chips flex
id="entity_list_chips"
ng-required="!useFilter"
ng-model="model.entityList" md-autocomplete-snap
md-require-match="true">
<md-autocomplete
md-no-cache="true"
id="entity"
md-selected-item="selectedEntity"
md-search-text="entitySearchText"
md-items="item in fetchEntities(entitySearchText, 10)"
md-item-text="item.name"
md-min-length="0"
placeholder="{{ 'entity.entity-list' | translate }}">
<md-item-template>
<span md-highlight-text="entitySearchText" md-highlight-flags="^i">{{item.name}}</span>
</md-item-template>
<md-not-found>
<span translate translate-values='{ entity: entitySearchText }'>entity.no-entities-matching</span>
</md-not-found>
</md-autocomplete>
<md-chip-template>
<span>
<strong>{{$chip.name}}</strong>
</span>
</md-chip-template>
</md-chips>
</section>
<section layout="row" flex ng-show="model.useFilter">
<md-input-container flex class="entity-name-filter-input">
<label translate>entity.name-starts-with</label>
<input ng-model="model.entityNameFilter" aria-label="{{ 'entity.name-starts-with' | translate }}">
<div layout='column' class="tb-entity-filter">
<md-input-container class="md-block">
<label>{{ 'alias.filter-type' | translate }}</label>
<md-select required name="filterType"
ng-model="filter.type" aria-label="{{ 'alias.filter-type' | translate }}">
<md-option ng-repeat="type in aliasFilterTypes" ng-value="type.value">
{{type.name | translate}}
</md-option>
</md-select>
<div ng-messages="theForm.filterType.$error">
<div ng-message="required" translate>alias.filter-type-required</div>
</div>
</md-input-container>
<section layout="column" ng-if="filter.type == types.aliasFilterType.entityList.value" id="entityListFilter">
<tb-entity-type-select
ng-model="filter.entityType"
the-form="theForm"
tb-required="true"
allowed-entity-types="allowedEntityTypes">
</tb-entity-type-select>
<tb-entity-list
ng-model="filter.entityList"
tb-required="true"
entity-type="filter.entityType">
</tb-entity-list>
</section>
<section flex layout="column" ng-if="filter.type == types.aliasFilterType.entityName.value" id="entityNameFilter">
<tb-entity-type-select
ng-model="filter.entityType"
the-form="theForm"
tb-required="true"
allowed-entity-types="allowedEntityTypes">
</tb-entity-type-select>
<md-input-container class="md-block">
<label translate>entity.name-starts-with</label>
<input required name="entityNameFilter"
ng-model="filter.entityNameFilter"
aria-label="{{ 'entity.name-starts-with' | translate }}">
<div ng-messages="theForm.entityNameFilter.$error">
<div ng-message="required" translate>entity.entity-name-filter-required</div>
</div>
</md-input-container>
</section>
<section layout="column" ng-if="filter.type == types.aliasFilterType.stateEntity.value" id="stateEntityFilter">
</section>
<section layout="column" ng-if="filter.type == types.aliasFilterType.assetType.value" id="assetTypeFilter">
<tb-entity-subtype-autocomplete
tb-required="true"
the-form="theForm"
ng-model="filter.assetType"
entity-type="types.entityType.asset">
</tb-entity-subtype-autocomplete>
<md-input-container class="md-block">
<label translate>asset.name-starts-with</label>
<input name="assetNameFilter"
ng-model="filter.assetNameFilter"
aria-label="{{ 'asset.name-starts-with' | translate }}">
</md-input-container>
</section>
<section layout="column" ng-if="filter.type == types.aliasFilterType.deviceType.value" id="deviceTypeFilter">
<tb-entity-subtype-autocomplete
tb-required="true"
the-form="theForm"
ng-model="filter.deviceType"
entity-type="types.entityType.device">
</tb-entity-subtype-autocomplete>
<md-input-container class="md-block">
<label translate>device.name-starts-with</label>
<input name="deviceNameFilter"
ng-model="filter.deviceNameFilter"
aria-label="{{ 'device.name-starts-with' | translate }}">
</md-input-container>
</section>
<section layout="column" ng-if="filter.type == types.aliasFilterType.relationsQuery.value" id="relationsQueryFilter">
<label class="tb-small">{{ 'alias.root-entity' | translate }}</label>
<div flex layout="row">
<tb-entity-select flex
the-form="theForm"
tb-required="!filter.rootStateEntity"
ng-disabled="filter.rootStateEntity"
ng-model="filter.rootEntity">
</tb-entity-select>
<section class="tb-root-state-entity-switch" layout="column" layout-align="start center">
<label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
<md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
aria-label="{{ 'alias.root-state-entity' | translate }}">
</md-switch>
</section>
</div>
<div flex layout="row">
<md-input-container class="md-block" style="min-width: 100px;">
<label translate>relation.direction</label>
<md-select required ng-model="filter.direction">
<md-option ng-repeat="direction in types.entitySearchDirection" ng-value="direction">
{{ ('relation.search-direction.' + direction) | translate}}
</md-option>
</md-select>
</md-input-container>
<md-input-container flex class="md-block">
<label translate>alias.max-relation-level</label>
<input name="maxRelationLevel"
type="number"
min="1"
step="1"
placeholder="{{ 'alias.unlimited-level' | translate }}"
ng-model="filter.maxLevel"
aria-label="{{ 'alias.max-relation-level' | translate }}">
</md-input-container>
</div>
<div class="md-caption" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>relation.relation-filters</div>
<tb-relation-filters
ng-model="filter.filters"
allowed-entity-types="allowedEntityTypes">
</tb-relation-filters>
</section>
<section layout="column" ng-if="filter.type == types.aliasFilterType.assetSearchQuery.value" id="assetSearchQueryFilter">
<label class="tb-small">{{ 'alias.root-entity' | translate }}</label>
<div flex layout="row">
<tb-entity-select flex
the-form="theForm"
tb-required="!filter.rootStateEntity"
ng-disabled="filter.rootStateEntity"
ng-model="filter.rootEntity">
</tb-entity-select>
<section class="tb-root-state-entity-switch" layout="column" layout-align="start center">
<label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
<md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
aria-label="{{ 'alias.root-state-entity' | translate }}">
</md-switch>
</section>
</div>
<div flex layout="row">
<md-input-container class="md-block" style="min-width: 100px;">
<label translate>relation.direction</label>
<md-select required ng-model="filter.direction">
<md-option ng-repeat="direction in types.entitySearchDirection" ng-value="direction">
{{ ('relation.search-direction.' + direction) | translate}}
</md-option>
</md-select>
</md-input-container>
<md-input-container flex class="md-block">
<label translate>alias.max-relation-level</label>
<input name="maxRelationLevel"
type="number"
min="1"
step="1"
placeholder="{{ 'alias.unlimited-level' | translate }}"
ng-model="filter.maxLevel"
aria-label="{{ 'alias.max-relation-level' | translate }}">
</md-input-container>
</div>
<div class="md-caption" style="color: rgba(0,0,0,0.57);" translate>relation.relation-type</div>
<tb-relation-type-autocomplete flex
hide-label
the-form="theForm"
ng-model="filter.relationType"
tb-required="false">
</tb-relation-type-autocomplete>
<div class="md-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>asset.asset-types</div>
<tb-entity-subtype-list
tb-required="true"
entity-type="types.entityType.asset"
ng-model="filter.assetTypes">
</tb-entity-subtype-list>
</section>
<section layout="column" ng-if="filter.type == types.aliasFilterType.deviceSearchQuery.value" id="deviceSearchQueryFilter">
<label class="tb-small">{{ 'alias.root-entity' | translate }}</label>
<div flex layout="row">
<tb-entity-select flex
the-form="theForm"
tb-required="!filter.rootStateEntity"
ng-disabled="filter.rootStateEntity"
ng-model="filter.rootEntity">
</tb-entity-select>
<section class="tb-root-state-entity-switch" layout="column" layout-align="start center">
<label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label>
<md-switch class="root-state-entity-switch" ng-model="filter.rootStateEntity"
aria-label="{{ 'alias.root-state-entity' | translate }}">
</md-switch>
</section>
</div>
<div flex layout="row">
<md-input-container class="md-block" style="min-width: 100px;">
<label translate>relation.direction</label>
<md-select required ng-model="filter.direction">
<md-option ng-repeat="direction in types.entitySearchDirection" ng-value="direction">
{{ ('relation.search-direction.' + direction) | translate}}
</md-option>
</md-select>
</md-input-container>
<md-input-container flex class="md-block">
<label translate>alias.max-relation-level</label>
<input name="maxRelationLevel"
type="number"
min="1"
step="1"
placeholder="{{ 'alias.unlimited-level' | translate }}"
ng-model="filter.maxLevel"
aria-label="{{ 'alias.max-relation-level' | translate }}">
</md-input-container>
</section>
<section class="tb-filter-switch" layout="column" layout-align="center center">
<label class="tb-small filter-label" translate>entity.use-entity-name-filter</label>
<md-switch class="filter-switch" ng-model="model.useFilter" aria-label="use-filter-switcher">
</md-switch>
</section>
</div>
<div class="md-caption" style="color: rgba(0,0,0,0.57);" translate>relation.relation-type</div>
<tb-relation-type-autocomplete flex
hide-label
the-form="theForm"
ng-model="filter.relationType"
tb-required="false">
</tb-relation-type-autocomplete>
<div class="md-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>device.device-types</div>
<tb-entity-subtype-list
tb-required="true"
entity-type="types.entityType.device"
ng-model="filter.deviceTypes">
</tb-entity-subtype-list>
</section>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
<div translate ng-message="entityList" class="tb-error-message">entity.entity-list-empty</div>
<div translate ng-message="entityNameFilter" class="tb-error-message">entity.entity-name-filter-required</div>
<div translate translate-values='{ entity: model.entityNameFilter }' ng-message="entityNameFilterEntityMatch"
class="tb-error-message">entity.entity-name-filter-no-entity-matched</div>
</div>
</section>
</div>

130
ui/src/app/entity/entity-list.directive.js

@ -0,0 +1,130 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable import/no-unresolved, import/default */
import entityListTemplate from './entity-list.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import './entity-list.scss';
/*@ngInject*/
export default function EntityListDirective($compile, $templateCache, $q, $mdUtil, entityService) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(entityListTemplate);
element.html(template);
scope.ngModelCtrl = ngModelCtrl;
scope.$watch('tbRequired', function () {
scope.updateValidity();
});
scope.fetchEntities = function(searchText, limit) {
var deferred = $q.defer();
entityService.getEntitiesByNameFilter(scope.entityType, searchText, limit).then(
function success(result) {
if (result) {
deferred.resolve(result);
} else {
deferred.resolve([]);
}
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
scope.updateValidity = function() {
var value = ngModelCtrl.$viewValue;
var valid = !scope.tbRequired || value && value.length > 0;
ngModelCtrl.$setValidity('entityList', valid);
}
ngModelCtrl.$render = function () {
destroyWatchers();
var value = ngModelCtrl.$viewValue;
scope.entityList = [];
if (value && value.length > 0) {
entityService.getEntities(scope.entityType, value).then(function (entities) {
scope.entityList = entities;
initWatchers();
});
} else {
initWatchers();
}
}
function initWatchers() {
scope.entityTypeDeregistration = scope.$watch('entityType', function (newEntityType, prevEntityType) {
if (!angular.equals(newEntityType, prevEntityType)) {
scope.entityList = [];
}
});
scope.entityListDeregistration = scope.$watch('entityList', function () {
var ids = [];
if (scope.entityList && scope.entityList.length > 0) {
for (var i=0;i<scope.entityList.length;i++) {
ids.push(scope.entityList[i].id.id);
}
}
var value = ngModelCtrl.$viewValue;
if (!angular.equals(ids, value)) {
ngModelCtrl.$setViewValue(ids);
}
scope.updateValidity();
}, true);
}
function destroyWatchers() {
if (scope.entityTypeDeregistration) {
scope.entityTypeDeregistration();
scope.entityTypeDeregistration = null;
}
if (scope.entityListDeregistration) {
scope.entityListDeregistration();
scope.entityListDeregistration = null;
}
}
$compile(element.contents())(scope);
$mdUtil.nextTick(function(){
var inputElement = angular.element('input', element);
inputElement.on('blur', function() {
scope.inputTouched = true;
} );
});
}
return {
restrict: "E",
require: "^ngModel",
link: linker,
scope: {
disabled:'=ngDisabled',
tbRequired: '=?',
entityType: '='
}
};
}

30
ui/src/app/entity/entity-list.scss

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*.tb-entity-list {
#entity_list_chips {
.md-chips {
padding-bottom: 1px;
}
}
.tb-error-messages {
margin-top: -11px;
height: 35px;
.tb-error-message {
padding-left: 1px;
}
}
}*/

52
ui/src/app/entity/entity-list.tpl.html

@ -0,0 +1,52 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section flex layout='column' class="tb-entity-list">
<md-chips flex
readonly="disabled"
id="entity_list_chips"
ng-required="tbRequired"
ng-model="entityList"
md-autocomplete-snap
md-require-match="true">
<md-autocomplete
md-no-cache="true"
id="entity"
md-selected-item="selectedEntity"
md-search-text="entitySearchText"
md-items="item in fetchEntities(entitySearchText, 10)"
md-item-text="item.name"
md-min-length="0"
placeholder="{{ 'entity.entity-list' | translate }}">
<md-item-template>
<span md-highlight-text="entitySearchText" md-highlight-flags="^i">{{item.name}}</span>
</md-item-template>
<md-not-found>
<span translate translate-values='{ entity: entitySearchText }'>entity.no-entities-matching</span>
</md-not-found>
</md-autocomplete>
<md-chip-template>
<span>
<strong>{{$chip.name}}</strong>
</span>
</md-chip-template>
</md-chips>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" ng-if="inputTouched" role="alert">
<div translate ng-message="entityList" class="tb-error-message">entity.entity-list-empty</div>
</div>
</section>

3
ui/src/app/entity/entity-subtype-autocomplete.directive.js

@ -114,6 +114,9 @@ export default function EntitySubtypeAutocomplete($compile, $templateCache, $q,
scope.selectEntitySubtypeText = 'asset.select-asset-type';
scope.entitySubtypeText = 'asset.asset-type';
scope.entitySubtypeRequiredText = 'asset.asset-type-required';
scope.$on('assetSaved', function() {
scope.entitySubtypes = null;
});
} else if (scope.entityType == types.entityType.device) {
scope.selectEntitySubtypeText = 'device.select-device-type';
scope.entitySubtypeText = 'device.device-type';

146
ui/src/app/entity/entity-subtype-list.directive.js

@ -0,0 +1,146 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable import/no-unresolved, import/default */
import entitySubtypeListTemplate from './entity-subtype-list.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import './entity-subtype-list.scss';
/*@ngInject*/
export default function EntitySubtypeListDirective($compile, $templateCache, $q, $mdUtil, $translate, $filter, types, assetService, deviceService) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(entitySubtypeListTemplate);
element.html(template);
scope.ngModelCtrl = ngModelCtrl;
scope.entitySubtypesList = [];
scope.entitySubtypes = null;
if (scope.entityType == types.entityType.asset) {
scope.placeholder = scope.tbRequired ? $translate.instant('asset.enter-asset-type')
: $translate.instant('asset.any-asset');
scope.secondaryPlaceholder = '+' + $translate.instant('asset.asset-type');
scope.noSubtypesMathingText = 'asset.no-asset-types-matching';
scope.subtypeListEmptyText = 'asset.asset-type-list-empty';
} else if (scope.entityType == types.entityType.device) {
scope.placeholder = scope.tbRequired ? $translate.instant('device.enter-device-type')
: $translate.instant('device.any-device');
scope.secondaryPlaceholder = '+' + $translate.instant('device.device-type');
scope.noSubtypesMathingText = 'device.no-device-types-matching';
scope.subtypeListEmptyText = 'device.device-type-list-empty';
}
scope.$watch('tbRequired', function () {
scope.updateValidity();
});
scope.fetchEntitySubtypes = function(searchText) {
var deferred = $q.defer();
loadSubTypes().then(
function success(subTypes) {
var result = $filter('filter')(subTypes, {'$': searchText});
if (result && result.length) {
deferred.resolve(result);
} else {
deferred.resolve([searchText]);
}
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
scope.updateValidity = function() {
var value = ngModelCtrl.$viewValue;
var valid = !scope.tbRequired || value && value.length > 0;
ngModelCtrl.$setValidity('entitySubtypesList', valid);
}
ngModelCtrl.$render = function () {
scope.entitySubtypesList = ngModelCtrl.$viewValue;
if (!scope.entitySubtypesList) {
scope.entitySubtypesList = [];
}
}
scope.$watch('entitySubtypesList', function () {
ngModelCtrl.$setViewValue(scope.entitySubtypesList);
scope.updateValidity();
}, true);
function loadSubTypes() {
var deferred = $q.defer();
if (!scope.entitySubtypes) {
var entitySubtypesPromise;
if (scope.entityType == types.entityType.asset) {
entitySubtypesPromise = assetService.getAssetTypes();
} else if (scope.entityType == types.entityType.device) {
entitySubtypesPromise = deviceService.getDeviceTypes();
}
if (entitySubtypesPromise) {
entitySubtypesPromise.then(
function success(types) {
scope.entitySubtypes = [];
types.forEach(function (type) {
scope.entitySubtypes.push(type.type);
});
deferred.resolve(scope.entitySubtypes);
},
function fail() {
deferred.reject();
}
);
} else {
deferred.reject();
}
} else {
deferred.resolve(scope.entitySubtypes);
}
return deferred.promise;
}
$compile(element.contents())(scope);
$mdUtil.nextTick(function(){
var inputElement = angular.element('input', element);
inputElement.on('blur', function() {
scope.inputTouched = true;
} );
});
}
return {
restrict: "E",
require: "^ngModel",
link: linker,
scope: {
disabled:'=ngDisabled',
tbRequired: '=?',
entityType: "="
}
};
}

30
ui/src/app/entity/entity-subtype-list.scss

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*.tb-entity-subtype-list {
#entity_subtype_list_chips {
.md-chips {
padding-bottom: 1px;
}
}
.tb-error-messages {
margin-top: -11px;
height: 35px;
.tb-error-message {
padding-left: 1px;
}
}
}*/

54
ui/src/app/entity/entity-subtype-list.tpl.html

@ -0,0 +1,54 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section flex layout='column' class="tb-entity-subtype-list">
<md-chips flex
readonly="disabled"
id="entity_subtype_list_chips"
ng-required="tbRequired"
ng-model="entitySubtypesList"
placeholder="{{placeholder}}"
secondary-placeholder="{{secondaryPlaceholder}}"
md-autocomplete-snap
md-require-match="false">
<md-autocomplete
md-no-cache="true"
id="entitySubtype"
md-selected-item="selectedEntitySubtype"
md-search-text="entitySubtypeSearchText"
md-items="item in fetchEntitySubtypes(entitySubtypeSearchText)"
md-item-text="item"
md-min-length="0"
placeholder="{{ (!entitySubtypesList || !entitySubtypesList.length) ? placeholder : secondaryPlaceholder }}">
<md-item-template>
<span md-highlight-text="entitySubtypeSearchText" md-highlight-flags="^i">{{item}}</span>
</md-item-template>
<md-not-found>
<span translate translate-values='{ entitySubtype: entitySubtypeSearchText }'>{{noSubtypesMathingText}}</span>
</md-not-found>
</md-autocomplete>
<md-chip-template>
<span>
<strong>{{$chip}}</strong>
</span>
</md-chip-template>
</md-chips>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" ng-if="inputTouched && tbRequired" role="alert">
<div translate ng-message="entitySubtypesList" class="tb-error-message">{{subtypeListEmptyText}}</div>
</div>
</section>

111
ui/src/app/entity/entity-type-list.directive.js

@ -0,0 +1,111 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable import/no-unresolved, import/default */
import entityTypeListTemplate from './entity-type-list.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import './entity-type-list.scss';
/*@ngInject*/
export default function EntityTypeListDirective($compile, $templateCache, $q, $mdUtil, $translate, $filter, types, entityService) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(entityTypeListTemplate);
element.html(template);
scope.ngModelCtrl = ngModelCtrl;
scope.placeholder = scope.tbRequired ? $translate.instant('entity.enter-entity-type')
: $translate.instant('entity.any-entity');
scope.secondaryPlaceholder = '+' + $translate.instant('entity.entity-type');
var entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes);
scope.entityTypesList = [];
for (var type in entityTypes) {
var entityTypeInfo = {};
entityTypeInfo.value = entityTypes[type];
entityTypeInfo.name = $translate.instant(types.entityTypeTranslations[entityTypeInfo.value].type) + '';
scope.entityTypesList.push(entityTypeInfo);
}
scope.$watch('tbRequired', function () {
scope.updateValidity();
});
scope.fetchEntityTypes = function(searchText) {
var deferred = $q.defer();
var entityTypes = $filter('filter')(scope.entityTypesList, {name: searchText});
deferred.resolve(entityTypes);
return deferred.promise;
}
scope.updateValidity = function() {
var value = ngModelCtrl.$viewValue;
var valid = !scope.tbRequired || value && value.length > 0;
ngModelCtrl.$setValidity('entityTypeList', valid);
}
ngModelCtrl.$render = function () {
scope.entityTypeList = [];
var value = ngModelCtrl.$viewValue;
if (value && value.length) {
value.forEach(function(type) {
var entityTypeInfo = {};
entityTypeInfo.value = type;
entityTypeInfo.name = $translate.instant(types.entityTypeTranslations[entityTypeInfo.value].type) + '';
scope.entityTypeList.push(entityTypeInfo);
});
}
}
scope.$watch('entityTypeList', function () {
var values = [];
if (scope.entityTypeList && scope.entityTypeList.length) {
scope.entityTypeList.forEach(function(entityType) {
values.push(entityType.value);
});
}
ngModelCtrl.$setViewValue(values);
scope.updateValidity();
}, true);
$compile(element.contents())(scope);
$mdUtil.nextTick(function(){
var inputElement = angular.element('input', element);
inputElement.on('blur', function() {
scope.inputTouched = true;
} );
});
}
return {
restrict: "E",
require: "^ngModel",
link: linker,
scope: {
disabled:'=ngDisabled',
tbRequired: '=?',
allowedEntityTypes: '=?'
}
};
}

30
ui/src/app/entity/entity-type-list.scss

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*.tb-entity-type-list {
#entity_type_list_chips {
.md-chips {
padding-bottom: 1px;
}
}
.tb-error-messages {
margin-top: -11px;
height: 35px;
.tb-error-message {
padding-left: 1px;
}
}
}*/

54
ui/src/app/entity/entity-type-list.tpl.html

@ -0,0 +1,54 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section flex layout='column' class="tb-entity-type-list">
<md-chips flex
readonly="disabled"
id="entity_type_list_chips"
ng-required="tbRequired"
ng-model="entityTypeList"
placeholder="{{placeholder}}"
secondary-placeholder="{{secondaryPlaceholder}}"
md-autocomplete-snap
md-require-match="true">
<md-autocomplete
md-no-cache="true"
id="entityType"
md-selected-item="selectedEntityType"
md-search-text="entityTypeSearchText"
md-items="item in fetchEntityTypes(entityTypeSearchText)"
md-item-text="item.name"
md-min-length="0"
placeholder="{{ (!entityTypeList || !entityTypeList.length) ? placeholder : secondaryPlaceholder }}">
<md-item-template>
<span md-highlight-text="entityTypeSearchText" md-highlight-flags="^i">{{item.name}}</span>
</md-item-template>
<md-not-found>
<span translate translate-values='{ entityType: entityTypeSearchText }'>entity.no-entity-types-matching</span>
</md-not-found>
</md-autocomplete>
<md-chip-template>
<span>
<strong>{{$chip.name}}</strong>
</span>
</md-chip-template>
</md-chips>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" ng-if="inputTouched && tbRequired" role="alert">
<div translate ng-message="entityTypeList" class="tb-error-message">entity.entity-type-list-empty</div>
</div>
</section>

35
ui/src/app/entity/entity-type-select.directive.js

@ -23,7 +23,7 @@ import entityTypeSelectTemplate from './entity-type-select.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function EntityTypeSelect($compile, $templateCache, utils, userService, types) {
export default function EntityTypeSelect($compile, $templateCache, utils, entityService, userService, types) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(entityTypeSelectTemplate);
@ -39,39 +39,10 @@ export default function EntityTypeSelect($compile, $templateCache, utils, userSe
scope.ngModelCtrl = ngModelCtrl;
var authority = userService.getAuthority();
scope.entityTypes = {};
switch(authority) {
case 'SYS_ADMIN':
scope.entityTypes.tenant = types.entityType.tenant;
scope.entityTypes.rule = types.entityType.rule;
scope.entityTypes.plugin = types.entityType.plugin;
break;
case 'TENANT_ADMIN':
scope.entityTypes.device = types.entityType.device;
scope.entityTypes.asset = types.entityType.asset;
scope.entityTypes.customer = types.entityType.customer;
scope.entityTypes.rule = types.entityType.rule;
scope.entityTypes.plugin = types.entityType.plugin;
scope.entityTypes.dashboard = types.entityType.dashboard;
break;
case 'CUSTOMER_USER':
scope.entityTypes.device = types.entityType.device;
scope.entityTypes.asset = types.entityType.asset;
scope.entityTypes.dashboard = types.entityType.dashboard;
break;
}
if (scope.allowedEntityTypes) {
for (var entityType in scope.entityTypes) {
if (scope.allowedEntityTypes.indexOf(scope.entityTypes[entityType]) === -1) {
delete scope.entityTypes[entityType];
}
}
}
scope.entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes);
scope.typeName = function(type) {
return utils.entityTypeName(type);
return type ? types.entityTypeTranslations[type].type : '';
}
scope.updateValidity = function () {

18
ui/src/app/entity/index.js

@ -14,34 +14,46 @@
* limitations under the License.
*/
import EntityAliasesController from './entity-aliases.controller';
import EntityAliasesController from './alias/entity-aliases.controller';
import EntityAliasDialogController from './alias/entity-alias-dialog.controller';
import EntityTypeSelectDirective from './entity-type-select.directive';
import EntityTypeListDirective from './entity-type-list.directive';
import EntitySubtypeListDirective from './entity-subtype-list.directive';
import EntitySubtypeSelectDirective from './entity-subtype-select.directive';
import EntitySubtypeAutocompleteDirective from './entity-subtype-autocomplete.directive';
import EntityAutocompleteDirective from './entity-autocomplete.directive';
import EntityListDirective from './entity-list.directive';
import EntitySelectDirective from './entity-select.directive';
import EntityFilterDirective from './entity-filter.directive';
import AliasesEntitySelectPanelController from './aliases-entity-select-panel.controller';
import AliasesEntitySelectDirective from './aliases-entity-select.directive';
import EntityFilterViewDirective from './entity-filter-view.directive';
import AliasesEntitySelectPanelController from './alias/aliases-entity-select-panel.controller';
import AliasesEntitySelectDirective from './alias/aliases-entity-select.directive';
import AddAttributeDialogController from './attribute/add-attribute-dialog.controller';
import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller';
import AttributeTableDirective from './attribute/attribute-table.directive';
import RelationFiltersDirective from './relation/relation-filters.directive';
import RelationTableDirective from './relation/relation-table.directive';
import RelationTypeAutocompleteDirective from './relation/relation-type-autocomplete.directive';
export default angular.module('thingsboard.entity', [])
.controller('EntityAliasesController', EntityAliasesController)
.controller('EntityAliasDialogController', EntityAliasDialogController)
.controller('AliasesEntitySelectPanelController', AliasesEntitySelectPanelController)
.controller('AddAttributeDialogController', AddAttributeDialogController)
.controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController)
.directive('tbEntityTypeSelect', EntityTypeSelectDirective)
.directive('tbEntityTypeList', EntityTypeListDirective)
.directive('tbEntitySubtypeList', EntitySubtypeListDirective)
.directive('tbEntitySubtypeSelect', EntitySubtypeSelectDirective)
.directive('tbEntitySubtypeAutocomplete', EntitySubtypeAutocompleteDirective)
.directive('tbEntityAutocomplete', EntityAutocompleteDirective)
.directive('tbEntityList', EntityListDirective)
.directive('tbEntitySelect', EntitySelectDirective)
.directive('tbEntityFilter', EntityFilterDirective)
.directive('tbEntityFilterView', EntityFilterViewDirective)
.directive('tbAliasesEntitySelect', AliasesEntitySelectDirective)
.directive('tbAttributeTable', AttributeTableDirective)
.directive('tbRelationFilters', RelationFiltersDirective)
.directive('tbRelationTable', RelationTableDirective)
.directive('tbRelationTypeAutocomplete', RelationTypeAutocompleteDirective)
.name;

85
ui/src/app/entity/relation/relation-filters.directive.js

@ -0,0 +1,85 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import './relation-filters.scss';
/* eslint-disable import/no-unresolved, import/default */
import relationFiltersTemplate from './relation-filters.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function RelationFilters($compile, $templateCache) {
return {
restrict: "E",
require: "^ngModel",
scope: {
allowedEntityTypes: '=?'
},
link: linker
};
function linker( scope, element, attrs, ngModelCtrl ) {
var template = $templateCache.get(relationFiltersTemplate);
element.html(template);
scope.relationFilters = [];
scope.addFilter = addFilter;
scope.removeFilter = removeFilter;
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
value.forEach(function (filter) {
scope.relationFilters.push(filter);
});
}
scope.$watch('relationFilters', function (newVal, prevVal) {
if (!angular.equals(newVal, prevVal)) {
updateValue();
}
}, true);
}
function addFilter() {
var filter = {
relationType: null,
entityTypes: []
};
scope.relationFilters.push(filter);
}
function removeFilter($event, filter) {
var index = scope.relationFilters.indexOf(filter);
if (index > -1) {
scope.relationFilters.splice(index, 1);
}
}
function updateValue() {
var value = [];
scope.relationFilters.forEach(function (filter) {
value.push(filter);
});
ngModelCtrl.$setViewValue(value);
}
$compile(element.contents())(scope);
}
}

77
ui/src/app/entity/relation/relation-filters.scss

@ -0,0 +1,77 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-relation-filters {
.header {
padding-left: 5px;
padding-right: 5px;
padding-bottom: 5px;
.cell {
padding-left: 5px;
padding-right: 5px;
color: rgba(0,0,0,.54);
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
}
.body {
padding-left: 5px;
padding-right: 5px;
max-height: 300px;
overflow: auto;
padding-bottom: 20px;
.row {
padding-top: 5px;
}
.cell {
padding-left: 5px;
padding-right: 5px;
md-select {
margin: 0 0 24px;
}
md-input-container {
margin: 0;
}
md-chips-wrap {
padding: 0px;
margin: 0 0 24px;
.md-chip-input-container {
margin: 0;
}
md-autocomplete {
height: 30px;
md-autocomplete-wrap {
height: 30px;
}
}
}
.md-chips .md-chip-input-container input {
padding: 2px 2px 2px;
height: 26px;
line-height: 26px;
}
}
.md-button {
margin: 0;
}
}
}

67
ui/src/app/entity/relation/relation-filters.tpl.html

@ -0,0 +1,67 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-relation-filters">
<div class="header" ng-show="relationFilters.length">
<div layout="row" layout-align="start center">
<span class="cell" style="width: 200px; min-width: 200px;" translate>relation.type</span>
<span class="cell" flex translate>entity.entity-types</span>
<span class="cell" style="width: 40px; min-width: 40px;">&nbsp</span>
</div>
</div>
<div class="body" ng-show="relationFilters.length">
<div class="row" ng-form name="relationFilterForm" flex layout="row" layout-align="start center" ng-repeat="filter in relationFilters track by $index">
<div class="md-whiteframe-1dp" flex layout="row" layout-align="start center">
<tb-relation-type-autocomplete class="cell" style="width: 200px; min-width: 200px;"
hide-label
the-form="relationFilterForm"
ng-model="filter.relationType"
tb-required="false">
</tb-relation-type-autocomplete>
<tb-entity-type-list class="cell" flex
ng-model="filter.entityTypes"
allowed-entity-types="allowedEntityTypes"
tb-required="false">
</tb-entity-type-list>
<md-button ng-disabled="loading" class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"
ng-click="removeFilter($event, filter)" aria-label="{{ 'action.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'relation.remove-relation-filter' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">
close
</md-icon>
</md-button>
</div>
</div>
</div>
<div class="any-filter" ng-show="!relationFilters.length">
<span layout-align="center center"
class="tb-prompt" translate>relation.any-relation</span>
</div>
<div>
<md-button ng-disabled="loading" class="md-primary md-raised" ng-click="addFilter($event)" aria-label="{{ 'action.add' | translate }}">
<md-tooltip md-direction="top">
{{ 'relation.add-relation-filter' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.add' | translate }}" class="material-icons">
add
</md-icon>
{{ 'action.add' | translate }}
</md-button>
</div>
</div>

4
ui/src/app/entity/relation/relation-table.directive.js

@ -218,9 +218,9 @@ function RelationTableController($scope, $q, $mdDialog, $document, $translate, $
function success(allRelations) {
allRelations.forEach(function(relation) {
if (vm.direction == vm.types.entitySearchDirection.from) {
relation.toEntityTypeName = $translate.instant(utils.entityTypeName(relation.to.entityType));
relation.toEntityTypeName = $translate.instant(types.entityTypeTranslations[relation.to.entityType].type);
} else {
relation.fromEntityTypeName = $translate.instant(utils.entityTypeName(relation.from.entityType));
relation.fromEntityTypeName = $translate.instant(types.entityTypeTranslations[relation.from.entityType].type);
}
});
vm.allRelations = allRelations;

2
ui/src/app/entity/relation/relation-type-autocomplete.directive.js

@ -29,6 +29,8 @@ export default function RelationTypeAutocomplete($compile, $templateCache, $q, $
element.html(template);
scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
scope.hideLabel = angular.isDefined(attrs.hideLabel) ? true : false;
scope.relationType = null;
scope.relationTypeSearchText = '';
scope.relationTypes = [];

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save