diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index 7a2c273002..19c75e7b9b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/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 getAlarms( + public TimePageData 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); } diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 1feef4a670..1040f3a892 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/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()); diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java index 3ddc5975a7..4aa1a0a5cb 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java +++ b/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 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) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java index 09695e5ee8..e48cf5b7a8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java @@ -53,6 +53,22 @@ public class Alarm extends BaseData 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() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java new file mode 100644 index 0000000000..ef24f1c2c0 --- /dev/null +++ b/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; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java index 00ca6c34ab..55019c8e33 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java +++ b/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; } diff --git a/ui/src/app/entity/aliases-entity-select-panel.controller.js b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java similarity index 54% rename from ui/src/app/entity/aliases-entity-select-panel.controller.js rename to common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java index 4128f948d5..c09b1fd37e 100644 --- a/ui/src/app/entity/aliases-entity-select-panel.controller.js +++ b/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); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java index 0f1b346f2c..a8704b2f58 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java +++ b/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; + } + + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java index 712bbd1adf..0ab762024e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java +++ b/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 save(Alarm alarm); - ListenableFuture> findAlarms(AlarmQuery query); + ListenableFuture> findAlarms(AlarmQuery query); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index 5399d9dd7f..3556d51f09 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/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 findAlarmByIdAsync(AlarmId alarmId); - ListenableFuture> findAlarms(AlarmQuery query); + ListenableFuture findAlarmInfoByIdAsync(AlarmId alarmId); + + ListenableFuture> findAlarms(AlarmQuery query); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 262a11b62c..94f8fdbf76 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/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 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> findAlarms(AlarmQuery query) { - ListenableFuture> alarms = alarmDao.findAlarms(query); - return Futures.transform(alarms, new Function, TimePageData>() { + public ListenableFuture findAlarmInfoByIdAsync(AlarmId alarmId) { + log.trace("Executing findAlarmInfoByIdAsync [{}]", alarmId); + validateId(alarmId, "Incorrect alarmId " + alarmId); + return Futures.transform(alarmDao.findAlarmByIdAsync(alarmId.getId()), + (AsyncFunction) alarm1 -> { + AlarmInfo alarmInfo = new AlarmInfo(alarm1); + return Futures.transform( + entityService.fetchEntityNameAsync(alarmInfo.getOriginator()), (Function) + originatorName -> { + alarmInfo.setOriginatorName(originatorName); + return alarmInfo; + } + ); + }); + } + + @Override + public ListenableFuture> findAlarms(AlarmQuery query) { + ListenableFuture> alarms = alarmDao.findAlarms(query); + if (query.getFetchOriginator() != null && query.getFetchOriginator().booleanValue()) { + alarms = Futures.transform(alarms, (AsyncFunction, List>) input -> { + List> alarmFutures = new ArrayList<>(input.size()); + for (AlarmInfo alarmInfo : input) { + alarmFutures.add(Futures.transform( + entityService.fetchEntityNameAsync(alarmInfo.getOriginator()), (Function) + originatorName -> { + alarmInfo.setOriginatorName(originatorName); + return alarmInfo; + } + )); + } + return Futures.successfulAsList(alarmFutures); + }); + } + return Futures.transform(alarms, new Function, TimePageData>() { @Nullable @Override - public TimePageData apply(@Nullable List alarms) { + public TimePageData apply(@Nullable List 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 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 ListenableFuture getAndUpdate(AlarmId alarmId, Function function) { validateId(alarmId, "Alarm id should be specified!"); ListenableFuture entity = alarmDao.findAlarmByIdAsync(alarmId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java index 343d4cc41a..f5b667205a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/CassandraAlarmDao.java +++ b/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> findAlarms(AlarmQuery query) { - log.trace("Try to find alarms by entity [{}], status [{}] and pageLink [{}]", query.getAffectedEntityId(), query.getStatus(), query.getPageLink()); + public ListenableFuture> 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> relations = relationDao.findRelations(affectedEntity, relationType, RelationTypeGroup.ALARM, EntityType.ALARM, query.getPageLink()); - return Futures.transform(relations, (AsyncFunction, List>) input -> { - List> alarmFutures = new ArrayList<>(input.size()); + return Futures.transform(relations, (AsyncFunction, List>) input -> { + List> alarmFutures = new ArrayList<>(input.size()); for (EntityRelation relation : input) { - alarmFutures.add(findAlarmByIdAsync(relation.getTo().getId())); + alarmFutures.add(Futures.transform( + findAlarmByIdAsync(relation.getTo().getId()), (Function) + alarm1 -> new AlarmInfo(alarm1))); } return Futures.successfulAsList(alarmFutures); }); diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java index 0fdd6ca650..2f6fe737e7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java +++ b/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)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index 36ec56735b..296874e20f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -191,7 +191,7 @@ public class BaseRelationService implements RelationService { @Override public ListenableFuture> findByQuery(EntityRelationsQuery query) { - log.trace("Executing findByQuery [{}][{}]", query); + log.trace("Executing findByQuery [{}]", query); RelationsSearchParameters params = query.getParameters(); final List filters = query.getFilters(); if (filters == null || filters.isEmpty()) { @@ -224,6 +224,30 @@ public class BaseRelationService implements RelationService { } } + @Override + public ListenableFuture> findInfoByQuery(EntityRelationsQuery query) { + log.trace("Executing findInfoByQuery [{}]", query); + ListenableFuture> relations = findByQuery(query); + EntitySearchDirection direction = query.getParameters().getDirection(); + ListenableFuture> relationsInfo = Futures.transform(relations, + (AsyncFunction, List>) relations1 -> { + List> 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!"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java index a810454f2a..bd2e785a5b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java @@ -52,6 +52,8 @@ public interface RelationService { ListenableFuture> findByQuery(EntityRelationsQuery query); + ListenableFuture> findInfoByQuery(EntityRelationsQuery query); + // TODO: This method may be useful for some validations in the future // ListenableFuture checkRecursiveRelation(EntityId from, EntityId to); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java index 3b72574302..b22df63e3f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java +++ b/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 alarms = alarmService.findAlarms(AlarmQuery.builder() + TimePageData alarms = alarmService.findAlarms(AlarmQuery.builder() .affectedEntityId(childId) .status(AlarmStatus.ACTIVE_UNACK).pageLink( new TimePageLink(1, 0L, System.currentTimeMillis(), false) diff --git a/ui/src/app/alarm/alarm-details-dialog.controller.js b/ui/src/app/alarm/alarm-details-dialog.controller.js new file mode 100644 index 0000000000..0cc05ef295 --- /dev/null +++ b/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); + } + +} diff --git a/ui/src/app/alarm/alarm-details-dialog.scss b/ui/src/app/alarm/alarm-details-dialog.scss new file mode 100644 index 0000000000..9b923d076c --- /dev/null +++ b/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%; + } +} diff --git a/ui/src/app/alarm/alarm-details-dialog.tpl.html b/ui/src/app/alarm/alarm-details-dialog.tpl.html new file mode 100644 index 0000000000..c958201ea9 --- /dev/null +++ b/ui/src/app/alarm/alarm-details-dialog.tpl.html @@ -0,0 +1,107 @@ + + + +
+

alarm.alarm-details

+ + + + +
+
+ +
+
+ + + + + + + + +
+
+ + + + + + + + + +
+
+ + + + + + + + + +
+
+ + + + + + + + + + + + +
+
alarm.details
+
+
+
+
+
+
+ + {{ 'alarm.acknowledge' | + translate }} + + {{ 'alarm.clear' | + translate }} + + + {{ 'action.close' | + translate }} + + +
diff --git a/ui/src/app/alarm/alarm-header.directive.js b/ui/src/app/alarm/alarm-header.directive.js new file mode 100644 index 0000000000..b66a9729a1 --- /dev/null +++ b/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 + }; +} diff --git a/ui/src/app/alarm/alarm-header.tpl.html b/ui/src/app/alarm/alarm-header.tpl.html new file mode 100644 index 0000000000..b67ac8f8e1 --- /dev/null +++ b/ui/src/app/alarm/alarm-header.tpl.html @@ -0,0 +1,23 @@ + +
alarm.created-time
+
alarm.originator
+
alarm.type
+
alarm.severity
+
alarm.status
+
alarm.details
diff --git a/ui/src/app/alarm/alarm-row.directive.js b/ui/src/app/alarm/alarm-row.directive.js new file mode 100644 index 0000000000..9cb9bedd68 --- /dev/null +++ b/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 + }; +} diff --git a/ui/src/app/event/event-row-alarm.tpl.html b/ui/src/app/alarm/alarm-row.tpl.html similarity index 59% rename from ui/src/app/event/event-row-alarm.tpl.html rename to ui/src/app/alarm/alarm-row.tpl.html index fa6a2148ab..00c83241f7 100644 --- a/ui/src/app/event/event-row-alarm.tpl.html +++ b/ui/src/app/alarm/alarm-row.tpl.html @@ -15,14 +15,19 @@ limitations under the License. --> -
{{event.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}
-
{{event.body.server}}
-
- {{alarm.createdTime | date : 'yyyy-MM-dd HH:mm:ss'}}
+
{{alarm.originatorName}}
+
{{alarm.type}}
+
+ {{ alarm ? (types.alarmSeverity[alarm.severity].name | translate) : '' }} +
+
{{ alarm ? (('alarm.display-status.' + alarm.status) | translate) : '' }}
+
+ - {{ 'action.view' | translate }} + {{ 'alarm.details' | translate }} diff --git a/ui/src/app/alarm/alarm-table.directive.js b/ui/src/app/alarm/alarm-table.directive.js new file mode 100644 index 0000000000..c93ea03981 --- /dev/null +++ b/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: '=' + } + }; +} diff --git a/ui/src/app/alarm/alarm-table.tpl.html b/ui/src/app/alarm/alarm-table.tpl.html new file mode 100644 index 0000000000..c32e39a2d2 --- /dev/null +++ b/ui/src/app/alarm/alarm-table.tpl.html @@ -0,0 +1,49 @@ + + +
+ + + + + {{ ('alarm.search-status.' + searchStatus) | translate }} + + + + +
+
+ + + + + + alarm.no-alarms-prompt + + + + + + + + +
+
diff --git a/ui/src/app/alarm/alarm.scss b/ui/src/app/alarm/alarm.scss new file mode 100644 index 0000000000..aea5225445 --- /dev/null +++ b/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%; +} diff --git a/ui/src/app/alarm/index.js b/ui/src/app/alarm/index.js new file mode 100644 index 0000000000..0ea4610c3d --- /dev/null +++ b/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; diff --git a/ui/src/app/api/alarm.service.js b/ui/src/app/api/alarm.service.js index d9ab4ff338..34e6b59e20 100644 --- a/ui/src/app/api/alarm.service.js +++ b/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 diff --git a/ui/src/app/api/alias-controller.js b/ui/src/app/api/alias-controller.js new file mode 100644 index 0000000000..60e8a31005 --- /dev/null +++ b/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 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]); + } + } + } + +} \ No newline at end of file diff --git a/ui/src/app/api/entity-relation.service.js b/ui/src/app/api/entity-relation.service.js index 875b2fac38..351c252c56 100644 --- a/ui/src/app/api/entity-relation.service.js +++ b/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; + } + } diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js index 891f9d83b2..20157ce918 100644 --- a/ui/src/app/api/entity.service.js +++ b/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 -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 } } -} \ No newline at end of file +} diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js index fb774a1f0a..1d4eb301c4 100644 --- a/ui/src/app/api/subscription.js +++ b/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]); diff --git a/ui/src/app/asset/assets.tpl.html b/ui/src/app/asset/assets.tpl.html index 11a118f5e4..8370d3dbac 100644 --- a/ui/src/app/asset/assets.tpl.html +++ b/ui/src/app/asset/assets.tpl.html @@ -48,11 +48,16 @@ disable-attribute-scope-selection="true"> + + + + + default-event-type="{{vm.types.eventType.error.value}}"> diff --git a/ui/src/app/asset/index.js b/ui/src/app/asset/index.js index 62ab201b69..2426e85b3c 100644 --- a/ui/src/app/asset/index.js +++ b/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 diff --git a/ui/src/app/common/dashboard-utils.service.js b/ui/src/app/common/dashboard-utils.service.js index 78136dfb2e..915d5501eb 100644 --- a/ui/src/app/common/dashboard-utils.service.js +++ b/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; diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 44e59a260d..b281ad1043 100644 --- a/ui/src/app/common/types.constant.js +++ b/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" diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js index 7b4d65ef78..8d2e565f4a 100644 --- a/ui/src/app/common/utils.service.js +++ b/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'; - } - } - } diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js index f26121ccea..a88e656fe2 100644 --- a/ui/src/app/components/dashboard.directive.js +++ b/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() { diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html index 69934ac215..8d77a6fcca 100644 --- a/ui/src/app/components/dashboard.tpl.html +++ b/ui/src/app/components/dashboard.tpl.html @@ -89,7 +89,7 @@
diff --git a/ui/src/app/components/datasource-func.directive.js b/ui/src/app/components/datasource-func.directive.js index 0f66dcaa29..fe546962c7 100644 --- a/ui/src/app/components/datasource-func.directive.js +++ b/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; } }; diff --git a/ui/src/app/components/datasource-func.scss b/ui/src/app/components/datasource-func.scss index 1a5dcc72ea..8739ed8fee 100644 --- a/ui/src/app/components/datasource-func.scss +++ b/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; + } } -} \ No newline at end of file +} diff --git a/ui/src/app/components/datasource-func.tpl.html b/ui/src/app/components/datasource-func.tpl.html index 1853b9d503..e8e08f7f0c 100644 --- a/ui/src/app/components/datasource-func.tpl.html +++ b/ui/src/app/components/datasource-func.tpl.html @@ -15,59 +15,68 @@ limitations under the License. --> -
- - - {{item}} - -
-
- device.no-keys-found +
+ + + +
+ + + {{item}} + +
+
+ device.no-keys-found +
+
+ device.no-key-matching + + device.create-new-key + +
-
- device.no-key-matching - - device.create-new-key - -
-
- - - -
-
-
-
-
-
- {{$chip.label}} + + + +
+
+
-
:
-
- {{$chip.name}} +
+
+ {{$chip.label}} +
+
:
+
+ {{$chip.name}} +
+ + edit +
- - edit - -
-
- - + + + +
diff --git a/ui/src/app/components/datasource.directive.js b/ui/src/app/components/datasource.directive.js index 2c06a381ee..eb9eafc4a4 100644 --- a/ui/src/app/components/datasource.directive.js +++ b/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: '=', diff --git a/ui/src/app/components/datasource.tpl.html b/ui/src/app/components/datasource.tpl.html index 88ff247fa0..0e91d9832e 100644 --- a/ui/src/app/components/datasource.tpl.html +++ b/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})"> diff --git a/ui/src/app/components/entity-alias-select.directive.js b/ui/src/app/components/entity-alias-select.directive.js index 204b83fe6a..ca2db855f0 100644 --- a/ui/src/app/components/entity-alias-select.directive.js +++ b/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: '&' } diff --git a/ui/src/app/components/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js index c3793be294..d5b423987f 100644 --- a/ui/src/app/components/widget-config.directive.js +++ b/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: '&', diff --git a/ui/src/app/components/widget-config.tpl.html b/ui/src/app/components/widget-config.tpl.html index 808e07b487..0500e91949 100644 --- a/ui/src/app/components/widget-config.tpl.html +++ b/ui/src/app/components/widget-config.tpl.html @@ -60,7 +60,7 @@ style="padding: 0 0 0 10px; margin: 5px;"> diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js index 6fc4bba646..08885b841e 100644 --- a/ui/src/app/components/widget.controller.js +++ b/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 = '
' + + 'Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}' + + '
' + + '
' + + '' + + '
'; + + var containerHtml = '
' + widgetInfo.templateHtml + '
'; + 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 = ''; + containerHtml = '
' + containerHtml + '
'; + html += '
'; + 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 += '
'; } 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 diff --git a/ui/src/app/components/widget.directive.js b/ui/src/app/components/widget.directive.js index c92e5443bc..a0f1b83c48 100644 --- a/ui/src/app/components/widget.directive.js +++ b/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 = '
' + - 'Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}' + - '
' + - '
' + - '' + - '
'; - - scope.displayLegend = angular.isDefined(widget.config.showLegend) ? - widget.config.showLegend : widget.type === types.widgetType.timeseries.value; - - - var containerHtml = '
' + widgetInfo.templateHtml + '
'; - 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 = ''; - containerHtml = '
' + containerHtml + '
'; - html += '
'; - 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 += '
'; - } 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); diff --git a/ui/src/app/customer/customers.tpl.html b/ui/src/app/customer/customers.tpl.html index 6c70a70dad..a6521f066d 100644 --- a/ui/src/app/customer/customers.tpl.html +++ b/ui/src/app/customer/customers.tpl.html @@ -48,11 +48,16 @@ disable-attribute-scope-selection="true"> + + + + + default-event-type="{{vm.types.eventType.error.value}}"> diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js index 2f23cf9f38..5372b6217e 100644 --- a/ui/src/app/dashboard/add-widget.controller.js +++ b/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(); }); diff --git a/ui/src/app/dashboard/add-widget.tpl.html b/ui/src/app/dashboard/add-widget.tpl.html index 870e20346a..761776057b 100644 --- a/ui/src/app/dashboard/add-widget.tpl.html +++ b/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)" diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js index ac14f7953b..65eb833dd4 100644 --- a/ui/src/app/dashboard/dashboard.controller.js +++ b/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() { diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html index 8338612484..202268fa5e 100644 --- a/ui/src/app/dashboard/dashboard.tpl.html +++ b/ui/src/app/dashboard/dashboard.tpl.html @@ -57,8 +57,7 @@ + alias-controller="vm.dashboardCtx.aliasController"> @@ -179,7 +178,7 @@
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js index 7cba4ee6c5..65329149e5 100644 --- a/ui/src/app/dashboard/edit-widget.directive.js +++ b/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: '=' diff --git a/ui/src/app/dashboard/edit-widget.tpl.html b/ui/src/app/dashboard/edit-widget.tpl.html index 279d3113a8..a9ff0b6d4d 100644 --- a/ui/src/app/dashboard/edit-widget.tpl.html +++ b/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)" diff --git a/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html b/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html index ea848585bf..4a4b25ea30 100644 --- a/ui/src/app/dashboard/layouts/dashboard-layout.tpl.html +++ b/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" diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html index 3ff6c1ce39..1e467be124 100644 --- a/ui/src/app/device/devices.tpl.html +++ b/ui/src/app/device/devices.tpl.html @@ -49,11 +49,16 @@ disable-attribute-scope-selection="true"> + + + + + default-event-type="{{vm.types.eventType.error.value}}"> diff --git a/ui/src/app/device/index.js b/ui/src/app/device/index.js index ea42ee8e95..5a8adaf9bb 100644 --- a/ui/src/app/device/index.js +++ b/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 diff --git a/ui/src/app/entity/aliases-entity-select-button.tpl.html b/ui/src/app/entity/alias/aliases-entity-select-button.tpl.html similarity index 100% rename from ui/src/app/entity/aliases-entity-select-button.tpl.html rename to ui/src/app/entity/alias/aliases-entity-select-button.tpl.html diff --git a/ui/src/app/entity/alias/aliases-entity-select-panel.controller.js b/ui/src/app/entity/alias/aliases-entity-select-panel.controller.js new file mode 100644 index 0000000000..5365db74be --- /dev/null +++ b/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(); + } + } + } + +} diff --git a/ui/src/app/entity/aliases-entity-select-panel.tpl.html b/ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html similarity index 70% rename from ui/src/app/entity/aliases-entity-select-panel.tpl.html rename to ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html index d9c5f2ff11..d197d0bd9c 100644 --- a/ui/src/app/entity/aliases-entity-select-panel.tpl.html +++ b/ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html @@ -18,12 +18,12 @@
-
+
- - - - {{entityInfo.name}} + + + + {{resolvedEntity.name}} diff --git a/ui/src/app/entity/aliases-entity-select.directive.js b/ui/src/app/entity/alias/aliases-entity-select.directive.js similarity index 75% rename from ui/src/app/entity/aliases-entity-select.directive.js rename to ui/src/app/entity/alias/aliases-entity-select.directive.js index 7512409f12..9a61311b1e 100644 --- a/ui/src/app/entity/aliases-entity-select.directive.js +++ b/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 + + + +
+

