diff --git a/application/src/main/data/upgrade/3.5.1/schema_update.sql b/application/src/main/data/upgrade/3.5.1/schema_update.sql index ab8e32e320..c8efd4390e 100644 --- a/application/src/main/data/upgrade/3.5.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.5.1/schema_update.sql @@ -127,6 +127,15 @@ UPDATE resource ALTER TABLE notification_request ALTER COLUMN info SET DATA TYPE varchar(1000000); +CREATE TABLE IF NOT EXISTS alarm_types ( + tenant_id uuid NOT NULL, + type varchar(255) NOT NULL, + CONSTRAINT tenant_id_type_unq_key UNIQUE (tenant_id, type), + CONSTRAINT fk_entity_tenant_id FOREIGN KEY (tenant_id) REFERENCES tenant(id) ON DELETE CASCADE +); + +INSERT INTO alarm_types (tenant_id, type) SELECT DISTINCT tenant_id, type FROM alarm ON CONFLICT (tenant_id, type) DO NOTHING; + ALTER TABLE widget_type ADD COLUMN IF NOT EXISTS fqn varchar(512); ALTER TABLE widget_type 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..4e6e1d5824 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; @@ -44,6 +45,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; @@ -498,4 +500,21 @@ 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 PageData getAlarmTypes(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException, ExecutionException, InterruptedException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, "type", sortOrder); + return checkNotNull(alarmService.findAlarmTypesByTenantId(getTenantId(), pageLink)); + } + } 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 b2b9d992e6..005ebd268a 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; @@ -46,6 +47,7 @@ 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.page.PageLink; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; @@ -235,6 +237,11 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService }); } + @Override + public PageData findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink) { + return alarmService.findAlarmTypesByTenantId(tenantId, pageLink); + } + private void onAlarmUpdated(AlarmApiCallResult result) { wsCallBackExecutor.submit(() -> { AlarmInfo alarm = result.getAlarm(); diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java index 5154829ea6..2f84767eff 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java @@ -39,7 +39,9 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.action.EntityActionService; import java.util.Date; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; @TbCoreComponent @@ -86,20 +88,25 @@ public class AlarmsCleanUpService { PageLink removalBatchRequest = new PageLink(removalBatchSize, 0); long totalRemoved = 0; + Set typesToRemove = new HashSet<>(); while (true) { PageData toRemove = alarmDao.findAlarmsIdsByEndTsBeforeAndTenantId(expirationTime, tenantId, removalBatchRequest); for (AlarmId alarmId : toRemove.getData()) { relationService.deleteEntityRelations(tenantId, alarmId); - Alarm alarm = alarmService.delAlarm(tenantId, alarmId).getAlarm(); + Alarm alarm = alarmService.delAlarm(tenantId, alarmId, false).getAlarm(); if (alarm != null) { entityActionService.pushEntityActionToRuleEngine(alarm.getOriginator(), alarm, tenantId, null, ActionType.ALARM_DELETE, null); totalRemoved++; + typesToRemove.add(alarm.getType()); } } if (!toRemove.hasNext()) { break; } } + + alarmService.delAlarmTypes(tenantId, typesToRemove); + if (totalRemoved > 0) { log.info("Removed {} outdated alarm(s) for tenant {} older than {}", totalRemoved, tenantId, new Date(expirationTime)); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 93464478f5..bdfacbe884 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -507,6 +507,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 778a97d690..0e6eb16713 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java @@ -32,6 +32,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; @@ -45,8 +46,11 @@ 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.LinkedList; import java.util.List; +import java.util.stream.Collectors; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -853,4 +857,110 @@ public class AlarmControllerTest extends AbstractControllerTest { doPost("/api/alarm", alarm).andExpect(status().isForbidden()); } + @Test + public void testSaveAlarmTypes() throws Exception { + loginTenantAdmin(); + + List types = new ArrayList<>(); + + for (int i = 1; i < 13; i++) { + types.add(createAlarm(TEST_ALARM_TYPE + i).getType()); + } + + Device device = new Device(); + device.setName("Test device 2"); + device.setCustomerId(customerId); + customerDevice = doPost("/api/device", device, Device.class); + + for (int i = 1; i < 10; i++) { + createAlarm(TEST_ALARM_TYPE + i); + } + + List foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference>() { + }) + .getData() + .stream() + .map(EntitySubtype::getType) + .collect(Collectors.toList()); + + Collections.sort(types); + Collections.sort(foundTypes); + + Assert.assertEquals(types, foundTypes); + } + + @Test + public void testDeleteAlarmTypes() throws Exception { + loginTenantAdmin(); + + List alarms = new ArrayList<>(); + + for (int i = 1; i < 13; i++) { + alarms.add(createAlarm(TEST_ALARM_TYPE + i)); + } + + Device device = new Device(); + device.setName("Test device 2"); + device.setCustomerId(customerId); + customerDevice = doPost("/api/device", device, Device.class); + + for (int i = 1; i < 14; i++) { + alarms.add(createAlarm(TEST_ALARM_TYPE + i)); + } + + List expectedTypes = alarms.stream().map(AlarmInfo::getType).distinct().sorted().collect(Collectors.toList()); + + List foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference>() { + }) + .getData() + .stream() + .map(EntitySubtype::getType) + .sorted() + .collect(Collectors.toList()); + + Assert.assertEquals(13, foundTypes.size()); + Assert.assertEquals(expectedTypes, foundTypes); + + for (int i = 0; i < 12; i++) { + doDelete("/api/alarm/" + alarms.get(i).getId()).andExpect(status().isOk()); + } + + foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference>() { + }) + .getData() + .stream() + .map(EntitySubtype::getType) + .sorted() + .collect(Collectors.toList()); + + Assert.assertEquals(13, foundTypes.size()); + Assert.assertEquals(expectedTypes, foundTypes); + + doDelete("/api/alarm/" + alarms.get(12).getId()).andExpect(status().isOk()); + + foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference>() { + }) + .getData() + .stream() + .map(EntitySubtype::getType) + .sorted() + .collect(Collectors.toList()); + + Assert.assertEquals(12, foundTypes.size()); + + for (int i = 13; i < alarms.size(); i++) { + doDelete("/api/alarm/" + alarms.get(i).getId()).andExpect(status().isOk()); + } + + foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference>() { + }) + .getData() + .stream() + .map(EntitySubtype::getType) + .sorted() + .collect(Collectors.toList()); + + Assert.assertTrue(foundTypes.isEmpty()); + } + } diff --git a/application/src/test/java/org/thingsboard/server/service/ttl/AlarmsCleanUpServiceTest.java b/application/src/test/java/org/thingsboard/server/service/ttl/AlarmsCleanUpServiceTest.java index 09e78df9ff..b2c06b419b 100644 --- a/application/src/test/java/org/thingsboard/server/service/ttl/AlarmsCleanUpServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/ttl/AlarmsCleanUpServiceTest.java @@ -104,10 +104,10 @@ public class AlarmsCleanUpServiceTest extends AbstractControllerTest { alarmsCleanUpService.cleanUp(); for (AlarmId outdatedAlarm : outdatedAlarms) { - verify(alarmService).delAlarm(eq(tenantId), eq(outdatedAlarm)); + verify(alarmService).delAlarm(eq(tenantId), eq(outdatedAlarm), eq(false)); } for (AlarmId freshAlarm : freshAlarms) { - verify(alarmService, never()).delAlarm(eq(tenantId), eq(freshAlarm)); + verify(alarmService, never()).delAlarm(eq(tenantId), eq(freshAlarm), eq(false)); } verify(cleanUpServiceLogger).info(startsWith("Removed {} outdated alarm"), eq((long) count), eq(tenantId), any()); 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 558a65e8f4..5c8b88e4b8 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; @@ -40,6 +41,7 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.Collection; +import java.util.Set; public interface AlarmService extends EntityDaoService { @@ -75,6 +77,10 @@ public interface AlarmService extends EntityDaoService { AlarmApiCallResult delAlarm(TenantId tenantId, AlarmId alarmId); + AlarmApiCallResult delAlarm(TenantId tenantId, AlarmId alarmId, boolean checkAndDeleteAlarmType); + + void delAlarmTypes(TenantId tenantId, Set types); + /* * Legacy API, before 3.5. */ @@ -124,4 +130,6 @@ public interface AlarmService extends EntityDaoService { void deleteEntityAlarmRelations(TenantId tenantId, EntityId entityId); long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); + + PageData findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink); } 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/common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java index 2ae94a0064..6eb9218956 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java @@ -20,13 +20,16 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import java.io.Serializable; import java.util.Collections; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; @ApiModel -public class PageData { +public class PageData implements Serializable { + + public static final PageData EMPTY_PAGE_DATA = new PageData<>(); private final List data; private final int totalPages; @@ -48,6 +51,11 @@ public class PageData { this.hasNext = hasNext; } + @SuppressWarnings("unchecked") + public static PageData emptyPageData() { + return (PageData) EMPTY_PAGE_DATA; + } + @ApiModelProperty(position = 1, value = "Array of the entities", accessMode = ApiModelProperty.AccessMode.READ_ONLY) public List getData() { return data; 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 9688f7b7e1..76f5a1948b 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,10 @@ 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.springframework.util.CollectionUtils; +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; @@ -147,4 +151,16 @@ public abstract class DaoUtil { return null; } } + + public static List convertTenantEntityTypesToDto(UUID tenantId, EntityType entityType, List types) { + if (CollectionUtils.isEmpty(types)) { + return Collections.emptyList(); + } + + List list = new ArrayList<>(types.size()); + 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 93ebc61581..2b6594a831 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; @@ -99,4 +100,7 @@ public interface AlarmDao extends Dao { long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); + PageData findTenantAlarmTypes(UUID tenantId, PageLink pageLink); + + boolean removeAlarmTypesIfNoAlarmsPresent(UUID tenantId, Set types); } 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..4ad08338c4 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesCaffeineCache.java @@ -0,0 +1,35 @@ +/** + * 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 org.thingsboard.server.common.data.page.PageData; + +@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..18eb81f690 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmTypesRedisCache.java @@ -0,0 +1,37 @@ +/** + * 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 org.thingsboard.server.common.data.page.PageData; + +@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 b26daba312..33f6ac0265 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 @@ -24,7 +24,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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.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; @@ -49,6 +51,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -56,7 +59,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.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; @@ -83,15 +86,24 @@ 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 "; + private static final PageLink DEFAULT_ALARM_TYPES_PAGE_LINK = new PageLink(25, 0, null, new SortOrder("type")); + private final TenantService tenantService; private final AlarmDao alarmDao; private final EntityService entityService; private final DataValidator alarmDataValidator; + @TransactionalEventListener(classes = AlarmTypesCacheEvictEvent.class) + @Override + public void handleEvictEvent(AlarmTypesCacheEvictEvent event) { + TenantId tenantId = event.getTenantId(); + cache.evict(tenantId); + } + @Override public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) { validateAlarmRequest(request); @@ -125,6 +137,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ if (result.getAlarm() != null) { eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(result.getAlarm().getTenantId()) .entityId(result.getAlarm().getId()).added(true).build()); + publishEvictEvent(new AlarmTypesCacheEvictEvent(request.getTenantId())); } return withPropagated(result); } @@ -205,6 +218,12 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ @Override @Transactional public AlarmApiCallResult delAlarm(TenantId tenantId, AlarmId alarmId) { + return delAlarm(tenantId, alarmId, true); + } + + @Override + @Transactional + public AlarmApiCallResult delAlarm(TenantId tenantId, AlarmId alarmId, boolean checkAndDeleteAlarmType) { log.debug("Deleting Alarm Id: {}", alarmId); AlarmInfo alarm = alarmDao.findAlarmInfoById(tenantId, alarmId.getId()); if (alarm == null) { @@ -214,10 +233,21 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ alarmDao.removeById(tenantId, alarm.getUuidId()); eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId) .entityId(alarmId).entity(alarm).build()); + if (checkAndDeleteAlarmType) { + delAlarmTypes(tenantId, Collections.singleton(alarm.getType())); + } return AlarmApiCallResult.builder().alarm(alarm).deleted(true).successful(true).build(); } } + @Override + @Transactional + public void delAlarmTypes(TenantId tenantId, Set types) { + if (!types.isEmpty() && alarmDao.removeAlarmTypesIfNoAlarmsPresent(tenantId.getId(), types)) { + publishEvictEvent(new AlarmTypesCacheEvictEvent(tenantId)); + } + } + @Override @Transactional public AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId) { @@ -419,6 +449,17 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return alarmDao.countAlarmsByQuery(tenantId, customerId, query); } + @Override + public PageData findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findAlarmTypesByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + if (DEFAULT_ALARM_TYPES_PAGE_LINK.equals(pageLink)) { + return cache.getAndPutInTransaction(tenantId, () -> + alarmDao.findTenantAlarmTypes(tenantId.getId(), pageLink), false); + } + return alarmDao.findTenantAlarmTypes(tenantId.getId(), pageLink); + } + 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 7db3ea24f8..30b6c5191d 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 @@ -18,8 +18,10 @@ package org.thingsboard.server.dao.sql.alarm; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.sql.AlarmEntity; @@ -349,4 +351,13 @@ 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 at.type FROM alarm_types AS at WHERE at.tenant_id = :tenantId AND at.type ILIKE CONCAT('%', :searchText, '%')", nativeQuery = true) + Page findTenantAlarmTypes(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); + + @Transactional + @Modifying + @Query(value = "DELETE FROM alarm_types AS at WHERE NOT EXISTS (SELECT 1 FROM alarm AS a WHERE a.tenant_id = at.tenant_id AND a.type = at.type) AND at.tenant_id = :tenantId AND at.type IN (:types)", nativeQuery = true) + int deleteTypeIfNoAlarmsExist(@Param("tenantId") UUID tenantId, @Param("types") Set types); + } 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 b6a1d0c92e..358c4aa019 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 @@ -19,11 +19,13 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; 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 +69,9 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; +import static org.thingsboard.server.dao.DaoUtil.toPageable; + /** * Created by Valerii Sosliuk on 5/19/2017. */ @@ -372,6 +377,22 @@ public class JpaAlarmDao extends JpaAbstractDao implements A return alarmQueryRepository.countAlarmsByQuery(tenantId, customerId, query); } + @Override + public PageData findTenantAlarmTypes(UUID tenantId, PageLink pageLink) { + Page page = alarmRepository.findTenantAlarmTypes(tenantId, Objects.toString(pageLink.getTextSearch(), ""), toPageable(pageLink)); + if (page.isEmpty()) { + return PageData.emptyPageData(); + } + + List data = convertTenantEntityTypesToDto(tenantId, EntityType.ALARM, page.getContent()); + return new PageData<>(data, page.getTotalPages(), page.getTotalElements(), page.hasNext()); + } + + @Override + public boolean removeAlarmTypesIfNoAlarmsPresent(UUID tenantId, Set types) { + return alarmRepository.deleteTypeIfNoAlarmsExist(tenantId, types) > 0; + } + 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 e3f9c0eb8e..c61040c000 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 @@ -193,17 +193,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/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 5133049819..883e9da4a4 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -25,7 +25,7 @@ CREATE OR REPLACE PROCEDURE insert_tb_schema_settings() $$ BEGIN IF (SELECT COUNT(*) FROM tb_schema_settings) = 0 THEN - INSERT INTO tb_schema_settings (schema_version) VALUES (3003000); + INSERT INTO tb_schema_settings (schema_version) VALUES (3005002); END IF; END; $$; @@ -867,3 +867,10 @@ CREATE TABLE IF NOT EXISTS user_settings ( CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE, CONSTRAINT user_settings_pkey PRIMARY KEY (user_id, type) ); + +CREATE TABLE IF NOT EXISTS alarm_types ( + tenant_id uuid NOT NULL, + type varchar(255) NOT NULL, + CONSTRAINT tenant_id_type_unq_key UNIQUE (tenant_id, type), + CONSTRAINT fk_entity_tenant_id FOREIGN KEY (tenant_id) REFERENCES tenant(id) ON DELETE CASCADE +); diff --git a/dao/src/main/resources/sql/schema-views-and-functions.sql b/dao/src/main/resources/sql/schema-views-and-functions.sql index 3cd20ac0c4..3aa797d7b6 100644 --- a/dao/src/main/resources/sql/schema-views-and-functions.sql +++ b/dao/src/main/resources/sql/schema-views-and-functions.sql @@ -114,6 +114,7 @@ BEGIN a_details, a_propagate, a_propagate_to_owner, a_propagate_to_tenant, a_propagation_types, false, 0, false, 0, NULL, 0); + INSERT INTO alarm_types (tenant_id, type) VALUES (t_id, a_type) ON CONFLICT (tenant_id, type) DO NOTHING; SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; RETURN json_build_object('success', true, 'created', true, 'modified', true, 'alarm', row_to_json(result))::text; ELSE diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java index 25231f822f..8f0122aa4d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java @@ -20,6 +20,8 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; @@ -29,8 +31,12 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.AbstractJpaDaoTest; import org.thingsboard.server.dao.alarm.AlarmDao; +import org.thingsboard.server.dao.tenant.TenantDao; +import org.thingsboard.server.dao.tenant.TenantProfileDao; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -54,6 +60,11 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { @Autowired private AlarmDao alarmDao; + @Autowired + protected TenantProfileDao tenantProfileDao; + + @Autowired + protected TenantDao tenantDao; @Test public void testFindLatestByOriginatorAndType() throws ExecutionException, InterruptedException, TimeoutException { @@ -88,7 +99,8 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { @Test public void createOrUpdateActiveAlarm() { - TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + Tenant tenant = createTenant(); + TenantId tenantId = tenant.getId(); DeviceId deviceId = new DeviceId(UUID.randomUUID()); AlarmCreateOrUpdateActiveRequest request = AlarmCreateOrUpdateActiveRequest.builder() @@ -156,6 +168,9 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { assertTrue(result.isModified()); assertNotNull(result.getAlarm()); assertNotEquals(newAlarmId, result.getAlarm().getUuidId()); + + tenantDao.removeById(TenantId.SYS_TENANT_ID, tenant.getUuidId()); + tenantProfileDao.removeById(TenantId.SYS_TENANT_ID, tenant.getTenantProfileId().getId()); } @Test @@ -329,4 +344,23 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { return alarmDao.save(TenantId.fromUUID(tenantId), alarm); } + private Tenant createTenant() { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("My tenant profile " + UUID.randomUUID()); + TenantProfileData profileData = new TenantProfileData(); + profileData.setConfiguration(new DefaultTenantProfileConfiguration()); + tenantProfile.setProfileData(profileData); + var savedTenantProfile = tenantProfileDao.save(TenantId.SYS_TENANT_ID, tenantProfile); + assertNotNull(savedTenantProfile); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant " + UUID.randomUUID()); + tenant.setTenantProfileId(savedTenantProfile.getId()); + Tenant savedTenant = tenantDao.save(TenantId.SYS_TENANT_ID, tenant); + + assertNotNull(savedTenant); + + return savedTenant; + } + } 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/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index 75b892452a..9c772df45b 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -19,6 +19,7 @@ DROP TABLE IF EXISTS admin_settings; DROP TABLE IF EXISTS entity_alarm; DROP TABLE IF EXISTS alarm_comment; DROP TABLE IF EXISTS alarm; +DROP TABLE IF EXISTS alarm_type; DROP TABLE IF EXISTS asset; DROP TABLE IF EXISTS audit_log; DROP TABLE IF EXISTS attribute_kv; 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 8c8829a727..fb878a33b6 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(PageLink pageLink) { + return restTemplate.exchange( + baseURL + "/api/alarm/types?" + getUrlParams(pageLink), + 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..ebaff2bfaf 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; @@ -34,6 +35,7 @@ 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.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.alarm.AlarmOperationResult; @@ -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); + + PageData findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink); } diff --git a/ui-ngx/src/app/core/http/alarm.service.ts b/ui-ngx/src/app/core/http/alarm.service.ts index f0ce187341..dc68a092b3 100644 --- a/ui-ngx/src/app/core/http/alarm.service.ts +++ b/ui-ngx/src/app/core/http/alarm.service.ts @@ -28,7 +28,8 @@ import { AlarmSeverity, AlarmStatus } from '@shared/models/alarm.models'; -import { UtilsService } from '@core/services/utils.service'; +import { EntitySubtype } from '@shared/models/entity-type.models'; +import { PageLink } from '@shared/models/page/page-link'; @Injectable({ providedIn: 'root' @@ -36,8 +37,7 @@ import { UtilsService } from '@core/services/utils.service'; export class AlarmService { constructor( - private http: HttpClient, - private utils: UtilsService + private http: HttpClient ) { } public getAlarm(alarmId: string, config?: RequestConfig): Observable { @@ -108,4 +108,8 @@ export class AlarmService { defaultHttpOptionsFromConfig(config)); } + public getAlarmTypes(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/types${pageLink.toQuery()}`, 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 3ccfc6ae9c..3a1be706ed 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 @@ -73,23 +73,13 @@
-
alarm.alarm-type-list
- - - - {{type}} - cancel - - - - +
alarm.alarm-type-list
+ +
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 edac6ddef1..9b0c323a40 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 @@ -37,6 +37,11 @@ .tb-alarm-filter-config-component { flex: 1; + tb-entity-subtype-list { + flex: 1; + width: 180px; + } + .mat-mdc-chip { .mdc-evolution-chip__cell, .mat-mdc-chip-action, .mat-mdc-chip-action-label { overflow: hidden; 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 0a26440951..d0877a8eb0 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'; import { fromEvent, Subscription } from 'rxjs'; import { POSITION_MAP } from '@shared/models/overlay.models'; @@ -93,8 +92,6 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal panelMode = false; - readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; - alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACK, @@ -115,6 +112,8 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal panelResult: AlarmFilterConfig = null; + entityType = EntityType; + private alarmFilterConfig: AlarmFilterConfig; private resizeWindows: Subscription; @@ -259,40 +258,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 278a4c89e5..44c61d605a 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 @@ -16,7 +16,8 @@ --> + [appearance]="appearance" [subscriptSizing]="subscriptSizing" + [class.tb-chip-list]="!label" [class]="additionalClasses" class="mat-block"> {{ label }} ; + @ViewChild('entitySubtypeInput') entitySubtypeInput: ElementRef; @ViewChild('entitySubtypeAutocomplete') entitySubtypeAutocomplete: MatAutocomplete; @ViewChild('chipList', {static: true}) chipList: MatChipGrid; entitySubtypeList: Array = []; filteredEntitySubtypeList: Observable>; - entitySubtypes: Observable>; + private entitySubtypes: Observable>; private broadcastSubscription: Subscription; @@ -107,13 +118,17 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, private propagateChange = (v: any) => { }; - constructor(private store: Store, - private broadcast: BroadcastService, + private hasPageDataEntitySubTypes = new Set([ + EntityType.ALARM + ]); + + constructor(private broadcast: BroadcastService, public translate: TranslateService, private assetService: AssetService, private deviceService: DeviceService, private edgeService: EdgeService, private entityViewService: EntityViewService, + private alarmService: AlarmService, private fb: FormBuilder) { this.entitySubtypeListFormGroup = this.fb.group({ entitySubtypeList: [this.entitySubtypeList, this.required ? [Validators.required] : []], @@ -176,6 +191,13 @@ 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'; + break; } if (this.emptyInputPlaceholder) { @@ -185,12 +207,12 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, this.secondaryPlaceholder = this.filledInputPlaceholder; } - this.filteredEntitySubtypeList = this.entitySubtypeListFormGroup.get('entitySubtype').valueChanges - .pipe( - map(value => value ? value : ''), - mergeMap(name => this.fetchEntitySubtypes(name) ), - share() - ); + this.filteredEntitySubtypeList = this.entitySubtypeListFormGroup.get('entitySubtype').valueChanges.pipe( + debounceTime(150), + map(value => value ? value : ''), + mergeMap(name => this.fetchEntitySubtypes(name)), + share() + ); } ngAfterViewInit(): void { @@ -266,11 +288,16 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, return entitySubtype ? entitySubtype : undefined; } - fetchEntitySubtypes(searchText?: string): Observable> { + private fetchEntitySubtypes(searchText?: string): Observable> { this.searchText = searchText; - return this.getEntitySubtypes().pipe( + return this.getEntitySubtypes(searchText).pipe( map(subTypes => { - let result = subTypes.filter( subType => searchText ? subType.toUpperCase().startsWith(searchText.toUpperCase()) : true); + let result; + if (this.hasPageDataEntitySubTypes.has(this.entityType)) { + result = subTypes; + } else { + result = subTypes.filter(subType => searchText ? subType.toUpperCase().startsWith(searchText.toUpperCase()) : true); + } if (!result.length) { result = [searchText]; } @@ -279,7 +306,23 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, ); } - getEntitySubtypes(): Observable> { + private getEntitySubtypes(searchText?: string): Observable> { + if (this.hasPageDataEntitySubTypes.has(this.entityType)) { + const pageLink = new PageLink(25, 0, searchText); + let subTypesPagesObservable: Observable>; + switch (this.entityType) { + case EntityType.ALARM: + subTypesPagesObservable = this.alarmService.getAlarmTypes(pageLink, {ignoreLoading: true}); + break; + } + if (subTypesPagesObservable) { + this.entitySubtypes = subTypesPagesObservable.pipe( + map(subTypesPage => subTypesPage.data.map(subType => subType.type)), + ); + } else { + return throwError(null); + } + } if (!this.entitySubtypes) { let subTypesObservable: Observable>; switch (this.entityType) { @@ -299,8 +342,12 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, if (subTypesObservable) { this.entitySubtypes = subTypesObservable.pipe( map(subTypes => subTypes.map(subType => subType.type)), - publishReplay(1), - refCount() + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: true, + }), ); } else { return throwError(null); 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 a51f40b8ff..16d80b7897 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -550,7 +550,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...",