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 44263d19bd..74b0075a17 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -29,6 +29,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -498,4 +499,13 @@ public class AlarmController extends BaseController { alarmStatus, assigneeId); } + @ApiOperation(value = "Get Alarm Types (getAlarmTypes)", + notes = "Returns a set of unique alarm types based on alarms that are either owned by the tenant or assigned to the customer which user is performing the request.", produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/types", method = RequestMethod.GET) + @ResponseBody + public List getAlarmTypes() throws ThingsboardException, ExecutionException, InterruptedException { + return checkNotNull(alarmService.findAlarmTypesByTenantId(getTenantId())); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java index 1603d33bfd..394e0598ae 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -25,6 +25,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmComment; @@ -44,20 +45,21 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmTrigger; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; -import org.thingsboard.server.common.data.notification.rule.trigger.AlarmTrigger; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.alarm.AlarmOperationResult; import org.thingsboard.server.dao.alarm.AlarmService; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import java.util.Collection; +import java.util.List; /** * Created by ashvayka on 27.03.18. @@ -235,6 +237,11 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService }); } + @Override + public List findAlarmTypesByTenantId(TenantId tenantId) { + return alarmService.findAlarmTypesByTenantId(tenantId); + } + private void onAlarmUpdated(AlarmApiCallResult result) { wsCallBackExecutor.submit(() -> { AlarmInfo alarm = result.getAlarm(); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index c8dfd1b029..09ecc2e5a3 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -500,6 +500,9 @@ cache: resourceInfo: timeToLiveInMinutes: "${CACHE_SPECS_RESOURCE_INFO_TTL:1440}" maxSize: "${CACHE_SPECS_RESOURCE_INFO_MAX_SIZE:100000}" + alarmTypes: + timeToLiveInMinutes: "${CACHE_SPECS_ALARM_TYPES_TTL:60}" + maxSize: "${CACHE_SPECS_ALARM_TYPES_MAX_SIZE:10000}" # deliberately placed outside 'specs' group above notificationRules: diff --git a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java index 50761be096..d4e5210ab2 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java @@ -31,6 +31,7 @@ import org.springframework.context.annotation.Primary; import org.springframework.test.context.ContextConfiguration; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; @@ -44,8 +45,12 @@ import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -738,4 +743,27 @@ public class AlarmControllerTest extends AbstractControllerTest { doPost("/api/alarm", alarm).andExpect(status().isForbidden()); } + @Test + public void testGetAlarmTypes() throws Exception { + loginTenantAdmin(); + + Mockito.reset(tbClusterService, auditLogService); + + List types = new ArrayList<>(); + + for (int i = 1; i < 13; i++) { + types.add(createAlarm(TEST_ALARM_TYPE + i).getType()); + } + + List foundTypes = doGetTyped("/api/alarm/types", new TypeReference>() {}) + .stream() + .map(EntitySubtype::getType) + .collect(Collectors.toList()); + + Collections.sort(types); + Collections.sort(foundTypes); + + Assert.assertEquals(types, foundTypes); + } + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index fbf8892bd5..de0e451bcb 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.alarm; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; @@ -39,6 +40,7 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.Collection; +import java.util.List; public interface AlarmService extends EntityDaoService { @@ -121,4 +123,6 @@ public interface AlarmService extends EntityDaoService { void deleteEntityAlarmRelations(TenantId tenantId, EntityId entityId); long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); + + List findAlarmTypesByTenantId(TenantId tenantId); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index f21b13a674..e06409d177 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -45,4 +45,5 @@ public class CacheConstants { public static final String DASHBOARD_TITLES_CACHE = "dashboardTitles"; public static final String ENTITY_COUNT_CACHE = "entityCount"; public static final String RESOURCE_INFO_CACHE = "resourceInfo"; + public static final String ALARM_TYPES_CACHE = "alarmTypes"; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 63b13bca66..4b407cb669 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -18,6 +18,9 @@ package org.thingsboard.server.dao; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -142,4 +145,15 @@ public abstract class DaoUtil { return null; } } + + public static List convertTenantEntityTypesToDto(UUID tenantId, EntityType entityType, List types) { + List list = Collections.emptyList(); + if (types != null && !types.isEmpty()) { + list = new ArrayList<>(); + for (String type : types) { + list.add(new EntitySubtype(TenantId.fromUUID(tenantId), entityType, type)); + } + } + return list; + } } 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 745861c6c8..6c6d8cb27a 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.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; @@ -97,4 +98,6 @@ public interface AlarmDao extends Dao { long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); + List findTenantAlarmTypesAsync(UUID tenantId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesCacheEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesCacheEvictEvent.java new file mode 100644 index 0000000000..985d0c8ad1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesCacheEvictEvent.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 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.dao.alarm; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@RequiredArgsConstructor +class AlarmTypesCacheEvictEvent { + private final TenantId tenantId; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesCaffeineCache.java new file mode 100644 index 0000000000..89b0d12b18 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesCaffeineCache.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2023 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.dao.alarm; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.ArrayList; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("AlarmTypesCache") +public class AlarmTypesCaffeineCache extends CaffeineTbTransactionalCache> { + + public AlarmTypesCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.ALARM_TYPES_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesRedisCache.java new file mode 100644 index 0000000000..4b5b0b3d9e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesRedisCache.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2023 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.dao.alarm; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbFSTRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.ArrayList; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("AlarmTypesCache") +public class AlarmTypesRedisCache extends RedisTbTransactionalCache> { + + public AlarmTypesRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.ALARM_TYPES_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>()); + } +} 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 96342aca2f..eef476ae72 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 @@ -22,9 +22,13 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.util.CollectionUtils; +import org.thingsboard.server.cache.TbTransactionalCache; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; @@ -54,7 +58,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; -import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; @@ -64,6 +68,7 @@ import org.thingsboard.server.dao.tenant.TenantService; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; @@ -78,7 +83,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @Service("AlarmDaoService") @Slf4j @RequiredArgsConstructor -public class BaseAlarmService extends AbstractEntityService implements AlarmService { +public class BaseAlarmService extends AbstractCachedEntityService, AlarmTypesCacheEvictEvent> implements AlarmService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; @@ -87,6 +92,17 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ private final EntityService entityService; private final DataValidator alarmDataValidator; + @Autowired + protected TbTransactionalCache> alarmTypesCache; + + @TransactionalEventListener(classes = AlarmTypesCacheEvictEvent.class) + @Override + public void handleEvictEvent(AlarmTypesCacheEvictEvent event) { + TenantId tenantId = event.getTenantId(); + cache.evict(tenantId); + alarmTypesCache.evict(tenantId); + } + @Override public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) { validateAlarmRequest(request); @@ -112,6 +128,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ if (!result.isSuccessful() && !alarmCreationEnabled) { throw new ApiUsageLimitsExceededException("Alarms creation is disabled"); } + publishEvictEvent(new AlarmTypesCacheEvictEvent(request.getTenantId())); return withPropagated(result); } @@ -188,6 +205,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } else { deleteEntityRelations(tenantId, alarm.getId()); alarmDao.removeById(tenantId, alarm.getUuidId()); + publishEvictEvent(new AlarmTypesCacheEvictEvent(tenantId)); return AlarmApiCallResult.builder().alarm(alarm).deleted(true).successful(true).build(); } } @@ -203,12 +221,14 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ AlarmOperationResult result = new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm))); deleteEntityRelations(tenantId, alarm.getId()); alarmDao.removeById(tenantId, alarm.getUuidId()); + publishEvictEvent(new AlarmTypesCacheEvictEvent(tenantId)); return result; } private AlarmOperationResult createAlarm(Alarm alarm) throws InterruptedException, ExecutionException { log.debug("New Alarm : {}", alarm); Alarm saved = alarmDao.save(alarm.getTenantId(), alarm); + publishEvictEvent(new AlarmTypesCacheEvictEvent(alarm.getTenantId())); List propagatedEntitiesList = createEntityAlarmRecords(saved); return new AlarmOperationResult(saved, true, true, propagatedEntitiesList); } @@ -376,6 +396,16 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return alarmDao.countAlarmsByQuery(tenantId, customerId, query); } + @Override + public List findAlarmTypesByTenantId(TenantId tenantId) { + log.trace("Executing findAlarmTypesByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return cache.getAndPutInTransaction(tenantId, () -> + alarmDao.findTenantAlarmTypesAsync(tenantId.getId()).stream() + .sorted(Comparator.comparing(EntitySubtype::getType)) + .collect(Collectors.toCollection(ArrayList::new)), false); + } + private Alarm merge(Alarm existing, Alarm alarm) { if (alarm.getStartTs() > existing.getEndTs()) { existing.setEndTs(alarm.getStartTs()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index 92537dc850..473814a0f5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -345,4 +345,7 @@ public interface AlarmRepository extends JpaRepository { @Query(value = "SELECT unassign_alarm(:t_id, :a_id, :a_ts)", nativeQuery = true) String unassignAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_ts") long unassignTime); + + @Query(value = "SELECT DISTINCT a.type FROM alarm a WHERE a.tenant_id = :tenantId LIMIT 256", nativeQuery = true) + List findTenantAlarmTypes(@Param("tenantId") UUID tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index 8dfdaf8a3e..ec5734df84 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -24,6 +24,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; @@ -67,6 +68,8 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; + /** * Created by Valerii Sosliuk on 5/19/2017. */ @@ -366,6 +369,11 @@ public class JpaAlarmDao extends JpaAbstractDao implements A return alarmQueryRepository.countAlarmsByQuery(tenantId, customerId, query); } + @Override + public List findTenantAlarmTypesAsync(UUID tenantId) { + return convertTenantEntityTypesToDto(tenantId, EntityType.ALARM, alarmRepository.findTenantAlarmTypes(tenantId)); + } + private static String getPropagationTypes(AlarmPropagationInfo ap) { String propagateRelationTypes; if (!CollectionUtils.isEmpty(ap.getPropagateRelationTypes())) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index ec5a8318f8..4c83815d19 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -37,14 +37,13 @@ import org.thingsboard.server.dao.model.sql.AssetInfoEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE; /** @@ -194,7 +193,7 @@ public class JpaAssetDao extends JpaAbstractDao implements A @Override public ListenableFuture> findTenantAssetTypesAsync(UUID tenantId) { - return service.submit(() -> convertTenantAssetTypesToDto(tenantId, assetRepository.findTenantAssetTypes(tenantId))); + return service.submit(() -> convertTenantEntityTypesToDto(tenantId, EntityType.ASSET, assetRepository.findTenantAssetTypes(tenantId))); } @Override @@ -212,17 +211,6 @@ public class JpaAssetDao extends JpaAbstractDao implements A DaoUtil.toPageable(pageLink))); } - private List convertTenantAssetTypesToDto(UUID tenantId, List types) { - List list = Collections.emptyList(); - if (types != null && !types.isEmpty()) { - list = new ArrayList<>(); - for (String type : types) { - list.add(new EntitySubtype(TenantId.fromUUID(tenantId), EntityType.ASSET, type)); - } - } - return list; - } - @Override public PageData findAssetsByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink) { log.debug("Try to find assets by tenantId [{}], edgeId [{}] and pageLink [{}]", tenantId, edgeId, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java index d1ea342bd2..9b63e945ea 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java @@ -43,13 +43,13 @@ import org.thingsboard.server.dao.model.sql.DeviceEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; + /** * Created by Valerii Sosliuk on 5/6/2017. */ @@ -217,7 +217,7 @@ public class JpaDeviceDao extends JpaAbstractDao implement @Override public ListenableFuture> findTenantDeviceTypesAsync(UUID tenantId) { - return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(tenantId))); + return service.submit(() -> convertTenantEntityTypesToDto(tenantId, EntityType.DEVICE, deviceRepository.findTenantDeviceTypes(tenantId))); } @Override @@ -240,17 +240,6 @@ public class JpaDeviceDao extends JpaAbstractDao implement return deviceRepository.countByTenantId(tenantId.getId()); } - private List convertTenantDeviceTypesToDto(UUID tenantId, List types) { - List list = Collections.emptyList(); - if (types != null && !types.isEmpty()) { - list = new ArrayList<>(); - for (String type : types) { - list.add(new EntitySubtype(TenantId.fromUUID(tenantId), EntityType.DEVICE, type)); - } - } - return list; - } - @Override public PageData findDevicesByTenantIdAndEdgeId(UUID tenantId, UUID edgeId, PageLink pageLink) { log.debug("Try to find devices by tenantId [{}], edgeId [{}] and pageLink [{}]", tenantId, edgeId, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java index c40ca2ca6d..29a9719b7d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java @@ -34,13 +34,13 @@ import org.thingsboard.server.dao.model.sql.EdgeInfoEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; + @Component @Slf4j @SqlDao @@ -144,7 +144,7 @@ public class JpaEdgeDao extends JpaAbstractDao implements Edge @Override public ListenableFuture> findTenantEdgeTypesAsync(UUID tenantId) { - return service.submit(() -> convertTenantEdgeTypesToDto(tenantId, edgeRepository.findTenantEdgeTypes(tenantId))); + return service.submit(() -> convertTenantEntityTypesToDto(tenantId, EntityType.EDGE, edgeRepository.findTenantEdgeTypes(tenantId))); } @Override @@ -184,17 +184,6 @@ public class JpaEdgeDao extends JpaAbstractDao implements Edge DaoUtil.toPageable(pageLink))); } - private List convertTenantEdgeTypesToDto(UUID tenantId, List types) { - List list = Collections.emptyList(); - if (types != null && !types.isEmpty()) { - list = new ArrayList<>(); - for (String type : types) { - list.add(new EntitySubtype(TenantId.fromUUID(tenantId), EntityType.EDGE, type)); - } - } - return list; - } - @Override public EntityType getEntityType() { return EntityType.EDGE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java index f3f82d34da..e3e9edf80d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java @@ -35,13 +35,13 @@ import org.thingsboard.server.dao.model.sql.EntityViewInfoEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; + /** * Created by Victor Basanets on 8/31/2017. */ @@ -167,18 +167,7 @@ public class JpaEntityViewDao extends JpaAbstractDao> findTenantEntityViewTypesAsync(UUID tenantId) { - return service.submit(() -> convertTenantEntityViewTypesToDto(tenantId, entityViewRepository.findTenantEntityViewTypes(tenantId))); - } - - private List convertTenantEntityViewTypesToDto(UUID tenantId, List types) { - List list = Collections.emptyList(); - if (types != null && !types.isEmpty()) { - list = new ArrayList<>(); - for (String type : types) { - list.add(new EntitySubtype(TenantId.fromUUID(tenantId), EntityType.ENTITY_VIEW, type)); - } - } - return list; + return service.submit(() -> convertTenantEntityTypesToDto(tenantId, EntityType.ENTITY_VIEW, entityViewRepository.findTenantEntityViewTypes(tenantId))); } @Override diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 98f9091318..8cadda8083 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -77,6 +77,9 @@ cache.specs.entityCount.maxSize=10000 cache.specs.resourceInfo.timeToLiveInMinutes=1440 cache.specs.resourceInfo.maxSize=10000 +cache.specs.alarmTypes.timeToLiveInMinutes=60 +cache.specs.alarmTypes.maxSize=10000 + redis.connection.host=localhost redis.connection.port=6379 redis.connection.db=0 diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 51eb446d70..635d5357b2 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -499,6 +499,15 @@ public class RestClient implements Closeable { return restTemplate.postForEntity(baseURL + "/api/alarm", alarm, Alarm.class).getBody(); } + public List getAlarmTypes() { + return restTemplate.exchange( + baseURL + "/api/alarm/types", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + public AlarmComment saveAlarmComment(AlarmId alarmId, AlarmComment alarmComment) { return restTemplate.postForEntity(baseURL + "/api/alarm/{alarmId}/comment", alarmComment, AlarmComment.class, alarmId.getId()).getBody(); } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java index 05e9bf164f..5aa76726a7 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.api; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; @@ -39,6 +40,7 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.alarm.AlarmOperationResult; import java.util.Collection; +import java.util.List; /** * Created by ashvayka on 02.04.18. @@ -54,6 +56,7 @@ public interface RuleEngineAlarmService { * Only one active alarm may exist for the pair {originatorId, alarmType} */ AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request); + /** * Designed to update existing alarm. Accepts only part of the alarm fields. */ @@ -110,4 +113,6 @@ public interface RuleEngineAlarmService { AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId); PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); + + List findAlarmTypesByTenantId(TenantId tenantId); } diff --git a/ui-ngx/src/app/core/http/alarm.service.ts b/ui-ngx/src/app/core/http/alarm.service.ts index e03e9ab783..022dbb2412 100644 --- a/ui-ngx/src/app/core/http/alarm.service.ts +++ b/ui-ngx/src/app/core/http/alarm.service.ts @@ -29,6 +29,7 @@ import { AlarmStatus } from '@shared/models/alarm.models'; import { UtilsService } from '@core/services/utils.service'; +import { EntitySubtype } from '@shared/models/entity-type.models'; @Injectable({ providedIn: 'root' @@ -108,4 +109,8 @@ export class AlarmService { defaultHttpOptionsFromConfig(config)); } + public getAlarmTypes(config?: RequestConfig): Observable> { + return this.http.get>('/api/alarm/types', defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html index cbbcc2ccb9..9b4426b7c5 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html @@ -55,7 +55,7 @@ -
+
alarm.alarm-status-list
@@ -74,23 +74,9 @@
alarm.alarm-type-list
- - - - {{type}} - cancel - - - - + +
alarm.assignee
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss index 95f78f8fde..311e6b9420 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss @@ -15,24 +15,10 @@ */ :host { display: block; - overflow: scroll; max-width: 100%; .mdc-button { max-width: 100%; } - - .filters-row-mobile { - flex-direction: column; - align-items: start; - border: none; - padding: 0; - } - .filters-title-mobile { - font-size: 14px; - } - .filters-fields-width-mobile { - width: 100%; - } } :host ::ng-deep { @@ -45,9 +31,34 @@ text-overflow: ellipsis; } } - .mat-mdc-chip { - .mdc-evolution-chip__cell, .mat-mdc-chip-action, .mat-mdc-chip-action-label { - overflow: hidden; +} + +::ng-deep { + .tb-alarm-filter-config-component{ + .filters-row-mobile { + flex-direction: column; + align-items: start; + border: none; + padding: 0; + } + .filters-title-mobile { + font-size: 14px; + } + .filters-fields-width-mobile { + width: 100%; + } + .mat-mdc-chip { + .mdc-evolution-chip__cell, .mat-mdc-chip-action, .mat-mdc-chip-action-label { + overflow: hidden; + } + } + tb-entity-subtype-list { + flex: 1 1 0; + display: flex; + + & > * { + flex: 1 1 0; + } } } } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts index 46402923a9..b884b56a4a 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts @@ -40,10 +40,9 @@ import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models'; -import { MatChipInputEvent } from '@angular/material/chips'; -import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { TranslateService } from '@ngx-translate/core'; import { deepClone } from '@core/utils'; +import { EntityType } from '@shared/models/entity-type.models'; export const ALARM_FILTER_CONFIG_DATA = new InjectionToken('AlarmFilterConfigData'); @@ -91,8 +90,6 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal panelMode = false; - readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; - alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACK, @@ -113,6 +110,8 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal panelResult: AlarmFilterConfig = null; + entityType = EntityType; + private alarmFilterConfig: AlarmFilterConfig; private propagateChange = (_: any) => {}; @@ -190,7 +189,7 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal $event.stopPropagation(); } const config = new OverlayConfig({ - panelClass: 'tb-filter-panel', + panelClass: ['tb-filter-panel', 'tb-scroll'], backdropClass: 'cdk-overlay-transparent-backdrop', hasBackdrop: true, maxHeight: '80vh', @@ -256,40 +255,6 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal } } - public alarmTypeList(): string[] { - return this.alarmFilterConfigForm.get('typeList').value; - } - - public removeAlarmType(type: string): void { - const types: string[] = this.alarmFilterConfigForm.get('typeList').value; - const index = types.indexOf(type); - if (index >= 0) { - types.splice(index, 1); - this.alarmFilterConfigForm.get('typeList').setValue(types); - this.alarmFilterConfigForm.get('typeList').markAsDirty(); - } - } - - public addAlarmType(event: MatChipInputEvent): void { - const input = event.chipInput.inputElement; - const value = event.value; - - let types: string[] = this.alarmFilterConfigForm.get('typeList').value; - - if ((value || '').trim()) { - if (!types) { - types = []; - } - types.push(value.trim()); - this.alarmFilterConfigForm.get('typeList').setValue(types); - this.alarmFilterConfigForm.get('typeList').markAsDirty(); - } - - if (input) { - input.value = ''; - } - } - private updateAlarmConfigForm(alarmFilterConfig?: AlarmFilterConfig) { this.alarmFilterConfigForm.patchValue({ statusList: alarmFilterConfig?.statusList, diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html index 3ad2ef2824..42f5d88a22 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html @@ -15,7 +15,8 @@ limitations under the License. --> - + {{ label }} ; @ViewChild('entitySubtypeAutocomplete') entitySubtypeAutocomplete: MatAutocomplete; @ViewChild('chipList', {static: true}) chipList: MatChipGrid; @@ -102,6 +113,7 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, private deviceService: DeviceService, private edgeService: EdgeService, private entityViewService: EntityViewService, + private alarmService: AlarmService, private fb: UntypedFormBuilder) { this.entitySubtypeListFormGroup = this.fb.group({ entitySubtypeList: [this.entitySubtypeList, this.required ? [Validators.required] : []], @@ -164,6 +176,16 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, this.entitySubtypes = null; }); break; + case EntityType.ALARM: + this.placeholder = this.required ? this.translate.instant('alarm.enter-alarm-type') + : this.translate.instant('alarm.any-type'); + this.secondaryPlaceholder = '+' + this.translate.instant('alarm.alarm-type'); + this.noSubtypesMathingText = 'alarm.no-alarm-types-matching'; + this.subtypeListEmptyText = 'alarm.alarm-type-list-empty'; + this.broadcastSubscription = this.broadcast.on('alarmSaved', () => { + this.entitySubtypes = null; + }); + break; } this.filteredEntitySubtypeList = this.entitySubtypeListFormGroup.get('entitySubtype').valueChanges @@ -283,6 +305,9 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, case EntityType.ENTITY_VIEW: subTypesObservable = this.entityViewService.getEntityViewTypes({ignoreLoading: true}); break; + case EntityType.ALARM: + subTypesObservable = this.alarmService.getAlarmTypes({ignoreLoading: true}); + break; } if (subTypesObservable) { this.entitySubtypes = subTypesObservable.pipe( diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index daafb3c206..ebe45e08d3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -537,7 +537,11 @@ "search-propagated-alarms": "Search propagated alarms", "comments": "Alarm comments", "show-more": "Show more", - "additional-info": "Additional info" + "additional-info": "Additional info", + "alarm-type": "Alarm type", + "enter-alarm-type": "Enter alarm type", + "no-alarm-types-matching": "No alarm types matching '{{entitySubtype}}' were found.", + "alarm-type-list-empty": "No alarm types selected." }, "alarm-activity": { "add": "Add a comment...", diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 75fc0845cf..cd3fb18d6f 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -276,6 +276,10 @@ pre.tb-highlight { 0 7px 8px -4px rgba(0, 0, 0, .2), 0 13px 19px 2px rgba(0, 0, 0, .14), 0 5px 24px 4px rgba(0, 0, 0, .12); + + &.tb-scroll { + overflow: scroll; + } } .tb-panel-actions {