{{ (vm.isAdd ? 'alias.add' : 'alias.edit') | translate }}

+ + + + +
+
+ + + +
+
+
+
+ + + +
+
alias.name-required
+
alias.duplicate-alias
+
+
+
+ + + +
+
+ + + +
+
+
+
+ + + + {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }} + + {{ 'action.cancel' | translate }} + + +
diff --git a/ui/src/app/entity/entity-aliases.controller.js b/ui/src/app/entity/alias/entity-aliases.controller.js similarity index 62% rename from ui/src/app/entity/entity-aliases.controller.js rename to ui/src/app/entity/alias/entity-aliases.controller.js index f1e2e38f6a..b72b9eca6e 100644 --- a/ui/src/app/entity/entity-aliases.controller.js +++ b/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); } } diff --git a/ui/src/app/entity/alias/entity-aliases.scss b/ui/src/app/entity/alias/entity-aliases.scss new file mode 100644 index 0000000000..9803be1cce --- /dev/null +++ b/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; + } + } + } +} diff --git a/ui/src/app/entity/alias/entity-aliases.tpl.html b/ui/src/app/entity/alias/entity-aliases.tpl.html new file mode 100644 index 0000000000..40010a2a97 --- /dev/null +++ b/ui/src/app/entity/alias/entity-aliases.tpl.html @@ -0,0 +1,105 @@ + + +
+ +
+

{{ vm.title | translate }}

+ + + + +
+
+ + +
+ +
+ alias.name + alias.entity-filter + alias.resolve-multiple + +
+
+ + +
+
+
+ {{$index + 1}}. + + + +
+
entity.alias-required
+
+
+ + +
+ + +
+ + + {{ 'alias.edit' | translate }} + + + edit + + + + + {{ 'entity.remove-alias' | translate }} + + + close + + +
+
+
+
+
+ + + + {{ 'alias.add' | translate }} + + alias.add + + + + {{ 'action.save' | translate }} + + {{ 'action.cancel' | translate }} + +
+
\ No newline at end of file diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js index e5822a44a5..bcf37dd825 100644 --- a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js +++ b/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) { diff --git a/ui/src/app/entity/attribute/attribute-table.directive.js b/ui/src/app/entity/attribute/attribute-table.directive.js index da7697d5d1..62d2081853 100644 --- a/ui/src/app/entity/attribute/attribute-table.directive.js +++ b/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; diff --git a/ui/src/app/entity/attribute/attribute-table.tpl.html b/ui/src/app/entity/attribute/attribute-table.tpl.html index 1afd1bb452..2566a08161 100644 --- a/ui/src/app/entity/attribute/attribute-table.tpl.html +++ b/ui/src/app/entity/attribute/attribute-table.tpl.html @@ -156,7 +156,7 @@ rn-swipe-disabled="true">
  • - -
    - -
    -

    {{ vm.isSingleEntityAlias ? ('entity.configure-alias' | translate:vm.singleEntityAlias ) : (vm.title | translate) }}

    - - - - -
    -
    - - - -
    -
    -
    - - - - -
    -
    - -
    - entity.alias - entity.type - entity.entities - -
    -
    -
    -
    - {{$index + 1}}. -
    - - -
    -
    entity.alias-required
    -
    -
    -
    - - -
    -
    - - -
    - - - {{ 'entity.remove-alias' | translate }} - - - close - - -
    -
    -
    -
    - - - {{ 'entity.add-alias' | translate }} - - action.add - -
    -
    -
    -
    - - - - {{ 'action.save' | translate }} - - {{ 'action.cancel' | translate }} - -
    -
    \ No newline at end of file diff --git a/ui/src/app/entity/entity-filter-view.directive.js b/ui/src/app/entity/entity-filter-view.directive.js new file mode 100644 index 0000000000..66c1b667fe --- /dev/null +++ b/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 + }; + +} diff --git a/ui/src/app/entity/entity-filter-view.scss b/ui/src/app/entity/entity-filter-view.scss new file mode 100644 index 0000000000..437f296f6d --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/ui/src/app/event/event-header-alarm.tpl.html b/ui/src/app/entity/entity-filter-view.tpl.html similarity index 66% rename from ui/src/app/event/event-header-alarm.tpl.html rename to ui/src/app/entity/entity-filter-view.tpl.html index be1b0752b3..84c540555c 100644 --- a/ui/src/app/event/event-header-alarm.tpl.html +++ b/ui/src/app/entity/entity-filter-view.tpl.html @@ -15,6 +15,10 @@ limitations under the License. --> -
    event.event-time
    -
    event.server
    -
    event.alarm
    + +
    +
    alias.no-entity-filter-specified
    +
    +
    {{ filterDisplayValue }}
    +
    +
    diff --git a/ui/src/app/entity/entity-filter.directive.js b/ui/src/app/entity/entity-filter.directive.js index b1d92c6cc2..b81f36941d 100644 --- a/ui/src/app/entity/entity-filter.directive.js +++ b/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 -
    -
    -
    - - - - {{item.name}} - - - entity.no-entities-matching - - - - - {{$chip.name}} - - - -
    -
    - - - +
    + + + + + {{type.name | translate}} + + +
    +
    alias.filter-type-required
    +
    +
    +
    + + + + +
    +
    + + + + + +
    +
    entity.entity-name-filter-required
    +
    +
    +
    +
    +
    +
    + + + + + + +
    +
    + + + + + + +
    +
    + +
    + + +
    + + + +
    +
    +
    + + + + + {{ ('relation.search-direction.' + direction) | translate}} + + + + + + + +
    +
    relation.relation-filters
    + + +
    +
    + +
    + + +
    + + + +
    +
    +
    + + + + + {{ ('relation.search-direction.' + direction) | translate}} + + + + + + + +
    +
    relation.relation-type
    + + +
    asset.asset-types
    + + +
    +
    + +
    + + +
    + + + +
    +
    +
    + + + + + {{ ('relation.search-direction.' + direction) | translate}} + + + + + + -
    -
    - - - -
    +
    +
    relation.relation-type
    + + +
    device.device-types
    + +
    - -
    \ No newline at end of file +
  • diff --git a/ui/src/app/entity/entity-list.directive.js b/ui/src/app/entity/entity-list.directive.js new file mode 100644 index 0000000000..0863153046 --- /dev/null +++ b/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 + + + + {{item.name}} + + + entity.no-entities-matching + + + + + {{$chip.name}} + + + + +
    \ No newline at end of file diff --git a/ui/src/app/entity/entity-subtype-autocomplete.directive.js b/ui/src/app/entity/entity-subtype-autocomplete.directive.js index 98110b05a5..76c15d3250 100644 --- a/ui/src/app/entity/entity-subtype-autocomplete.directive.js +++ b/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'; diff --git a/ui/src/app/entity/entity-subtype-list.directive.js b/ui/src/app/entity/entity-subtype-list.directive.js new file mode 100644 index 0000000000..c7d63297e2 --- /dev/null +++ b/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: "=" + } + }; + +} diff --git a/ui/src/app/entity/entity-subtype-list.scss b/ui/src/app/entity/entity-subtype-list.scss new file mode 100644 index 0000000000..bbb2a1cc94 --- /dev/null +++ b/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; + } + } +}*/ diff --git a/ui/src/app/entity/entity-subtype-list.tpl.html b/ui/src/app/entity/entity-subtype-list.tpl.html new file mode 100644 index 0000000000..2a1519aa9a --- /dev/null +++ b/ui/src/app/entity/entity-subtype-list.tpl.html @@ -0,0 +1,54 @@ + + +
    + + + + {{item}} + + + {{noSubtypesMathingText}} + + + + + {{$chip}} + + + + +
    diff --git a/ui/src/app/entity/entity-type-list.directive.js b/ui/src/app/entity/entity-type-list.directive.js new file mode 100644 index 0000000000..cb07cfc877 --- /dev/null +++ b/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: '=?' + } + }; + +} diff --git a/ui/src/app/entity/entity-type-list.scss b/ui/src/app/entity/entity-type-list.scss new file mode 100644 index 0000000000..b94b992f77 --- /dev/null +++ b/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; + } + } +}*/ \ No newline at end of file diff --git a/ui/src/app/entity/entity-type-list.tpl.html b/ui/src/app/entity/entity-type-list.tpl.html new file mode 100644 index 0000000000..ff10a30d9b --- /dev/null +++ b/ui/src/app/entity/entity-type-list.tpl.html @@ -0,0 +1,54 @@ + + +
    + + + + {{item.name}} + + + entity.no-entity-types-matching + + + + + {{$chip.name}} + + + + +
    diff --git a/ui/src/app/entity/entity-type-select.directive.js b/ui/src/app/entity/entity-type-select.directive.js index 1dc674173f..068c1412bf 100644 --- a/ui/src/app/entity/entity-type-select.directive.js +++ b/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 () { diff --git a/ui/src/app/entity/index.js b/ui/src/app/entity/index.js index e8cc43797d..2b8d434a9e 100644 --- a/ui/src/app/entity/index.js +++ b/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; diff --git a/ui/src/app/entity/relation/relation-filters.directive.js b/ui/src/app/entity/relation/relation-filters.directive.js new file mode 100644 index 0000000000..59821dd2ba --- /dev/null +++ b/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); + } +} diff --git a/ui/src/app/entity/relation/relation-filters.scss b/ui/src/app/entity/relation/relation-filters.scss new file mode 100644 index 0000000000..50d49af3a8 --- /dev/null +++ b/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; + } + } +} \ No newline at end of file diff --git a/ui/src/app/entity/relation/relation-filters.tpl.html b/ui/src/app/entity/relation/relation-filters.tpl.html new file mode 100644 index 0000000000..679a4884a3 --- /dev/null +++ b/ui/src/app/entity/relation/relation-filters.tpl.html @@ -0,0 +1,67 @@ + +
    +
    +
    + relation.type + entity.entity-types +   +
    +
    +
    +
    +
    + + + + + + + {{ 'relation.remove-relation-filter' | translate }} + + + close + + +
    +
    +
    +
    + relation.any-relation +
    +
    + + + {{ 'relation.add-relation-filter' | translate }} + + + add + + {{ 'action.add' | translate }} + +
    +
    \ No newline at end of file diff --git a/ui/src/app/entity/relation/relation-table.directive.js b/ui/src/app/entity/relation/relation-table.directive.js index 729ff36bc6..f0c192b9d1 100644 --- a/ui/src/app/entity/relation/relation-table.directive.js +++ b/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; diff --git a/ui/src/app/entity/relation/relation-type-autocomplete.directive.js b/ui/src/app/entity/relation/relation-type-autocomplete.directive.js index a7e5ea45bb..4b5480d61b 100644 --- a/ui/src/app/entity/relation/relation-type-autocomplete.directive.js +++ b/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 = []; diff --git a/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html b/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html index f71c13418a..39f32d6706 100644 --- a/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html +++ b/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html @@ -26,7 +26,7 @@ md-items="item in fetchRelationTypes(relationTypeSearchText)" md-item-text="item" md-min-length="0" - md-floating-label="{{ 'relation.relation-type' | translate }}" + md-floating-label="{{ tbRequired ? ('relation.relation-type' | translate) : ( !relationType ? ('relation.any-relation-type' | translate) : ' ') }}" md-select-on-match="true" md-menu-class="tb-relation-type-autocomplete"> diff --git a/ui/src/app/event/event-content-dialog.controller.js b/ui/src/app/event/event-content-dialog.controller.js index 19ddb8c4d5..235cfcff32 100644 --- a/ui/src/app/event/event-content-dialog.controller.js +++ b/ui/src/app/event/event-content-dialog.controller.js @@ -62,7 +62,7 @@ export default function EventContentDialogController($mdDialog, content, title, } newWidth = 8 * maxLineLength + 16; } - $('#tb-content', element).height(newHeight.toString() + "px") + $('#tb-event-content', element).height(newHeight.toString() + "px") .width(newWidth.toString() + "px"); vm.editor.resize(); } diff --git a/ui/src/app/event/event-content-dialog.tpl.html b/ui/src/app/event/event-content-dialog.tpl.html index 4cf046b080..7b4184c5f4 100644 --- a/ui/src/app/event/event-content-dialog.tpl.html +++ b/ui/src/app/event/event-content-dialog.tpl.html @@ -27,7 +27,7 @@
    -
    diff --git a/ui/src/app/event/event-header.directive.js b/ui/src/app/event/event-header.directive.js index 5e8cc7c6e7..c43894ef85 100644 --- a/ui/src/app/event/event-header.directive.js +++ b/ui/src/app/event/event-header.directive.js @@ -18,7 +18,6 @@ import eventHeaderLcEventTemplate from './event-header-lc-event.tpl.html'; import eventHeaderStatsTemplate from './event-header-stats.tpl.html'; import eventHeaderErrorTemplate from './event-header-error.tpl.html'; -import eventHeaderAlarmTemplate from './event-header-alarm.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ @@ -39,9 +38,6 @@ export default function EventHeaderDirective($compile, $templateCache, types) { case types.eventType.error.value: template = eventHeaderErrorTemplate; break; - case types.eventType.alarm.value: - template = eventHeaderAlarmTemplate; - break; } return $templateCache.get(template); } diff --git a/ui/src/app/event/event-row.directive.js b/ui/src/app/event/event-row.directive.js index 9bff1921a6..d1feb60760 100644 --- a/ui/src/app/event/event-row.directive.js +++ b/ui/src/app/event/event-row.directive.js @@ -20,7 +20,6 @@ import eventErrorDialogTemplate from './event-content-dialog.tpl.html'; import eventRowLcEventTemplate from './event-row-lc-event.tpl.html'; import eventRowStatsTemplate from './event-row-stats.tpl.html'; import eventRowErrorTemplate from './event-row-error.tpl.html'; -import eventRowAlarmTemplate from './event-row-alarm.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ @@ -41,9 +40,6 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $ case types.eventType.error.value: template = eventRowErrorTemplate; break; - case types.eventType.alarm.value: - template = eventRowAlarmTemplate; - break; } return $templateCache.get(template); } diff --git a/ui/src/app/event/event-table.tpl.html b/ui/src/app/event/event-table.tpl.html index f44aeb32eb..82a921fedd 100644 --- a/ui/src/app/event/event-table.tpl.html +++ b/ui/src/app/event/event-table.tpl.html @@ -27,7 +27,7 @@ - + {{widgetsList}}", "duplicate-alias-error": "Duplicate alias found '{{alias}}'.
    Entity aliases must be unique whithin the dashboard.", + "missing-entity-filter-error": "Filter is missing for alias '{{alias}}'.", "configure-alias": "Configure '{{alias}}' alias", "alias": "Alias", "alias-required": "Entity alias is required.", "remove-alias": "Remove entity alias", "add-alias": "Add entity alias", "entity-list": "Entity list", + "entity-type": "Entity type", + "entity-types": "Entity types", + "entity-type-list": "Entity type list", + "any-entity": "Any entity", + "enter-entity-type": "Enter entity type", "no-entities-matching": "No entities matching '{{entity}}' were found.", + "no-entity-types-matching": "No entity types matching '{{entityType}}' were found.", "name-starts-with": "Name starts with", "use-entity-name-filter": "Use filter", "entity-list-empty": "No entities selected.", + "entity-type-list-empty": "No entity types selected.", "entity-name-filter-required": "Entity name filter is required.", "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.", "all-subtypes": "All", + "select-entities": "Select entities", + "no-aliases-found": "No aliases found.", + "no-alias-matching": "'{{alias}}' not found.", + "create-new-alias": "Create a new one!", + "no-keys-found": "No keys found.", + "no-key-matching": "'{{key}}' not found.", + "create-new-key": "Create a new one!", "type": "Type", "type-required": "Entity type is required.", "type-device": "Device", + "type-devices": "Devices", + "list-of-devices": "{ count, select, 1 {One device} other {List of # devices} }", + "device-name-starts-with": "Devices whose names start with '{{prefix}}'", "type-asset": "Asset", + "type-assets": "Assets", + "list-of-assets": "{ count, select, 1 {One asset} other {List of # assets} }", + "asset-name-starts-with": "Assets whose names start with '{{prefix}}'", "type-rule": "Rule", + "type-rules": "Rules", + "list-of-rules": "{ count, select, 1 {One rule} other {List of # rules} }", + "rule-name-starts-with": "Rules whose names start with '{{prefix}}'", "type-plugin": "Plugin", + "type-plugins": "Plugins", + "list-of-plugins": "{ count, select, 1 {One plugin} other {List of # plugins} }", + "plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'", "type-tenant": "Tenant", + "type-tenants": "Tenants", + "list-of-tenants": "{ count, select, 1 {One tenant} other {List of # tenants} }", + "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'", "type-customer": "Customer", + "type-customers": "Customers", + "list-of-customers": "{ count, select, 1 {One customer} other {List of # customers} }", + "customer-name-starts-with": "Customers whose names start with '{{prefix}}'", "type-user": "User", + "type-users": "Users", + "list-of-users": "{ count, select, 1 {One user} other {List of # users} }", + "user-name-starts-with": "Users whose names start with '{{prefix}}'", "type-dashboard": "Dashboard", + "type-dashboards": "Dashboards", + "list-of-dashboards": "{ count, select, 1 {One dashboard} other {List of # dashboards} }", + "dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'", "type-alarm": "Alarm", - "select-entities": "Select entities", - "no-aliases-found": "No aliases found.", - "no-alias-matching": "'{{alias}}' not found.", - "create-new-alias": "Create a new one!", - "no-keys-found": "No keys found.", - "no-key-matching": "'{{key}}' not found.", - "create-new-key": "Create a new one!" + "type-alarms": "Alarms", + "list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }", + "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'" }, "event": { "event-type": "Event type", - "type-alarm": "Alarm", "type-error": "Error", "type-lc-event": "Lifecycle event", "type-stats": "Statistics", @@ -724,6 +840,10 @@ export default angular.module('thingsboard.locale', []) "FROM": "From", "TO": "To" }, + "direction-type": { + "FROM": "from", + "TO": "to" + }, "from-relations": "Outbound relations", "to-relations": "Inbound relations", "selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected", @@ -737,6 +857,7 @@ export default angular.module('thingsboard.locale', []) "delete": "Delete relation", "relation-type": "Relation type", "relation-type-required": "Relation type is required.", + "any-relation-type": "Any type", "add": "Add relation", "delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?", "delete-to-relation-text": "Be careful, after the confirmation the entity '{{entityName}}' will be unrelated from the current entity.", @@ -745,7 +866,11 @@ export default angular.module('thingsboard.locale', []) "delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?", "delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.", "delete-from-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?", - "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities." + "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities.", + "remove-relation-filter": "Remove relation filter", + "add-relation-filter": "Add relation filter", + "any-relation": "Any relation", + "relation-filters": "Relation filters" }, "rule": { "rule": "Rule", diff --git a/ui/src/app/plugin/index.js b/ui/src/app/plugin/index.js index 1c9e731658..6173d625c5 100644 --- a/ui/src/app/plugin/index.js +++ b/ui/src/app/plugin/index.js @@ -16,7 +16,6 @@ import uiRouter from 'angular-ui-router'; import thingsboardGrid from '../components/grid.directive'; import thingsboardJsonForm from '../components/json-form.directive'; -import thingsboardEvent from '../event'; import thingsboardApiPlugin from '../api/plugin.service'; import thingsboardApiComponentDescriptor from '../api/component-descriptor.service'; @@ -28,7 +27,6 @@ export default angular.module('thingsboard.plugin', [ uiRouter, thingsboardGrid, thingsboardJsonForm, - thingsboardEvent, thingsboardApiPlugin, thingsboardApiComponentDescriptor ]) diff --git a/ui/src/app/plugin/plugins.tpl.html b/ui/src/app/plugin/plugins.tpl.html index 5b03506e40..73b0adc39e 100644 --- a/ui/src/app/plugin/plugins.tpl.html +++ b/ui/src/app/plugin/plugins.tpl.html @@ -48,12 +48,16 @@ disable-attribute-scope-selection="true"> + + + + + default-event-type="{{vm.types.eventType.lcEvent.value}}"> diff --git a/ui/src/app/rule/index.js b/ui/src/app/rule/index.js index 64819201cf..2700ec71cf 100644 --- a/ui/src/app/rule/index.js +++ b/ui/src/app/rule/index.js @@ -17,7 +17,6 @@ import uiRouter from 'angular-ui-router'; import thingsboardGrid from '../components/grid.directive'; import thingsboardPluginSelect from '../components/plugin-select.directive'; import thingsboardComponent from '../component'; -import thingsboardEvent from '../event'; import thingsboardApiRule from '../api/rule.service'; import thingsboardApiPlugin from '../api/plugin.service'; import thingsboardApiComponentDescriptor from '../api/component-descriptor.service'; @@ -31,7 +30,6 @@ export default angular.module('thingsboard.rule', [ thingsboardGrid, thingsboardPluginSelect, thingsboardComponent, - thingsboardEvent, thingsboardApiRule, thingsboardApiPlugin, thingsboardApiComponentDescriptor diff --git a/ui/src/app/rule/rules.tpl.html b/ui/src/app/rule/rules.tpl.html index 098bbee7fb..336fe63ce9 100644 --- a/ui/src/app/rule/rules.tpl.html +++ b/ui/src/app/rule/rules.tpl.html @@ -48,12 +48,16 @@ disable-attribute-scope-selection="true"> + + + + + default-event-type="{{vm.types.eventType.lcEvent.value}}"> diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js index afb63bf049..d29ad18cb0 100644 --- a/ui/src/app/services/item-buffer.service.js +++ b/ui/src/app/services/item-buffer.service.js @@ -46,16 +46,14 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) { aliasesInfo { datasourceAliases: { datasourceIndex: { - aliasName: "...", - entityType: "...", - entityFilter: "..." + alias: "...", + filter: "..." } } targetDeviceAliases: { targetDeviceAliasIndex: { - aliasName: "...", - entityType: "...", - entityFilter: "..." + alias: "...", + filter: "..." } } .... @@ -64,9 +62,8 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) { function prepareAliasInfo(entityAlias) { return { - aliasName: entityAlias.alias, - entityType: entityAlias.entityType, - entityFilter: entityAlias.entityFilter + alias: entityAlias.alias, + filter: entityAlias.filter }; } @@ -264,14 +261,9 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) { } dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget, originalColumns, originalSize, row, column); if (callAliasUpdateFunction) { - onAliasesUpdateFunction().then( - function() { - deferred.resolve(theDashboard); - } - ); - } else { - deferred.resolve(theDashboard); + onAliasesUpdateFunction(); } + deferred.resolve(theDashboard); return deferred.promise; } @@ -293,8 +285,7 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) { } function isEntityAliasEqual(alias1, alias2) { - return alias1.entityType === alias2.entityType && - angular.equals(alias1.entityFilter, alias2.entityFilter); + return angular.equals(alias1.filter, alias2.filter); } function getEntityAliasId(entityAliases, aliasInfo) { @@ -306,13 +297,9 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) { } } if (!newAliasId) { - var newAliasName = createEntityAliasName(entityAliases, aliasInfo.aliasName); - newAliasId = 0; - for (aliasId in entityAliases) { - newAliasId = Math.max(newAliasId, aliasId); - } - newAliasId++; - entityAliases[newAliasId] = {alias: newAliasName, entityType: aliasInfo.entityType, entityFilter: aliasInfo.entityFilter}; + var newAliasName = createEntityAliasName(entityAliases, aliasInfo.alias); + newAliasId = utils.guid(); + entityAliases[newAliasId] = {id: newAliasId, alias: newAliasName, filter: aliasInfo.filter}; } return newAliasId; } diff --git a/ui/src/app/tenant/tenants.tpl.html b/ui/src/app/tenant/tenants.tpl.html index 00350a4e02..e407291757 100644 --- a/ui/src/app/tenant/tenants.tpl.html +++ b/ui/src/app/tenant/tenants.tpl.html @@ -46,11 +46,16 @@ disable-attribute-scope-selection="true"> + + + + + default-event-type="{{vm.types.eventType.error.value}}"> diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss index dbe3543efa..ab720c5e5a 100644 --- a/ui/src/scss/main.scss +++ b/ui/src/scss/main.scss @@ -236,6 +236,15 @@ div { } } +.md-caption { + &.tb-required:after { + content: ' *'; + font-size: 10px; + vertical-align: top; + color: rgba(0,0,0,0.54); + } +} + pre.tb-highlight { background-color: #f7f7f7; display: block; @@ -300,6 +309,24 @@ pre.tb-highlight { } } +.tb-severity { + font-weight: bold; + &.tb-critical { + color: red !important; + } + &.tb-major { + color: orange !important; + } + &.tb-minor { + color: #ffca3d !important; + } + &.tb-warning { + color: #abab00 !important; + } + &.tb-indeterminate { + color: green !important; + } +} /*********************** * Flow